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.
- package/README.md +10 -7
- package/dist/cli-spec.d.ts +1 -1
- package/dist/cli-spec.js +16 -4
- package/dist/cli.js +466 -65
- package/dist/completions.d.ts +12 -0
- package/dist/completions.js +51 -0
- package/dist/core.js +11 -0
- package/dist/hooks.d.ts +15 -1
- package/dist/hooks.js +190 -5
- 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 -64
- package/dist/upgrade.d.ts +60 -7
- package/dist/upgrade.js +146 -15
- package/docs/official-plugin-bootstrap.md +3 -4
- package/package.json +1 -2
- package/scripts/install-skills.sh +4 -1
- package/skills/agent/SKILL.md +1 -1
- package/skills/claude-code/SKILL.md +1 -1
- package/skills/codex/SKILL.md +1 -1
- package/skills/cursor/SKILL.md +1 -1
- package/skills/qoder/SKILL.md +164 -0
- package/src/cli-spec.ts +19 -4
- package/src/completions.ts +73 -0
- package/src/core.ts +11 -0
- package/src/hooks.ts +199 -6
- package/src/plugin-host.ts +7 -1
package/dist/completions.d.ts
CHANGED
|
@@ -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[];
|
package/dist/completions.js
CHANGED
|
@@ -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:
|
|
88
|
-
|
|
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({
|
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 {
|