myagentmemory 0.5.3 → 0.5.4

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 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. Memory content, session content, queries, and repository paths stay on this machine.
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. Pro installation uses a pseudonymous installation identifier and bounded compatibility metadata, never your memory or session content.
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. Memory, session, query, and repository content stay on your machine; installation sends only a pseudonymous identifier and bounded compatibility metadata. Full detail on [privacy, signing, and installation](docs/official-plugin-bootstrap.md).
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+; macOS, Linux, or Windows)
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 the AgentMemory account and billing website",
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
- " Upgrade for unlimited recall + automatic capture:",
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 Cursor.');
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 Memory and session content index locally and never leave this machine.");
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 billing state were preserved.");
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). Blocks at most once every STOP_NAG_INTERVAL turns per session_id
678
- * to nudge a memory-write check without being disruptive. Always allows the
679
- * stop (empty stdout) on missing session_id, `stop_hook_active` (Claude Code's
680
- * own re-entrancy signal never block twice in a row), or any internal error.
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({ decision: "block", reason: STOP_NAG_REASON }));
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 worker start", "cyan")} capture new sessions in real time`);
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, Cursor.`);
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("Real-time capture", "cyan")} Local worker indexes new sessions as they happen.`);
1858
- console.log(` ${colorize("Private by default", "cyan")} Memory and session content stay on this device — no account required.`);
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. Memory and session content stay on this device.`);
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)}. Upgrade: ${UPGRADE_URL}`);
3426
+ console.error(`AgentMemory free session allowance resets in ${formatResetTime(decision.resetAt)}. Paid plans are not available yet.`);
3420
3427
  }
3421
3428
  }
3422
3429
  }
@@ -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;
@@ -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;
@@ -147,7 +147,12 @@ export interface PluginMcpToolInputSchema {
147
147
  export interface PluginMcpToolV1 {
148
148
  name: string;
149
149
  description: string;
150
- requiredCapability: string;
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>;
@@ -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
  }
@@ -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");
@@ -1,4 +1,4 @@
1
- import { type PluginAccessDecisionV1, type PluginBootstrapBackendV1, type PluginNextActionV1, type PluginSessionUsageDecisionV1, type SignedPluginReleaseV1 } from "./plugin-bootstrap.js";
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 {
@@ -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 a daily count of opaque session-start operations. Activation records expire after 365 days without use.</p>
143
- <p class="never-sent"><strong>Never sent:</strong> The request never includes memory, session content, queries, repository paths, raw agent session identifiers, IP addresses, or user-agent strings.</p>
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, value.usageCredential, 1);
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.schemaVersion !== 3 ||
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
- !ACTIVATION_CREDENTIAL.test(value.usageCredential) ||
482
- !Number.isSafeInteger(value.dailySessionLimit) ||
483
- value.dailySessionLimit <= 0 ||
484
- value.dailySessionLimit > 10_000)
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 value;
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, usageCredential, dailySessionLimit) {
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: 3,
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 url.fileURLToPath(import.meta.url);
189
+ return fs.realpathSync(candidate);
173
190
  }
174
191
  catch {
175
- return process.argv[1] ?? "";
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 binary = process.argv[0];
245
- const script = process.argv[1];
246
- if (!binary || !script)
276
+ const launch = currentSelfLaunch();
277
+ if (!launch.command)
247
278
  return;
248
- const child = spawn(binary, [script, "upgrade", "--background", "--refresh", "--quiet", "--json"], {
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 credential.
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, opaque quota operation IDs, and authentication material required for the request.
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 billing 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 the activation credential) once the user opts in and confirms.
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",
3
+ "version": "0.5.4",
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 the AgentMemory account and billing website",
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> = {};
@@ -176,7 +176,12 @@ export interface PluginMcpToolInputSchema {
176
176
  export interface PluginMcpToolV1 {
177
177
  name: string;
178
178
  description: string;
179
- requiredCapability: string;
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>;