@empirical-gauzza/cli 1.0.19 → 1.0.21

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.
@@ -6,9 +6,69 @@ const { log, warn } = require('./ui');
6
6
  const { httpJson, printApiError } = require('./http');
7
7
  const { parseOption, parseFlag } = require('./env');
8
8
  const { requireAccessToken, resolveApiBaseUrl, buildApiAuthHeaders } = require('./auth');
9
- const { MEMORY_CATEGORIES } = require('./constants');
9
+ const { MEMORY_CATEGORIES, DOC_TOPICS } = require('./constants');
10
10
  const { withSelectedWorkspaceContext } = require('./workspaceContext');
11
11
 
12
+ // ─── Table rendering (--format table) ─────────────────────────────────────────
13
+
14
+ function truncateForTable(value, maxLen) {
15
+ const str = value === undefined || value === null ? '' : String(value);
16
+ return str.length > maxLen ? `${str.slice(0, maxLen - 1)}…` : str;
17
+ }
18
+
19
+ function renderTable(rows, columns) {
20
+ if (!Array.isArray(rows) || rows.length === 0) return '(no rows)';
21
+ const cols = columns && columns.length ? columns : Object.keys(rows[0]);
22
+ const cellText = (row, col) => (row[col] === undefined || row[col] === null ? '' : String(row[col]));
23
+ const widths = cols.map((col) => Math.max(col.length, ...rows.map((row) => cellText(row, col).length)));
24
+ const renderRow = (cells) => cells.map((cell, i) => String(cell).padEnd(widths[i])).join(' ');
25
+ const lines = [renderRow(cols), renderRow(widths.map((w) => '-'.repeat(w)))];
26
+ for (const row of rows) {
27
+ lines.push(renderRow(cols.map((col) => cellText(row, col))));
28
+ }
29
+ return lines.join('\n');
30
+ }
31
+
32
+ function renderMemoryResultsTable(items) {
33
+ const rows = (Array.isArray(items) ? items : []).map((m) => ({
34
+ id: m.id || m._id || '',
35
+ category: m.category || '',
36
+ summary: truncateForTable(m.summary, 60),
37
+ score: m.rankScore != null ? m.rankScore : (m.similarityScore != null ? m.similarityScore : '')
38
+ }));
39
+ return renderTable(rows, ['id', 'category', 'summary', 'score']);
40
+ }
41
+
42
+ // ─── Scoped `--help` (C-005 item 5) ───────────────────────────────────────────
43
+ // `empirical memory --help` / `empirical memory <sub> --help` print scoped usage instead of
44
+ // the 90-line global help wall bin/empirical.js otherwise short-circuits every --help to.
45
+
46
+ const MEMORY_SUBCOMMAND_USAGE = {
47
+ query: 'Usage: empirical memory query --match <query> [--top-k 3] [--category <cat>] [--tags <t1,t2>] [--pretty] [--format table]',
48
+ get: 'Usage: empirical memory get --memory-id <id> [--pretty]',
49
+ list: 'Usage: empirical memory list [--category <cat>] [--tags <t1,t2>] [--start-date <ISO>] [--end-date <ISO>] [--limit 20] [--cursor <cursor>] [--oldest] [--pretty] [--format table]',
50
+ record: 'Usage: empirical memory record --category <cat> --summary <text> [--tags t1,t2] [--node-type observation] [--json-file <path>] [--no-dedupe] [--no-auto-link]',
51
+ note: 'Usage: empirical memory note --text <note> (--match <query> | --memory-id <id>) [--min-score 0.35]',
52
+ update: 'Usage: empirical memory update (--match <query> | --memory-id <id>) (--patch-json <json> | --json-file <path>) [--min-score 0.35]',
53
+ delete: 'Usage: empirical memory delete (--match <query> | --memory-id <id>) [--confirm] [--min-score 0.35]\nUsage: empirical memory delete (--tag <t> | --category <c> | --all) --confirm',
54
+ duplicates: 'Usage: empirical memory duplicates --memory-id <id>',
55
+ merge: 'Usage: empirical memory merge --canonical-id <id> --duplicate-id <id> [--no-merge-tags] [--no-merge-relationships]',
56
+ export: 'Usage: empirical memory export --scope <personal|workspace> [--out <path>] [--workspace-id <id>]',
57
+ import: 'Usage: empirical memory import <file> [--dry-run]',
58
+ stats: 'Usage: empirical memory stats [--format table]',
59
+ compact: 'Usage: empirical memory compact [--apply] [--category <cat>]'
60
+ };
61
+
62
+ function printMemoryHelp(sub) {
63
+ if (sub && MEMORY_SUBCOMMAND_USAGE[sub]) {
64
+ log(MEMORY_SUBCOMMAND_USAGE[sub]);
65
+ return;
66
+ }
67
+ log(DOC_TOPICS.memory);
68
+ log('');
69
+ log('Subcommands: ' + Object.keys(MEMORY_SUBCOMMAND_USAGE).join(', '));
70
+ }
71
+
12
72
  async function withCliAnalyticsContext(context, fn) {
13
73
  globalThis.__empiricalCliAnalyticsContext = context || {};
14
74
  try {
@@ -18,11 +78,13 @@ async function withCliAnalyticsContext(context, fn) {
18
78
  }
19
79
  }
20
80
 
21
- async function resolveMemoryByQuery(match, minScore, accessToken, baseUrl) {
81
+ async function resolveMemoryByQuery(match, minScore, accessToken, baseUrl, filters) {
22
82
  const queryUrl = new URL('/api/v1/memories/query', baseUrl).toString();
83
+ const body = { query: match, topK: 5 };
84
+ if (filters && Object.keys(filters).length) body.filters = filters;
23
85
  let response;
24
86
  try {
25
- response = await httpJson(queryUrl, 'POST', { query: match, topK: 5 }, buildApiAuthHeaders(accessToken));
87
+ response = await httpJson(queryUrl, 'POST', body, buildApiAuthHeaders(accessToken));
26
88
  } catch (error) {
27
89
  return { found: false, reason: 'error', message: error.message };
28
90
  }
@@ -43,6 +105,19 @@ async function resolveMemoryByQuery(match, minScore, accessToken, baseUrl) {
43
105
 
44
106
  scored.sort((a, b) => b.score - a.score);
45
107
 
108
+ // A caller-supplied filter (e.g. --tags) that narrows the server-side result set to exactly
109
+ // one memory is already a stronger disambiguation signal than the semantic score - trust it
110
+ // outright rather than falling through to the score check below. This matters because Atlas
111
+ // vector search has a real async indexing lag: a memory created moments ago can come back
112
+ // with score 0 (unindexed, not "not this one") even when a tag filter already proves it is
113
+ // the only candidate. Fixes a real bug found 2026-08-25 where delete --match failed on a
114
+ // memory `query` found instantly, because delete had no way to trust a filtered, unambiguous
115
+ // result over a stale/zero semantic score.
116
+ if (filters && Object.keys(filters).length && results.length === 1) {
117
+ const only = scored[0];
118
+ return { found: true, memory: { id: only.id, summary: only.summary, score: only.score } };
119
+ }
120
+
46
121
  const top = scored[0];
47
122
  const second = scored[1];
48
123
 
@@ -57,6 +132,23 @@ async function resolveMemoryByQuery(match, minScore, accessToken, baseUrl) {
57
132
  return { found: true, memory: { id: top.id, summary: top.summary, score: top.score } };
58
133
  }
59
134
 
135
+ // --match resolution depends on Atlas vector search, which has a real async indexing lag:
136
+ // a memory created moments ago can be invisible to $vectorSearch even though it is already
137
+ // findable by tag. Letting --match commands pass --tags narrows the candidate pool directly
138
+ // instead of hoping semantic ranking surfaces the right one - the fix for a real bug found
139
+ // 2026-08-25 where delete --match returned unrelated score-0 candidates for a memory that
140
+ // `query` found instantly. Deliberately --tags (plural) only, not --category: `delete` already
141
+ // uses singular --tag/--category to mean "bulk delete everything in this category", so adding
142
+ // --category here would silently reroute a --match delete into a bulk delete instead of
143
+ // narrowing it - --tags never collides with that bulk-mode gate.
144
+ function parseMatchFilters(args) {
145
+ const tagsRaw = parseOption(args, '--tags', '');
146
+ const tags = tagsRaw ? tagsRaw.split(',').map((t) => t.trim()).filter(Boolean) : [];
147
+ const filters = {};
148
+ if (tags.length) filters.tags = tags;
149
+ return filters;
150
+ }
151
+
60
152
  function printMemoryResolveError(result) {
61
153
  if (result.reason === 'no_results') {
62
154
  warn(JSON.stringify({ error: true, code: 'no_results', message: 'No memories matched the query.' }));
@@ -96,6 +188,7 @@ async function runMemoryQuery(args) {
96
188
  const tagsRaw = parseOption(args, '--tags', '');
97
189
  const tags = tagsRaw ? tagsRaw.split(',').map((t) => t.trim()).filter(Boolean) : [];
98
190
  const pretty = parseFlag(args, '--pretty');
191
+ const format = parseOption(args, '--format', 'json');
99
192
 
100
193
  const accessToken = requireAccessToken();
101
194
  const baseUrl = resolveApiBaseUrl();
@@ -119,6 +212,11 @@ async function runMemoryQuery(args) {
119
212
  ? response.data.results
120
213
  : (Array.isArray(response) ? response : []);
121
214
 
215
+ if (format === 'table') {
216
+ log(renderMemoryResultsTable(results));
217
+ return;
218
+ }
219
+
122
220
  if (pretty) {
123
221
  log(JSON.stringify(results, null, 2));
124
222
  } else {
@@ -158,6 +256,7 @@ async function runMemoryList(args) {
158
256
  const cursor = parseOption(args, '--cursor', '');
159
257
  const sortOrder = parseFlag(args, '--oldest') ? 'asc' : 'desc';
160
258
  const pretty = parseFlag(args, '--pretty');
259
+ const format = parseOption(args, '--format', 'json');
161
260
 
162
261
  const params = new URLSearchParams();
163
262
  if (category) params.set('category', category);
@@ -180,6 +279,13 @@ async function runMemoryList(args) {
180
279
  process.exit(1);
181
280
  }
182
281
  const data = response && response.data ? response.data : response;
282
+
283
+ if (format === 'table') {
284
+ const items = Array.isArray(data && data.memories) ? data.memories : (Array.isArray(data) ? data : []);
285
+ log(renderMemoryResultsTable(items));
286
+ return;
287
+ }
288
+
183
289
  log(pretty ? JSON.stringify(data, null, 2) : JSON.stringify(data));
184
290
  }
185
291
 
@@ -199,7 +305,7 @@ async function runMemoryRecord(args) {
199
305
  const summary = parseOption(args, '--summary', '');
200
306
 
201
307
  if (!category || !summary) {
202
- warn('Usage: empirical memory record --category <cat> --summary <text> [--tags t1,t2] [--node-type observation]');
308
+ warn('Usage: empirical memory record --category <cat> --summary <text> [--tags t1,t2] [--node-type observation] [--no-dedupe] [--no-auto-link]');
203
309
  warn('Valid categories: ' + MEMORY_CATEGORIES.join(', '));
204
310
  process.exit(1);
205
311
  }
@@ -230,6 +336,19 @@ async function runMemoryRecord(args) {
230
336
  payload.data = { text: String(payload.summary) };
231
337
  }
232
338
 
339
+ // C-002 (docs/prd-cli-and-plugin-fixes.md): the server now dedupes/auto-links every
340
+ // POST /api/v1/memories write by default, matching the MCP record_graph_memory tool's
341
+ // guarantee. Only send dedupeEnabled/autoLink at all when the caller explicitly opted
342
+ // out here -- omitting them entirely lets the server's own default-on behavior apply,
343
+ // rather than this CLI baking in a redundant "true" that could drift from the server's
344
+ // actual default over time.
345
+ if (parseFlag(args, '--no-dedupe')) {
346
+ payload.dedupeEnabled = false;
347
+ }
348
+ if (parseFlag(args, '--no-auto-link')) {
349
+ payload.autoLink = false;
350
+ }
351
+
233
352
  const accessToken = requireAccessToken();
234
353
  const baseUrl = resolveApiBaseUrl();
235
354
  const recordUrl = new URL('/api/v1/memories', baseUrl).toString();
@@ -253,12 +372,12 @@ async function runMemoryNote(args) {
253
372
  const minScore = Number(parseOption(args, '--min-score', '0.35'));
254
373
 
255
374
  if (!text) {
256
- warn('Usage: empirical memory note --text <note> (--match <query> | --memory-id <id>) [--min-score 0.35]');
375
+ warn('Usage: empirical memory note --text <note> (--match <query> | --memory-id <id>) [--tags <t1,t2>] [--min-score 0.35]');
257
376
  process.exit(1);
258
377
  }
259
378
 
260
379
  if (!match && !memoryId) {
261
- warn('Usage: empirical memory note --text <note> (--match <query> | --memory-id <id>)');
380
+ warn('Usage: empirical memory note --text <note> (--match <query> | --memory-id <id>) [--tags <t1,t2>]');
262
381
  process.exit(1);
263
382
  }
264
383
 
@@ -267,7 +386,8 @@ async function runMemoryNote(args) {
267
386
 
268
387
  let targetId = memoryId;
269
388
  if (!targetId) {
270
- const result = await resolveMemoryByQuery(match, minScore, accessToken, baseUrl);
389
+ const filters = parseMatchFilters(args);
390
+ const result = await resolveMemoryByQuery(match, minScore, accessToken, baseUrl, filters);
271
391
  if (!result.found) {
272
392
  printMemoryResolveError(result);
273
393
  process.exit(1);
@@ -296,12 +416,12 @@ async function runMemoryUpdate(args) {
296
416
  const minScore = Number(parseOption(args, '--min-score', '0.35'));
297
417
 
298
418
  if (!patchJson && !jsonFile) {
299
- warn('Usage: empirical memory update (--match <query> | --memory-id <id>) (--patch-json <json> | --json-file <path>)');
419
+ warn('Usage: empirical memory update (--match <query> | --memory-id <id>) (--patch-json <json> | --json-file <path>) [--tags <t1,t2>]');
300
420
  process.exit(1);
301
421
  }
302
422
 
303
423
  if (!match && !memoryId) {
304
- warn('Usage: empirical memory update (--match <query> | --memory-id <id>) (--patch-json <json> | --json-file <path>)');
424
+ warn('Usage: empirical memory update (--match <query> | --memory-id <id>) (--patch-json <json> | --json-file <path>) [--tags <t1,t2>]');
305
425
  process.exit(1);
306
426
  }
307
427
 
@@ -327,7 +447,8 @@ async function runMemoryUpdate(args) {
327
447
 
328
448
  let targetId = memoryId;
329
449
  if (!targetId) {
330
- const result = await resolveMemoryByQuery(match, minScore, accessToken, baseUrl);
450
+ const filters = parseMatchFilters(args);
451
+ const result = await resolveMemoryByQuery(match, minScore, accessToken, baseUrl, filters);
331
452
  if (!result.found) {
332
453
  printMemoryResolveError(result);
333
454
  process.exit(1);
@@ -348,14 +469,57 @@ async function runMemoryUpdate(args) {
348
469
  log(JSON.stringify(updated));
349
470
  }
350
471
 
472
+ async function runMemoryDeleteBulk(args) {
473
+ const tag = parseOption(args, '--tag', '');
474
+ const category = parseOption(args, '--category', '');
475
+ const all = parseFlag(args, '--all');
476
+ const confirm = parseFlag(args, '--confirm');
477
+
478
+ const selectorCount = [Boolean(tag), Boolean(category), all].filter(Boolean).length;
479
+ if (selectorCount !== 1) {
480
+ warn('Usage: empirical memory delete (--tag <t> | --category <c> | --all) --confirm');
481
+ process.exit(1);
482
+ }
483
+
484
+ const accessToken = requireAccessToken();
485
+ const baseUrl = resolveApiBaseUrl();
486
+ const url = new URL('/api/v1/memory-bulk-delete', baseUrl).toString();
487
+ const body = { confirm };
488
+ if (tag) body.tag = tag;
489
+ if (category) body.category = category;
490
+ if (all) body.all = true;
491
+
492
+ let response;
493
+ try {
494
+ response = await httpJson(url, 'POST', body, buildApiAuthHeaders(accessToken));
495
+ } catch (error) {
496
+ printApiError(error);
497
+ process.exit(1);
498
+ }
499
+
500
+ // Server always reports the count first: without --confirm it's a dry-run preview and no
501
+ // writes happened; with --confirm it reports what it just deleted. Either way the count is
502
+ // printed, matching the single-memory delete's dry-run-by-default convention above.
503
+ const data = (response && response.data) ? response.data : response;
504
+ log(JSON.stringify(data));
505
+ }
506
+
351
507
  async function runMemoryDelete(args) {
508
+ const tag = parseOption(args, '--tag', '');
509
+ const category = parseOption(args, '--category', '');
510
+ const all = parseFlag(args, '--all');
511
+ if (tag || category || all) {
512
+ return runMemoryDeleteBulk(args);
513
+ }
514
+
352
515
  const match = parseOption(args, '--match', '');
353
516
  const memoryId = parseOption(args, '--memory-id', '');
354
517
  const minScore = Number(parseOption(args, '--min-score', '0.35'));
355
518
  const confirm = parseFlag(args, '--confirm');
356
519
 
357
520
  if (!match && !memoryId) {
358
- warn('Usage: empirical memory delete (--match <query> | --memory-id <id>) [--confirm] [--min-score 0.35]');
521
+ warn('Usage: empirical memory delete (--match <query> | --memory-id <id>) [--tags <t1,t2>] [--confirm] [--min-score 0.35]');
522
+ warn(' or: empirical memory delete (--tag <t> | --category <c> | --all) --confirm');
359
523
  process.exit(1);
360
524
  }
361
525
 
@@ -366,7 +530,8 @@ async function runMemoryDelete(args) {
366
530
  let targetSummary = '';
367
531
 
368
532
  if (!targetId) {
369
- const result = await resolveMemoryByQuery(match, minScore, accessToken, baseUrl);
533
+ const filters = parseMatchFilters(args);
534
+ const result = await resolveMemoryByQuery(match, minScore, accessToken, baseUrl, filters);
370
535
  if (!result.found) {
371
536
  if (!confirm && (result.reason === 'ambiguous' || result.reason === 'low_score')) {
372
537
  // Dry-run: return candidates info
@@ -479,14 +644,188 @@ async function runMemoryExport(args) {
479
644
  process.exit(1);
480
645
  }
481
646
 
647
+ // The raw server response is the export envelope itself (streamed as one JSON body, not the
648
+ // standard {status,data} wrapper): {schemaVersion, generatedAt, scope, workspaceId,
649
+ // memoryCount, truncated, memories: [...]}. Re-shape it into the versioned, round-trippable
650
+ // format `memory import` consumes: {schemaVersion: 1, exportedAt, memories: [{category,
651
+ // summary, tags, nodeType, data, mass, gravity, timeContext, notes, relationships}]}.
652
+ const rawMemories = Array.isArray(response && response.memories) ? response.memories : [];
653
+ const envelope = {
654
+ schemaVersion: 1,
655
+ exportedAt: new Date().toISOString(),
656
+ memories: rawMemories.map((m) => ({
657
+ category: m.category,
658
+ summary: m.summary,
659
+ tags: Array.isArray(m.tags) ? m.tags : [],
660
+ nodeType: m.nodeType,
661
+ data: m.data,
662
+ mass: m.mass,
663
+ gravity: m.gravity,
664
+ timeContext: m.timeContext || null,
665
+ notes: Array.isArray(m.notes) ? m.notes.map((n) => ({ text: n.text })) : [],
666
+ relationships: Array.isArray(m.relationships)
667
+ ? m.relationships.map((r) => ({ type: r.type, targetId: r.targetId, weight: r.weight }))
668
+ : []
669
+ }))
670
+ };
671
+
482
672
  const resolvedPath = path.resolve(outPath);
483
- fs.writeFileSync(resolvedPath, JSON.stringify(response, null, 2) + '\n', 'utf8');
673
+ fs.writeFileSync(resolvedPath, JSON.stringify(envelope, null, 2) + '\n', 'utf8');
484
674
 
485
- log(JSON.stringify({ exported: true, path: resolvedPath, memoryCount: response.memoryCount }));
675
+ log(JSON.stringify({ exported: true, path: resolvedPath, schemaVersion: envelope.schemaVersion, memoryCount: envelope.memories.length }));
676
+ }
677
+
678
+ async function runMemoryImport(args) {
679
+ const filePath = args.find((a) => a && !a.startsWith('--')) || '';
680
+ const dryRun = parseFlag(args, '--dry-run');
681
+
682
+ if (!filePath) {
683
+ warn('Usage: empirical memory import <file> [--dry-run]');
684
+ process.exit(1);
685
+ }
686
+
687
+ let doc;
688
+ try {
689
+ doc = JSON.parse(fs.readFileSync(path.resolve(filePath), 'utf8'));
690
+ } catch (error) {
691
+ warn(JSON.stringify({ error: true, code: 'json_file_error', message: error.message }));
692
+ process.exit(1);
693
+ }
694
+
695
+ const memories = Array.isArray(doc && doc.memories) ? doc.memories : null;
696
+ if (!memories) {
697
+ warn(JSON.stringify({
698
+ error: true,
699
+ code: 'invalid_format',
700
+ message: 'File is not a valid Empirical memory export: missing a top-level "memories" array. Re-export with `empirical memory export`.'
701
+ }));
702
+ process.exit(1);
703
+ }
704
+
705
+ if (dryRun) {
706
+ log(JSON.stringify({ dryRun: true, wouldImport: memories.length, schemaVersion: doc.schemaVersion || null }));
707
+ return;
708
+ }
709
+
710
+ const accessToken = requireAccessToken();
711
+ const baseUrl = resolveApiBaseUrl();
712
+ // Every row goes through the normal create endpoint, so imports get the same dedupe and
713
+ // auto-link treatment as any other write, no special-casing needed here.
714
+ const recordUrl = new URL('/api/v1/memories', baseUrl).toString();
715
+
716
+ let imported = 0;
717
+ let failed = 0;
718
+ const errors = [];
719
+
720
+ for (const row of memories) {
721
+ const payload = {
722
+ category: row.category,
723
+ summary: row.summary,
724
+ nodeType: row.nodeType || 'observation',
725
+ tags: Array.isArray(row.tags) ? row.tags : [],
726
+ data: row.data !== undefined
727
+ ? row.data
728
+ : (row.summary !== undefined ? { text: String(row.summary) } : undefined)
729
+ };
730
+ if (Array.isArray(row.relationships) && row.relationships.length) payload.relationships = row.relationships;
731
+ if (row.mass !== undefined && row.mass !== null) payload.mass = row.mass;
732
+ if (row.gravity !== undefined && row.gravity !== null) payload.gravity = row.gravity;
733
+ if (row.timeContext) payload.timeContext = row.timeContext;
734
+
735
+ let created;
736
+ try {
737
+ const response = await httpJson(recordUrl, 'POST', payload, buildApiAuthHeaders(accessToken));
738
+ created = (response && response.data) ? response.data : response;
739
+ imported++;
740
+ } catch (error) {
741
+ failed++;
742
+ errors.push({ summary: row.summary, message: error.message });
743
+ continue;
744
+ }
745
+
746
+ const notes = Array.isArray(row.notes) ? row.notes : [];
747
+ const createdId = created && (created.id || created._id);
748
+ if (notes.length && createdId) {
749
+ const noteUrl = new URL(`/api/v1/memories/${encodeURIComponent(createdId)}/notes`, baseUrl).toString();
750
+ for (const note of notes) {
751
+ const text = typeof note === 'string' ? note : (note && note.text);
752
+ if (!text) continue;
753
+ try {
754
+ await httpJson(noteUrl, 'POST', { text }, buildApiAuthHeaders(accessToken));
755
+ } catch (_error) {
756
+ // Best-effort: a note failing to re-attach shouldn't fail the whole import.
757
+ }
758
+ }
759
+ }
760
+ }
761
+
762
+ log(JSON.stringify({ imported, failed, total: memories.length, errors: errors.slice(0, 20) }));
763
+ }
764
+
765
+ async function runMemoryStats(args) {
766
+ const format = parseOption(args, '--format', 'json');
767
+
768
+ const accessToken = requireAccessToken();
769
+ const baseUrl = resolveApiBaseUrl();
770
+ const url = new URL('/api/v1/memory-stats', baseUrl).toString();
771
+
772
+ let response;
773
+ try {
774
+ response = await httpJson(url, 'GET', undefined, buildApiAuthHeaders(accessToken));
775
+ } catch (error) {
776
+ printApiError(error);
777
+ process.exit(1);
778
+ }
779
+
780
+ const data = (response && response.data) ? response.data : response;
781
+
782
+ if (format === 'table') {
783
+ log(`Total memories: ${data.total}`);
784
+ log(`Oldest: ${data.oldest || '(none)'}`);
785
+ log(`Newest: ${data.newest || '(none)'}`);
786
+ log('');
787
+ log('By category:');
788
+ log(renderTable(data.byCategory || [], ['category', 'count']));
789
+ log('');
790
+ log('By tag:');
791
+ log(renderTable(data.byTag || [], ['tag', 'count']));
792
+ return;
793
+ }
794
+
795
+ log(JSON.stringify(data));
796
+ }
797
+
798
+ async function runMemoryCompact(args) {
799
+ const apply = parseFlag(args, '--apply');
800
+ const category = parseOption(args, '--category', '');
801
+
802
+ const accessToken = requireAccessToken();
803
+ const baseUrl = resolveApiBaseUrl();
804
+ const url = new URL('/api/v1/memory-compaction', baseUrl).toString();
805
+ const body = { apply };
806
+ if (category) body.category = category;
807
+
808
+ let response;
809
+ try {
810
+ response = await httpJson(url, 'POST', body, buildApiAuthHeaders(accessToken));
811
+ } catch (error) {
812
+ printApiError(error);
813
+ process.exit(1);
814
+ }
815
+
816
+ const data = (response && response.data) ? response.data : response;
817
+ log(JSON.stringify(data));
486
818
  }
487
819
 
488
820
  async function runMemoryCommand(subArgs) {
489
- const sub = subArgs[0];
821
+ const helpRequested = subArgs.includes('--help') || subArgs.includes('-h');
822
+ const sub = subArgs[0] && !subArgs[0].startsWith('-') ? subArgs[0] : undefined;
823
+
824
+ if (helpRequested || !sub) {
825
+ printMemoryHelp(sub);
826
+ return;
827
+ }
828
+
490
829
  const rest = subArgs.slice(1);
491
830
 
492
831
  return withCliAnalyticsContext(
@@ -502,9 +841,12 @@ async function runMemoryCommand(subArgs) {
502
841
  if (sub === 'duplicates') return runMemoryDuplicates(rest);
503
842
  if (sub === 'merge') return runMemoryMerge(rest);
504
843
  if (sub === 'export') return runMemoryExport(rest);
844
+ if (sub === 'import') return runMemoryImport(rest);
845
+ if (sub === 'stats') return runMemoryStats(rest);
846
+ if (sub === 'compact') return runMemoryCompact(rest);
505
847
 
506
848
  warn('Unknown memory subcommand: ' + (sub || '(none)'));
507
- warn('Usage: empirical memory <query|record|note|update|delete|duplicates|merge|export> [options]');
849
+ warn('Usage: empirical memory <query|record|note|update|delete|duplicates|merge|export|import|stats|compact> [options]');
508
850
  process.exit(1);
509
851
  })
510
852
  );
@@ -524,5 +866,9 @@ module.exports = {
524
866
  runMemoryDuplicates,
525
867
  runMemoryMerge,
526
868
  runMemoryExport,
869
+ runMemoryImport,
870
+ runMemoryStats,
871
+ runMemoryCompact,
872
+ runMemoryDeleteBulk,
527
873
  runMemoryCommand
528
874
  };