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