myagentmemory 0.5.1 → 0.5.3

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.
@@ -5,9 +5,21 @@ export interface CompletionInstallResult {
5
5
  profilePath?: string;
6
6
  profileUpdated: boolean;
7
7
  }
8
+ export interface CompletionUninstallResult {
9
+ shell: CompletionShell;
10
+ completionPath: string;
11
+ removed: boolean;
12
+ profilePath?: string;
13
+ profileUpdated: boolean;
14
+ }
8
15
  export declare function generateCompletion(shell: CompletionShell): string;
9
16
  export declare function detectCompletionShell(environment?: Record<string, string | undefined>, platform?: NodeJS.Platform): CompletionShell | null;
10
17
  export declare function installCompletion(shell: CompletionShell, options?: {
11
18
  homeDir?: string;
12
19
  platform?: NodeJS.Platform;
13
20
  }): CompletionInstallResult;
21
+ /** Reverse of {@link installCompletion} across every supported shell. */
22
+ export declare function uninstallCompletion(options?: {
23
+ homeDir?: string;
24
+ platform?: NodeJS.Platform;
25
+ }): CompletionUninstallResult[];
@@ -395,6 +395,29 @@ function ensureProfileBlock(filePath, lines) {
395
395
  fs.writeFileSync(filePath, updated, { mode: 0o600 });
396
396
  return true;
397
397
  }
398
+ /** Reverse of {@link ensureProfileBlock}: strips the marker block, if present, from the given file. */
399
+ function removeProfileBlock(filePath) {
400
+ const start = "# >>> agent-memory completion >>>";
401
+ const end = "# <<< agent-memory completion <<<";
402
+ if (!fs.existsSync(filePath))
403
+ return false;
404
+ const current = fs.readFileSync(filePath, "utf8");
405
+ const startIndex = current.indexOf(start);
406
+ const endIndex = startIndex === -1 ? -1 : current.indexOf(end, startIndex);
407
+ if (startIndex === -1 || endIndex === -1)
408
+ return false;
409
+ const updated = (current.slice(0, startIndex) + current.slice(endIndex + end.length)).replace(/\n{3,}/g, "\n\n");
410
+ if (updated === current)
411
+ return false;
412
+ fs.writeFileSync(filePath, updated, { mode: 0o600 });
413
+ return true;
414
+ }
415
+ function removeCompletionFile(filePath) {
416
+ if (!fs.existsSync(filePath))
417
+ return false;
418
+ fs.unlinkSync(filePath);
419
+ return true;
420
+ }
398
421
  export function installCompletion(shell, options = {}) {
399
422
  const homeDir = options.homeDir ?? os.homedir();
400
423
  const platform = options.platform ?? process.platform;
@@ -433,3 +456,31 @@ export function installCompletion(shell, options = {}) {
433
456
  ]);
434
457
  return { shell, completionPath, profilePath, profileUpdated };
435
458
  }
459
+ function uninstallShellCompletion(shell, homeDir, platform) {
460
+ const completionDir = path.join(homeDir, ".config", "agent-memory", "completions");
461
+ if (shell === "fish") {
462
+ const completionPath = path.join(homeDir, ".config", "fish", "completions", "agent-memory.fish");
463
+ return { shell, completionPath, removed: removeCompletionFile(completionPath), profileUpdated: false };
464
+ }
465
+ const extension = shell === "powershell" ? "ps1" : shell;
466
+ const completionPath = path.join(completionDir, `agent-memory.${extension}`);
467
+ const removed = removeCompletionFile(completionPath);
468
+ if (shell === "bash") {
469
+ const profilePath = path.join(homeDir, ".bashrc");
470
+ return { shell, completionPath, removed, profilePath, profileUpdated: removeProfileBlock(profilePath) };
471
+ }
472
+ if (shell === "zsh") {
473
+ const profilePath = path.join(homeDir, ".zshrc");
474
+ return { shell, completionPath, removed, profilePath, profileUpdated: removeProfileBlock(profilePath) };
475
+ }
476
+ const profilePath = platform === "win32"
477
+ ? path.join(homeDir, "Documents", "PowerShell", "Microsoft.PowerShell_profile.ps1")
478
+ : path.join(homeDir, ".config", "powershell", "Microsoft.PowerShell_profile.ps1");
479
+ return { shell, completionPath, removed, profilePath, profileUpdated: removeProfileBlock(profilePath) };
480
+ }
481
+ /** Reverse of {@link installCompletion} across every supported shell. */
482
+ export function uninstallCompletion(options = {}) {
483
+ const homeDir = options.homeDir ?? os.homedir();
484
+ const platform = options.platform ?? process.platform;
485
+ return ["bash", "zsh", "fish", "powershell"].map((shell) => uninstallShellCompletion(shell, homeDir, platform));
486
+ }
package/dist/core.js CHANGED
@@ -872,6 +872,17 @@ export function installSkills() {
872
872
  destDir: path.join(homeDir, ".cursor", "skills", "agent-memory"),
873
873
  homeMarker: path.join(homeDir, ".cursor"),
874
874
  },
875
+ {
876
+ label: "Qoder skill",
877
+ srcDir: path.join(skillsDir, "qoder"),
878
+ destDir: path.join(homeDir, ".qoder", "skills", "agent-memory"),
879
+ homeMarker: path.join(homeDir, ".qoder"),
880
+ detectFiles: [
881
+ path.join(homeDir, ".qoder", "settings.json"),
882
+ path.join(homeDir, ".qoder", "settings.local.json"),
883
+ ],
884
+ detectCommand: "qoder",
885
+ },
875
886
  {
876
887
  label: "Agent CLI skill",
877
888
  srcDir: path.join(skillsDir, "agent"),
package/dist/hooks.d.ts CHANGED
@@ -1,7 +1,13 @@
1
+ import { execFileSync } from "node:child_process";
1
2
  import { type HookMode } from "./core.js";
3
+ type ExecFileSyncFn = typeof execFileSync;
4
+ /** Override the execFileSync implementation used by hook installers (for testing). */
5
+ export declare function _setHookExecForTest(fn: ExecFileSyncFn): void;
6
+ /** Reset the execFileSync implementation to the real one. */
7
+ export declare function _resetHookExecForTest(): void;
2
8
  /** Override the detected home directory in deterministic tests. */
3
9
  export declare function _setHookHomeDirForTest(directory: string | null): void;
4
- export type HookAgentKey = "claude" | "codex" | "cursor" | "opencode" | "pi";
10
+ export type HookAgentKey = "claude" | "codex" | "cursor" | "opencode" | "pi" | "qoder";
5
11
  export interface HookTargetInfo {
6
12
  key: HookAgentKey;
7
13
  label: string;
@@ -52,6 +58,13 @@ export interface InstallHooksReport {
52
58
  results: HookInstallResult[];
53
59
  error?: string;
54
60
  }
61
+ export interface PiMemoryState {
62
+ lastAttemptAt: string;
63
+ ok: boolean;
64
+ detail: string;
65
+ }
66
+ /** Read the last recorded `pi install npm:pi-memory` attempt, if any. Never throws. */
67
+ export declare function getPiMemoryState(): PiMemoryState | null;
55
68
  export declare function installHooks(agents: Set<HookAgentKey>, mode?: HookMode): InstallHooksReport;
56
69
  export interface UninstallHooksReport {
57
70
  ok: boolean;
@@ -60,3 +73,4 @@ export interface UninstallHooksReport {
60
73
  error?: string;
61
74
  }
62
75
  export declare function uninstallHooks(agents?: Set<HookAgentKey>): UninstallHooksReport;
76
+ export {};
package/dist/hooks.js CHANGED
@@ -1,7 +1,17 @@
1
+ import { execFileSync } from "node:child_process";
1
2
  import * as fs from "node:fs";
2
3
  import * as os from "node:os";
3
4
  import * as path from "node:path";
4
- import { writeHookMode } from "./core.js";
5
+ import { getMemoryDir, writeHookMode } from "./core.js";
6
+ let execFileSyncFn = execFileSync;
7
+ /** Override the execFileSync implementation used by hook installers (for testing). */
8
+ export function _setHookExecForTest(fn) {
9
+ execFileSyncFn = fn;
10
+ }
11
+ /** Reset the execFileSync implementation to the real one. */
12
+ export function _resetHookExecForTest() {
13
+ execFileSyncFn = execFileSync;
14
+ }
5
15
  let homeDirOverride = null;
6
16
  /** Override the detected home directory in deterministic tests. */
7
17
  export function _setHookHomeDirForTest(directory) {
@@ -80,12 +90,22 @@ function hookTargets(homeDir) {
80
90
  },
81
91
  {
82
92
  key: "pi",
83
- label: "pi",
93
+ label: "pi (via pi-memory)",
84
94
  homeMarker: path.join(homeDir, ".pi"),
85
95
  detectFiles: [],
86
96
  detectCommand: "pi",
87
- supported: false,
88
- unsupportedReason: "no documented SessionStart hook mechanism",
97
+ supported: true,
98
+ },
99
+ {
100
+ key: "qoder",
101
+ label: "Qoder",
102
+ homeMarker: path.join(homeDir, ".qoder"),
103
+ detectFiles: [
104
+ path.join(homeDir, ".qoder", "settings.json"),
105
+ path.join(homeDir, ".qoder", "settings.local.json"),
106
+ ],
107
+ detectCommand: "qoder",
108
+ supported: true,
89
109
  },
90
110
  ];
91
111
  }
@@ -143,6 +163,14 @@ export function isHookInstalled(homeDir, key) {
143
163
  const instructionsPath = path.join(homeDir, ".agent-memory", "hooks", "opencode.md");
144
164
  return list.includes(instructionsPath);
145
165
  }
166
+ if (key === "pi") {
167
+ // Live filesystem state is authoritative — pi-memory can be installed or
168
+ // removed outside agent-memory (manually, or via `pi uninstall`) at any
169
+ // time, so a recorded delegate attempt must never override what's
170
+ // actually on disk. The state file is diagnostic-only (surfaced
171
+ // separately in `doctor`'s detail text), not a substitute for this check.
172
+ return fs.existsSync(path.join(homeDir, ".pi", "agent", "memory"));
173
+ }
146
174
  }
147
175
  catch {
148
176
  return false;
@@ -232,6 +260,68 @@ function writeJson(filePath, data) {
232
260
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
233
261
  fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`, "utf-8");
234
262
  }
263
+ function piMemoryStatePath() {
264
+ return path.join(getMemoryDir(), "pi-memory-state.json");
265
+ }
266
+ function writePiMemoryState(ok, detail) {
267
+ try {
268
+ writeJson(piMemoryStatePath(), { lastAttemptAt: new Date().toISOString(), ok, detail });
269
+ }
270
+ catch {
271
+ // best-effort bookkeeping only — never block the actual install/uninstall result on this.
272
+ }
273
+ }
274
+ /** Read the last recorded `pi install npm:pi-memory` attempt, if any. Never throws. */
275
+ export function getPiMemoryState() {
276
+ try {
277
+ const filePath = piMemoryStatePath();
278
+ if (!fs.existsSync(filePath))
279
+ return null;
280
+ const parsed = JSON.parse(fs.readFileSync(filePath, "utf-8"));
281
+ if (typeof parsed?.lastAttemptAt !== "string" || typeof parsed?.ok !== "boolean")
282
+ return null;
283
+ return { lastAttemptAt: parsed.lastAttemptAt, ok: parsed.ok, detail: String(parsed.detail ?? "") };
284
+ }
285
+ catch {
286
+ return null;
287
+ }
288
+ }
289
+ function installPiMemoryDelegate(homeDir) {
290
+ const memoryPath = path.join(homeDir, ".pi", "agent", "memory");
291
+ try {
292
+ const stdout = execFileSyncFn("pi", ["install", "npm:pi-memory"], {
293
+ encoding: "utf-8",
294
+ timeout: 30_000,
295
+ });
296
+ const detail = typeof stdout === "string" ? stdout.trim() : "";
297
+ writePiMemoryState(true, detail);
298
+ return { key: "pi", label: "pi (via pi-memory)", installed: true, path: memoryPath, reason: detail || undefined };
299
+ }
300
+ catch (err) {
301
+ const detail = err && typeof err === "object" && "stderr" in err && err.stderr
302
+ ? String(err.stderr).trim()
303
+ : err instanceof Error
304
+ ? err.message
305
+ : String(err);
306
+ writePiMemoryState(false, detail);
307
+ return { key: "pi", label: "pi (via pi-memory)", installed: false, reason: detail };
308
+ }
309
+ }
310
+ function uninstallPiMemoryDelegate(homeDir) {
311
+ // agent-memory never owns this install, so it never runs `pi uninstall pi-memory` — but
312
+ // silently doing nothing would let an `agent-memory uninstall` report read as "fully cleaned
313
+ // up" while pi-memory keeps running. Phrase the reason distinctly when it's actually still
314
+ // active so callers (and cmdUninstall's step detail) can surface that honestly.
315
+ const stillActive = fs.existsSync(path.join(homeDir, ".pi", "agent", "memory"));
316
+ return {
317
+ key: "pi",
318
+ label: "pi (via pi-memory)",
319
+ installed: false,
320
+ reason: stillActive
321
+ ? "pi-memory left installed (not managed by agent-memory) — run `pi uninstall pi-memory` to remove it"
322
+ : "not installed",
323
+ };
324
+ }
235
325
  /**
236
326
  * Idempotently upsert the agent-memory-managed hook group for `eventKey`
237
327
  * (SessionStart or UserPromptSubmit) with `command`. Returns `{ changed,
@@ -524,6 +614,93 @@ function installOpencodeInstructions(homeDir) {
524
614
  writeJson(configPath, config);
525
615
  return { key: "opencode", label: "opencode", installed: true, path: configPath, backup };
526
616
  }
617
+ function installQoderHook(homeDir) {
618
+ const settingsPath = path.join(homeDir, ".qoder", "settings.json");
619
+ const backup = backupOnce(settingsPath);
620
+ const settings = readJsonConfig(settingsPath);
621
+ const hooks = settings.hooks ?? {};
622
+ const sessionStart = Array.isArray(hooks.SessionStart) ? [...hooks.SessionStart] : [];
623
+ const command = "agent-memory context";
624
+ let managed = 0;
625
+ let updated = 0;
626
+ for (const group of sessionStart) {
627
+ if (!group || typeof group !== "object")
628
+ continue;
629
+ const g = group;
630
+ const list = Array.isArray(g.hooks) ? g.hooks : [];
631
+ for (const hook of list) {
632
+ if (!hook || typeof hook !== "object")
633
+ continue;
634
+ const managedHook = hook;
635
+ if (managedHook[HOOK_MARKER_JSON] !== true)
636
+ continue;
637
+ managed++;
638
+ if (managedHook.command !== command) {
639
+ managedHook.command = command;
640
+ updated++;
641
+ }
642
+ }
643
+ }
644
+ if (managed && !updated) {
645
+ return { key: "qoder", label: "Qoder", installed: false, path: settingsPath, reason: "already installed" };
646
+ }
647
+ if (updated) {
648
+ hooks.SessionStart = sessionStart;
649
+ settings.hooks = hooks;
650
+ writeJson(settingsPath, settings);
651
+ return { key: "qoder", label: "Qoder", installed: true, path: settingsPath, backup, reason: "updated" };
652
+ }
653
+ sessionStart.push({
654
+ hooks: [{ type: "command", command, [HOOK_MARKER_JSON]: true }],
655
+ });
656
+ hooks.SessionStart = sessionStart;
657
+ settings.hooks = hooks;
658
+ writeJson(settingsPath, settings);
659
+ return { key: "qoder", label: "Qoder", installed: true, path: settingsPath, backup };
660
+ }
661
+ function uninstallQoderHook(homeDir) {
662
+ const settingsPath = path.join(homeDir, ".qoder", "settings.json");
663
+ if (!fs.existsSync(settingsPath)) {
664
+ return { key: "qoder", label: "Qoder", installed: false, reason: "not installed" };
665
+ }
666
+ const settings = readJsonConfig(settingsPath);
667
+ const hooks = settings.hooks ?? {};
668
+ const sessionStart = Array.isArray(hooks.SessionStart) ? hooks.SessionStart : [];
669
+ let removed = 0;
670
+ const filtered = sessionStart
671
+ .map((group) => {
672
+ if (!group || typeof group !== "object")
673
+ return group;
674
+ const g = { ...group };
675
+ const list = Array.isArray(g.hooks) ? g.hooks : [];
676
+ const kept = list.filter((h) => {
677
+ const isOurs = h && typeof h === "object" && h[HOOK_MARKER_JSON] === true;
678
+ if (isOurs)
679
+ removed++;
680
+ return !isOurs;
681
+ });
682
+ g.hooks = kept;
683
+ return g;
684
+ })
685
+ .filter((group) => {
686
+ if (!group || typeof group !== "object")
687
+ return true;
688
+ const g = group;
689
+ return Array.isArray(g.hooks) && g.hooks.length > 0;
690
+ });
691
+ if (removed === 0) {
692
+ return { key: "qoder", label: "Qoder", installed: false, reason: "not installed" };
693
+ }
694
+ hooks.SessionStart = filtered;
695
+ if (filtered.length === 0)
696
+ delete hooks.SessionStart;
697
+ if (Object.keys(hooks).length === 0)
698
+ delete settings.hooks;
699
+ else
700
+ settings.hooks = hooks;
701
+ writeJson(settingsPath, settings);
702
+ return { key: "qoder", label: "Qoder", installed: true, path: settingsPath };
703
+ }
527
704
  export function installHooks(agents, mode = "per-turn") {
528
705
  const { homeDir, targets } = detectHookAgents();
529
706
  if (!homeDir) {
@@ -566,6 +743,10 @@ export function installHooks(agents, mode = "per-turn") {
566
743
  result = installCursorHook(homeDir);
567
744
  else if (target.key === "opencode")
568
745
  result = installOpencodeInstructions(homeDir);
746
+ else if (target.key === "pi")
747
+ result = installPiMemoryDelegate(homeDir);
748
+ else if (target.key === "qoder")
749
+ result = installQoderHook(homeDir);
569
750
  else
570
751
  continue;
571
752
  results.push(result);
@@ -711,7 +892,7 @@ export function uninstallHooks(agents) {
711
892
  error: "Home directory not found. Set HOME (or USERPROFILE on Windows) and retry.",
712
893
  };
713
894
  }
714
- const keys = ["claude", "codex", "cursor", "opencode"];
895
+ const keys = ["claude", "codex", "cursor", "opencode", "pi", "qoder"];
715
896
  const results = [];
716
897
  for (const key of keys) {
717
898
  if (agents && !agents.has(key))
@@ -725,6 +906,10 @@ export function uninstallHooks(agents) {
725
906
  results.push(uninstallCursorHook(homeDir));
726
907
  else if (key === "opencode")
727
908
  results.push(uninstallOpencodeInstructions(homeDir));
909
+ else if (key === "pi")
910
+ results.push(uninstallPiMemoryDelegate(homeDir));
911
+ else if (key === "qoder")
912
+ results.push(uninstallQoderHook(homeDir));
728
913
  }
729
914
  catch (err) {
730
915
  results.push({
@@ -147,6 +147,7 @@ export interface PluginMcpToolInputSchema {
147
147
  export interface PluginMcpToolV1 {
148
148
  name: string;
149
149
  description: string;
150
+ requiredCapability: string;
150
151
  inputSchema: PluginMcpToolInputSchema;
151
152
  run(input: Record<string, unknown>): unknown | Promise<unknown>;
152
153
  }
@@ -34,6 +34,15 @@ export declare class InstalledPluginRuntimeV1 {
34
34
  signal: AbortSignal;
35
35
  }): Promise<PluginContextSectionV1[]>;
36
36
  getMcpTools(): PluginMcpToolV1[];
37
+ /**
38
+ * Invoke a registered MCP tool by name, re-checking entitlement on every
39
+ * call — matching `run()`'s behavior for commands. MCP tools are
40
+ * registered once at `serve --mcp` startup and the server process can
41
+ * live for a long session, so a tool's entitlement must be re-verified
42
+ * per-call rather than trusted from registration time (e.g. a trial
43
+ * expiring mid-session must actually stop the tool from working).
44
+ */
45
+ runMcpTool(name: string, input: Record<string, unknown>): Promise<unknown>;
37
46
  runMcpStartup(): Promise<void>;
38
47
  private createHost;
39
48
  private refreshEntitlement;
@@ -186,6 +186,25 @@ export class InstalledPluginRuntimeV1 {
186
186
  getMcpTools() {
187
187
  return [...this.mcpTools];
188
188
  }
189
+ /**
190
+ * Invoke a registered MCP tool by name, re-checking entitlement on every
191
+ * call — matching `run()`'s behavior for commands. MCP tools are
192
+ * registered once at `serve --mcp` startup and the server process can
193
+ * live for a long session, so a tool's entitlement must be re-verified
194
+ * per-call rather than trusted from registration time (e.g. a trial
195
+ * expiring mid-session must actually stop the tool from working).
196
+ */
197
+ async runMcpTool(name, input) {
198
+ if (!(await this.load()))
199
+ return { error: `Unknown MCP tool: ${name}` };
200
+ const tool = this.mcpTools.find((candidate) => candidate.name === name);
201
+ if (!tool)
202
+ return { error: `Unknown MCP tool: ${name}` };
203
+ const entitlement = await this.refreshEntitlement();
204
+ if (!isPluginCapabilityEnabled(entitlement, tool.requiredCapability))
205
+ return { error: `Capability ${tool.requiredCapability} is not enabled for the ${name} tool` };
206
+ return tool.run(input);
207
+ }
189
208
  async runMcpStartup() {
190
209
  for (const hook of this.mcpStartupHooks)
191
210
  await hook();
@@ -238,6 +257,8 @@ export class InstalledPluginRuntimeV1 {
238
257
  this.contextProviders.push({ provider, pluginId: manifest.id });
239
258
  },
240
259
  registerMcpTool: (tool) => {
260
+ if (!(manifest.capabilities ?? []).includes(tool.requiredCapability))
261
+ throw new PluginBootstrapFailure("plugin_mcp_tool_invalid", `Plugin ${manifest.id} registered an MCP tool with an undeclared capability`);
241
262
  if (!tool.name || this.mcpTools.some((existing) => existing.name === tool.name))
242
263
  throw new PluginBootstrapFailure("plugin_mcp_tool_invalid", `Plugin MCP tool ${tool.name || "(unnamed)"} is invalid or already registered`);
243
264
  this.mcpTools.push(tool);
@@ -402,14 +402,8 @@ export class AgentMemoryServiceBackend {
402
402
  value.entitlement.state !== "active" ||
403
403
  value.entitlement.capabilities.recall?.enabled !== true ||
404
404
  !recallQuota ||
405
- recallQuota.limit !== 20 ||
406
- recallQuota.scope !== "device" ||
407
- recallQuota.window !== "day" ||
408
405
  value.entitlement.capabilities.learning?.enabled !== true ||
409
406
  !learningQuota ||
410
- learningQuota.limit !== 5 ||
411
- learningQuota.scope !== "device" ||
412
- learningQuota.window !== "day" ||
413
407
  value.entitlement.capabilities["session-index"]?.enabled !== true ||
414
408
  value.entitlement.capabilities["session-worker"]?.enabled !== false ||
415
409
  value.entitlement.capabilities["web-console"]?.enabled !== true)
package/dist/upgrade.d.ts CHANGED
@@ -1,12 +1,18 @@
1
1
  /**
2
2
  * Upgrade orchestration for the `agent-memory` CLI and its official Pro plugin bundle.
3
3
  *
4
- * Two consumers:
4
+ * Three consumers:
5
5
  * 1. `agent-memory upgrade` — explicit user command; checks and (optionally) installs.
6
- * 2. `agent-memory hook session-start` — passive notice from a 24h-cached record.
6
+ * 2. `agent-memory upgrade --background` — detached, non-interactive; checks, then
7
+ * installs any target whose `readUpgradePolicy()` value is `"auto"` (the default).
8
+ * Spawned by `refreshUpgradeCacheBackground()` from `hook session-start`.
9
+ * 3. `agent-memory hook session-start` — passive notice from a 24h-cached record,
10
+ * including the outcome of the last `--background` auto-install attempt.
7
11
  *
8
12
  * Network calls always have a hard timeout and always fail closed (upgrade is a
9
- * quality-of-life feature; a flaky registry must never break the CLI).
13
+ * quality-of-life feature; a flaky registry must never break the CLI). Same fail-closed
14
+ * contract applies to auto-install: a failed background install is recorded, never
15
+ * retried before the next cache refresh, and never thrown.
10
16
  */
11
17
  import { type SpawnOptions } from "node:child_process";
12
18
  export type InstallManager = "bun" | "npm" | "pnpm" | "yarn" | "unknown";
@@ -24,6 +30,17 @@ export interface UpgradeCache {
24
30
  cliLatest: string | null;
25
31
  pluginCurrent: string | null;
26
32
  pluginLatest: string | null;
33
+ /** Outcome of the most recent `--background` auto-upgrade attempt, if any. */
34
+ cliAuto?: AutoUpgradeOutcome;
35
+ pluginAuto?: AutoUpgradeOutcome;
36
+ }
37
+ export interface AutoUpgradeOutcome {
38
+ at: string;
39
+ ok: boolean;
40
+ /** Version installed (ok) or the previous/current version (failure). */
41
+ version: string | null;
42
+ /** Failure reason; absent when ok. */
43
+ error?: string;
27
44
  }
28
45
  export interface UpgradeStatus {
29
46
  cli: {
@@ -42,6 +59,24 @@ export interface UpgradeStatus {
42
59
  export declare function readUpgradeCache(): UpgradeCache | null;
43
60
  export declare function writeUpgradeCache(record: UpgradeCache): void;
44
61
  export declare function isCacheFresh(record: UpgradeCache | null, now?: number): boolean;
62
+ export type UpgradePolicyValue = "off" | "notify" | "auto";
63
+ export interface UpgradePolicy {
64
+ cli: UpgradePolicyValue;
65
+ plugin: UpgradePolicyValue;
66
+ }
67
+ /**
68
+ * Resolve the persisted auto-upgrade policy.
69
+ * Precedence per target: `AGENT_MEMORY_AUTO_UPGRADE_{CLI,PLUGIN}` env var →
70
+ * `<memoryDir>/state/upgrade-policy.json` → default `"auto"`.
71
+ *
72
+ * `existed` tells callers whether the policy file was already on disk —
73
+ * used to fire a one-time "auto-upgrade is on" notice on first read.
74
+ */
75
+ export declare function readUpgradePolicy(): UpgradePolicy & {
76
+ existed: boolean;
77
+ };
78
+ /** Atomically persist the auto-upgrade policy. Merges with whatever is already on disk. */
79
+ export declare function writeUpgradePolicy(patch: Partial<UpgradePolicy>): UpgradePolicy;
45
80
  /**
46
81
  * Best-effort detection of how `myagentmemory` was installed. Path signatures
47
82
  * are heuristic but cover the common managers. On no match we fall back to
@@ -58,8 +93,10 @@ export interface InstallResult {
58
93
  export declare function runInstaller(method: InstallMethod, opts?: SpawnOptions): InstallResult;
59
94
  /**
60
95
  * Fire-and-forget: spawn a detached child that runs `agent-memory upgrade
61
- * --check --refresh --quiet` so the next session-start has a fresh cache.
62
- * Never awaits, never throws.
96
+ * --background --refresh --quiet` so the next session-start has a fresh
97
+ * cache. Unlike a plain check, `--background` also installs any target whose
98
+ * policy is `"auto"` (see `readUpgradePolicy`) — this is the one place
99
+ * auto-upgrade actually happens. Never awaits, never throws.
63
100
  */
64
101
  export declare function refreshUpgradeCacheBackground(): void;
65
102
  export interface CheckOptions {
@@ -77,4 +114,12 @@ export interface CheckOptions {
77
114
  pluginUpgradeAvailable?: boolean;
78
115
  }
79
116
  export declare function checkForUpgrades(opts: CheckOptions): Promise<UpgradeStatus>;
80
- export declare function formatUpgradeNotice(status: UpgradeStatus): string | null;
117
+ /**
118
+ * `cache` (when passed) lets this distinguish a plain "notify" signal from the
119
+ * outcome of the last `--background` auto-install attempt for that target:
120
+ * - succeeded, but this process is running older code than what's on disk
121
+ * (e.g. a long-running `serve --mcp`) → "auto-upgraded, restart to use it"
122
+ * - failed → surface the error and point at the manual command
123
+ * - succeeded and already caught up (this process's own version matches) → silent
124
+ */
125
+ export declare function formatUpgradeNotice(status: UpgradeStatus, cache?: UpgradeCache | null): string | null;