@empirical-gauzza/cli 1.0.9

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.
@@ -0,0 +1,528 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { log, warn } = require('./ui');
6
+ const { httpJson, printApiError } = require('./http');
7
+ const { parseOption, parseFlag } = require('./env');
8
+ const { requireAccessToken, resolveApiBaseUrl, buildApiAuthHeaders } = require('./auth');
9
+ const { MEMORY_CATEGORIES } = require('./constants');
10
+ const { withSelectedWorkspaceContext } = require('./workspaceContext');
11
+
12
+ async function withCliAnalyticsContext(context, fn) {
13
+ globalThis.__empiricalCliAnalyticsContext = context || {};
14
+ try {
15
+ return await fn();
16
+ } finally {
17
+ globalThis.__empiricalCliAnalyticsContext = null;
18
+ }
19
+ }
20
+
21
+ async function resolveMemoryByQuery(match, minScore, accessToken, baseUrl) {
22
+ const queryUrl = new URL('/api/v1/memories/query', baseUrl).toString();
23
+ let response;
24
+ try {
25
+ response = await httpJson(queryUrl, 'POST', { query: match, topK: 5 }, buildApiAuthHeaders(accessToken));
26
+ } catch (error) {
27
+ return { found: false, reason: 'error', message: error.message };
28
+ }
29
+
30
+ const results = (response && response.data && Array.isArray(response.data.results))
31
+ ? response.data.results
32
+ : (Array.isArray(response) ? response : []);
33
+
34
+ if (!results.length) {
35
+ return { found: false, reason: 'no_results' };
36
+ }
37
+
38
+ const scored = results.map((r) => ({
39
+ id: r.id,
40
+ summary: r.summary,
41
+ score: r.rankScore != null ? r.rankScore : (r.similarityScore != null ? r.similarityScore : 0)
42
+ }));
43
+
44
+ scored.sort((a, b) => b.score - a.score);
45
+
46
+ const top = scored[0];
47
+ const second = scored[1];
48
+
49
+ if (top.score < minScore) {
50
+ return { found: false, reason: 'low_score', candidates: scored.slice(0, 3) };
51
+ }
52
+
53
+ if (second && (top.score - second.score) <= 0.05) {
54
+ return { found: false, reason: 'ambiguous', candidates: scored.slice(0, 3) };
55
+ }
56
+
57
+ return { found: true, memory: { id: top.id, summary: top.summary, score: top.score } };
58
+ }
59
+
60
+ function printMemoryResolveError(result) {
61
+ if (result.reason === 'no_results') {
62
+ warn(JSON.stringify({ error: true, code: 'no_results', message: 'No memories matched the query.' }));
63
+ } else if (result.reason === 'low_score') {
64
+ warn(JSON.stringify({
65
+ error: true,
66
+ code: 'low_score',
67
+ message: 'Top result score was below --min-score threshold.',
68
+ candidates: result.candidates
69
+ }));
70
+ } else if (result.reason === 'ambiguous') {
71
+ warn(JSON.stringify({
72
+ error: true,
73
+ code: 'ambiguous',
74
+ message: 'Multiple memories matched with similar scores. Use --memory-id to target one directly.',
75
+ candidates: result.candidates
76
+ }));
77
+ } else if (result.reason === 'error') {
78
+ warn(JSON.stringify({ error: true, code: 'query_error', message: result.message }));
79
+ } else {
80
+ warn(JSON.stringify({ error: true, code: 'resolve_failed', message: 'Could not resolve memory.' }));
81
+ }
82
+ }
83
+
84
+ // ─── Memory subcommands ───────────────────────────────────────────────────────
85
+
86
+ async function runMemoryQuery(args) {
87
+ const match = parseOption(args, '--match', '');
88
+ if (!match) {
89
+ warn('Usage: empirical memory query --match <query> [--top-k 3] [--category <cat>] [--tags <t1,t2>] [--pretty]');
90
+ process.exit(1);
91
+ }
92
+
93
+ const topKRaw = Number(parseOption(args, '--top-k', '3'));
94
+ const topK = Math.min(100, Math.max(1, Number.isFinite(topKRaw) ? topKRaw : 3));
95
+ const category = parseOption(args, '--category', '');
96
+ const tagsRaw = parseOption(args, '--tags', '');
97
+ const tags = tagsRaw ? tagsRaw.split(',').map((t) => t.trim()).filter(Boolean) : [];
98
+ const pretty = parseFlag(args, '--pretty');
99
+
100
+ const accessToken = requireAccessToken();
101
+ const baseUrl = resolveApiBaseUrl();
102
+ const queryUrl = new URL('/api/v1/memories/query', baseUrl).toString();
103
+
104
+ const body = { query: match, topK };
105
+ const filters = {};
106
+ if (category) filters.category = category;
107
+ if (tags.length) filters.tags = tags;
108
+ if (Object.keys(filters).length) body.filters = filters;
109
+
110
+ let response;
111
+ try {
112
+ response = await httpJson(queryUrl, 'POST', body, buildApiAuthHeaders(accessToken));
113
+ } catch (error) {
114
+ printApiError(error);
115
+ process.exit(1);
116
+ }
117
+
118
+ const results = (response && response.data && Array.isArray(response.data.results))
119
+ ? response.data.results
120
+ : (Array.isArray(response) ? response : []);
121
+
122
+ if (pretty) {
123
+ log(JSON.stringify(results, null, 2));
124
+ } else {
125
+ log(JSON.stringify(results));
126
+ }
127
+ }
128
+
129
+ async function runMemoryGet(args) {
130
+ const memoryId = parseOption(args, '--memory-id', '');
131
+ const pretty = parseFlag(args, '--pretty');
132
+ if (!memoryId) {
133
+ warn('Usage: empirical memory get --memory-id <id> [--pretty]');
134
+ process.exit(1);
135
+ }
136
+
137
+ const accessToken = requireAccessToken();
138
+ const baseUrl = resolveApiBaseUrl();
139
+ const url = new URL(`/api/v1/memories/${encodeURIComponent(memoryId)}`, baseUrl).toString();
140
+ let response;
141
+ try {
142
+ response = await httpJson(url, 'GET', undefined, buildApiAuthHeaders(accessToken));
143
+ } catch (error) {
144
+ printApiError(error);
145
+ process.exit(1);
146
+ }
147
+ const data = response && response.data ? response.data : response;
148
+ log(pretty ? JSON.stringify(data, null, 2) : JSON.stringify(data));
149
+ }
150
+
151
+ async function runMemoryList(args) {
152
+ const category = parseOption(args, '--category', '');
153
+ const tags = parseOption(args, '--tags', '');
154
+ const startDate = parseOption(args, '--start-date', '');
155
+ const endDate = parseOption(args, '--end-date', '');
156
+ const limitRaw = Number(parseOption(args, '--limit', '20'));
157
+ const limit = Math.min(100, Math.max(1, Number.isFinite(limitRaw) ? limitRaw : 20));
158
+ const cursor = parseOption(args, '--cursor', '');
159
+ const sortOrder = parseFlag(args, '--oldest') ? 'asc' : 'desc';
160
+ const pretty = parseFlag(args, '--pretty');
161
+
162
+ const params = new URLSearchParams();
163
+ if (category) params.set('category', category);
164
+ if (tags) params.set('tags', tags);
165
+ if (startDate) params.set('startDate', startDate);
166
+ if (endDate) params.set('endDate', endDate);
167
+ params.set('limit', String(limit));
168
+ if (cursor) params.set('cursor', cursor);
169
+ params.set('sortBy', 'createdAt');
170
+ params.set('sortOrder', sortOrder);
171
+
172
+ const accessToken = requireAccessToken();
173
+ const baseUrl = resolveApiBaseUrl();
174
+ const url = new URL(`/api/v1/memories?${params.toString()}`, baseUrl).toString();
175
+ let response;
176
+ try {
177
+ response = await httpJson(url, 'GET', undefined, buildApiAuthHeaders(accessToken));
178
+ } catch (error) {
179
+ printApiError(error);
180
+ process.exit(1);
181
+ }
182
+ const data = response && response.data ? response.data : response;
183
+ log(pretty ? JSON.stringify(data, null, 2) : JSON.stringify(data));
184
+ }
185
+
186
+ async function runMemoryRecord(args) {
187
+ const jsonFile = parseOption(args, '--json-file', '');
188
+ let payload;
189
+
190
+ if (jsonFile) {
191
+ try {
192
+ payload = JSON.parse(fs.readFileSync(path.resolve(jsonFile), 'utf8'));
193
+ } catch (error) {
194
+ warn(JSON.stringify({ error: true, code: 'json_file_error', message: error.message }));
195
+ process.exit(1);
196
+ }
197
+ } else {
198
+ const category = parseOption(args, '--category', '');
199
+ const summary = parseOption(args, '--summary', '');
200
+
201
+ if (!category || !summary) {
202
+ warn('Usage: empirical memory record --category <cat> --summary <text> [--tags t1,t2] [--node-type observation]');
203
+ warn('Valid categories: ' + MEMORY_CATEGORIES.join(', '));
204
+ process.exit(1);
205
+ }
206
+
207
+ if (!MEMORY_CATEGORIES.includes(category)) {
208
+ warn(JSON.stringify({
209
+ error: true,
210
+ code: 'invalid_category',
211
+ message: `Invalid category "${category}". Valid: ${MEMORY_CATEGORIES.join(', ')}`
212
+ }));
213
+ process.exit(1);
214
+ }
215
+
216
+ const tagsRaw = parseOption(args, '--tags', '');
217
+ const tags = tagsRaw ? tagsRaw.split(',').map((t) => t.trim()).filter(Boolean) : [];
218
+ const nodeType = parseOption(args, '--node-type', 'observation');
219
+
220
+ payload = { category, summary, nodeType, data: { text: String(summary) } };
221
+ if (tags.length) payload.tags = tags;
222
+ }
223
+
224
+ if (
225
+ payload
226
+ && payload.summary !== undefined
227
+ && payload.data === undefined
228
+ && payload.originalPayload === undefined
229
+ ) {
230
+ payload.data = { text: String(payload.summary) };
231
+ }
232
+
233
+ const accessToken = requireAccessToken();
234
+ const baseUrl = resolveApiBaseUrl();
235
+ const recordUrl = new URL('/api/v1/memories', baseUrl).toString();
236
+
237
+ let response;
238
+ try {
239
+ response = await httpJson(recordUrl, 'POST', payload, buildApiAuthHeaders(accessToken));
240
+ } catch (error) {
241
+ printApiError(error);
242
+ process.exit(1);
243
+ }
244
+
245
+ const created = (response && response.data) ? response.data : response;
246
+ log(JSON.stringify(created));
247
+ }
248
+
249
+ async function runMemoryNote(args) {
250
+ const match = parseOption(args, '--match', '');
251
+ const text = parseOption(args, '--text', '');
252
+ const memoryId = parseOption(args, '--memory-id', '');
253
+ const minScore = Number(parseOption(args, '--min-score', '0.35'));
254
+
255
+ if (!text) {
256
+ warn('Usage: empirical memory note --text <note> (--match <query> | --memory-id <id>) [--min-score 0.35]');
257
+ process.exit(1);
258
+ }
259
+
260
+ if (!match && !memoryId) {
261
+ warn('Usage: empirical memory note --text <note> (--match <query> | --memory-id <id>)');
262
+ process.exit(1);
263
+ }
264
+
265
+ const accessToken = requireAccessToken();
266
+ const baseUrl = resolveApiBaseUrl();
267
+
268
+ let targetId = memoryId;
269
+ if (!targetId) {
270
+ const result = await resolveMemoryByQuery(match, minScore, accessToken, baseUrl);
271
+ if (!result.found) {
272
+ printMemoryResolveError(result);
273
+ process.exit(1);
274
+ }
275
+ targetId = result.memory.id;
276
+ }
277
+
278
+ const noteUrl = new URL(`/api/v1/memories/${encodeURIComponent(targetId)}/notes`, baseUrl).toString();
279
+ let response;
280
+ try {
281
+ response = await httpJson(noteUrl, 'POST', { text }, buildApiAuthHeaders(accessToken));
282
+ } catch (error) {
283
+ printApiError(error);
284
+ process.exit(1);
285
+ }
286
+
287
+ const created = (response && response.data) ? response.data : response;
288
+ log(JSON.stringify(created));
289
+ }
290
+
291
+ async function runMemoryUpdate(args) {
292
+ const match = parseOption(args, '--match', '');
293
+ const patchJson = parseOption(args, '--patch-json', '');
294
+ const jsonFile = parseOption(args, '--json-file', '');
295
+ const memoryId = parseOption(args, '--memory-id', '');
296
+ const minScore = Number(parseOption(args, '--min-score', '0.35'));
297
+
298
+ if (!patchJson && !jsonFile) {
299
+ warn('Usage: empirical memory update (--match <query> | --memory-id <id>) (--patch-json <json> | --json-file <path>)');
300
+ process.exit(1);
301
+ }
302
+
303
+ if (!match && !memoryId) {
304
+ warn('Usage: empirical memory update (--match <query> | --memory-id <id>) (--patch-json <json> | --json-file <path>)');
305
+ process.exit(1);
306
+ }
307
+
308
+ let patch;
309
+ if (jsonFile) {
310
+ try {
311
+ patch = JSON.parse(fs.readFileSync(path.resolve(jsonFile), 'utf8'));
312
+ } catch (error) {
313
+ warn(JSON.stringify({ error: true, code: 'json_file_error', message: error.message }));
314
+ process.exit(1);
315
+ }
316
+ } else {
317
+ try {
318
+ patch = JSON.parse(patchJson);
319
+ } catch (error) {
320
+ warn(JSON.stringify({ error: true, code: 'invalid_json', message: `--patch-json parse error: ${error.message}` }));
321
+ process.exit(1);
322
+ }
323
+ }
324
+
325
+ const accessToken = requireAccessToken();
326
+ const baseUrl = resolveApiBaseUrl();
327
+
328
+ let targetId = memoryId;
329
+ if (!targetId) {
330
+ const result = await resolveMemoryByQuery(match, minScore, accessToken, baseUrl);
331
+ if (!result.found) {
332
+ printMemoryResolveError(result);
333
+ process.exit(1);
334
+ }
335
+ targetId = result.memory.id;
336
+ }
337
+
338
+ const updateUrl = new URL(`/api/v1/memories/${encodeURIComponent(targetId)}`, baseUrl).toString();
339
+ let response;
340
+ try {
341
+ response = await httpJson(updateUrl, 'PUT', patch, buildApiAuthHeaders(accessToken));
342
+ } catch (error) {
343
+ printApiError(error);
344
+ process.exit(1);
345
+ }
346
+
347
+ const updated = (response && response.data) ? response.data : response;
348
+ log(JSON.stringify(updated));
349
+ }
350
+
351
+ async function runMemoryDelete(args) {
352
+ const match = parseOption(args, '--match', '');
353
+ const memoryId = parseOption(args, '--memory-id', '');
354
+ const minScore = Number(parseOption(args, '--min-score', '0.35'));
355
+ const confirm = parseFlag(args, '--confirm');
356
+
357
+ if (!match && !memoryId) {
358
+ warn('Usage: empirical memory delete (--match <query> | --memory-id <id>) [--confirm] [--min-score 0.35]');
359
+ process.exit(1);
360
+ }
361
+
362
+ const accessToken = requireAccessToken();
363
+ const baseUrl = resolveApiBaseUrl();
364
+
365
+ let targetId = memoryId;
366
+ let targetSummary = '';
367
+
368
+ if (!targetId) {
369
+ const result = await resolveMemoryByQuery(match, minScore, accessToken, baseUrl);
370
+ if (!result.found) {
371
+ if (!confirm && (result.reason === 'ambiguous' || result.reason === 'low_score')) {
372
+ // Dry-run: return candidates info
373
+ log(JSON.stringify({ dryRun: true, wouldDelete: false, reason: result.reason, candidates: result.candidates }));
374
+ process.exit(0);
375
+ }
376
+ printMemoryResolveError(result);
377
+ process.exit(1);
378
+ }
379
+ targetId = result.memory.id;
380
+ targetSummary = result.memory.summary;
381
+ }
382
+
383
+ if (!confirm) {
384
+ log(JSON.stringify({ dryRun: true, wouldDelete: true, id: targetId, summary: targetSummary }));
385
+ process.exit(0);
386
+ }
387
+
388
+ const deleteUrl = new URL(`/api/v1/memories/${encodeURIComponent(targetId)}`, baseUrl).toString();
389
+ try {
390
+ await httpJson(deleteUrl, 'DELETE', null, buildApiAuthHeaders(accessToken));
391
+ } catch (error) {
392
+ printApiError(error);
393
+ process.exit(1);
394
+ }
395
+
396
+ log(JSON.stringify({ deleted: true, id: targetId, summary: targetSummary }));
397
+ }
398
+
399
+ async function runMemoryDuplicates(args) {
400
+ const memoryId = parseOption(args, '--memory-id', '');
401
+ if (!memoryId) {
402
+ warn('Usage: empirical memory duplicates --memory-id <id>');
403
+ process.exit(1);
404
+ }
405
+
406
+ const accessToken = requireAccessToken();
407
+ const baseUrl = resolveApiBaseUrl();
408
+ const url = new URL(`/api/v1/memories/${encodeURIComponent(memoryId)}/duplicate-candidates`, baseUrl).toString();
409
+
410
+ let response;
411
+ try {
412
+ response = await httpJson(url, 'GET', undefined, buildApiAuthHeaders(accessToken));
413
+ } catch (error) {
414
+ printApiError(error);
415
+ process.exit(1);
416
+ }
417
+
418
+ const data = (response && response.data) ? response.data : response;
419
+ log(JSON.stringify(data));
420
+ }
421
+
422
+ async function runMemoryMerge(args) {
423
+ const canonicalId = parseOption(args, '--canonical-id', '');
424
+ const duplicateId = parseOption(args, '--duplicate-id', '');
425
+ const mergeTags = !parseFlag(args, '--no-merge-tags');
426
+ const mergeRelationships = !parseFlag(args, '--no-merge-relationships');
427
+
428
+ if (!canonicalId || !duplicateId) {
429
+ warn('Usage: empirical memory merge --canonical-id <id> --duplicate-id <id> [--no-merge-tags] [--no-merge-relationships]');
430
+ process.exit(1);
431
+ }
432
+
433
+ const accessToken = requireAccessToken();
434
+ const baseUrl = resolveApiBaseUrl();
435
+ const url = new URL('/api/v1/memories/merge', baseUrl).toString();
436
+
437
+ let response;
438
+ try {
439
+ response = await httpJson(url, 'POST', { canonicalId, duplicateId, mergeTags, mergeRelationships }, buildApiAuthHeaders(accessToken));
440
+ } catch (error) {
441
+ printApiError(error);
442
+ process.exit(1);
443
+ }
444
+
445
+ const data = (response && response.data) ? response.data : response;
446
+ log(JSON.stringify(data));
447
+ }
448
+
449
+ async function runMemoryExport(args) {
450
+ const scope = parseOption(args, '--scope', '');
451
+ if (scope !== 'personal' && scope !== 'workspace') {
452
+ warn('Usage: empirical memory export --scope <personal|workspace> [--out <path>] [--workspace-id <id>]');
453
+ process.exit(1);
454
+ }
455
+
456
+ const workspaceId = parseOption(args, '--workspace-id', '');
457
+ if (scope === 'workspace' && !workspaceId) {
458
+ warn('Usage: empirical memory export --scope workspace --workspace-id <id> [--out <path>]');
459
+ process.exit(1);
460
+ }
461
+
462
+ const dateStamp = new Date().toISOString().slice(0, 10);
463
+ const outPath = parseOption(args, '--out', `./empirical-memory-export-${scope}-${dateStamp}.json`);
464
+
465
+ const accessToken = requireAccessToken();
466
+ const baseUrl = resolveApiBaseUrl();
467
+ const url = new URL(`/api/v1/memories/export?scope=${encodeURIComponent(scope)}`, baseUrl).toString();
468
+
469
+ const headers = buildApiAuthHeaders(accessToken);
470
+ if (workspaceId) {
471
+ headers['x-workspace-id'] = workspaceId;
472
+ }
473
+
474
+ let response;
475
+ try {
476
+ response = await httpJson(url, 'GET', undefined, headers);
477
+ } catch (error) {
478
+ printApiError(error);
479
+ process.exit(1);
480
+ }
481
+
482
+ const resolvedPath = path.resolve(outPath);
483
+ fs.writeFileSync(resolvedPath, JSON.stringify(response, null, 2) + '\n', 'utf8');
484
+
485
+ log(JSON.stringify({ exported: true, path: resolvedPath, memoryCount: response.memoryCount }));
486
+ }
487
+
488
+ async function runMemoryCommand(subArgs) {
489
+ const sub = subArgs[0];
490
+ const rest = subArgs.slice(1);
491
+
492
+ return withCliAnalyticsContext(
493
+ { command: `memory.${sub || 'unknown'}`, subcommand: sub || 'unknown' },
494
+ async () => withSelectedWorkspaceContext(async () => {
495
+ if (sub === 'query') return runMemoryQuery(rest);
496
+ if (sub === 'get') return runMemoryGet(rest);
497
+ if (sub === 'list') return runMemoryList(rest);
498
+ if (sub === 'record') return runMemoryRecord(rest);
499
+ if (sub === 'note') return runMemoryNote(rest);
500
+ if (sub === 'update') return runMemoryUpdate(rest);
501
+ if (sub === 'delete') return runMemoryDelete(rest);
502
+ if (sub === 'duplicates') return runMemoryDuplicates(rest);
503
+ if (sub === 'merge') return runMemoryMerge(rest);
504
+ if (sub === 'export') return runMemoryExport(rest);
505
+
506
+ warn('Unknown memory subcommand: ' + (sub || '(none)'));
507
+ warn('Usage: empirical memory <query|record|note|update|delete|duplicates|merge|export> [options]');
508
+ process.exit(1);
509
+ })
510
+ );
511
+ }
512
+
513
+ module.exports = {
514
+ withCliAnalyticsContext,
515
+ resolveMemoryByQuery,
516
+ printMemoryResolveError,
517
+ runMemoryQuery,
518
+ runMemoryGet,
519
+ runMemoryList,
520
+ runMemoryRecord,
521
+ runMemoryNote,
522
+ runMemoryUpdate,
523
+ runMemoryDelete,
524
+ runMemoryDuplicates,
525
+ runMemoryMerge,
526
+ runMemoryExport,
527
+ runMemoryCommand
528
+ };