Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions integrations/openclaw/plugin.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,29 @@ function latestUserText(messages) {
return "";
}

function asNonEmptyString(value) {
return typeof value === "string" && value.trim() ? value.trim() : "";
}

function deriveProject(event) {
const candidates = [
event?.project,
event?.session?.project,
event?.workspace?.project,
event?.agent?.project,
event?.metadata?.project,
event?.context?.project,
event?.cwd,
event?.session?.cwd,
event?.workspace?.cwd,
];
for (const value of candidates) {
const text = asNonEmptyString(value);
if (text) return text;
}
return "";
}

function formatResults(results) {
if (!Array.isArray(results) || results.length === 0) return "";
return results
Expand Down Expand Up @@ -179,9 +202,11 @@ const plugin = {
if (!cfg.enabled) return;
const prompt = typeof event?.prompt === "string" ? event.prompt.trim() : "";
if (!prompt) return;
const project = deriveProject(event);
const result = await client.postJson("/agentmemory/smart-search", {
query: prompt,
limit: 5,
...(project ? { project } : {}),
});
const block = formatResults(result?.results || []);
if (!block) return;
Expand All @@ -200,9 +225,12 @@ const plugin = {
event.sessionKey ||
event.runId ||
`openclaw-${Date.now()}`;
const project = deriveProject(event);
await client.postJson("/agentmemory/observe", {
hookType: "post_tool_use",
sessionId,
project: project || sessionId,
cwd: project || sessionId,
timestamp: new Date().toISOString(),
data: {
tool_name: "conversation",
Expand Down
103 changes: 103 additions & 0 deletions test/openclaw-project-scope.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import openclawPlugin from "../integrations/openclaw/plugin.mjs";

type OpenClawHandler = (event: Record<string, unknown>) => Promise<unknown>;

function mockFetch(calls: Array<{ url: string; body: any }>) {
const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
const body = init?.body ? JSON.parse(String(init.body)) : undefined;
calls.push({ url: String(input), body });
return new Response(JSON.stringify({ results: [] }), {
status: 200,
headers: { "content-type": "application/json" },
});
});
(globalThis as { fetch: typeof fetch }).fetch = fetchMock as unknown as typeof fetch;
return fetchMock;
}

function registerPlugin() {
const handlers = new Map<string, OpenClawHandler>();
openclawPlugin.register({
pluginConfig: { base_url: "http://localhost:3111" },
logger: { warn: vi.fn() },
on(event: string, handler: OpenClawHandler) {
handlers.set(event, handler);
},
});
return handlers;
}

describe("OpenClaw plugin project scoping", () => {
const originalFetch = globalThis.fetch;
const originalAgentMemorySecret = process.env["AGENTMEMORY_SECRET"];

beforeEach(() => {
delete process.env["AGENTMEMORY_SECRET"];
});

afterEach(() => {
globalThis.fetch = originalFetch;
if (originalAgentMemorySecret === undefined) {
delete process.env["AGENTMEMORY_SECRET"];
} else {
process.env["AGENTMEMORY_SECRET"] = originalAgentMemorySecret;
}
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

it("passes event project into smart-search when available", async () => {
const calls: Array<{ url: string; body: any }> = [];
mockFetch(calls);
const handlers = registerPlugin();

await handlers.get("before_agent_start")?.({
prompt: "recall auth changes",
project: "team-alpha",
});

expect(calls).toHaveLength(1);
expect(calls[0].url).toContain("/agentmemory/smart-search");
expect(calls[0].body).toMatchObject({
query: "recall auth changes",
limit: 5,
project: "team-alpha",
});
});

it("passes project and cwd into observe payload, falling back to session id when missing", async () => {
const calls: Array<{ url: string; body: any }> = [];
mockFetch(calls);
const handlers = registerPlugin();

await handlers.get("agent_end")?.({
success: true,
sessionKey: "sess-123",
messages: [
{ role: "user", content: "hello" },
{ role: "assistant", content: "world" },
],
});

expect(calls).toHaveLength(1);
expect(calls[0].url).toContain("/agentmemory/observe");
expect(calls[0].body).toMatchObject({
hookType: "post_tool_use",
sessionId: "sess-123",
project: "sess-123",
cwd: "sess-123",
});
});

it("uses nested workspace project metadata when top-level project is absent", async () => {
const calls: Array<{ url: string; body: any }> = [];
mockFetch(calls);
const handlers = registerPlugin();

await handlers.get("before_agent_start")?.({
prompt: "open memory",
workspace: { project: "nested-proj" },
});

expect(calls[0].body).toMatchObject({ project: "nested-proj" });
});
});