@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.
- package/bin/empirical.js +12 -4
- package/lib/auth.js +126 -43
- package/lib/constants.js +72 -8
- package/lib/deviceConfig.js +37 -18
- package/lib/doctor.js +484 -116
- package/lib/help.js +8 -2
- package/lib/installWizard.js +8 -2
- package/lib/installers/claude.js +110 -93
- package/lib/installers/codex.js +101 -88
- package/lib/installers/copilot.js +97 -87
- package/lib/installers/coreMemoryBlock.js +91 -0
- package/lib/installers/nativePlugin.js +147 -87
- package/lib/installers/skill-variants/SKILL.both.md +154 -0
- package/lib/installers/skill-variants/SKILL.cli.md +150 -0
- package/lib/installers/skill-variants/SKILL.mcp.md +134 -0
- package/lib/installers/skillVariants.js +59 -0
- package/lib/kitCommands.js +219 -122
- package/lib/memoryCommands.js +362 -16
- package/lib/pluginCommands.js +84 -81
- package/lib/pluginUpdateCheck.js +48 -14
- package/lib/skillPaths.js +25 -0
- package/lib/skillSync.js +48 -56
- package/package.json +15 -2
package/lib/memoryCommands.js
CHANGED
|
@@ -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',
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
673
|
+
fs.writeFileSync(resolvedPath, JSON.stringify(envelope, null, 2) + '\n', 'utf8');
|
|
484
674
|
|
|
485
|
-
log(JSON.stringify({ exported: true, path: resolvedPath,
|
|
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
|
|
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
|
};
|