myagentmemory 0.4.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/src/cli.ts ADDED
@@ -0,0 +1,596 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * agent-memory CLI
4
+ *
5
+ * Subcommands:
6
+ * context — Build & print context injection string to stdout
7
+ * write — Write to memory files
8
+ * read — Read memory files
9
+ * scratchpad — Manage checklist
10
+ * search — Search via qmd
11
+ * init — Create dirs, detect qmd, setup collection
12
+ * status — Show config, qmd status, file counts
13
+ *
14
+ * Global flags:
15
+ * --dir <path> Override memory directory
16
+ * --json Machine-readable JSON output
17
+ */
18
+
19
+ import * as fs from "node:fs";
20
+ import {
21
+ _setBaseDir,
22
+ buildMemoryContext,
23
+ checkCollection,
24
+ dailyPath,
25
+ detectQmd,
26
+ ensureDirs,
27
+ ensureQmdAvailableForUpdate,
28
+ getCollectionName,
29
+ getDailyDir,
30
+ getMemoryDir,
31
+ getMemoryFile,
32
+ getQmdResultPath,
33
+ getQmdResultText,
34
+ getScratchpadFile,
35
+ nowTimestamp,
36
+ parseScratchpad,
37
+ readFileSafe,
38
+ runQmdSearch,
39
+ scheduleQmdUpdate,
40
+ searchRelevantMemories,
41
+ serializeScratchpad,
42
+ setupQmdCollection,
43
+ todayStr,
44
+ } from "./core.js";
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // Arg parsing (no external deps)
48
+ // ---------------------------------------------------------------------------
49
+
50
+ interface ParsedArgs {
51
+ command: string;
52
+ flags: Record<string, string | boolean>;
53
+ positional: string[];
54
+ }
55
+
56
+ function parseArgs(argv: string[]): ParsedArgs {
57
+ const flags: Record<string, string | boolean> = {};
58
+ const positional: string[] = [];
59
+ let command = "";
60
+
61
+ for (let i = 0; i < argv.length; i++) {
62
+ const arg = argv[i];
63
+
64
+ if (!command && !arg.startsWith("-")) {
65
+ command = arg;
66
+ continue;
67
+ }
68
+
69
+ if (arg.startsWith("--")) {
70
+ const key = arg.slice(2);
71
+ const next = argv[i + 1];
72
+ if (next && !next.startsWith("--")) {
73
+ flags[key] = next;
74
+ i++;
75
+ } else {
76
+ flags[key] = true;
77
+ }
78
+ } else if (!arg.startsWith("-")) {
79
+ positional.push(arg);
80
+ }
81
+ }
82
+
83
+ return { command, flags, positional };
84
+ }
85
+
86
+ function getFlag(flags: Record<string, string | boolean>, key: string): string | undefined {
87
+ const val = flags[key];
88
+ return typeof val === "string" ? val : undefined;
89
+ }
90
+
91
+ function hasFlag(flags: Record<string, string | boolean>, key: string): boolean {
92
+ return key in flags;
93
+ }
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // Output helpers
97
+ // ---------------------------------------------------------------------------
98
+
99
+ function output(data: unknown, json: boolean) {
100
+ if (json) {
101
+ console.log(JSON.stringify(data, null, 2));
102
+ } else if (typeof data === "string") {
103
+ console.log(data);
104
+ } else {
105
+ console.log(JSON.stringify(data, null, 2));
106
+ }
107
+ }
108
+
109
+ function exitError(message: string, json: boolean): never {
110
+ if (json) {
111
+ console.error(JSON.stringify({ error: message }));
112
+ } else {
113
+ console.error(`Error: ${message}`);
114
+ }
115
+ process.exit(1);
116
+ }
117
+
118
+ // ---------------------------------------------------------------------------
119
+ // Commands
120
+ // ---------------------------------------------------------------------------
121
+
122
+ async function cmdContext(flags: Record<string, string | boolean>) {
123
+ const json = hasFlag(flags, "json");
124
+ const noSearch = hasFlag(flags, "no-search");
125
+
126
+ ensureDirs();
127
+ const searchResults = noSearch ? "" : await searchRelevantMemories("");
128
+ const context = buildMemoryContext(searchResults);
129
+
130
+ if (json) {
131
+ output({ context, directory: getMemoryDir() }, true);
132
+ } else {
133
+ if (context) {
134
+ process.stdout.write(context);
135
+ }
136
+ }
137
+ }
138
+
139
+ async function cmdWrite(flags: Record<string, string | boolean>) {
140
+ const json = hasFlag(flags, "json");
141
+ const target = getFlag(flags, "target");
142
+ const content = getFlag(flags, "content");
143
+ const mode = getFlag(flags, "mode") ?? "append";
144
+
145
+ if (!target || !["long_term", "daily"].includes(target)) {
146
+ exitError("--target must be 'long_term' or 'daily'", json);
147
+ }
148
+ if (!content) {
149
+ exitError("--content is required", json);
150
+ }
151
+
152
+ ensureDirs();
153
+ const ts = nowTimestamp();
154
+ const sid = "cli";
155
+
156
+ if (target === "daily") {
157
+ const filePath = dailyPath(todayStr());
158
+ const existing = readFileSafe(filePath) ?? "";
159
+ const separator = existing.trim() ? "\n\n" : "";
160
+ const stamped = `<!-- ${ts} [${sid}] -->\n${content}`;
161
+ fs.writeFileSync(filePath, existing + separator + stamped, "utf-8");
162
+ await ensureQmdAvailableForUpdate();
163
+ scheduleQmdUpdate();
164
+ output(
165
+ json
166
+ ? { ok: true, path: filePath, target, mode: "append", timestamp: ts }
167
+ : `Appended to daily log: ${filePath}`,
168
+ json,
169
+ );
170
+ return;
171
+ }
172
+
173
+ // long_term
174
+ const memFile = getMemoryFile();
175
+ const existing = readFileSafe(memFile) ?? "";
176
+
177
+ if (mode === "overwrite") {
178
+ const stamped = `<!-- last updated: ${ts} [${sid}] -->\n${content}`;
179
+ fs.writeFileSync(memFile, stamped, "utf-8");
180
+ } else {
181
+ const separator = existing.trim() ? "\n\n" : "";
182
+ const stamped = `<!-- ${ts} [${sid}] -->\n${content}`;
183
+ fs.writeFileSync(memFile, existing + separator + stamped, "utf-8");
184
+ }
185
+ await ensureQmdAvailableForUpdate();
186
+ scheduleQmdUpdate();
187
+ output(
188
+ json
189
+ ? { ok: true, path: memFile, target, mode, timestamp: ts }
190
+ : `${mode === "overwrite" ? "Overwrote" : "Appended to"} MEMORY.md`,
191
+ json,
192
+ );
193
+ }
194
+
195
+ async function cmdRead(flags: Record<string, string | boolean>) {
196
+ const json = hasFlag(flags, "json");
197
+ const target = getFlag(flags, "target");
198
+ const date = getFlag(flags, "date");
199
+
200
+ if (!target || !["long_term", "scratchpad", "daily", "list"].includes(target)) {
201
+ exitError("--target must be 'long_term', 'scratchpad', 'daily', or 'list'", json);
202
+ }
203
+
204
+ ensureDirs();
205
+
206
+ if (target === "list") {
207
+ try {
208
+ const files = fs
209
+ .readdirSync(getDailyDir())
210
+ .filter((f) => f.endsWith(".md"))
211
+ .sort()
212
+ .reverse();
213
+ if (json) {
214
+ output({ files }, true);
215
+ } else if (files.length === 0) {
216
+ console.log("No daily logs found.");
217
+ } else {
218
+ console.log(`Daily logs:\n${files.map((f) => `- ${f}`).join("\n")}`);
219
+ }
220
+ } catch {
221
+ output(json ? { files: [] } : "No daily logs directory.", json);
222
+ }
223
+ return;
224
+ }
225
+
226
+ if (target === "daily") {
227
+ const d = date ?? todayStr();
228
+ const filePath = dailyPath(d);
229
+ const content = readFileSafe(filePath);
230
+ if (!content) {
231
+ output(json ? { content: null, date: d } : `No daily log for ${d}.`, json);
232
+ return;
233
+ }
234
+ output(json ? { content, date: d, path: filePath } : content, json);
235
+ return;
236
+ }
237
+
238
+ if (target === "scratchpad") {
239
+ const content = readFileSafe(getScratchpadFile());
240
+ if (!content?.trim()) {
241
+ output(json ? { content: null } : "SCRATCHPAD.md is empty or does not exist.", json);
242
+ return;
243
+ }
244
+ output(json ? { content, path: getScratchpadFile() } : content, json);
245
+ return;
246
+ }
247
+
248
+ // long_term
249
+ const content = readFileSafe(getMemoryFile());
250
+ if (!content) {
251
+ output(json ? { content: null } : "MEMORY.md is empty or does not exist.", json);
252
+ return;
253
+ }
254
+ output(json ? { content, path: getMemoryFile() } : content, json);
255
+ }
256
+
257
+ async function cmdScratchpad(flags: Record<string, string | boolean>, positional: string[]) {
258
+ const json = hasFlag(flags, "json");
259
+ const action = positional[0];
260
+ const text = getFlag(flags, "text");
261
+
262
+ if (!action || !["add", "done", "undo", "clear_done", "list"].includes(action)) {
263
+ exitError("Usage: agent-memory scratchpad <add|done|undo|clear_done|list> [--text <text>]", json);
264
+ }
265
+
266
+ ensureDirs();
267
+ const spFile = getScratchpadFile();
268
+ const existing = readFileSafe(spFile) ?? "";
269
+ let items = parseScratchpad(existing);
270
+
271
+ if (action === "list") {
272
+ if (items.length === 0) {
273
+ output(json ? { items: [], count: 0, open: 0 } : "Scratchpad is empty.", json);
274
+ return;
275
+ }
276
+ if (json) {
277
+ output(
278
+ {
279
+ items: items.map((i) => ({ done: i.done, text: i.text })),
280
+ count: items.length,
281
+ open: items.filter((i) => !i.done).length,
282
+ },
283
+ true,
284
+ );
285
+ } else {
286
+ console.log(serializeScratchpad(items));
287
+ }
288
+ return;
289
+ }
290
+
291
+ if (action === "add") {
292
+ if (!text) exitError("--text is required for add", json);
293
+ const ts = nowTimestamp();
294
+ items.push({ done: false, text: text!, meta: `<!-- ${ts} [cli] -->` });
295
+ fs.writeFileSync(spFile, serializeScratchpad(items), "utf-8");
296
+ await ensureQmdAvailableForUpdate();
297
+ scheduleQmdUpdate();
298
+ output(json ? { ok: true, action, text } : `Added: - [ ] ${text}`, json);
299
+ return;
300
+ }
301
+
302
+ if (action === "done" || action === "undo") {
303
+ if (!text) exitError(`--text is required for ${action}`, json);
304
+ const needle = text!.toLowerCase();
305
+ const targetDone = action === "done";
306
+ let matched = false;
307
+ for (const item of items) {
308
+ if (item.done !== targetDone && item.text.toLowerCase().includes(needle)) {
309
+ item.done = targetDone;
310
+ matched = true;
311
+ break;
312
+ }
313
+ }
314
+ if (!matched) {
315
+ exitError(`No matching ${targetDone ? "open" : "done"} item found for: "${text}"`, json);
316
+ }
317
+ fs.writeFileSync(spFile, serializeScratchpad(items), "utf-8");
318
+ await ensureQmdAvailableForUpdate();
319
+ scheduleQmdUpdate();
320
+ output(json ? { ok: true, action, text } : "Updated.", json);
321
+ return;
322
+ }
323
+
324
+ if (action === "clear_done") {
325
+ const before = items.length;
326
+ items = items.filter((i) => !i.done);
327
+ const removed = before - items.length;
328
+ fs.writeFileSync(spFile, serializeScratchpad(items), "utf-8");
329
+ await ensureQmdAvailableForUpdate();
330
+ scheduleQmdUpdate();
331
+ output(json ? { ok: true, action, removed } : `Cleared ${removed} done item(s).`, json);
332
+ }
333
+ }
334
+
335
+ async function cmdSearch(flags: Record<string, string | boolean>) {
336
+ const json = hasFlag(flags, "json");
337
+ const query = getFlag(flags, "query");
338
+ const mode = (getFlag(flags, "mode") ?? "keyword") as "keyword" | "semantic" | "deep";
339
+ const limit = Number.parseInt(getFlag(flags, "limit") ?? "5", 10);
340
+
341
+ if (!query) exitError("--query is required", json);
342
+ if (!["keyword", "semantic", "deep"].includes(mode)) {
343
+ exitError("--mode must be 'keyword', 'semantic', or 'deep'", json);
344
+ }
345
+
346
+ const qmdFound = await detectQmd();
347
+ if (!qmdFound) {
348
+ exitError("qmd is not installed. Install: bun install -g https://github.com/tobi/qmd", json);
349
+ }
350
+
351
+ const collName = getCollectionName();
352
+ const hasCollection = await checkCollection(collName);
353
+ if (!hasCollection) {
354
+ exitError(`qmd collection '${collName}' not found. Run: agent-memory init`, json);
355
+ }
356
+
357
+ try {
358
+ const { results, stderr } = await runQmdSearch(mode, query!, limit);
359
+
360
+ if (json) {
361
+ output({ mode, query, count: results.length, results }, true);
362
+ return;
363
+ }
364
+
365
+ if (results.length === 0) {
366
+ const needsEmbed = /need embeddings/i.test(stderr ?? "");
367
+ if (needsEmbed && (mode === "semantic" || mode === "deep")) {
368
+ console.log(`No results found. qmd reports missing embeddings — run: qmd embed`);
369
+ } else {
370
+ console.log(`No results found for "${query}" (mode: ${mode}).`);
371
+ }
372
+ return;
373
+ }
374
+
375
+ for (let i = 0; i < results.length; i++) {
376
+ const r = results[i];
377
+ const filePath = getQmdResultPath(r);
378
+ const text = getQmdResultText(r);
379
+ console.log(`--- Result ${i + 1} ---`);
380
+ if (filePath) console.log(`File: ${filePath}`);
381
+ if (r.score != null) console.log(`Score: ${r.score}`);
382
+ if (text) console.log(text);
383
+ console.log("");
384
+ }
385
+ } catch (err) {
386
+ exitError(`Search failed: ${err instanceof Error ? err.message : String(err)}`, json);
387
+ }
388
+ }
389
+
390
+ async function cmdInit(flags: Record<string, string | boolean>) {
391
+ const json = hasFlag(flags, "json");
392
+
393
+ ensureDirs();
394
+ const dir = getMemoryDir();
395
+
396
+ const qmdFound = await detectQmd();
397
+ let collectionCreated = false;
398
+
399
+ if (qmdFound) {
400
+ const collName = getCollectionName();
401
+ const hasCollection = await checkCollection(collName);
402
+ if (!hasCollection) {
403
+ collectionCreated = await setupQmdCollection();
404
+ }
405
+ }
406
+
407
+ if (json) {
408
+ output(
409
+ {
410
+ ok: true,
411
+ directory: dir,
412
+ qmd: qmdFound,
413
+ collectionCreated,
414
+ },
415
+ true,
416
+ );
417
+ } else {
418
+ console.log(`Memory directory: ${dir}`);
419
+ console.log(` MEMORY.md, SCRATCHPAD.md, daily/ created.`);
420
+ if (qmdFound) {
421
+ if (collectionCreated) {
422
+ console.log(` qmd collection '${getCollectionName()}' created.`);
423
+ } else {
424
+ console.log(` qmd collection '${getCollectionName()}' already exists.`);
425
+ }
426
+ } else {
427
+ console.log(` qmd not found — search features unavailable.`);
428
+ console.log(` Install: bun install -g https://github.com/tobi/qmd`);
429
+ }
430
+ }
431
+ }
432
+
433
+ async function cmdStatus(flags: Record<string, string | boolean>) {
434
+ const json = hasFlag(flags, "json");
435
+
436
+ ensureDirs();
437
+ const dir = getMemoryDir();
438
+ const memFile = getMemoryFile();
439
+ const spFile = getScratchpadFile();
440
+ const dailyDir = getDailyDir();
441
+
442
+ const memContent = readFileSafe(memFile);
443
+ const spContent = readFileSafe(spFile);
444
+
445
+ let dailyCount = 0;
446
+ try {
447
+ dailyCount = fs.readdirSync(dailyDir).filter((f) => f.endsWith(".md")).length;
448
+ } catch {
449
+ // directory may not exist
450
+ }
451
+
452
+ const qmdFound = await detectQmd();
453
+ let hasCollection = false;
454
+ if (qmdFound) {
455
+ hasCollection = await checkCollection();
456
+ }
457
+
458
+ if (json) {
459
+ output(
460
+ {
461
+ directory: dir,
462
+ memoryFile: {
463
+ exists: memContent !== null,
464
+ chars: memContent?.length ?? 0,
465
+ lines: memContent ? memContent.split("\n").length : 0,
466
+ },
467
+ scratchpadFile: {
468
+ exists: spContent !== null,
469
+ items: spContent ? parseScratchpad(spContent).length : 0,
470
+ openItems: spContent ? parseScratchpad(spContent).filter((i) => !i.done).length : 0,
471
+ },
472
+ dailyLogs: dailyCount,
473
+ qmd: {
474
+ available: qmdFound,
475
+ collection: hasCollection ? getCollectionName() : null,
476
+ },
477
+ },
478
+ true,
479
+ );
480
+ } else {
481
+ console.log(`Memory directory: ${dir}`);
482
+ console.log("");
483
+ if (memContent !== null) {
484
+ const lines = memContent.split("\n").length;
485
+ console.log(`MEMORY.md: ${memContent.length} chars, ${lines} lines`);
486
+ } else {
487
+ console.log("MEMORY.md: not created yet");
488
+ }
489
+ if (spContent !== null) {
490
+ const items = parseScratchpad(spContent);
491
+ const open = items.filter((i) => !i.done).length;
492
+ console.log(`SCRATCHPAD.md: ${items.length} items (${open} open)`);
493
+ } else {
494
+ console.log("SCRATCHPAD.md: not created yet");
495
+ }
496
+ console.log(`Daily logs: ${dailyCount} file(s)`);
497
+ console.log("");
498
+ if (qmdFound) {
499
+ console.log(`qmd: available`);
500
+ console.log(
501
+ `Collection '${getCollectionName()}': ${hasCollection ? "configured" : "not configured — run: agent-memory init"}`,
502
+ );
503
+ } else {
504
+ console.log("qmd: not installed");
505
+ }
506
+ }
507
+ }
508
+
509
+ // ---------------------------------------------------------------------------
510
+ // Usage
511
+ // ---------------------------------------------------------------------------
512
+
513
+ function printUsage() {
514
+ console.log(`agent-memory — persistent memory for coding agents
515
+
516
+ Usage:
517
+ agent-memory <command> [options]
518
+
519
+ Commands:
520
+ context Build & print context injection string
521
+ write Write to memory files
522
+ read Read memory files
523
+ scratchpad Manage checklist items
524
+ search Search across memory files (requires qmd)
525
+ init Initialize memory directory and qmd collection
526
+ status Show configuration and status
527
+
528
+ Global flags:
529
+ --dir <path> Override memory directory
530
+ --json Machine-readable JSON output
531
+
532
+ Examples:
533
+ agent-memory init
534
+ agent-memory write --target long_term --content "User prefers dark mode"
535
+ agent-memory write --target daily --content "Fixed auth bug in login flow"
536
+ agent-memory read --target long_term
537
+ agent-memory read --target daily --date 2026-02-15
538
+ agent-memory read --target list
539
+ agent-memory scratchpad add --text "Review PR #42"
540
+ agent-memory scratchpad list
541
+ agent-memory scratchpad done --text "PR #42"
542
+ agent-memory search --query "database choice" --mode keyword
543
+ agent-memory context --no-search
544
+ agent-memory status --json`);
545
+ }
546
+
547
+ // ---------------------------------------------------------------------------
548
+ // Main
549
+ // ---------------------------------------------------------------------------
550
+
551
+ async function main() {
552
+ const { command, flags, positional } = parseArgs(process.argv.slice(2));
553
+ const json = hasFlag(flags, "json");
554
+
555
+ // Apply --dir override
556
+ const dir = getFlag(flags, "dir");
557
+ if (dir) {
558
+ _setBaseDir(dir);
559
+ }
560
+
561
+ if (!command || command === "help" || hasFlag(flags, "help")) {
562
+ printUsage();
563
+ return;
564
+ }
565
+
566
+ switch (command) {
567
+ case "context":
568
+ await cmdContext(flags);
569
+ break;
570
+ case "write":
571
+ await cmdWrite(flags);
572
+ break;
573
+ case "read":
574
+ await cmdRead(flags);
575
+ break;
576
+ case "scratchpad":
577
+ await cmdScratchpad(flags, positional);
578
+ break;
579
+ case "search":
580
+ await cmdSearch(flags);
581
+ break;
582
+ case "init":
583
+ await cmdInit(flags);
584
+ break;
585
+ case "status":
586
+ await cmdStatus(flags);
587
+ break;
588
+ default:
589
+ exitError(`Unknown command: ${command}. Run 'agent-memory help' for usage.`, json);
590
+ }
591
+ }
592
+
593
+ main().catch((err) => {
594
+ console.error(err instanceof Error ? err.message : String(err));
595
+ process.exit(1);
596
+ });