myagentmemory 0.5.2 → 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.
- package/README.md +5 -2
- package/dist/cli-spec.d.ts +1 -1
- package/dist/cli-spec.js +15 -3
- package/dist/cli.js +436 -42
- 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/plugin-service.js +0 -6
- package/dist/upgrade.d.ts +51 -6
- package/dist/upgrade.js +110 -10
- package/docs/official-plugin-bootstrap.md +1 -1
- package/package.json +1 -1
- 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 +18 -3
- package/src/completions.ts +73 -0
- package/src/core.ts +11 -0
- package/src/hooks.ts +199 -6
package/src/hooks.ts
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
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
5
|
|
|
5
|
-
import { type HookMode, writeHookMode } from "./core.js";
|
|
6
|
+
import { getMemoryDir, type HookMode, writeHookMode } from "./core.js";
|
|
7
|
+
|
|
8
|
+
type ExecFileSyncFn = typeof execFileSync;
|
|
9
|
+
let execFileSyncFn: ExecFileSyncFn = execFileSync;
|
|
10
|
+
|
|
11
|
+
/** Override the execFileSync implementation used by hook installers (for testing). */
|
|
12
|
+
export function _setHookExecForTest(fn: ExecFileSyncFn): void {
|
|
13
|
+
execFileSyncFn = fn;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Reset the execFileSync implementation to the real one. */
|
|
17
|
+
export function _resetHookExecForTest(): void {
|
|
18
|
+
execFileSyncFn = execFileSync;
|
|
19
|
+
}
|
|
6
20
|
|
|
7
21
|
let homeDirOverride: string | null = null;
|
|
8
22
|
|
|
@@ -38,7 +52,7 @@ function commandExists(command: string): boolean {
|
|
|
38
52
|
// Hook installers (SessionStart auto-injection)
|
|
39
53
|
// ---------------------------------------------------------------------------
|
|
40
54
|
|
|
41
|
-
export type HookAgentKey = "claude" | "codex" | "cursor" | "opencode" | "pi";
|
|
55
|
+
export type HookAgentKey = "claude" | "codex" | "cursor" | "opencode" | "pi" | "qoder";
|
|
42
56
|
|
|
43
57
|
export interface HookTargetInfo {
|
|
44
58
|
key: HookAgentKey;
|
|
@@ -109,12 +123,22 @@ function hookTargets(homeDir: string): HookTargetInfo[] {
|
|
|
109
123
|
},
|
|
110
124
|
{
|
|
111
125
|
key: "pi",
|
|
112
|
-
label: "pi",
|
|
126
|
+
label: "pi (via pi-memory)",
|
|
113
127
|
homeMarker: path.join(homeDir, ".pi"),
|
|
114
128
|
detectFiles: [],
|
|
115
129
|
detectCommand: "pi",
|
|
116
|
-
supported:
|
|
117
|
-
|
|
130
|
+
supported: true,
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
key: "qoder",
|
|
134
|
+
label: "Qoder",
|
|
135
|
+
homeMarker: path.join(homeDir, ".qoder"),
|
|
136
|
+
detectFiles: [
|
|
137
|
+
path.join(homeDir, ".qoder", "settings.json"),
|
|
138
|
+
path.join(homeDir, ".qoder", "settings.local.json"),
|
|
139
|
+
],
|
|
140
|
+
detectCommand: "qoder",
|
|
141
|
+
supported: true,
|
|
118
142
|
},
|
|
119
143
|
];
|
|
120
144
|
}
|
|
@@ -166,6 +190,14 @@ export function isHookInstalled(homeDir: string, key: HookAgentKey): boolean {
|
|
|
166
190
|
const instructionsPath = path.join(homeDir, ".agent-memory", "hooks", "opencode.md");
|
|
167
191
|
return list.includes(instructionsPath);
|
|
168
192
|
}
|
|
193
|
+
if (key === "pi") {
|
|
194
|
+
// Live filesystem state is authoritative — pi-memory can be installed or
|
|
195
|
+
// removed outside agent-memory (manually, or via `pi uninstall`) at any
|
|
196
|
+
// time, so a recorded delegate attempt must never override what's
|
|
197
|
+
// actually on disk. The state file is diagnostic-only (surfaced
|
|
198
|
+
// separately in `doctor`'s detail text), not a substitute for this check.
|
|
199
|
+
return fs.existsSync(path.join(homeDir, ".pi", "agent", "memory"));
|
|
200
|
+
}
|
|
169
201
|
} catch {
|
|
170
202
|
return false;
|
|
171
203
|
}
|
|
@@ -269,6 +301,81 @@ function writeJson(filePath: string, data: unknown) {
|
|
|
269
301
|
fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`, "utf-8");
|
|
270
302
|
}
|
|
271
303
|
|
|
304
|
+
// ---------------------------------------------------------------------------
|
|
305
|
+
// pi hook support (delegates to the pi-memory extension rather than editing
|
|
306
|
+
// a JSON/TOML hook config — pi's extensibility model is a registered .ts
|
|
307
|
+
// extension, installed via its own package manager).
|
|
308
|
+
// ---------------------------------------------------------------------------
|
|
309
|
+
|
|
310
|
+
export interface PiMemoryState {
|
|
311
|
+
lastAttemptAt: string;
|
|
312
|
+
ok: boolean;
|
|
313
|
+
detail: string;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function piMemoryStatePath(): string {
|
|
317
|
+
return path.join(getMemoryDir(), "pi-memory-state.json");
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function writePiMemoryState(ok: boolean, detail: string): void {
|
|
321
|
+
try {
|
|
322
|
+
writeJson(piMemoryStatePath(), { lastAttemptAt: new Date().toISOString(), ok, detail });
|
|
323
|
+
} catch {
|
|
324
|
+
// best-effort bookkeeping only — never block the actual install/uninstall result on this.
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** Read the last recorded `pi install npm:pi-memory` attempt, if any. Never throws. */
|
|
329
|
+
export function getPiMemoryState(): PiMemoryState | null {
|
|
330
|
+
try {
|
|
331
|
+
const filePath = piMemoryStatePath();
|
|
332
|
+
if (!fs.existsSync(filePath)) return null;
|
|
333
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
334
|
+
if (typeof parsed?.lastAttemptAt !== "string" || typeof parsed?.ok !== "boolean") return null;
|
|
335
|
+
return { lastAttemptAt: parsed.lastAttemptAt, ok: parsed.ok, detail: String(parsed.detail ?? "") };
|
|
336
|
+
} catch {
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function installPiMemoryDelegate(homeDir: string): HookInstallResult {
|
|
342
|
+
const memoryPath = path.join(homeDir, ".pi", "agent", "memory");
|
|
343
|
+
try {
|
|
344
|
+
const stdout = execFileSyncFn("pi", ["install", "npm:pi-memory"], {
|
|
345
|
+
encoding: "utf-8",
|
|
346
|
+
timeout: 30_000,
|
|
347
|
+
});
|
|
348
|
+
const detail = typeof stdout === "string" ? stdout.trim() : "";
|
|
349
|
+
writePiMemoryState(true, detail);
|
|
350
|
+
return { key: "pi", label: "pi (via pi-memory)", installed: true, path: memoryPath, reason: detail || undefined };
|
|
351
|
+
} catch (err) {
|
|
352
|
+
const detail =
|
|
353
|
+
err && typeof err === "object" && "stderr" in err && err.stderr
|
|
354
|
+
? String(err.stderr).trim()
|
|
355
|
+
: err instanceof Error
|
|
356
|
+
? err.message
|
|
357
|
+
: String(err);
|
|
358
|
+
writePiMemoryState(false, detail);
|
|
359
|
+
return { key: "pi", label: "pi (via pi-memory)", installed: false, reason: detail };
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function uninstallPiMemoryDelegate(homeDir: string): HookInstallResult {
|
|
364
|
+
// agent-memory never owns this install, so it never runs `pi uninstall pi-memory` — but
|
|
365
|
+
// silently doing nothing would let an `agent-memory uninstall` report read as "fully cleaned
|
|
366
|
+
// up" while pi-memory keeps running. Phrase the reason distinctly when it's actually still
|
|
367
|
+
// active so callers (and cmdUninstall's step detail) can surface that honestly.
|
|
368
|
+
const stillActive = fs.existsSync(path.join(homeDir, ".pi", "agent", "memory"));
|
|
369
|
+
return {
|
|
370
|
+
key: "pi",
|
|
371
|
+
label: "pi (via pi-memory)",
|
|
372
|
+
installed: false,
|
|
373
|
+
reason: stillActive
|
|
374
|
+
? "pi-memory left installed (not managed by agent-memory) — run `pi uninstall pi-memory` to remove it"
|
|
375
|
+
: "not installed",
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
272
379
|
/**
|
|
273
380
|
* Idempotently upsert the agent-memory-managed hook group for `eventKey`
|
|
274
381
|
* (SessionStart or UserPromptSubmit) with `command`. Returns `{ changed,
|
|
@@ -581,6 +688,88 @@ function installOpencodeInstructions(homeDir: string): HookInstallResult {
|
|
|
581
688
|
return { key: "opencode", label: "opencode", installed: true, path: configPath, backup };
|
|
582
689
|
}
|
|
583
690
|
|
|
691
|
+
function installQoderHook(homeDir: string): HookInstallResult {
|
|
692
|
+
const settingsPath = path.join(homeDir, ".qoder", "settings.json");
|
|
693
|
+
const backup = backupOnce(settingsPath);
|
|
694
|
+
const settings = readJsonConfig(settingsPath);
|
|
695
|
+
const hooks = (settings.hooks as Record<string, unknown>) ?? {};
|
|
696
|
+
const sessionStart = Array.isArray(hooks.SessionStart) ? [...(hooks.SessionStart as unknown[])] : [];
|
|
697
|
+
|
|
698
|
+
const command = "agent-memory context";
|
|
699
|
+
let managed = 0;
|
|
700
|
+
let updated = 0;
|
|
701
|
+
for (const group of sessionStart) {
|
|
702
|
+
if (!group || typeof group !== "object") continue;
|
|
703
|
+
const g = group as Record<string, unknown>;
|
|
704
|
+
const list = Array.isArray(g.hooks) ? (g.hooks as unknown[]) : [];
|
|
705
|
+
for (const hook of list) {
|
|
706
|
+
if (!hook || typeof hook !== "object") continue;
|
|
707
|
+
const managedHook = hook as Record<string, unknown>;
|
|
708
|
+
if (managedHook[HOOK_MARKER_JSON] !== true) continue;
|
|
709
|
+
managed++;
|
|
710
|
+
if (managedHook.command !== command) {
|
|
711
|
+
managedHook.command = command;
|
|
712
|
+
updated++;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
if (managed && !updated) {
|
|
717
|
+
return { key: "qoder", label: "Qoder", installed: false, path: settingsPath, reason: "already installed" };
|
|
718
|
+
}
|
|
719
|
+
if (updated) {
|
|
720
|
+
hooks.SessionStart = sessionStart;
|
|
721
|
+
settings.hooks = hooks;
|
|
722
|
+
writeJson(settingsPath, settings);
|
|
723
|
+
return { key: "qoder", label: "Qoder", installed: true, path: settingsPath, backup, reason: "updated" };
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
sessionStart.push({
|
|
727
|
+
hooks: [{ type: "command", command, [HOOK_MARKER_JSON]: true }],
|
|
728
|
+
});
|
|
729
|
+
hooks.SessionStart = sessionStart;
|
|
730
|
+
settings.hooks = hooks;
|
|
731
|
+
writeJson(settingsPath, settings);
|
|
732
|
+
return { key: "qoder", label: "Qoder", installed: true, path: settingsPath, backup };
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
function uninstallQoderHook(homeDir: string): HookInstallResult {
|
|
736
|
+
const settingsPath = path.join(homeDir, ".qoder", "settings.json");
|
|
737
|
+
if (!fs.existsSync(settingsPath)) {
|
|
738
|
+
return { key: "qoder", label: "Qoder", installed: false, reason: "not installed" };
|
|
739
|
+
}
|
|
740
|
+
const settings = readJsonConfig(settingsPath);
|
|
741
|
+
const hooks = (settings.hooks as Record<string, unknown>) ?? {};
|
|
742
|
+
const sessionStart = Array.isArray(hooks.SessionStart) ? (hooks.SessionStart as unknown[]) : [];
|
|
743
|
+
let removed = 0;
|
|
744
|
+
const filtered = sessionStart
|
|
745
|
+
.map((group) => {
|
|
746
|
+
if (!group || typeof group !== "object") return group;
|
|
747
|
+
const g = { ...(group as Record<string, unknown>) };
|
|
748
|
+
const list = Array.isArray(g.hooks) ? (g.hooks as unknown[]) : [];
|
|
749
|
+
const kept = list.filter((h) => {
|
|
750
|
+
const isOurs = h && typeof h === "object" && (h as Record<string, unknown>)[HOOK_MARKER_JSON] === true;
|
|
751
|
+
if (isOurs) removed++;
|
|
752
|
+
return !isOurs;
|
|
753
|
+
});
|
|
754
|
+
g.hooks = kept;
|
|
755
|
+
return g;
|
|
756
|
+
})
|
|
757
|
+
.filter((group) => {
|
|
758
|
+
if (!group || typeof group !== "object") return true;
|
|
759
|
+
const g = group as Record<string, unknown>;
|
|
760
|
+
return Array.isArray(g.hooks) && (g.hooks as unknown[]).length > 0;
|
|
761
|
+
});
|
|
762
|
+
if (removed === 0) {
|
|
763
|
+
return { key: "qoder", label: "Qoder", installed: false, reason: "not installed" };
|
|
764
|
+
}
|
|
765
|
+
hooks.SessionStart = filtered;
|
|
766
|
+
if (filtered.length === 0) delete (hooks as Record<string, unknown>).SessionStart;
|
|
767
|
+
if (Object.keys(hooks).length === 0) delete (settings as Record<string, unknown>).hooks;
|
|
768
|
+
else settings.hooks = hooks;
|
|
769
|
+
writeJson(settingsPath, settings);
|
|
770
|
+
return { key: "qoder", label: "Qoder", installed: true, path: settingsPath };
|
|
771
|
+
}
|
|
772
|
+
|
|
584
773
|
export function installHooks(agents: Set<HookAgentKey>, mode: HookMode = "per-turn"): InstallHooksReport {
|
|
585
774
|
const { homeDir, targets } = detectHookAgents();
|
|
586
775
|
if (!homeDir) {
|
|
@@ -619,6 +808,8 @@ export function installHooks(agents: Set<HookAgentKey>, mode: HookMode = "per-tu
|
|
|
619
808
|
else if (target.key === "codex") result = installCodexHook(homeDir, mode);
|
|
620
809
|
else if (target.key === "cursor") result = installCursorHook(homeDir);
|
|
621
810
|
else if (target.key === "opencode") result = installOpencodeInstructions(homeDir);
|
|
811
|
+
else if (target.key === "pi") result = installPiMemoryDelegate(homeDir);
|
|
812
|
+
else if (target.key === "qoder") result = installQoderHook(homeDir);
|
|
622
813
|
else continue;
|
|
623
814
|
results.push(result);
|
|
624
815
|
if (result.installed) anyInstalled = true;
|
|
@@ -773,7 +964,7 @@ export function uninstallHooks(agents?: Set<HookAgentKey>): UninstallHooksReport
|
|
|
773
964
|
error: "Home directory not found. Set HOME (or USERPROFILE on Windows) and retry.",
|
|
774
965
|
};
|
|
775
966
|
}
|
|
776
|
-
const keys: HookAgentKey[] = ["claude", "codex", "cursor", "opencode"];
|
|
967
|
+
const keys: HookAgentKey[] = ["claude", "codex", "cursor", "opencode", "pi", "qoder"];
|
|
777
968
|
const results: HookInstallResult[] = [];
|
|
778
969
|
for (const key of keys) {
|
|
779
970
|
if (agents && !agents.has(key)) continue;
|
|
@@ -782,6 +973,8 @@ export function uninstallHooks(agents?: Set<HookAgentKey>): UninstallHooksReport
|
|
|
782
973
|
else if (key === "codex") results.push(uninstallCodexHook(homeDir));
|
|
783
974
|
else if (key === "cursor") results.push(uninstallCursorHook(homeDir));
|
|
784
975
|
else if (key === "opencode") results.push(uninstallOpencodeInstructions(homeDir));
|
|
976
|
+
else if (key === "pi") results.push(uninstallPiMemoryDelegate(homeDir));
|
|
977
|
+
else if (key === "qoder") results.push(uninstallQoderHook(homeDir));
|
|
785
978
|
} catch (err) {
|
|
786
979
|
results.push({
|
|
787
980
|
key,
|