myagentmemory 0.5.3 → 0.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/dist/cli-spec.js +1 -1
- package/dist/cli.js +30 -23
- package/dist/hooks.js +70 -38
- package/dist/mcp-server.d.ts +2 -0
- package/dist/mcp-server.js +15 -0
- package/dist/plugin-host.d.ts +7 -1
- package/dist/plugin-runtime.d.ts +2 -0
- package/dist/plugin-runtime.js +13 -1
- package/dist/plugin-service.d.ts +1 -5
- package/dist/plugin-service.js +16 -58
- package/dist/upgrade.d.ts +9 -1
- package/dist/upgrade.js +37 -6
- package/docs/official-plugin-bootstrap.md +3 -4
- package/package.json +1 -2
- package/src/cli-spec.ts +1 -1
- package/src/hooks.ts +71 -38
- package/src/plugin-host.ts +7 -1
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ Prefer to have an agent drive the whole thing — install, configure, verify, th
|
|
|
24
24
|
|
|
25
25
|
- AgentMemory injects your decisions, scratchpad, and daily log at session start — no copy-paste, no re-explaining.
|
|
26
26
|
- Repeated corrections become durable memory you can inspect and undo (Pro).
|
|
27
|
-
- Every memory is a plain Markdown file you own.
|
|
27
|
+
- Every memory is a plain Markdown file you own. AgentMemory's services do not receive memory content, session content, queries, or repository paths. Context you ask AgentMemory to return to a coding agent is then subject to that agent or model provider's data handling.
|
|
28
28
|
|
|
29
29
|
AgentMemory does not provide a Python SDK, does not provide a vector database, and does not provide a knowledge graph. It is a local Markdown store with a CLI, agent skills, and optional full-text and semantic search via [qmd](https://github.com/tobi/qmd). See [product boundary](docs/product-boundary.md) for full scope.
|
|
30
30
|
|
|
@@ -36,9 +36,9 @@ AgentMemory does not provide a Python SDK, does not provide a vector database, a
|
|
|
36
36
|
|
|
37
37
|
**Core remembers what you save. Pro learns from what you do.** Core remains free, MIT-licensed, and useful forever. Pro adds three things:
|
|
38
38
|
|
|
39
|
-
- **Remember past sessions** — ask *"what did we decide about auth?"* across Claude Code, Codex, and Cursor.
|
|
39
|
+
- **Remember past sessions** — ask *"what did we decide about auth?"* across Claude Code, Codex, and Pi session history. Cursor can use AgentMemory's skills and hooks, but Cursor transcript ingestion is not currently supported.
|
|
40
40
|
- **Learn from your patterns** — turn repeated corrections into memory you can inspect and undo.
|
|
41
|
-
- **Private by default** — memory and session content index locally.
|
|
41
|
+
- **Private by default** — memory and session content index locally. AgentMemory's services receive only a pseudonymous installation identifier and bounded compatibility metadata, never your memory or session content. Recall results provided locally to a coding agent are subject to that agent or model provider's data handling.
|
|
42
42
|
|
|
43
43
|
Preview what Pro would find in your existing sessions *before* installing anything:
|
|
44
44
|
|
|
@@ -50,7 +50,7 @@ agent-memory learn
|
|
|
50
50
|
agent-memory dashboard
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
-
Pre-install preview: up to 50 local sessions per day. Free installed preview: 20 recalls + 5 learning scans per local day.
|
|
53
|
+
Pre-install preview: up to 50 local sessions per day. Free installed preview: 20 recalls + 5 learning scans per local day. AgentMemory's services do not receive memory, session, query, or repository content; installation sends only a pseudonymous identifier and bounded compatibility metadata. Recall results provided locally to a coding agent are subject to that agent or model provider's data handling. Full detail on [privacy, signing, and installation](docs/official-plugin-bootstrap.md).
|
|
54
54
|
|
|
55
55
|
## Installation
|
|
56
56
|
|
|
@@ -59,7 +59,7 @@ Pre-install preview: up to 50 local sessions per day. Free installed preview: 20
|
|
|
59
59
|
brew tap jayzeng/agentmemory https://github.com/jayzeng/agentmemory
|
|
60
60
|
brew install jayzeng/agentmemory/agent-memory
|
|
61
61
|
|
|
62
|
-
# Install the portable CLI globally (Node.js 20+;
|
|
62
|
+
# Install the portable Core CLI globally (Node.js 20+; Pro session recall requires Node.js 22.13+)
|
|
63
63
|
npm install -g myagentmemory
|
|
64
64
|
|
|
65
65
|
# If corporate TLS inspection requires a private CA, use your organization's CA file:
|
package/dist/cli-spec.js
CHANGED
|
@@ -67,7 +67,7 @@ export const PLUGIN_COMMAND_DESCRIPTIONS = {
|
|
|
67
67
|
install: "authenticate if needed, then install or upgrade the official bundle",
|
|
68
68
|
update: "upgrade an existing official bundle when a compatible release exists",
|
|
69
69
|
uninstall: "remove official plugin executables while preserving user data",
|
|
70
|
-
manage: "open
|
|
70
|
+
manage: "open account management when that future service is available",
|
|
71
71
|
};
|
|
72
72
|
export const WORKER_ACTION_DESCRIPTIONS = {};
|
|
73
73
|
export const SCRATCHPAD_ACTION_DESCRIPTIONS = {
|
package/dist/cli.js
CHANGED
|
@@ -124,10 +124,6 @@ function levenshtein(a, b) {
|
|
|
124
124
|
}
|
|
125
125
|
return prev[b.length];
|
|
126
126
|
}
|
|
127
|
-
// ---------------------------------------------------------------------------
|
|
128
|
-
// Pro plan / cap-exhausted UX
|
|
129
|
-
// ---------------------------------------------------------------------------
|
|
130
|
-
const UPGRADE_URL = "https://agentmemory.paperpilot.me/upgrade";
|
|
131
127
|
function detectCapExhausted(result) {
|
|
132
128
|
if (result.ok !== false)
|
|
133
129
|
return null;
|
|
@@ -194,13 +190,11 @@ function printCapExhaustedBox(command, info) {
|
|
|
194
190
|
"─────────────────────────────────────────────────────────────",
|
|
195
191
|
usedLine,
|
|
196
192
|
"",
|
|
197
|
-
"
|
|
198
|
-
` ${UPGRADE_URL}`,
|
|
193
|
+
" Paid plans are not available yet. Try again after the free-preview allowance resets.",
|
|
199
194
|
"─────────────────────────────────────────────────────────────",
|
|
200
195
|
"",
|
|
201
196
|
];
|
|
202
197
|
console.error(lines.join("\n"));
|
|
203
|
-
openExternalUrl(UPGRADE_URL);
|
|
204
198
|
}
|
|
205
199
|
// Persist the last usage decision to disk so `pro status` can show counters.
|
|
206
200
|
function cacheProUsage(decision) {
|
|
@@ -394,9 +388,9 @@ function printProOverview(installed) {
|
|
|
394
388
|
console.log("Core remembers what you save. Pro learns from what you do.");
|
|
395
389
|
console.log("");
|
|
396
390
|
console.log("AgentMemory Pro:");
|
|
397
|
-
console.log(' Remember past sessions Ask "what did we decide about auth?" across Claude Code, Codex, and
|
|
391
|
+
console.log(' Remember past sessions Ask "what did we decide about auth?" across Claude Code, Codex, and Pi history.');
|
|
398
392
|
console.log(" Learn from your patterns Turn repeated corrections into memory you can inspect and undo.");
|
|
399
|
-
console.log(" Private by default
|
|
393
|
+
console.log(" Private by default AgentMemory services never receive memory or session content.");
|
|
400
394
|
console.log("");
|
|
401
395
|
if (installed) {
|
|
402
396
|
printProUsageCounters();
|
|
@@ -443,7 +437,7 @@ function printPluginResult(result, json, allowBrowser) {
|
|
|
443
437
|
console.log(`AgentMemory Pro${version} has an update available.`);
|
|
444
438
|
break;
|
|
445
439
|
case "uninstalled":
|
|
446
|
-
console.log("AgentMemory Pro executable components were removed. Memory and
|
|
440
|
+
console.log("AgentMemory Pro executable components were removed. Memory and local activation state were preserved.");
|
|
447
441
|
break;
|
|
448
442
|
case "not_installed":
|
|
449
443
|
console.log("AgentMemory Pro is not installed.");
|
|
@@ -674,10 +668,15 @@ const STOP_NAG_REASON = "Before stopping: if this session produced a durable fac
|
|
|
674
668
|
"ignore this and stop normally.";
|
|
675
669
|
/**
|
|
676
670
|
* Stop hook handler — fires at the end of every assistant turn (not once per
|
|
677
|
-
* session).
|
|
678
|
-
* to nudge a memory-write check without being
|
|
679
|
-
*
|
|
680
|
-
*
|
|
671
|
+
* session). Continues the conversation at most once every STOP_NAG_INTERVAL
|
|
672
|
+
* turns per session_id to nudge a memory-write check without being
|
|
673
|
+
* disruptive. Uses `hookSpecificOutput.additionalContext` rather than
|
|
674
|
+
* `decision: "block"` — functionally identical (both go through the same
|
|
675
|
+
* `stop_hook_active` re-entry check and Claude Code's loop-protection cap),
|
|
676
|
+
* but additionalContext renders as "Stop hook feedback" in the transcript
|
|
677
|
+
* instead of the alarming-looking "Stop hook error". Always allows the stop
|
|
678
|
+
* (empty stdout) on missing session_id, `stop_hook_active` (Claude Code's own
|
|
679
|
+
* re-entrancy signal — never nag twice in a row), or any internal error.
|
|
681
680
|
*/
|
|
682
681
|
async function cmdStop(_flags) {
|
|
683
682
|
const TIMEOUT_MS = 3_000;
|
|
@@ -695,7 +694,9 @@ async function cmdStop(_flags) {
|
|
|
695
694
|
if (!sessionId || payload?.stop_hook_active === true)
|
|
696
695
|
return;
|
|
697
696
|
if (shouldNagOnStop(sessionId, Date.now())) {
|
|
698
|
-
process.stdout.write(JSON.stringify({
|
|
697
|
+
process.stdout.write(JSON.stringify({
|
|
698
|
+
hookSpecificOutput: { hookEventName: "Stop", additionalContext: STOP_NAG_REASON },
|
|
699
|
+
}));
|
|
699
700
|
}
|
|
700
701
|
})().catch(() => {
|
|
701
702
|
// Any failure in the Stop hook must be swallowed — never trap the user
|
|
@@ -1698,7 +1699,7 @@ async function cmdSetup(flags) {
|
|
|
1698
1699
|
console.log(colorize("The local plugin is live. Feel the magic now:", "green"));
|
|
1699
1700
|
console.log(` ${colorize('agent-memory recall "what did we decide about auth?"', "cyan")} — search past sessions`);
|
|
1700
1701
|
console.log(` ${colorize("agent-memory learn", "cyan")} — surface repeated corrections`);
|
|
1701
|
-
console.log(` ${colorize("agent-memory
|
|
1702
|
+
console.log(` ${colorize("agent-memory index", "cyan")} — refresh the supported local session index`);
|
|
1702
1703
|
console.log(` ${colorize("agent-memory dashboard", "cyan")} — private local dashboard`);
|
|
1703
1704
|
}
|
|
1704
1705
|
else if (skipPlugin) {
|
|
@@ -1852,10 +1853,10 @@ function printProPitch(mode) {
|
|
|
1852
1853
|
else {
|
|
1853
1854
|
console.log(`${colorize("Optional: AgentMemory Pro", "bold")} — ${colorize("memory that learns from your work", "dim")}`);
|
|
1854
1855
|
}
|
|
1855
|
-
console.log(` ${colorize("Recall across sessions", "cyan")} Ask "what did we decide about auth?" across Claude, Codex,
|
|
1856
|
+
console.log(` ${colorize("Recall across sessions", "cyan")} Ask "what did we decide about auth?" across Claude Code, Codex, and Pi history.`);
|
|
1856
1857
|
console.log(` ${colorize("Learn from corrections", "cyan")} Turn repeated fixes into memory you can inspect and undo.`);
|
|
1857
|
-
console.log(` ${colorize("
|
|
1858
|
-
console.log(` ${colorize("Private by default", "cyan")}
|
|
1858
|
+
console.log(` ${colorize("Local session index", "cyan")} Scan supported session history without uploading it to AgentMemory.`);
|
|
1859
|
+
console.log(` ${colorize("Private by default", "cyan")} AgentMemory services never receive memory or session content.`);
|
|
1859
1860
|
console.log(` ${colorize("Included at no cost:", "green")} ${colorize("20 recalls + 5 learning scans per day", "bold")}. Local indexing and dashboard remain free.`);
|
|
1860
1861
|
console.log("");
|
|
1861
1862
|
}
|
|
@@ -2383,9 +2384,10 @@ Usage:
|
|
|
2383
2384
|
agent-memory plugin manage [--no-browser]
|
|
2384
2385
|
|
|
2385
2386
|
The public core remains fully usable without AgentMemory Pro. Install uses a random
|
|
2386
|
-
installation identifier and requires no account or email. The free tier includes
|
|
2387
|
-
20 recalls and 5 learning scans per local day; indexing and the Memory Dashboard
|
|
2388
|
-
remain available.
|
|
2387
|
+
installation identifier and requires no account or email. The free tier includes
|
|
2388
|
+
20 recalls and 5 learning scans per local day; indexing and the Memory Dashboard
|
|
2389
|
+
remain available. AgentMemory services never receive memory or session content;
|
|
2390
|
+
recall results are subject to the coding agent or model provider you invoke.`);
|
|
2389
2391
|
}
|
|
2390
2392
|
function pluginCommandFailure(command, error) {
|
|
2391
2393
|
return {
|
|
@@ -3123,11 +3125,16 @@ async function cmdServe(flags) {
|
|
|
3123
3125
|
server.addTool({ name: tool.name, description: tool.description, inputSchema: tool.inputSchema }, (input) => runtime.runMcpTool(tool.name, input));
|
|
3124
3126
|
}
|
|
3125
3127
|
server.addStartupHook(() => runtime.runMcpStartup());
|
|
3128
|
+
server.addShutdownHook(() => runtime.runMcpShutdown());
|
|
3126
3129
|
}
|
|
3127
3130
|
catch {
|
|
3128
3131
|
// Pro not installed or failed to load — serve with core tools only.
|
|
3129
3132
|
}
|
|
3130
3133
|
await server.start();
|
|
3134
|
+
// Hard backstop: a Pro plugin's fs.watch handles (or any other resource
|
|
3135
|
+
// that keeps the event loop alive) must never prevent this process from
|
|
3136
|
+
// exiting once stdin has closed.
|
|
3137
|
+
process.exit(0);
|
|
3131
3138
|
}
|
|
3132
3139
|
// ---------------------------------------------------------------------------
|
|
3133
3140
|
// Usage
|
|
@@ -3416,7 +3423,7 @@ async function main() {
|
|
|
3416
3423
|
capability: "session",
|
|
3417
3424
|
});
|
|
3418
3425
|
if (decision.state === "exhausted") {
|
|
3419
|
-
console.error(`AgentMemory free session allowance resets in ${formatResetTime(decision.resetAt)}.
|
|
3426
|
+
console.error(`AgentMemory free session allowance resets in ${formatResetTime(decision.resetAt)}. Paid plans are not available yet.`);
|
|
3420
3427
|
}
|
|
3421
3428
|
}
|
|
3422
3429
|
}
|
package/dist/hooks.js
CHANGED
|
@@ -125,7 +125,13 @@ function hasClaudeHookGroup(homeDir, eventKey, command) {
|
|
|
125
125
|
if (!hook || typeof hook !== "object")
|
|
126
126
|
continue;
|
|
127
127
|
const h = hook;
|
|
128
|
-
|
|
128
|
+
// Match by command alone (marker is optional) — legacy entries that
|
|
129
|
+
// predate HOOK_MARKER_JSON already run this exact command, and the
|
|
130
|
+
// command string is specific enough to agent-memory that it's a safe
|
|
131
|
+
// signal on its own. Otherwise a pre-marker install is invisible here,
|
|
132
|
+
// doctor/isHookInstalled falsely report "not installed" for hooks that
|
|
133
|
+
// are actually live, and upsertClaudeHookGroup can't find them to dedupe.
|
|
134
|
+
if (h.command === command)
|
|
129
135
|
return true;
|
|
130
136
|
}
|
|
131
137
|
}
|
|
@@ -330,9 +336,10 @@ function uninstallPiMemoryDelegate(homeDir) {
|
|
|
330
336
|
*/
|
|
331
337
|
function upsertClaudeHookGroup(hooks, eventKey, command) {
|
|
332
338
|
const groups = Array.isArray(hooks[eventKey]) ? [...hooks[eventKey]] : [];
|
|
333
|
-
|
|
334
|
-
for
|
|
335
|
-
|
|
339
|
+
// Locate every agent-memory-owned hook entry — matched by marker, or by
|
|
340
|
+
// exact command string for legacy pre-marker installs — wherever it lives.
|
|
341
|
+
const owned = [];
|
|
342
|
+
for (const group of groups) {
|
|
336
343
|
if (!group || typeof group !== "object")
|
|
337
344
|
continue;
|
|
338
345
|
const g = group;
|
|
@@ -341,44 +348,68 @@ function upsertClaudeHookGroup(hooks, eventKey, command) {
|
|
|
341
348
|
if (!hook || typeof hook !== "object")
|
|
342
349
|
continue;
|
|
343
350
|
const h = hook;
|
|
344
|
-
if (h[HOOK_MARKER_JSON] === true)
|
|
345
|
-
|
|
346
|
-
break;
|
|
347
|
-
}
|
|
351
|
+
if (h[HOOK_MARKER_JSON] === true || h.command === command)
|
|
352
|
+
owned.push({ group: g, hook: h });
|
|
348
353
|
}
|
|
349
354
|
}
|
|
350
|
-
if (
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
for (const idx of dupes.reverse())
|
|
354
|
-
groups.splice(idx, 1);
|
|
355
|
-
const group = groups[keep];
|
|
356
|
-
let changed = dupes.length > 0;
|
|
357
|
-
if ("matcher" in group) {
|
|
358
|
-
delete group.matcher;
|
|
359
|
-
changed = true;
|
|
360
|
-
}
|
|
361
|
-
const list = group.hooks;
|
|
362
|
-
for (const h of list) {
|
|
363
|
-
if (h[HOOK_MARKER_JSON] === true && h.command !== command) {
|
|
364
|
-
h.command = command;
|
|
365
|
-
changed = true;
|
|
366
|
-
}
|
|
367
|
-
}
|
|
355
|
+
if (owned.length === 0) {
|
|
356
|
+
// No existing managed hook anywhere — add a fresh group without a matcher so it fires on all harnesses.
|
|
357
|
+
groups.push({ hooks: [{ type: "command", command, [HOOK_MARKER_JSON]: true }] });
|
|
368
358
|
hooks[eventKey] = groups;
|
|
369
|
-
return { changed, hadManaged:
|
|
370
|
-
}
|
|
371
|
-
//
|
|
372
|
-
|
|
373
|
-
hooks
|
|
374
|
-
|
|
359
|
+
return { changed: true, hadManaged: false };
|
|
360
|
+
}
|
|
361
|
+
// Keep the first owned entry in place — fixed up in place, preserving its
|
|
362
|
+
// identity so a no-op re-install reports unchanged — and remove every
|
|
363
|
+
// other owned entry from its own group's hooks array, whether it's a
|
|
364
|
+
// duplicate in another group or piled up alongside the keeper in the same
|
|
365
|
+
// group. Never delete a whole group, since it could carry unrelated
|
|
366
|
+
// hand-added hooks.
|
|
367
|
+
const keeper = owned[0];
|
|
368
|
+
let changed = false;
|
|
369
|
+
if (keeper.hook.command !== command) {
|
|
370
|
+
keeper.hook.command = command;
|
|
371
|
+
changed = true;
|
|
372
|
+
}
|
|
373
|
+
if (keeper.hook[HOOK_MARKER_JSON] !== true) {
|
|
374
|
+
keeper.hook[HOOK_MARKER_JSON] = true;
|
|
375
|
+
changed = true;
|
|
376
|
+
}
|
|
377
|
+
for (const dupe of owned.slice(1)) {
|
|
378
|
+
const list = dupe.group.hooks;
|
|
379
|
+
const idx = list.indexOf(dupe.hook);
|
|
380
|
+
if (idx !== -1)
|
|
381
|
+
list.splice(idx, 1);
|
|
382
|
+
changed = true;
|
|
383
|
+
}
|
|
384
|
+
// Only clear the keeper's group matcher when that group is exclusively
|
|
385
|
+
// ours (no unrelated hooks left in it after dedup) — never when it also
|
|
386
|
+
// carries an unrelated hand-added hook.
|
|
387
|
+
const keeperList = keeper.group.hooks;
|
|
388
|
+
if (keeperList.length === 1 && "matcher" in keeper.group) {
|
|
389
|
+
delete keeper.group.matcher;
|
|
390
|
+
changed = true;
|
|
391
|
+
}
|
|
392
|
+
// Drop any group left with zero hooks; never drop one that still has
|
|
393
|
+
// unrelated hooks in it.
|
|
394
|
+
const filtered = groups.filter((group) => {
|
|
395
|
+
if (!group || typeof group !== "object")
|
|
396
|
+
return true;
|
|
397
|
+
const g = group;
|
|
398
|
+
return !Array.isArray(g.hooks) || g.hooks.length > 0;
|
|
399
|
+
});
|
|
400
|
+
hooks[eventKey] = filtered;
|
|
401
|
+
return { changed, hadManaged: true };
|
|
375
402
|
}
|
|
376
403
|
/**
|
|
377
404
|
* Remove all agent-memory-managed hook entries for `eventKey`. Used when
|
|
378
405
|
* downgrading from per-turn back to stable (drops UserPromptSubmit).
|
|
406
|
+
* `command`, when given, also matches legacy entries that predate
|
|
407
|
+
* HOOK_MARKER_JSON — the same broadened detection upsertClaudeHookGroup and
|
|
408
|
+
* hasClaudeHookGroup use — so a pre-marker install isn't left behind after a
|
|
409
|
+
* downgrade/uninstall that reports success.
|
|
379
410
|
* Returns true if anything was removed.
|
|
380
411
|
*/
|
|
381
|
-
function removeClaudeHookGroup(hooks, eventKey) {
|
|
412
|
+
function removeClaudeHookGroup(hooks, eventKey, command) {
|
|
382
413
|
const groups = Array.isArray(hooks[eventKey]) ? hooks[eventKey] : [];
|
|
383
414
|
if (groups.length === 0)
|
|
384
415
|
return false;
|
|
@@ -390,7 +421,8 @@ function removeClaudeHookGroup(hooks, eventKey) {
|
|
|
390
421
|
const g = { ...group };
|
|
391
422
|
const list = Array.isArray(g.hooks) ? g.hooks : [];
|
|
392
423
|
const kept = list.filter((h) => {
|
|
393
|
-
const
|
|
424
|
+
const hook = h && typeof h === "object" ? h : null;
|
|
425
|
+
const isOurs = !!hook && (hook[HOOK_MARKER_JSON] === true || (!!command && hook.command === command));
|
|
394
426
|
if (isOurs)
|
|
395
427
|
removed++;
|
|
396
428
|
return !isOurs;
|
|
@@ -426,7 +458,7 @@ function installClaudeCodeHook(homeDir, mode = "per-turn") {
|
|
|
426
458
|
promptHadManaged = prompt.hadManaged;
|
|
427
459
|
}
|
|
428
460
|
else {
|
|
429
|
-
promptChanged = removeClaudeHookGroup(hooks, "UserPromptSubmit");
|
|
461
|
+
promptChanged = removeClaudeHookGroup(hooks, "UserPromptSubmit", userPromptSubmitHookCommand("claude"));
|
|
430
462
|
}
|
|
431
463
|
// Stop backs the write side of memory with a periodic nudge. It is orthogonal
|
|
432
464
|
// to stable/per-turn context injection, so it is installed unconditionally.
|
|
@@ -779,9 +811,9 @@ function uninstallClaudeCodeHook(homeDir) {
|
|
|
779
811
|
}
|
|
780
812
|
const settings = readJsonConfig(settingsPath);
|
|
781
813
|
const hooks = settings.hooks ?? {};
|
|
782
|
-
const sessionRemoved = removeClaudeHookGroup(hooks, "SessionStart");
|
|
783
|
-
const promptRemoved = removeClaudeHookGroup(hooks, "UserPromptSubmit");
|
|
784
|
-
const stopRemoved = removeClaudeHookGroup(hooks, "Stop");
|
|
814
|
+
const sessionRemoved = removeClaudeHookGroup(hooks, "SessionStart", sessionStartHookCommand("claude"));
|
|
815
|
+
const promptRemoved = removeClaudeHookGroup(hooks, "UserPromptSubmit", userPromptSubmitHookCommand("claude"));
|
|
816
|
+
const stopRemoved = removeClaudeHookGroup(hooks, "Stop", stopHookCommand("claude"));
|
|
785
817
|
const legacyPreCompactRemoved = removeClaudeHookGroup(hooks, "PreCompact");
|
|
786
818
|
if (!sessionRemoved && !promptRemoved && !stopRemoved && !legacyPreCompactRemoved) {
|
|
787
819
|
return { key: "claude", label: "Claude Code", installed: false, reason: "not installed" };
|
package/dist/mcp-server.d.ts
CHANGED
|
@@ -17,9 +17,11 @@ export declare class StdioMcpServer {
|
|
|
17
17
|
private readonly version;
|
|
18
18
|
private readonly tools;
|
|
19
19
|
private readonly startupHooks;
|
|
20
|
+
private readonly shutdownHooks;
|
|
20
21
|
constructor(version?: string);
|
|
21
22
|
addTool(definition: McpToolDefinition, handler: McpToolHandler): void;
|
|
22
23
|
addStartupHook(fn: () => void | Promise<void>): void;
|
|
24
|
+
addShutdownHook(fn: () => void | Promise<void>): void;
|
|
23
25
|
start(): Promise<void>;
|
|
24
26
|
private handleMessage;
|
|
25
27
|
private respond;
|
package/dist/mcp-server.js
CHANGED
|
@@ -9,6 +9,7 @@ export class StdioMcpServer {
|
|
|
9
9
|
version;
|
|
10
10
|
tools = new Map();
|
|
11
11
|
startupHooks = [];
|
|
12
|
+
shutdownHooks = [];
|
|
12
13
|
constructor(version = "0.0.0") {
|
|
13
14
|
this.version = version;
|
|
14
15
|
}
|
|
@@ -18,6 +19,9 @@ export class StdioMcpServer {
|
|
|
18
19
|
addStartupHook(fn) {
|
|
19
20
|
this.startupHooks.push(fn);
|
|
20
21
|
}
|
|
22
|
+
addShutdownHook(fn) {
|
|
23
|
+
this.shutdownHooks.push(fn);
|
|
24
|
+
}
|
|
21
25
|
async start() {
|
|
22
26
|
// Run all startup hooks before entering the message loop.
|
|
23
27
|
for (const hook of this.startupHooks)
|
|
@@ -40,6 +44,17 @@ export class StdioMcpServer {
|
|
|
40
44
|
rl.on("close", resolve);
|
|
41
45
|
process.stdin.on("end", resolve);
|
|
42
46
|
});
|
|
47
|
+
// A hook may hold resources (e.g. fs.watch handles) that keep the event
|
|
48
|
+
// loop alive past stdin close — run them, but don't let one broken hook
|
|
49
|
+
// block the others or block process exit.
|
|
50
|
+
for (const hook of this.shutdownHooks) {
|
|
51
|
+
try {
|
|
52
|
+
await hook();
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// Non-fatal — the caller still hard-exits after start() returns.
|
|
56
|
+
}
|
|
57
|
+
}
|
|
43
58
|
}
|
|
44
59
|
handleMessage(msg) {
|
|
45
60
|
const id = msg.id;
|
package/dist/plugin-host.d.ts
CHANGED
|
@@ -147,7 +147,12 @@ export interface PluginMcpToolInputSchema {
|
|
|
147
147
|
export interface PluginMcpToolV1 {
|
|
148
148
|
name: string;
|
|
149
149
|
description: string;
|
|
150
|
-
|
|
150
|
+
/**
|
|
151
|
+
* Capability checked on every invocation. Optional only to keep plugin API 1
|
|
152
|
+
* source-compatible with bundles built before capability-gated MCP tools
|
|
153
|
+
* were introduced; legacy tools load but are denied until updated.
|
|
154
|
+
*/
|
|
155
|
+
requiredCapability?: string;
|
|
151
156
|
inputSchema: PluginMcpToolInputSchema;
|
|
152
157
|
run(input: Record<string, unknown>): unknown | Promise<unknown>;
|
|
153
158
|
}
|
|
@@ -160,6 +165,7 @@ export interface AgentMemoryPluginHostV1 {
|
|
|
160
165
|
registerContextProvider?(provider: PluginContextProviderV1): void;
|
|
161
166
|
registerMcpTool?(tool: PluginMcpToolV1): void;
|
|
162
167
|
registerMcpStartup?(fn: () => void | Promise<void>): void;
|
|
168
|
+
registerMcpShutdown?(fn: () => void | Promise<void>): void;
|
|
163
169
|
getStateDirectory(): string;
|
|
164
170
|
getMemoryDirectory(): string;
|
|
165
171
|
getEntitlement(): Promise<PluginEntitlementStatusV1>;
|
package/dist/plugin-runtime.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export declare class InstalledPluginRuntimeV1 {
|
|
|
15
15
|
private readonly contextProviders;
|
|
16
16
|
private readonly mcpTools;
|
|
17
17
|
private readonly mcpStartupHooks;
|
|
18
|
+
private readonly mcpShutdownHooks;
|
|
18
19
|
private loaded;
|
|
19
20
|
constructor(options: PluginRuntimeOptionsV1);
|
|
20
21
|
load(): Promise<boolean>;
|
|
@@ -44,6 +45,7 @@ export declare class InstalledPluginRuntimeV1 {
|
|
|
44
45
|
*/
|
|
45
46
|
runMcpTool(name: string, input: Record<string, unknown>): Promise<unknown>;
|
|
46
47
|
runMcpStartup(): Promise<void>;
|
|
48
|
+
runMcpShutdown(): Promise<void>;
|
|
47
49
|
private createHost;
|
|
48
50
|
private refreshEntitlement;
|
|
49
51
|
}
|
package/dist/plugin-runtime.js
CHANGED
|
@@ -65,6 +65,7 @@ export class InstalledPluginRuntimeV1 {
|
|
|
65
65
|
contextProviders = [];
|
|
66
66
|
mcpTools = [];
|
|
67
67
|
mcpStartupHooks = [];
|
|
68
|
+
mcpShutdownHooks = [];
|
|
68
69
|
loaded = false;
|
|
69
70
|
constructor(options) {
|
|
70
71
|
this.options = options;
|
|
@@ -200,6 +201,10 @@ export class InstalledPluginRuntimeV1 {
|
|
|
200
201
|
const tool = this.mcpTools.find((candidate) => candidate.name === name);
|
|
201
202
|
if (!tool)
|
|
202
203
|
return { error: `Unknown MCP tool: ${name}` };
|
|
204
|
+
if (!tool.requiredCapability)
|
|
205
|
+
return {
|
|
206
|
+
error: `The ${name} tool was built for an older plugin API and must be updated before it can run`,
|
|
207
|
+
};
|
|
203
208
|
const entitlement = await this.refreshEntitlement();
|
|
204
209
|
if (!isPluginCapabilityEnabled(entitlement, tool.requiredCapability))
|
|
205
210
|
return { error: `Capability ${tool.requiredCapability} is not enabled for the ${name} tool` };
|
|
@@ -209,6 +214,10 @@ export class InstalledPluginRuntimeV1 {
|
|
|
209
214
|
for (const hook of this.mcpStartupHooks)
|
|
210
215
|
await hook();
|
|
211
216
|
}
|
|
217
|
+
async runMcpShutdown() {
|
|
218
|
+
for (const hook of this.mcpShutdownHooks)
|
|
219
|
+
await hook();
|
|
220
|
+
}
|
|
212
221
|
createHost(manifest) {
|
|
213
222
|
const descriptors = new Map(manifest.commands.map((command) => [command.name, command]));
|
|
214
223
|
const stateRoot = path.join(this.store.root, "state");
|
|
@@ -257,7 +266,7 @@ export class InstalledPluginRuntimeV1 {
|
|
|
257
266
|
this.contextProviders.push({ provider, pluginId: manifest.id });
|
|
258
267
|
},
|
|
259
268
|
registerMcpTool: (tool) => {
|
|
260
|
-
if (!(manifest.capabilities ?? []).includes(tool.requiredCapability))
|
|
269
|
+
if (tool.requiredCapability && !(manifest.capabilities ?? []).includes(tool.requiredCapability))
|
|
261
270
|
throw new PluginBootstrapFailure("plugin_mcp_tool_invalid", `Plugin ${manifest.id} registered an MCP tool with an undeclared capability`);
|
|
262
271
|
if (!tool.name || this.mcpTools.some((existing) => existing.name === tool.name))
|
|
263
272
|
throw new PluginBootstrapFailure("plugin_mcp_tool_invalid", `Plugin MCP tool ${tool.name || "(unnamed)"} is invalid or already registered`);
|
|
@@ -266,6 +275,9 @@ export class InstalledPluginRuntimeV1 {
|
|
|
266
275
|
registerMcpStartup: (fn) => {
|
|
267
276
|
this.mcpStartupHooks.push(fn);
|
|
268
277
|
},
|
|
278
|
+
registerMcpShutdown: (fn) => {
|
|
279
|
+
this.mcpShutdownHooks.push(fn);
|
|
280
|
+
},
|
|
269
281
|
getStateDirectory: () => stateDirectory,
|
|
270
282
|
getMemoryDirectory: () => {
|
|
271
283
|
assertPermission(manifest, "memory:read");
|
package/dist/plugin-service.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type PluginAccessDecisionV1, type PluginBootstrapBackendV1, type PluginNextActionV1, type
|
|
1
|
+
import { type PluginAccessDecisionV1, type PluginBootstrapBackendV1, type PluginNextActionV1, type SignedPluginReleaseV1 } from "./plugin-bootstrap.js";
|
|
2
2
|
import { type PluginEntitlementStatusV1 } from "./plugin-host.js";
|
|
3
3
|
interface AgentMemoryServiceBackendOptions {
|
|
4
4
|
root?: string;
|
|
@@ -27,9 +27,6 @@ export declare class AgentMemoryServiceBackend implements PluginBootstrapBackend
|
|
|
27
27
|
channel: string;
|
|
28
28
|
allowAuthentication: boolean;
|
|
29
29
|
}): Promise<PluginAccessDecisionV1>;
|
|
30
|
-
reserveSession(operationId: string): Promise<PluginSessionUsageDecisionV1>;
|
|
31
|
-
commitSession(operationId: string): Promise<PluginSessionUsageDecisionV1>;
|
|
32
|
-
releaseSession(operationId: string): Promise<PluginSessionUsageDecisionV1>;
|
|
33
30
|
listReleases(request: {
|
|
34
31
|
bundleId: string;
|
|
35
32
|
channel: string;
|
|
@@ -43,7 +40,6 @@ export declare class AgentMemoryServiceBackend implements PluginBootstrapBackend
|
|
|
43
40
|
private activationPath;
|
|
44
41
|
private readActivation;
|
|
45
42
|
private writeActivation;
|
|
46
|
-
private sessionUsage;
|
|
47
43
|
private request;
|
|
48
44
|
}
|
|
49
45
|
export declare class TemporaryPluginBackend extends AgentMemoryServiceBackend {
|
package/dist/plugin-service.js
CHANGED
|
@@ -139,8 +139,8 @@ function activationPage(action, error) {
|
|
|
139
139
|
<p class="terminal-note" id="terminal-note">Your terminal will finish setup after activation.</p>
|
|
140
140
|
<details>
|
|
141
141
|
<summary>What’s shared during activation</summary>
|
|
142
|
-
<p>Your email identifies your free daily allowance. The CLI also sends core and bundle versions, platform, architecture, and release channel. The service stores
|
|
143
|
-
<p class="never-sent"><strong>
|
|
142
|
+
<p>Your email identifies your free daily allowance. The CLI also sends core and bundle versions, platform, architecture, and release channel. The service stores bounded activation metadata. Activation records expire after 365 days without use.</p>
|
|
143
|
+
<p class="never-sent"><strong>Not included in AgentMemory's application payload:</strong> memory, session content, queries, repository paths, raw agent session identifiers, IP addresses, or user-agent strings.</p>
|
|
144
144
|
</details>
|
|
145
145
|
</section>
|
|
146
146
|
</main>
|
|
@@ -394,8 +394,6 @@ export class AgentMemoryServiceBackend {
|
|
|
394
394
|
validatePluginEntitlementStatusV1(value.entitlement);
|
|
395
395
|
if (typeof value.artifactGrant !== "string" || !value.artifactGrant)
|
|
396
396
|
throw new PluginBootstrapFailure("service_response_invalid", "The access response omitted its artifact grant");
|
|
397
|
-
if (typeof value.usageCredential !== "string" || !ACTIVATION_CREDENTIAL.test(value.usageCredential))
|
|
398
|
-
throw new PluginBootstrapFailure("service_response_invalid", "The access response omitted its usage credential");
|
|
399
397
|
const recallQuota = value.entitlement.capabilities.recall?.quota;
|
|
400
398
|
const learningQuota = value.entitlement.capabilities.learning?.quota;
|
|
401
399
|
if (value.entitlement.plan !== "free" ||
|
|
@@ -408,18 +406,9 @@ export class AgentMemoryServiceBackend {
|
|
|
408
406
|
value.entitlement.capabilities["session-worker"]?.enabled !== false ||
|
|
409
407
|
value.entitlement.capabilities["web-console"]?.enabled !== true)
|
|
410
408
|
throw new PluginBootstrapFailure("service_response_invalid", "The free preview policy is invalid");
|
|
411
|
-
this.writeActivation(installationId
|
|
409
|
+
this.writeActivation(installationId);
|
|
412
410
|
return { kind: "granted", entitlement: value.entitlement, artifactGrant: value.artifactGrant };
|
|
413
411
|
}
|
|
414
|
-
async reserveSession(operationId) {
|
|
415
|
-
return this.sessionUsage("reserve", operationId);
|
|
416
|
-
}
|
|
417
|
-
async commitSession(operationId) {
|
|
418
|
-
return this.sessionUsage("commit", operationId);
|
|
419
|
-
}
|
|
420
|
-
async releaseSession(operationId) {
|
|
421
|
-
return this.sessionUsage("release", operationId);
|
|
422
|
-
}
|
|
423
412
|
async listReleases(request) {
|
|
424
413
|
const response = await this.request(`${this.apiOrigin}/v1/plugin/releases`, {
|
|
425
414
|
headers: { Authorization: `Bearer ${request.artifactGrant}` },
|
|
@@ -474,28 +463,28 @@ export class AgentMemoryServiceBackend {
|
|
|
474
463
|
if (!stat.isFile() || stat.isSymbolicLink() || (process.platform !== "win32" && (stat.mode & 0o077) !== 0))
|
|
475
464
|
return null;
|
|
476
465
|
const value = JSON.parse(fs.readFileSync(activationPath, "utf-8"));
|
|
477
|
-
if (value.
|
|
478
|
-
typeof value.installationId !== "string" ||
|
|
466
|
+
if (typeof value.installationId !== "string" ||
|
|
479
467
|
!/^am_install_[A-Za-z0-9_-]{32}$/.test(value.installationId) ||
|
|
480
468
|
!Number.isFinite(Date.parse(value.activatedAt)) ||
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
469
|
+
(value.schemaVersion !== 4 &&
|
|
470
|
+
(value.schemaVersion !== 3 ||
|
|
471
|
+
typeof value.usageCredential !== "string" ||
|
|
472
|
+
!ACTIVATION_CREDENTIAL.test(value.usageCredential) ||
|
|
473
|
+
!Number.isSafeInteger(value.dailySessionLimit))))
|
|
485
474
|
return null;
|
|
486
|
-
return
|
|
475
|
+
return {
|
|
476
|
+
schemaVersion: 4,
|
|
477
|
+
installationId: value.installationId,
|
|
478
|
+
activatedAt: value.activatedAt,
|
|
479
|
+
};
|
|
487
480
|
}
|
|
488
481
|
catch {
|
|
489
482
|
return null;
|
|
490
483
|
}
|
|
491
484
|
}
|
|
492
|
-
writeActivation(installationId
|
|
485
|
+
writeActivation(installationId) {
|
|
493
486
|
if (!/^am_install_[A-Za-z0-9_-]{32}$/.test(installationId))
|
|
494
487
|
throw new PluginBootstrapFailure("activation_failed", "The installation identifier is invalid");
|
|
495
|
-
if (!ACTIVATION_CREDENTIAL.test(usageCredential))
|
|
496
|
-
throw new PluginBootstrapFailure("activation_failed", "The activation credential is invalid");
|
|
497
|
-
if (!Number.isSafeInteger(dailySessionLimit) || dailySessionLimit <= 0 || dailySessionLimit > 10_000)
|
|
498
|
-
throw new PluginBootstrapFailure("activation_failed", "The free session allowance is invalid");
|
|
499
488
|
const target = this.activationPath();
|
|
500
489
|
fs.mkdirSync(this.root, { recursive: true, mode: 0o700 });
|
|
501
490
|
const rootStat = fs.lstatSync(this.root);
|
|
@@ -509,43 +498,12 @@ export class AgentMemoryServiceBackend {
|
|
|
509
498
|
throw new PluginBootstrapFailure("activation_path_invalid", "The plugin activation directory is unsafe");
|
|
510
499
|
const temporary = `${target}.tmp-${process.pid}-${randomUUID()}`;
|
|
511
500
|
fs.writeFileSync(temporary, `${JSON.stringify({
|
|
512
|
-
schemaVersion:
|
|
501
|
+
schemaVersion: 4,
|
|
513
502
|
installationId,
|
|
514
503
|
activatedAt: new Date().toISOString(),
|
|
515
|
-
usageCredential,
|
|
516
|
-
dailySessionLimit,
|
|
517
504
|
}, null, 2)}\n`, { mode: 0o600, flag: "wx" });
|
|
518
505
|
fs.renameSync(temporary, target);
|
|
519
506
|
}
|
|
520
|
-
async sessionUsage(action, operationId) {
|
|
521
|
-
const activation = this.readActivation();
|
|
522
|
-
if (!activation)
|
|
523
|
-
throw new PluginBootstrapFailure("auth_required", "Run plugin install to activate AgentMemory");
|
|
524
|
-
const response = await this.request(`${this.apiOrigin}/v1/plugin/sessions/${action}`, {
|
|
525
|
-
method: "POST",
|
|
526
|
-
headers: {
|
|
527
|
-
Authorization: `Bearer ${activation.usageCredential}`,
|
|
528
|
-
"Content-Type": "application/json",
|
|
529
|
-
},
|
|
530
|
-
body: JSON.stringify({ schemaVersion: 1, operationId }),
|
|
531
|
-
});
|
|
532
|
-
const value = (await readJson(response));
|
|
533
|
-
const decision = value.decision;
|
|
534
|
-
if (!decision ||
|
|
535
|
-
typeof decision.allowed !== "boolean" ||
|
|
536
|
-
!["reserved", "committed", "released", "exhausted", "missing"].includes(String(decision.state)) ||
|
|
537
|
-
!Number.isSafeInteger(decision.limit) ||
|
|
538
|
-
Number(decision.limit) <= 0 ||
|
|
539
|
-
!Number.isSafeInteger(decision.used) ||
|
|
540
|
-
Number(decision.used) < 0 ||
|
|
541
|
-
!Number.isSafeInteger(decision.remaining) ||
|
|
542
|
-
Number(decision.remaining) < 0 ||
|
|
543
|
-
typeof decision.resetAt !== "string" ||
|
|
544
|
-
!Number.isFinite(Date.parse(decision.resetAt)) ||
|
|
545
|
-
typeof decision.idempotent !== "boolean")
|
|
546
|
-
throw new PluginBootstrapFailure("service_response_invalid", "The session usage response is invalid");
|
|
547
|
-
return decision;
|
|
548
|
-
}
|
|
549
507
|
async request(url, init = {}) {
|
|
550
508
|
let response;
|
|
551
509
|
try {
|
package/dist/upgrade.d.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* retried before the next cache refresh, and never thrown.
|
|
16
16
|
*/
|
|
17
17
|
import { type SpawnOptions } from "node:child_process";
|
|
18
|
-
export type InstallManager = "bun" | "npm" | "pnpm" | "yarn" | "unknown";
|
|
18
|
+
export type InstallManager = "bun" | "homebrew" | "npm" | "pnpm" | "yarn" | "unknown";
|
|
19
19
|
export interface InstallMethod {
|
|
20
20
|
manager: InstallManager;
|
|
21
21
|
global: boolean;
|
|
@@ -77,6 +77,14 @@ export declare function readUpgradePolicy(): UpgradePolicy & {
|
|
|
77
77
|
};
|
|
78
78
|
/** Atomically persist the auto-upgrade policy. Merges with whatever is already on disk. */
|
|
79
79
|
export declare function writeUpgradePolicy(patch: Partial<UpgradePolicy>): UpgradePolicy;
|
|
80
|
+
export declare function resolveSelfLaunch(input: {
|
|
81
|
+
execPath: string;
|
|
82
|
+
scriptCandidate: string | undefined;
|
|
83
|
+
fileExists: (candidate: string) => boolean;
|
|
84
|
+
}): {
|
|
85
|
+
command: string;
|
|
86
|
+
args: string[];
|
|
87
|
+
};
|
|
80
88
|
/**
|
|
81
89
|
* Best-effort detection of how `myagentmemory` was installed. Path signatures
|
|
82
90
|
* are heuristic but cover the common managers. On no match we fall back to
|
package/dist/upgrade.js
CHANGED
|
@@ -167,12 +167,34 @@ export function writeUpgradePolicy(patch) {
|
|
|
167
167
|
// ---------------------------------------------------------------------------
|
|
168
168
|
// Install-method detection
|
|
169
169
|
// ---------------------------------------------------------------------------
|
|
170
|
+
export function resolveSelfLaunch(input) {
|
|
171
|
+
const scriptCandidate = input.scriptCandidate;
|
|
172
|
+
const isRealScript = typeof scriptCandidate === "string" &&
|
|
173
|
+
scriptCandidate.length > 0 &&
|
|
174
|
+
!scriptCandidate.startsWith("/$bunfs/") &&
|
|
175
|
+
input.fileExists(scriptCandidate);
|
|
176
|
+
return isRealScript ? { command: input.execPath, args: [scriptCandidate] } : { command: input.execPath, args: [] };
|
|
177
|
+
}
|
|
178
|
+
function currentSelfLaunch() {
|
|
179
|
+
return resolveSelfLaunch({
|
|
180
|
+
execPath: process.execPath,
|
|
181
|
+
scriptCandidate: process.argv[1],
|
|
182
|
+
fileExists: fs.existsSync,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
170
185
|
function selfInstallPath() {
|
|
186
|
+
const launch = currentSelfLaunch();
|
|
187
|
+
const candidate = launch.args[0] ?? launch.command;
|
|
171
188
|
try {
|
|
172
|
-
return
|
|
189
|
+
return fs.realpathSync(candidate);
|
|
173
190
|
}
|
|
174
191
|
catch {
|
|
175
|
-
|
|
192
|
+
try {
|
|
193
|
+
return url.fileURLToPath(import.meta.url);
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
return candidate;
|
|
197
|
+
}
|
|
176
198
|
}
|
|
177
199
|
}
|
|
178
200
|
/**
|
|
@@ -184,6 +206,16 @@ export function detectInstallMethod(location = selfInstallPath()) {
|
|
|
184
206
|
const normalized = location.replace(/\\/g, "/");
|
|
185
207
|
const home = os.homedir().replace(/\\/g, "/");
|
|
186
208
|
const pkg = `${NPM_PACKAGE_NAME}@latest`;
|
|
209
|
+
// Compiled CLI installed by the official Homebrew formula. Resolve symlinks
|
|
210
|
+
// before detection so /opt/homebrew/bin/agent-memory reaches its Cellar path.
|
|
211
|
+
if (normalized.includes("/Cellar/agent-memory/")) {
|
|
212
|
+
return {
|
|
213
|
+
manager: "homebrew",
|
|
214
|
+
global: true,
|
|
215
|
+
origin: location,
|
|
216
|
+
command: ["brew", "upgrade", "jayzeng/agentmemory/agent-memory"],
|
|
217
|
+
};
|
|
218
|
+
}
|
|
187
219
|
// bun global install
|
|
188
220
|
if (normalized.includes("/.bun/install/global/") || normalized.includes("/bun/install/global/")) {
|
|
189
221
|
return { manager: "bun", global: true, origin: location, command: ["bun", "add", "-g", pkg] };
|
|
@@ -241,11 +273,10 @@ export function runInstaller(method, opts = {}) {
|
|
|
241
273
|
*/
|
|
242
274
|
export function refreshUpgradeCacheBackground() {
|
|
243
275
|
try {
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
if (!binary || !script)
|
|
276
|
+
const launch = currentSelfLaunch();
|
|
277
|
+
if (!launch.command)
|
|
247
278
|
return;
|
|
248
|
-
const child = spawn(
|
|
279
|
+
const child = spawn(launch.command, [...launch.args, "upgrade", "--background", "--refresh", "--quiet", "--json"], {
|
|
249
280
|
detached: true,
|
|
250
281
|
stdio: "ignore",
|
|
251
282
|
env: { ...process.env, AGENT_MEMORY_UPGRADE_BACKGROUND: "1" },
|
|
@@ -89,7 +89,7 @@ The `pro` namespace is the user-facing surface. The `plugin` namespace remains s
|
|
|
89
89
|
- `status` is read-only. It reports the installed bundle, selected channel, compatibility, entitlement state, and update availability.
|
|
90
90
|
- `install` authenticates when necessary, then installs, upgrades, or reports current state.
|
|
91
91
|
- `update` requires an existing installation and never starts a new purchase implicitly.
|
|
92
|
-
- `uninstall` removes executable plugin material and the active receipt. It preserves core memory, plugin state, and the permission-restricted activation
|
|
92
|
+
- `uninstall` removes executable plugin material and the active receipt. It preserves core memory, plugin state, and the permission-restricted local activation record.
|
|
93
93
|
- `manage` remains unavailable until authenticated account and billing management exists.
|
|
94
94
|
|
|
95
95
|
Installed plugins contribute top-level commands including `recall` and `learn`; `dashboard` is a product-facing alias for the lower-level `web` command. Bootstrap command names are reserved by the core and cannot be replaced by a plugin.
|
|
@@ -172,7 +172,6 @@ An Enterprise administrator may pre-provision an organization entitlement or man
|
|
|
172
172
|
The service exposes:
|
|
173
173
|
|
|
174
174
|
- `POST /v1/plugin/access` for an anonymous free-preview policy, compatibility credential, and short-lived artifact grant;
|
|
175
|
-
- `POST /v1/plugin/sessions/reserve|commit|release` for migration compatibility with activation-v2 clients;
|
|
176
175
|
- `GET /v1/plugin/releases` for an Ed25519-signed release selected from the private R2 catalog;
|
|
177
176
|
- `GET|HEAD /v1/artifacts/download` for the exact content-addressed object authorized by the bearer grant.
|
|
178
177
|
|
|
@@ -191,7 +190,7 @@ The bootstrap may send only:
|
|
|
191
190
|
- core version, plugin-host API version, platform, and architecture;
|
|
192
191
|
- requested bundle ID, installed bundle version, and release channel;
|
|
193
192
|
- a pseudonymous license or organization identifier;
|
|
194
|
-
- protocol nonces
|
|
193
|
+
- protocol nonces and authentication material required for the request.
|
|
195
194
|
|
|
196
195
|
It must never send memory contents, search queries, session contents, raw agent session identifiers, working-directory names, repository names, filesystem paths, or qmd data. The bounded allowance counter is authorization state, not general product telemetry.
|
|
197
196
|
|
|
@@ -267,7 +266,7 @@ An install or upgrade must:
|
|
|
267
266
|
|
|
268
267
|
Failure before activation leaves the previous version active. Failure immediately after activation restores the previous receipt. Concurrent installers do not interleave. The core never invokes package-manager lifecycle scripts or elevates privileges.
|
|
269
268
|
|
|
270
|
-
Uninstall removes executable versions, the active receipt, contributed skills, and managed hooks. It does not remove `MEMORY.md`, daily logs, topics, scratchpad items, source session logs, plugin-created review data, or
|
|
269
|
+
Uninstall removes executable versions, the active receipt, contributed skills, and managed hooks. It does not remove `MEMORY.md`, daily logs, topics, scratchpad items, source session logs, plugin-created review data, or local activation state. The top-level `agent-memory uninstall` command composes this with hook/skill/MCP/completion removal in one step; its explicit `--data` flag additionally deletes the memory directory and the entire plugin install root (bundles, receipts, and activation state) once the user opts in and confirms.
|
|
271
270
|
|
|
272
271
|
## Plugin host API v1
|
|
273
272
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "myagentmemory",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.5",
|
|
4
4
|
"description": "agentmemory (agent-memory) is persistent memory for coding agents (Claude Code, OpenAI Codex, Cursor, Agent) with qmd-powered semantic search across daily logs, long-term memory, and scratchpad",
|
|
5
5
|
"main": "./dist/core.js",
|
|
6
6
|
"types": "./dist/core.d.ts",
|
|
@@ -93,7 +93,6 @@
|
|
|
93
93
|
"dist/plugin-runtime.js",
|
|
94
94
|
"dist/plugin-service.d.ts",
|
|
95
95
|
"dist/plugin-service.js",
|
|
96
|
-
|
|
97
96
|
"dist/mcp-server.d.ts",
|
|
98
97
|
"dist/mcp-server.js",
|
|
99
98
|
"dist/upgrade.d.ts",
|
package/src/cli-spec.ts
CHANGED
|
@@ -82,7 +82,7 @@ export const PLUGIN_COMMAND_DESCRIPTIONS: Record<(typeof PLUGIN_COMMANDS)[number
|
|
|
82
82
|
install: "authenticate if needed, then install or upgrade the official bundle",
|
|
83
83
|
update: "upgrade an existing official bundle when a compatible release exists",
|
|
84
84
|
uninstall: "remove official plugin executables while preserving user data",
|
|
85
|
-
manage: "open
|
|
85
|
+
manage: "open account management when that future service is available",
|
|
86
86
|
};
|
|
87
87
|
|
|
88
88
|
export const WORKER_ACTION_DESCRIPTIONS: Record<(typeof WORKER_ACTIONS)[number], string> = {};
|
package/src/hooks.ts
CHANGED
|
@@ -156,7 +156,13 @@ function hasClaudeHookGroup(homeDir: string, eventKey: string, command: string):
|
|
|
156
156
|
for (const hook of list) {
|
|
157
157
|
if (!hook || typeof hook !== "object") continue;
|
|
158
158
|
const h = hook as Record<string, unknown>;
|
|
159
|
-
|
|
159
|
+
// Match by command alone (marker is optional) — legacy entries that
|
|
160
|
+
// predate HOOK_MARKER_JSON already run this exact command, and the
|
|
161
|
+
// command string is specific enough to agent-memory that it's a safe
|
|
162
|
+
// signal on its own. Otherwise a pre-marker install is invisible here,
|
|
163
|
+
// doctor/isHookInstalled falsely report "not installed" for hooks that
|
|
164
|
+
// are actually live, and upsertClaudeHookGroup can't find them to dedupe.
|
|
165
|
+
if (h.command === command) return true;
|
|
160
166
|
}
|
|
161
167
|
}
|
|
162
168
|
return false;
|
|
@@ -388,55 +394,81 @@ function upsertClaudeHookGroup(
|
|
|
388
394
|
command: string,
|
|
389
395
|
): { changed: boolean; hadManaged: boolean } {
|
|
390
396
|
const groups = Array.isArray(hooks[eventKey]) ? [...(hooks[eventKey] as unknown[])] : [];
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
397
|
+
|
|
398
|
+
// Locate every agent-memory-owned hook entry — matched by marker, or by
|
|
399
|
+
// exact command string for legacy pre-marker installs — wherever it lives.
|
|
400
|
+
const owned: Array<{ group: Record<string, unknown>; hook: Record<string, unknown> }> = [];
|
|
401
|
+
for (const group of groups) {
|
|
394
402
|
if (!group || typeof group !== "object") continue;
|
|
395
403
|
const g = group as Record<string, unknown>;
|
|
396
404
|
const list = Array.isArray(g.hooks) ? (g.hooks as unknown[]) : [];
|
|
397
405
|
for (const hook of list) {
|
|
398
406
|
if (!hook || typeof hook !== "object") continue;
|
|
399
407
|
const h = hook as Record<string, unknown>;
|
|
400
|
-
if (h[HOOK_MARKER_JSON] === true) {
|
|
401
|
-
managedIndexes.push(i);
|
|
402
|
-
break;
|
|
403
|
-
}
|
|
408
|
+
if (h[HOOK_MARKER_JSON] === true || h.command === command) owned.push({ group: g, hook: h });
|
|
404
409
|
}
|
|
405
410
|
}
|
|
406
411
|
|
|
407
|
-
if (
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
for (const idx of dupes.reverse()) groups.splice(idx, 1);
|
|
411
|
-
const group = groups[keep] as Record<string, unknown>;
|
|
412
|
-
let changed = dupes.length > 0;
|
|
413
|
-
if ("matcher" in group) {
|
|
414
|
-
delete group.matcher;
|
|
415
|
-
changed = true;
|
|
416
|
-
}
|
|
417
|
-
const list = group.hooks as Record<string, unknown>[];
|
|
418
|
-
for (const h of list) {
|
|
419
|
-
if (h[HOOK_MARKER_JSON] === true && h.command !== command) {
|
|
420
|
-
h.command = command;
|
|
421
|
-
changed = true;
|
|
422
|
-
}
|
|
423
|
-
}
|
|
412
|
+
if (owned.length === 0) {
|
|
413
|
+
// No existing managed hook anywhere — add a fresh group without a matcher so it fires on all harnesses.
|
|
414
|
+
groups.push({ hooks: [{ type: "command", command, [HOOK_MARKER_JSON]: true }] });
|
|
424
415
|
hooks[eventKey] = groups;
|
|
425
|
-
return { changed, hadManaged:
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
//
|
|
429
|
-
|
|
430
|
-
hooks
|
|
431
|
-
|
|
416
|
+
return { changed: true, hadManaged: false };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Keep the first owned entry in place — fixed up in place, preserving its
|
|
420
|
+
// identity so a no-op re-install reports unchanged — and remove every
|
|
421
|
+
// other owned entry from its own group's hooks array, whether it's a
|
|
422
|
+
// duplicate in another group or piled up alongside the keeper in the same
|
|
423
|
+
// group. Never delete a whole group, since it could carry unrelated
|
|
424
|
+
// hand-added hooks.
|
|
425
|
+
const keeper = owned[0];
|
|
426
|
+
let changed = false;
|
|
427
|
+
if (keeper.hook.command !== command) {
|
|
428
|
+
keeper.hook.command = command;
|
|
429
|
+
changed = true;
|
|
430
|
+
}
|
|
431
|
+
if (keeper.hook[HOOK_MARKER_JSON] !== true) {
|
|
432
|
+
keeper.hook[HOOK_MARKER_JSON] = true;
|
|
433
|
+
changed = true;
|
|
434
|
+
}
|
|
435
|
+
for (const dupe of owned.slice(1)) {
|
|
436
|
+
const list = dupe.group.hooks as unknown[];
|
|
437
|
+
const idx = list.indexOf(dupe.hook);
|
|
438
|
+
if (idx !== -1) list.splice(idx, 1);
|
|
439
|
+
changed = true;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Only clear the keeper's group matcher when that group is exclusively
|
|
443
|
+
// ours (no unrelated hooks left in it after dedup) — never when it also
|
|
444
|
+
// carries an unrelated hand-added hook.
|
|
445
|
+
const keeperList = keeper.group.hooks as unknown[];
|
|
446
|
+
if (keeperList.length === 1 && "matcher" in keeper.group) {
|
|
447
|
+
delete keeper.group.matcher;
|
|
448
|
+
changed = true;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Drop any group left with zero hooks; never drop one that still has
|
|
452
|
+
// unrelated hooks in it.
|
|
453
|
+
const filtered = groups.filter((group) => {
|
|
454
|
+
if (!group || typeof group !== "object") return true;
|
|
455
|
+
const g = group as Record<string, unknown>;
|
|
456
|
+
return !Array.isArray(g.hooks) || (g.hooks as unknown[]).length > 0;
|
|
457
|
+
});
|
|
458
|
+
hooks[eventKey] = filtered;
|
|
459
|
+
return { changed, hadManaged: true };
|
|
432
460
|
}
|
|
433
461
|
|
|
434
462
|
/**
|
|
435
463
|
* Remove all agent-memory-managed hook entries for `eventKey`. Used when
|
|
436
464
|
* downgrading from per-turn back to stable (drops UserPromptSubmit).
|
|
465
|
+
* `command`, when given, also matches legacy entries that predate
|
|
466
|
+
* HOOK_MARKER_JSON — the same broadened detection upsertClaudeHookGroup and
|
|
467
|
+
* hasClaudeHookGroup use — so a pre-marker install isn't left behind after a
|
|
468
|
+
* downgrade/uninstall that reports success.
|
|
437
469
|
* Returns true if anything was removed.
|
|
438
470
|
*/
|
|
439
|
-
function removeClaudeHookGroup(hooks: Record<string, unknown>, eventKey: string): boolean {
|
|
471
|
+
function removeClaudeHookGroup(hooks: Record<string, unknown>, eventKey: string, command?: string): boolean {
|
|
440
472
|
const groups = Array.isArray(hooks[eventKey]) ? (hooks[eventKey] as unknown[]) : [];
|
|
441
473
|
if (groups.length === 0) return false;
|
|
442
474
|
let removed = 0;
|
|
@@ -446,7 +478,8 @@ function removeClaudeHookGroup(hooks: Record<string, unknown>, eventKey: string)
|
|
|
446
478
|
const g = { ...(group as Record<string, unknown>) };
|
|
447
479
|
const list = Array.isArray(g.hooks) ? (g.hooks as unknown[]) : [];
|
|
448
480
|
const kept = list.filter((h) => {
|
|
449
|
-
const
|
|
481
|
+
const hook = h && typeof h === "object" ? (h as Record<string, unknown>) : null;
|
|
482
|
+
const isOurs = !!hook && (hook[HOOK_MARKER_JSON] === true || (!!command && hook.command === command));
|
|
450
483
|
if (isOurs) removed++;
|
|
451
484
|
return !isOurs;
|
|
452
485
|
});
|
|
@@ -478,7 +511,7 @@ function installClaudeCodeHook(homeDir: string, mode: HookMode = "per-turn"): Ho
|
|
|
478
511
|
promptChanged = prompt.changed;
|
|
479
512
|
promptHadManaged = prompt.hadManaged;
|
|
480
513
|
} else {
|
|
481
|
-
promptChanged = removeClaudeHookGroup(hooks, "UserPromptSubmit");
|
|
514
|
+
promptChanged = removeClaudeHookGroup(hooks, "UserPromptSubmit", userPromptSubmitHookCommand("claude"));
|
|
482
515
|
}
|
|
483
516
|
// Stop backs the write side of memory with a periodic nudge. It is orthogonal
|
|
484
517
|
// to stable/per-turn context injection, so it is installed unconditionally.
|
|
@@ -848,9 +881,9 @@ function uninstallClaudeCodeHook(homeDir: string): HookInstallResult {
|
|
|
848
881
|
}
|
|
849
882
|
const settings = readJsonConfig(settingsPath);
|
|
850
883
|
const hooks = (settings.hooks as Record<string, unknown>) ?? {};
|
|
851
|
-
const sessionRemoved = removeClaudeHookGroup(hooks, "SessionStart");
|
|
852
|
-
const promptRemoved = removeClaudeHookGroup(hooks, "UserPromptSubmit");
|
|
853
|
-
const stopRemoved = removeClaudeHookGroup(hooks, "Stop");
|
|
884
|
+
const sessionRemoved = removeClaudeHookGroup(hooks, "SessionStart", sessionStartHookCommand("claude"));
|
|
885
|
+
const promptRemoved = removeClaudeHookGroup(hooks, "UserPromptSubmit", userPromptSubmitHookCommand("claude"));
|
|
886
|
+
const stopRemoved = removeClaudeHookGroup(hooks, "Stop", stopHookCommand("claude"));
|
|
854
887
|
const legacyPreCompactRemoved = removeClaudeHookGroup(hooks, "PreCompact");
|
|
855
888
|
if (!sessionRemoved && !promptRemoved && !stopRemoved && !legacyPreCompactRemoved) {
|
|
856
889
|
return { key: "claude", label: "Claude Code", installed: false, reason: "not installed" };
|
package/src/plugin-host.ts
CHANGED
|
@@ -176,7 +176,12 @@ export interface PluginMcpToolInputSchema {
|
|
|
176
176
|
export interface PluginMcpToolV1 {
|
|
177
177
|
name: string;
|
|
178
178
|
description: string;
|
|
179
|
-
|
|
179
|
+
/**
|
|
180
|
+
* Capability checked on every invocation. Optional only to keep plugin API 1
|
|
181
|
+
* source-compatible with bundles built before capability-gated MCP tools
|
|
182
|
+
* were introduced; legacy tools load but are denied until updated.
|
|
183
|
+
*/
|
|
184
|
+
requiredCapability?: string;
|
|
180
185
|
inputSchema: PluginMcpToolInputSchema;
|
|
181
186
|
run(input: Record<string, unknown>): unknown | Promise<unknown>;
|
|
182
187
|
}
|
|
@@ -190,6 +195,7 @@ export interface AgentMemoryPluginHostV1 {
|
|
|
190
195
|
registerContextProvider?(provider: PluginContextProviderV1): void;
|
|
191
196
|
registerMcpTool?(tool: PluginMcpToolV1): void;
|
|
192
197
|
registerMcpStartup?(fn: () => void | Promise<void>): void;
|
|
198
|
+
registerMcpShutdown?(fn: () => void | Promise<void>): void;
|
|
193
199
|
getStateDirectory(): string;
|
|
194
200
|
getMemoryDirectory(): string;
|
|
195
201
|
getEntitlement(): Promise<PluginEntitlementStatusV1>;
|