mio-agent-runtime 0.5.23 → 0.7.0
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/README.md +524 -85
- package/bin/mio.js +2861 -758
- package/observe/observer.js +73 -15
- package/package.json +45 -45
- package/scripts/check-syntax.cjs +72 -0
- package/server/agent-store.js +140 -0
- package/server/evolution-report.js +311 -0
- package/server/experience-store.js +214 -0
- package/server/insight-store.js +118 -0
- package/server/llm-client.js +161 -0
- package/server/memory-store.js +505 -3
- package/server/mio-intelligence-mcp/index.js +129 -1521
- package/server/observer-store.js +228 -0
- package/server/policy-store.js +263 -0
- package/server/query-log.js +66 -0
- package/server/subscription-store.js +213 -0
- package/server/task-store.js +385 -0
package/bin/mio.js
CHANGED
|
@@ -1,758 +1,2861 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict'
|
|
3
|
-
|
|
4
|
-
const fs = require('fs')
|
|
5
|
-
const path = require('path')
|
|
6
|
-
const os = require('os')
|
|
7
|
-
const { execFileSync } = require('child_process')
|
|
8
|
-
|
|
9
|
-
const SERVER_SCRIPT = path.resolve(__dirname, '..', 'server', 'mio-intelligence-mcp', 'index.js')
|
|
10
|
-
const MIO_HOME = process.env.MIO_HOME || path.join(os.homedir(), '.mio-intelligence')
|
|
11
|
-
const CONFIG_FILE = path.join(MIO_HOME, 'config.json')
|
|
12
|
-
|
|
13
|
-
const adapters = {
|
|
14
|
-
codex: require('../adapters/codex.js'),
|
|
15
|
-
opencode: require('../adapters/opencode.js'),
|
|
16
|
-
workbuddy: require('../adapters/workbuddy.js'),
|
|
17
|
-
hermes: require('../adapters/hermes.js'),
|
|
18
|
-
claude: require('../adapters/claude-code.js'),
|
|
19
|
-
}
|
|
20
|
-
const observer = require('../observe/observer.js')
|
|
21
|
-
const { getEvolutionStatus, formatEvolutionStatusText } = require('../server/runtime-modules.js')
|
|
22
|
-
const { createEvolutionCutoverTools } = require('../server/evolution-cutover.js')
|
|
23
|
-
const { createMemoryStore } = require('../server/memory-store.js')
|
|
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
|
-
function
|
|
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
|
-
if (
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
function
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (
|
|
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
|
-
if (args[1] === '
|
|
208
|
-
return
|
|
209
|
-
evolutionCutover.
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
if (useJson) return jsonOrText(
|
|
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
|
-
if (useJson)
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
console.log(
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
const
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
console.
|
|
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
|
-
if (
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
if (
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
if (
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
console.log(
|
|
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
|
-
mio
|
|
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
|
-
}
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
const fs = require('fs')
|
|
5
|
+
const path = require('path')
|
|
6
|
+
const os = require('os')
|
|
7
|
+
const { execFileSync } = require('child_process')
|
|
8
|
+
|
|
9
|
+
const SERVER_SCRIPT = path.resolve(__dirname, '..', 'server', 'mio-intelligence-mcp', 'index.js')
|
|
10
|
+
const MIO_HOME = process.env.MIO_HOME || path.join(os.homedir(), '.mio-intelligence')
|
|
11
|
+
const CONFIG_FILE = path.join(MIO_HOME, 'config.json')
|
|
12
|
+
|
|
13
|
+
const adapters = {
|
|
14
|
+
codex: require('../adapters/codex.js'),
|
|
15
|
+
opencode: require('../adapters/opencode.js'),
|
|
16
|
+
workbuddy: require('../adapters/workbuddy.js'),
|
|
17
|
+
hermes: require('../adapters/hermes.js'),
|
|
18
|
+
claude: require('../adapters/claude-code.js'),
|
|
19
|
+
}
|
|
20
|
+
const observer = require('../observe/observer.js')
|
|
21
|
+
const { getEvolutionStatus, formatEvolutionStatusText } = require('../server/runtime-modules.js')
|
|
22
|
+
const { createEvolutionCutoverTools } = require('../server/evolution-cutover.js')
|
|
23
|
+
const { createMemoryStore } = require('../server/memory-store.js')
|
|
24
|
+
const { createExperienceStore } = require('../server/experience-store.js')
|
|
25
|
+
const { createPolicyStore } = require('../server/policy-store.js')
|
|
26
|
+
const { CreativityEngine } = require('../server/creativity-engine.js')
|
|
27
|
+
const { createAgentStore } = require('../server/agent-store.js')
|
|
28
|
+
const { createInsightStore } = require('../server/insight-store.js')
|
|
29
|
+
const { createObserverStore } = require('../server/observer-store.js')
|
|
30
|
+
const { loadPhase0, renderPhase0Markdown } = require('../server/mio-intelligence-mcp/phase0.js')
|
|
31
|
+
const { listHostCapabilities } = require('../server/host-capabilities.js')
|
|
32
|
+
const { createTaskStore } = require('../server/task-store.js')
|
|
33
|
+
const {
|
|
34
|
+
chatJson,
|
|
35
|
+
llmConfig,
|
|
36
|
+
isLlmConfigured,
|
|
37
|
+
llmConfigSources,
|
|
38
|
+
resetLlmConfigCache,
|
|
39
|
+
DEFAULT_API_URL,
|
|
40
|
+
DEFAULT_MODEL,
|
|
41
|
+
} = require('../server/llm-client.js')
|
|
42
|
+
const { createQueryLog } = require('../server/query-log.js')
|
|
43
|
+
const { createSubscriptionStore, subscribeKey } = require('../server/subscription-store.js')
|
|
44
|
+
const { createRetention } = require('../server/retention.js')
|
|
45
|
+
const { createDigest } = require('../server/digest.js')
|
|
46
|
+
const { createEvolutionReport, formatEvolutionReportText } = require('../server/evolution-report.js')
|
|
47
|
+
|
|
48
|
+
function appendJsonl(file, value) {
|
|
49
|
+
fs.mkdirSync(path.dirname(file), { recursive: true })
|
|
50
|
+
fs.appendFileSync(file, `${JSON.stringify(value)}\n`, 'utf8')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function readJsonl(file) {
|
|
54
|
+
if (!fs.existsSync(file)) return []
|
|
55
|
+
return fs
|
|
56
|
+
.readFileSync(file, 'utf8')
|
|
57
|
+
.split(/\r?\n/)
|
|
58
|
+
.filter(Boolean)
|
|
59
|
+
.map((line) => {
|
|
60
|
+
try {
|
|
61
|
+
return JSON.parse(line)
|
|
62
|
+
} catch (_) {
|
|
63
|
+
return null
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
.filter(Boolean)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function projectName() {
|
|
70
|
+
try {
|
|
71
|
+
const commonDir = execFileSync(
|
|
72
|
+
'git',
|
|
73
|
+
['rev-parse', '--path-format=absolute', '--git-common-dir'],
|
|
74
|
+
{
|
|
75
|
+
cwd: process.cwd(),
|
|
76
|
+
encoding: 'utf8',
|
|
77
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
78
|
+
}
|
|
79
|
+
).trim()
|
|
80
|
+
if (commonDir) return path.basename(path.dirname(commonDir))
|
|
81
|
+
} catch (_) {}
|
|
82
|
+
return path.basename(process.cwd())
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const evolutionCutover = createEvolutionCutoverTools({
|
|
86
|
+
dataDir: MIO_HOME,
|
|
87
|
+
appendJsonl,
|
|
88
|
+
readJsonl,
|
|
89
|
+
projectName,
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
function optionValue(args, name) {
|
|
93
|
+
const index = args.indexOf(name)
|
|
94
|
+
if (index === -1) return undefined
|
|
95
|
+
return args[index + 1]
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function flagPresent(args, name) {
|
|
99
|
+
return args.includes(name)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function parseJsonOption(args, name) {
|
|
103
|
+
const raw = optionValue(args, name)
|
|
104
|
+
if (raw === undefined) throw new Error(`${name} is required`)
|
|
105
|
+
try {
|
|
106
|
+
return JSON.parse(raw)
|
|
107
|
+
} catch (error) {
|
|
108
|
+
throw new Error(`${name} must be valid JSON`)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function parseNumberOption(args, name) {
|
|
113
|
+
const raw = optionValue(args, name)
|
|
114
|
+
if (raw === undefined) return undefined
|
|
115
|
+
const value = Number(raw)
|
|
116
|
+
if (!Number.isFinite(value)) throw new Error(`${name} must be a number`)
|
|
117
|
+
return value
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// mio evolution report shares one implementation with mio.evolution.report
|
|
121
|
+
// (see ../server/evolution-report.js), so CLI and MCP cannot report different numbers.
|
|
122
|
+
function cliEvolutionReport() {
|
|
123
|
+
return createEvolutionReport({ dataDir: MIO_HOME, projectName })
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const EVOLUTION_REPORT_PERIODS = new Set(['24h', '7d', '30d', 'all'])
|
|
127
|
+
|
|
128
|
+
function evolutionReportCommand(args, useJson) {
|
|
129
|
+
const flags = args.slice(2)
|
|
130
|
+
const period = optionValue(flags, '--period')
|
|
131
|
+
if (period !== undefined && !EVOLUTION_REPORT_PERIODS.has(period)) {
|
|
132
|
+
console.error(`--period must be one of: ${[...EVOLUTION_REPORT_PERIODS].join(', ')}`)
|
|
133
|
+
process.exitCode = 1
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let result
|
|
138
|
+
try {
|
|
139
|
+
result = cliEvolutionReport().report({
|
|
140
|
+
project: optionValue(flags, '--project'),
|
|
141
|
+
period,
|
|
142
|
+
})
|
|
143
|
+
} catch (error) {
|
|
144
|
+
console.error(error.message || error)
|
|
145
|
+
process.exitCode = 1
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (useJson) return jsonOrText(result, true)
|
|
150
|
+
console.log(formatEvolutionReportText(result))
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function evolutionStatus(useJson) {
|
|
154
|
+
const payload = getEvolutionStatus()
|
|
155
|
+
if (useJson) {
|
|
156
|
+
jsonOrText(payload, true)
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
console.log(formatEvolutionStatusText(payload))
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function printCutoverReadiness(readiness, useJson) {
|
|
163
|
+
if (useJson) return jsonOrText(readiness, true)
|
|
164
|
+
console.log(`Cutover readiness for ${readiness.project}: ${readiness.status}`)
|
|
165
|
+
console.log(`shadow=${readiness.shadowRuns} dual-write=${readiness.dualWriteRuns}`)
|
|
166
|
+
if (readiness.reasons.length > 0) console.log(`reasons: ${readiness.reasons.join('; ')}`)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function printAuthorityPlan(plan, useJson) {
|
|
170
|
+
if (useJson) return jsonOrText(plan, true)
|
|
171
|
+
console.log(`Authority switch ${plan.from} -> ${plan.to}: ${plan.approved ? 'ready' : 'blocked'}`)
|
|
172
|
+
for (const action of plan.actions) console.log(`- ${action.type}`)
|
|
173
|
+
for (const blocker of plan.blockers) console.log(`blocked: ${blocker}`)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function printMigrationPlan(plan, useJson) {
|
|
177
|
+
if (useJson) return jsonOrText(plan, true)
|
|
178
|
+
console.log(`Migration plan: ${plan.ready ? 'ready' : 'conflicts'}`)
|
|
179
|
+
console.log(
|
|
180
|
+
`legacy=${plan.summary.legacy} modular=${plan.summary.modular} toCreate=${plan.summary.toCreate} conflicts=${plan.summary.conflicts}`
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function printCutoverDryRun(result, useJson) {
|
|
185
|
+
if (useJson) return jsonOrText(result, true)
|
|
186
|
+
console.log(`Cutover apply dry-run: ${result.status}`)
|
|
187
|
+
console.log(`applied=${result.applied}`)
|
|
188
|
+
for (const action of result.actions) console.log(`- ${action.type}`)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function printShadowRecord(record, useJson) {
|
|
192
|
+
if (useJson) return jsonOrText(record, true)
|
|
193
|
+
console.log(`Shadow comparison ${record.id}: ${record.matched ? 'matched' : 'mismatch'}`)
|
|
194
|
+
if (record.diffs.length > 0) console.log(`diffs=${record.diffs.length}`)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function printDualWriteRecord(record, useJson) {
|
|
198
|
+
if (useJson) return jsonOrText(record, true)
|
|
199
|
+
console.log(`Dual-write sample ${record.id}: authoritative=${record.authoritative}`)
|
|
200
|
+
console.log(`shadow=${record.shadow.matched ? 'matched' : 'mismatch'}`)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function evolutionCommand(args, useJson) {
|
|
204
|
+
if (args[1] === 'status') return evolutionStatus(useJson)
|
|
205
|
+
if (args[1] === 'report') return evolutionReportCommand(args, useJson)
|
|
206
|
+
try {
|
|
207
|
+
if (args[1] === 'shadow' && args[2] === 'record') {
|
|
208
|
+
return printShadowRecord(
|
|
209
|
+
await evolutionCutover.recordShadowComparison({
|
|
210
|
+
project: optionValue(args, '--project'),
|
|
211
|
+
label: optionValue(args, '--label'),
|
|
212
|
+
legacy: parseJsonOption(args, '--legacy'),
|
|
213
|
+
modular: parseJsonOption(args, '--modular'),
|
|
214
|
+
input: optionValue(args, '--input') === undefined ? undefined : parseJsonOption(args, '--input'),
|
|
215
|
+
}),
|
|
216
|
+
useJson,
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
if (args[1] === 'dual-write' && args[2] === 'record') {
|
|
220
|
+
return printDualWriteRecord(
|
|
221
|
+
await evolutionCutover.recordDualWrite({
|
|
222
|
+
project: optionValue(args, '--project'),
|
|
223
|
+
label: optionValue(args, '--label'),
|
|
224
|
+
authoritative: optionValue(args, '--authoritative'),
|
|
225
|
+
legacyResult: parseJsonOption(args, '--legacy-result'),
|
|
226
|
+
modularResult: parseJsonOption(args, '--modular-result'),
|
|
227
|
+
record: optionValue(args, '--record') === undefined ? undefined : parseJsonOption(args, '--record'),
|
|
228
|
+
}),
|
|
229
|
+
useJson,
|
|
230
|
+
)
|
|
231
|
+
}
|
|
232
|
+
if (args[1] === 'cutover' && args[2] === 'readiness') {
|
|
233
|
+
return printCutoverReadiness(
|
|
234
|
+
evolutionCutover.cutoverReadiness({
|
|
235
|
+
project: optionValue(args, '--project'),
|
|
236
|
+
minShadowRuns: parseNumberOption(args, '--min-shadow-runs'),
|
|
237
|
+
maxMismatchRate: parseNumberOption(args, '--max-mismatch-rate'),
|
|
238
|
+
}),
|
|
239
|
+
useJson,
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
if (args[1] === 'cutover' && args[2] === 'apply') {
|
|
243
|
+
return printCutoverDryRun(
|
|
244
|
+
evolutionCutover.applyCutover({
|
|
245
|
+
dryRun: flagPresent(args, '--dry-run'),
|
|
246
|
+
project: optionValue(args, '--project'),
|
|
247
|
+
plan: parseJsonOption(args, '--plan'),
|
|
248
|
+
}),
|
|
249
|
+
useJson,
|
|
250
|
+
)
|
|
251
|
+
}
|
|
252
|
+
if (args[1] === 'authority' && args[2] === 'plan') {
|
|
253
|
+
return printAuthorityPlan(
|
|
254
|
+
evolutionCutover.authorityPlan({
|
|
255
|
+
readiness: parseJsonOption(args, '--readiness'),
|
|
256
|
+
from: optionValue(args, '--from'),
|
|
257
|
+
to: optionValue(args, '--to'),
|
|
258
|
+
}),
|
|
259
|
+
useJson,
|
|
260
|
+
)
|
|
261
|
+
}
|
|
262
|
+
if (args[1] === 'migration' && args[2] === 'plan') {
|
|
263
|
+
return printMigrationPlan(
|
|
264
|
+
evolutionCutover.migrationPlan({
|
|
265
|
+
legacyRecords: parseJsonOption(args, '--legacy-records'),
|
|
266
|
+
modularRecords: parseJsonOption(args, '--modular-records'),
|
|
267
|
+
}),
|
|
268
|
+
useJson,
|
|
269
|
+
)
|
|
270
|
+
}
|
|
271
|
+
} catch (error) {
|
|
272
|
+
console.error(error.message || error)
|
|
273
|
+
process.exitCode = 1
|
|
274
|
+
return
|
|
275
|
+
}
|
|
276
|
+
console.error('Usage: mio evolution status|report|shadow record|dual-write record|cutover readiness|cutover apply|authority plan|migration plan')
|
|
277
|
+
process.exitCode = 1
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function ensureHome() {
|
|
281
|
+
fs.mkdirSync(MIO_HOME, { recursive: true })
|
|
282
|
+
fs.mkdirSync(path.join(MIO_HOME, 'logs'), { recursive: true })
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// CLI memory store: same ranking/persistence as the MCP server, pointed at
|
|
286
|
+
// MIO_HOME. No recordQuery hook — a terminal command cannot be followed by a
|
|
287
|
+
// task_outcome, so a recorded query would only be phase-0 noise.
|
|
288
|
+
function cliMemoryStore() {
|
|
289
|
+
return createMemoryStore({
|
|
290
|
+
dataDir: MIO_HOME,
|
|
291
|
+
projectName,
|
|
292
|
+
agentId: () => 'cli',
|
|
293
|
+
})
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// policy.check deliberately reads the *global* MIO_HOME rather than a
|
|
297
|
+
// per-project directory, so its history matches `mio recall` and `mio traces`.
|
|
298
|
+
// It shares the memory store's tokenizer and scoring, so risk evidence and
|
|
299
|
+
// related-memory ranking line up with `mio.memory.query`.
|
|
300
|
+
function cliPolicyStore() {
|
|
301
|
+
const memoryStore = cliMemoryStore()
|
|
302
|
+
return createPolicyStore({
|
|
303
|
+
dataDir: MIO_HOME,
|
|
304
|
+
projectName,
|
|
305
|
+
memoryStore,
|
|
306
|
+
})
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// The creativity engine is the single implementation behind mio.creativity.*;
|
|
310
|
+
// the CLI points it at the global MIO_HOME so a terminal `mio creativity list`
|
|
311
|
+
// sees the same hypotheses the MCP server would. generate/ferment need an LLM,
|
|
312
|
+
// so they use the shared client -- configured with LLM_API_URL / LLM_KEY /
|
|
313
|
+
// LLM_CHAT_MODEL, the same environment the MCP server reads. A local Ollama
|
|
314
|
+
// works: LLM_API_URL=http://localhost:11434/v1/chat/completions
|
|
315
|
+
function cliCreativityEngine() {
|
|
316
|
+
const creativityDir = path.join(MIO_HOME, 'creativity')
|
|
317
|
+
return new CreativityEngine(creativityDir, chatJson)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Observed-agent telemetry (agents.jsonl + traces/memory/reuse cross-reference)
|
|
321
|
+
// shares one store with the MCP server, so CLI and MCP report identical agents.
|
|
322
|
+
function cliAgentStore() {
|
|
323
|
+
return createAgentStore({ dataDir: MIO_HOME, projectName })
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Task routing shares one store with mio.task.route. persistQuery stays false:
|
|
327
|
+
// the MCP call feeds the query log for auto-claim, but a terminal inspection
|
|
328
|
+
// must not write to queries.jsonl every time it runs.
|
|
329
|
+
function cliTaskStore() {
|
|
330
|
+
// Shares one query-log instance with nothing else here, but created from the
|
|
331
|
+
// same module the MCP server uses, so queries.jsonl has a single reader/writer
|
|
332
|
+
// implementation. cliMemoryStore deliberately does NOT get one: a terminal
|
|
333
|
+
// recall is never followed by a task_outcome, so recording those queries
|
|
334
|
+
// would only add noise.
|
|
335
|
+
return createTaskStore({
|
|
336
|
+
dataDir: MIO_HOME,
|
|
337
|
+
projectName,
|
|
338
|
+
memoryStore: cliMemoryStore(),
|
|
339
|
+
queryLog: createQueryLog({ dataDir: MIO_HOME }),
|
|
340
|
+
agentId: () => 'cli',
|
|
341
|
+
})
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function splitTagsOption(args, name) {
|
|
345
|
+
const raw = optionValue(args, name)
|
|
346
|
+
if (raw === undefined) return []
|
|
347
|
+
return raw
|
|
348
|
+
.split(',')
|
|
349
|
+
.map((tag) => tag.trim())
|
|
350
|
+
.filter(Boolean)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function recallCommand(args, useJson) {
|
|
354
|
+
const query = args[1]
|
|
355
|
+
if (!query) {
|
|
356
|
+
console.error('Usage: mio recall "<query>" [--project name] [--scope project|global|all] [--kind kind] [--tags a,b] [--limit n] [--json]')
|
|
357
|
+
process.exitCode = 1
|
|
358
|
+
return
|
|
359
|
+
}
|
|
360
|
+
const flags = args.slice(2)
|
|
361
|
+
const store = cliMemoryStore()
|
|
362
|
+
let result
|
|
363
|
+
try {
|
|
364
|
+
result = store.queryMemory({
|
|
365
|
+
query,
|
|
366
|
+
project: optionValue(flags, '--project'),
|
|
367
|
+
scope: optionValue(flags, '--scope'),
|
|
368
|
+
kind: optionValue(flags, '--kind'),
|
|
369
|
+
tags: splitTagsOption(flags, '--tags'),
|
|
370
|
+
limit: parseNumberOption(flags, '--limit'),
|
|
371
|
+
})
|
|
372
|
+
} catch (error) {
|
|
373
|
+
console.error(error.message || error)
|
|
374
|
+
process.exitCode = 1
|
|
375
|
+
return
|
|
376
|
+
}
|
|
377
|
+
if (useJson) return jsonOrText(result, true)
|
|
378
|
+
console.log(
|
|
379
|
+
`Mio memory: ${result.count} result(s) for "${result.query}" (project=${result.project}, scope=${result.scope})`
|
|
380
|
+
)
|
|
381
|
+
result.results.forEach((record, index) => {
|
|
382
|
+
const date = (record.timestamp || '').slice(0, 10)
|
|
383
|
+
console.log(`${index + 1}. [${record.kind || 'note'}] ${record.content}`)
|
|
384
|
+
const meta = [
|
|
385
|
+
(record.tags || []).length > 0 ? `tags: ${record.tags.join(', ')}` : null,
|
|
386
|
+
record.source ? `source: ${record.source}` : null,
|
|
387
|
+
date,
|
|
388
|
+
record.id,
|
|
389
|
+
]
|
|
390
|
+
.filter(Boolean)
|
|
391
|
+
.join(' | ')
|
|
392
|
+
console.log(` ${meta}`)
|
|
393
|
+
})
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function tracesCommand(args, useJson) {
|
|
397
|
+
const flags = args.slice(1)
|
|
398
|
+
const store = cliMemoryStore()
|
|
399
|
+
let result
|
|
400
|
+
try {
|
|
401
|
+
result = store.queryTraces({
|
|
402
|
+
event_type: optionValue(flags, '--type'),
|
|
403
|
+
outcome: optionValue(flags, '--outcome'),
|
|
404
|
+
project: optionValue(flags, '--project'),
|
|
405
|
+
agent: optionValue(flags, '--agent'),
|
|
406
|
+
host: optionValue(flags, '--host'),
|
|
407
|
+
since: optionValue(flags, '--since'),
|
|
408
|
+
until: optionValue(flags, '--until'),
|
|
409
|
+
limit: parseNumberOption(flags, '--limit'),
|
|
410
|
+
include_payload: !flagPresent(flags, '--compact'),
|
|
411
|
+
})
|
|
412
|
+
} catch (error) {
|
|
413
|
+
console.error(error.message || error)
|
|
414
|
+
process.exitCode = 1
|
|
415
|
+
return
|
|
416
|
+
}
|
|
417
|
+
if (useJson) return jsonOrText(result, true)
|
|
418
|
+
const filters = Object.entries(result.filters)
|
|
419
|
+
.filter(([, value]) => value !== null && value !== undefined)
|
|
420
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
421
|
+
.join(', ')
|
|
422
|
+
console.log(
|
|
423
|
+
`Mio traces: ${result.matched} matched / ${result.total} total${filters ? ' (' + filters + ')' : ''}`
|
|
424
|
+
)
|
|
425
|
+
const summary = Object.entries(result.summary.byOutcome)
|
|
426
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
427
|
+
.join(' ')
|
|
428
|
+
if (summary) console.log(`outcomes: ${summary}`)
|
|
429
|
+
result.results.forEach((trace, index) => {
|
|
430
|
+
const time = (trace.timestamp || '').replace('T', ' ').slice(0, 19)
|
|
431
|
+
const brief = trace.payload && trace.payload.summary ? ' | ' + String(trace.payload.summary).slice(0, 100) : ''
|
|
432
|
+
console.log(
|
|
433
|
+
`${index + 1}. ${time} [${trace.event_type || '?'}${trace.outcome ? '/' + trace.outcome : ''}] ${trace.agent || '?'} @ ${trace.project || '?'}${brief}`
|
|
434
|
+
)
|
|
435
|
+
console.log(` ${trace.id} trace_id=${trace.trace_id || '-'}`)
|
|
436
|
+
})
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function digestCommand(args, useJson) {
|
|
440
|
+
const flags = args.slice(1)
|
|
441
|
+
const days = parseNumberOption(flags, '--days') || 7
|
|
442
|
+
const writeBack = flagPresent(flags, '--write-back')
|
|
443
|
+
const outDir = path.join(MIO_HOME, 'digest')
|
|
444
|
+
|
|
445
|
+
const report = createDigest({ home: MIO_HOME }).generate({
|
|
446
|
+
days,
|
|
447
|
+
project: optionValue(flags, '--project'),
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
if (writeBack) {
|
|
451
|
+
// Feed the actionable lines back into each active workspace context file
|
|
452
|
+
// (AGENTS.md / CLAUDE.md MIO_CONTEXT block), so every agent passively
|
|
453
|
+
// receives the digest's value on its next session.
|
|
454
|
+
//
|
|
455
|
+
// Targets are resolved per project, never broadcast. An earlier version
|
|
456
|
+
// wrote one global headline into Object.keys(state.contexts) -- every
|
|
457
|
+
// workspace ever observed -- so unrelated repos (ComfyUI, douyin_store)
|
|
458
|
+
// received the same line and their real history was pushed out of the
|
|
459
|
+
// six-slot block. Write only where the digest actually has data for that
|
|
460
|
+
// project, or where the caller explicitly asked with --cwd/--project.
|
|
461
|
+
const stateFile = path.join(MIO_HOME, 'observe-state.json')
|
|
462
|
+
let state = {}
|
|
463
|
+
try {
|
|
464
|
+
state = JSON.parse(fs.readFileSync(stateFile, 'utf8'))
|
|
465
|
+
} catch (_) {}
|
|
466
|
+
state.contexts = state.contexts || {}
|
|
467
|
+
const stamp = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
468
|
+
|
|
469
|
+
// Project name -> per-project digest line.
|
|
470
|
+
const projectLine = new Map()
|
|
471
|
+
for (const entry of report.projects || []) {
|
|
472
|
+
if (!entry || !entry.project) continue
|
|
473
|
+
const rate = entry.tasks > 0 ? Math.round(((entry.success || 0) / entry.tasks) * 100) : 0
|
|
474
|
+
projectLine.set(String(entry.project).toLowerCase(), `digest(${days}d): ${entry.project} ${entry.tasks} 任务 ${rate}% 成功`)
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const explicitCwd = optionValue(flags, '--cwd')
|
|
478
|
+
const explicitProject = optionValue(flags, '--project')
|
|
479
|
+
let targetCwds = []
|
|
480
|
+
if (explicitCwd) {
|
|
481
|
+
targetCwds = [explicitCwd]
|
|
482
|
+
} else if (explicitProject) {
|
|
483
|
+
const wanted = String(explicitProject).toLowerCase()
|
|
484
|
+
targetCwds = Object.keys(state.contexts).filter(
|
|
485
|
+
(cwd) => path.basename(cwd).toLowerCase() === wanted,
|
|
486
|
+
)
|
|
487
|
+
} else {
|
|
488
|
+
// No explicit target: only write to workspaces this digest has data for.
|
|
489
|
+
// A workspace with no matching project gets nothing.
|
|
490
|
+
targetCwds = Object.keys(state.contexts).filter((cwd) =>
|
|
491
|
+
projectLine.has(path.basename(cwd).toLowerCase()),
|
|
492
|
+
)
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
let written = 0
|
|
496
|
+
for (const cwd of targetCwds) {
|
|
497
|
+
const line = projectLine.get(path.basename(cwd).toLowerCase())
|
|
498
|
+
if (!line) continue
|
|
499
|
+
try {
|
|
500
|
+
observer.updateProjectContext(state, cwd, stamp + ' | ' + line, 'CLAUDE.md')
|
|
501
|
+
observer.updateProjectContext(state, cwd, stamp + ' | ' + line, 'AGENTS.md')
|
|
502
|
+
written += 1
|
|
503
|
+
} catch (_) {}
|
|
504
|
+
}
|
|
505
|
+
fs.mkdirSync(MIO_HOME, { recursive: true })
|
|
506
|
+
fs.writeFileSync(stateFile, JSON.stringify(state) + '\n', 'utf8')
|
|
507
|
+
report.writeBack = {
|
|
508
|
+
workspaces: written,
|
|
509
|
+
skipped: Object.keys(state.contexts).length - written,
|
|
510
|
+
projects: [...projectLine.values()],
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Persist the report so it becomes part of the data asset itself.
|
|
515
|
+
fs.mkdirSync(outDir, { recursive: true })
|
|
516
|
+
const reportFile = path.join(outDir, 'digest-' + report.generatedAt.slice(0, 10) + '.md')
|
|
517
|
+
fs.writeFileSync(reportFile, report.markdown, 'utf8')
|
|
518
|
+
report.reportFile = reportFile
|
|
519
|
+
|
|
520
|
+
if (useJson) return jsonOrText(report, true)
|
|
521
|
+
console.log(report.markdown)
|
|
522
|
+
console.log(`Report saved: ${reportFile}`)
|
|
523
|
+
if (report.writeBack) {
|
|
524
|
+
console.log(
|
|
525
|
+
`Write-back: ${report.writeBack.workspaces} workspace(s), ${report.writeBack.skipped} skipped (no digest data)`,
|
|
526
|
+
)
|
|
527
|
+
for (const line of report.writeBack.projects) console.log(` - ${line}`)
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function pruneCommand(args, useJson) {
|
|
532
|
+
const flags = args.slice(1)
|
|
533
|
+
const days = parseNumberOption(flags, '--days') || 30
|
|
534
|
+
const includeMemory = flagPresent(flags, '--memory')
|
|
535
|
+
const dryRun = flagPresent(flags, '--dry-run')
|
|
536
|
+
if (includeMemory && !dryRun && !flagPresent(flags, '--yes')) {
|
|
537
|
+
console.error('Pruning memory.jsonl is destructive. Re-run with --yes to confirm (a .bak-prune-* backup is still written).')
|
|
538
|
+
process.exitCode = 1
|
|
539
|
+
return
|
|
540
|
+
}
|
|
541
|
+
const retention = createRetention({ home: MIO_HOME })
|
|
542
|
+
let result
|
|
543
|
+
try {
|
|
544
|
+
result = retention.prune({ days, includeMemory, dryRun })
|
|
545
|
+
} catch (error) {
|
|
546
|
+
console.error(error.message || error)
|
|
547
|
+
process.exitCode = 1
|
|
548
|
+
return
|
|
549
|
+
}
|
|
550
|
+
if (useJson) return jsonOrText(result, true)
|
|
551
|
+
console.log(
|
|
552
|
+
`Prune ${dryRun ? 'plan (dry-run)' : 'result'}: cutoff ${result.cutoff}, days=${days}${includeMemory ? ', memory included' : ''}`
|
|
553
|
+
)
|
|
554
|
+
for (const file of result.files) {
|
|
555
|
+
console.log(`- ${file.file}: ${file.total} total -> keep ${file.kept}, remove ${file.removed}`)
|
|
556
|
+
}
|
|
557
|
+
console.log(`Total removed: ${result.totalRemoved}`)
|
|
558
|
+
if (dryRun) console.log('Dry run - nothing written. Re-run without --dry-run to apply.')
|
|
559
|
+
else console.log('Backups written as <file>.bak-prune-<ts> before rewriting.')
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function rememberCommand(args, useJson) {
|
|
563
|
+
const content = args[1]
|
|
564
|
+
if (!content) {
|
|
565
|
+
console.error('Usage: mio remember "<content>" [--kind decision|context|problem|note] [--tags a,b] [--scope project|global] [--project name] [--json]')
|
|
566
|
+
process.exitCode = 1
|
|
567
|
+
return
|
|
568
|
+
}
|
|
569
|
+
const flags = args.slice(2)
|
|
570
|
+
const store = cliMemoryStore()
|
|
571
|
+
let record
|
|
572
|
+
try {
|
|
573
|
+
record = store.recordMemory({
|
|
574
|
+
content,
|
|
575
|
+
kind: optionValue(flags, '--kind'),
|
|
576
|
+
tags: splitTagsOption(flags, '--tags'),
|
|
577
|
+
scope: optionValue(flags, '--scope'),
|
|
578
|
+
project: optionValue(flags, '--project'),
|
|
579
|
+
source: 'cli',
|
|
580
|
+
})
|
|
581
|
+
} catch (error) {
|
|
582
|
+
console.error(error.message || error)
|
|
583
|
+
process.exitCode = 1
|
|
584
|
+
return
|
|
585
|
+
}
|
|
586
|
+
if (useJson) return jsonOrText(record, true)
|
|
587
|
+
console.log(`Recorded ${record.id} (kind=${record.kind}, project=${record.project || 'global'}, scope=${record.scope})`)
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Every flag that consumes the next token. A positional directly after one of
|
|
591
|
+
// these is that flag's value, not an id.
|
|
592
|
+
const VALUE_FLAGS = new Set([
|
|
593
|
+
'--ids', '--reason', '--project', '--limit', '--scope', '--keep',
|
|
594
|
+
'--by', '--notes', '--source-agent', '--target-agent', '--experience-id', '--status',
|
|
595
|
+
])
|
|
596
|
+
|
|
597
|
+
// Accept ids both as `--ids a,b,c` and as bare positionals
|
|
598
|
+
// (`mio memory archive mem_a mem_b`).
|
|
599
|
+
function collectIds(flags) {
|
|
600
|
+
const ids = []
|
|
601
|
+
for (let i = 0; i < flags.length; i += 1) {
|
|
602
|
+
const token = flags[i]
|
|
603
|
+
if (token.startsWith('--')) continue
|
|
604
|
+
const previous = flags[i - 1]
|
|
605
|
+
if (previous && VALUE_FLAGS.has(previous)) continue
|
|
606
|
+
// Ids never contain commas, so a comma-separated positional is unambiguous.
|
|
607
|
+
for (const id of token.split(',')) {
|
|
608
|
+
const trimmed = id.trim()
|
|
609
|
+
if (trimmed) ids.push(trimmed)
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
const raw = optionValue(flags, '--ids')
|
|
613
|
+
if (raw !== undefined) {
|
|
614
|
+
for (const id of raw.split(',')) {
|
|
615
|
+
const trimmed = id.trim()
|
|
616
|
+
if (trimmed) ids.push(trimmed)
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
return [...new Set(ids)]
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function printMemoryAnalyze(result) {
|
|
623
|
+
const project = result.project || 'all'
|
|
624
|
+
console.log(
|
|
625
|
+
`Memory analyze: ${result.total} active, ${result.archived} archived (project=${project})`,
|
|
626
|
+
)
|
|
627
|
+
console.log(`layers: project=${result.layers.project} global=${result.layers.global}`)
|
|
628
|
+
const kinds = Object.entries(result.byKind)
|
|
629
|
+
if (kinds.length > 0) {
|
|
630
|
+
console.log(`by kind: ${kinds.map(([kind, count]) => `${kind}=${count}`).join(', ')}`)
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if (result.duplicates.length > 0) {
|
|
634
|
+
console.log(`\nDuplicate groups: ${result.duplicates.length}`)
|
|
635
|
+
for (const group of result.duplicates) {
|
|
636
|
+
console.log(`- ${group.size} record(s): ${group.records.map((record) => record.id).join(', ')}`)
|
|
637
|
+
const preview = String(group.records[0].content || '')
|
|
638
|
+
if (preview) {
|
|
639
|
+
console.log(` ${preview.length > 80 ? `${preview.slice(0, 80)}...` : preview}`)
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
if (result.lowQuality.length > 0) {
|
|
645
|
+
console.log(`\nLow quality: ${result.issues.lowQuality} record(s)`)
|
|
646
|
+
for (const record of result.lowQuality) {
|
|
647
|
+
console.log(`- ${record.id} [${record.kind || 'note'}] ${record.issues.join(', ')}`)
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (result.issues.duplicatesSkipped) {
|
|
652
|
+
console.log('\nDuplicate scan skipped: too many active records to compare pairwise.')
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
if (result.suggestions.length > 0) {
|
|
656
|
+
console.log('')
|
|
657
|
+
for (const suggestion of result.suggestions) console.log(`suggestion: ${suggestion}`)
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// Merging divergent content is refused by default; when that happens the
|
|
662
|
+
// caller needs to see *what* differs before deciding, so print the candidates.
|
|
663
|
+
function printMemoryMerge(result) {
|
|
664
|
+
if (!result.merged) {
|
|
665
|
+
console.log(`No merge applied (project=${result.project || 'current'}): ${result.reason}`)
|
|
666
|
+
if (result.divergent.length > 0) {
|
|
667
|
+
console.log('\nDivergent content — inspect before choosing a survivor:')
|
|
668
|
+
for (const record of result.divergent) {
|
|
669
|
+
console.log(`- ${record.id} (${record.contentLength} chars, ${record.timestamp || 'no timestamp'})`)
|
|
670
|
+
console.log(` ${record.content.split('\n')[0].slice(0, 110)}`)
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
if (result.notFound.length > 0) console.log(`\nnot found: ${result.notFound.join(', ')}`)
|
|
674
|
+
if (result.outOfScope && result.outOfScope.length > 0) {
|
|
675
|
+
console.log(`\nbelongs to another project: ${result.outOfScope.join(', ')}`)
|
|
676
|
+
}
|
|
677
|
+
if (result.hint) console.log(`\n${result.hint}`)
|
|
678
|
+
process.exitCode = 1
|
|
679
|
+
return
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// A merge that archived nothing is a no-op: every requested id was already
|
|
683
|
+
// archived (i.e. these were merged before). Report it as failure the same way
|
|
684
|
+
// archive does, so scripts do not read success out of an idempotent re-run.
|
|
685
|
+
if (result.archivedCount === 0) {
|
|
686
|
+
console.log(`No merge applied (project=${result.project || 'current'}): nothing left to merge`)
|
|
687
|
+
if (result.skipped.length > 0) {
|
|
688
|
+
console.log(`already archived (previously merged): ${result.skipped.join(', ')}`)
|
|
689
|
+
console.log(`superseded by: ${result.survivor.id}`)
|
|
690
|
+
}
|
|
691
|
+
if (result.notFound.length > 0) console.log(`not found: ${result.notFound.join(', ')}`)
|
|
692
|
+
process.exitCode = 1
|
|
693
|
+
return
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
console.log(
|
|
697
|
+
`Merged ${result.archivedCount} record(s) into ${result.survivor.id} (project=${result.project || 'current'})`,
|
|
698
|
+
)
|
|
699
|
+
console.log(`survivor: ${result.survivor.contentLength} chars, ${result.survivor.timestamp || 'no timestamp'}`)
|
|
700
|
+
if (result.divergentContent) console.log('note: content differed across members; the survivor body was chosen explicitly')
|
|
701
|
+
console.log(`supersedes: ${result.survivor.supersedes.join(', ')}`)
|
|
702
|
+
if (result.skipped.length > 0) console.log(`already archived: ${result.skipped.join(', ')}`)
|
|
703
|
+
if (result.notFound.length > 0) console.log(`not found: ${result.notFound.join(', ')}`)
|
|
704
|
+
console.log(`Undo with: mio memory restore --ids ${result.archived.join(',')}`)
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function memoryUsage() {
|
|
708
|
+
console.log(`Usage:
|
|
709
|
+
mio memory analyze Report duplicates, low-quality records and kind histogram
|
|
710
|
+
mio memory archive --ids a,b Archive records (soft delete; hidden from recall/analyze)
|
|
711
|
+
mio memory restore --ids a,b Un-archive previously archived records
|
|
712
|
+
mio memory forget --ids a,b PERMANENTLY delete records (--yes required; writes an audit entry)
|
|
713
|
+
mio memory merge --ids a,b Merge duplicates: one survives, the rest are archived
|
|
714
|
+
mio memory migrate --ids a,b --scope global Move records between project and global layers
|
|
715
|
+
|
|
716
|
+
Options:
|
|
717
|
+
--ids a,b,c Memory record ids (also accepted as bare positionals)
|
|
718
|
+
--keep id Merge: which record's content survives (default: newest)
|
|
719
|
+
--allow-divergent Merge: permit merging records whose content differs (requires --keep)
|
|
720
|
+
--scope global|project Target layer for migrate
|
|
721
|
+
--project name Project filter (defaults to current directory name)
|
|
722
|
+
--reason text Optional note stored on the record
|
|
723
|
+
--limit n Max duplicate groups / low-quality rows to show (analyze, 1-20)
|
|
724
|
+
--yes Apply archive/merge (without it they only preview)
|
|
725
|
+
--json Machine-readable output
|
|
726
|
+
`)
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function memoryCommand(args, useJson) {
|
|
730
|
+
const sub = args[1]
|
|
731
|
+
const flags = args.slice(2)
|
|
732
|
+
|
|
733
|
+
if (!sub || sub === 'help' || sub === '--help' || sub === '-h') {
|
|
734
|
+
memoryUsage()
|
|
735
|
+
if (!sub) process.exitCode = 1
|
|
736
|
+
return
|
|
737
|
+
}
|
|
738
|
+
if (!['analyze', 'archive', 'restore', 'forget', 'merge', 'migrate'].includes(sub)) {
|
|
739
|
+
console.error(`Unknown memory subcommand: ${sub}`)
|
|
740
|
+
memoryUsage()
|
|
741
|
+
process.exitCode = 1
|
|
742
|
+
return
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
const store = cliMemoryStore()
|
|
746
|
+
|
|
747
|
+
if (sub === 'analyze') {
|
|
748
|
+
let result
|
|
749
|
+
try {
|
|
750
|
+
result = store.analyzeMemory({
|
|
751
|
+
project: optionValue(flags, '--project'),
|
|
752
|
+
limit: parseNumberOption(flags, '--limit'),
|
|
753
|
+
})
|
|
754
|
+
} catch (error) {
|
|
755
|
+
console.error(error.message || error)
|
|
756
|
+
process.exitCode = 1
|
|
757
|
+
return
|
|
758
|
+
}
|
|
759
|
+
if (useJson) return jsonOrText(result, true)
|
|
760
|
+
return printMemoryAnalyze(result)
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
const ids = collectIds(flags)
|
|
764
|
+
if (ids.length === 0) {
|
|
765
|
+
console.error(`mio memory ${sub} requires --ids <id,id,...> (or bare ids)`)
|
|
766
|
+
process.exitCode = 1
|
|
767
|
+
return
|
|
768
|
+
}
|
|
769
|
+
if (sub === 'merge' && ids.length < 2) {
|
|
770
|
+
console.error('mio memory merge requires at least 2 ids (nothing to merge otherwise)')
|
|
771
|
+
process.exitCode = 1
|
|
772
|
+
return
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
if ((sub === 'archive' || sub === 'merge' || sub === 'forget') && !flagPresent(flags, '--yes')) {
|
|
776
|
+
// Archiving hides records from recall, merging additionally rewrites the
|
|
777
|
+
// survivor's supersedes list, and forgetting deletes outright. Preview first
|
|
778
|
+
// so a mistyped id cannot silently destroy data.
|
|
779
|
+
const previewProject = optionValue(flags, '--project') || projectName() || 'current'
|
|
780
|
+
if (useJson) {
|
|
781
|
+
jsonOrText(
|
|
782
|
+
{
|
|
783
|
+
preview: true,
|
|
784
|
+
applied: false,
|
|
785
|
+
project: previewProject,
|
|
786
|
+
ids,
|
|
787
|
+
keep: optionValue(flags, '--keep') || null,
|
|
788
|
+
reversible: sub !== 'forget',
|
|
789
|
+
hint:
|
|
790
|
+
sub === 'forget'
|
|
791
|
+
? 'PERMANENT: re-run with --yes to delete. Consider mio memory archive instead.'
|
|
792
|
+
: 'Re-run with --yes to apply.',
|
|
793
|
+
},
|
|
794
|
+
true,
|
|
795
|
+
)
|
|
796
|
+
} else if (sub === 'forget') {
|
|
797
|
+
// Show *what* is about to disappear, not just the ids: this is the one
|
|
798
|
+
// memory operation that cannot be undone.
|
|
799
|
+
const all = readJsonl(cliMemoryStore().memoryPath)
|
|
800
|
+
const idSet = new Set(ids)
|
|
801
|
+
const matched = all.filter((r) => idSet.has(r.id))
|
|
802
|
+
console.log(`Forget preview: ${ids.length} id(s) (project=${previewProject})`)
|
|
803
|
+
for (const record of matched) {
|
|
804
|
+
console.log(` ${record.id} ${String(record.content || '').replace(/\s+/g, ' ').slice(0, 80)}`)
|
|
805
|
+
}
|
|
806
|
+
for (const id of ids) {
|
|
807
|
+
if (!matched.some((r) => r.id === id)) console.log(` ${id} (not found)`)
|
|
808
|
+
}
|
|
809
|
+
console.log('')
|
|
810
|
+
console.log('WARNING: this permanently deletes the record(s) and cannot be undone.')
|
|
811
|
+
console.log('An audit entry (excerpt only) is written to memory-forget-audit.jsonl.')
|
|
812
|
+
console.log(`Reversible alternative: mio memory archive --ids ${ids.join(',')} --yes`)
|
|
813
|
+
console.log('Re-run with --yes to delete permanently.')
|
|
814
|
+
} else {
|
|
815
|
+
// Archive keeps its historical "Archive preview" wording; merge is newer
|
|
816
|
+
// and uses a lowercase verb to read naturally mid-sentence.
|
|
817
|
+
const label = sub === 'merge' ? 'merge preview' : 'Archive preview'
|
|
818
|
+
console.log(`${label}: ${ids.length} id(s) (project=${previewProject})`)
|
|
819
|
+
console.log(` ${ids.join(', ')}`)
|
|
820
|
+
if (sub === 'merge') {
|
|
821
|
+
const keep = optionValue(flags, '--keep')
|
|
822
|
+
console.log(` survivor: ${keep || 'newest by timestamp (default)'}`)
|
|
823
|
+
}
|
|
824
|
+
console.log(`Re-run with --yes to apply. Undo with: mio memory restore --ids <ids>`)
|
|
825
|
+
}
|
|
826
|
+
process.exitCode = 1
|
|
827
|
+
return
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
let result
|
|
831
|
+
try {
|
|
832
|
+
if (sub === 'archive' || sub === 'restore') {
|
|
833
|
+
result = store.archiveMemory({
|
|
834
|
+
ids,
|
|
835
|
+
project: optionValue(flags, '--project'),
|
|
836
|
+
restore: sub === 'restore',
|
|
837
|
+
reason: optionValue(flags, '--reason'),
|
|
838
|
+
})
|
|
839
|
+
} else if (sub === 'merge') {
|
|
840
|
+
result = store.mergeMemory({
|
|
841
|
+
ids,
|
|
842
|
+
project: optionValue(flags, '--project'),
|
|
843
|
+
keep: optionValue(flags, '--keep'),
|
|
844
|
+
allowDivergent: flagPresent(flags, '--allow-divergent'),
|
|
845
|
+
reason: optionValue(flags, '--reason'),
|
|
846
|
+
})
|
|
847
|
+
} else if (sub === 'forget') {
|
|
848
|
+
result = store.forgetMemory({
|
|
849
|
+
ids,
|
|
850
|
+
project: optionValue(flags, '--project'),
|
|
851
|
+
reason: optionValue(flags, '--reason'),
|
|
852
|
+
by: 'cli',
|
|
853
|
+
})
|
|
854
|
+
} else {
|
|
855
|
+
result = store.migrateMemory({
|
|
856
|
+
ids,
|
|
857
|
+
scope: optionValue(flags, '--scope'),
|
|
858
|
+
project: optionValue(flags, '--project'),
|
|
859
|
+
reason: optionValue(flags, '--reason'),
|
|
860
|
+
})
|
|
861
|
+
}
|
|
862
|
+
} catch (error) {
|
|
863
|
+
console.error(error.message || error)
|
|
864
|
+
process.exitCode = 1
|
|
865
|
+
return
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
if (useJson) return jsonOrText(result, true)
|
|
869
|
+
|
|
870
|
+
if (sub === 'merge') return printMemoryMerge(result)
|
|
871
|
+
|
|
872
|
+
if (sub === 'forget') {
|
|
873
|
+
console.log(`Forgot ${result.forgottenCount} record(s) permanently (project=${result.project || 'current'})`)
|
|
874
|
+
if (result.forgotten.length > 0) console.log(`deleted: ${result.forgotten.join(', ')}`)
|
|
875
|
+
if (result.notFound.length > 0) console.log(`not found: ${result.notFound.join(', ')}`)
|
|
876
|
+
console.log(`audit: ${result.auditPath}`)
|
|
877
|
+
// Everything missing is a typo; do not let a no-op read as success.
|
|
878
|
+
if (result.forgottenCount === 0) {
|
|
879
|
+
process.exitCode = 1
|
|
880
|
+
return
|
|
881
|
+
}
|
|
882
|
+
// No undo is possible -- say so, and point at what should have been used.
|
|
883
|
+
console.log('This cannot be undone. (Next time consider `mio memory archive`, which is reversible.)')
|
|
884
|
+
return
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
if (sub === 'migrate') {
|
|
888
|
+
const target = result.scope === 'global' ? 'global' : `project=${result.project || 'current'}`
|
|
889
|
+
console.log(`Migrated ${result.updatedCount} record(s) to ${target}`)
|
|
890
|
+
if (result.updated.length > 0) console.log(`updated: ${result.updated.join(', ')}`)
|
|
891
|
+
if (result.unchanged.length > 0) console.log(`already ${result.scope}: ${result.unchanged.join(', ')}`)
|
|
892
|
+
if (result.notFound.length > 0) console.log(`not found: ${result.notFound.join(', ')}`)
|
|
893
|
+
// Every requested id missing is almost certainly a typo; signal it so
|
|
894
|
+
// scripts do not treat a no-op as success.
|
|
895
|
+
if (result.updatedCount === 0 && result.unchanged.length === 0) {
|
|
896
|
+
process.exitCode = 1
|
|
897
|
+
return
|
|
898
|
+
}
|
|
899
|
+
if (result.updated.length > 0) {
|
|
900
|
+
const back = result.scope === 'global' ? 'project' : 'global'
|
|
901
|
+
console.log(`Undo with: mio memory migrate --ids ${result.updated.join(',')} --scope ${back}`)
|
|
902
|
+
}
|
|
903
|
+
return
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
const verb = sub === 'restore' ? 'Restored' : 'Archived'
|
|
907
|
+
console.log(`${verb} ${result.archivedCount} record(s) (project=${result.project || 'current'})`)
|
|
908
|
+
if (result.archived.length > 0) console.log(`${sub}d: ${result.archived.join(', ')}`)
|
|
909
|
+
if (result.skipped.length > 0) console.log(`already ${sub}d: ${result.skipped.join(', ')}`)
|
|
910
|
+
if (result.notFound.length > 0) console.log(`not found: ${result.notFound.join(', ')}`)
|
|
911
|
+
if (result.archivedCount === 0) {
|
|
912
|
+
// Nothing changed: either the ids do not exist or they were already in the
|
|
913
|
+
// target state. Either way the caller's intent was not fulfilled, so say so
|
|
914
|
+
// instead of silently exiting 0.
|
|
915
|
+
console.error(`No records ${sub}d. Check the ids, or run: mio memory analyze`)
|
|
916
|
+
process.exitCode = 1
|
|
917
|
+
return
|
|
918
|
+
}
|
|
919
|
+
if (sub === 'archive' && result.archived.length > 0) {
|
|
920
|
+
console.log(`Undo with: mio memory restore --ids ${result.archived.join(',')}`)
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// The observer auto-claims experience reuse, but an unconfirmed auto_claim
|
|
925
|
+
// never counts as `verified` and so never feeds memory ranking or task
|
|
926
|
+
// routing. In practice most claims sat unconfirmed forever because confirming
|
|
927
|
+
// was only possible through the MCP server.
|
|
928
|
+
function cliExperienceStore() {
|
|
929
|
+
return createExperienceStore({
|
|
930
|
+
dataDir: MIO_HOME,
|
|
931
|
+
projectName,
|
|
932
|
+
})
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
function experienceUsage() {
|
|
936
|
+
console.log(`Usage:
|
|
937
|
+
mio experience list List reuse records (--status/--target-agent/--project/--limit)
|
|
938
|
+
mio experience confirm --ids a,b Confirm auto-claimed reuse (bulk supported)
|
|
939
|
+
mio experience reuse --source-agent A --target-agent B --experience-id X Record a reuse manually
|
|
940
|
+
|
|
941
|
+
Options:
|
|
942
|
+
--ids a,b,c Record ids (also accepted as bare positionals; confirm only)
|
|
943
|
+
--status pending|confirmed|verified|auto_claim|agent_report|all Filter for list (default all)
|
|
944
|
+
--target-agent name Only records reused by this agent
|
|
945
|
+
--experience-id id The memory/experience that was reused
|
|
946
|
+
--source-agent name Agent the experience came from
|
|
947
|
+
--reuse --behavior-changed --outcome-improved Booleans describing the reuse
|
|
948
|
+
--outcome-improved Confirm: accept the claim (--no-improved rejects it)
|
|
949
|
+
--by name Who is confirming (default: cli)
|
|
950
|
+
--notes text Optional note
|
|
951
|
+
--force Re-confirm records that are already confirmed
|
|
952
|
+
--project name Project filter (defaults to current directory name)
|
|
953
|
+
--json Machine-readable output
|
|
954
|
+
`)
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
function printExperienceList(result) {
|
|
958
|
+
console.log(
|
|
959
|
+
`Experience reuse: ${result.count} shown of ${result.total} (project=${result.project || 'all'}, status=${result.status})`,
|
|
960
|
+
)
|
|
961
|
+
if (result.count === 0) {
|
|
962
|
+
console.log('No reuse records match.')
|
|
963
|
+
return
|
|
964
|
+
}
|
|
965
|
+
result.records.forEach((record, index) => {
|
|
966
|
+
const state = record.confirmed ? 'confirmed' : 'pending'
|
|
967
|
+
const badge = record.reuse && record.behaviorChanged && record.outcomeImproved ? 'verified' : state
|
|
968
|
+
const arrow = `${record.sourceAgent || '?'} -> ${record.targetAgent || '?'}`
|
|
969
|
+
const flags = [
|
|
970
|
+
record.reuse ? 'reused' : null,
|
|
971
|
+
record.behaviorChanged ? 'changed' : null,
|
|
972
|
+
record.outcomeImproved ? 'improved' : null,
|
|
973
|
+
]
|
|
974
|
+
.filter(Boolean)
|
|
975
|
+
.join(',')
|
|
976
|
+
console.log(`${index + 1}. [${badge}] ${record.experienceId} ${arrow}`)
|
|
977
|
+
console.log(
|
|
978
|
+
` ${flags || 'no effect flags'} | ${(record.timestamp || '').slice(0, 19)} | ${record.id}`,
|
|
979
|
+
)
|
|
980
|
+
if (record.notes) console.log(` note: ${record.notes}`)
|
|
981
|
+
})
|
|
982
|
+
// Only auto_claims are confirmable — experience.confirm rejects anything else.
|
|
983
|
+
const pending = result.records
|
|
984
|
+
.filter((record) => record.source === 'auto_claim' && !record.confirmed)
|
|
985
|
+
.map((record) => record.id)
|
|
986
|
+
if (pending.length > 0) {
|
|
987
|
+
console.log('')
|
|
988
|
+
console.log(
|
|
989
|
+
`${pending.length} unconfirmed auto-claim(s) shown. Unconfirmed auto-claims never count as verified, so they do not affect ranking or routing.`,
|
|
990
|
+
)
|
|
991
|
+
console.log(`Confirm with: mio experience confirm --ids ${pending.join(',')}`)
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
function experienceCommand(args, useJson) {
|
|
996
|
+
const sub = args[1]
|
|
997
|
+
const flags = args.slice(2)
|
|
998
|
+
|
|
999
|
+
if (!sub || sub === 'help' || sub === '--help' || sub === '-h') {
|
|
1000
|
+
experienceUsage()
|
|
1001
|
+
if (!sub) process.exitCode = 1
|
|
1002
|
+
return
|
|
1003
|
+
}
|
|
1004
|
+
if (!['list', 'confirm', 'reuse'].includes(sub)) {
|
|
1005
|
+
console.error(`Unknown experience subcommand: ${sub}`)
|
|
1006
|
+
experienceUsage()
|
|
1007
|
+
process.exitCode = 1
|
|
1008
|
+
return
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
const store = cliExperienceStore()
|
|
1012
|
+
|
|
1013
|
+
if (sub === 'list') {
|
|
1014
|
+
let result
|
|
1015
|
+
try {
|
|
1016
|
+
result = store.listReuse({
|
|
1017
|
+
status: optionValue(flags, '--status'),
|
|
1018
|
+
targetAgent: optionValue(flags, '--target-agent'),
|
|
1019
|
+
project: optionValue(flags, '--project'),
|
|
1020
|
+
limit: parseNumberOption(flags, '--limit'),
|
|
1021
|
+
})
|
|
1022
|
+
} catch (error) {
|
|
1023
|
+
console.error(error.message || error)
|
|
1024
|
+
process.exitCode = 1
|
|
1025
|
+
return
|
|
1026
|
+
}
|
|
1027
|
+
if (useJson) return jsonOrText(result, true)
|
|
1028
|
+
return printExperienceList(result)
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
let result
|
|
1032
|
+
try {
|
|
1033
|
+
if (sub === 'confirm') {
|
|
1034
|
+
const ids = collectIds(flags)
|
|
1035
|
+
if (ids.length === 0) {
|
|
1036
|
+
console.error('mio experience confirm requires --ids <id,id,...> (or bare ids)')
|
|
1037
|
+
process.exitCode = 1
|
|
1038
|
+
return
|
|
1039
|
+
}
|
|
1040
|
+
// Absent = keep whatever the auto-claim recorded; --no-improved rejects it.
|
|
1041
|
+
const improved = flagPresent(flags, '--no-improved')
|
|
1042
|
+
? false
|
|
1043
|
+
: flagPresent(flags, '--outcome-improved')
|
|
1044
|
+
? true
|
|
1045
|
+
: undefined
|
|
1046
|
+
result = store.confirmReuse({
|
|
1047
|
+
ids,
|
|
1048
|
+
outcomeImproved: improved,
|
|
1049
|
+
confirmedBy: optionValue(flags, '--by') || 'cli',
|
|
1050
|
+
notes: optionValue(flags, '--notes'),
|
|
1051
|
+
force: flagPresent(flags, '--force'),
|
|
1052
|
+
})
|
|
1053
|
+
} else {
|
|
1054
|
+
result = store.recordReuse({
|
|
1055
|
+
sourceAgent: optionValue(flags, '--source-agent'),
|
|
1056
|
+
targetAgent: optionValue(flags, '--target-agent'),
|
|
1057
|
+
experienceId: optionValue(flags, '--experience-id'),
|
|
1058
|
+
reuse: flagPresent(flags, '--reuse'),
|
|
1059
|
+
behaviorChanged: flagPresent(flags, '--behavior-changed'),
|
|
1060
|
+
outcomeImproved: flagPresent(flags, '--outcome-improved'),
|
|
1061
|
+
project: optionValue(flags, '--project'),
|
|
1062
|
+
notes: optionValue(flags, '--notes'),
|
|
1063
|
+
})
|
|
1064
|
+
}
|
|
1065
|
+
} catch (error) {
|
|
1066
|
+
console.error(error.message || error)
|
|
1067
|
+
process.exitCode = 1
|
|
1068
|
+
return
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
if (useJson) return jsonOrText(result, true)
|
|
1072
|
+
|
|
1073
|
+
if (sub === 'reuse') {
|
|
1074
|
+
console.log(
|
|
1075
|
+
`Recorded reuse ${result.evidence.id}: ${result.evidence.sourceAgent} -> ${result.evidence.targetAgent} (${result.evidence.experienceId})`,
|
|
1076
|
+
)
|
|
1077
|
+
return
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
console.log(`Confirmed ${result.confirmedCount} reuse record(s)`)
|
|
1081
|
+
if (result.confirmedIds.length > 0) console.log(`confirmed: ${result.confirmedIds.join(', ')}`)
|
|
1082
|
+
if (result.rejected.length > 0) {
|
|
1083
|
+
console.log(`skipped (not auto_claim): ${result.rejected.join(', ')}`)
|
|
1084
|
+
}
|
|
1085
|
+
if (result.alreadyConfirmed.length > 0) {
|
|
1086
|
+
console.log(`already confirmed: ${result.alreadyConfirmed.join(', ')} (use --force to redo)`)
|
|
1087
|
+
}
|
|
1088
|
+
if (result.notFound.length > 0) console.log(`not found: ${result.notFound.join(', ')}`)
|
|
1089
|
+
if (result.confirmedCount === 0) {
|
|
1090
|
+
console.error('Nothing confirmed. Check the ids, or run: mio experience list --status pending')
|
|
1091
|
+
process.exitCode = 1
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// mio.policy.check has always been able to answer "has this action failed
|
|
1096
|
+
// before?", but only from inside an MCP session -- so in practice nobody asked
|
|
1097
|
+
// before running the command. This gives it a terminal entry point.
|
|
1098
|
+
function policyUsage() {
|
|
1099
|
+
console.log(`Usage:
|
|
1100
|
+
mio policy check "<action>" Historical risk for an action, from trace + memory evidence
|
|
1101
|
+
|
|
1102
|
+
Options:
|
|
1103
|
+
--project name Project filter (defaults to current directory name)
|
|
1104
|
+
--json Machine-readable output (same shape as mio.policy.check)
|
|
1105
|
+
|
|
1106
|
+
Examples:
|
|
1107
|
+
mio policy check "npm publish"
|
|
1108
|
+
mio policy check "git reset --hard" --project akemi-mio
|
|
1109
|
+
|
|
1110
|
+
Reading the result:
|
|
1111
|
+
unknown no history for this action; treat as normal risk
|
|
1112
|
+
low history exists and is mostly clean
|
|
1113
|
+
moderate add verification before proceeding
|
|
1114
|
+
low/high driven by the failure share of matching traces`)
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
function printPolicyCheck(result) {
|
|
1118
|
+
const level = result.riskLevel.toUpperCase()
|
|
1119
|
+
console.log(`Policy check: "${result.action}" (project=${result.project || 'all'})`)
|
|
1120
|
+
console.log(`risk: ${level}${result.risk === null ? '' : ` (${result.risk})`}`)
|
|
1121
|
+
if (result.total === 0) {
|
|
1122
|
+
console.log('evidence: no matching traces')
|
|
1123
|
+
} else {
|
|
1124
|
+
console.log(`evidence: ${result.total} matching trace(s), ${result.failures} failure(s)`)
|
|
1125
|
+
const outcomes = Object.entries(result.outcomeCounts)
|
|
1126
|
+
if (outcomes.length > 0) {
|
|
1127
|
+
console.log(`outcomes: ${outcomes.map(([k, v]) => `${k}=${v}`).join(', ')}`)
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
// A risk level derived from tokens that match most of the corpus looks
|
|
1132
|
+
// authoritative but means nothing. Say so instead of letting it stand.
|
|
1133
|
+
const diag = result.diagnostics
|
|
1134
|
+
if (diag && diag.lowSignal) {
|
|
1135
|
+
console.log('')
|
|
1136
|
+
console.log(
|
|
1137
|
+
`WARNING: low-signal match. Every token in this action (${diag.genericTokens.join(', ')}) ` +
|
|
1138
|
+
'appears in most traces regardless of subject, so the level above is not meaningful.',
|
|
1139
|
+
)
|
|
1140
|
+
console.log('Try a more specific action, e.g. mio policy check "npm publish".')
|
|
1141
|
+
} else if (diag && diag.lowSample) {
|
|
1142
|
+
console.log('')
|
|
1143
|
+
console.log(
|
|
1144
|
+
`NOTE: only ${diag.sampleSize} matching trace(s); treat the level above as a weak signal.`,
|
|
1145
|
+
)
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
console.log(`\n${result.suggestion}`)
|
|
1149
|
+
|
|
1150
|
+
if (result.failureExamples.length > 0) {
|
|
1151
|
+
console.log('\nRecent failures:')
|
|
1152
|
+
for (const example of result.failureExamples) {
|
|
1153
|
+
const when = example.timestamp ? example.timestamp.slice(0, 19).replace('T', ' ') : 'unknown time'
|
|
1154
|
+
console.log(`- [${example.outcome || 'failure'}] ${when}`)
|
|
1155
|
+
console.log(` ${example.summary}`)
|
|
1156
|
+
if (example.trace_id) console.log(` trace: ${example.trace_id}`)
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
if (result.guidance.level !== 'none') {
|
|
1161
|
+
console.log(`\nGuidance (${result.guidance.level}): ${result.guidance.rationale}`)
|
|
1162
|
+
if (result.guidance.avoid.length > 0) {
|
|
1163
|
+
console.log('avoid repeating:')
|
|
1164
|
+
for (const item of result.guidance.avoid) console.log(`- ${item}`)
|
|
1165
|
+
}
|
|
1166
|
+
for (const step of result.guidance.verificationSteps) console.log(`- ${step}`)
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
if (result.related_memories.length > 0) {
|
|
1170
|
+
console.log('\nRelated memory:')
|
|
1171
|
+
for (const record of result.related_memories) {
|
|
1172
|
+
const badge = record.evidence ? ` [verified x${record.evidence.reuseCount}]` : ''
|
|
1173
|
+
console.log(`- ${record.id}${badge} ${String(record.content).replace(/\s+/g, ' ').slice(0, 110)}`)
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
function policyCommand(args, useJson) {
|
|
1179
|
+
const sub = args[1]
|
|
1180
|
+
|
|
1181
|
+
if (!sub || sub === 'help' || sub === '--help' || sub === '-h') {
|
|
1182
|
+
policyUsage()
|
|
1183
|
+
if (!sub) process.exitCode = 1
|
|
1184
|
+
return
|
|
1185
|
+
}
|
|
1186
|
+
if (sub !== 'check') {
|
|
1187
|
+
console.error(`Unknown policy subcommand: ${sub}`)
|
|
1188
|
+
policyUsage()
|
|
1189
|
+
process.exitCode = 1
|
|
1190
|
+
return
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
const flags = args.slice(2)
|
|
1194
|
+
// The action is a positional, and may be quoted as one argument or passed as
|
|
1195
|
+
// several words (`mio policy check npm publish`). Only flags this command
|
|
1196
|
+
// actually understands are treated as options -- anything else, including
|
|
1197
|
+
// `--hard` in `git reset --hard`, belongs to the action being described.
|
|
1198
|
+
const POLICY_KNOWN_FLAGS = new Set(['--project', '--json'])
|
|
1199
|
+
const actionParts = []
|
|
1200
|
+
for (let i = 0; i < flags.length; i += 1) {
|
|
1201
|
+
const token = flags[i]
|
|
1202
|
+
if (POLICY_KNOWN_FLAGS.has(token)) continue
|
|
1203
|
+
if (i > 0 && POLICY_KNOWN_FLAGS.has(flags[i - 1])) continue
|
|
1204
|
+
actionParts.push(token)
|
|
1205
|
+
}
|
|
1206
|
+
const action = actionParts.join(' ').trim()
|
|
1207
|
+
|
|
1208
|
+
if (!action) {
|
|
1209
|
+
console.error('mio policy check requires an action, e.g. mio policy check "npm publish"')
|
|
1210
|
+
process.exitCode = 1
|
|
1211
|
+
return
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
let result
|
|
1215
|
+
try {
|
|
1216
|
+
result = cliPolicyStore().policyCheck({
|
|
1217
|
+
action,
|
|
1218
|
+
project: optionValue(flags, '--project'),
|
|
1219
|
+
})
|
|
1220
|
+
} catch (error) {
|
|
1221
|
+
console.error(error.message || error)
|
|
1222
|
+
process.exitCode = 1
|
|
1223
|
+
return
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
if (useJson) return jsonOrText(result, true)
|
|
1227
|
+
printPolicyCheck(result)
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
// ═════════════════════════════════════════════════════════════
|
|
1231
|
+
// mio insight — terminal entry point for mio.insight.*
|
|
1232
|
+
//
|
|
1233
|
+
// The insight engine (self-observation) shipped MCP-only: an agent could read
|
|
1234
|
+
// its own insights, but there was no way to see them from a terminal. These
|
|
1235
|
+
// commands delegate to the same shared store as the MCP tools, so the two
|
|
1236
|
+
// cannot disagree.
|
|
1237
|
+
//
|
|
1238
|
+
// `generate` is deliberately not exposed. It needs an LLM and the CLI has no
|
|
1239
|
+
// LLM wiring; the same call was made for creativity generate/ferment.
|
|
1240
|
+
// ═════════════════════════════════════════════════════════════
|
|
1241
|
+
function cliInsightStore() {
|
|
1242
|
+
return createInsightStore({ dataDir: MIO_HOME })
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
function insightUsage() {
|
|
1246
|
+
console.log(`Usage:
|
|
1247
|
+
mio insight status Insight counts: total, reported, unreported, high-value
|
|
1248
|
+
mio insight list List insights
|
|
1249
|
+
mio insight mark-reported --ids a,b Mark insights as reported (acknowledged)
|
|
1250
|
+
|
|
1251
|
+
Options:
|
|
1252
|
+
--unreported Only unreported insights (list)
|
|
1253
|
+
--min-score N Minimum score (list)
|
|
1254
|
+
--detector name Filter by detector (list)
|
|
1255
|
+
--limit N Max insights (list)
|
|
1256
|
+
--ids a,b Comma-separated insight ids (mark-reported)
|
|
1257
|
+
--json Machine-readable output (same shape as mio.insight.*)
|
|
1258
|
+
|
|
1259
|
+
The insight engine is an optional package (@akemi-mio/insight). If it is not
|
|
1260
|
+
installed these commands say so, rather than reporting a misleading empty list.`)
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
function printInsightStatus(result) {
|
|
1264
|
+
console.log('Insight engine')
|
|
1265
|
+
console.log(` total: ${result.total}`)
|
|
1266
|
+
console.log(` reported: ${result.reported}`)
|
|
1267
|
+
console.log(` unreported: ${result.unreported}`)
|
|
1268
|
+
console.log(` high-value: ${result.highValue}`)
|
|
1269
|
+
if (result.unreported > 0) {
|
|
1270
|
+
console.log('\nUnreported insights are waiting: mio insight list --unreported')
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
function printInsightList(insights) {
|
|
1275
|
+
if (insights.length === 0) {
|
|
1276
|
+
console.log('No insights match.')
|
|
1277
|
+
return
|
|
1278
|
+
}
|
|
1279
|
+
for (const item of insights) {
|
|
1280
|
+
const score = typeof item.score === 'number' ? item.score.toFixed(2) : 'n/a'
|
|
1281
|
+
const state = item.reported ? 'reported' : 'unreported'
|
|
1282
|
+
const detector = item.detector ? `, ${item.detector}` : ''
|
|
1283
|
+
console.log(`- [${score}] ${item.id || '(no id)'} (${state}${detector})`)
|
|
1284
|
+
const text = String(item.content || item.summary || item.title || '')
|
|
1285
|
+
if (text) console.log(` ${text.replace(/\s+/g, ' ').slice(0, 140)}`)
|
|
1286
|
+
}
|
|
1287
|
+
console.log(`\n${insights.length} insight(s)`)
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
function insightCommand(args, useJson) {
|
|
1291
|
+
const sub = args[1]
|
|
1292
|
+
if (!sub || sub === 'help' || sub === '--help' || sub === '-h') {
|
|
1293
|
+
insightUsage()
|
|
1294
|
+
if (!sub) process.exitCode = 1
|
|
1295
|
+
return
|
|
1296
|
+
}
|
|
1297
|
+
if (!['status', 'list', 'mark-reported'].includes(sub)) {
|
|
1298
|
+
console.error(`Unknown insight subcommand: ${sub}`)
|
|
1299
|
+
insightUsage()
|
|
1300
|
+
process.exitCode = 1
|
|
1301
|
+
return
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
const flags = args.slice(2)
|
|
1305
|
+
const store = cliInsightStore()
|
|
1306
|
+
try {
|
|
1307
|
+
if (sub === 'status') {
|
|
1308
|
+
const result = store.status()
|
|
1309
|
+
if (useJson) return jsonOrText(result, true)
|
|
1310
|
+
return printInsightStatus(result)
|
|
1311
|
+
}
|
|
1312
|
+
if (sub === 'list') {
|
|
1313
|
+
const result = store.list({
|
|
1314
|
+
unreported: flagPresent(flags, '--unreported'),
|
|
1315
|
+
minScore: parseNumberOption(flags, '--min-score'),
|
|
1316
|
+
detector: optionValue(flags, '--detector'),
|
|
1317
|
+
limit: parseNumberOption(flags, '--limit'),
|
|
1318
|
+
})
|
|
1319
|
+
if (useJson) return jsonOrText(result, true)
|
|
1320
|
+
return printInsightList(result)
|
|
1321
|
+
}
|
|
1322
|
+
const ids = splitTagsOption(flags, '--ids')
|
|
1323
|
+
if (ids.length === 0) {
|
|
1324
|
+
console.error('mio insight mark-reported requires --ids a,b')
|
|
1325
|
+
process.exitCode = 1
|
|
1326
|
+
return
|
|
1327
|
+
}
|
|
1328
|
+
const result = store.markReported({ ids })
|
|
1329
|
+
if (useJson) return jsonOrText(result, true)
|
|
1330
|
+
console.log(`Marked ${result.marked} insight(s) as reported.`)
|
|
1331
|
+
} catch (error) {
|
|
1332
|
+
console.error(error.message || error)
|
|
1333
|
+
process.exitCode = 1
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
// ═════════════════════════════════════════════════════════════
|
|
1338
|
+
// mio observer — terminal entry point for the observer research pipeline
|
|
1339
|
+
// (mio.observer.*). Not to be confused with `mio observe`, which is the
|
|
1340
|
+
// WorkBuddy transcript daemon.
|
|
1341
|
+
//
|
|
1342
|
+
// The read-only views plus the two driver commands (collect, ferment) are
|
|
1343
|
+
// exposed. An earlier version of this comment claimed collect/ferment "belong
|
|
1344
|
+
// to the daemon" -- they do not: observe/observer.js only tails WorkBuddy
|
|
1345
|
+
// transcripts and never calls either. Until they got a terminal entry point the
|
|
1346
|
+
// only way to run a collection was from inside an MCP session.
|
|
1347
|
+
//
|
|
1348
|
+
// Base directory defaults to <cwd>/.local/observer -- the MCP server's default
|
|
1349
|
+
// -- because the pipeline is project-local by design. --base-dir overrides it.
|
|
1350
|
+
// ═════════════════════════════════════════════════════════════
|
|
1351
|
+
function cliObserverStore() {
|
|
1352
|
+
return createObserverStore({})
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
function observerUsage() {
|
|
1356
|
+
console.log(`Usage:
|
|
1357
|
+
mio observer status Pipeline stage counts (observations/trends/topics/...)
|
|
1358
|
+
mio observer world-model Entities, events, trends and narratives
|
|
1359
|
+
mio observer trends Recent trend reports
|
|
1360
|
+
mio observer research Research reports
|
|
1361
|
+
mio observer insights Observer-generated insight articles
|
|
1362
|
+
mio observer essays Published essays
|
|
1363
|
+
mio observer dag Daily summaries for the last N days
|
|
1364
|
+
mio observer ingest Record a trace event (tool_call/error/retry/task_outcome)
|
|
1365
|
+
--trace-id <id> --event-type <type> [--payload '<json>'] [--outcome]
|
|
1366
|
+
[--agent] [--host] [--project] (writes immediately, like mio remember)
|
|
1367
|
+
mio observer subscribe Subscribe to events: --event-types a,b [--topic T] [--ttl-days N]
|
|
1368
|
+
(--yes required to apply; previews by default)
|
|
1369
|
+
mio observer digest New events since the last digest (--limit/--project/--event-types)
|
|
1370
|
+
NOTE: advances the cursor, so a second run returns only newer events
|
|
1371
|
+
mio observer collect Fetch from the configured sources (--sources a,b [--keywords k1,k2])
|
|
1372
|
+
[--limit N] (needs @akemi-mio/observer; hits the network)
|
|
1373
|
+
mio observer ferment Run the fermentation engine over recent observations
|
|
1374
|
+
(--session morning|afternoon|night; needs @akemi-mio/observer)
|
|
1375
|
+
|
|
1376
|
+
Options:
|
|
1377
|
+
--base-dir DIR Observer data directory (default: <cwd>/.local/observer)
|
|
1378
|
+
--date YYYY-MM-DD A single trend report (trends)
|
|
1379
|
+
--type name Essay type (default: published)
|
|
1380
|
+
--days N Days of history (dag, default 7)
|
|
1381
|
+
--limit N Max items
|
|
1382
|
+
--sources a,b Sources to collect from (default: all configured)
|
|
1383
|
+
--keywords a,b Keyword filter, OR matched (collect)
|
|
1384
|
+
--session label Fermentation session (default: afternoon)
|
|
1385
|
+
--json Machine-readable output (same shape as mio.observer.*)`)
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
// Pipeline items are free-form JSON written by different stages, so pull a
|
|
1389
|
+
// human label from whichever well-known field happens to be present.
|
|
1390
|
+
function observerItemLabel(item) {
|
|
1391
|
+
if (item === null || typeof item !== 'object') return String(item)
|
|
1392
|
+
for (const key of ['title', 'name', 'topic', 'headline', 'summary', 'text', 'id', 'date']) {
|
|
1393
|
+
const value = item[key]
|
|
1394
|
+
if (typeof value === 'string' && value.trim()) return value.trim().replace(/\s+/g, ' ').slice(0, 120)
|
|
1395
|
+
}
|
|
1396
|
+
return JSON.stringify(item).slice(0, 120)
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
function printObserverList(kind, items) {
|
|
1400
|
+
if (items.length === 0) {
|
|
1401
|
+
console.log(`No ${kind} found. (Run the observer pipeline, or check --base-dir.)`)
|
|
1402
|
+
return
|
|
1403
|
+
}
|
|
1404
|
+
for (const item of items) console.log(`- ${observerItemLabel(item)}`)
|
|
1405
|
+
console.log(`\n${items.length} ${kind}`)
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
function printObserverStatus(status) {
|
|
1409
|
+
console.log('Observer pipeline (file counts per stage)')
|
|
1410
|
+
let total = 0
|
|
1411
|
+
for (const [stage, count] of Object.entries(status)) {
|
|
1412
|
+
total += count
|
|
1413
|
+
console.log(` ${stage.padEnd(14)} ${count}`)
|
|
1414
|
+
}
|
|
1415
|
+
console.log(`\n ${'total'.padEnd(14)} ${total}`)
|
|
1416
|
+
if (total === 0) {
|
|
1417
|
+
console.log('\nNo pipeline data yet. Nothing has been collected into this directory.')
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
function printObserverWorldModel(model) {
|
|
1422
|
+
const sections = ['entities', 'events', 'trends', 'narratives']
|
|
1423
|
+
console.log('World model')
|
|
1424
|
+
for (const key of sections) {
|
|
1425
|
+
const value = model[key]
|
|
1426
|
+
const count = Array.isArray(value) ? value.length : Object.keys(value || {}).length
|
|
1427
|
+
console.log(` ${key.padEnd(12)} ${count}`)
|
|
1428
|
+
}
|
|
1429
|
+
if (sections.every((key) => (Array.isArray(model[key]) ? model[key].length : 0) === 0)) {
|
|
1430
|
+
console.log('\nWorld model is empty.')
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
function printObserverEssays(essays) {
|
|
1435
|
+
if (essays.length === 0) {
|
|
1436
|
+
console.log('No essays found.')
|
|
1437
|
+
return
|
|
1438
|
+
}
|
|
1439
|
+
for (const essay of essays) {
|
|
1440
|
+
console.log(`- ${essay.file} (${essay.type}, ${String(essay.created).slice(0, 10)})`)
|
|
1441
|
+
const firstLine = String(essay.content || '').split('\n').find((l) => l.trim()) || ''
|
|
1442
|
+
if (firstLine) console.log(` ${firstLine.replace(/\s+/g, ' ').slice(0, 120)}`)
|
|
1443
|
+
}
|
|
1444
|
+
console.log(`\n${essays.length} essay(s)`)
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
function printObserverDag(result, days) {
|
|
1448
|
+
if (result.summaryCount === 0) {
|
|
1449
|
+
console.log(`No daily summaries found in the last ${days || 7} day(s).`)
|
|
1450
|
+
return
|
|
1451
|
+
}
|
|
1452
|
+
for (const day of result.summaries) {
|
|
1453
|
+
console.log(`- ${day.date}: ${day.summaries.length} summary(ies)`)
|
|
1454
|
+
}
|
|
1455
|
+
console.log(`\n${result.summaryCount} summary(ies) across ${result.summaries.length} day(s)`)
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
function cliSubscriptionStore() {
|
|
1459
|
+
return createSubscriptionStore({
|
|
1460
|
+
dataDir: MIO_HOME,
|
|
1461
|
+
projectName,
|
|
1462
|
+
agentId: () => 'cli',
|
|
1463
|
+
})
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
// Records an arbitrary trace event (tool_call / error / retry / task_outcome).
|
|
1467
|
+
// Unlike `mio task record-outcome`, which is specific to task outcomes, this is
|
|
1468
|
+
// the general-purpose half -- and the one AGENTS.md asks agents to call.
|
|
1469
|
+
//
|
|
1470
|
+
// It appends rather than modifies, so like `mio remember` it writes straight
|
|
1471
|
+
// away instead of previewing: there is nothing to destroy. Missing arguments are
|
|
1472
|
+
// still rejected before anything is written.
|
|
1473
|
+
function observerIngestCommand(args, useJson) {
|
|
1474
|
+
const flags = args.slice(2)
|
|
1475
|
+
const traceId = optionValue(flags, '--trace-id')
|
|
1476
|
+
const eventType = optionValue(flags, '--event-type')
|
|
1477
|
+
if (!traceId || !eventType) {
|
|
1478
|
+
console.error('mio observer ingest requires --trace-id <id> and --event-type <type>')
|
|
1479
|
+
console.error('Example: mio observer ingest --trace-id t1 --event-type tool_call --payload \'{"tool":"Bash"}\'')
|
|
1480
|
+
process.exitCode = 1
|
|
1481
|
+
return
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
let payload = {}
|
|
1485
|
+
const rawPayload = optionValue(flags, '--payload')
|
|
1486
|
+
if (rawPayload) {
|
|
1487
|
+
try {
|
|
1488
|
+
payload = JSON.parse(rawPayload)
|
|
1489
|
+
} catch (error) {
|
|
1490
|
+
console.error(`--payload must be valid JSON: ${error.message}`)
|
|
1491
|
+
process.exitCode = 1
|
|
1492
|
+
return
|
|
1493
|
+
}
|
|
1494
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
1495
|
+
console.error('--payload must be a JSON object')
|
|
1496
|
+
process.exitCode = 1
|
|
1497
|
+
return
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
let result
|
|
1502
|
+
try {
|
|
1503
|
+
result = cliTaskStore().ingestObservation({
|
|
1504
|
+
trace_id: traceId,
|
|
1505
|
+
event_type: eventType,
|
|
1506
|
+
outcome: optionValue(flags, '--outcome'),
|
|
1507
|
+
payload,
|
|
1508
|
+
agent: optionValue(flags, '--agent'),
|
|
1509
|
+
host: optionValue(flags, '--host'),
|
|
1510
|
+
project: optionValue(flags, '--project'),
|
|
1511
|
+
})
|
|
1512
|
+
} catch (error) {
|
|
1513
|
+
console.error(error.message || error)
|
|
1514
|
+
process.exitCode = 1
|
|
1515
|
+
return
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
if (useJson) return jsonOrText(result, true)
|
|
1519
|
+
console.log(`Ingested ${result.event.event_type} (trace=${result.event.trace_id})`)
|
|
1520
|
+
console.log(` agent=${result.event.agent} project=${result.event.project} host=${result.event.host}`)
|
|
1521
|
+
if (result.autoClaims && result.autoClaims.length > 0) {
|
|
1522
|
+
console.log(` auto-claims: ${result.autoClaims.length}`)
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
// Subscribe creates or renews a subscription, so it previews like every other
|
|
1527
|
+
// mutating command (`mio agents register`, `mio memory archive`): exit 1 and
|
|
1528
|
+
// nothing written without --yes.
|
|
1529
|
+
function observerSubscribeCommand(args, useJson) {
|
|
1530
|
+
const flags = args.slice(2)
|
|
1531
|
+
const project = optionValue(flags, '--project') || projectName()
|
|
1532
|
+
const eventTypes = splitTagsOption(flags, '--event-types')
|
|
1533
|
+
const topic = optionValue(flags, '--topic') || ''
|
|
1534
|
+
const ttlDays = optionValue(flags, '--ttl-days')
|
|
1535
|
+
|
|
1536
|
+
if (!flagPresent(flags, '--yes')) {
|
|
1537
|
+
const store = cliSubscriptionStore()
|
|
1538
|
+
const existing = readJsonl(store.subscriptionPath).find((subscription) => {
|
|
1539
|
+
return (
|
|
1540
|
+
subscription &&
|
|
1541
|
+
subscription.agent === 'cli' &&
|
|
1542
|
+
subscribeKey(subscription.agent, subscription.project, subscription.eventTypes || [], subscription.topic || '') ===
|
|
1543
|
+
subscribeKey('cli', project, eventTypes, topic)
|
|
1544
|
+
)
|
|
1545
|
+
})
|
|
1546
|
+
if (useJson) {
|
|
1547
|
+
jsonOrText(
|
|
1548
|
+
{
|
|
1549
|
+
preview: true,
|
|
1550
|
+
applied: false,
|
|
1551
|
+
project,
|
|
1552
|
+
eventTypes,
|
|
1553
|
+
topic: topic || null,
|
|
1554
|
+
ttlDays: ttlDays ? Number(ttlDays) : null,
|
|
1555
|
+
willCreate: !existing,
|
|
1556
|
+
hint: 'Re-run with --yes to apply.',
|
|
1557
|
+
},
|
|
1558
|
+
true,
|
|
1559
|
+
)
|
|
1560
|
+
} else {
|
|
1561
|
+
console.log(`Subscribe preview: project=${project}`)
|
|
1562
|
+
console.log(` event types: ${eventTypes.length > 0 ? eventTypes.join(', ') : '(all)'}`)
|
|
1563
|
+
if (topic) console.log(` topic: ${topic}`)
|
|
1564
|
+
console.log(` ${existing ? 'will renew the existing subscription' : 'will create a new subscription'}`)
|
|
1565
|
+
console.log('Re-run with --yes to apply.')
|
|
1566
|
+
}
|
|
1567
|
+
process.exitCode = 1
|
|
1568
|
+
return
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
let result
|
|
1572
|
+
try {
|
|
1573
|
+
result = cliSubscriptionStore().subscribe({ project, eventTypes, topic, ttlDays })
|
|
1574
|
+
} catch (error) {
|
|
1575
|
+
console.error(error.message || error)
|
|
1576
|
+
process.exitCode = 1
|
|
1577
|
+
return
|
|
1578
|
+
}
|
|
1579
|
+
if (useJson) return jsonOrText(result, true)
|
|
1580
|
+
console.log(`Subscribed (project=${result.project})`)
|
|
1581
|
+
console.log(` id=${result.subscription.id} expires=${result.subscription.expiresAt}`)
|
|
1582
|
+
console.log(` event types: ${(result.subscription.eventTypes || []).join(', ') || '(all)'}`)
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
// The digest is cursor-based: it returns only events newer than the last one it
|
|
1586
|
+
// delivered, and ADVANCES that cursor. Running it twice therefore gives different
|
|
1587
|
+
// results -- so it cannot be previewed, and the output says so rather than
|
|
1588
|
+
// leaving that surprise for the user to discover.
|
|
1589
|
+
function observerDigestCommand(args, useJson) {
|
|
1590
|
+
const flags = args.slice(2)
|
|
1591
|
+
let result
|
|
1592
|
+
try {
|
|
1593
|
+
result = cliSubscriptionStore().digest({
|
|
1594
|
+
project: optionValue(flags, '--project'),
|
|
1595
|
+
limit: optionValue(flags, '--limit'),
|
|
1596
|
+
eventTypes: splitTagsOption(flags, '--event-types'),
|
|
1597
|
+
})
|
|
1598
|
+
} catch (error) {
|
|
1599
|
+
console.error(error.message || error)
|
|
1600
|
+
process.exitCode = 1
|
|
1601
|
+
return
|
|
1602
|
+
}
|
|
1603
|
+
if (useJson) return jsonOrText(result, true)
|
|
1604
|
+
console.log(`Observer digest: ${result.count} event(s) (project=${result.project}, agent=${result.agent})`)
|
|
1605
|
+
console.log(` subscriptions: ${result.subscriptionCount} active, ${result.matchedSubscriptions.length} matched`)
|
|
1606
|
+
for (const event of result.events) {
|
|
1607
|
+
console.log(`- ${event.event_type}${event.outcome ? ` (${event.outcome})` : ''} trace=${event.trace_id || '-'}`)
|
|
1608
|
+
}
|
|
1609
|
+
if (result.count === 0) {
|
|
1610
|
+
console.log('Nothing new since the last digest.')
|
|
1611
|
+
}
|
|
1612
|
+
console.log('Note: this advances the cursor; the next digest returns only newer events.')
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
// collect drives the upstream collectors, so it needs @akemi-mio/observer and
|
|
1616
|
+
// hits the network. Unlike the mutating commands it is not previewed: it only
|
|
1617
|
+
// appends observations, and a preview would have to fetch everything twice.
|
|
1618
|
+
function printObserverCollect(result) {
|
|
1619
|
+
console.log(`Collected ${result.collected} observation(s)`)
|
|
1620
|
+
if (result.collected === 0) {
|
|
1621
|
+
console.log('Nothing came back. Check the source names and network access.')
|
|
1622
|
+
return
|
|
1623
|
+
}
|
|
1624
|
+
const perSource = new Map()
|
|
1625
|
+
for (const item of result.observations) {
|
|
1626
|
+
const key = item.source || '(unknown source)'
|
|
1627
|
+
const bucket = perSource.get(key) || { ok: 0, errors: [] }
|
|
1628
|
+
if (item.error) bucket.errors.push(item.error)
|
|
1629
|
+
else bucket.ok += 1
|
|
1630
|
+
perSource.set(key, bucket)
|
|
1631
|
+
}
|
|
1632
|
+
for (const [source, bucket] of perSource) {
|
|
1633
|
+
const suffix = bucket.errors.length > 0 ? ` (errors: ${bucket.errors.join('; ')})` : ''
|
|
1634
|
+
console.log(` ${source}: ${bucket.ok}${suffix}`)
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
async function observerCollectCommand(args, useJson) {
|
|
1639
|
+
const flags = args.slice(2)
|
|
1640
|
+
const base = { baseDir: optionValue(flags, '--base-dir') }
|
|
1641
|
+
// splitTagsOption returns [] for an absent flag, and [] is truthy -- passing
|
|
1642
|
+
// it through would mean "collect from zero sources" instead of "all sources".
|
|
1643
|
+
const sources = splitTagsOption(flags, '--sources')
|
|
1644
|
+
const keywords = splitTagsOption(flags, '--keywords')
|
|
1645
|
+
let result
|
|
1646
|
+
try {
|
|
1647
|
+
result = await cliObserverStore().collect({
|
|
1648
|
+
...base,
|
|
1649
|
+
sources: sources.length > 0 ? sources : undefined,
|
|
1650
|
+
keywords: keywords.length > 0 ? keywords : undefined,
|
|
1651
|
+
limit: parseNumberOption(flags, '--limit'),
|
|
1652
|
+
})
|
|
1653
|
+
} catch (error) {
|
|
1654
|
+
console.error(error.message || error)
|
|
1655
|
+
process.exitCode = 1
|
|
1656
|
+
return
|
|
1657
|
+
}
|
|
1658
|
+
if (useJson) return jsonOrText(result, true)
|
|
1659
|
+
printObserverCollect(result)
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
// ferment is likewise a driver command: it may write an essay when a cluster
|
|
1663
|
+
// clears the writing threshold, which is why it is not part of the read-only
|
|
1664
|
+
// views even though it mostly reads.
|
|
1665
|
+
function printObserverFerment(result, session) {
|
|
1666
|
+
if (result.error) {
|
|
1667
|
+
console.log(`Fermentation failed: ${result.error}`)
|
|
1668
|
+
process.exitCode = 1
|
|
1669
|
+
return
|
|
1670
|
+
}
|
|
1671
|
+
const clusters = Array.isArray(result.clusters) ? result.clusters : []
|
|
1672
|
+
console.log(`Fermentation (session=${session}): ${clusters.length} cluster(s)`)
|
|
1673
|
+
for (const cluster of clusters) {
|
|
1674
|
+
const strength = typeof cluster.strength === 'number' ? cluster.strength.toFixed(2) : 'n/a'
|
|
1675
|
+
const words = Array.isArray(cluster.associations) ? cluster.associations.join(', ') : ''
|
|
1676
|
+
console.log(`- [${strength}] ${cluster.theme || '(untitled)'}${words ? ` — ${words}` : ''}`)
|
|
1677
|
+
}
|
|
1678
|
+
if (clusters.length === 0) {
|
|
1679
|
+
console.log('No recurring themes found in the recent observations.')
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
async function observerFermentCommand(args, useJson) {
|
|
1684
|
+
const flags = args.slice(2)
|
|
1685
|
+
const session = optionValue(flags, '--session') || 'afternoon'
|
|
1686
|
+
const base = { baseDir: optionValue(flags, '--base-dir') }
|
|
1687
|
+
let result
|
|
1688
|
+
try {
|
|
1689
|
+
result = await cliObserverStore().ferment({ ...base, session })
|
|
1690
|
+
} catch (error) {
|
|
1691
|
+
console.error(error.message || error)
|
|
1692
|
+
process.exitCode = 1
|
|
1693
|
+
return
|
|
1694
|
+
}
|
|
1695
|
+
if (useJson) return jsonOrText(result, true)
|
|
1696
|
+
printObserverFerment(result, session)
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
function observerCommand(args, useJson) {
|
|
1700
|
+
const sub = args[1]
|
|
1701
|
+
if (!sub || sub === 'help' || sub === '--help' || sub === '-h') {
|
|
1702
|
+
observerUsage()
|
|
1703
|
+
if (!sub) process.exitCode = 1
|
|
1704
|
+
return
|
|
1705
|
+
}
|
|
1706
|
+
if (sub === 'ingest') return observerIngestCommand(args, useJson)
|
|
1707
|
+
if (sub === 'subscribe') return observerSubscribeCommand(args, useJson)
|
|
1708
|
+
if (sub === 'digest') return observerDigestCommand(args, useJson)
|
|
1709
|
+
// Driver commands return promises (the upstream engine is async), so they are
|
|
1710
|
+
// returned rather than called -- main() is async and awaits them.
|
|
1711
|
+
if (sub === 'collect') return observerCollectCommand(args, useJson)
|
|
1712
|
+
if (sub === 'ferment') return observerFermentCommand(args, useJson)
|
|
1713
|
+
|
|
1714
|
+
if (!['status', 'world-model', 'trends', 'research', 'insights', 'essays', 'dag'].includes(sub)) {
|
|
1715
|
+
console.error(`Unknown observer subcommand: ${sub}`)
|
|
1716
|
+
observerUsage()
|
|
1717
|
+
process.exitCode = 1
|
|
1718
|
+
return
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
const flags = args.slice(2)
|
|
1722
|
+
const store = cliObserverStore()
|
|
1723
|
+
const base = { baseDir: optionValue(flags, '--base-dir') }
|
|
1724
|
+
try {
|
|
1725
|
+
if (sub === 'status') {
|
|
1726
|
+
const result = store.status(base)
|
|
1727
|
+
if (useJson) return jsonOrText(result, true)
|
|
1728
|
+
return printObserverStatus(result)
|
|
1729
|
+
}
|
|
1730
|
+
if (sub === 'world-model') {
|
|
1731
|
+
const result = store.worldModel(base)
|
|
1732
|
+
if (useJson) return jsonOrText(result, true)
|
|
1733
|
+
return printObserverWorldModel(result)
|
|
1734
|
+
}
|
|
1735
|
+
if (sub === 'trends') {
|
|
1736
|
+
const result = store.trends({ ...base, date: optionValue(flags, '--date'), limit: parseNumberOption(flags, '--limit') })
|
|
1737
|
+
if (useJson) return jsonOrText(result, true)
|
|
1738
|
+
return printObserverList('trend report(s)', result)
|
|
1739
|
+
}
|
|
1740
|
+
if (sub === 'research') {
|
|
1741
|
+
const result = store.research({ ...base, limit: parseNumberOption(flags, '--limit') })
|
|
1742
|
+
if (useJson) return jsonOrText(result, true)
|
|
1743
|
+
return printObserverList('research report(s)', result)
|
|
1744
|
+
}
|
|
1745
|
+
if (sub === 'insights') {
|
|
1746
|
+
const result = store.insights({ ...base, limit: parseNumberOption(flags, '--limit') })
|
|
1747
|
+
if (useJson) return jsonOrText(result, true)
|
|
1748
|
+
return printObserverList('insight(s)', result)
|
|
1749
|
+
}
|
|
1750
|
+
if (sub === 'essays') {
|
|
1751
|
+
const result = store.essays({ ...base, type: optionValue(flags, '--type'), limit: parseNumberOption(flags, '--limit') })
|
|
1752
|
+
if (useJson) return jsonOrText(result, true)
|
|
1753
|
+
return printObserverEssays(result)
|
|
1754
|
+
}
|
|
1755
|
+
const days = parseNumberOption(flags, '--days')
|
|
1756
|
+
const result = store.dag({ ...base, days })
|
|
1757
|
+
if (useJson) return jsonOrText(result, true)
|
|
1758
|
+
return printObserverDag(result, days)
|
|
1759
|
+
} catch (error) {
|
|
1760
|
+
console.error(error.message || error)
|
|
1761
|
+
process.exitCode = 1
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
function creativityUsage() {
|
|
1766
|
+
console.log(`Usage:
|
|
1767
|
+
mio creativity status Show hypothesis counts and recent top ideas
|
|
1768
|
+
mio creativity list List hypotheses (--status active|validated|rejected|draft, --limit N)
|
|
1769
|
+
|
|
1770
|
+
mio creativity generate Generate hypotheses by combining sources (needs 2+ --source, calls an LLM)
|
|
1771
|
+
mio creativity ferment Review/refine active hypotheses (calls an LLM)
|
|
1772
|
+
|
|
1773
|
+
Options:
|
|
1774
|
+
--status name Filter list by status
|
|
1775
|
+
--limit N Max results for list / ferment (default 20 / 5)
|
|
1776
|
+
--source "name|content" A concept source for generate; repeat 2+ times
|
|
1777
|
+
--strategy explore|signal|stable Generation strategy (auto-selected if omitted)
|
|
1778
|
+
--json Machine-readable output (same shape as the mio.creativity.* tools)
|
|
1779
|
+
|
|
1780
|
+
Examples:
|
|
1781
|
+
mio creativity status
|
|
1782
|
+
mio creativity list --status rejected --limit 10
|
|
1783
|
+
mio creativity generate --source "auth|token rotation" --source "cache|write-through"
|
|
1784
|
+
|
|
1785
|
+
generate/ferment call an LLM using the shared client. Configure it with
|
|
1786
|
+
LLM_API_URL / LLM_KEY / LLM_CHAT_MODEL (the same environment the MCP server
|
|
1787
|
+
reads). A local Ollama works:
|
|
1788
|
+
LLM_API_URL=http://localhost:11434/v1/chat/completions`)
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
function printCreativityStatus(result) {
|
|
1792
|
+
console.log('Creativity engine:')
|
|
1793
|
+
console.log(` hypotheses: ${result.hypotheses} combos: ${result.combos} experiments: ${result.experiments}`)
|
|
1794
|
+
console.log(` active: ${result.active} validated: ${result.validated} rejected: ${result.rejected}`)
|
|
1795
|
+
if (result.recentIdeas.length === 0) {
|
|
1796
|
+
console.log('No hypotheses yet.')
|
|
1797
|
+
return
|
|
1798
|
+
}
|
|
1799
|
+
console.log('\nRecent top ideas:')
|
|
1800
|
+
for (const idea of result.recentIdeas) {
|
|
1801
|
+
console.log(`- ${idea.title} (novelty=${idea.novelty} feasibility=${idea.feasibility} impact=${idea.impact} score=${idea.score})`)
|
|
1802
|
+
console.log(` ${idea.id}`)
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
function printCreativityList(items) {
|
|
1807
|
+
if (items.length === 0) {
|
|
1808
|
+
console.log('No hypotheses match.')
|
|
1809
|
+
return
|
|
1810
|
+
}
|
|
1811
|
+
console.log(`Hypotheses: ${items.length} shown`)
|
|
1812
|
+
items.forEach((h, index) => {
|
|
1813
|
+
const when = h.createdAt ? new Date(h.createdAt).toISOString().slice(0, 19).replace('T', ' ') : ''
|
|
1814
|
+
const labels = Array.isArray(h.sourceLabels) && h.sourceLabels.length > 0 ? h.sourceLabels.join(' + ') : '—'
|
|
1815
|
+
console.log(`${index + 1}. [${h.status}] ${h.title} (N=${h.novelty} F=${h.feasibility} I=${h.impact} score=${h.score})`)
|
|
1816
|
+
console.log(` ${labels} | ${when} | ${h.id}`)
|
|
1817
|
+
if (h.fermentCount) console.log(` fermented ${h.fermentCount}x`)
|
|
1818
|
+
})
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
// Parses repeated `--source "name|content"` into the shape CreativityEngine
|
|
1822
|
+
// expects. At least two are required: the engine pairs up sources, so one
|
|
1823
|
+
// source can never produce a combination.
|
|
1824
|
+
function parseSources(flags) {
|
|
1825
|
+
const sources = []
|
|
1826
|
+
for (let i = 0; i < flags.length; i += 1) {
|
|
1827
|
+
if (flags[i] !== '--source') continue
|
|
1828
|
+
const raw = flags[i + 1]
|
|
1829
|
+
if (!raw) continue
|
|
1830
|
+
const sep = raw.indexOf('|')
|
|
1831
|
+
if (sep === -1) {
|
|
1832
|
+
sources.push({ name: raw, content: raw })
|
|
1833
|
+
} else {
|
|
1834
|
+
sources.push({
|
|
1835
|
+
name: raw.slice(0, sep).trim(),
|
|
1836
|
+
content: raw.slice(sep + 1).trim(),
|
|
1837
|
+
})
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
return sources
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
function warnIfLlmUnconfigured() {
|
|
1844
|
+
if (isLlmConfigured()) return
|
|
1845
|
+
const { apiUrl, model } = llmConfig()
|
|
1846
|
+
console.log(`Note: no LLM configured, so this will call the default endpoint (${model} @ ${apiUrl}).`)
|
|
1847
|
+
console.log('Configure one with: mio config llm --url http://localhost:11434/v1/chat/completions --model qwen2.5:14b')
|
|
1848
|
+
console.log('(or set LLM_API_URL / LLM_KEY / LLM_CHAT_MODEL in the environment)')
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
function creativityGenerateCommand(args, useJson) {
|
|
1852
|
+
const flags = args.slice(2)
|
|
1853
|
+
const sources = parseSources(flags)
|
|
1854
|
+
if (sources.length < 2) {
|
|
1855
|
+
console.error('mio creativity generate requires at least two --source "name|content" arguments')
|
|
1856
|
+
console.error('Example: mio creativity generate --source "auth|token rotation" --source "cache|write-through cache"')
|
|
1857
|
+
process.exitCode = 1
|
|
1858
|
+
return
|
|
1859
|
+
}
|
|
1860
|
+
const strategy = optionValue(flags, '--strategy')
|
|
1861
|
+
|
|
1862
|
+
if (!useJson) warnIfLlmUnconfigured()
|
|
1863
|
+
|
|
1864
|
+
let result
|
|
1865
|
+
try {
|
|
1866
|
+
result = cliCreativityEngine().generate(sources, strategy)
|
|
1867
|
+
} catch (error) {
|
|
1868
|
+
console.error(error.message || error)
|
|
1869
|
+
process.exitCode = 1
|
|
1870
|
+
return
|
|
1871
|
+
}
|
|
1872
|
+
// generate is async in the engine.
|
|
1873
|
+
return Promise.resolve(result).then((resolved) => {
|
|
1874
|
+
if (useJson) return jsonOrText(resolved, true)
|
|
1875
|
+
if (resolved.reason) {
|
|
1876
|
+
console.log(`No hypotheses generated: ${resolved.reason}`)
|
|
1877
|
+
return
|
|
1878
|
+
}
|
|
1879
|
+
console.log(`Generated ${resolved.ideas.length} hypothesis/hypotheses (strategy=${resolved.strategy})`)
|
|
1880
|
+
resolved.ideas.forEach((idea, index) => {
|
|
1881
|
+
console.log(`${index + 1}. [${idea.status}] ${idea.title}`)
|
|
1882
|
+
console.log(` novelty=${idea.novelty} feasibility=${idea.feasibility} impact=${idea.impact} | ${idea.id}`)
|
|
1883
|
+
if (idea.rejectionReason) console.log(` rejected: ${idea.rejectionReason}`)
|
|
1884
|
+
})
|
|
1885
|
+
})
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
function creativityFermentCommand(args, useJson) {
|
|
1889
|
+
const flags = args.slice(2)
|
|
1890
|
+
const limit = parseNumberOption(flags, '--limit')
|
|
1891
|
+
|
|
1892
|
+
if (!useJson) warnIfLlmUnconfigured()
|
|
1893
|
+
|
|
1894
|
+
let result
|
|
1895
|
+
try {
|
|
1896
|
+
result = cliCreativityEngine().ferment(limit)
|
|
1897
|
+
} catch (error) {
|
|
1898
|
+
console.error(error.message || error)
|
|
1899
|
+
process.exitCode = 1
|
|
1900
|
+
return
|
|
1901
|
+
}
|
|
1902
|
+
return Promise.resolve(result).then((resolved) => {
|
|
1903
|
+
if (useJson) return jsonOrText(resolved, true)
|
|
1904
|
+
if (resolved.fermented === 0) {
|
|
1905
|
+
console.log(`Nothing fermented: ${resolved.reason || 'no eligible hypotheses'}`)
|
|
1906
|
+
return
|
|
1907
|
+
}
|
|
1908
|
+
console.log(`Fermented ${resolved.fermented} hypothesis/hypotheses`)
|
|
1909
|
+
for (const r of resolved.results || []) {
|
|
1910
|
+
console.log(`- ${r.title || r.id}: ${r.verdict || 'updated'}${r.reason ? ` — ${r.reason}` : ''}`)
|
|
1911
|
+
}
|
|
1912
|
+
})
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
function creativityCommand(args, useJson) {
|
|
1916
|
+
const sub = args[1]
|
|
1917
|
+
|
|
1918
|
+
if (!sub || sub === 'help' || sub === '--help' || sub === '-h') {
|
|
1919
|
+
creativityUsage()
|
|
1920
|
+
if (!sub) process.exitCode = 1
|
|
1921
|
+
return
|
|
1922
|
+
}
|
|
1923
|
+
if (sub === 'generate') return creativityGenerateCommand(args, useJson)
|
|
1924
|
+
if (sub === 'ferment') return creativityFermentCommand(args, useJson)
|
|
1925
|
+
|
|
1926
|
+
if (!['status', 'list'].includes(sub)) {
|
|
1927
|
+
console.error(`Unknown creativity subcommand: ${sub}`)
|
|
1928
|
+
creativityUsage()
|
|
1929
|
+
process.exitCode = 1
|
|
1930
|
+
return
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
const flags = args.slice(2)
|
|
1934
|
+
const engine = cliCreativityEngine()
|
|
1935
|
+
|
|
1936
|
+
let result
|
|
1937
|
+
try {
|
|
1938
|
+
if (sub === 'status') {
|
|
1939
|
+
result = engine.status()
|
|
1940
|
+
} else {
|
|
1941
|
+
result = engine.list({
|
|
1942
|
+
status: optionValue(flags, '--status'),
|
|
1943
|
+
limit: parseNumberOption(flags, '--limit'),
|
|
1944
|
+
})
|
|
1945
|
+
}
|
|
1946
|
+
} catch (error) {
|
|
1947
|
+
console.error(error.message || error)
|
|
1948
|
+
process.exitCode = 1
|
|
1949
|
+
return
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
if (useJson) return jsonOrText(result, true)
|
|
1953
|
+
if (sub === 'status') printCreativityStatus(result)
|
|
1954
|
+
else printCreativityList(result)
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
function readConfig() {
|
|
1958
|
+
const fallback = { version: 1, home: MIO_HOME, agents: {} }
|
|
1959
|
+
if (!fs.existsSync(CONFIG_FILE)) return fallback
|
|
1960
|
+
try {
|
|
1961
|
+
return { ...fallback, ...JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')) }
|
|
1962
|
+
} catch (_) {
|
|
1963
|
+
return fallback
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
function writeConfig(config) {
|
|
1968
|
+
ensureHome()
|
|
1969
|
+
fs.writeFileSync(CONFIG_FILE, `${JSON.stringify(config, null, 2)}\n`, 'utf8')
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
// Mask a secret for display: keep a short prefix/suffix so users can tell which
|
|
1973
|
+
// key is stored without leaking it. Anything short enough that masking would
|
|
1974
|
+
// still reveal most of it is hidden entirely.
|
|
1975
|
+
function maskSecret(value) {
|
|
1976
|
+
if (!value) return ''
|
|
1977
|
+
if (value.length <= 8) return '*'.repeat(value.length)
|
|
1978
|
+
return `${value.slice(0, 4)}${'*'.repeat(Math.min(value.length - 8, 20))}${value.slice(-4)}`
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
// Where a key lives, so an edit to config.json is not silently shadowed by an
|
|
1982
|
+
// environment variable. Reporting a value without its origin is the whole
|
|
1983
|
+
// reason users conclude "config didn't work".
|
|
1984
|
+
const LLM_SOURCE_LABELS = {
|
|
1985
|
+
env: 'environment variable',
|
|
1986
|
+
config: CONFIG_FILE,
|
|
1987
|
+
default: 'built-in default',
|
|
1988
|
+
unset: 'not set',
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
function configShowCommand(useJson) {
|
|
1992
|
+
const config = readConfig()
|
|
1993
|
+
const stored = config.llm && typeof config.llm === 'object' ? config.llm : {}
|
|
1994
|
+
const effective = llmConfig()
|
|
1995
|
+
const sources = llmConfigSources()
|
|
1996
|
+
|
|
1997
|
+
if (useJson) {
|
|
1998
|
+
return jsonOrText(
|
|
1999
|
+
{
|
|
2000
|
+
home: MIO_HOME,
|
|
2001
|
+
configFile: CONFIG_FILE,
|
|
2002
|
+
exists: fs.existsSync(CONFIG_FILE),
|
|
2003
|
+
llm: {
|
|
2004
|
+
// The effective values (what a call would actually use) ...
|
|
2005
|
+
effective: {
|
|
2006
|
+
apiUrl: effective.apiUrl,
|
|
2007
|
+
apiKey: maskSecret(effective.apiKey),
|
|
2008
|
+
hasApiKey: Boolean(effective.apiKey),
|
|
2009
|
+
model: effective.model,
|
|
2010
|
+
},
|
|
2011
|
+
// ... and where each one came from.
|
|
2012
|
+
sources,
|
|
2013
|
+
// What is stored on disk, independent of any env override.
|
|
2014
|
+
stored: {
|
|
2015
|
+
apiUrl: stored.apiUrl || '',
|
|
2016
|
+
apiKey: maskSecret(stored.apiKey),
|
|
2017
|
+
hasApiKey: Boolean(stored.apiKey),
|
|
2018
|
+
model: stored.model || '',
|
|
2019
|
+
},
|
|
2020
|
+
configured: isLlmConfigured(),
|
|
2021
|
+
},
|
|
2022
|
+
},
|
|
2023
|
+
true
|
|
2024
|
+
)
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
console.log(`MIO_HOME: ${MIO_HOME}`)
|
|
2028
|
+
console.log(`config file: ${CONFIG_FILE}${fs.existsSync(CONFIG_FILE) ? '' : ' (not created yet)'}`)
|
|
2029
|
+
console.log('')
|
|
2030
|
+
console.log('LLM (effective):')
|
|
2031
|
+
console.log(` apiUrl: ${effective.apiUrl} [${LLM_SOURCE_LABELS[sources.apiUrl]}]`)
|
|
2032
|
+
console.log(
|
|
2033
|
+
` apiKey: ${effective.apiKey ? `${maskSecret(effective.apiKey)} [${LLM_SOURCE_LABELS[sources.apiKey]}]` : '(none / unauthenticated)'}`
|
|
2034
|
+
)
|
|
2035
|
+
console.log(` model: ${effective.model} [${LLM_SOURCE_LABELS[sources.model]}]`)
|
|
2036
|
+
console.log('')
|
|
2037
|
+
|
|
2038
|
+
const storedKeys = ['apiUrl', 'apiKey', 'model'].filter((k) => stored[k])
|
|
2039
|
+
if (storedKeys.length > 0) {
|
|
2040
|
+
console.log(`Stored in config.json: ${storedKeys.join(', ')}`)
|
|
2041
|
+
if (sources.apiUrl === 'env' || sources.apiKey === 'env' || sources.model === 'env') {
|
|
2042
|
+
console.log('⚠️ An environment variable currently overrides part of this — the shell wins over the file.')
|
|
2043
|
+
}
|
|
2044
|
+
} else {
|
|
2045
|
+
console.log('Nothing stored in config.json yet. Set it with:')
|
|
2046
|
+
console.log(' mio config llm --url <endpoint> --model <id> [--key <token>]')
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
if (!isLlmConfigured()) {
|
|
2050
|
+
console.log('')
|
|
2051
|
+
console.log('No LLM configured — `mio creativity generate` / `ferment` and `mio insight generate`')
|
|
2052
|
+
console.log('would call the shared default endpoint instead of yours.')
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
function configLlmCommand(flags, useJson) {
|
|
2057
|
+
const url = optionValue(flags, '--url')
|
|
2058
|
+
const key = optionValue(flags, '--key')
|
|
2059
|
+
const model = optionValue(flags, '--model')
|
|
2060
|
+
const clearAll = flagPresent(flags, '--clear')
|
|
2061
|
+
|
|
2062
|
+
// Validate before writing: a typo here would otherwise fail much later, at the
|
|
2063
|
+
// first real call, with a far less useful error.
|
|
2064
|
+
if (url !== undefined) {
|
|
2065
|
+
let parsed
|
|
2066
|
+
try {
|
|
2067
|
+
parsed = new URL(url)
|
|
2068
|
+
} catch (_) {
|
|
2069
|
+
throw new Error(`--url must be a valid URL (got: ${url})`)
|
|
2070
|
+
}
|
|
2071
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
2072
|
+
throw new Error(`--url must be http(s) (got: ${parsed.protocol})`)
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
const config = readConfig()
|
|
2077
|
+
const before = config.llm && typeof config.llm === 'object' ? { ...config.llm } : {}
|
|
2078
|
+
|
|
2079
|
+
if (clearAll) {
|
|
2080
|
+
delete config.llm
|
|
2081
|
+
writeConfig(config)
|
|
2082
|
+
resetLlmConfigCache()
|
|
2083
|
+
if (useJson) return jsonOrText({ cleared: true, llm: null }, true)
|
|
2084
|
+
console.log('Cleared the stored LLM config. Environment variables (if any) still apply.')
|
|
2085
|
+
return
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
if (url === undefined && key === undefined && model === undefined) {
|
|
2089
|
+
console.error('Nothing to set. Pass at least one of --url / --key / --model, or --clear.')
|
|
2090
|
+
console.error('Example: mio config llm --url http://localhost:11434/v1/chat/completions --model qwen2.5:14b')
|
|
2091
|
+
process.exitCode = 1
|
|
2092
|
+
return
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
const llm = { ...before }
|
|
2096
|
+
if (url !== undefined) llm.apiUrl = url
|
|
2097
|
+
if (key !== undefined) llm.apiKey = key
|
|
2098
|
+
if (model !== undefined) llm.model = model
|
|
2099
|
+
config.llm = llm
|
|
2100
|
+
writeConfig(config)
|
|
2101
|
+
resetLlmConfigCache()
|
|
2102
|
+
|
|
2103
|
+
const changed = ['apiUrl', 'apiKey', 'model'].filter((k) => llm[k] !== before[k])
|
|
2104
|
+
|
|
2105
|
+
if (useJson) {
|
|
2106
|
+
return jsonOrText(
|
|
2107
|
+
{
|
|
2108
|
+
updated: changed,
|
|
2109
|
+
llm: {
|
|
2110
|
+
apiUrl: llm.apiUrl || '',
|
|
2111
|
+
apiKey: maskSecret(llm.apiKey),
|
|
2112
|
+
hasApiKey: Boolean(llm.apiKey),
|
|
2113
|
+
model: llm.model || '',
|
|
2114
|
+
},
|
|
2115
|
+
configFile: CONFIG_FILE,
|
|
2116
|
+
},
|
|
2117
|
+
true
|
|
2118
|
+
)
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
console.log(`Updated ${CONFIG_FILE}: ${changed.join(', ')}`)
|
|
2122
|
+
if (llm.apiKey) console.log(` apiKey: ${maskSecret(llm.apiKey)}`)
|
|
2123
|
+
if (llm.apiUrl) console.log(` apiUrl: ${llm.apiUrl}`)
|
|
2124
|
+
if (llm.model) console.log(` model: ${llm.model}`)
|
|
2125
|
+
|
|
2126
|
+
// Warn when the value just written will NOT be the one used at call time.
|
|
2127
|
+
const sources = llmConfigSources()
|
|
2128
|
+
const shadowed = changed.filter((k) => sources[k] === 'env')
|
|
2129
|
+
if (shadowed.length > 0) {
|
|
2130
|
+
console.log('')
|
|
2131
|
+
console.log(`⚠️ ${shadowed.join(', ')} is currently overridden by an environment variable —`)
|
|
2132
|
+
console.log(' the shell value wins at call time. Unset it to use what you just stored.')
|
|
2133
|
+
} else {
|
|
2134
|
+
console.log('')
|
|
2135
|
+
console.log('Environment variables (if set) still take precedence at call time.')
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
function configCommand(args, useJson) {
|
|
2140
|
+
const sub = args[1] || 'show'
|
|
2141
|
+
const flags = args.slice(2)
|
|
2142
|
+
|
|
2143
|
+
if (sub === 'show' || sub === 'list') return configShowCommand(useJson)
|
|
2144
|
+
if (sub === 'llm') return configLlmCommand(flags, useJson)
|
|
2145
|
+
|
|
2146
|
+
if (sub === 'path') {
|
|
2147
|
+
if (useJson) return jsonOrText({ home: MIO_HOME, configFile: CONFIG_FILE }, true)
|
|
2148
|
+
console.log(CONFIG_FILE)
|
|
2149
|
+
return
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
console.error(`Unknown config subcommand: ${sub}`)
|
|
2153
|
+
console.error('Usage: mio config show | mio config llm [--url U] [--key K] [--model M] | mio config llm --clear | mio config path')
|
|
2154
|
+
process.exitCode = 1
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
function jsonOrText(payload, useJson) {
|
|
2158
|
+
if (useJson) {
|
|
2159
|
+
console.log(JSON.stringify(payload, null, 2))
|
|
2160
|
+
return
|
|
2161
|
+
}
|
|
2162
|
+
return payload
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
function initCommand() {
|
|
2166
|
+
ensureHome()
|
|
2167
|
+
const config = readConfig()
|
|
2168
|
+
if (!config.createdAt) config.createdAt = new Date().toISOString()
|
|
2169
|
+
writeConfig(config)
|
|
2170
|
+
console.log(`MIO_HOME ready: ${MIO_HOME}`)
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
function statusCommand(useJson) {
|
|
2174
|
+
const config = readConfig()
|
|
2175
|
+
const codexRestart = adapters.codex.restartWarningMessage()
|
|
2176
|
+
const opencodeRestart = adapters.opencode.restartWarningMessage()
|
|
2177
|
+
const installed = {
|
|
2178
|
+
codex: adapters.codex.isInstalled(),
|
|
2179
|
+
opencode: adapters.opencode.isInstalled(),
|
|
2180
|
+
workbuddy: adapters.workbuddy.isInstalled(),
|
|
2181
|
+
hermes: adapters.hermes.isInstalled(),
|
|
2182
|
+
claude: adapters.claude.isInstalled(),
|
|
2183
|
+
}
|
|
2184
|
+
const payload = {
|
|
2185
|
+
version: config.version,
|
|
2186
|
+
home: MIO_HOME,
|
|
2187
|
+
serverScript: SERVER_SCRIPT,
|
|
2188
|
+
serverScriptExists: fs.existsSync(SERVER_SCRIPT),
|
|
2189
|
+
agents: config.agents || {},
|
|
2190
|
+
codexInstalled: installed.codex,
|
|
2191
|
+
opencodeInstalled: installed.opencode,
|
|
2192
|
+
workbuddyInstalled: installed.workbuddy,
|
|
2193
|
+
hermesInstalled: installed.hermes,
|
|
2194
|
+
claudeInstalled: installed.claude,
|
|
2195
|
+
codexNeedsRestart: Boolean(codexRestart),
|
|
2196
|
+
opencodeNeedsRestart: Boolean(opencodeRestart),
|
|
2197
|
+
codexRestartHint: codexRestart,
|
|
2198
|
+
opencodeRestartHint: opencodeRestart,
|
|
2199
|
+
hermesObserverReady: fs.existsSync(observer.hermesDbPath()),
|
|
2200
|
+
claudeObserverReady: fs.existsSync(observer.claudeProjectsDir()),
|
|
2201
|
+
opencodeObserverReady: fs.existsSync(observer.opencodeDbPath()),
|
|
2202
|
+
observerRunning: observer.isRunning(MIO_HOME),
|
|
2203
|
+
observerPid: observer.isRunning(MIO_HOME) ? observer.readPid(MIO_HOME) : null,
|
|
2204
|
+
}
|
|
2205
|
+
if (useJson) {
|
|
2206
|
+
jsonOrText(payload, true)
|
|
2207
|
+
return
|
|
2208
|
+
}
|
|
2209
|
+
console.log(`MIO_HOME: ${MIO_HOME}`)
|
|
2210
|
+
console.log(`MCP Server: ${SERVER_SCRIPT}`)
|
|
2211
|
+
console.log(`Codex MCP: ${installed.codex ? 'installed' : 'not installed'}`)
|
|
2212
|
+
if (codexRestart) console.log(`Codex app: running with an older Mio config - fully restart Codex to load it`)
|
|
2213
|
+
console.log(`OpenCode MCP: ${installed.opencode ? 'installed' : 'not installed'}`)
|
|
2214
|
+
if (opencodeRestart) console.log(`OpenCode Desktop: running with an older Mio config - restart it to load it`)
|
|
2215
|
+
console.log(`OpenCode observer: ${fs.existsSync(observer.opencodeDbPath()) ? 'ready' : 'no database'}`)
|
|
2216
|
+
console.log(`WorkBuddy MCP: ${installed.workbuddy ? 'installed' : 'not installed'}`)
|
|
2217
|
+
console.log(`Hermes MCP: ${installed.hermes ? 'installed' : 'not installed'}`)
|
|
2218
|
+
console.log(`Hermes observer: ${fs.existsSync(observer.hermesDbPath()) ? 'ready' : 'no database'}`)
|
|
2219
|
+
console.log(`Claude Code MCP: ${installed.claude ? 'installed' : 'not installed'}`)
|
|
2220
|
+
console.log(`Claude Code observer: ${fs.existsSync(observer.claudeProjectsDir()) ? 'ready' : 'no transcripts'}`)
|
|
2221
|
+
console.log(`Observer: ${observer.isRunning(MIO_HOME) ? 'running (pid ' + observer.readPid(MIO_HOME) + ')' : 'not running'}`)
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
function agentsCommand(args, useJson) {
|
|
2225
|
+
const sub = args[1]
|
|
2226
|
+
|
|
2227
|
+
// `mio agents` with no subcommand lists installed host adapters (config) —
|
|
2228
|
+
// distinct from the observed-agent telemetry below.
|
|
2229
|
+
if (!sub || sub === 'help' || sub === '--help' || sub === '-h') {
|
|
2230
|
+
const config = readConfig()
|
|
2231
|
+
const agents = config.agents || {}
|
|
2232
|
+
if (useJson) {
|
|
2233
|
+
jsonOrText(agents, true)
|
|
2234
|
+
return
|
|
2235
|
+
}
|
|
2236
|
+
const names = Object.keys(agents)
|
|
2237
|
+
if (names.length === 0) {
|
|
2238
|
+
console.log('No agents installed. Run `mio agents list` for observed agents, or `mio install <host>` to add one.')
|
|
2239
|
+
return
|
|
2240
|
+
}
|
|
2241
|
+
for (const name of names) {
|
|
2242
|
+
console.log(`${name}\t${agents[name].installedAt || ''}`)
|
|
2243
|
+
}
|
|
2244
|
+
return
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
if (sub === 'list') {
|
|
2248
|
+
const flags = args.slice(2)
|
|
2249
|
+
let result
|
|
2250
|
+
try {
|
|
2251
|
+
result = cliAgentStore().listAgents({ project: optionValue(flags, '--project') })
|
|
2252
|
+
} catch (error) {
|
|
2253
|
+
console.error(error.message || error)
|
|
2254
|
+
process.exitCode = 1
|
|
2255
|
+
return
|
|
2256
|
+
}
|
|
2257
|
+
if (useJson) return jsonOrText(result, true)
|
|
2258
|
+
return printAgentList(result)
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
if (sub === 'report') {
|
|
2262
|
+
const flags = args.slice(2)
|
|
2263
|
+
let result
|
|
2264
|
+
try {
|
|
2265
|
+
result = cliAgentStore().reportAgent({
|
|
2266
|
+
agentId: optionValue(flags, '--agent'),
|
|
2267
|
+
project: optionValue(flags, '--project'),
|
|
2268
|
+
})
|
|
2269
|
+
} catch (error) {
|
|
2270
|
+
console.error(error.message || error)
|
|
2271
|
+
process.exitCode = 1
|
|
2272
|
+
return
|
|
2273
|
+
}
|
|
2274
|
+
if (useJson) return jsonOrText(result, true)
|
|
2275
|
+
return printAgentReport(result)
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
if (sub === 'register') {
|
|
2279
|
+
const flags = args.slice(2)
|
|
2280
|
+
const agentId = optionValue(flags, '--agent-id')
|
|
2281
|
+
if (!agentId) {
|
|
2282
|
+
console.error('mio agents register requires --agent-id <id>')
|
|
2283
|
+
process.exitCode = 1
|
|
2284
|
+
return
|
|
2285
|
+
}
|
|
2286
|
+
const project = optionValue(flags, '--project') || projectName()
|
|
2287
|
+
const hostType = optionValue(flags, '--host-type') || 'mcp'
|
|
2288
|
+
const capabilities = splitTagsOption(flags, '--capabilities')
|
|
2289
|
+
|
|
2290
|
+
// Preview by default, like `mio memory archive`: nothing is written
|
|
2291
|
+
// without --yes, in --json mode either.
|
|
2292
|
+
if (!flagPresent(flags, '--yes')) {
|
|
2293
|
+
const existing = cliAgentStore()
|
|
2294
|
+
.listAgents({ project })
|
|
2295
|
+
.agents.find((a) => a.agentId === agentId)
|
|
2296
|
+
if (useJson) {
|
|
2297
|
+
jsonOrText(
|
|
2298
|
+
{
|
|
2299
|
+
preview: true,
|
|
2300
|
+
applied: false,
|
|
2301
|
+
agentId,
|
|
2302
|
+
project,
|
|
2303
|
+
hostType,
|
|
2304
|
+
capabilities,
|
|
2305
|
+
willCreate: !existing,
|
|
2306
|
+
existingSessionCount: existing ? existing.sessionCount || 0 : null,
|
|
2307
|
+
hint: 'Re-run with --yes to apply.',
|
|
2308
|
+
},
|
|
2309
|
+
true,
|
|
2310
|
+
)
|
|
2311
|
+
} else {
|
|
2312
|
+
console.log(`Register preview: ${agentId} (project=${project}, host=${hostType})`)
|
|
2313
|
+
if (existing) {
|
|
2314
|
+
const next = (existing.sessionCount || 0) + 1
|
|
2315
|
+
console.log(` will update existing agent (sessionCount ${existing.sessionCount || 0} -> ${next})`)
|
|
2316
|
+
} else {
|
|
2317
|
+
console.log(' will create a new agent record')
|
|
2318
|
+
}
|
|
2319
|
+
if (capabilities.length > 0) console.log(` capabilities: ${capabilities.join(', ')}`)
|
|
2320
|
+
console.log('Re-run with --yes to apply.')
|
|
2321
|
+
}
|
|
2322
|
+
process.exitCode = 1
|
|
2323
|
+
return
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
let result
|
|
2327
|
+
try {
|
|
2328
|
+
result = cliAgentStore().registerAgent({ agentId, project, hostType, capabilities })
|
|
2329
|
+
} catch (error) {
|
|
2330
|
+
console.error(error.message || error)
|
|
2331
|
+
process.exitCode = 1
|
|
2332
|
+
return
|
|
2333
|
+
}
|
|
2334
|
+
if (useJson) return jsonOrText(result, true)
|
|
2335
|
+
console.log(`Registered ${result.agentId} (project=${project})`)
|
|
2336
|
+
console.log(
|
|
2337
|
+
` ${result.registered ? 'created' : `updated (session ${result.sessionCount})`} | host=${result.hostType}`,
|
|
2338
|
+
)
|
|
2339
|
+
if (result.capabilities && result.capabilities.length > 0) {
|
|
2340
|
+
console.log(` capabilities: ${result.capabilities.join(', ')}`)
|
|
2341
|
+
}
|
|
2342
|
+
return
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
console.error(`Unknown agents subcommand: ${sub}`)
|
|
2346
|
+
console.error('Usage: mio agents [list|register|report] [--agent X] [--project Y]')
|
|
2347
|
+
process.exitCode = 1
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
function printAgentList(result) {
|
|
2351
|
+
console.log(`Observed agents: ${result.count} shown${result.project ? ` (project=${result.project})` : ''}`)
|
|
2352
|
+
if (result.count === 0) {
|
|
2353
|
+
console.log('No observed agents match.')
|
|
2354
|
+
return
|
|
2355
|
+
}
|
|
2356
|
+
for (const a of result.agents) {
|
|
2357
|
+
const caps = Array.isArray(a.capabilities) && a.capabilities.length > 0 ? ` [${a.capabilities.join(', ')}]` : ''
|
|
2358
|
+
console.log(`${a.agentId} (${a.hostType})${caps} project=${a.project || '-'}`)
|
|
2359
|
+
console.log(
|
|
2360
|
+
` sessions=${a.sessionCount || 0} tasks=${a.taskCount} success=${a.successCount} failure=${a.failureCount} | ${a.id}`,
|
|
2361
|
+
)
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
function printAgentReport(result) {
|
|
2366
|
+
console.log(`Agent report${result.project ? ` (project=${result.project})` : ''}: ${result.count} agent(s)`)
|
|
2367
|
+
if (result.count === 0) {
|
|
2368
|
+
console.log('No observed agents match.')
|
|
2369
|
+
return
|
|
2370
|
+
}
|
|
2371
|
+
for (const r of result.reports) {
|
|
2372
|
+
const o = r.taskOutcomes
|
|
2373
|
+
const when = r.lastSeenAt ? r.lastSeenAt.slice(0, 19).replace('T', ' ') : 'unknown'
|
|
2374
|
+
console.log(`${r.agentId} (${r.hostType}) ${r.active ? 'active' : 'idle'}`)
|
|
2375
|
+
console.log(
|
|
2376
|
+
` tasks: ${o.total} total, ${o.success} success, ${o.failure} failure (${o.successRate}% success)`,
|
|
2377
|
+
)
|
|
2378
|
+
console.log(` memories: ${r.memories} experience reuses: ${r.experienceReuses.total} (verified ${r.experienceReuses.verified})`)
|
|
2379
|
+
console.log(` last seen: ${when} | sessions=${r.sessionCount}`)
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
function phase0Command(args, useJson) {
|
|
2384
|
+
const sub = args[1]
|
|
2385
|
+
if (!sub || sub === 'help' || sub === '--help' || sub === '-h') {
|
|
2386
|
+
console.log(`Usage:
|
|
2387
|
+
mio phase0 report Show the Phase 0 validation report (memory/trace/reuse evidence)
|
|
2388
|
+
mio phase0 report --format markdown Render as Markdown
|
|
2389
|
+
mio phase0 report --json Machine-readable report (same shape as mio.phase0.report)
|
|
2390
|
+
|
|
2391
|
+
Options:
|
|
2392
|
+
--project name Project filter (defaults to the current directory name)
|
|
2393
|
+
--format markdown Human-readable Markdown instead of JSON`)
|
|
2394
|
+
if (!sub) process.exitCode = 1
|
|
2395
|
+
return
|
|
2396
|
+
}
|
|
2397
|
+
if (sub !== 'report') {
|
|
2398
|
+
console.error(`Unknown phase0 subcommand: ${sub}`)
|
|
2399
|
+
process.exitCode = 1
|
|
2400
|
+
return
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
const flags = args.slice(2)
|
|
2404
|
+
const format = (optionValue(flags, '--format') || '').toLowerCase()
|
|
2405
|
+
let report
|
|
2406
|
+
try {
|
|
2407
|
+
report = loadPhase0(MIO_HOME, optionValue(flags, '--project') || projectName())
|
|
2408
|
+
} catch (error) {
|
|
2409
|
+
console.error(error.message || error)
|
|
2410
|
+
process.exitCode = 1
|
|
2411
|
+
return
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
if (useJson) {
|
|
2415
|
+
return jsonOrText({ project: optionValue(flags, '--project') || projectName(), ...report }, true)
|
|
2416
|
+
}
|
|
2417
|
+
// Markdown is the human-readable form; the MCP tool exposes the same renderer
|
|
2418
|
+
// via --format markdown, so text and MCP output cannot diverge.
|
|
2419
|
+
console.log(renderPhase0Markdown(report))
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
function hostCommand(args, useJson) {
|
|
2423
|
+
const sub = args[1]
|
|
2424
|
+
if (!sub || sub === 'help' || sub === '--help' || sub === '-h') {
|
|
2425
|
+
console.log(`Usage:
|
|
2426
|
+
mio host capabilities Show what each host supports and whether it is installed
|
|
2427
|
+
mio host capabilities --json Machine-readable (same shape as mio.host.capabilities)
|
|
2428
|
+
|
|
2429
|
+
Notes:
|
|
2430
|
+
"installed" is probed live from each adapter, so it reflects this machine right
|
|
2431
|
+
now -- unlike \`mio install <host>\`, which only records intent in config.json.`)
|
|
2432
|
+
if (!sub) process.exitCode = 1
|
|
2433
|
+
return
|
|
2434
|
+
}
|
|
2435
|
+
if (sub !== 'capabilities') {
|
|
2436
|
+
console.error(`Unknown host subcommand: ${sub}`)
|
|
2437
|
+
process.exitCode = 1
|
|
2438
|
+
return
|
|
2439
|
+
}
|
|
2440
|
+
|
|
2441
|
+
const result = listHostCapabilities()
|
|
2442
|
+
if (useJson) return jsonOrText(result, true)
|
|
2443
|
+
|
|
2444
|
+
console.log(`Host capabilities: ${result.hosts.length} host(s)`)
|
|
2445
|
+
for (const host of result.hosts) {
|
|
2446
|
+
const mark = host.installed ? 'installed' : 'not installed'
|
|
2447
|
+
console.log(` ${host.name.padEnd(10)} ${mark}`)
|
|
2448
|
+
console.log(` ${host.capabilities.join(', ')}`)
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2452
|
+
function recordOutcomeCommand(args, useJson) {
|
|
2453
|
+
const flags = args.slice(2)
|
|
2454
|
+
const outcome = String(optionValue(flags, '--outcome') || '').toLowerCase()
|
|
2455
|
+
if (!['success', 'failure', 'aborted'].includes(outcome)) {
|
|
2456
|
+
console.error('mio task record-outcome requires --outcome success|failure|aborted')
|
|
2457
|
+
process.exitCode = 1
|
|
2458
|
+
return
|
|
2459
|
+
}
|
|
2460
|
+
const project = optionValue(flags, '--project') || projectName()
|
|
2461
|
+
// Mirrors the store default so the preview reports the same agent that the
|
|
2462
|
+
// applied run would use.
|
|
2463
|
+
const agentId = optionValue(flags, '--agent-id') || 'cli'
|
|
2464
|
+
const task = optionValue(flags, '--task') || ''
|
|
2465
|
+
const summary = optionValue(flags, '--summary') || ''
|
|
2466
|
+
const verification = optionValue(flags, '--verification') || ''
|
|
2467
|
+
const traceId = optionValue(flags, '--trace-id') || ''
|
|
2468
|
+
|
|
2469
|
+
// Preview by default, like every other CLI write (mio memory archive,
|
|
2470
|
+
// mio agents register): nothing is written without --yes, --json included.
|
|
2471
|
+
if (!flagPresent(flags, '--yes')) {
|
|
2472
|
+
const existing = cliAgentStore()
|
|
2473
|
+
.listAgents({ project })
|
|
2474
|
+
.agents.find((a) => a.agentId === agentId)
|
|
2475
|
+
if (useJson) {
|
|
2476
|
+
jsonOrText(
|
|
2477
|
+
{
|
|
2478
|
+
preview: true,
|
|
2479
|
+
applied: false,
|
|
2480
|
+
outcome,
|
|
2481
|
+
agentId,
|
|
2482
|
+
project,
|
|
2483
|
+
task,
|
|
2484
|
+
summary,
|
|
2485
|
+
verification,
|
|
2486
|
+
agentWillUpdate: !!existing,
|
|
2487
|
+
currentTaskCount: existing ? existing.taskCount || 0 : null,
|
|
2488
|
+
hint: 'Re-run with --yes to apply.',
|
|
2489
|
+
},
|
|
2490
|
+
true,
|
|
2491
|
+
)
|
|
2492
|
+
} else {
|
|
2493
|
+
console.log(`Record outcome preview: ${outcome} (project=${project}, agent=${agentId})`)
|
|
2494
|
+
if (task) console.log(` task: ${task}`)
|
|
2495
|
+
if (summary) console.log(` summary: ${summary}`)
|
|
2496
|
+
if (existing) {
|
|
2497
|
+
const nextTasks = (existing.taskCount || 0) + 1
|
|
2498
|
+
const nextSuccess = (existing.successCount || 0) + (outcome === 'success' ? 1 : 0)
|
|
2499
|
+
console.log(
|
|
2500
|
+
` will update agent (taskCount ${existing.taskCount || 0} -> ${nextTasks}, successCount ${existing.successCount || 0} -> ${nextSuccess})`,
|
|
2501
|
+
)
|
|
2502
|
+
} else {
|
|
2503
|
+
console.log(` agent not registered: trace only, agent registry untouched`)
|
|
2504
|
+
console.log(` register it with: mio agents register --agent-id ${agentId} --yes`)
|
|
2505
|
+
}
|
|
2506
|
+
console.log('Re-run with --yes to apply.')
|
|
2507
|
+
}
|
|
2508
|
+
process.exitCode = 1
|
|
2509
|
+
return
|
|
2510
|
+
}
|
|
2511
|
+
|
|
2512
|
+
let result
|
|
2513
|
+
try {
|
|
2514
|
+
result = cliTaskStore().recordTaskOutcome({
|
|
2515
|
+
outcome,
|
|
2516
|
+
agentId,
|
|
2517
|
+
project,
|
|
2518
|
+
task,
|
|
2519
|
+
summary,
|
|
2520
|
+
verification,
|
|
2521
|
+
traceId,
|
|
2522
|
+
})
|
|
2523
|
+
} catch (error) {
|
|
2524
|
+
console.error(error.message || error)
|
|
2525
|
+
process.exitCode = 1
|
|
2526
|
+
return
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
if (useJson) return jsonOrText(result, true)
|
|
2530
|
+
console.log(`Recorded ${result.event.outcome} for ${result.event.agent} (project=${result.event.project})`)
|
|
2531
|
+
console.log(` trace: ${result.event.trace_id}`)
|
|
2532
|
+
console.log(` agent registry: ${result.agentUpdated ? 'updated' : 'not updated (agent not registered)'}`)
|
|
2533
|
+
if (result.autoClaims) console.log(` auto-claims: ${result.autoClaims.length}`)
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2536
|
+
function taskCommand(args, useJson) {
|
|
2537
|
+
const sub = args[1]
|
|
2538
|
+
if (!sub || sub === 'help' || sub === '--help' || sub === '-h') {
|
|
2539
|
+
console.log(`Usage:
|
|
2540
|
+
mio task route "<task>" Which verified experiences apply to this task
|
|
2541
|
+
mio task route "<task>" --json Machine-readable (same shape as mio.task.route)
|
|
2542
|
+
|
|
2543
|
+
Options:
|
|
2544
|
+
--project name Project filter (defaults to the current directory name)
|
|
2545
|
+
--scope all|project|global Memory scope to consider
|
|
2546
|
+
--limit N Max routes (1-10, default 5)
|
|
2547
|
+
|
|
2548
|
+
This is read-only: unlike the MCP call it does not write to the query log.`)
|
|
2549
|
+
if (!sub) process.exitCode = 1
|
|
2550
|
+
return
|
|
2551
|
+
}
|
|
2552
|
+
if (sub === 'record-outcome' || sub === 'record_outcome') {
|
|
2553
|
+
return recordOutcomeCommand(args, useJson)
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2556
|
+
if (sub !== 'route') {
|
|
2557
|
+
console.error(`Unknown task subcommand: ${sub}`)
|
|
2558
|
+
process.exitCode = 1
|
|
2559
|
+
return
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2562
|
+
const flags = args.slice(2)
|
|
2563
|
+
// The task is free text and may be quoted as one argument or passed as
|
|
2564
|
+
// several words. Only flags this command understands are treated as options.
|
|
2565
|
+
const TASK_KNOWN_FLAGS = new Set(['--project', '--scope', '--limit', '--json'])
|
|
2566
|
+
const taskParts = []
|
|
2567
|
+
for (let i = 0; i < flags.length; i += 1) {
|
|
2568
|
+
const token = flags[i]
|
|
2569
|
+
if (TASK_KNOWN_FLAGS.has(token)) continue
|
|
2570
|
+
if (i > 0 && TASK_KNOWN_FLAGS.has(flags[i - 1])) continue
|
|
2571
|
+
taskParts.push(token)
|
|
2572
|
+
}
|
|
2573
|
+
const task = taskParts.join(' ').trim()
|
|
2574
|
+
if (!task) {
|
|
2575
|
+
console.error('mio task route requires a task, e.g. mio task route "publish the npm package"')
|
|
2576
|
+
process.exitCode = 1
|
|
2577
|
+
return
|
|
2578
|
+
}
|
|
2579
|
+
|
|
2580
|
+
let result
|
|
2581
|
+
try {
|
|
2582
|
+
result = cliTaskStore().routeTask({
|
|
2583
|
+
task,
|
|
2584
|
+
project: optionValue(flags, '--project'),
|
|
2585
|
+
scope: optionValue(flags, '--scope'),
|
|
2586
|
+
limit: parseNumberOption(flags, '--limit'),
|
|
2587
|
+
persistQuery: false,
|
|
2588
|
+
})
|
|
2589
|
+
} catch (error) {
|
|
2590
|
+
console.error(error.message || error)
|
|
2591
|
+
process.exitCode = 1
|
|
2592
|
+
return
|
|
2593
|
+
}
|
|
2594
|
+
|
|
2595
|
+
if (useJson) return jsonOrText(result, true)
|
|
2596
|
+
printTaskRoute(result)
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
function printTaskRoute(result) {
|
|
2600
|
+
console.log(`Task route: "${result.task}" (project=${result.project || 'all'}, scope=${result.scope})`)
|
|
2601
|
+
console.log(`verified routes: ${result.count}`)
|
|
2602
|
+
|
|
2603
|
+
if (result.count > 0) {
|
|
2604
|
+
console.log('')
|
|
2605
|
+
console.log('Routes (apply the top match first):')
|
|
2606
|
+
result.routes.forEach((route, index) => {
|
|
2607
|
+
const via = route.sourceAgents.length > 0 ? route.sourceAgents.join(', ') : '?'
|
|
2608
|
+
const to = route.targetAgents.length > 0 ? route.targetAgents.join(', ') : '?'
|
|
2609
|
+
console.log(
|
|
2610
|
+
`${index + 1}. [score ${route.score}] ${route.experienceId} — reused ${route.reuseCount}x${route.confirmed ? ' (confirmed)' : ''}`,
|
|
2611
|
+
)
|
|
2612
|
+
console.log(` ${String(route.memory.content).replace(/\s+/g, ' ').slice(0, 110)}`)
|
|
2613
|
+
console.log(` ${via} -> ${to}`)
|
|
2614
|
+
})
|
|
2615
|
+
}
|
|
2616
|
+
|
|
2617
|
+
if (result.relatedMemories.length > 0) {
|
|
2618
|
+
console.log('')
|
|
2619
|
+
console.log('Related memories:')
|
|
2620
|
+
for (const record of result.relatedMemories) {
|
|
2621
|
+
console.log(`- ${record.id} ${String(record.content).replace(/\s+/g, ' ').slice(0, 100)}`)
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2625
|
+
if (result.summary && result.summary.routingSignal) {
|
|
2626
|
+
console.log(`\nAgent health: ${result.summary.routingSignal}`)
|
|
2627
|
+
}
|
|
2628
|
+
if (result.summary && result.summary.suggestion) {
|
|
2629
|
+
console.log(`\n${result.summary.suggestion}`)
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
|
|
2633
|
+
function installCommand(host, useJson) {
|
|
2634
|
+
if (!host) {
|
|
2635
|
+
console.error('Usage: mio install <codex|opencode|workbuddy|hermes|claude>')
|
|
2636
|
+
process.exitCode = 1
|
|
2637
|
+
return
|
|
2638
|
+
}
|
|
2639
|
+
const adapter = adapters[host]
|
|
2640
|
+
if (!adapter) {
|
|
2641
|
+
console.error(`Unknown host: ${host}`)
|
|
2642
|
+
process.exitCode = 1
|
|
2643
|
+
return
|
|
2644
|
+
}
|
|
2645
|
+
ensureHome()
|
|
2646
|
+
const config = readConfig()
|
|
2647
|
+
let result
|
|
2648
|
+
try {
|
|
2649
|
+
result = adapter.install({
|
|
2650
|
+
node: process.execPath,
|
|
2651
|
+
serverScript: SERVER_SCRIPT,
|
|
2652
|
+
home: MIO_HOME,
|
|
2653
|
+
workspace: process.cwd(),
|
|
2654
|
+
})
|
|
2655
|
+
} catch (error) {
|
|
2656
|
+
console.error(`Failed to install ${host}: ${error.message || error}`)
|
|
2657
|
+
process.exitCode = 1
|
|
2658
|
+
return
|
|
2659
|
+
}
|
|
2660
|
+
if (result.changed) {
|
|
2661
|
+
config.agents = config.agents || {}
|
|
2662
|
+
config.agents[host] = {
|
|
2663
|
+
installedAt: new Date().toISOString(),
|
|
2664
|
+
configPath: result.configPath || null,
|
|
2665
|
+
}
|
|
2666
|
+
writeConfig(config)
|
|
2667
|
+
}
|
|
2668
|
+
try {
|
|
2669
|
+
observer.runCycle(MIO_HOME, observer.loadState(MIO_HOME), observer.createSinks(MIO_HOME))
|
|
2670
|
+
} catch (_) {}
|
|
2671
|
+
let observerStarted = false
|
|
2672
|
+
try {
|
|
2673
|
+
observerStarted = observer.startBackground(MIO_HOME).started
|
|
2674
|
+
} catch (_) {}
|
|
2675
|
+
if (useJson) {
|
|
2676
|
+
result.observer = {
|
|
2677
|
+
running: observer.isRunning(MIO_HOME),
|
|
2678
|
+
started: observerStarted,
|
|
2679
|
+
}
|
|
2680
|
+
jsonOrText(result, true)
|
|
2681
|
+
return
|
|
2682
|
+
}
|
|
2683
|
+
console.log(result.message || `${host} adapter completed.`)
|
|
2684
|
+
console.log(
|
|
2685
|
+
observer.isRunning(MIO_HOME)
|
|
2686
|
+
? 'Observer daemon running.'
|
|
2687
|
+
: 'Observer daemon not running. Run `mio observe --start`.'
|
|
2688
|
+
)
|
|
2689
|
+
}
|
|
2690
|
+
|
|
2691
|
+
function mcpCommand() {
|
|
2692
|
+
require(SERVER_SCRIPT)
|
|
2693
|
+
}
|
|
2694
|
+
|
|
2695
|
+
function observeCommand(args) {
|
|
2696
|
+
const flags = args.slice(1)
|
|
2697
|
+
const useJson = flags.includes('--json')
|
|
2698
|
+
if (flags.includes('--start')) {
|
|
2699
|
+
const result = observer.startBackground(MIO_HOME)
|
|
2700
|
+
if (useJson) return jsonOrText(result, true)
|
|
2701
|
+
console.log(result.started ? 'Observer started.' : 'Observer already running.')
|
|
2702
|
+
return
|
|
2703
|
+
}
|
|
2704
|
+
if (flags.includes('--stop')) {
|
|
2705
|
+
const result = observer.stopBackground(MIO_HOME)
|
|
2706
|
+
if (useJson) return jsonOrText(result, true)
|
|
2707
|
+
console.log(result.stopped ? 'Observer stopped.' : 'Observer was not running.')
|
|
2708
|
+
return
|
|
2709
|
+
}
|
|
2710
|
+
if (flags.includes('--status')) {
|
|
2711
|
+
const running = observer.isRunning(MIO_HOME)
|
|
2712
|
+
const pid = observer.readPid(MIO_HOME)
|
|
2713
|
+
if (useJson) return jsonOrText({ running, pid }, true)
|
|
2714
|
+
console.log(running ? 'Observer: running (pid ' + pid + ')' : 'Observer: not running')
|
|
2715
|
+
return
|
|
2716
|
+
}
|
|
2717
|
+
if (flags.includes('--once')) {
|
|
2718
|
+
const res = observer.runCycle(
|
|
2719
|
+
MIO_HOME,
|
|
2720
|
+
observer.loadState(MIO_HOME),
|
|
2721
|
+
observer.createSinks(MIO_HOME)
|
|
2722
|
+
)
|
|
2723
|
+
if (useJson) return jsonOrText(res, true)
|
|
2724
|
+
console.log('Observed files=' + res.files + ' lines=' + res.lines + ' outcomes=' + res.outcomes)
|
|
2725
|
+
return
|
|
2726
|
+
}
|
|
2727
|
+
console.log('Observing WorkBuddy transcripts into ' + MIO_HOME + ' (Ctrl+C to stop)...')
|
|
2728
|
+
observer.startForeground(MIO_HOME)
|
|
2729
|
+
}
|
|
2730
|
+
|
|
2731
|
+
function help() {
|
|
2732
|
+
console.log(`Mio Agent Runtime CLI
|
|
2733
|
+
|
|
2734
|
+
Usage:
|
|
2735
|
+
mio init Initialize MIO_HOME
|
|
2736
|
+
mio config show Show effective settings and where each value comes from
|
|
2737
|
+
mio config llm --url U --model M [--key K] Store LLM endpoint/key/model in config.json
|
|
2738
|
+
mio config llm --clear Remove the stored LLM config (env vars still apply)
|
|
2739
|
+
mio config path Print the config.json path
|
|
2740
|
+
mio mcp Start Mio MCP server (stdio)
|
|
2741
|
+
mio install <host> Install Mio into a host (codex|opencode|workbuddy|hermes|claude)
|
|
2742
|
+
mio status Show runtime and adapter status
|
|
2743
|
+
mio agents List installed host adapters
|
|
2744
|
+
mio agents list List observed agents (from agents.jsonl, --project X)
|
|
2745
|
+
mio agents report Report per-agent task/memory/reuse telemetry (--agent X, --project Y)
|
|
2746
|
+
mio agents register --agent-id X Register an observed agent (--yes to apply; previews by default)
|
|
2747
|
+
mio evolution status Show composed evolution module health
|
|
2748
|
+
mio evolution report Cross-agent evolution report: ecosystem, agents, memory health, suggestions (--period 24h|7d|30d|all)
|
|
2749
|
+
mio evolution shadow record Record a shadow comparison sample
|
|
2750
|
+
mio evolution dual-write record Record a dual-write comparison sample
|
|
2751
|
+
mio evolution cutover readiness Assess shadow/dual-write cutover readiness
|
|
2752
|
+
mio evolution authority plan Preview a gated authority switch plan
|
|
2753
|
+
mio evolution migration plan Preview state migration diffs
|
|
2754
|
+
mio evolution cutover apply --dry-run Dry-run a cutover plan without switching authority
|
|
2755
|
+
mio observe Watch WorkBuddy transcripts and auto-ingest task outcomes
|
|
2756
|
+
mio observe --start|--stop|--status|--once Manage the background observer daemon
|
|
2757
|
+
mio recall "<query>" Search Mio memory from the terminal (same ranking as mio.memory.query)
|
|
2758
|
+
mio traces Show recent observer traces (--type/--outcome/--agent/--since/--limit/--compact)
|
|
2759
|
+
mio remember "<content>" Write a memory record from the terminal (same schema as mio.memory.record)
|
|
2760
|
+
mio memory analyze Report duplicates, low-quality records and kind histogram
|
|
2761
|
+
mio memory archive --ids a,b Archive records (soft delete; --yes required to apply)
|
|
2762
|
+
mio memory forget --ids a,b PERMANENTLY delete records (--yes required; not undoable)
|
|
2763
|
+
mio memory restore --ids a,b Un-archive previously archived records
|
|
2764
|
+
mio memory merge --ids a,b Merge duplicates into one survivor (--yes required to apply)
|
|
2765
|
+
mio memory migrate --ids a,b --scope global|project Move records between layers
|
|
2766
|
+
mio experience list List experience reuse (--status pending|confirmed|verified)
|
|
2767
|
+
mio experience confirm --ids a,b Confirm auto-claimed reuse (bulk supported)
|
|
2768
|
+
mio experience reuse --source-agent A --target-agent B --experience-id X Record a reuse
|
|
2769
|
+
mio creativity status Show creativity hypothesis counts and recent top ideas
|
|
2770
|
+
mio creativity list List creativity hypotheses (--status active|validated|rejected|draft, --limit N)
|
|
2771
|
+
mio creativity generate Generate hypotheses from 2+ --source "name|content" (calls an LLM)
|
|
2772
|
+
mio creativity ferment Review and refine active hypotheses (calls an LLM)
|
|
2773
|
+
mio insight status Insight counts: total, reported, unreported, high-value
|
|
2774
|
+
mio insight list List insights (--unreported, --min-score N, --detector X, --limit N)
|
|
2775
|
+
mio insight mark-reported Mark insights as reported (--ids a,b)
|
|
2776
|
+
mio observer <view> Observer pipeline views (research pipeline, not the observe daemon):
|
|
2777
|
+
status | world-model | trends | research | insights | essays | dag
|
|
2778
|
+
mio observer ingest --trace-id T --event-type E Record a trace event (--payload/--outcome)
|
|
2779
|
+
mio observer subscribe --event-types a,b Subscribe to events (--yes to apply)
|
|
2780
|
+
mio observer digest New events since the last digest (advances cursor)
|
|
2781
|
+
mio observer collect Fetch from the configured sources (--sources/--keywords/--limit)
|
|
2782
|
+
mio observer ferment Run the fermentation engine (--session morning|afternoon|night)
|
|
2783
|
+
mio phase0 report Show the Phase 0 validation report (--project X, --format markdown)
|
|
2784
|
+
mio host capabilities Show what each host supports and whether it is installed
|
|
2785
|
+
mio task route "<task>" Which verified experiences apply to this task (--project/--scope/--limit)
|
|
2786
|
+
mio task record-outcome --outcome success Record a task outcome (--yes to apply; previews by default)
|
|
2787
|
+
mio policy check "<action>" Check historical risk for an action before running it
|
|
2788
|
+
mio prune --days 30 Trim old traces/queries/reuse records and observe.log (--dry-run to preview; --memory needs --yes)
|
|
2789
|
+
mio digest --days 7 Aggregate traces/memory/reuse into an actionable report (--write-back feeds agent context files; --json)
|
|
2790
|
+
mio --json status Machine-readable status
|
|
2791
|
+
mio --json agents Machine-readable agents
|
|
2792
|
+
mio --json evolution status Machine-readable evolution module health
|
|
2793
|
+
`)
|
|
2794
|
+
}
|
|
2795
|
+
|
|
2796
|
+
async function main() {
|
|
2797
|
+
const args = process.argv.slice(2)
|
|
2798
|
+
const jsonIndex = args.indexOf('--json')
|
|
2799
|
+
const useJson = jsonIndex !== -1
|
|
2800
|
+
if (useJson) args.splice(jsonIndex, 1)
|
|
2801
|
+
const command = args[0] || 'help'
|
|
2802
|
+
|
|
2803
|
+
switch (command) {
|
|
2804
|
+
case 'init':
|
|
2805
|
+
return initCommand()
|
|
2806
|
+
case 'config':
|
|
2807
|
+
return configCommand(args, useJson)
|
|
2808
|
+
case 'mcp':
|
|
2809
|
+
return mcpCommand()
|
|
2810
|
+
case 'install':
|
|
2811
|
+
return installCommand(args[1], useJson)
|
|
2812
|
+
case 'status':
|
|
2813
|
+
return statusCommand(useJson)
|
|
2814
|
+
case 'agents':
|
|
2815
|
+
return agentsCommand(args, useJson)
|
|
2816
|
+
case 'observe':
|
|
2817
|
+
return observeCommand(args)
|
|
2818
|
+
case 'recall':
|
|
2819
|
+
return recallCommand(args, useJson)
|
|
2820
|
+
case 'traces':
|
|
2821
|
+
return tracesCommand(args, useJson)
|
|
2822
|
+
case 'remember':
|
|
2823
|
+
return rememberCommand(args, useJson)
|
|
2824
|
+
case 'memory':
|
|
2825
|
+
return memoryCommand(args, useJson)
|
|
2826
|
+
case 'experience':
|
|
2827
|
+
return experienceCommand(args, useJson)
|
|
2828
|
+
case 'policy':
|
|
2829
|
+
return policyCommand(args, useJson)
|
|
2830
|
+
case 'creativity':
|
|
2831
|
+
return creativityCommand(args, useJson)
|
|
2832
|
+
case 'insight':
|
|
2833
|
+
return insightCommand(args, useJson)
|
|
2834
|
+
case 'observer':
|
|
2835
|
+
return observerCommand(args, useJson)
|
|
2836
|
+
case 'phase0':
|
|
2837
|
+
return phase0Command(args, useJson)
|
|
2838
|
+
case 'host':
|
|
2839
|
+
return hostCommand(args, useJson)
|
|
2840
|
+
case 'task':
|
|
2841
|
+
return taskCommand(args, useJson)
|
|
2842
|
+
case 'prune':
|
|
2843
|
+
return pruneCommand(args, useJson)
|
|
2844
|
+
case 'digest':
|
|
2845
|
+
return digestCommand(args, useJson)
|
|
2846
|
+
case 'evolution':
|
|
2847
|
+
return evolutionCommand(args, useJson)
|
|
2848
|
+
case 'help':
|
|
2849
|
+
case '--help':
|
|
2850
|
+
return help()
|
|
2851
|
+
default:
|
|
2852
|
+
console.error(`Unknown command: ${command}`)
|
|
2853
|
+
help()
|
|
2854
|
+
process.exitCode = 1
|
|
2855
|
+
}
|
|
2856
|
+
}
|
|
2857
|
+
|
|
2858
|
+
main().catch((error) => {
|
|
2859
|
+
console.error(error.message || error)
|
|
2860
|
+
process.exitCode = 1
|
|
2861
|
+
})
|