@empirical-gauzza/cli 1.0.18 → 1.0.20

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 {
@@ -96,6 +156,7 @@ async function runMemoryQuery(args) {
96
156
  const tagsRaw = parseOption(args, '--tags', '');
97
157
  const tags = tagsRaw ? tagsRaw.split(',').map((t) => t.trim()).filter(Boolean) : [];
98
158
  const pretty = parseFlag(args, '--pretty');
159
+ const format = parseOption(args, '--format', 'json');
99
160
 
100
161
  const accessToken = requireAccessToken();
101
162
  const baseUrl = resolveApiBaseUrl();
@@ -119,6 +180,11 @@ async function runMemoryQuery(args) {
119
180
  ? response.data.results
120
181
  : (Array.isArray(response) ? response : []);
121
182
 
183
+ if (format === 'table') {
184
+ log(renderMemoryResultsTable(results));
185
+ return;
186
+ }
187
+
122
188
  if (pretty) {
123
189
  log(JSON.stringify(results, null, 2));
124
190
  } else {
@@ -158,6 +224,7 @@ async function runMemoryList(args) {
158
224
  const cursor = parseOption(args, '--cursor', '');
159
225
  const sortOrder = parseFlag(args, '--oldest') ? 'asc' : 'desc';
160
226
  const pretty = parseFlag(args, '--pretty');
227
+ const format = parseOption(args, '--format', 'json');
161
228
 
162
229
  const params = new URLSearchParams();
163
230
  if (category) params.set('category', category);
@@ -180,6 +247,13 @@ async function runMemoryList(args) {
180
247
  process.exit(1);
181
248
  }
182
249
  const data = response && response.data ? response.data : response;
250
+
251
+ if (format === 'table') {
252
+ const items = Array.isArray(data && data.memories) ? data.memories : (Array.isArray(data) ? data : []);
253
+ log(renderMemoryResultsTable(items));
254
+ return;
255
+ }
256
+
183
257
  log(pretty ? JSON.stringify(data, null, 2) : JSON.stringify(data));
184
258
  }
185
259
 
@@ -199,7 +273,7 @@ async function runMemoryRecord(args) {
199
273
  const summary = parseOption(args, '--summary', '');
200
274
 
201
275
  if (!category || !summary) {
202
- warn('Usage: empirical memory record --category <cat> --summary <text> [--tags t1,t2] [--node-type observation]');
276
+ warn('Usage: empirical memory record --category <cat> --summary <text> [--tags t1,t2] [--node-type observation] [--no-dedupe] [--no-auto-link]');
203
277
  warn('Valid categories: ' + MEMORY_CATEGORIES.join(', '));
204
278
  process.exit(1);
205
279
  }
@@ -230,6 +304,19 @@ async function runMemoryRecord(args) {
230
304
  payload.data = { text: String(payload.summary) };
231
305
  }
232
306
 
307
+ // C-002 (docs/prd-cli-and-plugin-fixes.md): the server now dedupes/auto-links every
308
+ // POST /api/v1/memories write by default, matching the MCP record_graph_memory tool's
309
+ // guarantee. Only send dedupeEnabled/autoLink at all when the caller explicitly opted
310
+ // out here -- omitting them entirely lets the server's own default-on behavior apply,
311
+ // rather than this CLI baking in a redundant "true" that could drift from the server's
312
+ // actual default over time.
313
+ if (parseFlag(args, '--no-dedupe')) {
314
+ payload.dedupeEnabled = false;
315
+ }
316
+ if (parseFlag(args, '--no-auto-link')) {
317
+ payload.autoLink = false;
318
+ }
319
+
233
320
  const accessToken = requireAccessToken();
234
321
  const baseUrl = resolveApiBaseUrl();
235
322
  const recordUrl = new URL('/api/v1/memories', baseUrl).toString();
@@ -348,7 +435,49 @@ async function runMemoryUpdate(args) {
348
435
  log(JSON.stringify(updated));
349
436
  }
350
437
 
438
+ async function runMemoryDeleteBulk(args) {
439
+ const tag = parseOption(args, '--tag', '');
440
+ const category = parseOption(args, '--category', '');
441
+ const all = parseFlag(args, '--all');
442
+ const confirm = parseFlag(args, '--confirm');
443
+
444
+ const selectorCount = [Boolean(tag), Boolean(category), all].filter(Boolean).length;
445
+ if (selectorCount !== 1) {
446
+ warn('Usage: empirical memory delete (--tag <t> | --category <c> | --all) --confirm');
447
+ process.exit(1);
448
+ }
449
+
450
+ const accessToken = requireAccessToken();
451
+ const baseUrl = resolveApiBaseUrl();
452
+ const url = new URL('/api/v1/memory-bulk-delete', baseUrl).toString();
453
+ const body = { confirm };
454
+ if (tag) body.tag = tag;
455
+ if (category) body.category = category;
456
+ if (all) body.all = true;
457
+
458
+ let response;
459
+ try {
460
+ response = await httpJson(url, 'POST', body, buildApiAuthHeaders(accessToken));
461
+ } catch (error) {
462
+ printApiError(error);
463
+ process.exit(1);
464
+ }
465
+
466
+ // Server always reports the count first: without --confirm it's a dry-run preview and no
467
+ // writes happened; with --confirm it reports what it just deleted. Either way the count is
468
+ // printed, matching the single-memory delete's dry-run-by-default convention above.
469
+ const data = (response && response.data) ? response.data : response;
470
+ log(JSON.stringify(data));
471
+ }
472
+
351
473
  async function runMemoryDelete(args) {
474
+ const tag = parseOption(args, '--tag', '');
475
+ const category = parseOption(args, '--category', '');
476
+ const all = parseFlag(args, '--all');
477
+ if (tag || category || all) {
478
+ return runMemoryDeleteBulk(args);
479
+ }
480
+
352
481
  const match = parseOption(args, '--match', '');
353
482
  const memoryId = parseOption(args, '--memory-id', '');
354
483
  const minScore = Number(parseOption(args, '--min-score', '0.35'));
@@ -356,6 +485,7 @@ async function runMemoryDelete(args) {
356
485
 
357
486
  if (!match && !memoryId) {
358
487
  warn('Usage: empirical memory delete (--match <query> | --memory-id <id>) [--confirm] [--min-score 0.35]');
488
+ warn(' or: empirical memory delete (--tag <t> | --category <c> | --all) --confirm');
359
489
  process.exit(1);
360
490
  }
361
491
 
@@ -479,14 +609,188 @@ async function runMemoryExport(args) {
479
609
  process.exit(1);
480
610
  }
481
611
 
612
+ // The raw server response is the export envelope itself (streamed as one JSON body, not the
613
+ // standard {status,data} wrapper): {schemaVersion, generatedAt, scope, workspaceId,
614
+ // memoryCount, truncated, memories: [...]}. Re-shape it into the versioned, round-trippable
615
+ // format `memory import` consumes: {schemaVersion: 1, exportedAt, memories: [{category,
616
+ // summary, tags, nodeType, data, mass, gravity, timeContext, notes, relationships}]}.
617
+ const rawMemories = Array.isArray(response && response.memories) ? response.memories : [];
618
+ const envelope = {
619
+ schemaVersion: 1,
620
+ exportedAt: new Date().toISOString(),
621
+ memories: rawMemories.map((m) => ({
622
+ category: m.category,
623
+ summary: m.summary,
624
+ tags: Array.isArray(m.tags) ? m.tags : [],
625
+ nodeType: m.nodeType,
626
+ data: m.data,
627
+ mass: m.mass,
628
+ gravity: m.gravity,
629
+ timeContext: m.timeContext || null,
630
+ notes: Array.isArray(m.notes) ? m.notes.map((n) => ({ text: n.text })) : [],
631
+ relationships: Array.isArray(m.relationships)
632
+ ? m.relationships.map((r) => ({ type: r.type, targetId: r.targetId, weight: r.weight }))
633
+ : []
634
+ }))
635
+ };
636
+
482
637
  const resolvedPath = path.resolve(outPath);
483
- fs.writeFileSync(resolvedPath, JSON.stringify(response, null, 2) + '\n', 'utf8');
638
+ fs.writeFileSync(resolvedPath, JSON.stringify(envelope, null, 2) + '\n', 'utf8');
639
+
640
+ log(JSON.stringify({ exported: true, path: resolvedPath, schemaVersion: envelope.schemaVersion, memoryCount: envelope.memories.length }));
641
+ }
642
+
643
+ async function runMemoryImport(args) {
644
+ const filePath = args.find((a) => a && !a.startsWith('--')) || '';
645
+ const dryRun = parseFlag(args, '--dry-run');
646
+
647
+ if (!filePath) {
648
+ warn('Usage: empirical memory import <file> [--dry-run]');
649
+ process.exit(1);
650
+ }
651
+
652
+ let doc;
653
+ try {
654
+ doc = JSON.parse(fs.readFileSync(path.resolve(filePath), 'utf8'));
655
+ } catch (error) {
656
+ warn(JSON.stringify({ error: true, code: 'json_file_error', message: error.message }));
657
+ process.exit(1);
658
+ }
659
+
660
+ const memories = Array.isArray(doc && doc.memories) ? doc.memories : null;
661
+ if (!memories) {
662
+ warn(JSON.stringify({
663
+ error: true,
664
+ code: 'invalid_format',
665
+ message: 'File is not a valid Empirical memory export: missing a top-level "memories" array. Re-export with `empirical memory export`.'
666
+ }));
667
+ process.exit(1);
668
+ }
669
+
670
+ if (dryRun) {
671
+ log(JSON.stringify({ dryRun: true, wouldImport: memories.length, schemaVersion: doc.schemaVersion || null }));
672
+ return;
673
+ }
674
+
675
+ const accessToken = requireAccessToken();
676
+ const baseUrl = resolveApiBaseUrl();
677
+ // Every row goes through the normal create endpoint, so imports get the same dedupe and
678
+ // auto-link treatment as any other write, no special-casing needed here.
679
+ const recordUrl = new URL('/api/v1/memories', baseUrl).toString();
680
+
681
+ let imported = 0;
682
+ let failed = 0;
683
+ const errors = [];
684
+
685
+ for (const row of memories) {
686
+ const payload = {
687
+ category: row.category,
688
+ summary: row.summary,
689
+ nodeType: row.nodeType || 'observation',
690
+ tags: Array.isArray(row.tags) ? row.tags : [],
691
+ data: row.data !== undefined
692
+ ? row.data
693
+ : (row.summary !== undefined ? { text: String(row.summary) } : undefined)
694
+ };
695
+ if (Array.isArray(row.relationships) && row.relationships.length) payload.relationships = row.relationships;
696
+ if (row.mass !== undefined && row.mass !== null) payload.mass = row.mass;
697
+ if (row.gravity !== undefined && row.gravity !== null) payload.gravity = row.gravity;
698
+ if (row.timeContext) payload.timeContext = row.timeContext;
699
+
700
+ let created;
701
+ try {
702
+ const response = await httpJson(recordUrl, 'POST', payload, buildApiAuthHeaders(accessToken));
703
+ created = (response && response.data) ? response.data : response;
704
+ imported++;
705
+ } catch (error) {
706
+ failed++;
707
+ errors.push({ summary: row.summary, message: error.message });
708
+ continue;
709
+ }
484
710
 
485
- log(JSON.stringify({ exported: true, path: resolvedPath, memoryCount: response.memoryCount }));
711
+ const notes = Array.isArray(row.notes) ? row.notes : [];
712
+ const createdId = created && (created.id || created._id);
713
+ if (notes.length && createdId) {
714
+ const noteUrl = new URL(`/api/v1/memories/${encodeURIComponent(createdId)}/notes`, baseUrl).toString();
715
+ for (const note of notes) {
716
+ const text = typeof note === 'string' ? note : (note && note.text);
717
+ if (!text) continue;
718
+ try {
719
+ await httpJson(noteUrl, 'POST', { text }, buildApiAuthHeaders(accessToken));
720
+ } catch (_error) {
721
+ // Best-effort: a note failing to re-attach shouldn't fail the whole import.
722
+ }
723
+ }
724
+ }
725
+ }
726
+
727
+ log(JSON.stringify({ imported, failed, total: memories.length, errors: errors.slice(0, 20) }));
728
+ }
729
+
730
+ async function runMemoryStats(args) {
731
+ const format = parseOption(args, '--format', 'json');
732
+
733
+ const accessToken = requireAccessToken();
734
+ const baseUrl = resolveApiBaseUrl();
735
+ const url = new URL('/api/v1/memory-stats', baseUrl).toString();
736
+
737
+ let response;
738
+ try {
739
+ response = await httpJson(url, 'GET', undefined, buildApiAuthHeaders(accessToken));
740
+ } catch (error) {
741
+ printApiError(error);
742
+ process.exit(1);
743
+ }
744
+
745
+ const data = (response && response.data) ? response.data : response;
746
+
747
+ if (format === 'table') {
748
+ log(`Total memories: ${data.total}`);
749
+ log(`Oldest: ${data.oldest || '(none)'}`);
750
+ log(`Newest: ${data.newest || '(none)'}`);
751
+ log('');
752
+ log('By category:');
753
+ log(renderTable(data.byCategory || [], ['category', 'count']));
754
+ log('');
755
+ log('By tag:');
756
+ log(renderTable(data.byTag || [], ['tag', 'count']));
757
+ return;
758
+ }
759
+
760
+ log(JSON.stringify(data));
761
+ }
762
+
763
+ async function runMemoryCompact(args) {
764
+ const apply = parseFlag(args, '--apply');
765
+ const category = parseOption(args, '--category', '');
766
+
767
+ const accessToken = requireAccessToken();
768
+ const baseUrl = resolveApiBaseUrl();
769
+ const url = new URL('/api/v1/memory-compaction', baseUrl).toString();
770
+ const body = { apply };
771
+ if (category) body.category = category;
772
+
773
+ let response;
774
+ try {
775
+ response = await httpJson(url, 'POST', body, buildApiAuthHeaders(accessToken));
776
+ } catch (error) {
777
+ printApiError(error);
778
+ process.exit(1);
779
+ }
780
+
781
+ const data = (response && response.data) ? response.data : response;
782
+ log(JSON.stringify(data));
486
783
  }
487
784
 
488
785
  async function runMemoryCommand(subArgs) {
489
- const sub = subArgs[0];
786
+ const helpRequested = subArgs.includes('--help') || subArgs.includes('-h');
787
+ const sub = subArgs[0] && !subArgs[0].startsWith('-') ? subArgs[0] : undefined;
788
+
789
+ if (helpRequested || !sub) {
790
+ printMemoryHelp(sub);
791
+ return;
792
+ }
793
+
490
794
  const rest = subArgs.slice(1);
491
795
 
492
796
  return withCliAnalyticsContext(
@@ -502,9 +806,12 @@ async function runMemoryCommand(subArgs) {
502
806
  if (sub === 'duplicates') return runMemoryDuplicates(rest);
503
807
  if (sub === 'merge') return runMemoryMerge(rest);
504
808
  if (sub === 'export') return runMemoryExport(rest);
809
+ if (sub === 'import') return runMemoryImport(rest);
810
+ if (sub === 'stats') return runMemoryStats(rest);
811
+ if (sub === 'compact') return runMemoryCompact(rest);
505
812
 
506
813
  warn('Unknown memory subcommand: ' + (sub || '(none)'));
507
- warn('Usage: empirical memory <query|record|note|update|delete|duplicates|merge|export> [options]');
814
+ warn('Usage: empirical memory <query|record|note|update|delete|duplicates|merge|export|import|stats|compact> [options]');
508
815
  process.exit(1);
509
816
  })
510
817
  );
@@ -524,5 +831,9 @@ module.exports = {
524
831
  runMemoryDuplicates,
525
832
  runMemoryMerge,
526
833
  runMemoryExport,
834
+ runMemoryImport,
835
+ runMemoryStats,
836
+ runMemoryCompact,
837
+ runMemoryDeleteBulk,
527
838
  runMemoryCommand
528
839
  };
@@ -1,81 +1,84 @@
1
- 'use strict';
2
-
3
- const { log, warn, uiHeader, uiSuccess } = require('./ui');
4
- const { detectInstallTargets } = require('./installers/detect');
5
- const {
6
- installNativePlugin,
7
- OPTIONAL_PLUGIN_NAMES = ['empirical-work-history', 'empirical-scar-tissue', 'empirical-todo']
8
- } = require('./installers/nativePlugin');
9
- const { checkNativePluginUpdates } = require('./pluginUpdateCheck');
10
-
11
- const SUPPORTED_TARGETS = ['codex', 'copilot', 'claude'];
12
-
13
- function isPluginCommand(args = []) {
14
- return args[0] === 'plugin';
15
- }
16
-
17
- function resolveTargets(targetArg) {
18
- if (targetArg === 'all') return SUPPORTED_TARGETS;
19
- if (targetArg && SUPPORTED_TARGETS.includes(targetArg)) return [targetArg];
20
- if (targetArg) {
21
- throw new Error(`Unsupported plugin target "${targetArg}". Use codex, copilot, claude, or all.`);
22
- }
23
-
24
- const detected = detectInstallTargets().map((target) => target.id);
25
- if (detected.length === 0) {
26
- throw new Error('No supported CLI detected. Specify a target explicitly: codex, copilot, claude, or all.');
27
- }
28
- return detected;
29
- }
30
-
31
- function resolvePluginNames(args = []) {
32
- const names = ['empirical-memory'];
33
- if (args.includes('--all-optional') || args.includes('--work-history')) names.push('empirical-work-history');
34
- if (args.includes('--all-optional') || args.includes('--scar-tissue')) names.push('empirical-scar-tissue');
35
- if (args.includes('--all-optional') || args.includes('--todo')) names.push('empirical-todo');
36
- return names.filter((name) => name === 'empirical-memory' || OPTIONAL_PLUGIN_NAMES.includes(name));
37
- }
38
-
39
- async function runPluginCommand(args = []) {
40
- const action = args[0];
41
- if (action === 'check') {
42
- const result = await checkNativePluginUpdates({
43
- force: args.includes('--force'),
44
- interactive: Boolean(process.stdin.isTTY)
45
- });
46
- if (result.skipped && result.reason === 'rate_limited') {
47
- log('Native plugin update check was recently completed.');
48
- } else if (result.outdated?.length) {
49
- log(uiSuccess('Native plugin update check complete.'));
50
- } else if (!result.skipped) {
51
- log('Native plugins are up to date.');
52
- }
53
- return;
54
- }
55
- if (action !== 'install') {
56
- throw new Error('Usage: empirical plugin install <codex|copilot|claude|all> or empirical plugin check [--force]');
57
- }
58
-
59
- const targets = resolveTargets(args[1]);
60
- const pluginNames = resolvePluginNames(args.slice(2));
61
- log(uiHeader('Empirical native plugin installer'));
62
- log('');
63
- for (const target of targets) {
64
- log(`Installing empirical-memory for ${target}...`);
65
- if (pluginNames.length === 1) installNativePlugin(target);
66
- else installNativePlugin(target, pluginNames);
67
- log(uiSuccess(`${target} plugins installed through its marketplace: ${pluginNames.join(', ')}.`));
68
- }
69
- log('');
70
- log('Native plugin installation does not configure MCP or credentials.');
71
- log('Optional packages: --work-history, --scar-tissue, --todo, or --all-optional.');
72
- log('Use `empirical auth login` for normal CLI OAuth, or the full `empirical install` flow for MCP setup.');
73
- }
74
-
75
- module.exports = {
76
- SUPPORTED_TARGETS,
77
- isPluginCommand,
78
- resolveTargets,
79
- resolvePluginNames,
80
- runPluginCommand
81
- };
1
+ 'use strict';
2
+
3
+ const { log, warn, uiHeader, uiSuccess } = require('./ui');
4
+ const { detectInstallTargets } = require('./installers/detect');
5
+ const {
6
+ installNativePlugin,
7
+ OPTIONAL_PLUGIN_NAMES = ['empirical-work-history', 'empirical-scar-tissue', 'empirical-todo']
8
+ } = require('./installers/nativePlugin');
9
+ const { checkNativePluginUpdates } = require('./pluginUpdateCheck');
10
+
11
+ const SUPPORTED_TARGETS = ['codex', 'copilot', 'claude'];
12
+
13
+ function isPluginCommand(args = []) {
14
+ return args[0] === 'plugin';
15
+ }
16
+
17
+ function resolveTargets(targetArg) {
18
+ if (targetArg === 'all') return SUPPORTED_TARGETS;
19
+ if (targetArg && SUPPORTED_TARGETS.includes(targetArg)) return [targetArg];
20
+ if (targetArg) {
21
+ throw new Error(`Unsupported plugin target "${targetArg}". Use codex, copilot, claude, or all.`);
22
+ }
23
+
24
+ const detected = detectInstallTargets().map((target) => target.id);
25
+ if (detected.length === 0) {
26
+ throw new Error('No supported CLI detected. Specify a target explicitly: codex, copilot, claude, or all.');
27
+ }
28
+ return detected;
29
+ }
30
+
31
+ function resolvePluginNames(args = []) {
32
+ const names = ['empirical-memory'];
33
+ if (args.includes('--all-optional') || args.includes('--work-history')) names.push('empirical-work-history');
34
+ if (args.includes('--all-optional') || args.includes('--scar-tissue')) names.push('empirical-scar-tissue');
35
+ if (args.includes('--all-optional') || args.includes('--todo')) names.push('empirical-todo');
36
+ return names.filter((name) => name === 'empirical-memory' || OPTIONAL_PLUGIN_NAMES.includes(name));
37
+ }
38
+
39
+ async function runPluginCommand(args = []) {
40
+ const action = args[0];
41
+ if (action === 'check') {
42
+ const result = await checkNativePluginUpdates({
43
+ force: args.includes('--force'),
44
+ interactive: Boolean(process.stdin.isTTY)
45
+ });
46
+ if (result.skipped && result.reason === 'rate_limited') {
47
+ log('Native plugin update check was recently completed.');
48
+ } else if (result.outdated?.length) {
49
+ log(uiSuccess('Native plugin update check complete.'));
50
+ } else if (!result.skipped) {
51
+ log('Native plugins are up to date.');
52
+ }
53
+ if (result.notInstalled?.length) {
54
+ for (const target of result.notInstalled) log(`${target}: not installed`);
55
+ }
56
+ return;
57
+ }
58
+ if (action !== 'install') {
59
+ throw new Error('Usage: empirical plugin install <codex|copilot|claude|all> or empirical plugin check [--force]');
60
+ }
61
+
62
+ const targets = resolveTargets(args[1]);
63
+ const pluginNames = resolvePluginNames(args.slice(2));
64
+ log(uiHeader('Empirical native plugin installer'));
65
+ log('');
66
+ for (const target of targets) {
67
+ log(`Installing empirical-memory for ${target}...`);
68
+ if (pluginNames.length === 1) installNativePlugin(target);
69
+ else installNativePlugin(target, pluginNames);
70
+ log(uiSuccess(`${target} plugins installed through its marketplace: ${pluginNames.join(', ')}.`));
71
+ }
72
+ log('');
73
+ log('Native plugin installation does not configure MCP or credentials.');
74
+ log('Optional packages: --work-history, --scar-tissue, --todo, or --all-optional.');
75
+ log('Use `empirical auth login` for normal CLI OAuth, or the full `empirical install` flow for MCP setup.');
76
+ }
77
+
78
+ module.exports = {
79
+ SUPPORTED_TARGETS,
80
+ isPluginCommand,
81
+ resolveTargets,
82
+ resolvePluginNames,
83
+ runPluginCommand
84
+ };