myagentmemory 0.4.17 → 0.5.1

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/src/cli.ts DELETED
@@ -1,1332 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * agent-memory CLI
4
- *
5
- * Subcommands:
6
- * version — Print binary version
7
- * install-skills — Install (or --uninstall) SKILL.md files into local agent directories
8
- * context — Build & print context injection string to stdout
9
- * write — Write to memory files
10
- * read — Read memory files
11
- * scratchpad — Manage checklist
12
- * search — Search via qmd
13
- * init — Create dirs, detect qmd, setup collection
14
- * status — Show config, qmd status, file counts
15
- * completion — Install or print shell completion
16
- * install-hooks — Install managed session-start hooks
17
- * uninstall-hooks — Remove managed session-start hooks
18
- * plugin — Discover and bootstrap optional official plugins
19
- *
20
- * Global flags:
21
- * --dir <path> Override memory directory
22
- * --json Machine-readable JSON output
23
- */
24
-
25
- import { spawn } from "node:child_process";
26
- import * as fs from "node:fs";
27
-
28
- import { COMMAND_DESCRIPTIONS, COMMANDS } from "./cli-spec.js";
29
- import { type CompletionShell, detectCompletionShell, generateCompletion, installCompletion } from "./completions.js";
30
-
31
- import {
32
- _setBaseDir,
33
- buildMemoryContext,
34
- checkCollection,
35
- dailyPath,
36
- detectQmd,
37
- distilMemories,
38
- ensureDirs,
39
- ensureQmdAvailableForSync,
40
- ensureQmdAvailableForUpdate,
41
- getCollectionName,
42
- getDailyDir,
43
- getMemoryDir,
44
- getMemoryFile,
45
- getQmdEmbedMode,
46
- getQmdHealth,
47
- getQmdResultPath,
48
- getQmdResultText,
49
- getScratchpadFile,
50
- getTopicsDir,
51
- installSkills,
52
- memoryWrite,
53
- nowTimestamp,
54
- parseScratchpad,
55
- probeEmbeddings,
56
- readFileSafe,
57
- redactSecrets,
58
- runQmdEmbedDetached,
59
- runQmdSearch,
60
- runQmdSync,
61
- runQmdUpdateNow,
62
- scheduleQmdUpdate,
63
- searchRelevantMemories,
64
- serializeScratchpad,
65
- setupQmdCollection,
66
- slugifyTopic,
67
- todayStr,
68
- topicPath,
69
- uninstallSkills,
70
- } from "./core.js";
71
- import { detectHookAgents, type HookAgentKey, installHooks, uninstallHooks } from "./hooks.js";
72
- import {
73
- createDefaultPluginBootstrap,
74
- PluginBootstrapFailure,
75
- type PluginBootstrapResultV1,
76
- } from "./plugin-bootstrap.js";
77
- import type { PluginContextSectionV1 } from "./plugin-host.js";
78
- import { InstalledPluginRuntimeV1 } from "./plugin-runtime.js";
79
-
80
- declare const __VERSION__: string;
81
-
82
- function readPackageVersion(): string {
83
- try {
84
- const packageJson = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf-8")) as {
85
- version?: unknown;
86
- };
87
- return typeof packageJson.version === "string" ? packageJson.version : "dev";
88
- } catch {
89
- return "dev";
90
- }
91
- }
92
-
93
- const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : readPackageVersion();
94
-
95
- // ---------------------------------------------------------------------------
96
- // Arg parsing (no external deps)
97
- // ---------------------------------------------------------------------------
98
-
99
- interface ParsedArgs {
100
- command: string;
101
- flags: Record<string, string | boolean>;
102
- positional: string[];
103
- }
104
-
105
- function parseArgs(argv: string[]): ParsedArgs {
106
- const flags: Record<string, string | boolean> = {};
107
- const positional: string[] = [];
108
- let command = "";
109
-
110
- for (let i = 0; i < argv.length; i++) {
111
- const arg = argv[i];
112
-
113
- if (!command && !arg.startsWith("-")) {
114
- command = arg;
115
- continue;
116
- }
117
-
118
- if (arg.startsWith("--")) {
119
- const key = arg.slice(2);
120
- const next = argv[i + 1];
121
- if (next && !next.startsWith("--")) {
122
- flags[key] = next;
123
- i++;
124
- } else {
125
- flags[key] = true;
126
- }
127
- } else if (!arg.startsWith("-")) {
128
- positional.push(arg);
129
- }
130
- }
131
-
132
- return { command, flags, positional };
133
- }
134
-
135
- function getFlag(flags: Record<string, string | boolean>, key: string): string | undefined {
136
- const val = flags[key];
137
- return typeof val === "string" ? val : undefined;
138
- }
139
-
140
- function hasFlag(flags: Record<string, string | boolean>, key: string): boolean {
141
- return key in flags;
142
- }
143
-
144
- // ---------------------------------------------------------------------------
145
- // Output helpers
146
- // ---------------------------------------------------------------------------
147
-
148
- function output(data: unknown, json: boolean) {
149
- if (json) {
150
- console.log(JSON.stringify(data, null, 2));
151
- } else if (typeof data === "string") {
152
- console.log(data);
153
- } else {
154
- console.log(JSON.stringify(data, null, 2));
155
- }
156
- }
157
-
158
- function exitError(message: string, json: boolean): never {
159
- if (json) {
160
- console.error(JSON.stringify({ error: message }));
161
- } else {
162
- console.error(`Error: ${message}`);
163
- }
164
- process.exit(1);
165
- }
166
-
167
- function openExternalUrl(url: string): boolean {
168
- let parsed: URL;
169
- try {
170
- parsed = new URL(url);
171
- } catch {
172
- return false;
173
- }
174
- if (parsed.protocol !== "https:") return false;
175
- try {
176
- const child =
177
- process.platform === "darwin"
178
- ? spawn("open", [parsed.toString()], { detached: true, stdio: "ignore" })
179
- : process.platform === "win32"
180
- ? spawn("explorer.exe", [parsed.toString()], { detached: true, stdio: "ignore" })
181
- : spawn("xdg-open", [parsed.toString()], { detached: true, stdio: "ignore" });
182
- child.unref();
183
- return true;
184
- } catch {
185
- return false;
186
- }
187
- }
188
-
189
- function printProOverview(installed: boolean): void {
190
- console.log("");
191
- console.log("Core remembers what you save. Pro learns from what you do.");
192
- console.log("");
193
- console.log("AgentMemory Pro:");
194
- console.log(" Recall coding history Find decisions and context across Pi, Codex, and Claude Code sessions.");
195
- console.log(" Learn from corrections Turn repeated fixes into reviewable, reversible memory.");
196
- console.log(" See and control learning Inspect what AgentMemory remembers and why in the Memory Dashboard.");
197
- console.log("");
198
- console.log("No account is required for the free preview. Your coding history stays on this device.");
199
- console.log("");
200
- if (installed) {
201
- console.log("Try it:");
202
- console.log(' agent-memory recall "what did we decide about authentication?"');
203
- console.log(" agent-memory learn");
204
- console.log(" agent-memory dashboard");
205
- } else {
206
- console.log("Start your free Pro preview:");
207
- console.log(" agent-memory pro install");
208
- }
209
- }
210
-
211
- function printPluginResult(result: PluginBootstrapResultV1, json: boolean, allowBrowser: boolean): void {
212
- if (json) {
213
- output(result, true);
214
- } else if (result.command === "plugin.list" && result.plugins) {
215
- for (const plugin of result.plugins) {
216
- const state = plugin.available ? "available" : plugin.installed ? plugin.entitlement : "not installed";
217
- console.log(`${plugin.name}: ${state}`);
218
- }
219
- printProOverview(Boolean(result.bundle));
220
- } else {
221
- const version = result.bundle?.version ? ` ${result.bundle.version}` : "";
222
- let showOverview = false;
223
- switch (result.result) {
224
- case "installed":
225
- console.log(`AgentMemory Pro${version} installed.`);
226
- showOverview = true;
227
- break;
228
- case "upgraded":
229
- console.log(`AgentMemory Pro upgraded to${version}.`);
230
- showOverview = true;
231
- break;
232
- case "current":
233
- console.log(
234
- result.bundle
235
- ? `AgentMemory Pro${version} is installed and ready.`
236
- : "AgentMemory Pro is not installed.",
237
- );
238
- showOverview = Boolean(result.bundle);
239
- break;
240
- case "update_available":
241
- console.log(`AgentMemory Pro${version} has an update available.`);
242
- break;
243
- case "uninstalled":
244
- console.log("AgentMemory Pro executable components were removed. Memory and billing state were preserved.");
245
- break;
246
- case "not_installed":
247
- console.log("AgentMemory Pro is not installed.");
248
- console.log("Run: agent-memory pro install");
249
- break;
250
- case "auth_required":
251
- console.log("Run agent-memory pro install to activate the free preview.");
252
- break;
253
- case "renewal_required":
254
- console.log("Renew AgentMemory Pro to continue using paid capabilities.");
255
- break;
256
- default:
257
- console.log(result.error?.message ?? "AgentMemory Pro is currently unavailable.");
258
- }
259
- if (showOverview) printProOverview(true);
260
- }
261
-
262
- if (result.nextAction) {
263
- if (allowBrowser && openExternalUrl(result.nextAction.url)) {
264
- if (!json) console.log("Opened the AgentMemory account website.");
265
- } else if (!json) {
266
- console.log(`Open: ${result.nextAction.url}`);
267
- }
268
- if (!json && result.nextAction.userCode) console.log(`Code: ${result.nextAction.userCode}`);
269
- }
270
- if (!result.ok) process.exitCode = 1;
271
- }
272
-
273
- // ---------------------------------------------------------------------------
274
- // Commands
275
- // ---------------------------------------------------------------------------
276
-
277
- async function cmdContext(flags: Record<string, string | boolean>) {
278
- const json = hasFlag(flags, "json");
279
- const noSearch = hasFlag(flags, "no-search");
280
- const query = getFlag(flags, "query") ?? "";
281
-
282
- ensureDirs();
283
- if (!noSearch && query) await ensureQmdAvailableForSync();
284
- const searchResults = noSearch ? "" : await searchRelevantMemories(query);
285
- const coreContext = buildMemoryContext(searchResults);
286
- let pluginSections: PluginContextSectionV1[] = [];
287
- try {
288
- pluginSections = await new InstalledPluginRuntimeV1({ coreVersion: VERSION }).provideContext({
289
- host: "agent-memory-cli",
290
- cwd: process.cwd(),
291
- query: query || undefined,
292
- signal: new AbortController().signal,
293
- });
294
- } catch {
295
- // Optional Pro context must never make public-core context unavailable.
296
- }
297
- const pluginContext = pluginSections.map((section) => `${section.label}\n\n${section.content}`).join("\n\n");
298
- const context = [coreContext, pluginContext].filter(Boolean).join("\n\n");
299
-
300
- if (json) {
301
- output({ context, directory: getMemoryDir(), ...(pluginSections.length ? { pluginSections } : {}) }, true);
302
- } else {
303
- if (context) {
304
- process.stdout.write(context);
305
- }
306
- }
307
- }
308
-
309
- async function cmdWrite(flags: Record<string, string | boolean>) {
310
- const json = hasFlag(flags, "json");
311
- const target = getFlag(flags, "target") ?? "daily";
312
- const content = getFlag(flags, "content");
313
- const mode = getFlag(flags, "mode") ?? "append";
314
- const topic = getFlag(flags, "topic");
315
- const date = getFlag(flags, "date");
316
- const sourceUri = getFlag(flags, "source-uri");
317
-
318
- if (!["long_term", "daily", "topic"].includes(target)) {
319
- exitError("--target must be 'long_term', 'daily', or 'topic' (default: daily)", json);
320
- }
321
- if (!["append", "overwrite"].includes(mode)) {
322
- exitError("--mode must be 'append' or 'overwrite'", json);
323
- }
324
- if (!content) {
325
- exitError("--content is required", json);
326
- }
327
-
328
- const result = await memoryWrite({
329
- target: target as "long_term" | "daily" | "topic",
330
- content,
331
- mode: mode as "append" | "overwrite",
332
- sessionId: "cli",
333
- topic,
334
- date,
335
- sourceUri,
336
- });
337
- if (result.isError) exitError(result.text.replace(/^Error:\s*/, ""), json);
338
- output(json ? { ok: true, ...result.details } : result.text.split("\n\n", 1)[0], json);
339
- }
340
-
341
- async function cmdRead(flags: Record<string, string | boolean>) {
342
- const json = hasFlag(flags, "json");
343
- const target = getFlag(flags, "target");
344
- const date = getFlag(flags, "date");
345
- const topic = getFlag(flags, "topic");
346
-
347
- if (!target || !["long_term", "scratchpad", "daily", "list", "topic", "topics"].includes(target)) {
348
- exitError("--target must be 'long_term', 'scratchpad', 'daily', 'list', 'topic', or 'topics'", json);
349
- }
350
-
351
- ensureDirs();
352
-
353
- if (target === "list") {
354
- try {
355
- const files = fs
356
- .readdirSync(getDailyDir())
357
- .filter((f) => f.endsWith(".md"))
358
- .sort()
359
- .reverse();
360
- if (json) {
361
- output({ files }, true);
362
- } else if (files.length === 0) {
363
- console.log("No daily logs found.");
364
- } else {
365
- console.log(`Daily logs:\n${files.map((f) => `- ${f}`).join("\n")}`);
366
- }
367
- } catch {
368
- output(json ? { files: [] } : "No daily logs directory.", json);
369
- }
370
- return;
371
- }
372
-
373
- if (target === "daily") {
374
- const d = date ?? todayStr();
375
- const filePath = dailyPath(d);
376
- const content = readFileSafe(filePath);
377
- if (!content) {
378
- output(json ? { content: null, date: d } : `No daily log for ${d}.`, json);
379
- return;
380
- }
381
- output(json ? { content, date: d, path: filePath } : content, json);
382
- return;
383
- }
384
-
385
- if (target === "topics") {
386
- try {
387
- const files = fs
388
- .readdirSync(getTopicsDir())
389
- .filter((f) => f.endsWith(".md"))
390
- .sort()
391
- .reverse();
392
- if (json) {
393
- output({ files }, true);
394
- } else if (files.length === 0) {
395
- console.log("No topics found.");
396
- } else {
397
- console.log(`Topics:\n${files.map((f) => `- ${f}`).join("\n")}`);
398
- }
399
- } catch {
400
- output(json ? { files: [] } : "No topics directory.", json);
401
- }
402
- return;
403
- }
404
-
405
- if (target === "topic") {
406
- if (!topic) {
407
- exitError("--topic is required when --target is 'topic'", json);
408
- }
409
- const slug = slugifyTopic(topic);
410
- const filePath = topicPath(slug);
411
- const content = readFileSafe(filePath);
412
- if (!content) {
413
- output(json ? { content: null, topic } : `No topic file found for ${topic}.`, json);
414
- return;
415
- }
416
- output(json ? { content, topic, slug, path: filePath } : content, json);
417
- return;
418
- }
419
-
420
- if (target === "scratchpad") {
421
- const content = readFileSafe(getScratchpadFile());
422
- if (!content?.trim()) {
423
- output(json ? { content: null } : "SCRATCHPAD.md is empty or does not exist.", json);
424
- return;
425
- }
426
- output(json ? { content, path: getScratchpadFile() } : content, json);
427
- return;
428
- }
429
-
430
- // long_term
431
- const content = readFileSafe(getMemoryFile());
432
- if (!content) {
433
- output(json ? { content: null } : "MEMORY.md is empty or does not exist.", json);
434
- return;
435
- }
436
- output(json ? { content, path: getMemoryFile() } : content, json);
437
- }
438
-
439
- async function cmdScratchpad(flags: Record<string, string | boolean>, positional: string[]) {
440
- const json = hasFlag(flags, "json");
441
- const action = positional[0];
442
- const text = getFlag(flags, "text");
443
-
444
- if (!action || !["add", "done", "undo", "clear_done", "list"].includes(action)) {
445
- exitError("Usage: agent-memory scratchpad <add|done|undo|clear_done|list> [--text <text>]", json);
446
- }
447
-
448
- ensureDirs();
449
- const spFile = getScratchpadFile();
450
- const existing = readFileSafe(spFile) ?? "";
451
- let items = parseScratchpad(existing).map((item) => ({
452
- ...item,
453
- text: redactSecrets(item.text).content,
454
- meta: redactSecrets(item.meta).content,
455
- }));
456
-
457
- if (action === "list") {
458
- if (items.length === 0) {
459
- output(json ? { items: [], count: 0, open: 0 } : "Scratchpad is empty.", json);
460
- return;
461
- }
462
- if (json) {
463
- output(
464
- {
465
- items: items.map((i) => ({ done: i.done, text: i.text })),
466
- count: items.length,
467
- open: items.filter((i) => !i.done).length,
468
- },
469
- true,
470
- );
471
- } else {
472
- console.log(serializeScratchpad(items));
473
- }
474
- return;
475
- }
476
-
477
- if (action === "add") {
478
- if (!text) exitError("--text is required for add", json);
479
- const ts = nowTimestamp();
480
- const safeText = redactSecrets(text!).content;
481
- items.push({ done: false, text: safeText, meta: `<!-- ${ts} [cli] -->` });
482
- fs.writeFileSync(spFile, serializeScratchpad(items), "utf-8");
483
- await ensureQmdAvailableForUpdate();
484
- scheduleQmdUpdate();
485
- output(json ? { ok: true, action, text: safeText } : `Added: - [ ] ${safeText}`, json);
486
- return;
487
- }
488
-
489
- if (action === "done" || action === "undo") {
490
- if (!text) exitError(`--text is required for ${action}`, json);
491
- const needle = text!.toLowerCase();
492
- const targetDone = action === "done";
493
- let matched = false;
494
- for (const item of items) {
495
- if (item.done !== targetDone && item.text.toLowerCase().includes(needle)) {
496
- item.done = targetDone;
497
- matched = true;
498
- break;
499
- }
500
- }
501
- if (!matched) {
502
- exitError(`No matching ${targetDone ? "open" : "done"} item found for: "${text}"`, json);
503
- }
504
- fs.writeFileSync(spFile, serializeScratchpad(items), "utf-8");
505
- await ensureQmdAvailableForUpdate();
506
- scheduleQmdUpdate();
507
- output(json ? { ok: true, action, text } : "Updated.", json);
508
- return;
509
- }
510
-
511
- if (action === "clear_done") {
512
- const before = items.length;
513
- items = items.filter((i) => !i.done);
514
- const removed = before - items.length;
515
- fs.writeFileSync(spFile, serializeScratchpad(items), "utf-8");
516
- await ensureQmdAvailableForUpdate();
517
- scheduleQmdUpdate();
518
- output(json ? { ok: true, action, removed } : `Cleared ${removed} done item(s).`, json);
519
- }
520
- }
521
-
522
- async function cmdSearch(flags: Record<string, string | boolean>) {
523
- const json = hasFlag(flags, "json");
524
- const query = getFlag(flags, "query");
525
- const mode = (getFlag(flags, "mode") ?? "keyword") as "keyword" | "semantic" | "deep";
526
- const limit = Number.parseInt(getFlag(flags, "limit") ?? "5", 10);
527
-
528
- if (!query) exitError("--query is required", json);
529
- if (!["keyword", "semantic", "deep"].includes(mode)) {
530
- exitError("--mode must be 'keyword', 'semantic', or 'deep'", json);
531
- }
532
-
533
- const qmdFound = await detectQmd();
534
- if (!qmdFound) {
535
- exitError("qmd is not installed. Install: bun install -g https://github.com/tobi/qmd", json);
536
- }
537
-
538
- const collName = getCollectionName();
539
- const hasCollection = await checkCollection(collName);
540
- if (!hasCollection) {
541
- exitError(`qmd collection '${collName}' not found. Run: agent-memory init`, json);
542
- }
543
-
544
- try {
545
- const { results, stderr } = await runQmdSearch(mode, query!, limit);
546
-
547
- if (json) {
548
- output({ mode, query, count: results.length, results }, true);
549
- return;
550
- }
551
-
552
- if (results.length === 0) {
553
- const needsEmbed = /need embeddings/i.test(stderr ?? "");
554
- if (needsEmbed && (mode === "semantic" || mode === "deep")) {
555
- console.log(`No results found. qmd reports missing embeddings — run: qmd embed`);
556
- } else {
557
- console.log(`No results found for "${query}" (mode: ${mode}).`);
558
- }
559
- return;
560
- }
561
-
562
- for (let i = 0; i < results.length; i++) {
563
- const r = results[i];
564
- const filePath = getQmdResultPath(r);
565
- const text = getQmdResultText(r);
566
- console.log(`--- Result ${i + 1} ---`);
567
- if (filePath) console.log(`File: ${filePath}`);
568
- if (r.score != null) console.log(`Score: ${r.score}`);
569
- if (text) console.log(text);
570
- console.log("");
571
- }
572
- } catch (err) {
573
- exitError(`Search failed: ${err instanceof Error ? err.message : String(err)}`, json);
574
- }
575
- }
576
-
577
- function cmdInstallSkills(flags: Record<string, string | boolean>) {
578
- const json = hasFlag(flags, "json");
579
- const uninstall = hasFlag(flags, "uninstall");
580
-
581
- if (uninstall) {
582
- const report = uninstallSkills();
583
-
584
- if (!report.ok) {
585
- exitError(report.error ?? "Failed to uninstall skills.", json);
586
- }
587
-
588
- if (json) {
589
- output(report, true);
590
- return;
591
- }
592
-
593
- for (const item of report.removed) {
594
- console.log(`Uninstalled ${item.label}: ${item.path}`);
595
- }
596
- for (const item of report.skipped) {
597
- console.log(`Skipping ${item.label} (${item.reason})`);
598
- }
599
- if (report.removed.length === 0) {
600
- console.log("No skills were installed.");
601
- }
602
- return;
603
- }
604
-
605
- const report = installSkills();
606
-
607
- if (!report.ok) {
608
- exitError(report.error ?? "Failed to install skills.", json);
609
- }
610
-
611
- if (json) {
612
- output(report, true);
613
- return;
614
- }
615
-
616
- if (report.checked.length > 0) {
617
- for (const item of report.checked) {
618
- if (item.status === "detected") {
619
- console.log(`Detecting ${item.label}... found`);
620
- } else {
621
- console.log(`Detecting ${item.label}... not found (${item.reason ?? "unknown"})`);
622
- }
623
- }
624
- } else if (report.detected.length === 0) {
625
- console.log("No supported agent installations detected.");
626
- } else {
627
- const detectedLabels = report.detected.map((item) => item.label).join(", ");
628
- console.log(`Detected: ${detectedLabels}`);
629
- }
630
-
631
- if (report.installed.length === 0) {
632
- console.log("No skills installed.");
633
- } else {
634
- for (const item of report.installed) {
635
- console.log(`Installed ${item.label}: ${item.path}`);
636
- }
637
- }
638
-
639
- if (report.skipped.length > 0) {
640
- for (const item of report.skipped) {
641
- console.log(`Skipped ${item.label} (${item.reason})`);
642
- }
643
- }
644
- }
645
-
646
- async function promptYesNo(question: string, defaultYes: boolean): Promise<boolean> {
647
- const readline = await import("node:readline/promises");
648
- const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
649
- try {
650
- const answer = (await rl.question(`${question} ${defaultYes ? "[Y/n]" : "[y/N]"} `)).trim().toLowerCase();
651
- if (!answer) return defaultYes;
652
- return answer === "y" || answer === "yes";
653
- } finally {
654
- rl.close();
655
- }
656
- }
657
-
658
- async function cmdInstallHooks(flags: Record<string, string | boolean>): Promise<void> {
659
- const json = hasFlag(flags, "json");
660
- const requested = getFlag(flags, "only");
661
- const requestedKeys = requested ? new Set(requested.split(",").map((value) => value.trim())) : null;
662
- const { homeDir, targets } = detectHookAgents();
663
- if (!homeDir) exitError("Home directory not found.", json);
664
- const eligible = targets.filter(
665
- (target) => target.supported && target.detected && (!requestedKeys || requestedKeys.has(target.key)),
666
- );
667
- const selected = new Set<HookAgentKey>();
668
- const applyAll = hasFlag(flags, "yes") || hasFlag(flags, "all") || !process.stdin.isTTY;
669
- for (const target of eligible) {
670
- if (applyAll || (await promptYesNo(`Install SessionStart hook for ${target.label}?`, true)))
671
- selected.add(target.key);
672
- }
673
- const report = installHooks(selected);
674
- if (!report.ok) exitError(report.error ?? "install failed", json);
675
- if (json) return output(report, true);
676
- if (!report.results.length) return output("No eligible agents. Nothing to install.", false);
677
- for (const result of report.results) {
678
- console.log(
679
- result.installed
680
- ? `Installed ${result.label} hook: ${result.path}`
681
- : `Skipped ${result.label} (${result.reason ?? "unknown"})`,
682
- );
683
- }
684
- }
685
-
686
- function cmdUninstallHooks(flags: Record<string, string | boolean>): void {
687
- const json = hasFlag(flags, "json");
688
- const only = getFlag(flags, "only");
689
- const agents = only ? new Set(only.split(",").map((value) => value.trim()) as HookAgentKey[]) : undefined;
690
- const report = uninstallHooks(agents);
691
- if (!report.ok) exitError(report.error ?? "uninstall failed", json);
692
- if (json) {
693
- output(report, true);
694
- return;
695
- }
696
- for (const result of report.results) {
697
- console.log(
698
- result.installed
699
- ? `Uninstalled ${result.label}: ${result.path}`
700
- : `Skipped ${result.label} (${result.reason ?? "unknown"})`,
701
- );
702
- }
703
- }
704
-
705
- function cmdCompletion(flags: Record<string, string | boolean>, positional: string[]): void {
706
- const requestedShell = positional[0];
707
- const shells: CompletionShell[] = ["bash", "zsh", "fish", "powershell"];
708
- if (requestedShell && !shells.includes(requestedShell as CompletionShell))
709
- exitError(
710
- `Unsupported shell '${requestedShell}'. Choose bash, zsh, fish, or powershell.`,
711
- hasFlag(flags, "json"),
712
- );
713
- const shell = (requestedShell as CompletionShell | undefined) ?? detectCompletionShell();
714
- if (!shell)
715
- exitError("Could not detect your shell. Specify bash, zsh, fish, or powershell.", hasFlag(flags, "json"));
716
- if (hasFlag(flags, "stdout")) {
717
- process.stdout.write(generateCompletion(shell));
718
- return;
719
- }
720
- const result = installCompletion(shell);
721
- if (hasFlag(flags, "json")) {
722
- output(result, true);
723
- return;
724
- }
725
- console.log(`Installed ${shell} completion: ${result.completionPath}`);
726
- if (result.profilePath)
727
- console.log(`${result.profileUpdated ? "Configured" : "Already configured"}: ${result.profilePath}`);
728
- }
729
-
730
- async function cmdSync(flags: Record<string, string | boolean>) {
731
- const json = hasFlag(flags, "json");
732
-
733
- ensureDirs();
734
-
735
- const qmdFound = await ensureQmdAvailableForSync();
736
- if (!qmdFound) {
737
- exitError("qmd is not installed. Install: bun install -g https://github.com/tobi/qmd", json);
738
- }
739
-
740
- const collName = getCollectionName();
741
- const hasCollection = await checkCollection(collName);
742
- if (!hasCollection) {
743
- exitError(`qmd collection '${collName}' not found. Run: agent-memory init`, json);
744
- }
745
-
746
- const result = await runQmdSync();
747
-
748
- if (json) {
749
- output({ ok: result.updateOk && result.embedOk, updateOk: result.updateOk, embedOk: result.embedOk }, true);
750
- } else {
751
- if (result.updateOk) {
752
- console.log("qmd update: ok");
753
- } else {
754
- console.log("qmd update: failed");
755
- }
756
- if (result.embedOk) {
757
- console.log("qmd embed: ok");
758
- } else {
759
- console.log("qmd embed: failed");
760
- }
761
- if (result.updateOk && result.embedOk) {
762
- console.log("\nIndex fully synced.");
763
- }
764
- }
765
- }
766
-
767
- async function cmdInit(flags: Record<string, string | boolean>) {
768
- const json = hasFlag(flags, "json");
769
-
770
- ensureDirs();
771
- const dir = getMemoryDir();
772
-
773
- const qmdFound = await detectQmd();
774
- let collectionCreated = false;
775
- let indexUpdated = false;
776
- let embedStarted = false;
777
-
778
- if (qmdFound) {
779
- const collName = getCollectionName();
780
- const hasCollection = await checkCollection(collName);
781
- if (!hasCollection) {
782
- collectionCreated = await setupQmdCollection();
783
- }
784
-
785
- // Run initial index update + start background embed
786
- await ensureQmdAvailableForUpdate();
787
- await runQmdUpdateNow();
788
- indexUpdated = true;
789
- const child = runQmdEmbedDetached();
790
- embedStarted = child !== null;
791
- }
792
-
793
- if (json) {
794
- output(
795
- {
796
- ok: true,
797
- directory: dir,
798
- qmd: qmdFound,
799
- collectionCreated,
800
- indexUpdated,
801
- embedStarted,
802
- },
803
- true,
804
- );
805
- } else {
806
- console.log(`Memory directory: ${dir}`);
807
- console.log(` MEMORY.md, SCRATCHPAD.md, daily/, topics/ created.`);
808
- if (qmdFound) {
809
- if (collectionCreated) {
810
- console.log(` qmd collection '${getCollectionName()}' created.`);
811
- } else {
812
- console.log(` qmd collection '${getCollectionName()}' already exists.`);
813
- }
814
- if (indexUpdated) {
815
- console.log(` Index updated.`);
816
- }
817
- if (embedStarted) {
818
- console.log(` Embedding started in background.`);
819
- }
820
- } else {
821
- console.log(` qmd not found — search features unavailable.`);
822
- console.log(` Install: bun install -g https://github.com/tobi/qmd`);
823
- }
824
- if (process.stdout.isTTY) {
825
- try {
826
- const plugin = await createDefaultPluginBootstrap(VERSION).list();
827
- if (plugin.result === "not_installed") {
828
- console.log("");
829
- console.log("Optional: Pro recalls coding history and learns from repeated corrections.");
830
- console.log("Try it without an account: agent-memory pro install");
831
- }
832
- } catch {
833
- // Commercial discovery must never make core initialization fail.
834
- }
835
- }
836
- }
837
- }
838
-
839
- async function cmdStatus(flags: Record<string, string | boolean>) {
840
- const json = hasFlag(flags, "json");
841
-
842
- ensureDirs();
843
- const dir = getMemoryDir();
844
- const memFile = getMemoryFile();
845
- const spFile = getScratchpadFile();
846
- const dailyDir = getDailyDir();
847
- const topicsDir = getTopicsDir();
848
-
849
- const memContent = readFileSafe(memFile);
850
- const spContent = readFileSafe(spFile);
851
-
852
- let dailyCount = 0;
853
- try {
854
- dailyCount = fs.readdirSync(dailyDir).filter((f) => f.endsWith(".md")).length;
855
- } catch {
856
- // directory may not exist
857
- }
858
- let topicCount = 0;
859
- try {
860
- topicCount = fs.readdirSync(topicsDir).filter((f) => f.endsWith(".md")).length;
861
- } catch {
862
- // directory may not exist
863
- }
864
-
865
- const qmdFound = await detectQmd();
866
- let hasCollection = false;
867
- let health = null;
868
- let embeddings: "ready" | "missing" | "unknown" | "n/a" = "n/a";
869
- if (qmdFound) {
870
- hasCollection = await checkCollection();
871
- if (hasCollection) {
872
- await ensureQmdAvailableForSync();
873
- health = await getQmdHealth();
874
- // A live semantic probe confirms embeddings are actually usable, but
875
- // it costs a real qmd query (and a possible model load), so it's
876
- // opt-in — the cheap pending-embed count below covers the common case.
877
- if (hasFlag(flags, "probe")) {
878
- embeddings = await probeEmbeddings();
879
- }
880
- }
881
- }
882
-
883
- const embedMode = getQmdEmbedMode();
884
- let officialPlugin: { installed: boolean; result: string; entitlement: string } = {
885
- installed: false,
886
- result: "unavailable",
887
- entitlement: "missing",
888
- };
889
- try {
890
- const plugin = await createDefaultPluginBootstrap(VERSION).status();
891
- officialPlugin = {
892
- installed: Boolean(plugin.bundle),
893
- result: plugin.result,
894
- entitlement: plugin.entitlement.state,
895
- };
896
- } catch {
897
- // Commercial status must never make core status fail.
898
- }
899
-
900
- if (json) {
901
- output(
902
- {
903
- directory: dir,
904
- memoryFile: {
905
- exists: memContent !== null,
906
- chars: memContent?.length ?? 0,
907
- lines: memContent ? memContent.split("\n").length : 0,
908
- },
909
- scratchpadFile: {
910
- exists: spContent !== null,
911
- items: spContent ? parseScratchpad(spContent).length : 0,
912
- openItems: spContent ? parseScratchpad(spContent).filter((i) => !i.done).length : 0,
913
- },
914
- dailyLogs: dailyCount,
915
- topics: topicCount,
916
- qmd: {
917
- available: qmdFound,
918
- collection: hasCollection ? getCollectionName() : null,
919
- health,
920
- embeddings,
921
- },
922
- embedMode,
923
- officialPlugin,
924
- },
925
- true,
926
- );
927
- } else {
928
- console.log(`Memory directory: ${dir}`);
929
- console.log("");
930
- if (memContent !== null) {
931
- const lines = memContent.split("\n").length;
932
- console.log(`MEMORY.md: ${memContent.length} chars, ${lines} lines`);
933
- } else {
934
- console.log("MEMORY.md: not created yet");
935
- }
936
- if (spContent !== null) {
937
- const items = parseScratchpad(spContent);
938
- const open = items.filter((i) => !i.done).length;
939
- console.log(`SCRATCHPAD.md: ${items.length} items (${open} open)`);
940
- } else {
941
- console.log("SCRATCHPAD.md: not created yet");
942
- }
943
- console.log(`Daily logs: ${dailyCount} file(s)`);
944
- console.log(`Topics: ${topicCount} file(s)`);
945
- console.log("");
946
- if (qmdFound) {
947
- console.log(`qmd: available`);
948
- console.log(
949
- `Collection '${getCollectionName()}': ${hasCollection ? "configured" : "not configured — run: agent-memory init"}`,
950
- );
951
- console.log(`Embed mode: ${embedMode}`);
952
- if (hasCollection && embeddings !== "n/a") {
953
- const embLabel =
954
- embeddings === "ready"
955
- ? "ready"
956
- : embeddings === "missing"
957
- ? "missing — run: agent-memory sync"
958
- : "unknown (could not verify within probe timeout)";
959
- console.log(`Embeddings (semantic/deep search): ${embLabel}`);
960
- }
961
- if (health) {
962
- if (health.totalFiles !== null) console.log(`Files indexed: ${health.totalFiles}`);
963
- if (health.vectorsEmbedded !== null) console.log(`Vectors embedded: ${health.vectorsEmbedded}`);
964
- if (health.pendingEmbed !== null && health.pendingEmbed > 0) {
965
- console.log(`Pending embeds: ${health.pendingEmbed}`);
966
- console.log(` run: agent-memory sync`);
967
- }
968
- if (health.lastUpdated) console.log(`Last updated: ${health.lastUpdated}`);
969
- }
970
- } else {
971
- console.log("qmd: not installed");
972
- }
973
- if (!officialPlugin.installed) {
974
- console.log("");
975
- console.log("AgentMemory Pro: not installed");
976
- console.log(" try without an account: agent-memory pro install");
977
- }
978
- }
979
- }
980
-
981
- async function cmdDistil(flags: Record<string, string | boolean>) {
982
- const json = hasFlag(flags, "json");
983
- const dryRun = hasFlag(flags, "dry-run");
984
-
985
- const result = await distilMemories({ dryRun });
986
-
987
- if (json) {
988
- output(result, true);
989
- } else {
990
- if (result.totalEntries === 0) {
991
- console.log(result.output.trim());
992
- return;
993
- }
994
- if (dryRun) {
995
- console.log("--- Dry run (MEMORY.md not modified) ---\n");
996
- }
997
- console.log(result.output.trim());
998
- console.log("");
999
- console.log(
1000
- `Distilled ${result.totalEntries} entries from ${result.totalDailyFiles} daily file(s) and ${result.totalTopicFiles} topic file(s), ${result.totalTags} tag(s).`,
1001
- );
1002
- if (!dryRun) {
1003
- console.log("MEMORY.md updated.");
1004
- }
1005
- }
1006
- }
1007
-
1008
- function printPluginUsage(): void {
1009
- console.log(`agent-memory plugin — optional official plugins
1010
-
1011
- Usage:
1012
- agent-memory plugin [list]
1013
- agent-memory plugin status
1014
- agent-memory plugin install [--channel stable] [--no-browser]
1015
- agent-memory plugin update [--channel stable]
1016
- agent-memory plugin uninstall --yes
1017
- agent-memory plugin manage [--no-browser]
1018
-
1019
- The public core remains fully usable without AgentMemory Pro. Install uses a random
1020
- installation identifier and requires no account or email. The free preview includes
1021
- 10 recalls and one learning scan per local day; indexing and the Memory Dashboard
1022
- remain available. Memory and session content stay on this device.`);
1023
- }
1024
-
1025
- function pluginCommandFailure(command: string, error: unknown): PluginBootstrapResultV1 {
1026
- return {
1027
- schemaVersion: 1,
1028
- command: `plugin.${command}`,
1029
- ok: false,
1030
- result: "unavailable",
1031
- bundle: null,
1032
- entitlement: {
1033
- plan: null,
1034
- state: "missing",
1035
- features: [],
1036
- capabilities: {},
1037
- },
1038
- nextAction: null,
1039
- error: {
1040
- code: error instanceof PluginBootstrapFailure ? error.code : "plugin_command_failed",
1041
- message: error instanceof Error ? error.message : String(error),
1042
- ...(error instanceof PluginBootstrapFailure && error.retryable ? { retryable: true } : {}),
1043
- },
1044
- };
1045
- }
1046
-
1047
- async function cmdPlugin(
1048
- flags: Record<string, string | boolean>,
1049
- positional: string[],
1050
- ): Promise<PluginBootstrapResultV1 | null> {
1051
- const json = hasFlag(flags, "json");
1052
- const subcommand = positional[0] ?? "list";
1053
- if (subcommand === "help" || hasFlag(flags, "help")) {
1054
- printPluginUsage();
1055
- return null;
1056
- }
1057
- const channel = getFlag(flags, "channel") ?? "stable";
1058
- if (channel !== "stable") {
1059
- const failure = pluginCommandFailure(
1060
- subcommand,
1061
- new PluginBootstrapFailure("channel_invalid", "--channel supports only 'stable'"),
1062
- );
1063
- printPluginResult(failure, json, false);
1064
- return failure;
1065
- }
1066
- const allowBrowser = !json && !hasFlag(flags, "no-browser") && Boolean(process.stdin.isTTY && process.stdout.isTTY);
1067
- const manager = createDefaultPluginBootstrap(VERSION);
1068
-
1069
- let result: PluginBootstrapResultV1;
1070
- try {
1071
- switch (subcommand) {
1072
- case "list":
1073
- result = await manager.list();
1074
- break;
1075
- case "status":
1076
- result = await manager.status(channel);
1077
- break;
1078
- case "install":
1079
- result = await manager.install({ channel, allowAuthentication: allowBrowser });
1080
- break;
1081
- case "update":
1082
- result = await manager.update({ channel, allowAuthentication: false });
1083
- break;
1084
- case "uninstall":
1085
- if (!hasFlag(flags, "yes")) {
1086
- const status = await manager.status(channel);
1087
- result = {
1088
- ...status,
1089
- command: "plugin.uninstall",
1090
- ok: false,
1091
- result: "unavailable",
1092
- error: {
1093
- code: "confirmation_required",
1094
- message: "Re-run with --yes to remove AgentMemory Pro executable components",
1095
- },
1096
- };
1097
- break;
1098
- }
1099
- result = await manager.uninstall();
1100
- break;
1101
- case "manage":
1102
- result = await manager.manage();
1103
- break;
1104
- default:
1105
- result = pluginCommandFailure(
1106
- subcommand,
1107
- new PluginBootstrapFailure(
1108
- "unknown_plugin_command",
1109
- `Unknown plugin command: ${subcommand}. Available bootstrap commands: list, status, install, update, uninstall, manage.`,
1110
- ),
1111
- );
1112
- }
1113
- } catch (error) {
1114
- result = pluginCommandFailure(subcommand, error);
1115
- }
1116
- printPluginResult(result, json, allowBrowser && (subcommand === "install" || subcommand === "manage"));
1117
- return result;
1118
- }
1119
-
1120
- async function printFirstRunProof(): Promise<void> {
1121
- try {
1122
- const result = await new InstalledPluginRuntimeV1({ coreVersion: VERSION }).run("index", {
1123
- args: [],
1124
- flags: {},
1125
- signal: new AbortController().signal,
1126
- });
1127
- if (!result?.ok || !result.data || typeof result.data !== "object") return;
1128
- const stats = (result.data as { stats?: { discovered?: Record<string, number>; selected?: number } }).stats;
1129
- if (!stats?.discovered) return;
1130
- const hosts = [
1131
- ["Claude Code", stats.discovered.claude ?? 0],
1132
- ["Codex", stats.discovered.codex ?? 0],
1133
- ["Pi", stats.discovered.pi ?? 0],
1134
- ] as const;
1135
- console.log("");
1136
- console.log("Found local coding history:");
1137
- for (const [label, count] of hosts) console.log(` ${label.padEnd(13)} ${count} sessions`);
1138
- console.log("");
1139
- console.log(`${stats.selected ?? 0} sessions available for local recall. Nothing was uploaded.`);
1140
- console.log("");
1141
- console.log('Try: agent-memory recall "what did we decide about authentication?"');
1142
- console.log("Open: agent-memory dashboard");
1143
- } catch {
1144
- // Personalized proof is helpful but must never turn a successful install into a failure.
1145
- }
1146
- }
1147
-
1148
- async function cmdPro(flags: Record<string, string | boolean>, positional: string[]): Promise<void> {
1149
- const subcommand = positional[0];
1150
- if (!subcommand) {
1151
- await cmdPlugin(flags, ["list"]);
1152
- return;
1153
- }
1154
- const mapped = subcommand === "upgrade" ? "update" : subcommand;
1155
- if (!["install", "status", "update", "manage"].includes(mapped)) {
1156
- const json = hasFlag(flags, "json");
1157
- const message = `Unknown Pro command: ${subcommand}. Available commands: install, status, upgrade, manage.`;
1158
- if (json) console.log(JSON.stringify({ error: message }));
1159
- else console.error(`Error: ${message}`);
1160
- process.exitCode = 1;
1161
- return;
1162
- }
1163
- const result = await cmdPlugin(flags, [mapped]);
1164
- if (!hasFlag(flags, "json") && mapped === "install" && ["installed", "upgraded"].includes(result?.result ?? ""))
1165
- await printFirstRunProof();
1166
- }
1167
-
1168
- // ---------------------------------------------------------------------------
1169
- // Usage
1170
- // ---------------------------------------------------------------------------
1171
-
1172
- function printUsage() {
1173
- const commandWidth = Math.max(...COMMANDS.map((command) => command.length));
1174
- const commandList = COMMANDS.map(
1175
- (command) => ` ${command.padEnd(commandWidth)} ${COMMAND_DESCRIPTIONS[command]}`,
1176
- ).join("\n");
1177
-
1178
- console.log(`agent-memory — persistent memory for coding agents
1179
-
1180
- Usage:
1181
- agent-memory <command> [options]
1182
-
1183
- Commands:
1184
- ${commandList}
1185
-
1186
- Global flags:
1187
- --dir <path> Override memory directory
1188
- --json Machine-readable JSON output
1189
-
1190
- Examples:
1191
- agent-memory init
1192
- agent-memory write --content "Fixed auth bug in login flow"
1193
- agent-memory write --target long_term --content "User prefers dark mode" --source-uri "session://agent/turn/12"
1194
- agent-memory write --target topic --topic "auth" --content "Rolled JWT refresh to edge"
1195
- agent-memory read --target long_term
1196
- agent-memory read --target daily --date 2026-02-15
1197
- agent-memory read --target list
1198
- agent-memory read --target topic --topic "auth"
1199
- agent-memory read --target topics
1200
- agent-memory scratchpad add --text "Review PR #42"
1201
- agent-memory scratchpad list
1202
- agent-memory scratchpad done --text "PR #42"
1203
- agent-memory search --query "database choice" --mode keyword
1204
- agent-memory distil --dry-run
1205
- agent-memory context --query "database choice"
1206
- agent-memory sync
1207
- agent-memory status --json
1208
- agent-memory completion zsh
1209
- agent-memory install-hooks --yes
1210
- agent-memory pro status
1211
- agent-memory pro install
1212
- agent-memory recall "what did we decide about authentication?"
1213
- agent-memory dashboard`);
1214
- }
1215
-
1216
- // ---------------------------------------------------------------------------
1217
- // Main
1218
- // ---------------------------------------------------------------------------
1219
-
1220
- async function main() {
1221
- const { command, flags, positional } = parseArgs(process.argv.slice(2));
1222
- const json = hasFlag(flags, "json");
1223
-
1224
- // Apply --dir override
1225
- const dir = getFlag(flags, "dir");
1226
- if (dir) {
1227
- _setBaseDir(dir);
1228
- }
1229
-
1230
- if (command === "version" || hasFlag(flags, "version")) {
1231
- output(json ? { version: VERSION } : VERSION, json);
1232
- return;
1233
- }
1234
-
1235
- if (!command || command === "help" || (hasFlag(flags, "help") && command !== "plugin")) {
1236
- printUsage();
1237
- return;
1238
- }
1239
-
1240
- switch (command) {
1241
- case "context":
1242
- await cmdContext(flags);
1243
- break;
1244
- case "write":
1245
- await cmdWrite(flags);
1246
- break;
1247
- case "read":
1248
- await cmdRead(flags);
1249
- break;
1250
- case "scratchpad":
1251
- await cmdScratchpad(flags, positional);
1252
- break;
1253
- case "search":
1254
- await cmdSearch(flags);
1255
- break;
1256
- case "install-skills":
1257
- cmdInstallSkills(flags);
1258
- break;
1259
- case "uninstall-skills":
1260
- cmdInstallSkills({ ...flags, uninstall: true });
1261
- break;
1262
- case "distil":
1263
- case "distill":
1264
- await cmdDistil(flags);
1265
- break;
1266
- case "sync":
1267
- await cmdSync(flags);
1268
- break;
1269
- case "init":
1270
- await cmdInit(flags);
1271
- break;
1272
- case "status":
1273
- await cmdStatus(flags);
1274
- break;
1275
- case "completion":
1276
- cmdCompletion(flags, positional);
1277
- break;
1278
- case "install-hooks":
1279
- await cmdInstallHooks(flags);
1280
- break;
1281
- case "uninstall-hooks":
1282
- cmdUninstallHooks(flags);
1283
- break;
1284
- case "hook": {
1285
- if (positional[0] !== "session-start") exitError("hook requires 'session-start'", json);
1286
- const agent = getFlag(flags, "agent");
1287
- if (!agent) exitError("hook session-start requires --agent", json);
1288
- await cmdContext({ "no-search": true });
1289
- try {
1290
- const decision = await new InstalledPluginRuntimeV1({ coreVersion: VERSION }).runSessionStart({
1291
- host: agent,
1292
- cwd: process.cwd(),
1293
- signal: new AbortController().signal,
1294
- });
1295
- if (decision?.state === "exhausted")
1296
- console.error(`AgentMemory free session allowance resets at ${decision.resetAt}`);
1297
- } catch {
1298
- // Paid SessionStart work must never make public-core context unavailable.
1299
- }
1300
- break;
1301
- }
1302
- case "plugin":
1303
- await cmdPlugin(flags, positional);
1304
- break;
1305
- case "pro":
1306
- await cmdPro(flags, positional);
1307
- break;
1308
- default: {
1309
- const controller = new AbortController();
1310
- const abort = () => controller.abort();
1311
- process.once("SIGINT", abort);
1312
- try {
1313
- const pluginCommand = command === "dashboard" ? "web" : command;
1314
- const result = await new InstalledPluginRuntimeV1({ coreVersion: VERSION }).run(pluginCommand, {
1315
- args: positional,
1316
- flags,
1317
- signal: controller.signal,
1318
- });
1319
- if (!result) exitError(`Unknown command: ${command}. Run 'agent-memory help' for usage.`, json);
1320
- if (!result.ok) exitError(result.error?.message ?? `Plugin command ${pluginCommand} failed`, json);
1321
- output(result.data ?? { ok: true }, json);
1322
- } finally {
1323
- process.removeListener("SIGINT", abort);
1324
- }
1325
- }
1326
- }
1327
- }
1328
-
1329
- main().catch((err) => {
1330
- console.error(err instanceof Error ? err.message : String(err));
1331
- process.exit(1);
1332
- });