@empirical-gauzza/cli 1.0.36 → 1.0.38
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 +6 -0
- package/lib/attachmentCommands.js +388 -0
- package/lib/auth.js +16 -2
- package/lib/blueprintCommands.js +23 -3
- package/lib/constants.js +722 -670
- package/lib/empiricalTokensFormat.js +68 -0
- package/lib/help.js +23 -1
- package/lib/http.js +171 -3
- package/lib/installWizard.js +3 -1
- package/lib/installers/coreMemoryBlock.js +2 -1
- package/lib/installers/hooks.js +1312 -1312
- package/lib/installers/nativePlugin.js +1 -1
- package/lib/installers/promptBlocks.js +9 -0
- package/lib/installers/skill-variants/SKILL.both.md +20 -0
- package/lib/installers/skill-variants/SKILL.cli.md +231 -221
- package/lib/installers/skill-variants/SKILL.mcp.md +18 -0
- package/lib/memoryCommands.js +1267 -1220
- package/lib/memoryResolve.js +112 -0
- package/lib/pluginCommands.js +3 -2
- package/lib/setupCommands.js +751 -31
- package/lib/usageCommands.js +52 -0
- package/lib/workspaceCommands.js +72 -5
- package/lib/workspaceContext.js +176 -176
- package/package.json +1 -1
package/lib/memoryCommands.js
CHANGED
|
@@ -1,1220 +1,1267 @@
|
|
|
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, NODE_TYPES, DOC_TOPICS } = require('./constants');
|
|
10
|
-
const { withSelectedWorkspaceContext } = require('./workspaceContext');
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
//
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
if (
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
if (
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
if (
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
//
|
|
557
|
-
//
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
const
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
if (!
|
|
637
|
-
warn(
|
|
638
|
-
process.exit(1);
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
const
|
|
642
|
-
const
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
const
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
const
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
const
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
let
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
if (
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
const
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
const
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
const
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
const
|
|
911
|
-
const
|
|
912
|
-
const
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
const
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
} catch (error) {
|
|
1099
|
-
|
|
1100
|
-
process.exit(1);
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
|
-
const
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
const
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
}
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
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, NODE_TYPES, DOC_TOPICS } = require('./constants');
|
|
10
|
+
const { withSelectedWorkspaceContext } = require('./workspaceContext');
|
|
11
|
+
const { resolveMemoryByQuery, parseMatchFilters, printMemoryResolveError } = require('./memoryResolve');
|
|
12
|
+
const { runAttachmentCommand, ATTACHMENT_SUBCOMMAND_USAGE } = require('./attachmentCommands');
|
|
13
|
+
|
|
14
|
+
// ─── Table rendering (--format table) ─────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
function truncateForTable(value, maxLen) {
|
|
17
|
+
const str = value === undefined || value === null ? '' : String(value);
|
|
18
|
+
return str.length > maxLen ? `${str.slice(0, maxLen - 1)}…` : str;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function renderTable(rows, columns) {
|
|
22
|
+
if (!Array.isArray(rows) || rows.length === 0) return '(no rows)';
|
|
23
|
+
const cols = columns && columns.length ? columns : Object.keys(rows[0]);
|
|
24
|
+
const cellText = (row, col) => (row[col] === undefined || row[col] === null ? '' : String(row[col]));
|
|
25
|
+
const widths = cols.map((col) => Math.max(col.length, ...rows.map((row) => cellText(row, col).length)));
|
|
26
|
+
const renderRow = (cells) => cells.map((cell, i) => String(cell).padEnd(widths[i])).join(' ');
|
|
27
|
+
const lines = [renderRow(cols), renderRow(widths.map((w) => '-'.repeat(w)))];
|
|
28
|
+
for (const row of rows) {
|
|
29
|
+
lines.push(renderRow(cols.map((col) => cellText(row, col))));
|
|
30
|
+
}
|
|
31
|
+
return lines.join('\n');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function renderMemoryResultsTable(items) {
|
|
35
|
+
const rows = (Array.isArray(items) ? items : []).map((m) => ({
|
|
36
|
+
id: m.id || m._id || '',
|
|
37
|
+
category: m.category || '',
|
|
38
|
+
// A row marked usage 'style_preference_only' (a writing voice memory) says so, so a person
|
|
39
|
+
// reading the table sees what an agent reading the JSON sees.
|
|
40
|
+
summary: truncateForTable(m.summary, 60) + (m.usage === 'style_preference_only' ? ' [style]' : ''),
|
|
41
|
+
// The recall front door emits `score` (toQueryRow plus score in mcp/tools/memory.ts); the
|
|
42
|
+
// older /query route emitted rankScore/similarityScore. Read all three so the column is not
|
|
43
|
+
// blank against a real /recall row. On the list pathways score is deliberately absent, and
|
|
44
|
+
// an empty cell is the honest rendering of "this pathway does not rank".
|
|
45
|
+
score: m.score != null ? m.score : (m.rankScore != null ? m.rankScore : (m.similarityScore != null ? m.similarityScore : ''))
|
|
46
|
+
}));
|
|
47
|
+
return renderTable(rows, ['id', 'category', 'summary', 'score']);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// The retrieval pathways `memory query --path` may pin. Same four names the server's shared
|
|
51
|
+
// normalizer accepts (packages/server/src/services/recallParams.service.ts). Omitting --path
|
|
52
|
+
// leaves the classifier deciding, exactly as before.
|
|
53
|
+
const RECALL_PATHS = ['semantic', 'list', 'graph', 'hebbian'];
|
|
54
|
+
const RECALL_HOPS = [1, 2];
|
|
55
|
+
// Hops only mean something on a pathway that walks relationships.
|
|
56
|
+
const RECALL_HOP_PATHS = ['graph', 'hebbian'];
|
|
57
|
+
// Reminder states `memory query --remind-state` may keep. Same enum the server's shared
|
|
58
|
+
// normalizer and Memory.remindState declare.
|
|
59
|
+
const REMIND_STATES = ['pending', 'notified', 'acknowledged', 'cancelled'];
|
|
60
|
+
|
|
61
|
+
// ─── Scoped `--help` (C-005 item 5) ───────────────────────────────────────────
|
|
62
|
+
// `empirical memory --help` / `empirical memory <sub> --help` print scoped usage instead of
|
|
63
|
+
// the 90-line global help wall bin/empirical.js otherwise short-circuits every --help to.
|
|
64
|
+
|
|
65
|
+
const MEMORY_SUBCOMMAND_USAGE = {
|
|
66
|
+
query: 'Usage: empirical memory query --match <query> [--top-k 3] [--category <cat>] [--tags <t1,t2>] [--tags-all <t1,t2>] [--status <s1,s2>] [--due-before <ISO date>] [--due-after <ISO date>] [--remind-state pending,notified] [--path semantic|list|graph|hebbian] [--hops 1|2] [--workspaces <n|name|id,...>] [--all-workspaces] [--pretty] [--format table]',
|
|
67
|
+
get: 'Usage: empirical memory get --memory-id <id> [--pretty]',
|
|
68
|
+
list: 'Usage: empirical memory list [--category <cat>] [--tags <t1,t2>] [--start-date <ISO>] [--end-date <ISO>] [--limit 20] [--cursor <cursor>] [--oldest] [--pretty] [--format table]',
|
|
69
|
+
record: 'Usage: empirical memory record --category <cat> (--summary <text> | --summary - | --summary-file <path>) [--tags t1,t2] [--node-type observation] [--json-file <path>] [--no-dedupe] [--no-auto-link] [--author-type user|agent] [--item-type <key>] [--item-status <key>] [--parent <memoryId>] [--assignee <userId>] [--field name=value ...]\n The five item arguments only mean something in a TEAM workspace that has a shape: elsewhere they are accepted and stored as nothing.',
|
|
70
|
+
note: 'Usage: empirical memory note --text <note> (--match <query> | --memory-id <id>) [--min-score 0.35]',
|
|
71
|
+
update: 'Usage: empirical memory update (--match <query> | --memory-id <id>) (--patch-json <json> | --json-file <path>) [--min-score 0.35]',
|
|
72
|
+
acknowledge: 'Usage: empirical memory acknowledge --memory-id <id> [--cancel] (marks a due reminder done, or dropped with --cancel; frees its slot and clears the alert)',
|
|
73
|
+
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',
|
|
74
|
+
attachment: `Files attached to a memory. Up to 20 per memory, 25MB each; a delete is reversible for 30 days.\n${Object.values(ATTACHMENT_SUBCOMMAND_USAGE).join('\n')}`,
|
|
75
|
+
attach: ATTACHMENT_SUBCOMMAND_USAGE.add + '\n (shorthand for `empirical memory attachment add`)',
|
|
76
|
+
attachments: ATTACHMENT_SUBCOMMAND_USAGE.list + '\n (shorthand for `empirical memory attachment list`)',
|
|
77
|
+
tree: 'Usage: empirical memory tree (--memory-id <id> | --match <query>) [--depth 10] [--max-nodes 500] [--no-notes] [--format markdown|json] [--out <path>] [--pretty]\n One item and everything nested under it, as a readable document. Markdown by default.',
|
|
78
|
+
duplicates: 'Usage: empirical memory duplicates --memory-id <id>',
|
|
79
|
+
merge: 'Usage: empirical memory merge --canonical-id <id> --duplicate-id <id> [--no-merge-tags] [--no-merge-relationships]',
|
|
80
|
+
export: 'Usage: empirical memory export --scope <personal|workspace> [--out <path>] [--workspace-id <id>]',
|
|
81
|
+
import: 'Usage: empirical memory import <file> [--dry-run]',
|
|
82
|
+
'bulk-import': 'Usage: empirical memory bulk-import <file> [--dry-run]\nFile format: { "memories": [{ "localKey": "...", "category": "...", "nodeType": "...", "summary": "...", "data": {...} }], "relationships": [{ "fromLocalKey": "...", "toLocalKey": "...", "type": "..." }] }',
|
|
83
|
+
stats: 'Usage: empirical memory stats [--format table]',
|
|
84
|
+
compact: 'Usage: empirical memory compact [--apply] [--category <cat>]'
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
function printMemoryHelp(sub) {
|
|
88
|
+
if (sub && MEMORY_SUBCOMMAND_USAGE[sub]) {
|
|
89
|
+
log(MEMORY_SUBCOMMAND_USAGE[sub]);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
log(DOC_TOPICS.memory);
|
|
93
|
+
log('');
|
|
94
|
+
log('Subcommands: ' + Object.keys(MEMORY_SUBCOMMAND_USAGE).join(', '));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function withCliAnalyticsContext(context, fn) {
|
|
98
|
+
globalThis.__empiricalCliAnalyticsContext = context || {};
|
|
99
|
+
try {
|
|
100
|
+
return await fn();
|
|
101
|
+
} finally {
|
|
102
|
+
globalThis.__empiricalCliAnalyticsContext = null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
// ─── Memory subcommands ───────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Turn `--workspaces` selectors (list numbers, names, or ids) into workspace ids using the
|
|
111
|
+
* per-user listing (/api/v1/users/me/workspaces, the same list `workspace list` prints; number 1
|
|
112
|
+
* is always the Default Workspace). An id (ws_...) passes through without a lookup. An unknown
|
|
113
|
+
* name is fatal at the terminal rather than a 403 or an empty answer after the round trip.
|
|
114
|
+
*/
|
|
115
|
+
async function resolveWorkspaceIdsForQuery({ baseUrl, accessToken, selectors = [], all = false }) {
|
|
116
|
+
const needsListing = all || selectors.some((s) => !/^ws_/.test(s));
|
|
117
|
+
let options = [];
|
|
118
|
+
if (needsListing) {
|
|
119
|
+
const listUrl = new URL('/api/v1/users/me/workspaces', baseUrl).toString();
|
|
120
|
+
let payload;
|
|
121
|
+
try {
|
|
122
|
+
payload = await httpJson(listUrl, 'GET', undefined, buildApiAuthHeaders(accessToken));
|
|
123
|
+
} catch (error) {
|
|
124
|
+
printApiError(error);
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
options = [
|
|
128
|
+
...(payload?.defaultOption ? [payload.defaultOption] : []),
|
|
129
|
+
...(Array.isArray(payload?.workspaces) ? payload.workspaces : [])
|
|
130
|
+
].filter((o) => o && o.workspaceId);
|
|
131
|
+
}
|
|
132
|
+
if (all) return [...new Set(options.map((o) => o.workspaceId))];
|
|
133
|
+
|
|
134
|
+
const ids = [];
|
|
135
|
+
for (const selector of selectors) {
|
|
136
|
+
if (/^ws_/.test(selector)) { ids.push(selector); continue; }
|
|
137
|
+
const asNumber = Number(selector);
|
|
138
|
+
let match = null;
|
|
139
|
+
if (Number.isInteger(asNumber) && asNumber >= 1 && asNumber <= options.length) {
|
|
140
|
+
match = options[asNumber - 1];
|
|
141
|
+
} else {
|
|
142
|
+
const lower = selector.toLowerCase();
|
|
143
|
+
const byLabel = options.filter((o) => String(o.label || '').toLowerCase() === lower || String(o.key || '').toLowerCase() === lower);
|
|
144
|
+
if (byLabel.length > 1) {
|
|
145
|
+
warn(`Ambiguous workspace "${selector}". Use its list number or workspace id.`);
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
match = byLabel[0] || null;
|
|
149
|
+
}
|
|
150
|
+
if (!match) {
|
|
151
|
+
warn(`Workspace not found: "${selector}". Run \`empirical workspace list\` to see names and numbers.`);
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
ids.push(match.workspaceId);
|
|
155
|
+
}
|
|
156
|
+
return [...new Set(ids)];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function runMemoryQuery(args) {
|
|
160
|
+
const match = parseOption(args, '--match', '');
|
|
161
|
+
if (!match) {
|
|
162
|
+
warn(MEMORY_SUBCOMMAND_USAGE.query);
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const topKRaw = Number(parseOption(args, '--top-k', '3'));
|
|
167
|
+
const topK = Math.min(100, Math.max(1, Number.isFinite(topKRaw) ? topKRaw : 3));
|
|
168
|
+
const category = parseOption(args, '--category', '');
|
|
169
|
+
const tagsRaw = parseOption(args, '--tags', '');
|
|
170
|
+
const tags = tagsRaw ? tagsRaw.split(',').map((t) => t.trim()).filter(Boolean) : [];
|
|
171
|
+
// R3 (2026-09-06): --tags-all is an AND match (all must be present), unlike --tags' OR match;
|
|
172
|
+
// --status filters data.status. Both are optional and additive to --tags/--category.
|
|
173
|
+
const tagsAllRaw = parseOption(args, '--tags-all', '');
|
|
174
|
+
const tagsAll = tagsAllRaw ? tagsAllRaw.split(',').map((t) => t.trim()).filter(Boolean) : [];
|
|
175
|
+
// Multi-workspace recall (2026-09-24). --workspaces takes list numbers, names, or ids; names
|
|
176
|
+
// and numbers are resolved against the same listing `workspace list` prints, so the server
|
|
177
|
+
// contract stays ids-only. --all-workspaces sends every selectable workspace. Without either
|
|
178
|
+
// flag the query is scoped to the single active workspace exactly as before.
|
|
179
|
+
const workspacesRaw = parseOption(args, '--workspaces', '');
|
|
180
|
+
const workspaceSelectors = workspacesRaw ? workspacesRaw.split(',').map((w) => w.trim()).filter(Boolean) : [];
|
|
181
|
+
const allWorkspaces = parseFlag(args, '--all-workspaces');
|
|
182
|
+
if (workspaceSelectors.length && allWorkspaces) {
|
|
183
|
+
warn('Pass either --workspaces or --all-workspaces, not both.');
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
186
|
+
const statusRaw = parseOption(args, '--status', '');
|
|
187
|
+
const status = statusRaw ? statusRaw.split(',').map((s) => s.trim()).filter(Boolean) : [];
|
|
188
|
+
const pretty = parseFlag(args, '--pretty');
|
|
189
|
+
const format = parseOption(args, '--format', 'json');
|
|
190
|
+
|
|
191
|
+
// Reminder filters. Dates are parsed HERE as well as server-side, for the same reason --due
|
|
192
|
+
// is on record: a typo fails at the terminal, not as a 400 after the round trip. Anything
|
|
193
|
+
// Date can parse is accepted and sent as the caller typed it.
|
|
194
|
+
const dueFilters = {};
|
|
195
|
+
for (const [flag, key] of [['--due-before', 'dueBefore'], ['--due-after', 'dueAfter']]) {
|
|
196
|
+
const raw = parseOption(args, flag, '');
|
|
197
|
+
if (!raw) continue;
|
|
198
|
+
if (Number.isNaN(new Date(raw).getTime())) {
|
|
199
|
+
warn(`Invalid ${flag} "${raw}". Use an ISO 8601 date, for example 2026-10-14 or 2026-10-14T09:00:00Z.`);
|
|
200
|
+
process.exit(1);
|
|
201
|
+
}
|
|
202
|
+
dueFilters[key] = String(raw).trim();
|
|
203
|
+
}
|
|
204
|
+
const remindStateRaw = parseOption(args, '--remind-state', '');
|
|
205
|
+
const remindState = remindStateRaw
|
|
206
|
+
? remindStateRaw.split(',').map((s) => s.trim().toLowerCase()).filter(Boolean)
|
|
207
|
+
: [];
|
|
208
|
+
const unknownStates = remindState.filter((s) => !REMIND_STATES.includes(s));
|
|
209
|
+
if (unknownStates.length) {
|
|
210
|
+
warn(`--remind-state must be one of: ${REMIND_STATES.join(', ')} (received ${JSON.stringify(unknownStates)})`);
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// --path / --hops are validated HERE, where the value is decided, so a typo costs no round
|
|
215
|
+
// trip and the message names the four pathways. The server validates the same way (the shared
|
|
216
|
+
// normalizer in packages/server/src/services/recallParams.service.ts); this is a fast local
|
|
217
|
+
// echo of that contract, never a second, looser one.
|
|
218
|
+
const pathRaw = parseOption(args, '--path', '');
|
|
219
|
+
let path = '';
|
|
220
|
+
if (pathRaw) {
|
|
221
|
+
path = String(pathRaw).trim().toLowerCase();
|
|
222
|
+
if (!RECALL_PATHS.includes(path)) {
|
|
223
|
+
warn(`--path must be one of: ${RECALL_PATHS.join(', ')} (received ${JSON.stringify(pathRaw)})`);
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const hopsRaw = parseOption(args, '--hops', '');
|
|
229
|
+
let hops = null;
|
|
230
|
+
if (hopsRaw) {
|
|
231
|
+
const parsedHops = Number(hopsRaw);
|
|
232
|
+
if (!RECALL_HOPS.includes(parsedHops)) {
|
|
233
|
+
warn(`--hops must be 1 or 2 (received ${JSON.stringify(hopsRaw)})`);
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
236
|
+
// Silently ignoring --hops on a ranked or list pathway would look like it took effect.
|
|
237
|
+
if (!RECALL_HOP_PATHS.includes(path)) {
|
|
238
|
+
warn(`--hops only applies with --path graph or --path hebbian${path ? ` (received --path ${path})` : ' (no --path given)'}`);
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
hops = parsedHops;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const accessToken = requireAccessToken();
|
|
245
|
+
const baseUrl = resolveApiBaseUrl();
|
|
246
|
+
// The recall front door: the same answer the MCP query_memories tool gives, which /query never
|
|
247
|
+
// gave (classifier routing, the list block, the exact-tag retry, agent-note exclusion,
|
|
248
|
+
// retrievalPath). It takes its parameters FLAT, matching the shared normalizer, not nested
|
|
249
|
+
// under `filters` the way the older /query route did. Send one shape only.
|
|
250
|
+
const queryUrl = new URL('/api/v1/memories/recall', baseUrl).toString();
|
|
251
|
+
|
|
252
|
+
const body = { query: match, topK };
|
|
253
|
+
if (category) body.category = category;
|
|
254
|
+
if (tags.length) body.tags = tags;
|
|
255
|
+
if (tagsAll.length) body.tagsAll = tagsAll;
|
|
256
|
+
if (workspaceSelectors.length || allWorkspaces) {
|
|
257
|
+
body.workspaceIds = await resolveWorkspaceIdsForQuery({
|
|
258
|
+
baseUrl,
|
|
259
|
+
accessToken,
|
|
260
|
+
selectors: workspaceSelectors,
|
|
261
|
+
all: allWorkspaces
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
if (status.length) body.status = status;
|
|
265
|
+
if (dueFilters.dueBefore) body.dueBefore = dueFilters.dueBefore;
|
|
266
|
+
if (dueFilters.dueAfter) body.dueAfter = dueFilters.dueAfter;
|
|
267
|
+
if (remindState.length) body.remindState = remindState;
|
|
268
|
+
if (path) body.path = path;
|
|
269
|
+
if (hops !== null) body.hops = hops;
|
|
270
|
+
|
|
271
|
+
let response;
|
|
272
|
+
try {
|
|
273
|
+
response = await httpJson(queryUrl, 'POST', body, buildApiAuthHeaders(accessToken));
|
|
274
|
+
} catch (error) {
|
|
275
|
+
// A 404 here means the server predates the recall front door. Say that plainly instead of
|
|
276
|
+
// rerouting to /query: that fallback would quietly answer through a different routing path
|
|
277
|
+
// with different defaults (topK 10 against 3) and keep the old route alive forever.
|
|
278
|
+
if (error && error.statusCode === 404) {
|
|
279
|
+
warn(JSON.stringify({
|
|
280
|
+
error: true,
|
|
281
|
+
code: 'recall_route_missing',
|
|
282
|
+
message: 'This server does not serve /api/v1/memories/recall yet. Upgrade the Empirical server, or use an older CLI against it.'
|
|
283
|
+
}));
|
|
284
|
+
process.exit(1);
|
|
285
|
+
}
|
|
286
|
+
printApiError(error);
|
|
287
|
+
process.exit(1);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// The front door answers with data.memories; the older /query route answered with data.results.
|
|
291
|
+
// Both are accepted so a CLI pointed at a server that predates /recall still prints rows.
|
|
292
|
+
const data = response && response.data ? response.data : null;
|
|
293
|
+
const results = (data && Array.isArray(data.memories))
|
|
294
|
+
? data.memories
|
|
295
|
+
: ((data && Array.isArray(data.results))
|
|
296
|
+
? data.results
|
|
297
|
+
: (Array.isArray(response) ? response : []));
|
|
298
|
+
|
|
299
|
+
if (format === 'table') {
|
|
300
|
+
log(renderMemoryResultsTable(results));
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (pretty) {
|
|
305
|
+
log(JSON.stringify(results, null, 2));
|
|
306
|
+
} else {
|
|
307
|
+
log(JSON.stringify(results));
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async function runMemoryGet(args) {
|
|
312
|
+
const memoryId = parseOption(args, '--memory-id', '');
|
|
313
|
+
const pretty = parseFlag(args, '--pretty');
|
|
314
|
+
if (!memoryId) {
|
|
315
|
+
warn('Usage: empirical memory get --memory-id <id> [--pretty]');
|
|
316
|
+
process.exit(1);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const accessToken = requireAccessToken();
|
|
320
|
+
const baseUrl = resolveApiBaseUrl();
|
|
321
|
+
const url = new URL(`/api/v1/memories/${encodeURIComponent(memoryId)}`, baseUrl).toString();
|
|
322
|
+
let response;
|
|
323
|
+
try {
|
|
324
|
+
response = await httpJson(url, 'GET', undefined, buildApiAuthHeaders(accessToken));
|
|
325
|
+
} catch (error) {
|
|
326
|
+
printApiError(error);
|
|
327
|
+
process.exit(1);
|
|
328
|
+
}
|
|
329
|
+
const data = response && response.data ? response.data : response;
|
|
330
|
+
log(pretty ? JSON.stringify(data, null, 2) : JSON.stringify(data));
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async function runMemoryList(args) {
|
|
334
|
+
const category = parseOption(args, '--category', '');
|
|
335
|
+
const tags = parseOption(args, '--tags', '');
|
|
336
|
+
const startDate = parseOption(args, '--start-date', '');
|
|
337
|
+
const endDate = parseOption(args, '--end-date', '');
|
|
338
|
+
const limitRaw = Number(parseOption(args, '--limit', '20'));
|
|
339
|
+
const limit = Math.min(100, Math.max(1, Number.isFinite(limitRaw) ? limitRaw : 20));
|
|
340
|
+
const cursor = parseOption(args, '--cursor', '');
|
|
341
|
+
const sortOrder = parseFlag(args, '--oldest') ? 'asc' : 'desc';
|
|
342
|
+
const pretty = parseFlag(args, '--pretty');
|
|
343
|
+
const format = parseOption(args, '--format', 'json');
|
|
344
|
+
|
|
345
|
+
const params = new URLSearchParams();
|
|
346
|
+
if (category) params.set('category', category);
|
|
347
|
+
if (tags) params.set('tags', tags);
|
|
348
|
+
if (startDate) params.set('startDate', startDate);
|
|
349
|
+
if (endDate) params.set('endDate', endDate);
|
|
350
|
+
params.set('limit', String(limit));
|
|
351
|
+
if (cursor) params.set('cursor', cursor);
|
|
352
|
+
params.set('sortBy', 'createdAt');
|
|
353
|
+
params.set('sortOrder', sortOrder);
|
|
354
|
+
|
|
355
|
+
const accessToken = requireAccessToken();
|
|
356
|
+
const baseUrl = resolveApiBaseUrl();
|
|
357
|
+
const url = new URL(`/api/v1/memories?${params.toString()}`, baseUrl).toString();
|
|
358
|
+
let response;
|
|
359
|
+
try {
|
|
360
|
+
response = await httpJson(url, 'GET', undefined, buildApiAuthHeaders(accessToken));
|
|
361
|
+
} catch (error) {
|
|
362
|
+
printApiError(error);
|
|
363
|
+
process.exit(1);
|
|
364
|
+
}
|
|
365
|
+
const data = response && response.data ? response.data : response;
|
|
366
|
+
|
|
367
|
+
if (format === 'table') {
|
|
368
|
+
const items = Array.isArray(data && data.memories) ? data.memories : (Array.isArray(data) ? data : []);
|
|
369
|
+
log(renderMemoryResultsTable(items));
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
log(pretty ? JSON.stringify(data, null, 2) : JSON.stringify(data));
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Shell substitution silently eats code out of a double-quoted --summary: backticks become
|
|
377
|
+
// command substitution and ${...} becomes variable expansion BEFORE the CLI ever sees the
|
|
378
|
+
// text, and the write still succeeds, so the memory looks fine with the code gone. Two
|
|
379
|
+
// defenses: (1) inputs that never pass through shell quoting -- `--summary-file <path>` and
|
|
380
|
+
// `--summary -` (stdin, pair it with a single-quoted heredoc); (2) a warning when an inline
|
|
381
|
+
// summary carries the residue of an eaten substitution.
|
|
382
|
+
function resolveSummaryInput(args, { readFile = (p) => fs.readFileSync(path.resolve(p), 'utf8'), readStdin = () => fs.readFileSync(0, 'utf8') } = {}) {
|
|
383
|
+
const summaryFile = parseOption(args, '--summary-file', '');
|
|
384
|
+
if (summaryFile) {
|
|
385
|
+
return { summary: String(readFile(summaryFile)).replace(/\r?\n$/, ''), source: 'file' };
|
|
386
|
+
}
|
|
387
|
+
const inline = parseOption(args, '--summary', '');
|
|
388
|
+
if (inline === '-') {
|
|
389
|
+
return { summary: String(readStdin()).replace(/\r?\n$/, ''), source: 'stdin' };
|
|
390
|
+
}
|
|
391
|
+
return { summary: inline, source: inline ? 'inline' : 'none' };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function detectShellMangling(summary) {
|
|
395
|
+
const text = String(summary || '');
|
|
396
|
+
const reasons = [];
|
|
397
|
+
if (/``/.test(text)) reasons.push('an empty backtick pair (a `command` was probably substituted away)');
|
|
398
|
+
if (/\$(\s|$|[^A-Za-z0-9_{(])/.test(text) && !/\$\d/.test(text)) reasons.push('a lone "$" (a ${VAR} or $(cmd) was probably expanded to nothing)');
|
|
399
|
+
if (/\$\{\s*\}/.test(text)) reasons.push('an empty ${} placeholder');
|
|
400
|
+
return reasons;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
async function runMemoryRecord(args) {
|
|
404
|
+
const jsonFile = parseOption(args, '--json-file', '');
|
|
405
|
+
let payload;
|
|
406
|
+
|
|
407
|
+
if (jsonFile) {
|
|
408
|
+
try {
|
|
409
|
+
payload = JSON.parse(fs.readFileSync(path.resolve(jsonFile), 'utf8'));
|
|
410
|
+
} catch (error) {
|
|
411
|
+
warn(JSON.stringify({ error: true, code: 'json_file_error', message: error.message }));
|
|
412
|
+
process.exit(1);
|
|
413
|
+
}
|
|
414
|
+
} else {
|
|
415
|
+
const category = parseOption(args, '--category', '');
|
|
416
|
+
const { summary, source: summarySource } = resolveSummaryInput(args);
|
|
417
|
+
|
|
418
|
+
if (summarySource === 'inline') {
|
|
419
|
+
const mangled = detectShellMangling(summary);
|
|
420
|
+
if (mangled.length) {
|
|
421
|
+
warn(JSON.stringify({
|
|
422
|
+
warning: 'possible_shell_substitution',
|
|
423
|
+
message: 'The summary looks like the shell altered it before the CLI saw it: ' + mangled.join('; ') + '. Recording anyway. For text containing backticks or ${...}, use --summary-file <path>, or --summary - with a single-quoted heredoc, or the MCP record_graph_memory tool.'
|
|
424
|
+
}));
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (!category || !summary) {
|
|
429
|
+
// The canonical string, not a second copy: this line used to hold its own and had already
|
|
430
|
+
// drifted from MEMORY_SUBCOMMAND_USAGE.record, so a flag added to one was missing from the
|
|
431
|
+
// other, which is the message a person actually sees when they get the arguments wrong.
|
|
432
|
+
warn(MEMORY_SUBCOMMAND_USAGE.record);
|
|
433
|
+
warn('Valid categories: ' + MEMORY_CATEGORIES.join(', '));
|
|
434
|
+
warn('Valid node types: ' + NODE_TYPES.join(', '));
|
|
435
|
+
process.exit(1);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (!MEMORY_CATEGORIES.includes(category)) {
|
|
439
|
+
warn(JSON.stringify({
|
|
440
|
+
error: true,
|
|
441
|
+
code: 'invalid_category',
|
|
442
|
+
message: `Invalid category "${category}". Valid: ${MEMORY_CATEGORIES.join(', ')}`
|
|
443
|
+
}));
|
|
444
|
+
process.exit(1);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const tagsRaw = parseOption(args, '--tags', '');
|
|
448
|
+
const tags = tagsRaw ? tagsRaw.split(',').map((t) => t.trim()).filter(Boolean) : [];
|
|
449
|
+
const nodeType = parseOption(args, '--node-type', 'observation');
|
|
450
|
+
|
|
451
|
+
if (!NODE_TYPES.includes(nodeType)) {
|
|
452
|
+
warn(JSON.stringify({
|
|
453
|
+
error: true,
|
|
454
|
+
code: 'invalid_node_type',
|
|
455
|
+
message: `Invalid nodeType "${nodeType}". Valid: ${NODE_TYPES.join(', ')}`
|
|
456
|
+
}));
|
|
457
|
+
process.exit(1);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
payload = { category, summary, nodeType, data: { text: String(summary) } };
|
|
461
|
+
if (tags.length) payload.tags = tags;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (
|
|
465
|
+
payload
|
|
466
|
+
&& payload.summary !== undefined
|
|
467
|
+
&& payload.data === undefined
|
|
468
|
+
&& payload.originalPayload === undefined
|
|
469
|
+
) {
|
|
470
|
+
payload.data = { text: String(payload.summary) };
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// C-002 (docs/prd-cli-and-plugin-fixes.md): the server now dedupes/auto-links every
|
|
474
|
+
// POST /api/v1/memories write by default, matching the MCP record_graph_memory tool's
|
|
475
|
+
// guarantee. Only send dedupeEnabled/autoLink at all when the caller explicitly opted
|
|
476
|
+
// out here -- omitting them entirely lets the server's own default-on behavior apply,
|
|
477
|
+
// rather than this CLI baking in a redundant "true" that could drift from the server's
|
|
478
|
+
// actual default over time.
|
|
479
|
+
if (parseFlag(args, '--no-dedupe')) {
|
|
480
|
+
payload.dedupeEnabled = false;
|
|
481
|
+
}
|
|
482
|
+
if (parseFlag(args, '--no-auto-link')) {
|
|
483
|
+
payload.autoLink = false;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Authorship (2026-09-09). `source` is observable truth: this is the CLI, so say so.
|
|
487
|
+
// Until now the CLI sent nothing, the server fell back to the transport it saw ('api'),
|
|
488
|
+
// and api maps to authorType 'user' because that mapping was written for the web portal.
|
|
489
|
+
// Every agent write through this command was therefore stored as authored by the user.
|
|
490
|
+
// 'cli' resolves to authorType 'unknown', which is the honest answer when nobody says.
|
|
491
|
+
if (payload.source === undefined) payload.source = 'cli';
|
|
492
|
+
|
|
493
|
+
// --author-type lets the caller state what the transport cannot know. An agent recording
|
|
494
|
+
// its own session notes should pass 'agent'; an agent recording something the user said
|
|
495
|
+
// or decided should pass 'user'. Omitted, it stays 'unknown' rather than being guessed.
|
|
496
|
+
const authorType = parseOption(args, '--author-type', '');
|
|
497
|
+
if (authorType) {
|
|
498
|
+
if (!['user', 'agent', 'system'].includes(authorType)) {
|
|
499
|
+
warn(JSON.stringify({
|
|
500
|
+
error: true,
|
|
501
|
+
code: 'invalid_author_type',
|
|
502
|
+
message: `Invalid --author-type "${authorType}". Valid: user, agent, system`
|
|
503
|
+
}));
|
|
504
|
+
process.exit(1);
|
|
505
|
+
}
|
|
506
|
+
payload.authorType = authorType;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// PRD 8.1 item arguments. Sent under the names the REST door takes (ITEM_WRITE_ARGUMENTS in
|
|
510
|
+
// @empirical/shared): itemType, itemStatus, parentId, assignee and fields. The CLI deliberately
|
|
511
|
+
// does NOT check them against a shape; it has no shape and the server refuses with the
|
|
512
|
+
// workspace's own vocabulary, which is a better message than anything guessed here.
|
|
513
|
+
//
|
|
514
|
+
// Guarded on payload being an object: --json-file accepts whatever parses, so a file holding a
|
|
515
|
+
// bare array or null would otherwise crash here on assignment rather than in the request.
|
|
516
|
+
const canCarryItemArguments = payload && typeof payload === 'object' && !Array.isArray(payload);
|
|
517
|
+
for (const [flag, key] of [
|
|
518
|
+
['--item-type', 'itemType'],
|
|
519
|
+
['--item-status', 'itemStatus'],
|
|
520
|
+
['--parent', 'parentId'],
|
|
521
|
+
['--assignee', 'assignee']
|
|
522
|
+
]) {
|
|
523
|
+
const value = parseOption(args, flag, '');
|
|
524
|
+
if (value && canCarryItemArguments) payload[key] = value;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// --field is repeatable and collected into one object, so `--field notes=hi --field source=web`
|
|
528
|
+
// sends { notes: 'hi', source: 'web' }. Split on the FIRST '=' only, or a value that contains one
|
|
529
|
+
// ('query=a=b') would be truncated. A malformed pair fails here rather than as a 400, because the
|
|
530
|
+
// person is at a terminal and the fix is in what they just typed.
|
|
531
|
+
const fieldPairs = [];
|
|
532
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
533
|
+
if (args[i] !== '--field') continue;
|
|
534
|
+
fieldPairs.push(args[i + 1]);
|
|
535
|
+
i += 1;
|
|
536
|
+
}
|
|
537
|
+
if (fieldPairs.length) {
|
|
538
|
+
const fields = {};
|
|
539
|
+
for (const pair of fieldPairs) {
|
|
540
|
+
const at = typeof pair === 'string' ? pair.indexOf('=') : -1;
|
|
541
|
+
const name = at > 0 ? pair.slice(0, at).trim() : '';
|
|
542
|
+
if (!name) {
|
|
543
|
+
warn(JSON.stringify({
|
|
544
|
+
error: true,
|
|
545
|
+
code: 'invalid_field',
|
|
546
|
+
message: `Invalid --field ${JSON.stringify(pair === undefined ? '' : pair)}. Use the form --field name=value, for example --field notes=hello. Repeat --field for more than one.`
|
|
547
|
+
}));
|
|
548
|
+
process.exit(1);
|
|
549
|
+
}
|
|
550
|
+
fields[name] = pair.slice(at + 1);
|
|
551
|
+
}
|
|
552
|
+
if (canCarryItemArguments) payload.fields = fields;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// --due turns the memory into a reminder. Parsed and rejected HERE as well as server-side, so
|
|
556
|
+
// a typo fails immediately at the terminal instead of coming back as a 400 after the write was
|
|
557
|
+
// composed. Accepts anything Date can parse, so '2026-10-14' and a full ISO timestamp both work.
|
|
558
|
+
const dueRaw = parseOption(args, '--due', '');
|
|
559
|
+
if (dueRaw) {
|
|
560
|
+
const parsedDue = new Date(dueRaw);
|
|
561
|
+
if (Number.isNaN(parsedDue.getTime())) {
|
|
562
|
+
warn(JSON.stringify({
|
|
563
|
+
error: true,
|
|
564
|
+
code: 'invalid_due',
|
|
565
|
+
message: `Invalid --due "${dueRaw}". Use an ISO 8601 date, for example 2026-10-14 or 2026-10-14T09:00:00Z.`
|
|
566
|
+
}));
|
|
567
|
+
process.exit(1);
|
|
568
|
+
}
|
|
569
|
+
payload.dueAt = parsedDue.toISOString();
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const accessToken = requireAccessToken();
|
|
573
|
+
const baseUrl = resolveApiBaseUrl();
|
|
574
|
+
const recordUrl = new URL('/api/v1/memories', baseUrl).toString();
|
|
575
|
+
|
|
576
|
+
let response;
|
|
577
|
+
try {
|
|
578
|
+
response = await httpJson(recordUrl, 'POST', payload, buildApiAuthHeaders(accessToken));
|
|
579
|
+
} catch (error) {
|
|
580
|
+
printApiError(error);
|
|
581
|
+
process.exit(1);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const created = (response && response.data) ? response.data : response;
|
|
585
|
+
log(JSON.stringify(created));
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
async function runMemoryNote(args) {
|
|
589
|
+
const match = parseOption(args, '--match', '');
|
|
590
|
+
const text = parseOption(args, '--text', '');
|
|
591
|
+
const memoryId = parseOption(args, '--memory-id', '');
|
|
592
|
+
const minScore = Number(parseOption(args, '--min-score', '0.35'));
|
|
593
|
+
|
|
594
|
+
if (!text) {
|
|
595
|
+
warn('Usage: empirical memory note --text <note> (--match <query> | --memory-id <id>) [--tags <t1,t2>] [--min-score 0.35]');
|
|
596
|
+
process.exit(1);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (!match && !memoryId) {
|
|
600
|
+
warn('Usage: empirical memory note --text <note> (--match <query> | --memory-id <id>) [--tags <t1,t2>]');
|
|
601
|
+
process.exit(1);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const accessToken = requireAccessToken();
|
|
605
|
+
const baseUrl = resolveApiBaseUrl();
|
|
606
|
+
|
|
607
|
+
let targetId = memoryId;
|
|
608
|
+
if (!targetId) {
|
|
609
|
+
const filters = parseMatchFilters(args);
|
|
610
|
+
const result = await resolveMemoryByQuery(match, minScore, accessToken, baseUrl, filters);
|
|
611
|
+
if (!result.found) {
|
|
612
|
+
printMemoryResolveError(result);
|
|
613
|
+
process.exit(1);
|
|
614
|
+
}
|
|
615
|
+
targetId = result.memory.id;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const noteUrl = new URL(`/api/v1/memories/${encodeURIComponent(targetId)}/notes`, baseUrl).toString();
|
|
619
|
+
let response;
|
|
620
|
+
try {
|
|
621
|
+
response = await httpJson(noteUrl, 'POST', { text }, buildApiAuthHeaders(accessToken));
|
|
622
|
+
} catch (error) {
|
|
623
|
+
printApiError(error);
|
|
624
|
+
process.exit(1);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const created = (response && response.data) ? response.data : response;
|
|
628
|
+
log(JSON.stringify(created));
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Closing a reminder from the terminal or from an agent reading a CLI banner. One PUT, the same
|
|
632
|
+
// transition the portal's Mark done / Cancel make; the server retires the alert on every surface.
|
|
633
|
+
async function runMemoryAcknowledge(args) {
|
|
634
|
+
const memoryId = parseOption(args, '--memory-id', '');
|
|
635
|
+
const cancel = parseFlag(args, '--cancel');
|
|
636
|
+
if (!memoryId) {
|
|
637
|
+
warn(MEMORY_SUBCOMMAND_USAGE.acknowledge);
|
|
638
|
+
process.exit(1);
|
|
639
|
+
}
|
|
640
|
+
const accessToken = requireAccessToken();
|
|
641
|
+
const baseUrl = resolveApiBaseUrl();
|
|
642
|
+
const url = new URL(`/api/v1/memories/${encodeURIComponent(memoryId)}`, baseUrl).toString();
|
|
643
|
+
let response;
|
|
644
|
+
try {
|
|
645
|
+
response = await httpJson(url, 'PUT', { remindState: cancel ? 'cancelled' : 'acknowledged' }, buildApiAuthHeaders(accessToken));
|
|
646
|
+
} catch (error) {
|
|
647
|
+
printApiError(error);
|
|
648
|
+
process.exit(1);
|
|
649
|
+
}
|
|
650
|
+
const data = response && response.data ? response.data : response;
|
|
651
|
+
log(JSON.stringify({ id: data.id, remindState: data.remindState, dueAt: data.dueAt || null, message: cancel ? 'Reminder cancelled. Its slot is free.' : 'Reminder marked done. Its slot is free.' }, null, 2));
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
async function runMemoryUpdate(args) {
|
|
655
|
+
const match = parseOption(args, '--match', '');
|
|
656
|
+
const patchJson = parseOption(args, '--patch-json', '');
|
|
657
|
+
const jsonFile = parseOption(args, '--json-file', '');
|
|
658
|
+
const memoryId = parseOption(args, '--memory-id', '');
|
|
659
|
+
const minScore = Number(parseOption(args, '--min-score', '0.35'));
|
|
660
|
+
|
|
661
|
+
if (!patchJson && !jsonFile) {
|
|
662
|
+
warn('Usage: empirical memory update (--match <query> | --memory-id <id>) (--patch-json <json> | --json-file <path>) [--tags <t1,t2>]');
|
|
663
|
+
process.exit(1);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (!match && !memoryId) {
|
|
667
|
+
warn('Usage: empirical memory update (--match <query> | --memory-id <id>) (--patch-json <json> | --json-file <path>) [--tags <t1,t2>]');
|
|
668
|
+
process.exit(1);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
let patch;
|
|
672
|
+
if (jsonFile) {
|
|
673
|
+
try {
|
|
674
|
+
patch = JSON.parse(fs.readFileSync(path.resolve(jsonFile), 'utf8'));
|
|
675
|
+
} catch (error) {
|
|
676
|
+
warn(JSON.stringify({ error: true, code: 'json_file_error', message: error.message }));
|
|
677
|
+
process.exit(1);
|
|
678
|
+
}
|
|
679
|
+
} else {
|
|
680
|
+
try {
|
|
681
|
+
patch = JSON.parse(patchJson);
|
|
682
|
+
} catch (error) {
|
|
683
|
+
warn(JSON.stringify({ error: true, code: 'invalid_json', message: `--patch-json parse error: ${error.message}` }));
|
|
684
|
+
process.exit(1);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const accessToken = requireAccessToken();
|
|
689
|
+
const baseUrl = resolveApiBaseUrl();
|
|
690
|
+
|
|
691
|
+
let targetId = memoryId;
|
|
692
|
+
if (!targetId) {
|
|
693
|
+
const filters = parseMatchFilters(args);
|
|
694
|
+
const result = await resolveMemoryByQuery(match, minScore, accessToken, baseUrl, filters);
|
|
695
|
+
if (!result.found) {
|
|
696
|
+
printMemoryResolveError(result);
|
|
697
|
+
process.exit(1);
|
|
698
|
+
}
|
|
699
|
+
targetId = result.memory.id;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
const updateUrl = new URL(`/api/v1/memories/${encodeURIComponent(targetId)}`, baseUrl).toString();
|
|
703
|
+
let response;
|
|
704
|
+
try {
|
|
705
|
+
response = await httpJson(updateUrl, 'PUT', patch, buildApiAuthHeaders(accessToken));
|
|
706
|
+
} catch (error) {
|
|
707
|
+
printApiError(error);
|
|
708
|
+
process.exit(1);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
const updated = (response && response.data) ? response.data : response;
|
|
712
|
+
log(JSON.stringify(updated));
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
async function runMemoryDeleteBulk(args) {
|
|
716
|
+
const tag = parseOption(args, '--tag', '');
|
|
717
|
+
const category = parseOption(args, '--category', '');
|
|
718
|
+
const all = parseFlag(args, '--all');
|
|
719
|
+
const confirm = parseFlag(args, '--confirm');
|
|
720
|
+
|
|
721
|
+
const selectorCount = [Boolean(tag), Boolean(category), all].filter(Boolean).length;
|
|
722
|
+
if (selectorCount !== 1) {
|
|
723
|
+
warn('Usage: empirical memory delete (--tag <t> | --category <c> | --all) --confirm');
|
|
724
|
+
process.exit(1);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
const accessToken = requireAccessToken();
|
|
728
|
+
const baseUrl = resolveApiBaseUrl();
|
|
729
|
+
const url = new URL('/api/v1/memory-bulk-delete', baseUrl).toString();
|
|
730
|
+
const body = { confirm };
|
|
731
|
+
if (tag) body.tag = tag;
|
|
732
|
+
if (category) body.category = category;
|
|
733
|
+
if (all) body.all = true;
|
|
734
|
+
|
|
735
|
+
let response;
|
|
736
|
+
try {
|
|
737
|
+
response = await httpJson(url, 'POST', body, buildApiAuthHeaders(accessToken));
|
|
738
|
+
} catch (error) {
|
|
739
|
+
printApiError(error);
|
|
740
|
+
process.exit(1);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// Server always reports the count first: without --confirm it's a dry-run preview and no
|
|
744
|
+
// writes happened; with --confirm it reports what it just deleted. Either way the count is
|
|
745
|
+
// printed, matching the single-memory delete's dry-run-by-default convention above.
|
|
746
|
+
const data = (response && response.data) ? response.data : response;
|
|
747
|
+
log(JSON.stringify(data));
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
async function runMemoryDelete(args) {
|
|
751
|
+
const tag = parseOption(args, '--tag', '');
|
|
752
|
+
const category = parseOption(args, '--category', '');
|
|
753
|
+
const all = parseFlag(args, '--all');
|
|
754
|
+
if (tag || category || all) {
|
|
755
|
+
return runMemoryDeleteBulk(args);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const match = parseOption(args, '--match', '');
|
|
759
|
+
const memoryId = parseOption(args, '--memory-id', '');
|
|
760
|
+
const minScore = Number(parseOption(args, '--min-score', '0.35'));
|
|
761
|
+
const confirm = parseFlag(args, '--confirm');
|
|
762
|
+
|
|
763
|
+
if (!match && !memoryId) {
|
|
764
|
+
warn('Usage: empirical memory delete (--match <query> | --memory-id <id>) [--tags <t1,t2>] [--confirm] [--min-score 0.35]');
|
|
765
|
+
warn(' or: empirical memory delete (--tag <t> | --category <c> | --all) --confirm');
|
|
766
|
+
process.exit(1);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
const accessToken = requireAccessToken();
|
|
770
|
+
const baseUrl = resolveApiBaseUrl();
|
|
771
|
+
|
|
772
|
+
let targetId = memoryId;
|
|
773
|
+
let targetSummary = '';
|
|
774
|
+
|
|
775
|
+
if (!targetId) {
|
|
776
|
+
const filters = parseMatchFilters(args);
|
|
777
|
+
const result = await resolveMemoryByQuery(match, minScore, accessToken, baseUrl, filters);
|
|
778
|
+
if (!result.found) {
|
|
779
|
+
if (!confirm && (result.reason === 'ambiguous' || result.reason === 'low_score')) {
|
|
780
|
+
// Dry-run: return candidates info
|
|
781
|
+
log(JSON.stringify({ dryRun: true, wouldDelete: false, reason: result.reason, candidates: result.candidates }));
|
|
782
|
+
process.exit(0);
|
|
783
|
+
}
|
|
784
|
+
printMemoryResolveError(result);
|
|
785
|
+
process.exit(1);
|
|
786
|
+
}
|
|
787
|
+
targetId = result.memory.id;
|
|
788
|
+
targetSummary = result.memory.summary;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
if (!confirm) {
|
|
792
|
+
log(JSON.stringify({ dryRun: true, wouldDelete: true, id: targetId, summary: targetSummary }));
|
|
793
|
+
process.exit(0);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
const deleteUrl = new URL(`/api/v1/memories/${encodeURIComponent(targetId)}`, baseUrl).toString();
|
|
797
|
+
try {
|
|
798
|
+
await httpJson(deleteUrl, 'DELETE', null, buildApiAuthHeaders(accessToken));
|
|
799
|
+
} catch (error) {
|
|
800
|
+
printApiError(error);
|
|
801
|
+
process.exit(1);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
log(JSON.stringify({ deleted: true, id: targetId, summary: targetSummary }));
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// `memory tree` is the terminal's answer to "give me this idea with everything under it". The
|
|
808
|
+
// server assembles the document (one indexed query over the item hierarchy), so this is a fetch
|
|
809
|
+
// and a write: no walking, no per-level calls.
|
|
810
|
+
async function runMemoryTree(args) {
|
|
811
|
+
const accessToken = requireAccessToken();
|
|
812
|
+
const baseUrl = resolveApiBaseUrl();
|
|
813
|
+
|
|
814
|
+
const memoryId = parseOption(args, '--memory-id', '');
|
|
815
|
+
const match = parseOption(args, '--match', '');
|
|
816
|
+
if (!memoryId && !match) {
|
|
817
|
+
warn(MEMORY_SUBCOMMAND_USAGE.tree);
|
|
818
|
+
process.exit(1);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
let targetId = memoryId;
|
|
822
|
+
if (!targetId) {
|
|
823
|
+
const minScore = Number(parseOption(args, '--min-score', '0.35'));
|
|
824
|
+
const result = await resolveMemoryByQuery(match, minScore, accessToken, baseUrl, parseMatchFilters(args));
|
|
825
|
+
if (!result.found) {
|
|
826
|
+
printMemoryResolveError(result);
|
|
827
|
+
process.exit(1);
|
|
828
|
+
}
|
|
829
|
+
targetId = result.memory.id;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
const query = new URLSearchParams();
|
|
833
|
+
const depth = parseOption(args, '--depth', '');
|
|
834
|
+
if (depth) query.set('depth', depth);
|
|
835
|
+
const maxNodes = parseOption(args, '--max-nodes', '');
|
|
836
|
+
if (maxNodes) query.set('maxNodes', maxNodes);
|
|
837
|
+
if (parseFlag(args, '--no-notes')) query.set('includeNotes', 'false');
|
|
838
|
+
|
|
839
|
+
const url = new URL(
|
|
840
|
+
`/api/v1/memories/${encodeURIComponent(targetId)}/tree${query.toString() ? `?${query}` : ''}`,
|
|
841
|
+
baseUrl
|
|
842
|
+
).toString();
|
|
843
|
+
|
|
844
|
+
let response;
|
|
845
|
+
try {
|
|
846
|
+
response = await httpJson(url, 'GET', undefined, buildApiAuthHeaders(accessToken));
|
|
847
|
+
} catch (error) {
|
|
848
|
+
printApiError(error);
|
|
849
|
+
process.exit(1);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
const data = (response && response.data) ? response.data : response;
|
|
853
|
+
|
|
854
|
+
// Markdown is the default here, unlike every other memory subcommand, because the whole point
|
|
855
|
+
// of this one is a document a person reads. --format json gives the nodes for scripting.
|
|
856
|
+
if (parseOption(args, '--format', 'markdown') === 'json') {
|
|
857
|
+
log(parseFlag(args, '--pretty') ? JSON.stringify(data, null, 2) : JSON.stringify(data));
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
const out = parseOption(args, '--out', '');
|
|
862
|
+
if (out) {
|
|
863
|
+
const target = path.resolve(out);
|
|
864
|
+
try {
|
|
865
|
+
fs.writeFileSync(target, data.markdown);
|
|
866
|
+
} catch (error) {
|
|
867
|
+
warn(JSON.stringify({ error: true, code: 'write_failed', message: `Could not write ${target}: ${error.message}` }));
|
|
868
|
+
process.exit(1);
|
|
869
|
+
}
|
|
870
|
+
log(JSON.stringify({ written: true, path: target, nodeCount: data.nodeCount, depth: data.depth, truncated: data.truncated }));
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
log(data.markdown);
|
|
875
|
+
// Truncation goes to stderr so it cannot end up inside a redirected document.
|
|
876
|
+
if (data.truncated) {
|
|
877
|
+
warn(JSON.stringify({
|
|
878
|
+
warning: true,
|
|
879
|
+
code: 'truncated',
|
|
880
|
+
message: `Document cut off at ${data.nodes.length} items. Raise --max-nodes or start lower in the tree.`
|
|
881
|
+
}));
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
async function runMemoryDuplicates(args) {
|
|
886
|
+
const memoryId = parseOption(args, '--memory-id', '');
|
|
887
|
+
if (!memoryId) {
|
|
888
|
+
warn('Usage: empirical memory duplicates --memory-id <id>');
|
|
889
|
+
process.exit(1);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
const accessToken = requireAccessToken();
|
|
893
|
+
const baseUrl = resolveApiBaseUrl();
|
|
894
|
+
const url = new URL(`/api/v1/memories/${encodeURIComponent(memoryId)}/duplicate-candidates`, baseUrl).toString();
|
|
895
|
+
|
|
896
|
+
let response;
|
|
897
|
+
try {
|
|
898
|
+
response = await httpJson(url, 'GET', undefined, buildApiAuthHeaders(accessToken));
|
|
899
|
+
} catch (error) {
|
|
900
|
+
printApiError(error);
|
|
901
|
+
process.exit(1);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
const data = (response && response.data) ? response.data : response;
|
|
905
|
+
log(JSON.stringify(data));
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
async function runMemoryMerge(args) {
|
|
909
|
+
const canonicalId = parseOption(args, '--canonical-id', '');
|
|
910
|
+
const duplicateId = parseOption(args, '--duplicate-id', '');
|
|
911
|
+
const mergeTags = !parseFlag(args, '--no-merge-tags');
|
|
912
|
+
const mergeRelationships = !parseFlag(args, '--no-merge-relationships');
|
|
913
|
+
|
|
914
|
+
if (!canonicalId || !duplicateId) {
|
|
915
|
+
warn('Usage: empirical memory merge --canonical-id <id> --duplicate-id <id> [--no-merge-tags] [--no-merge-relationships]');
|
|
916
|
+
process.exit(1);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
const accessToken = requireAccessToken();
|
|
920
|
+
const baseUrl = resolveApiBaseUrl();
|
|
921
|
+
const url = new URL('/api/v1/memories/merge', baseUrl).toString();
|
|
922
|
+
|
|
923
|
+
let response;
|
|
924
|
+
try {
|
|
925
|
+
response = await httpJson(url, 'POST', { canonicalId, duplicateId, mergeTags, mergeRelationships }, buildApiAuthHeaders(accessToken));
|
|
926
|
+
} catch (error) {
|
|
927
|
+
printApiError(error);
|
|
928
|
+
process.exit(1);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
const data = (response && response.data) ? response.data : response;
|
|
932
|
+
log(JSON.stringify(data));
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
async function runMemoryExport(args) {
|
|
936
|
+
const scope = parseOption(args, '--scope', '');
|
|
937
|
+
if (scope !== 'personal' && scope !== 'workspace') {
|
|
938
|
+
warn('Usage: empirical memory export --scope <personal|workspace> [--out <path>] [--workspace-id <id>]');
|
|
939
|
+
process.exit(1);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
const workspaceId = parseOption(args, '--workspace-id', '');
|
|
943
|
+
if (scope === 'workspace' && !workspaceId) {
|
|
944
|
+
warn('Usage: empirical memory export --scope workspace --workspace-id <id> [--out <path>]');
|
|
945
|
+
process.exit(1);
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
const dateStamp = new Date().toISOString().slice(0, 10);
|
|
949
|
+
const outPath = parseOption(args, '--out', `./empirical-memory-export-${scope}-${dateStamp}.json`);
|
|
950
|
+
|
|
951
|
+
const accessToken = requireAccessToken();
|
|
952
|
+
const baseUrl = resolveApiBaseUrl();
|
|
953
|
+
const url = new URL(`/api/v1/memories/export?scope=${encodeURIComponent(scope)}`, baseUrl).toString();
|
|
954
|
+
|
|
955
|
+
const headers = buildApiAuthHeaders(accessToken);
|
|
956
|
+
if (workspaceId) {
|
|
957
|
+
headers['x-workspace-id'] = workspaceId;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
let response;
|
|
961
|
+
try {
|
|
962
|
+
response = await httpJson(url, 'GET', undefined, headers);
|
|
963
|
+
} catch (error) {
|
|
964
|
+
printApiError(error);
|
|
965
|
+
process.exit(1);
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// The raw server response is the export envelope itself (streamed as one JSON body, not the
|
|
969
|
+
// standard {status,data} wrapper): {schemaVersion, generatedAt, scope, workspaceId,
|
|
970
|
+
// memoryCount, truncated, memories: [...]}. Re-shape it into the versioned, round-trippable
|
|
971
|
+
// format `memory import` consumes: {schemaVersion: 1, exportedAt, memories: [{category,
|
|
972
|
+
// summary, tags, nodeType, data, mass, gravity, timeContext, notes, relationships}]}.
|
|
973
|
+
const rawMemories = Array.isArray(response && response.memories) ? response.memories : [];
|
|
974
|
+
const envelope = {
|
|
975
|
+
schemaVersion: 1,
|
|
976
|
+
exportedAt: new Date().toISOString(),
|
|
977
|
+
memories: rawMemories.map((m) => ({
|
|
978
|
+
category: m.category,
|
|
979
|
+
summary: m.summary,
|
|
980
|
+
tags: Array.isArray(m.tags) ? m.tags : [],
|
|
981
|
+
nodeType: m.nodeType,
|
|
982
|
+
data: m.data,
|
|
983
|
+
mass: m.mass,
|
|
984
|
+
gravity: m.gravity,
|
|
985
|
+
timeContext: m.timeContext || null,
|
|
986
|
+
notes: Array.isArray(m.notes) ? m.notes.map((n) => ({ text: n.text })) : [],
|
|
987
|
+
relationships: Array.isArray(m.relationships)
|
|
988
|
+
? m.relationships.map((r) => ({ type: r.type, targetId: r.targetId, weight: r.weight }))
|
|
989
|
+
: []
|
|
990
|
+
}))
|
|
991
|
+
};
|
|
992
|
+
|
|
993
|
+
const resolvedPath = path.resolve(outPath);
|
|
994
|
+
fs.writeFileSync(resolvedPath, JSON.stringify(envelope, null, 2) + '\n', 'utf8');
|
|
995
|
+
|
|
996
|
+
log(JSON.stringify({ exported: true, path: resolvedPath, schemaVersion: envelope.schemaVersion, memoryCount: envelope.memories.length }));
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
async function runMemoryImport(args) {
|
|
1000
|
+
const filePath = args.find((a) => a && !a.startsWith('--')) || '';
|
|
1001
|
+
const dryRun = parseFlag(args, '--dry-run');
|
|
1002
|
+
|
|
1003
|
+
if (!filePath) {
|
|
1004
|
+
warn('Usage: empirical memory import <file> [--dry-run]');
|
|
1005
|
+
process.exit(1);
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
let doc;
|
|
1009
|
+
try {
|
|
1010
|
+
doc = JSON.parse(fs.readFileSync(path.resolve(filePath), 'utf8'));
|
|
1011
|
+
} catch (error) {
|
|
1012
|
+
warn(JSON.stringify({ error: true, code: 'json_file_error', message: error.message }));
|
|
1013
|
+
process.exit(1);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
const memories = Array.isArray(doc && doc.memories) ? doc.memories : null;
|
|
1017
|
+
if (!memories) {
|
|
1018
|
+
warn(JSON.stringify({
|
|
1019
|
+
error: true,
|
|
1020
|
+
code: 'invalid_format',
|
|
1021
|
+
message: 'File is not a valid Empirical memory export: missing a top-level "memories" array. Re-export with `empirical memory export`.'
|
|
1022
|
+
}));
|
|
1023
|
+
process.exit(1);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
if (dryRun) {
|
|
1027
|
+
log(JSON.stringify({ dryRun: true, wouldImport: memories.length, schemaVersion: doc.schemaVersion || null }));
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
const accessToken = requireAccessToken();
|
|
1032
|
+
const baseUrl = resolveApiBaseUrl();
|
|
1033
|
+
// Every row goes through the normal create endpoint, so imports get the same dedupe and
|
|
1034
|
+
// auto-link treatment as any other write, no special-casing needed here.
|
|
1035
|
+
const recordUrl = new URL('/api/v1/memories', baseUrl).toString();
|
|
1036
|
+
|
|
1037
|
+
let imported = 0;
|
|
1038
|
+
let failed = 0;
|
|
1039
|
+
const errors = [];
|
|
1040
|
+
|
|
1041
|
+
for (const row of memories) {
|
|
1042
|
+
const payload = {
|
|
1043
|
+
category: row.category,
|
|
1044
|
+
summary: row.summary,
|
|
1045
|
+
nodeType: row.nodeType || 'observation',
|
|
1046
|
+
tags: Array.isArray(row.tags) ? row.tags : [],
|
|
1047
|
+
data: row.data !== undefined
|
|
1048
|
+
? row.data
|
|
1049
|
+
: (row.summary !== undefined ? { text: String(row.summary) } : undefined)
|
|
1050
|
+
};
|
|
1051
|
+
if (Array.isArray(row.relationships) && row.relationships.length) payload.relationships = row.relationships;
|
|
1052
|
+
if (row.mass !== undefined && row.mass !== null) payload.mass = row.mass;
|
|
1053
|
+
if (row.gravity !== undefined && row.gravity !== null) payload.gravity = row.gravity;
|
|
1054
|
+
if (row.timeContext) payload.timeContext = row.timeContext;
|
|
1055
|
+
|
|
1056
|
+
let created;
|
|
1057
|
+
try {
|
|
1058
|
+
const response = await httpJson(recordUrl, 'POST', payload, buildApiAuthHeaders(accessToken));
|
|
1059
|
+
created = (response && response.data) ? response.data : response;
|
|
1060
|
+
imported++;
|
|
1061
|
+
} catch (error) {
|
|
1062
|
+
failed++;
|
|
1063
|
+
errors.push({ summary: row.summary, message: error.message });
|
|
1064
|
+
continue;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
const notes = Array.isArray(row.notes) ? row.notes : [];
|
|
1068
|
+
const createdId = created && (created.id || created._id);
|
|
1069
|
+
if (notes.length && createdId) {
|
|
1070
|
+
const noteUrl = new URL(`/api/v1/memories/${encodeURIComponent(createdId)}/notes`, baseUrl).toString();
|
|
1071
|
+
for (const note of notes) {
|
|
1072
|
+
const text = typeof note === 'string' ? note : (note && note.text);
|
|
1073
|
+
if (!text) continue;
|
|
1074
|
+
try {
|
|
1075
|
+
await httpJson(noteUrl, 'POST', { text }, buildApiAuthHeaders(accessToken));
|
|
1076
|
+
} catch (_error) {
|
|
1077
|
+
// Best-effort: a note failing to re-attach shouldn't fail the whole import.
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
log(JSON.stringify({ imported, failed, total: memories.length, errors: errors.slice(0, 20) }));
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
async function runMemoryBulkImport(args) {
|
|
1087
|
+
const filePath = args.find((a) => a && !a.startsWith('--')) || '';
|
|
1088
|
+
const dryRun = parseFlag(args, '--dry-run');
|
|
1089
|
+
|
|
1090
|
+
if (!filePath) {
|
|
1091
|
+
warn('Usage: empirical memory bulk-import <file> [--dry-run]');
|
|
1092
|
+
process.exit(1);
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
let plan;
|
|
1096
|
+
try {
|
|
1097
|
+
plan = JSON.parse(fs.readFileSync(path.resolve(filePath), 'utf8'));
|
|
1098
|
+
} catch (error) {
|
|
1099
|
+
warn(JSON.stringify({ error: true, code: 'json_file_error', message: error.message }));
|
|
1100
|
+
process.exit(1);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
const memories = Array.isArray(plan && plan.memories) ? plan.memories : null;
|
|
1104
|
+
if (!memories) {
|
|
1105
|
+
warn(JSON.stringify({
|
|
1106
|
+
error: true,
|
|
1107
|
+
code: 'invalid_format',
|
|
1108
|
+
message: 'File must have a top-level "memories" array. See `empirical memory bulk-import --help`.'
|
|
1109
|
+
}));
|
|
1110
|
+
process.exit(1);
|
|
1111
|
+
}
|
|
1112
|
+
const relationships = Array.isArray(plan.relationships) ? plan.relationships : [];
|
|
1113
|
+
|
|
1114
|
+
if (dryRun) {
|
|
1115
|
+
log(JSON.stringify({ dryRun: true, wouldImportMemories: memories.length, wouldImportRelationships: relationships.length }));
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
const accessToken = requireAccessToken();
|
|
1120
|
+
const baseUrl = resolveApiBaseUrl();
|
|
1121
|
+
const bulkUrl = new URL('/api/v1/memories/bulk', baseUrl).toString();
|
|
1122
|
+
|
|
1123
|
+
try {
|
|
1124
|
+
const enqueueResponse = await httpJson(bulkUrl, 'POST', { memories, relationships }, buildApiAuthHeaders(accessToken));
|
|
1125
|
+
const { jobId } = (enqueueResponse && enqueueResponse.data) ? enqueueResponse.data : enqueueResponse;
|
|
1126
|
+
const statusUrl = new URL(`/api/v1/memories/bulk/${jobId}`, baseUrl).toString();
|
|
1127
|
+
|
|
1128
|
+
const pollIntervalMs = 3000;
|
|
1129
|
+
const maxPolls = 200;
|
|
1130
|
+
for (let i = 0; i < maxPolls; i += 1) {
|
|
1131
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
1132
|
+
const statusResponse = await httpJson(statusUrl, 'GET', undefined, buildApiAuthHeaders(accessToken));
|
|
1133
|
+
const job = (statusResponse && statusResponse.data) ? statusResponse.data : statusResponse;
|
|
1134
|
+
if (job.status === 'succeeded') { log(JSON.stringify(job)); return; }
|
|
1135
|
+
if (job.status === 'failed') { warn(JSON.stringify(job)); process.exit(1); }
|
|
1136
|
+
}
|
|
1137
|
+
warn(JSON.stringify({ error: true, code: 'timeout', message: `Job ${jobId} did not complete within ${(maxPolls * pollIntervalMs) / 1000}s` }));
|
|
1138
|
+
process.exit(1);
|
|
1139
|
+
} catch (error) {
|
|
1140
|
+
printApiError(error);
|
|
1141
|
+
process.exit(1);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
async function runMemoryStats(args) {
|
|
1146
|
+
const format = parseOption(args, '--format', 'json');
|
|
1147
|
+
|
|
1148
|
+
const accessToken = requireAccessToken();
|
|
1149
|
+
const baseUrl = resolveApiBaseUrl();
|
|
1150
|
+
const url = new URL('/api/v1/memory-stats', baseUrl).toString();
|
|
1151
|
+
|
|
1152
|
+
let response;
|
|
1153
|
+
try {
|
|
1154
|
+
response = await httpJson(url, 'GET', undefined, buildApiAuthHeaders(accessToken));
|
|
1155
|
+
} catch (error) {
|
|
1156
|
+
printApiError(error);
|
|
1157
|
+
process.exit(1);
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
const data = (response && response.data) ? response.data : response;
|
|
1161
|
+
|
|
1162
|
+
if (format === 'table') {
|
|
1163
|
+
log(`Total memories: ${data.total}`);
|
|
1164
|
+
log(`Oldest: ${data.oldest || '(none)'}`);
|
|
1165
|
+
log(`Newest: ${data.newest || '(none)'}`);
|
|
1166
|
+
log('');
|
|
1167
|
+
log('By category:');
|
|
1168
|
+
log(renderTable(data.byCategory || [], ['category', 'count']));
|
|
1169
|
+
log('');
|
|
1170
|
+
log('By tag:');
|
|
1171
|
+
log(renderTable(data.byTag || [], ['tag', 'count']));
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
log(JSON.stringify(data));
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
async function runMemoryCompact(args) {
|
|
1179
|
+
const apply = parseFlag(args, '--apply');
|
|
1180
|
+
const category = parseOption(args, '--category', '');
|
|
1181
|
+
|
|
1182
|
+
const accessToken = requireAccessToken();
|
|
1183
|
+
const baseUrl = resolveApiBaseUrl();
|
|
1184
|
+
const url = new URL('/api/v1/memory-compaction', baseUrl).toString();
|
|
1185
|
+
const body = { apply };
|
|
1186
|
+
if (category) body.category = category;
|
|
1187
|
+
|
|
1188
|
+
let response;
|
|
1189
|
+
try {
|
|
1190
|
+
response = await httpJson(url, 'POST', body, buildApiAuthHeaders(accessToken));
|
|
1191
|
+
} catch (error) {
|
|
1192
|
+
printApiError(error);
|
|
1193
|
+
process.exit(1);
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
const data = (response && response.data) ? response.data : response;
|
|
1197
|
+
log(JSON.stringify(data));
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
async function runMemoryCommand(subArgs) {
|
|
1201
|
+
const helpRequested = subArgs.includes('--help') || subArgs.includes('-h');
|
|
1202
|
+
const sub = subArgs[0] && !subArgs[0].startsWith('-') ? subArgs[0] : undefined;
|
|
1203
|
+
|
|
1204
|
+
if (helpRequested || !sub) {
|
|
1205
|
+
printMemoryHelp(sub);
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
const rest = subArgs.slice(1);
|
|
1210
|
+
|
|
1211
|
+
return withCliAnalyticsContext(
|
|
1212
|
+
{ command: `memory.${sub || 'unknown'}`, subcommand: sub || 'unknown' },
|
|
1213
|
+
async () => withSelectedWorkspaceContext(async () => {
|
|
1214
|
+
if (sub === 'query') return runMemoryQuery(rest);
|
|
1215
|
+
if (sub === 'get') return runMemoryGet(rest);
|
|
1216
|
+
if (sub === 'list') return runMemoryList(rest);
|
|
1217
|
+
if (sub === 'record') return runMemoryRecord(rest);
|
|
1218
|
+
if (sub === 'note') return runMemoryNote(rest);
|
|
1219
|
+
if (sub === 'update') return runMemoryUpdate(rest);
|
|
1220
|
+
if (sub === 'acknowledge' || sub === 'ack' || sub === 'done') return runMemoryAcknowledge(rest);
|
|
1221
|
+
if (sub === 'delete') return runMemoryDelete(rest);
|
|
1222
|
+
// `attachments` and `attach` are shorthands for the two subcommands that get used most.
|
|
1223
|
+
if (sub === 'attachment') return runAttachmentCommand(rest);
|
|
1224
|
+
if (sub === 'attachments') return runAttachmentCommand(['list', ...rest]);
|
|
1225
|
+
if (sub === 'attach') return runAttachmentCommand(['add', ...rest]);
|
|
1226
|
+
if (sub === 'tree') return runMemoryTree(rest);
|
|
1227
|
+
if (sub === 'duplicates') return runMemoryDuplicates(rest);
|
|
1228
|
+
if (sub === 'merge') return runMemoryMerge(rest);
|
|
1229
|
+
if (sub === 'export') return runMemoryExport(rest);
|
|
1230
|
+
if (sub === 'import') return runMemoryImport(rest);
|
|
1231
|
+
if (sub === 'bulk-import') return runMemoryBulkImport(rest);
|
|
1232
|
+
if (sub === 'stats') return runMemoryStats(rest);
|
|
1233
|
+
if (sub === 'compact') return runMemoryCompact(rest);
|
|
1234
|
+
|
|
1235
|
+
warn('Unknown memory subcommand: ' + (sub || '(none)'));
|
|
1236
|
+
warn('Usage: empirical memory <query|record|note|update|delete|attachment|tree|duplicates|merge|export|import|stats|compact> [options]');
|
|
1237
|
+
process.exit(1);
|
|
1238
|
+
})
|
|
1239
|
+
);
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
module.exports = {
|
|
1243
|
+
resolveSummaryInput,
|
|
1244
|
+
detectShellMangling,
|
|
1245
|
+
withCliAnalyticsContext,
|
|
1246
|
+
resolveMemoryByQuery,
|
|
1247
|
+
printMemoryResolveError,
|
|
1248
|
+
resolveWorkspaceIdsForQuery,
|
|
1249
|
+
runMemoryQuery,
|
|
1250
|
+
runMemoryGet,
|
|
1251
|
+
runMemoryList,
|
|
1252
|
+
runMemoryRecord,
|
|
1253
|
+
runMemoryNote,
|
|
1254
|
+
runMemoryUpdate,
|
|
1255
|
+
runMemoryAcknowledge,
|
|
1256
|
+
runMemoryDelete,
|
|
1257
|
+
runMemoryTree,
|
|
1258
|
+
runMemoryDuplicates,
|
|
1259
|
+
runMemoryMerge,
|
|
1260
|
+
runMemoryExport,
|
|
1261
|
+
runMemoryImport,
|
|
1262
|
+
runMemoryBulkImport,
|
|
1263
|
+
runMemoryStats,
|
|
1264
|
+
runMemoryCompact,
|
|
1265
|
+
runMemoryDeleteBulk,
|
|
1266
|
+
runMemoryCommand
|
|
1267
|
+
};
|