myagentmemory 0.5.2 → 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.
@@ -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({
@@ -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 {