From b6f0b1165e178bb16f2e5a550b0a7d4fdc8c5d4e Mon Sep 17 00:00:00 2001 From: Rohit Ghumare Date: Sun, 16 Aug 2026 12:38:14 +0100 Subject: [PATCH 1/9] feat: cursor marketplace plugin with native hooks and mcp config --- .cursor-plugin/plugin.json | 34 +++++++++++++ plugin/cursor/hooks.json | 41 ++++++++++++++++ plugin/cursor/mcp.json | 12 +++++ test/cursor-plugin.test.ts | 98 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 185 insertions(+) create mode 100644 .cursor-plugin/plugin.json create mode 100644 plugin/cursor/hooks.json create mode 100644 plugin/cursor/mcp.json create mode 100644 test/cursor-plugin.test.ts diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json new file mode 100644 index 000000000..7fe3ea1fe --- /dev/null +++ b/.cursor-plugin/plugin.json @@ -0,0 +1,34 @@ +{ + "name": "agentmemory", + "version": "0.9.29", + "description": "Persistent memory for Cursor: auto-captures sessions, prompts, and tool use via hooks, recalls with hybrid BM25 + vector + graph search, and exposes 54 memory tools over MCP. Keyless by default, runs entirely on your machine.", + "author": { + "name": "Rohit Ghumare", + "email": "ghumare64@gmail.com" + }, + "homepage": "https://agent-memory.dev", + "repository": "https://github.com/rohitg00/agentmemory", + "license": "Apache-2.0", + "keywords": ["memory", "mcp", "hooks", "recall", "knowledge-graph", "agent-memory"], + "logo": "assets/logo.svg", + "skills": "plugin/skills/", + "hooks": "plugin/cursor/hooks.json", + "mcpServers": "plugin/cursor/mcp.json", + "variables": { + "type": "object", + "properties": { + "AGENTMEMORY_URL": { + "type": "string", + "title": "agentmemory server URL", + "description": "REST base URL of the running agentmemory server", + "default": "http://localhost:3111" + }, + "AGENTMEMORY_SECRET": { + "type": "string", + "title": "agentmemory secret", + "description": "Bearer token, required only when the server sets AGENTMEMORY_SECRET", + "default": "" + } + } + } +} diff --git a/plugin/cursor/hooks.json b/plugin/cursor/hooks.json new file mode 100644 index 000000000..01ee1a076 --- /dev/null +++ b/plugin/cursor/hooks.json @@ -0,0 +1,41 @@ +{ + "version": 1, + "hooks": { + "sessionStart": [ + { + "command": "node plugin/scripts/session-start.mjs" + } + ], + "beforeSubmitPrompt": [ + { + "command": "node plugin/scripts/prompt-submit.mjs" + } + ], + "preToolUse": [ + { + "command": "node plugin/scripts/pre-tool-use.mjs", + "matcher": "Shell|Read|Write|Grep" + } + ], + "postToolUse": [ + { + "command": "node plugin/scripts/post-tool-use.mjs" + } + ], + "postToolUseFailure": [ + { + "command": "node plugin/scripts/post-tool-failure.mjs" + } + ], + "stop": [ + { + "command": "node plugin/scripts/stop.mjs" + } + ], + "sessionEnd": [ + { + "command": "node plugin/scripts/session-end.mjs" + } + ] + } +} diff --git a/plugin/cursor/mcp.json b/plugin/cursor/mcp.json new file mode 100644 index 000000000..f7c376033 --- /dev/null +++ b/plugin/cursor/mcp.json @@ -0,0 +1,12 @@ +{ + "mcpServers": { + "agentmemory": { + "command": "npx", + "args": ["-y", "@agentmemory/mcp"], + "env": { + "AGENTMEMORY_URL": "${AGENTMEMORY_URL}", + "AGENTMEMORY_SECRET": "${AGENTMEMORY_SECRET}" + } + } + } +} diff --git a/test/cursor-plugin.test.ts b/test/cursor-plugin.test.ts new file mode 100644 index 000000000..b98c29959 --- /dev/null +++ b/test/cursor-plugin.test.ts @@ -0,0 +1,98 @@ +import { describe, it, expect } from "vitest"; +import { readFileSync, existsSync } from "node:fs"; +import { join } from "node:path"; + +const manifest = JSON.parse(readFileSync(".cursor-plugin/plugin.json", "utf-8")); +const hooks = JSON.parse(readFileSync("plugin/cursor/hooks.json", "utf-8")); +const mcp = JSON.parse(readFileSync("plugin/cursor/mcp.json", "utf-8")); + +const CURSOR_HOOK_EVENTS = new Set([ + "sessionStart", + "sessionEnd", + "preToolUse", + "postToolUse", + "postToolUseFailure", + "subagentStart", + "subagentStop", + "beforeShellExecution", + "afterShellExecution", + "beforeMCPExecution", + "afterMCPExecution", + "beforeReadFile", + "afterFileEdit", + "beforeSubmitPrompt", + "preCompact", + "stop", + "afterAgentResponse", + "afterAgentThought", + "beforeTabFileRead", + "afterTabFileEdit", + "workspaceOpen", +]); + +describe("Cursor plugin manifest", () => { + it("has a kebab-case name and version matching the package", () => { + expect(manifest.name).toMatch(/^[a-z0-9][a-z0-9.-]*[a-z0-9]$/); + const pkg = JSON.parse(readFileSync("package.json", "utf-8")); + expect(manifest.version).toBe(pkg.version); + }); + + it("references only paths that exist in the repo", () => { + for (const p of [ + manifest.skills, + manifest.hooks, + manifest.mcpServers, + manifest.logo, + ]) { + expect(typeof p).toBe("string"); + expect(p.includes("..")).toBe(false); + expect(p.startsWith("/")).toBe(false); + expect(existsSync(p)).toBe(true); + } + }); + + it("skills path points at the shipped skill set", () => { + expect(existsSync(join(manifest.skills, "recall", "SKILL.md"))).toBe(true); + expect(existsSync(join(manifest.skills, "memory-discipline", "SKILL.md"))).toBe( + true, + ); + }); +}); + +describe("Cursor plugin hooks config", () => { + it("uses the native format with known event names", () => { + expect(hooks.version).toBe(1); + for (const event of Object.keys(hooks.hooks)) { + expect(CURSOR_HOOK_EVENTS.has(event)).toBe(true); + } + }); + + it("every hook command points at an existing built script", () => { + for (const entries of Object.values(hooks.hooks) as Array< + Array<{ command: string }> + >) { + for (const entry of entries) { + const script = entry.command.match(/plugin\/scripts\/\S+\.mjs/)?.[0]; + expect(script).toBeDefined(); + expect(existsSync(script!)).toBe(true); + } + } + }); +}); + +describe("Cursor plugin MCP config", () => { + it("declares every ${VAR} placeholder in the manifest variables schema", () => { + const raw = readFileSync("plugin/cursor/mcp.json", "utf-8"); + const vars = [...raw.matchAll(/\$\{([A-Z0-9_]+)\}/g)].map((m) => m[1]); + expect(vars.length).toBeGreaterThan(0); + for (const v of vars) { + expect(manifest.variables.properties[v]).toBeDefined(); + } + }); + + it("wires the standalone MCP shim over stdio", () => { + const server = mcp.mcpServers.agentmemory; + expect(server.command).toBe("npx"); + expect(server.args).toContain("@agentmemory/mcp"); + }); +}); From 633e63b7018c1d95944233f089b081bc634c8911 Mon Sep 17 00:00:00 2001 From: Rohit Ghumare Date: Sun, 16 Aug 2026 12:38:14 +0100 Subject: [PATCH 2/9] fix: cursor payload compat and transcript prompt backfill in hooks --- plugin/scripts/notification.mjs | 13 ++- plugin/scripts/post-tool-failure.mjs | 13 ++- plugin/scripts/post-tool-use.mjs | 13 ++- plugin/scripts/pre-compact.mjs | 10 ++- plugin/scripts/pre-tool-use.mjs | 2 +- plugin/scripts/prompt-submit.mjs | 13 ++- plugin/scripts/session-end.mjs | 78 ++++++++++++++++- plugin/scripts/session-start.mjs | 15 +++- plugin/scripts/stop.mjs | 2 +- plugin/scripts/subagent-start.mjs | 13 ++- plugin/scripts/subagent-stop.mjs | 13 ++- plugin/scripts/task-completed.mjs | 11 ++- src/hooks/_project.ts | 10 +++ src/hooks/notification.ts | 10 ++- src/hooks/post-tool-failure.ts | 10 ++- src/hooks/post-tool-use.ts | 9 +- src/hooks/pre-compact.ts | 6 +- src/hooks/pre-tool-use.ts | 2 +- src/hooks/prompt-submit.ts | 10 ++- src/hooks/session-end.ts | 58 ++++++++++++- src/hooks/session-start.ts | 16 ++-- src/hooks/stop.ts | 2 +- src/hooks/subagent-start.ts | 10 ++- src/hooks/subagent-stop.ts | 10 ++- src/hooks/task-completed.ts | 8 +- test/session-end-transcript.test.ts | 124 +++++++++++++++++++++++++++ 26 files changed, 414 insertions(+), 67 deletions(-) create mode 100644 test/session-end-transcript.test.ts diff --git a/plugin/scripts/notification.mjs b/plugin/scripts/notification.mjs index 0a814e892..603ee0762 100755 --- a/plugin/scripts/notification.mjs +++ b/plugin/scripts/notification.mjs @@ -20,6 +20,12 @@ function resolveProject(cwd) { } catch {} return basename(dir); } +function hookCwd(data) { + if (!data || typeof data !== "object") return void 0; + if (typeof data.cwd === "string" && data.cwd.trim()) return data.cwd; + const roots = data.workspace_roots; + if (Array.isArray(roots) && typeof roots[0] === "string" && roots[0].trim()) return roots[0]; +} //#endregion //#region src/hooks/notification.ts function isSdkChildContext(payload) { @@ -47,16 +53,17 @@ async function main() { if (isSdkChildContext(data)) return; const notificationType = data.notification_type ?? data.notificationType; if (notificationType !== "permission_prompt") return; - const rawSessionId = data.session_id ?? data.sessionId; + const rawSessionId = data.session_id ?? data.sessionId ?? data.conversation_id; const sessionId = typeof rawSessionId === "string" && rawSessionId.length > 0 ? rawSessionId : "unknown"; + const cwd = hookCwd(data) || process.cwd(); fetch(`${REST_URL}/agentmemory/observe`, { method: "POST", headers: authHeaders(), body: JSON.stringify({ hookType: "notification", sessionId, - project: resolveProject(data.cwd), - cwd: data.cwd || process.cwd(), + project: resolveProject(cwd), + cwd, timestamp: (/* @__PURE__ */ new Date()).toISOString(), data: { notification_type: notificationType, diff --git a/plugin/scripts/post-tool-failure.mjs b/plugin/scripts/post-tool-failure.mjs index 18782a561..6434d020e 100755 --- a/plugin/scripts/post-tool-failure.mjs +++ b/plugin/scripts/post-tool-failure.mjs @@ -20,6 +20,12 @@ function resolveProject(cwd) { } catch {} return basename(dir); } +function hookCwd(data) { + if (!data || typeof data !== "object") return void 0; + if (typeof data.cwd === "string" && data.cwd.trim()) return data.cwd; + const roots = data.workspace_roots; + if (Array.isArray(roots) && typeof roots[0] === "string" && roots[0].trim()) return roots[0]; +} //#endregion //#region src/hooks/post-tool-failure.ts function isSdkChildContext(payload) { @@ -46,18 +52,19 @@ async function main() { if (!data || typeof data !== "object") return; if (isSdkChildContext(data)) return; if (data.is_interrupt || data.isInterrupt) return; - const sessionId = data.session_id || data.sessionId || "unknown"; + const sessionId = data.session_id || data.sessionId || data.conversation_id || "unknown"; const toolName = data.tool_name ?? data.toolName; const toolInput = data.tool_input ?? data.toolArgs; const error = data.error ?? data.errorMessage; + const cwd = hookCwd(data) || process.cwd(); fetch(`${REST_URL}/agentmemory/observe`, { method: "POST", headers: authHeaders(), body: JSON.stringify({ hookType: "post_tool_failure", sessionId, - project: resolveProject(data.cwd), - cwd: data.cwd || process.cwd(), + project: resolveProject(cwd), + cwd, timestamp: (/* @__PURE__ */ new Date()).toISOString(), data: { tool_name: toolName, diff --git a/plugin/scripts/post-tool-use.mjs b/plugin/scripts/post-tool-use.mjs index 53853ee14..3ce2d5c2d 100755 --- a/plugin/scripts/post-tool-use.mjs +++ b/plugin/scripts/post-tool-use.mjs @@ -20,6 +20,12 @@ function resolveProject(cwd) { } catch {} return basename(dir); } +function hookCwd(data) { + if (!data || typeof data !== "object") return void 0; + if (typeof data.cwd === "string" && data.cwd.trim()) return data.cwd; + const roots = data.workspace_roots; + if (Array.isArray(roots) && typeof roots[0] === "string" && roots[0].trim()) return roots[0]; +} //#endregion //#region src/hooks/post-tool-use.ts function isSdkChildContext(payload) { @@ -45,18 +51,19 @@ async function main() { } if (!data || typeof data !== "object") return; if (isSdkChildContext(data)) return; - const sessionId = data.session_id || data.sessionId || "unknown"; + const sessionId = data.session_id || data.sessionId || data.conversation_id || "unknown"; const toolName = data.tool_name ?? data.toolName; const toolInput = data.tool_input ?? data.toolArgs; const { imageData, cleanOutput } = extractImageData(toolOutput(data)); + const cwd = hookCwd(data) || process.cwd(); fetch(`${REST_URL}/agentmemory/observe`, { method: "POST", headers: authHeaders(), body: JSON.stringify({ hookType: "post_tool_use", sessionId, - project: resolveProject(data.cwd), - cwd: data.cwd || process.cwd(), + project: resolveProject(cwd), + cwd, timestamp: (/* @__PURE__ */ new Date()).toISOString(), data: { tool_name: toolName, diff --git a/plugin/scripts/pre-compact.mjs b/plugin/scripts/pre-compact.mjs index c4b8cea1a..53f67bc19 100755 --- a/plugin/scripts/pre-compact.mjs +++ b/plugin/scripts/pre-compact.mjs @@ -20,6 +20,12 @@ function resolveProject(cwd) { } catch {} return basename(dir); } +function hookCwd(data) { + if (!data || typeof data !== "object") return void 0; + if (typeof data.cwd === "string" && data.cwd.trim()) return data.cwd; + const roots = data.workspace_roots; + if (Array.isArray(roots) && typeof roots[0] === "string" && roots[0].trim()) return roots[0]; +} //#endregion //#region src/hooks/pre-compact.ts function isSdkChildContext(payload) { @@ -45,8 +51,8 @@ async function main() { } if (!data || typeof data !== "object") return; if (isSdkChildContext(data)) return; - const sessionId = data.session_id || data.sessionId || "unknown"; - const project = resolveProject(data.cwd); + const sessionId = data.session_id || data.sessionId || data.conversation_id || "unknown"; + const project = resolveProject(hookCwd(data)); if (process.env["CLAUDE_MEMORY_BRIDGE"] === "true") try { await fetch(`${REST_URL}/agentmemory/claude-bridge/sync`, { method: "POST", diff --git a/plugin/scripts/pre-tool-use.mjs b/plugin/scripts/pre-tool-use.mjs index ad1094a11..97c65dc90 100755 --- a/plugin/scripts/pre-tool-use.mjs +++ b/plugin/scripts/pre-tool-use.mjs @@ -56,7 +56,7 @@ async function main() { const pattern = toolInput["pattern"]; if (typeof pattern === "string" && pattern.length > 0) terms.push(pattern); } - const rawSessionId = data.session_id || data.sessionId; + const rawSessionId = data.session_id || data.sessionId || data.conversation_id; const sessionId = typeof rawSessionId === "string" && rawSessionId.length > 0 ? rawSessionId : "unknown"; const project = typeof data.project === "string" && data.project.trim().length > 0 ? data.project.trim() : void 0; try { diff --git a/plugin/scripts/prompt-submit.mjs b/plugin/scripts/prompt-submit.mjs index 3bb271ae0..e1f9cb17a 100755 --- a/plugin/scripts/prompt-submit.mjs +++ b/plugin/scripts/prompt-submit.mjs @@ -20,6 +20,12 @@ function resolveProject(cwd) { } catch {} return basename(dir); } +function hookCwd(data) { + if (!data || typeof data !== "object") return void 0; + if (typeof data.cwd === "string" && data.cwd.trim()) return data.cwd; + const roots = data.workspace_roots; + if (Array.isArray(roots) && typeof roots[0] === "string" && roots[0].trim()) return roots[0]; +} //#endregion //#region src/hooks/prompt-submit.ts function isSdkChildContext(payload) { @@ -45,15 +51,16 @@ async function main() { } if (!data || typeof data !== "object") return; if (isSdkChildContext(data)) return; - const sessionId = data.session_id || data.sessionId || "unknown"; + const sessionId = data.session_id || data.sessionId || data.conversation_id || "unknown"; + const cwd = hookCwd(data) || process.cwd(); fetch(`${REST_URL}/agentmemory/observe`, { method: "POST", headers: authHeaders(), body: JSON.stringify({ hookType: "prompt_submit", sessionId, - project: resolveProject(data.cwd), - cwd: data.cwd || process.cwd(), + project: resolveProject(cwd), + cwd, timestamp: (/* @__PURE__ */ new Date()).toISOString(), data: { prompt: data.prompt ?? data.userPrompt } }), diff --git a/plugin/scripts/session-end.mjs b/plugin/scripts/session-end.mjs index f19d80add..aff4cb7f6 100755 --- a/plugin/scripts/session-end.mjs +++ b/plugin/scripts/session-end.mjs @@ -1,4 +1,33 @@ #!/usr/bin/env node +import { readFileSync } from "node:fs"; +import { execSync } from "node:child_process"; +import { basename } from "node:path"; +//#region src/hooks/_project.ts +function resolveProject(cwd) { + const explicit = process.env["AGENTMEMORY_PROJECT_NAME"]; + if (explicit && explicit.trim()) return explicit.trim(); + const dir = cwd && cwd.trim() ? cwd : process.cwd(); + try { + const top = execSync("git rev-parse --show-toplevel", { + cwd: dir, + stdio: [ + "ignore", + "pipe", + "ignore" + ], + timeout: 500 + }).toString().trim(); + if (top) return basename(top); + } catch {} + return basename(dir); +} +function hookCwd(data) { + if (!data || typeof data !== "object") return void 0; + if (typeof data.cwd === "string" && data.cwd.trim()) return data.cwd; + const roots = data.workspace_roots; + if (Array.isArray(roots) && typeof roots[0] === "string" && roots[0].trim()) return roots[0]; +} +//#endregion //#region src/hooks/session-end.ts function isSdkChildContext(payload) { if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true; @@ -12,6 +41,35 @@ function authHeaders() { if (SECRET) h["Authorization"] = `Bearer ${SECRET}`; return h; } +function extractTranscriptPrompts(data) { + const path = data.transcript_path; + if (typeof path !== "string" || !path.endsWith(".jsonl")) return []; + let raw; + try { + raw = readFileSync(path, "utf-8"); + } catch { + return []; + } + const prompts = []; + for (const line of raw.split("\n")) { + if (!line.trim()) continue; + let msg; + try { + msg = JSON.parse(line); + } catch { + continue; + } + if (msg.role !== "user") continue; + for (const block of msg.message?.content ?? []) { + if (block.type !== "text" || typeof block.text !== "string") continue; + const m = block.text.match(/\n?([\s\S]*?)\n?<\/user_query>/); + const text = (m ? m[1] : block.text).trim(); + if (text) prompts.push(text.slice(0, 8e3)); + } + if (prompts.length >= 50) break; + } + return prompts; +} async function main() { let input = ""; for await (const chunk of process.stdin) input += chunk; @@ -23,7 +81,25 @@ async function main() { } if (!data || typeof data !== "object") return; if (isSdkChildContext(data)) return; - const sessionId = data.session_id || data.sessionId || "unknown"; + const sessionId = data.session_id || data.sessionId || data.conversation_id || "unknown"; + const transcriptPrompts = extractTranscriptPrompts(data); + if (transcriptPrompts.length > 0) { + const cwd = hookCwd(data) || process.cwd(); + const project = resolveProject(cwd); + for (const prompt of transcriptPrompts) fetch(`${REST_URL}/agentmemory/observe`, { + method: "POST", + headers: authHeaders(), + body: JSON.stringify({ + hookType: "prompt_submit", + sessionId, + project, + cwd, + timestamp: (/* @__PURE__ */ new Date()).toISOString(), + data: { prompt } + }), + signal: AbortSignal.timeout(3e3) + }).catch(() => {}); + } fetch(`${REST_URL}/agentmemory/session/end`, { method: "POST", headers: authHeaders(), diff --git a/plugin/scripts/session-start.mjs b/plugin/scripts/session-start.mjs index 8c1bb6477..61c7b0eda 100755 --- a/plugin/scripts/session-start.mjs +++ b/plugin/scripts/session-start.mjs @@ -20,6 +20,12 @@ function resolveProject(cwd) { } catch {} return basename(dir); } +function hookCwd(data) { + if (!data || typeof data !== "object") return void 0; + if (typeof data.cwd === "string" && data.cwd.trim()) return data.cwd; + const roots = data.workspace_roots; + if (Array.isArray(roots) && typeof roots[0] === "string" && roots[0].trim()) return roots[0]; +} //#endregion //#region src/hooks/session-start.ts function isSdkChildContext(payload) { @@ -48,9 +54,9 @@ async function main() { } if (!data || typeof data !== "object") return; if (isSdkChildContext(data)) return; - const sessionId = data.session_id || data.sessionId || `ses_${Date.now().toString(36)}`; - const cwd = data.cwd || process.cwd(); - const project = resolveProject(data.cwd); + const sessionId = data.session_id || data.sessionId || data.conversation_id || `ses_${Date.now().toString(36)}`; + const cwd = hookCwd(data) || process.cwd(); + const project = resolveProject(cwd); const url = `${REST_URL}/agentmemory/session/start`; const init = { method: "POST", @@ -75,7 +81,8 @@ async function main() { }); if (res.ok) { const result = await res.json(); - if (result.context) process.stdout.write(result.context); + if (result.context) if (typeof data.cursor_version === "string" || data.hook_event_name === "sessionStart") process.stdout.write(JSON.stringify({ additional_context: result.context })); + else process.stdout.write(result.context); } } catch {} } diff --git a/plugin/scripts/stop.mjs b/plugin/scripts/stop.mjs index 41fda645d..bf9d4e083 100755 --- a/plugin/scripts/stop.mjs +++ b/plugin/scripts/stop.mjs @@ -23,7 +23,7 @@ async function main() { } if (!data || typeof data !== "object") return; if (isSdkChildContext(data)) return; - const sessionId = data.session_id || data.sessionId || "unknown"; + const sessionId = data.session_id || data.sessionId || data.conversation_id || "unknown"; fetch(`${REST_URL}/agentmemory/session/end`, { method: "POST", headers: authHeaders(), diff --git a/plugin/scripts/subagent-start.mjs b/plugin/scripts/subagent-start.mjs index a089ae3d7..bc5e5989f 100755 --- a/plugin/scripts/subagent-start.mjs +++ b/plugin/scripts/subagent-start.mjs @@ -20,6 +20,12 @@ function resolveProject(cwd) { } catch {} return basename(dir); } +function hookCwd(data) { + if (!data || typeof data !== "object") return void 0; + if (typeof data.cwd === "string" && data.cwd.trim()) return data.cwd; + const roots = data.workspace_roots; + if (Array.isArray(roots) && typeof roots[0] === "string" && roots[0].trim()) return roots[0]; +} //#endregion //#region src/hooks/subagent-start.ts function isSdkChildContext(payload) { @@ -46,17 +52,18 @@ async function main() { } if (!data || typeof data !== "object") return; if (isSdkChildContext(data)) return; - const sessionId = data.session_id || data.sessionId || "unknown"; + const sessionId = data.session_id || data.sessionId || data.conversation_id || "unknown"; const agentId = data.agent_id || data.agentName; const agentType = data.agent_type || data.agentDisplayName || data.agentName; + const cwd = hookCwd(data) || process.cwd(); fetch(`${REST_URL}/agentmemory/observe`, { method: "POST", headers: authHeaders(), body: JSON.stringify({ hookType: "subagent_start", sessionId, - project: resolveProject(data.cwd), - cwd: data.cwd || process.cwd(), + project: resolveProject(cwd), + cwd, timestamp: (/* @__PURE__ */ new Date()).toISOString(), data: { agent_id: agentId, diff --git a/plugin/scripts/subagent-stop.mjs b/plugin/scripts/subagent-stop.mjs index 4e00f9d7b..88f12f236 100755 --- a/plugin/scripts/subagent-stop.mjs +++ b/plugin/scripts/subagent-stop.mjs @@ -20,6 +20,12 @@ function resolveProject(cwd) { } catch {} return basename(dir); } +function hookCwd(data) { + if (!data || typeof data !== "object") return void 0; + if (typeof data.cwd === "string" && data.cwd.trim()) return data.cwd; + const roots = data.workspace_roots; + if (Array.isArray(roots) && typeof roots[0] === "string" && roots[0].trim()) return roots[0]; +} //#endregion //#region src/hooks/subagent-stop.ts function isSdkChildContext(payload) { @@ -45,18 +51,19 @@ async function main() { } if (!data || typeof data !== "object") return; if (isSdkChildContext(data)) return; - const sessionId = data.session_id || data.sessionId || "unknown"; + const sessionId = data.session_id || data.sessionId || data.conversation_id || "unknown"; const agentId = data.agent_id || data.agentName; const agentType = data.agent_type || data.agentDisplayName || data.agentName; const lastMsg = typeof data.last_assistant_message === "string" ? data.last_assistant_message.slice(0, 4e3) : ""; + const cwd = hookCwd(data) || process.cwd(); fetch(`${REST_URL}/agentmemory/observe`, { method: "POST", headers: authHeaders(), body: JSON.stringify({ hookType: "subagent_stop", sessionId, - project: resolveProject(data.cwd), - cwd: data.cwd || process.cwd(), + project: resolveProject(cwd), + cwd, timestamp: (/* @__PURE__ */ new Date()).toISOString(), data: { agent_id: agentId, diff --git a/plugin/scripts/task-completed.mjs b/plugin/scripts/task-completed.mjs index e8a5bfcca..f12713e44 100755 --- a/plugin/scripts/task-completed.mjs +++ b/plugin/scripts/task-completed.mjs @@ -20,6 +20,12 @@ function resolveProject(cwd) { } catch {} return basename(dir); } +function hookCwd(data) { + if (!data || typeof data !== "object") return void 0; + if (typeof data.cwd === "string" && data.cwd.trim()) return data.cwd; + const roots = data.workspace_roots; + if (Array.isArray(roots) && typeof roots[0] === "string" && roots[0].trim()) return roots[0]; +} //#endregion //#region src/hooks/task-completed.ts function isSdkChildContext(payload) { @@ -46,14 +52,15 @@ async function main() { if (!data || typeof data !== "object") return; if (isSdkChildContext(data)) return; const sessionId = data.session_id || "unknown"; + const cwd = hookCwd(data) || process.cwd(); fetch(`${REST_URL}/agentmemory/observe`, { method: "POST", headers: authHeaders(), body: JSON.stringify({ hookType: "task_completed", sessionId, - project: resolveProject(data.cwd), - cwd: data.cwd || process.cwd(), + project: resolveProject(cwd), + cwd, timestamp: (/* @__PURE__ */ new Date()).toISOString(), data: { task_id: data.task_id, diff --git a/src/hooks/_project.ts b/src/hooks/_project.ts index 35364ea3b..22eeeb55f 100644 --- a/src/hooks/_project.ts +++ b/src/hooks/_project.ts @@ -18,3 +18,13 @@ export function resolveProject(cwd?: string): string { } catch {} return basename(dir); } + +export function hookCwd(data: Record | null | undefined): string | undefined { + if (!data || typeof data !== "object") return undefined; + if (typeof data.cwd === "string" && data.cwd.trim()) return data.cwd; + const roots = data.workspace_roots; + if (Array.isArray(roots) && typeof roots[0] === "string" && roots[0].trim()) { + return roots[0]; + } + return undefined; +} diff --git a/src/hooks/notification.ts b/src/hooks/notification.ts index af3075ab5..75faf81e8 100644 --- a/src/hooks/notification.ts +++ b/src/hooks/notification.ts @@ -1,5 +1,5 @@ #!/usr/bin/env node -import { resolveProject } from "./_project.js"; +import { resolveProject, hookCwd } from "./_project.js"; function isSdkChildContext(payload: unknown): boolean { if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true; @@ -34,20 +34,22 @@ async function main() { const notificationType = data.notification_type ?? data.notificationType; if (notificationType !== "permission_prompt") return; - const rawSessionId = data.session_id ?? data.sessionId; + const rawSessionId = data.session_id ?? data.sessionId ?? data.conversation_id; const sessionId = typeof rawSessionId === "string" && rawSessionId.length > 0 ? rawSessionId : "unknown"; + const cwd = hookCwd(data) || process.cwd(); + fetch(`${REST_URL}/agentmemory/observe`, { method: "POST", headers: authHeaders(), body: JSON.stringify({ hookType: "notification", sessionId, - project: resolveProject(data.cwd as string | undefined), - cwd: (data.cwd as string | undefined) || process.cwd(), + project: resolveProject(cwd), + cwd, timestamp: new Date().toISOString(), data: { notification_type: notificationType, diff --git a/src/hooks/post-tool-failure.ts b/src/hooks/post-tool-failure.ts index 3c8b25a15..69ec145df 100644 --- a/src/hooks/post-tool-failure.ts +++ b/src/hooks/post-tool-failure.ts @@ -1,5 +1,5 @@ #!/usr/bin/env node -import { resolveProject } from "./_project.js"; +import { resolveProject, hookCwd } from "./_project.js"; function isSdkChildContext(payload: unknown): boolean { if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true; @@ -33,19 +33,21 @@ async function main() { if (isSdkChildContext(data)) return; if (data.is_interrupt || data.isInterrupt) return; - const sessionId = ((data.session_id || data.sessionId) as string) || "unknown"; + const sessionId = ((data.session_id || data.sessionId || data.conversation_id) as string) || "unknown"; const toolName = data.tool_name ?? data.toolName; const toolInput = data.tool_input ?? data.toolArgs; const error = data.error ?? data.errorMessage; + const cwd = hookCwd(data) || process.cwd(); + fetch(`${REST_URL}/agentmemory/observe`, { method: "POST", headers: authHeaders(), body: JSON.stringify({ hookType: "post_tool_failure", sessionId, - project: resolveProject(data.cwd as string | undefined), - cwd: (data.cwd as string | undefined) || process.cwd(), + project: resolveProject(cwd), + cwd, timestamp: new Date().toISOString(), data: { tool_name: toolName, diff --git a/src/hooks/post-tool-use.ts b/src/hooks/post-tool-use.ts index a7e556de2..e8fe3483c 100644 --- a/src/hooks/post-tool-use.ts +++ b/src/hooks/post-tool-use.ts @@ -1,5 +1,5 @@ #!/usr/bin/env node -import { resolveProject } from "./_project.js"; +import { resolveProject, hookCwd } from "./_project.js"; function isSdkChildContext(payload: unknown): boolean { if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true; @@ -32,11 +32,12 @@ async function main() { if (!data || typeof data !== "object") return; if (isSdkChildContext(data)) return; - const sessionId = ((data.session_id || data.sessionId) as string) || "unknown"; + const sessionId = ((data.session_id || data.sessionId || data.conversation_id) as string) || "unknown"; const toolName = data.tool_name ?? data.toolName; const toolInput = data.tool_input ?? data.toolArgs; const { imageData, cleanOutput } = extractImageData(toolOutput(data)); + const cwd = hookCwd(data) || process.cwd(); fetch(`${REST_URL}/agentmemory/observe`, { method: "POST", @@ -44,8 +45,8 @@ async function main() { body: JSON.stringify({ hookType: "post_tool_use", sessionId, - project: resolveProject(data.cwd as string | undefined), - cwd: (data.cwd as string | undefined) || process.cwd(), + project: resolveProject(cwd), + cwd, timestamp: new Date().toISOString(), data: { tool_name: toolName, diff --git a/src/hooks/pre-compact.ts b/src/hooks/pre-compact.ts index 339eec5d3..71d7c5cbd 100644 --- a/src/hooks/pre-compact.ts +++ b/src/hooks/pre-compact.ts @@ -1,5 +1,5 @@ #!/usr/bin/env node -import { resolveProject } from "./_project.js"; +import { resolveProject, hookCwd } from "./_project.js"; function isSdkChildContext(payload: unknown): boolean { if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true; @@ -32,8 +32,8 @@ async function main() { if (!data || typeof data !== "object") return; if (isSdkChildContext(data)) return; - const sessionId = ((data.session_id || data.sessionId) as string) || "unknown"; - const project = resolveProject(data.cwd as string | undefined); + const sessionId = ((data.session_id || data.sessionId || data.conversation_id) as string) || "unknown"; + const project = resolveProject(hookCwd(data)); if (process.env["CLAUDE_MEMORY_BRIDGE"] === "true") { try { diff --git a/src/hooks/pre-tool-use.ts b/src/hooks/pre-tool-use.ts index eda68b458..0262fdea5 100644 --- a/src/hooks/pre-tool-use.ts +++ b/src/hooks/pre-tool-use.ts @@ -89,7 +89,7 @@ async function main() { } } - const rawSessionId = data.session_id || data.sessionId; + const rawSessionId = data.session_id || data.sessionId || data.conversation_id; const sessionId = typeof rawSessionId === "string" && rawSessionId.length > 0 ? rawSessionId diff --git a/src/hooks/prompt-submit.ts b/src/hooks/prompt-submit.ts index 7f973b76c..91527b742 100644 --- a/src/hooks/prompt-submit.ts +++ b/src/hooks/prompt-submit.ts @@ -1,5 +1,5 @@ #!/usr/bin/env node -import { resolveProject } from "./_project.js"; +import { resolveProject, hookCwd } from "./_project.js"; function isSdkChildContext(payload: unknown): boolean { if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true; @@ -32,7 +32,9 @@ async function main() { if (!data || typeof data !== "object") return; if (isSdkChildContext(data)) return; - const sessionId = ((data.session_id || data.sessionId) as string) || "unknown"; + const sessionId = ((data.session_id || data.sessionId || data.conversation_id) as string) || "unknown"; + + const cwd = hookCwd(data) || process.cwd(); fetch(`${REST_URL}/agentmemory/observe`, { method: "POST", @@ -40,8 +42,8 @@ async function main() { body: JSON.stringify({ hookType: "prompt_submit", sessionId, - project: resolveProject(data.cwd as string | undefined), - cwd: (data.cwd as string | undefined) || process.cwd(), + project: resolveProject(cwd), + cwd, timestamp: new Date().toISOString(), data: { prompt: data.prompt ?? data.userPrompt }, }), diff --git a/src/hooks/session-end.ts b/src/hooks/session-end.ts index c1f6cc984..7cd5b0f7b 100644 --- a/src/hooks/session-end.ts +++ b/src/hooks/session-end.ts @@ -1,4 +1,6 @@ #!/usr/bin/env node +import { readFileSync } from "node:fs"; +import { resolveProject, hookCwd } from "./_project.js"; function isSdkChildContext(payload: unknown): boolean { if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true; @@ -15,6 +17,39 @@ function authHeaders(): Record { return h; } +function extractTranscriptPrompts(data: Record): string[] { + const path = data.transcript_path; + if (typeof path !== "string" || !path.endsWith(".jsonl")) return []; + let raw: string; + try { + raw = readFileSync(path, "utf-8"); + } catch { + return []; + } + const prompts: string[] = []; + for (const line of raw.split("\n")) { + if (!line.trim()) continue; + let msg: { + role?: string; + message?: { content?: Array<{ type?: string; text?: string }> }; + }; + try { + msg = JSON.parse(line); + } catch { + continue; + } + if (msg.role !== "user") continue; + for (const block of msg.message?.content ?? []) { + if (block.type !== "text" || typeof block.text !== "string") continue; + const m = block.text.match(/\n?([\s\S]*?)\n?<\/user_query>/); + const text = (m ? m[1] : block.text).trim(); + if (text) prompts.push(text.slice(0, 8000)); + } + if (prompts.length >= 50) break; + } + return prompts; +} + async function main() { let input = ""; for await (const chunk of process.stdin) { @@ -31,7 +66,28 @@ async function main() { if (!data || typeof data !== "object") return; if (isSdkChildContext(data)) return; - const sessionId = ((data.session_id || data.sessionId) as string) || "unknown"; + const sessionId = ((data.session_id || data.sessionId || data.conversation_id) as string) || "unknown"; + + const transcriptPrompts = extractTranscriptPrompts(data); + if (transcriptPrompts.length > 0) { + const cwd = hookCwd(data) || process.cwd(); + const project = resolveProject(cwd); + for (const prompt of transcriptPrompts) { + fetch(`${REST_URL}/agentmemory/observe`, { + method: "POST", + headers: authHeaders(), + body: JSON.stringify({ + hookType: "prompt_submit", + sessionId, + project, + cwd, + timestamp: new Date().toISOString(), + data: { prompt }, + }), + signal: AbortSignal.timeout(3000), + }).catch(() => {}); + } + } fetch(`${REST_URL}/agentmemory/session/end`, { method: "POST", diff --git a/src/hooks/session-start.ts b/src/hooks/session-start.ts index 7e49a2df2..d3956e6d6 100644 --- a/src/hooks/session-start.ts +++ b/src/hooks/session-start.ts @@ -1,5 +1,5 @@ #!/usr/bin/env node -import { resolveProject } from "./_project.js"; +import { resolveProject, hookCwd } from "./_project.js"; // Inlined from ./sdk-guard so each hook bundles to a single self-contained // .mjs (matches the pattern used by every other hook entry in tsdown.config). @@ -51,10 +51,10 @@ async function main() { if (isSdkChildContext(data)) return; const sessionId = - ((data.session_id || data.sessionId) as string) || + ((data.session_id || data.sessionId || data.conversation_id) as string) || `ses_${Date.now().toString(36)}`; - const cwd = (data.cwd as string) || process.cwd(); - const project = resolveProject(data.cwd as string | undefined); + const cwd = hookCwd(data) || process.cwd(); + const project = resolveProject(cwd); const url = `${REST_URL}/agentmemory/session/start`; const init: RequestInit = { @@ -82,7 +82,13 @@ async function main() { if (res.ok) { const result = (await res.json()) as { context?: string }; if (result.context) { - process.stdout.write(result.context); + const isCursor = + typeof data.cursor_version === "string" || data.hook_event_name === "sessionStart"; + if (isCursor) { + process.stdout.write(JSON.stringify({ additional_context: result.context })); + } else { + process.stdout.write(result.context); + } } } } catch { diff --git a/src/hooks/stop.ts b/src/hooks/stop.ts index 4f666781a..7cda5d2d5 100644 --- a/src/hooks/stop.ts +++ b/src/hooks/stop.ts @@ -38,7 +38,7 @@ async function main() { return; } - const sessionId = ((data.session_id || data.sessionId) as string) || "unknown"; + const sessionId = ((data.session_id || data.sessionId || data.conversation_id) as string) || "unknown"; // session/end already fans out the summary server-side (#1203). fetch(`${REST_URL}/agentmemory/session/end`, { diff --git a/src/hooks/subagent-start.ts b/src/hooks/subagent-start.ts index da1e6d34a..18cfe5e30 100644 --- a/src/hooks/subagent-start.ts +++ b/src/hooks/subagent-start.ts @@ -1,5 +1,5 @@ #!/usr/bin/env node -import { resolveProject } from "./_project.js"; +import { resolveProject, hookCwd } from "./_project.js"; // Inlined from ./sdk-guard so each hook bundles to a single self-contained // .mjs (matches the pattern used by every other hook entry in tsdown.config). @@ -40,18 +40,20 @@ async function main() { if (!data || typeof data !== "object") return; if (isSdkChildContext(data)) return; - const sessionId = ((data.session_id || data.sessionId) as string) || "unknown"; + const sessionId = ((data.session_id || data.sessionId || data.conversation_id) as string) || "unknown"; const agentId = data.agent_id || data.agentName; const agentType = data.agent_type || data.agentDisplayName || data.agentName; + const cwd = hookCwd(data) || process.cwd(); + fetch(`${REST_URL}/agentmemory/observe`, { method: "POST", headers: authHeaders(), body: JSON.stringify({ hookType: "subagent_start", sessionId, - project: resolveProject(data.cwd as string | undefined), - cwd: (data.cwd as string | undefined) || process.cwd(), + project: resolveProject(cwd), + cwd, timestamp: new Date().toISOString(), data: { agent_id: agentId, diff --git a/src/hooks/subagent-stop.ts b/src/hooks/subagent-stop.ts index be453ba93..d071bb36c 100644 --- a/src/hooks/subagent-stop.ts +++ b/src/hooks/subagent-stop.ts @@ -1,5 +1,5 @@ #!/usr/bin/env node -import { resolveProject } from "./_project.js"; +import { resolveProject, hookCwd } from "./_project.js"; function isSdkChildContext(payload: unknown): boolean { if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true; @@ -32,7 +32,7 @@ async function main() { if (!data || typeof data !== "object") return; if (isSdkChildContext(data)) return; - const sessionId = ((data.session_id || data.sessionId) as string) || "unknown"; + const sessionId = ((data.session_id || data.sessionId || data.conversation_id) as string) || "unknown"; const agentId = data.agent_id || data.agentName; const agentType = data.agent_type || data.agentDisplayName || data.agentName; const lastMsg = @@ -40,14 +40,16 @@ async function main() { ? data.last_assistant_message.slice(0, 4000) : ""; + const cwd = hookCwd(data) || process.cwd(); + fetch(`${REST_URL}/agentmemory/observe`, { method: "POST", headers: authHeaders(), body: JSON.stringify({ hookType: "subagent_stop", sessionId, - project: resolveProject(data.cwd as string | undefined), - cwd: (data.cwd as string | undefined) || process.cwd(), + project: resolveProject(cwd), + cwd, timestamp: new Date().toISOString(), data: { agent_id: agentId, diff --git a/src/hooks/task-completed.ts b/src/hooks/task-completed.ts index a72d594b8..27371b16c 100644 --- a/src/hooks/task-completed.ts +++ b/src/hooks/task-completed.ts @@ -1,5 +1,5 @@ #!/usr/bin/env node -import { resolveProject } from "./_project.js"; +import { resolveProject, hookCwd } from "./_project.js"; function isSdkChildContext(payload: unknown): boolean { if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true; @@ -34,14 +34,16 @@ async function main() { const sessionId = (data.session_id as string) || "unknown"; + const cwd = hookCwd(data) || process.cwd(); + fetch(`${REST_URL}/agentmemory/observe`, { method: "POST", headers: authHeaders(), body: JSON.stringify({ hookType: "task_completed", sessionId, - project: resolveProject(data.cwd as string | undefined), - cwd: (data.cwd as string | undefined) || process.cwd(), + project: resolveProject(cwd), + cwd, timestamp: new Date().toISOString(), data: { task_id: data.task_id, diff --git a/test/session-end-transcript.test.ts b/test/session-end-transcript.test.ts new file mode 100644 index 000000000..13c90728b --- /dev/null +++ b/test/session-end-transcript.test.ts @@ -0,0 +1,124 @@ +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import { spawn } from "node:child_process"; +import { createServer, type Server } from "node:http"; +import { mkdtempSync, writeFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +let server: Server; +let port: number; +const posts: Array<{ path: string; body: Record }> = []; + +function runHook(payload: Record): Promise { + return new Promise((resolve) => { + const child = spawn("node", ["plugin/scripts/session-end.mjs"], { + env: { ...process.env, AGENTMEMORY_URL: `http://127.0.0.1:${port}` }, + }); + child.on("exit", (code) => resolve(code ?? 1)); + child.stdin.write(JSON.stringify(payload)); + child.stdin.end(); + }); +} + +describe("session-end transcript prompt backfill", () => { + let dir: string; + + beforeAll(async () => { + dir = mkdtempSync(join(tmpdir(), "am-transcript-")); + server = createServer((req, res) => { + let body = ""; + req.on("data", (c) => (body += c)); + req.on("end", () => { + try { + posts.push({ path: req.url ?? "", body: JSON.parse(body || "{}") }); + } catch { + posts.push({ path: req.url ?? "", body: {} }); + } + res.writeHead(200, { "Content-Type": "application/json" }); + res.end("{}"); + }); + }); + await new Promise((r) => server.listen(0, "127.0.0.1", r)); + port = (server.address() as { port: number }).port; + }); + + afterAll(() => { + server.close(); + rmSync(dir, { recursive: true, force: true }); + }); + + it("posts each user_query from a Cursor transcript before session end", async () => { + posts.length = 0; + const transcript = join(dir, "t1.jsonl"); + writeFileSync( + transcript, + [ + JSON.stringify({ + role: "user", + message: { + content: [ + { + type: "text", + text: "Sunday\n\nfirst prompt here\n", + }, + ], + }, + }), + JSON.stringify({ + role: "assistant", + message: { content: [{ type: "text", text: "answer" }] }, + }), + JSON.stringify({ + role: "user", + message: { + content: [{ type: "text", text: "bare second prompt" }], + }, + }), + "", + ].join("\n"), + ); + + const code = await runHook({ + session_id: "ses_t1", + hook_event_name: "sessionEnd", + workspace_roots: ["/tmp"], + reason: "completed", + transcript_path: transcript, + }); + expect(code).toBe(0); + + const observes = posts.filter((p) => p.path.includes("/observe")); + expect(observes.map((p) => (p.body.data as { prompt: string }).prompt)).toEqual([ + "first prompt here", + "bare second prompt", + ]); + for (const p of observes) { + expect(p.body.hookType).toBe("prompt_submit"); + expect(p.body.sessionId).toBe("ses_t1"); + } + expect(posts.some((p) => p.path.includes("/session/end"))).toBe(true); + }); + + it("skips backfill cleanly when transcript_path is absent or unreadable", async () => { + posts.length = 0; + expect( + await runHook({ + session_id: "ses_t2", + hook_event_name: "sessionEnd", + reason: "completed", + }), + ).toBe(0); + expect( + await runHook({ + session_id: "ses_t3", + hook_event_name: "sessionEnd", + reason: "completed", + transcript_path: join(dir, "missing.jsonl"), + }), + ).toBe(0); + expect(posts.filter((p) => p.path.includes("/observe"))).toHaveLength(0); + expect( + posts.filter((p) => p.path.includes("/session/end")), + ).toHaveLength(2); + }); +}); From ce172d2a7bc4e19b28e09443af81589ec13a94b1 Mon Sep 17 00:00:00 2001 From: Rohit Ghumare Date: Sun, 16 Aug 2026 12:57:24 +0100 Subject: [PATCH 3/9] fix: plugin-root hook paths, backfill ordering, session-id fallbacks --- plugin/cursor/hooks.json | 14 ++++++------ plugin/scripts/notification.mjs | 12 +++++++--- plugin/scripts/post-tool-failure.mjs | 4 +++- plugin/scripts/post-tool-use.mjs | 4 +++- plugin/scripts/pre-compact.mjs | 4 +++- plugin/scripts/prompt-submit.mjs | 4 +++- plugin/scripts/session-end.mjs | 13 ++++++----- plugin/scripts/session-start.mjs | 4 +++- plugin/scripts/subagent-start.mjs | 4 +++- plugin/scripts/subagent-stop.mjs | 4 +++- plugin/scripts/task-completed.mjs | 6 +++-- src/hooks/_project.ts | 6 +++-- src/hooks/notification.ts | 9 ++++---- src/hooks/session-end.ts | 33 +++++++++++++++------------- src/hooks/task-completed.ts | 2 +- test/cursor-plugin.test.ts | 3 ++- test/session-end-transcript.test.ts | 28 ++++++++++++++++++++++- 17 files changed, 105 insertions(+), 49 deletions(-) diff --git a/plugin/cursor/hooks.json b/plugin/cursor/hooks.json index 01ee1a076..8f6e393b7 100644 --- a/plugin/cursor/hooks.json +++ b/plugin/cursor/hooks.json @@ -3,38 +3,38 @@ "hooks": { "sessionStart": [ { - "command": "node plugin/scripts/session-start.mjs" + "command": "node ${CURSOR_PLUGIN_ROOT}/plugin/scripts/session-start.mjs" } ], "beforeSubmitPrompt": [ { - "command": "node plugin/scripts/prompt-submit.mjs" + "command": "node ${CURSOR_PLUGIN_ROOT}/plugin/scripts/prompt-submit.mjs" } ], "preToolUse": [ { - "command": "node plugin/scripts/pre-tool-use.mjs", + "command": "node ${CURSOR_PLUGIN_ROOT}/plugin/scripts/pre-tool-use.mjs", "matcher": "Shell|Read|Write|Grep" } ], "postToolUse": [ { - "command": "node plugin/scripts/post-tool-use.mjs" + "command": "node ${CURSOR_PLUGIN_ROOT}/plugin/scripts/post-tool-use.mjs" } ], "postToolUseFailure": [ { - "command": "node plugin/scripts/post-tool-failure.mjs" + "command": "node ${CURSOR_PLUGIN_ROOT}/plugin/scripts/post-tool-failure.mjs" } ], "stop": [ { - "command": "node plugin/scripts/stop.mjs" + "command": "node ${CURSOR_PLUGIN_ROOT}/plugin/scripts/stop.mjs" } ], "sessionEnd": [ { - "command": "node plugin/scripts/session-end.mjs" + "command": "node ${CURSOR_PLUGIN_ROOT}/plugin/scripts/session-end.mjs" } ] } diff --git a/plugin/scripts/notification.mjs b/plugin/scripts/notification.mjs index 603ee0762..29c3c40d9 100755 --- a/plugin/scripts/notification.mjs +++ b/plugin/scripts/notification.mjs @@ -24,7 +24,9 @@ function hookCwd(data) { if (!data || typeof data !== "object") return void 0; if (typeof data.cwd === "string" && data.cwd.trim()) return data.cwd; const roots = data.workspace_roots; - if (Array.isArray(roots) && typeof roots[0] === "string" && roots[0].trim()) return roots[0]; + if (Array.isArray(roots)) { + for (const root of roots) if (typeof root === "string" && root.trim()) return root; + } } //#endregion //#region src/hooks/notification.ts @@ -53,8 +55,12 @@ async function main() { if (isSdkChildContext(data)) return; const notificationType = data.notification_type ?? data.notificationType; if (notificationType !== "permission_prompt") return; - const rawSessionId = data.session_id ?? data.sessionId ?? data.conversation_id; - const sessionId = typeof rawSessionId === "string" && rawSessionId.length > 0 ? rawSessionId : "unknown"; + const rawSessionId = [ + data.session_id, + data.sessionId, + data.conversation_id + ].find((v) => typeof v === "string" && v.length > 0); + const sessionId = typeof rawSessionId === "string" ? rawSessionId : "unknown"; const cwd = hookCwd(data) || process.cwd(); fetch(`${REST_URL}/agentmemory/observe`, { method: "POST", diff --git a/plugin/scripts/post-tool-failure.mjs b/plugin/scripts/post-tool-failure.mjs index 6434d020e..ce872d175 100755 --- a/plugin/scripts/post-tool-failure.mjs +++ b/plugin/scripts/post-tool-failure.mjs @@ -24,7 +24,9 @@ function hookCwd(data) { if (!data || typeof data !== "object") return void 0; if (typeof data.cwd === "string" && data.cwd.trim()) return data.cwd; const roots = data.workspace_roots; - if (Array.isArray(roots) && typeof roots[0] === "string" && roots[0].trim()) return roots[0]; + if (Array.isArray(roots)) { + for (const root of roots) if (typeof root === "string" && root.trim()) return root; + } } //#endregion //#region src/hooks/post-tool-failure.ts diff --git a/plugin/scripts/post-tool-use.mjs b/plugin/scripts/post-tool-use.mjs index 3ce2d5c2d..857866902 100755 --- a/plugin/scripts/post-tool-use.mjs +++ b/plugin/scripts/post-tool-use.mjs @@ -24,7 +24,9 @@ function hookCwd(data) { if (!data || typeof data !== "object") return void 0; if (typeof data.cwd === "string" && data.cwd.trim()) return data.cwd; const roots = data.workspace_roots; - if (Array.isArray(roots) && typeof roots[0] === "string" && roots[0].trim()) return roots[0]; + if (Array.isArray(roots)) { + for (const root of roots) if (typeof root === "string" && root.trim()) return root; + } } //#endregion //#region src/hooks/post-tool-use.ts diff --git a/plugin/scripts/pre-compact.mjs b/plugin/scripts/pre-compact.mjs index 53f67bc19..7ab693be9 100755 --- a/plugin/scripts/pre-compact.mjs +++ b/plugin/scripts/pre-compact.mjs @@ -24,7 +24,9 @@ function hookCwd(data) { if (!data || typeof data !== "object") return void 0; if (typeof data.cwd === "string" && data.cwd.trim()) return data.cwd; const roots = data.workspace_roots; - if (Array.isArray(roots) && typeof roots[0] === "string" && roots[0].trim()) return roots[0]; + if (Array.isArray(roots)) { + for (const root of roots) if (typeof root === "string" && root.trim()) return root; + } } //#endregion //#region src/hooks/pre-compact.ts diff --git a/plugin/scripts/prompt-submit.mjs b/plugin/scripts/prompt-submit.mjs index e1f9cb17a..b5704a317 100755 --- a/plugin/scripts/prompt-submit.mjs +++ b/plugin/scripts/prompt-submit.mjs @@ -24,7 +24,9 @@ function hookCwd(data) { if (!data || typeof data !== "object") return void 0; if (typeof data.cwd === "string" && data.cwd.trim()) return data.cwd; const roots = data.workspace_roots; - if (Array.isArray(roots) && typeof roots[0] === "string" && roots[0].trim()) return roots[0]; + if (Array.isArray(roots)) { + for (const root of roots) if (typeof root === "string" && root.trim()) return root; + } } //#endregion //#region src/hooks/prompt-submit.ts diff --git a/plugin/scripts/session-end.mjs b/plugin/scripts/session-end.mjs index aff4cb7f6..53472fb8d 100755 --- a/plugin/scripts/session-end.mjs +++ b/plugin/scripts/session-end.mjs @@ -25,7 +25,9 @@ function hookCwd(data) { if (!data || typeof data !== "object") return void 0; if (typeof data.cwd === "string" && data.cwd.trim()) return data.cwd; const roots = data.workspace_roots; - if (Array.isArray(roots) && typeof roots[0] === "string" && roots[0].trim()) return roots[0]; + if (Array.isArray(roots)) { + for (const root of roots) if (typeof root === "string" && root.trim()) return root; + } } //#endregion //#region src/hooks/session-end.ts @@ -61,12 +63,12 @@ function extractTranscriptPrompts(data) { } if (msg.role !== "user") continue; for (const block of msg.message?.content ?? []) { + if (prompts.length >= 50) return prompts; if (block.type !== "text" || typeof block.text !== "string") continue; const m = block.text.match(/\n?([\s\S]*?)\n?<\/user_query>/); const text = (m ? m[1] : block.text).trim(); if (text) prompts.push(text.slice(0, 8e3)); } - if (prompts.length >= 50) break; } return prompts; } @@ -86,7 +88,8 @@ async function main() { if (transcriptPrompts.length > 0) { const cwd = hookCwd(data) || process.cwd(); const project = resolveProject(cwd); - for (const prompt of transcriptPrompts) fetch(`${REST_URL}/agentmemory/observe`, { + const timestamp = (/* @__PURE__ */ new Date()).toISOString(); + await Promise.allSettled(transcriptPrompts.map((prompt) => fetch(`${REST_URL}/agentmemory/observe`, { method: "POST", headers: authHeaders(), body: JSON.stringify({ @@ -94,11 +97,11 @@ async function main() { sessionId, project, cwd, - timestamp: (/* @__PURE__ */ new Date()).toISOString(), + timestamp, data: { prompt } }), signal: AbortSignal.timeout(3e3) - }).catch(() => {}); + }))); } fetch(`${REST_URL}/agentmemory/session/end`, { method: "POST", diff --git a/plugin/scripts/session-start.mjs b/plugin/scripts/session-start.mjs index 61c7b0eda..eec9b6e2f 100755 --- a/plugin/scripts/session-start.mjs +++ b/plugin/scripts/session-start.mjs @@ -24,7 +24,9 @@ function hookCwd(data) { if (!data || typeof data !== "object") return void 0; if (typeof data.cwd === "string" && data.cwd.trim()) return data.cwd; const roots = data.workspace_roots; - if (Array.isArray(roots) && typeof roots[0] === "string" && roots[0].trim()) return roots[0]; + if (Array.isArray(roots)) { + for (const root of roots) if (typeof root === "string" && root.trim()) return root; + } } //#endregion //#region src/hooks/session-start.ts diff --git a/plugin/scripts/subagent-start.mjs b/plugin/scripts/subagent-start.mjs index bc5e5989f..fa57f363c 100755 --- a/plugin/scripts/subagent-start.mjs +++ b/plugin/scripts/subagent-start.mjs @@ -24,7 +24,9 @@ function hookCwd(data) { if (!data || typeof data !== "object") return void 0; if (typeof data.cwd === "string" && data.cwd.trim()) return data.cwd; const roots = data.workspace_roots; - if (Array.isArray(roots) && typeof roots[0] === "string" && roots[0].trim()) return roots[0]; + if (Array.isArray(roots)) { + for (const root of roots) if (typeof root === "string" && root.trim()) return root; + } } //#endregion //#region src/hooks/subagent-start.ts diff --git a/plugin/scripts/subagent-stop.mjs b/plugin/scripts/subagent-stop.mjs index 88f12f236..5fb7a0857 100755 --- a/plugin/scripts/subagent-stop.mjs +++ b/plugin/scripts/subagent-stop.mjs @@ -24,7 +24,9 @@ function hookCwd(data) { if (!data || typeof data !== "object") return void 0; if (typeof data.cwd === "string" && data.cwd.trim()) return data.cwd; const roots = data.workspace_roots; - if (Array.isArray(roots) && typeof roots[0] === "string" && roots[0].trim()) return roots[0]; + if (Array.isArray(roots)) { + for (const root of roots) if (typeof root === "string" && root.trim()) return root; + } } //#endregion //#region src/hooks/subagent-stop.ts diff --git a/plugin/scripts/task-completed.mjs b/plugin/scripts/task-completed.mjs index f12713e44..bd413a0ab 100755 --- a/plugin/scripts/task-completed.mjs +++ b/plugin/scripts/task-completed.mjs @@ -24,7 +24,9 @@ function hookCwd(data) { if (!data || typeof data !== "object") return void 0; if (typeof data.cwd === "string" && data.cwd.trim()) return data.cwd; const roots = data.workspace_roots; - if (Array.isArray(roots) && typeof roots[0] === "string" && roots[0].trim()) return roots[0]; + if (Array.isArray(roots)) { + for (const root of roots) if (typeof root === "string" && root.trim()) return root; + } } //#endregion //#region src/hooks/task-completed.ts @@ -51,7 +53,7 @@ async function main() { } if (!data || typeof data !== "object") return; if (isSdkChildContext(data)) return; - const sessionId = data.session_id || "unknown"; + const sessionId = data.session_id || data.sessionId || data.conversation_id || "unknown"; const cwd = hookCwd(data) || process.cwd(); fetch(`${REST_URL}/agentmemory/observe`, { method: "POST", diff --git a/src/hooks/_project.ts b/src/hooks/_project.ts index 22eeeb55f..cf92edf60 100644 --- a/src/hooks/_project.ts +++ b/src/hooks/_project.ts @@ -23,8 +23,10 @@ export function hookCwd(data: Record | null | undefined): strin if (!data || typeof data !== "object") return undefined; if (typeof data.cwd === "string" && data.cwd.trim()) return data.cwd; const roots = data.workspace_roots; - if (Array.isArray(roots) && typeof roots[0] === "string" && roots[0].trim()) { - return roots[0]; + if (Array.isArray(roots)) { + for (const root of roots) { + if (typeof root === "string" && root.trim()) return root; + } } return undefined; } diff --git a/src/hooks/notification.ts b/src/hooks/notification.ts index 75faf81e8..4c6a1063f 100644 --- a/src/hooks/notification.ts +++ b/src/hooks/notification.ts @@ -34,11 +34,10 @@ async function main() { const notificationType = data.notification_type ?? data.notificationType; if (notificationType !== "permission_prompt") return; - const rawSessionId = data.session_id ?? data.sessionId ?? data.conversation_id; - const sessionId = - typeof rawSessionId === "string" && rawSessionId.length > 0 - ? rawSessionId - : "unknown"; + const rawSessionId = [data.session_id, data.sessionId, data.conversation_id].find( + (v) => typeof v === "string" && v.length > 0, + ); + const sessionId = typeof rawSessionId === "string" ? rawSessionId : "unknown"; const cwd = hookCwd(data) || process.cwd(); diff --git a/src/hooks/session-end.ts b/src/hooks/session-end.ts index 7cd5b0f7b..f39f964a5 100644 --- a/src/hooks/session-end.ts +++ b/src/hooks/session-end.ts @@ -40,12 +40,12 @@ function extractTranscriptPrompts(data: Record): string[] { } if (msg.role !== "user") continue; for (const block of msg.message?.content ?? []) { + if (prompts.length >= 50) return prompts; if (block.type !== "text" || typeof block.text !== "string") continue; const m = block.text.match(/\n?([\s\S]*?)\n?<\/user_query>/); const text = (m ? m[1] : block.text).trim(); if (text) prompts.push(text.slice(0, 8000)); } - if (prompts.length >= 50) break; } return prompts; } @@ -72,21 +72,24 @@ async function main() { if (transcriptPrompts.length > 0) { const cwd = hookCwd(data) || process.cwd(); const project = resolveProject(cwd); - for (const prompt of transcriptPrompts) { - fetch(`${REST_URL}/agentmemory/observe`, { - method: "POST", - headers: authHeaders(), - body: JSON.stringify({ - hookType: "prompt_submit", - sessionId, - project, - cwd, - timestamp: new Date().toISOString(), - data: { prompt }, + const timestamp = new Date().toISOString(); + await Promise.allSettled( + transcriptPrompts.map((prompt) => + fetch(`${REST_URL}/agentmemory/observe`, { + method: "POST", + headers: authHeaders(), + body: JSON.stringify({ + hookType: "prompt_submit", + sessionId, + project, + cwd, + timestamp, + data: { prompt }, + }), + signal: AbortSignal.timeout(3000), }), - signal: AbortSignal.timeout(3000), - }).catch(() => {}); - } + ), + ); } fetch(`${REST_URL}/agentmemory/session/end`, { diff --git a/src/hooks/task-completed.ts b/src/hooks/task-completed.ts index 27371b16c..724b2594e 100644 --- a/src/hooks/task-completed.ts +++ b/src/hooks/task-completed.ts @@ -32,7 +32,7 @@ async function main() { if (!data || typeof data !== "object") return; if (isSdkChildContext(data)) return; - const sessionId = (data.session_id as string) || "unknown"; + const sessionId = ((data.session_id || data.sessionId || data.conversation_id) as string) || "unknown"; const cwd = hookCwd(data) || process.cwd(); diff --git a/test/cursor-plugin.test.ts b/test/cursor-plugin.test.ts index b98c29959..afba14073 100644 --- a/test/cursor-plugin.test.ts +++ b/test/cursor-plugin.test.ts @@ -67,11 +67,12 @@ describe("Cursor plugin hooks config", () => { } }); - it("every hook command points at an existing built script", () => { + it("every hook command resolves from the plugin root to a built script", () => { for (const entries of Object.values(hooks.hooks) as Array< Array<{ command: string }> >) { for (const entry of entries) { + expect(entry.command).toContain("${CURSOR_PLUGIN_ROOT}/"); const script = entry.command.match(/plugin\/scripts\/\S+\.mjs/)?.[0]; expect(script).toBeDefined(); expect(existsSync(script!)).toBe(true); diff --git a/test/session-end-transcript.test.ts b/test/session-end-transcript.test.ts index 13c90728b..f48cf42d6 100644 --- a/test/session-end-transcript.test.ts +++ b/test/session-end-transcript.test.ts @@ -96,7 +96,33 @@ describe("session-end transcript prompt backfill", () => { expect(p.body.hookType).toBe("prompt_submit"); expect(p.body.sessionId).toBe("ses_t1"); } - expect(posts.some((p) => p.path.includes("/session/end"))).toBe(true); + const endIndex = posts.findIndex((p) => p.path.includes("/session/end")); + expect(endIndex).toBeGreaterThanOrEqual(0); + for (let i = 0; i < posts.length; i++) { + if (posts[i].path.includes("/observe")) expect(i).toBeLessThan(endIndex); + } + }); + + it("caps backfill at 50 prompts even within a single transcript record", async () => { + posts.length = 0; + const transcript = join(dir, "t-cap.jsonl"); + const blocks = Array.from({ length: 60 }, (_, i) => ({ + type: "text", + text: `\nprompt number ${i}\n`, + })); + writeFileSync( + transcript, + JSON.stringify({ role: "user", message: { content: blocks } }) + "\n", + ); + + const code = await runHook({ + session_id: "ses_cap", + hook_event_name: "sessionEnd", + reason: "completed", + transcript_path: transcript, + }); + expect(code).toBe(0); + expect(posts.filter((p) => p.path.includes("/observe"))).toHaveLength(50); }); it("skips backfill cleanly when transcript_path is absent or unreadable", async () => { From 8f84dbf270d31a04ce0a01989b1a92544cc5686d Mon Sep 17 00:00:00 2001 From: Rohit Ghumare Date: Sun, 16 Aug 2026 13:05:55 +0100 Subject: [PATCH 4/9] docs: cursor plugin rows in readme, translations, and changelog --- CHANGELOG.md | 4 ++++ README.md | 5 +++-- READMEs/README.de-DE.md | 5 +++-- READMEs/README.es-ES.md | 5 +++-- READMEs/README.fr-FR.md | 5 +++-- READMEs/README.hi-IN.md | 5 +++-- READMEs/README.ja-JP.md | 5 +++-- READMEs/README.ko-KR.md | 5 +++-- READMEs/README.pt-BR.md | 5 +++-- READMEs/README.ru-RU.md | 5 +++-- READMEs/README.tr-TR.md | 5 +++-- READMEs/README.zh-CN.md | 5 +++-- READMEs/README.zh-TW.md | 5 +++-- 13 files changed, 40 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 89da67258..0904921e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ Release wave in two parts. Recall quality: hybrid ranking reaches the primary re ### Added +- **Cursor plugin.** Full Cursor Marketplace plugin (`.cursor-plugin/`): 7 native auto-capture hooks (`sessionStart`, `beforeSubmitPrompt`, `preToolUse`, `postToolUse`, `postToolUseFailure`, `stop`, `sessionEnd`), all 17 skills, and the MCP server, with `AGENTMEMORY_URL` / `AGENTMEMORY_SECRET` declared as dashboard-managed plugin variables. Hook scripts accept Cursor's payload dialect (`conversation_id` session fallback, `workspace_roots` project attribution) without changing Claude Code behavior, and context injection answers each host in its own output shape. Cursor's CLI print mode never dispatches `beforeSubmitPrompt`, so session end backfills user prompts from the session transcript; server-side dedup absorbs the re-post where a live hook already captured the prompt. Verified end to end against Cursor 3.13.25, GUI and `cursor-agent` CLI. + - **Write-time provenance on every record.** Each observation and memory carries an immutable origin block (channel `user` / `agent` / `tool` / `import` / `shared`, detail, capturedAt) stamped at capture, save, and import, and inherited through both compression paths. The base for trust-aware retrieval and ingest screening. - **`similarTo` advisory hint on save.** `mem::remember` reports a near-miss similarity match (0.4 to 0.7) back to the caller so agents can spot near-duplicates without the write being blocked. - **`AGENTMEMORY_LLM_NOTHINK=1`** (opt-in): asks local reasoning models to skip their hidden thinking pass during graph extraction. Extraction runs faster; relation quality can drop slightly. Default behavior unchanged; documented in `.env.example`. @@ -41,6 +43,8 @@ Release wave in two parts. Recall quality: hybrid ranking reaches the primary re ### Fixed +- **MCP protocol version negotiation.** The standalone MCP server hardcoded `protocolVersion: "2024-11-05"` and ignored the client's requested version, so hosts that drop that revision disconnected with `-32000` ([#908](https://github.com/rohitg00/agentmemory/issues/908)). `initialize` now echoes any supported revision (`2025-11-25`, `2025-06-18`, `2025-03-26`, `2024-11-05`) and answers with the latest supported otherwise. + - **Hybrid ranking on the primary recall path.** `mem::search` (behind `memory_recall`) now ranks through the full BM25 + vector + graph fusion when the vector index is populated; it was keyword-only while only smart-search got hybrid ranking. Fusion weights normalize per item over the streams that actually ranked it, with an explicit cross-stream agreement bonus, replacing the every-enabled-stream denominator that permanently penalized single-stream hits. Result order is deterministic (score, best rank, id). - **Indexed lesson recall.** Lessons get a dedicated in-memory BM25 index built lazily from one KV list and maintained incrementally on save, delete, and decay; recall previously listed and substring-scanned the whole corpus per query. A record cache beside the index takes recall to zero KV round-trips. - **Superseded versions leave recall.** Superseded memory versions are removed from the BM25 and vector indexes; the version chain stays in KV for history, but recall no longer returns an outdated fact as if current. `mem::remember` also finds supersession candidates through the search index (top 50) instead of walking every memory per save, with a full-scan fallback while the index is cold. diff --git a/README.md b/README.md index d1dc1e711..07093e65a 100644 --- a/README.md +++ b/README.md @@ -174,7 +174,7 @@ agentmemory works with any agent that supports hooks, MCP, or REST API. All agen Cursor
Cursor
-MCP server +native plugin + MCP Gemini CLI
@@ -688,7 +688,8 @@ The agentmemory entry is the **same MCP server block** across every host that us | Agent | Config file | Notes | |---|---|---| -| **Cursor** | `~/.cursor/mcp.json` | Merge into `mcpServers`. One-click deeplink also available on the website. | +| **Cursor (MCP only)** | `~/.cursor/mcp.json` | Merge into `mcpServers`, or `agentmemory connect cursor`. One-click deeplink also available on the website. | +| **Cursor (full plugin)** | `.cursor-plugin/` | Cursor Marketplace listing (submission in review) or Cursor Settings → Plugins → local checkout. Registers 7 auto-capture hooks (sessionStart, beforeSubmitPrompt, preToolUse, postToolUse, postToolUseFailure, stop, sessionEnd) + 17 skills + the MCP server, with `AGENTMEMORY_URL` / `AGENTMEMORY_SECRET` managed in Cursor's plugin dashboard. Works in the Cursor IDE and `cursor-agent` CLI; CLI print-mode prompts are backfilled from the session transcript at session end. | | **Claude Desktop** | `claude_desktop_config.json` (Application Support) | Merge into `mcpServers`. Restart Claude Desktop after editing. | | **Cline / Roo Code / Kilo Code** | Cline MCP settings (Settings UI → MCP Servers → Edit) | Same `mcpServers` block. | | **Windsurf** | `~/.codeium/windsurf/mcp_config.json` | Same `mcpServers` block. | diff --git a/READMEs/README.de-DE.md b/READMEs/README.de-DE.md index cba326c54..9da89238c 100644 --- a/READMEs/README.de-DE.md +++ b/READMEs/README.de-DE.md @@ -169,7 +169,7 @@ agentmemory funktioniert mit jedem Agenten, der Hooks, MCP oder REST API unterst Cursor
Cursor
-MCP-Server +native plugin + MCP Gemini CLI
@@ -657,7 +657,8 @@ Der agentmemory-Eintrag ist der **gleiche MCP-Server-Block** für jeden Host, de | Agent | Konfigurationsdatei | Hinweise | |---|---|---| -| **Cursor** | `~/.cursor/mcp.json` | In `mcpServers` einfügen. Ein-Klick-Deeplink auch auf der Website. | +| **Cursor (nur MCP)** | `~/.cursor/mcp.json` | In `mcpServers` einfügen, oder `agentmemory connect cursor`. Ein-Klick-Deeplink auch auf der Website. | +| **Cursor (volles Plugin)** | `.cursor-plugin/` | Cursor-Marketplace-Eintrag (Einreichung in Prüfung) oder Cursor Settings → Plugins → lokaler Checkout. Registriert 7 Auto-Capture-Hooks (sessionStart, beforeSubmitPrompt, preToolUse, postToolUse, postToolUseFailure, stop, sessionEnd) + 17 Skills + den MCP-Server; `AGENTMEMORY_URL` / `AGENTMEMORY_SECRET` werden im Plugin-Dashboard von Cursor verwaltet. Funktioniert in der Cursor-IDE und der `cursor-agent`-CLI; im CLI-Print-Modus werden Prompts beim Sitzungsende aus dem Transkript nachgetragen. | | **Claude Desktop** | `claude_desktop_config.json` (Application Support) | In `mcpServers` einfügen. Claude Desktop nach dem Editieren neu starten. | | **Cline / Roo Code / Kilo Code** | Cline-MCP-Einstellungen (Settings UI → MCP Servers → Edit) | Gleicher `mcpServers`-Block. | | **Windsurf** | `~/.codeium/windsurf/mcp_config.json` | Gleicher `mcpServers`-Block. | diff --git a/READMEs/README.es-ES.md b/READMEs/README.es-ES.md index c63ce1a82..4651598b1 100644 --- a/READMEs/README.es-ES.md +++ b/READMEs/README.es-ES.md @@ -169,7 +169,7 @@ agentmemory funciona con cualquier agente que soporte hooks, MCP o REST API. Tod Cursor
Cursor
-MCP server +plugin nativo + MCP Gemini CLI
@@ -657,7 +657,8 @@ La entrada de agentmemory es el **mismo bloque de servidor MCP** en cada host qu | Agente | Fichero de configuración | Notas | |---|---|---| -| **Cursor** | `~/.cursor/mcp.json` | Fusiona en `mcpServers`. También hay deeplink de un clic en el sitio web. | +| **Cursor (solo MCP)** | `~/.cursor/mcp.json` | Fusiona en `mcpServers`, o `agentmemory connect cursor`. También hay deeplink de un clic en el sitio web. | +| **Cursor (plugin completo)** | `.cursor-plugin/` | Listado en el Cursor Marketplace (envío en revisión) o Cursor Settings → Plugins → checkout local. Registra 7 hooks de captura automática (sessionStart, beforeSubmitPrompt, preToolUse, postToolUse, postToolUseFailure, stop, sessionEnd) + 17 skills + el servidor MCP; `AGENTMEMORY_URL` / `AGENTMEMORY_SECRET` se gestionan en el panel de plugins de Cursor. Funciona en el IDE de Cursor y en la CLI `cursor-agent`; en modo print de la CLI los prompts se recuperan del transcript al cerrar la sesión. | | **Claude Desktop** | `claude_desktop_config.json` (Application Support) | Fusiona en `mcpServers`. Reinicia Claude Desktop tras editar. | | **Cline / Roo Code / Kilo Code** | Ajustes MCP de Cline (Settings UI → MCP Servers → Edit) | Mismo bloque `mcpServers`. | | **Windsurf** | `~/.codeium/windsurf/mcp_config.json` | Mismo bloque `mcpServers`. | diff --git a/READMEs/README.fr-FR.md b/READMEs/README.fr-FR.md index 18b2b59f8..303602a02 100644 --- a/READMEs/README.fr-FR.md +++ b/READMEs/README.fr-FR.md @@ -169,7 +169,7 @@ agentmemory fonctionne avec tout agent qui prend en charge les hooks, MCP ou l'A Cursor
Cursor
-serveur MCP +plugin natif + MCP Gemini CLI
@@ -657,7 +657,8 @@ L'entrée agentmemory est le **même bloc serveur MCP** pour tous les hôtes uti | Agent | Fichier de config | Notes | |---|---|---| -| **Cursor** | `~/.cursor/mcp.json` | Fusionner dans `mcpServers`. Deeplink en un clic également disponible sur le site web. | +| **Cursor (MCP seul)** | `~/.cursor/mcp.json` | Fusionner dans `mcpServers`, ou `agentmemory connect cursor`. Deeplink en un clic également disponible sur le site web. | +| **Cursor (plugin complet)** | `.cursor-plugin/` | Fiche du Cursor Marketplace (soumission en cours de revue) ou Cursor Settings → Plugins → checkout local. Enregistre 7 hooks de capture automatique (sessionStart, beforeSubmitPrompt, preToolUse, postToolUse, postToolUseFailure, stop, sessionEnd) + 17 skills + le serveur MCP ; `AGENTMEMORY_URL` / `AGENTMEMORY_SECRET` sont gérés dans le tableau de bord des plugins de Cursor. Fonctionne dans l'IDE Cursor et la CLI `cursor-agent` ; en mode print de la CLI, les prompts sont récupérés depuis le transcript à la fin de session. | | **Claude Desktop** | `claude_desktop_config.json` (Application Support) | Fusionner dans `mcpServers`. Redémarrer Claude Desktop après modification. | | **Cline / Roo Code / Kilo Code** | Paramètres MCP de Cline (Settings UI → MCP Servers → Edit) | Même bloc `mcpServers`. | | **Windsurf** | `~/.codeium/windsurf/mcp_config.json` | Même bloc `mcpServers`. | diff --git a/READMEs/README.hi-IN.md b/READMEs/README.hi-IN.md index e5a860ddd..9c5b874c2 100644 --- a/READMEs/README.hi-IN.md +++ b/READMEs/README.hi-IN.md @@ -169,7 +169,7 @@ agentmemory किसी भी ऐसे एजेंट के साथ क Cursor
Cursor
-MCP सर्वर +native plugin + MCP Gemini CLI
@@ -636,7 +636,8 @@ agentmemory entry `mcpServers` shape का उपयोग करने वा | एजेंट | Config फाइल | नोट्स | |---|---|---| -| **Cursor** | `~/.cursor/mcp.json` | `mcpServers` में merge करें। Website पर one-click deeplink भी उपलब्ध। | +| **Cursor (केवल MCP)** | `~/.cursor/mcp.json` | `mcpServers` में merge करें, या `agentmemory connect cursor`। Website पर one-click deeplink भी उपलब्ध। | +| **Cursor (पूर्ण plugin)** | `.cursor-plugin/` | Cursor Marketplace listing (submission समीक्षा में) या Cursor Settings → Plugins → local checkout। 7 auto-capture hooks (sessionStart, beforeSubmitPrompt, preToolUse, postToolUse, postToolUseFailure, stop, sessionEnd) + 17 skills + MCP server register करता है; `AGENTMEMORY_URL` / `AGENTMEMORY_SECRET` Cursor के plugin dashboard में manage होते हैं। Cursor IDE और `cursor-agent` CLI दोनों में काम करता है; CLI print mode के prompts session end पर transcript से backfill होते हैं। | | **Claude Desktop** | `claude_desktop_config.json` (Application Support) | `mcpServers` में merge करें। Edit के बाद Claude Desktop restart करें। | | **Cline / Roo Code / Kilo Code** | Cline MCP settings (Settings UI → MCP Servers → Edit) | वही `mcpServers` block। | | **Windsurf** | `~/.codeium/windsurf/mcp_config.json` | वही `mcpServers` block। | diff --git a/READMEs/README.ja-JP.md b/READMEs/README.ja-JP.md index ec746e970..e04c3fef9 100644 --- a/READMEs/README.ja-JP.md +++ b/READMEs/README.ja-JP.md @@ -169,7 +169,7 @@ agentmemory は hooks、MCP、REST API をサポートするあらゆるエー Cursor
Cursor
-MCP サーバー +ネイティブプラグイン + MCP Gemini CLI
@@ -658,7 +658,8 @@ skills CLI がまだカバーしていない少数のエージェント(Zed v1.3 | エージェント | 設定ファイル | 備考 | |---|---|---| -| **Cursor** | `~/.cursor/mcp.json` | `mcpServers` にマージ。ウェブサイトでワンクリックディープリンクも利用可能。 | +| **Cursor (MCP のみ)** | `~/.cursor/mcp.json` | `mcpServers` にマージ、または `agentmemory connect cursor`。ウェブサイトでワンクリックディープリンクも利用可能。 | +| **Cursor (フルプラグイン)** | `.cursor-plugin/` | Cursor Marketplace の掲載(申請レビュー中)、または Cursor Settings → Plugins → ローカルチェックアウト。7 つの自動キャプチャ hooks(sessionStart, beforeSubmitPrompt, preToolUse, postToolUse, postToolUseFailure, stop, sessionEnd)+ 17 の skills + MCP サーバーを登録し、`AGENTMEMORY_URL` / `AGENTMEMORY_SECRET` は Cursor のプラグインダッシュボードで管理します。Cursor IDE と `cursor-agent` CLI の両方で動作。CLI の print モードのプロンプトはセッション終了時にトランスクリプトから補完されます。 | | **Claude Desktop** | `claude_desktop_config.json`(Application Support) | `mcpServers` にマージ。編集後 Claude Desktop を再起動。 | | **Cline / Roo Code / Kilo Code** | Cline MCP 設定(設定 UI → MCP Servers → Edit) | 同じ `mcpServers` ブロック。 | | **Windsurf** | `~/.codeium/windsurf/mcp_config.json` | 同じ `mcpServers` ブロック。 | diff --git a/READMEs/README.ko-KR.md b/READMEs/README.ko-KR.md index 08401a984..3145aaa06 100644 --- a/READMEs/README.ko-KR.md +++ b/READMEs/README.ko-KR.md @@ -169,7 +169,7 @@ agentmemory는 hooks, MCP, REST API를 지원하는 모든 에이전트와 호 Cursor
Cursor
-MCP server +네이티브 플러그인 + MCP Gemini CLI
@@ -655,7 +655,8 @@ agentmemory 항목은 `mcpServers` 형태를 사용하는 모든 호스트(Curso | 에이전트 | 설정 파일 | 비고 | |---|---|---| -| **Cursor** | `~/.cursor/mcp.json` | `mcpServers`에 병합. 웹사이트에서 원클릭 deeplink도 사용 가능. | +| **Cursor (MCP 전용)** | `~/.cursor/mcp.json` | `mcpServers`에 병합하거나 `agentmemory connect cursor`. 웹사이트에서 원클릭 deeplink도 사용 가능. | +| **Cursor (전체 플러그인)** | `.cursor-plugin/` | Cursor Marketplace 등록(제출 심사 중) 또는 Cursor Settings → Plugins → 로컬 체크아웃. 자동 캡처 hooks 7개(sessionStart, beforeSubmitPrompt, preToolUse, postToolUse, postToolUseFailure, stop, sessionEnd) + skills 17개 + MCP 서버를 등록하며, `AGENTMEMORY_URL` / `AGENTMEMORY_SECRET`은 Cursor 플러그인 대시보드에서 관리됩니다. Cursor IDE와 `cursor-agent` CLI 모두에서 동작; CLI print 모드의 프롬프트는 세션 종료 시 transcript에서 채워집니다. | | **Claude Desktop** | `claude_desktop_config.json` (Application Support) | `mcpServers`에 병합. 편집 후 Claude Desktop 재시작. | | **Cline / Roo Code / Kilo Code** | Cline MCP settings (Settings UI → MCP Servers → Edit) | 동일한 `mcpServers` 블록. | | **Windsurf** | `~/.codeium/windsurf/mcp_config.json` | 동일한 `mcpServers` 블록. | diff --git a/READMEs/README.pt-BR.md b/READMEs/README.pt-BR.md index a01c3dc00..a9f9f5b53 100644 --- a/READMEs/README.pt-BR.md +++ b/READMEs/README.pt-BR.md @@ -169,7 +169,7 @@ agentmemory funciona com qualquer agente que suporte hooks, MCP ou REST API. Tod Cursor
Cursor
-MCP server +plugin nativo + MCP Gemini CLI
@@ -657,7 +657,8 @@ A entrada do agentmemory é o **mesmo bloco de servidor MCP** em todo host que u | Agente | Arquivo de configuração | Notas | |---|---|---| -| **Cursor** | `~/.cursor/mcp.json` | Mescle em `mcpServers`. Deeplink de um clique também disponível no site. | +| **Cursor (só MCP)** | `~/.cursor/mcp.json` | Mescle em `mcpServers`, ou `agentmemory connect cursor`. Deeplink de um clique também disponível no site. | +| **Cursor (plugin completo)** | `.cursor-plugin/` | Listagem no Cursor Marketplace (submissão em revisão) ou Cursor Settings → Plugins → checkout local. Registra 7 hooks de captura automática (sessionStart, beforeSubmitPrompt, preToolUse, postToolUse, postToolUseFailure, stop, sessionEnd) + 17 skills + o servidor MCP; `AGENTMEMORY_URL` / `AGENTMEMORY_SECRET` são gerenciados no painel de plugins do Cursor. Funciona no IDE do Cursor e na CLI `cursor-agent`; no modo print da CLI os prompts são recuperados do transcript no fim da sessão. | | **Claude Desktop** | `claude_desktop_config.json` (Application Support) | Mescle em `mcpServers`. Reinicie o Claude Desktop após editar. | | **Cline / Roo Code / Kilo Code** | Configurações MCP do Cline (Settings UI → MCP Servers → Edit) | Mesmo bloco `mcpServers`. | | **Windsurf** | `~/.codeium/windsurf/mcp_config.json` | Mesmo bloco `mcpServers`. | diff --git a/READMEs/README.ru-RU.md b/READMEs/README.ru-RU.md index 5363fd2b6..b2b44ff9a 100644 --- a/READMEs/README.ru-RU.md +++ b/READMEs/README.ru-RU.md @@ -169,7 +169,7 @@ agentmemory работает с любым агентом, поддержива Cursor
Cursor
-MCP-сервер +нативный плагин + MCP Gemini CLI
@@ -657,7 +657,8 @@ npx skills add rohitg00/agentmemory -y -a '*' # install to every installed age | Агент | Файл конфигурации | Заметки | |---|---|---| -| **Cursor** | `~/.cursor/mcp.json` | Добавить в `mcpServers`. Также доступен deeplink в один клик на сайте. | +| **Cursor (только MCP)** | `~/.cursor/mcp.json` | Добавить в `mcpServers`, или `agentmemory connect cursor`. Также доступен deeplink в один клик на сайте. | +| **Cursor (полный плагин)** | `.cursor-plugin/` | Карточка в Cursor Marketplace (заявка на рассмотрении) или Cursor Settings → Plugins → локальный checkout. Регистрирует 7 hooks автозахвата (sessionStart, beforeSubmitPrompt, preToolUse, postToolUse, postToolUseFailure, stop, sessionEnd) + 17 skills + MCP-сервер; `AGENTMEMORY_URL` / `AGENTMEMORY_SECRET` управляются в панели плагинов Cursor. Работает в Cursor IDE и CLI `cursor-agent`; промпты CLI в режиме print дозаписываются из транскрипта при завершении сессии. | | **Claude Desktop** | `claude_desktop_config.json` (Application Support) | Добавить в `mcpServers`. После правки перезапустить Claude Desktop. | | **Cline / Roo Code / Kilo Code** | Настройки MCP в Cline (Settings UI → MCP Servers → Edit) | Тот же блок `mcpServers`. | | **Windsurf** | `~/.codeium/windsurf/mcp_config.json` | Тот же блок `mcpServers`. | diff --git a/READMEs/README.tr-TR.md b/READMEs/README.tr-TR.md index a07eb0fb9..8f58d5a86 100644 --- a/READMEs/README.tr-TR.md +++ b/READMEs/README.tr-TR.md @@ -169,7 +169,7 @@ agentmemory; hook'ları, MCP'yi veya REST API'yi destekleyen her ajanla çalış Cursor
Cursor
-MCP sunucusu +yerel eklenti + MCP Gemini CLI
@@ -655,7 +655,8 @@ agentmemory girdisi, `mcpServers` şeklini kullanan her host'ta (Cursor, Claude | Ajan | Yapılandırma dosyası | Notlar | |---|---|---| -| **Cursor** | `~/.cursor/mcp.json` | `mcpServers` içine birleştirin. Web sitesinde tek tıklamayla deeplink de mevcut. | +| **Cursor (yalnız MCP)** | `~/.cursor/mcp.json` | `mcpServers` içine birleştirin, veya `agentmemory connect cursor`. Web sitesinde tek tıklamayla deeplink de mevcut. | +| **Cursor (tam eklenti)** | `.cursor-plugin/` | Cursor Marketplace kaydı (gönderim incelemede) veya Cursor Settings → Plugins → yerel checkout. 7 otomatik yakalama hook'u (sessionStart, beforeSubmitPrompt, preToolUse, postToolUse, postToolUseFailure, stop, sessionEnd) + 17 skill + MCP sunucusunu kaydeder; `AGENTMEMORY_URL` / `AGENTMEMORY_SECRET` Cursor eklenti panosunda yönetilir. Cursor IDE ve `cursor-agent` CLI'de çalışır; CLI print modundaki prompt'lar oturum sonunda transcript'ten geri doldurulur. | | **Claude Desktop** | `claude_desktop_config.json` (Application Support) | `mcpServers` içine birleştirin. Düzenlemeden sonra Claude Desktop'ı yeniden başlatın. | | **Cline / Roo Code / Kilo Code** | Cline MCP ayarları (Settings UI → MCP Servers → Edit) | Aynı `mcpServers` bloğu. | | **Windsurf** | `~/.codeium/windsurf/mcp_config.json` | Aynı `mcpServers` bloğu. | diff --git a/READMEs/README.zh-CN.md b/READMEs/README.zh-CN.md index ba09e75f8..e6926589c 100644 --- a/READMEs/README.zh-CN.md +++ b/READMEs/README.zh-CN.md @@ -169,7 +169,7 @@ agentmemory 兼容任何支持 hooks、MCP 或 REST API 的代理。所有代理 Cursor
Cursor
-MCP 服务器 +原生插件 + MCP Gemini CLI
@@ -656,7 +656,8 @@ npx skills add rohitg00/agentmemory -y -a '*' # 安装到每个已安装的代 | 代理 | 配置文件 | 备注 | |---|---|---| -| **Cursor** | `~/.cursor/mcp.json` | 合并到 `mcpServers`。网站上也提供一键深链。 | +| **Cursor(仅 MCP)** | `~/.cursor/mcp.json` | 合并到 `mcpServers`,或 `agentmemory connect cursor`。网站上也提供一键深链。 | +| **Cursor(完整插件)** | `.cursor-plugin/` | Cursor Marketplace 条目(提交审核中)或 Cursor Settings → Plugins → 本地 checkout。注册 7 个自动捕获 hooks(sessionStart, beforeSubmitPrompt, preToolUse, postToolUse, postToolUseFailure, stop, sessionEnd)+ 17 个 skills + MCP 服务器;`AGENTMEMORY_URL` / `AGENTMEMORY_SECRET` 在 Cursor 插件面板中管理。Cursor IDE 和 `cursor-agent` CLI 均可用;CLI print 模式的提示词会在会话结束时从 transcript 回填。 | | **Claude Desktop** | `claude_desktop_config.json` (Application Support) | 合并到 `mcpServers`。编辑后重启 Claude Desktop。 | | **Cline / Roo Code / Kilo Code** | Cline MCP 设置 (设置 UI → MCP Servers → Edit) | 同样的 `mcpServers` 块。 | | **Windsurf** | `~/.codeium/windsurf/mcp_config.json` | 同样的 `mcpServers` 块。 | diff --git a/READMEs/README.zh-TW.md b/READMEs/README.zh-TW.md index c8f7adbb0..e9f4f2a0c 100644 --- a/READMEs/README.zh-TW.md +++ b/READMEs/README.zh-TW.md @@ -169,7 +169,7 @@ agentmemory 相容任何支援 hooks、MCP 或 REST API 的代理。所有代理 Cursor
Cursor
-MCP 伺服器 +原生外掛 + MCP Gemini CLI
@@ -656,7 +656,8 @@ npx skills add rohitg00/agentmemory -y -a '*' # 安裝到每個已安裝的代 | 代理 | 設定檔 | 備註 | |---|---|---| -| **Cursor** | `~/.cursor/mcp.json` | 合併到 `mcpServers`。網站上也提供一鍵深層連結。 | +| **Cursor(僅 MCP)** | `~/.cursor/mcp.json` | 合併到 `mcpServers`,或 `agentmemory connect cursor`。網站上也提供一鍵深層連結。 | +| **Cursor(完整外掛)** | `.cursor-plugin/` | Cursor Marketplace 條目(提交審核中)或 Cursor Settings → Plugins → 本地 checkout。註冊 7 個自動擷取 hooks(sessionStart, beforeSubmitPrompt, preToolUse, postToolUse, postToolUseFailure, stop, sessionEnd)+ 17 個 skills + MCP 伺服器;`AGENTMEMORY_URL` / `AGENTMEMORY_SECRET` 在 Cursor 外掛面板中管理。Cursor IDE 與 `cursor-agent` CLI 皆可用;CLI print 模式的提示詞會在工作階段結束時從 transcript 回填。 | | **Claude Desktop** | `claude_desktop_config.json`(Application Support) | 合併到 `mcpServers`。編輯後重新啟動 Claude Desktop。 | | **Cline / Roo Code / Kilo Code** | Cline MCP 設定(設定 UI → MCP Servers → Edit) | 同樣的 `mcpServers` 區塊。 | | **Windsurf** | `~/.codeium/windsurf/mcp_config.json` | 同樣的 `mcpServers` 區塊。 | From 7bee58c6ce649c18229f4716a0a108feec595679 Mon Sep 17 00:00:00 2001 From: Rohit Ghumare Date: Sun, 16 Aug 2026 13:11:31 +0100 Subject: [PATCH 5/9] fix: cursor native-plugin card, broken agent logos --- website/components/Agents.tsx | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/website/components/Agents.tsx b/website/components/Agents.tsx index 9ab4c80d9..afb8e2a9c 100644 --- a/website/components/Agents.tsx +++ b/website/components/Agents.tsx @@ -75,14 +75,14 @@ const FEATURED: Agent[] = [ sub: "NATIVE PLUGIN", }, { - id: "openhuman", - name: "OpenHuman", - from: "tinyhumansai", - logo: "https://raw.githubusercontent.com/tinyhumansai/openhuman/main/app/src-tauri/icons/128x128.png", - accent: "#9b5cf6", - href: "https://github.com/tinyhumansai/openhuman", - pitch: "Native Memory trait backend (Rust)", - sub: "NATIVE BACKEND", + id: "cursor", + name: "Cursor", + from: "Anysphere", + logo: "https://svgl.app/library/cursor_dark.svg", + accent: "#4F8BF7", + href: "https://github.com/rohitg00/agentmemory/tree/main/.cursor-plugin", + pitch: "7 hooks + 17 skills + MCP · IDE and CLI", + sub: "NATIVE PLUGIN", }, { id: "opencode", @@ -106,12 +106,12 @@ const MARQUEE: Agent[] = [ href: "https://claude.ai/download", }, { - id: "cursor", - name: "Cursor", - from: "Anysphere", - logo: "https://svgl.app/library/cursor_dark.svg", - accent: "#000000", - href: "https://cursor.com", + id: "openhuman", + name: "OpenHuman", + from: "tinyhumansai", + logo: "https://raw.githubusercontent.com/tinyhumansai/openhuman/main/app/src-tauri/icons/128x128.png", + accent: "#9b5cf6", + href: "https://github.com/tinyhumansai/openhuman", }, { id: "warp", @@ -125,7 +125,7 @@ const MARQUEE: Agent[] = [ id: "continue", name: "Continue", from: "Continue.dev", - logo: "https://continue.dev/icon.svg", + logo: "https://github.com/continuedev.png", accent: "#000000", href: "https://continue.dev", }, @@ -229,7 +229,7 @@ const MARQUEE: Agent[] = [ id: "windsurf", name: "Windsurf", from: "Cognition", - logo: "https://svgl.app/library/windsurf-dark.svg", + logo: "https://github.com/Exafunction.png", accent: "#00A699", href: "https://windsurf.com", }, @@ -302,7 +302,7 @@ export function Agents() {

Native plugins for Claude Code, Copilot CLI, Codex CLI, OpenClaw, - Hermes, pi, and OpenHuman. OpenCode gets a plugin that attributes + Hermes, pi, and Cursor. OpenCode gets a plugin that attributes each session to its own project. Every other MCP client gets it for free. `agentmemory connect <agent>` auto-wires them all.

From d3196bf9b2a4c1817c0ebc81e925315e88e267e8 Mon Sep 17 00:00:00 2001 From: Rohit Ghumare Date: Sun, 16 Aug 2026 13:13:19 +0100 Subject: [PATCH 6/9] chore: sync openclaw and hermes plugin manifest versions --- integrations/hermes/plugin.yaml | 2 +- integrations/openclaw/openclaw.plugin.json | 2 +- integrations/openclaw/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/integrations/hermes/plugin.yaml b/integrations/hermes/plugin.yaml index 9ea5cb989..4d647c8a8 100644 --- a/integrations/hermes/plugin.yaml +++ b/integrations/hermes/plugin.yaml @@ -1,5 +1,5 @@ name: agentmemory -version: 0.8.0 +version: 0.9.29 description: "Persistent cross-session memory for Hermes Agent via agentmemory. 95.2% retrieval accuracy on LongMemEval." author: "Rohit Ghumare" homepage: "https://github.com/rohitg00/agentmemory" diff --git a/integrations/openclaw/openclaw.plugin.json b/integrations/openclaw/openclaw.plugin.json index 9f154384d..aa990ccdf 100644 --- a/integrations/openclaw/openclaw.plugin.json +++ b/integrations/openclaw/openclaw.plugin.json @@ -3,7 +3,7 @@ "kind": "memory", "name": "agentmemory", "description": "Persistent cross-session memory for OpenClaw via agentmemory.", - "version": "0.9.4", + "version": "0.9.29", "configSchema": { "type": "object", "additionalProperties": false, diff --git a/integrations/openclaw/package.json b/integrations/openclaw/package.json index c671f5d5b..4a2d25017 100644 --- a/integrations/openclaw/package.json +++ b/integrations/openclaw/package.json @@ -1,6 +1,6 @@ { "name": "agentmemory", - "version": "0.9.4", + "version": "0.9.29", "type": "module", "openclaw": { "extensions": [ From 5e740c755815fbd3d6a362655fe8cfc7bd9c8751 Mon Sep 17 00:00:00 2001 From: Rohit Ghumare Date: Sun, 16 Aug 2026 13:22:11 +0100 Subject: [PATCH 7/9] docs: openclaw hook permission and hermes tool count --- integrations/hermes/README.md | 6 +++--- integrations/openclaw/README.md | 11 +++++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/integrations/hermes/README.md b/integrations/hermes/README.md index ba06c105c..bedd12aa8 100644 --- a/integrations/hermes/README.md +++ b/integrations/hermes/README.md @@ -13,7 +13,7 @@

- 43 MCP tools + 54 MCP tools 6 lifecycle hooks 95.2% R@5 Self-hosted @@ -30,7 +30,7 @@ Install agentmemory for Hermes. Run `npx @agentmemory/agentmemory` in a separate terminal to start the memory server on localhost:3111. Then add this to `~/.hermes/config.yaml` so Hermes can use agentmemory as -an MCP server with all 43 memory tools: +an MCP server with all 54 memory tools: mcp_servers: agentmemory: @@ -70,7 +70,7 @@ memory: provider: agentmemory ``` -This gives Hermes access to all 43 MCP tools and enables the agentmemory memory provider. Start the server separately: +This gives Hermes access to all 54 MCP tools and enables the agentmemory memory provider. Start the server separately: ```bash npx @agentmemory/agentmemory diff --git a/integrations/openclaw/README.md b/integrations/openclaw/README.md index 1fe774ae8..29c3a682e 100644 --- a/integrations/openclaw/README.md +++ b/integrations/openclaw/README.md @@ -128,6 +128,17 @@ What the plugin does: - claims the `plugins.slots.memory = "agentmemory"` slot via `api.registerMemoryCapability({ promptBuilder })` so OpenClaw recognises it as the active memory plugin - recalls relevant long-term memory before the agent starts (via the `before_agent_start` hook) - captures completed conversation turns after the agent finishes (via the `agent_end` hook) + +OpenClaw blocks conversation-reading hooks from non-bundled plugins by default. Allow it once in `openclaw.json` so turn capture works: + +```json +{ + "plugins": { + "allow": ["agentmemory"], + "entries": { "agentmemory": { "hooks": { "allowConversationAccess": true } } } + } +} +``` - shares the same backend with Claude Code, Codex CLI, Gemini CLI, Hermes, pi, and other agents ### Memory runtime (current scope) From 30ef70965cb41b7cf77f661d1c6f8c68da2111d1 Mon Sep 17 00:00:00 2001 From: Rohit Ghumare Date: Sun, 16 Aug 2026 13:23:00 +0100 Subject: [PATCH 8/9] chore: clawhub compat metadata for openclaw plugin --- integrations/openclaw/package.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/integrations/openclaw/package.json b/integrations/openclaw/package.json index 4a2d25017..75232379c 100644 --- a/integrations/openclaw/package.json +++ b/integrations/openclaw/package.json @@ -5,6 +5,12 @@ "openclaw": { "extensions": [ "./plugin.mjs" - ] + ], + "compat": { + "pluginApi": ">=2026.7.1" + }, + "build": { + "openclawVersion": "2026.7.1-2" + } } } From 3aa8c42a546e6d90a35d16dc618e240840bc879c Mon Sep 17 00:00:00 2001 From: Rohit Ghumare Date: Sun, 16 Aug 2026 13:26:34 +0100 Subject: [PATCH 9/9] docs: tested openclaw and hermes install rows in readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 07093e65a..84ef0b537 100644 --- a/README.md +++ b/README.md @@ -696,13 +696,13 @@ The agentmemory entry is the **same MCP server block** across every host that us | **Gemini CLI** | `~/.gemini/settings.json` | `gemini mcp add agentmemory npx -y @agentmemory/mcp --scope user` (auto-merges). | | **GitHub Copilot CLI (MCP only)** | `~/.copilot/mcp-config.json` | `agentmemory connect copilot-cli` merges `mcpServers.agentmemory`; Copilot picks it up on next launch or `/mcp`. | | **GitHub Copilot CLI (full plugin)** | Copilot plugin install | `copilot plugin install rohitg00/agentmemory:plugin` for the plugin from the GitHub subdir. | -| **OpenClaw** | OpenClaw MCP config | Same `mcpServers` block, or use the deeper [memory plugin](integrations/openclaw/). | +| **OpenClaw** | OpenClaw MCP config | Same `mcpServers` block. Deeper: `openclaw plugins install ./integrations/openclaw` claims OpenClaw's memory slot (auto-switches from `memory-core`); set `plugins.entries.agentmemory.hooks.allowConversationAccess=true` or turn capture is silently blocked. See [`integrations/openclaw`](integrations/openclaw/). | | **Codex CLI (MCP only)** | `.codex/config.toml` | TOML shape: `codex mcp add agentmemory -- npx -y @agentmemory/mcp`, or add `[mcp_servers.agentmemory]` manually. | | **Codex CLI (full plugin)** | Codex plugin marketplace | `codex plugin marketplace add rohitg00/agentmemory` then `codex plugin add agentmemory@agentmemory`. Registers MCP + 6 lifecycle hooks (SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, PreCompact, Stop) + 17 skills. On Codex Desktop, also run `agentmemory connect codex --with-hooks` until [openai/codex#16430](https://github.com/openai/codex/issues/16430) lands; plugin hooks are currently silent there. | | **OpenCode (MCP only)** | `opencode.json` | Different shape: top-level `mcp` key, command as array: `{"mcp": {"agentmemory": {"type": "local", "command": ["npx", "-y", "@agentmemory/mcp"], "enabled": true}}}`. | | **OpenCode (full plugin)** | `plugin/opencode/` | 22 auto-capture hooks covering session lifecycle, messages, tools, errors. Project attribution is per-session, so one OpenCode process spanning several repositories files each session under its own project. Two slash commands (`/recall`, `/remember`). Copy `plugin/opencode/` into your OpenCode workspace and add the plugin entry to `opencode.json`. See [`plugin/opencode/README.md`](plugin/opencode/README.md) for the full hook table + gap analysis. | | **pi** | `~/.pi/agent/extensions/agentmemory` | `agentmemory connect pi` installs the bundled extension into pi's auto-discovery directory (recall on agent start, capture on agent end, `memory_search` / `memory_save` / `memory_health` tools, `/agentmemory-status`). `/reload` in a running pi picks it up. [`integrations/pi`](integrations/pi/) is also a pi package (`pi install ./integrations/pi` from a checkout). | -| **Hermes Agent** | `~/.hermes/config.yaml` | Use the deeper [memory provider plugin](integrations/hermes/) with `memory.provider: agentmemory`. | +| **Hermes Agent** | `~/.hermes/config.yaml` | `cp -r integrations/hermes ~/.hermes/plugins/agentmemory` + `memory.provider: agentmemory` gives the 6-hook memory provider (prefetch, turn capture, session end, pre-compress, MEMORY.md mirroring, system prompt block). Validate with `hermes plugins doctor` and `hermes memory status`. See [`integrations/hermes`](integrations/hermes/). | | **Qwen Code** | `~/.qwen/settings.json` | `agentmemory connect qwen` writes the standard `mcpServers` block. Hook payload is field-compatible with Claude Code, so the existing 12-hook scripts work without modification; wire them via the `hooks` section in the same `settings.json`. | | **Antigravity** (replaces Gemini CLI) | `mcp_config.json` (in Antigravity's User dir) | `agentmemory connect antigravity` writes the standard `mcpServers` block. macOS: `~/Library/Application Support/Antigravity/User/`. Linux: `~/.config/Antigravity/User/`. Use after the 2026-06-18 Gemini CLI sunset. | | **Antigravity CLI** (`agy`) | `~/.gemini/config/mcp_config.json` | `agentmemory connect antigravity-cli`. The `agy` CLI keeps its own config under `~/.gemini/`, separate from the Antigravity IDE above. Pass `--with-hooks` for native auto-capture via `~/.gemini/config/hooks.json`. |