@xmemo/client 0.4.134 → 0.4.136
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/LICENSE +7 -7
- package/README.md +393 -327
- package/bin/memory-os.js +12 -12
- package/package.json +46 -46
- package/src/cli.js +2406 -2206
package/src/cli.js
CHANGED
|
@@ -1,2206 +1,2406 @@
|
|
|
1
|
-
import fs from 'node:fs/promises';
|
|
2
|
-
import http from 'node:http';
|
|
3
|
-
import os from 'node:os';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import { spawn } from 'node:child_process';
|
|
6
|
-
import { randomUUID } from 'node:crypto';
|
|
7
|
-
|
|
8
|
-
const PRODUCT_NAME = 'XMemo';
|
|
9
|
-
const PACKAGE_NAME = '@xmemo/client';
|
|
10
|
-
const FALLBACK_PACKAGE_NAME = '@yonro/xmemo-client';
|
|
11
|
-
const COMMAND_NAME = 'xmemo';
|
|
12
|
-
const LEGACY_COMMAND_NAME = 'memory-os';
|
|
13
|
-
const CLI_VERSION = '0.4.
|
|
14
|
-
const DEFAULT_SERVICE_URL = 'https://xmemo.dev';
|
|
15
|
-
const TOKEN_ENV_VAR = 'XMEMO_KEY';
|
|
16
|
-
const LEGACY_TOKEN_ENV_VAR = 'MEMORY_OS_MCP_TOKEN';
|
|
17
|
-
const AGENT_ID_ENV_VAR = 'XMEMO_AGENT_ID';
|
|
18
|
-
const AGENT_INSTANCE_ENV_VAR = 'XMEMO_AGENT_INSTANCE_ID';
|
|
19
|
-
const AGENT_ID_HEADER = 'X-Memory-OS-Agent-ID';
|
|
20
|
-
const AGENT_INSTANCE_HEADER = 'X-Memory-OS-Agent-Instance-ID';
|
|
21
|
-
const MCP_SERVER_NAME = 'memory_os';
|
|
22
|
-
const CODEX_PROFILE_TARGET = 'AGENTS.md';
|
|
23
|
-
const CODEX_PROFILE_MARKER_START = '<!-- memory-os:codex-profile:start -->';
|
|
24
|
-
const CODEX_PROFILE_MARKER_END = '<!-- memory-os:codex-profile:end -->';
|
|
25
|
-
const DEVICE_LOGIN_START_PATH = '/api/v1/auth/device/start';
|
|
26
|
-
const DEVICE_LOGIN_TOKEN_PATH = '/api/v1/auth/device/token';
|
|
27
|
-
const DEFAULT_PROXY_HOST = '127.0.0.1';
|
|
28
|
-
const DEFAULT_PROXY_PORT = 8765;
|
|
29
|
-
|
|
30
|
-
const MCP_CLIENTS = new Map([
|
|
31
|
-
['codex', {
|
|
32
|
-
label: 'Codex',
|
|
33
|
-
defaultConfigPath: defaultCodexConfigPath,
|
|
34
|
-
buildSnippet: codexTomlSnippet,
|
|
35
|
-
writeConfig: appendTomlServerConfig,
|
|
36
|
-
configKind: 'toml'
|
|
37
|
-
}],
|
|
38
|
-
['cursor', {
|
|
39
|
-
label: 'Cursor',
|
|
40
|
-
defaultConfigPath: defaultCursorConfigPath,
|
|
41
|
-
buildSnippet: cursorJsonSnippet,
|
|
42
|
-
writeConfig: mergeJsonMcpConfig,
|
|
43
|
-
configKind: 'json'
|
|
44
|
-
}]
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
return
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
if (command === '
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
return
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
return
|
|
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
|
-
writeLine(io.stdout,
|
|
171
|
-
writeLine(io.stdout, `
|
|
172
|
-
writeLine(io.stdout,
|
|
173
|
-
writeLine(io.stdout,
|
|
174
|
-
writeLine(io.stdout, ` ${COMMAND_NAME}
|
|
175
|
-
writeLine(io.stdout, ` ${COMMAND_NAME}
|
|
176
|
-
writeLine(io.stdout, ` ${COMMAND_NAME}
|
|
177
|
-
writeLine(io.stdout,
|
|
178
|
-
writeLine(io.stdout, `
|
|
179
|
-
writeLine(io.stdout,
|
|
180
|
-
writeLine(io.stdout,
|
|
181
|
-
writeLine(io.stdout,
|
|
182
|
-
writeLine(io.stdout,
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
if (
|
|
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
|
-
const
|
|
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
|
-
if (
|
|
295
|
-
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
const
|
|
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
|
-
const
|
|
384
|
-
const
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
const
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
const
|
|
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
|
-
const
|
|
494
|
-
const
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
writeLine(io.stdout,
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
writeLine(io.stdout,
|
|
536
|
-
|
|
537
|
-
writeLine(io.stdout,
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
async function
|
|
549
|
-
const subcommand = args[0] ?? 'help';
|
|
550
|
-
|
|
551
|
-
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
|
|
552
|
-
writeLine(io.stdout, '
|
|
553
|
-
writeLine(io.stdout, ` ${COMMAND_NAME}
|
|
554
|
-
writeLine(io.stdout,
|
|
555
|
-
writeLine(io.stdout, `
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
}
|
|
577
|
-
return 0;
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
if (subcommand === '
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
writeLine(io.stdout, `
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
}
|
|
704
|
-
writeLine(io.stdout, `
|
|
705
|
-
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
writeLine(io.stdout,
|
|
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
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
const
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
const
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
});
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
});
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
const
|
|
845
|
-
if (
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
});
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
const
|
|
885
|
-
const
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
const
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
if (
|
|
919
|
-
throw new UsageError(
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
const
|
|
926
|
-
const
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
};
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
writeLine(io.stdout,
|
|
947
|
-
writeLine(io.stdout, `
|
|
948
|
-
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
}
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
}
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
}
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
},
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
}
|
|
1227
|
-
|
|
1228
|
-
}
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
}
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
}
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
}
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
const
|
|
1432
|
-
return {
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
}
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
.
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
})
|
|
1521
|
-
}
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
}
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
}
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
''
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
return
|
|
1611
|
-
}
|
|
1612
|
-
|
|
1613
|
-
function
|
|
1614
|
-
return
|
|
1615
|
-
}
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
};
|
|
1652
|
-
}
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
const
|
|
1657
|
-
const
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
}
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
}
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
:
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
}
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
}
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
}
|
|
1812
|
-
|
|
1813
|
-
function
|
|
1814
|
-
const
|
|
1815
|
-
const
|
|
1816
|
-
const
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
const
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
}
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
}
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
}
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
}
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
await
|
|
1915
|
-
|
|
1916
|
-
await
|
|
1917
|
-
|
|
1918
|
-
}
|
|
1919
|
-
|
|
1920
|
-
async function
|
|
1921
|
-
const existing = await readTextIfExists(
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
}
|
|
1941
|
-
|
|
1942
|
-
function
|
|
1943
|
-
return
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
}
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
}
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
}
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
const
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
await fs.
|
|
2005
|
-
await
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
}
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
}
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
if (!
|
|
2018
|
-
|
|
2019
|
-
}
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
}
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
return
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
}
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
}
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
}
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
const
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
}
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
const
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
}
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
}
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
}
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
}
|
|
2187
|
-
|
|
2188
|
-
function
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
}
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
}
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import http from 'node:http';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import { randomUUID } from 'node:crypto';
|
|
7
|
+
|
|
8
|
+
const PRODUCT_NAME = 'XMemo';
|
|
9
|
+
const PACKAGE_NAME = '@xmemo/client';
|
|
10
|
+
const FALLBACK_PACKAGE_NAME = '@yonro/xmemo-client';
|
|
11
|
+
const COMMAND_NAME = 'xmemo';
|
|
12
|
+
const LEGACY_COMMAND_NAME = 'memory-os';
|
|
13
|
+
const CLI_VERSION = '0.4.136';
|
|
14
|
+
const DEFAULT_SERVICE_URL = 'https://xmemo.dev';
|
|
15
|
+
const TOKEN_ENV_VAR = 'XMEMO_KEY';
|
|
16
|
+
const LEGACY_TOKEN_ENV_VAR = 'MEMORY_OS_MCP_TOKEN';
|
|
17
|
+
const AGENT_ID_ENV_VAR = 'XMEMO_AGENT_ID';
|
|
18
|
+
const AGENT_INSTANCE_ENV_VAR = 'XMEMO_AGENT_INSTANCE_ID';
|
|
19
|
+
const AGENT_ID_HEADER = 'X-Memory-OS-Agent-ID';
|
|
20
|
+
const AGENT_INSTANCE_HEADER = 'X-Memory-OS-Agent-Instance-ID';
|
|
21
|
+
const MCP_SERVER_NAME = 'memory_os';
|
|
22
|
+
const CODEX_PROFILE_TARGET = 'AGENTS.md';
|
|
23
|
+
const CODEX_PROFILE_MARKER_START = '<!-- memory-os:codex-profile:start -->';
|
|
24
|
+
const CODEX_PROFILE_MARKER_END = '<!-- memory-os:codex-profile:end -->';
|
|
25
|
+
const DEVICE_LOGIN_START_PATH = '/api/v1/auth/device/start';
|
|
26
|
+
const DEVICE_LOGIN_TOKEN_PATH = '/api/v1/auth/device/token';
|
|
27
|
+
const DEFAULT_PROXY_HOST = '127.0.0.1';
|
|
28
|
+
const DEFAULT_PROXY_PORT = 8765;
|
|
29
|
+
|
|
30
|
+
const MCP_CLIENTS = new Map([
|
|
31
|
+
['codex', {
|
|
32
|
+
label: 'Codex',
|
|
33
|
+
defaultConfigPath: defaultCodexConfigPath,
|
|
34
|
+
buildSnippet: codexTomlSnippet,
|
|
35
|
+
writeConfig: appendTomlServerConfig,
|
|
36
|
+
configKind: 'toml'
|
|
37
|
+
}],
|
|
38
|
+
['cursor', {
|
|
39
|
+
label: 'Cursor',
|
|
40
|
+
defaultConfigPath: defaultCursorConfigPath,
|
|
41
|
+
buildSnippet: cursorJsonSnippet,
|
|
42
|
+
writeConfig: mergeJsonMcpConfig,
|
|
43
|
+
configKind: 'json'
|
|
44
|
+
}],
|
|
45
|
+
['gemini-cli', {
|
|
46
|
+
label: 'Gemini CLI',
|
|
47
|
+
defaultConfigPath: defaultGeminiConfigPath,
|
|
48
|
+
buildSnippet: geminiJsonSnippet,
|
|
49
|
+
writeConfig: mergeGeminiMcpConfig,
|
|
50
|
+
configKind: 'json'
|
|
51
|
+
}],
|
|
52
|
+
['antigravity', {
|
|
53
|
+
label: 'Antigravity',
|
|
54
|
+
defaultConfigPath: defaultAntigravityConfigPath,
|
|
55
|
+
buildSnippet: antigravityJsonSnippet,
|
|
56
|
+
writeConfig: mergeAntigravityMcpConfig,
|
|
57
|
+
configKind: 'json'
|
|
58
|
+
}]
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
const SETUP_CLIENT_ALIASES = new Map([
|
|
62
|
+
['codex', 'codex'],
|
|
63
|
+
['cursor', 'cursor'],
|
|
64
|
+
['copilot', 'copilot-cli'],
|
|
65
|
+
['copilot-cli', 'copilot-cli'],
|
|
66
|
+
['gemini', 'gemini-cli'],
|
|
67
|
+
['gemini-cli', 'gemini-cli'],
|
|
68
|
+
['antigravity', 'antigravity']
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
class UsageError extends Error {
|
|
72
|
+
constructor(message) {
|
|
73
|
+
super(message);
|
|
74
|
+
this.name = 'UsageError';
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function run(args, io = defaultIo()) {
|
|
79
|
+
try {
|
|
80
|
+
const command = args[0] ?? 'help';
|
|
81
|
+
|
|
82
|
+
if (command === '--help' || command === '-h' || command === 'help') {
|
|
83
|
+
writeHelp(io);
|
|
84
|
+
return 0;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (command === '--version' || command === '-v' || command === 'version') {
|
|
88
|
+
writeLine(io.stdout, CLI_VERSION);
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (command === 'update' || command === '--update') {
|
|
93
|
+
return await updateCommand(args.slice(1), io);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (command === 'doctor') {
|
|
97
|
+
return await doctorCommand(args.slice(1), io);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (command === 'discovery') {
|
|
101
|
+
return await discoveryCommand(args.slice(1), io);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (command === 'status') {
|
|
105
|
+
return await statusCommand(args.slice(1), io);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (command === 'setup') {
|
|
109
|
+
return await setupCommand(args.slice(1), io);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (command === 'login') {
|
|
113
|
+
return await loginCommand(args.slice(1), io);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (command === 'auth') {
|
|
117
|
+
return await authCommand(args.slice(1), io);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (command === 'token') {
|
|
121
|
+
return await tokenCommand(args.slice(1), io);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (command === 'mcp') {
|
|
125
|
+
return await mcpCommand(args.slice(1), io);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (command === 'profile') {
|
|
129
|
+
return await profileCommand(args.slice(1), io);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (command === 'smoke') {
|
|
133
|
+
return await smokeCommand(args.slice(1), io);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (command === 'env') {
|
|
137
|
+
return envCommand(args.slice(1), io);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (command === 'privacy') {
|
|
141
|
+
writePrivacy(io);
|
|
142
|
+
return 0;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
throw new UsageError(`Unknown command: ${command}`);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
if (error instanceof UsageError) {
|
|
148
|
+
writeLine(io.stderr, `Error: ${error.message}`);
|
|
149
|
+
writeLine(io.stderr, `Run \`${COMMAND_NAME} help\` for usage.`);
|
|
150
|
+
return 2;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
writeLine(io.stderr, `Unexpected error: ${error.message}`);
|
|
154
|
+
return 1;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function defaultIo() {
|
|
159
|
+
return {
|
|
160
|
+
env: process.env,
|
|
161
|
+
stdin: process.stdin,
|
|
162
|
+
stdout: process.stdout,
|
|
163
|
+
stderr: process.stderr,
|
|
164
|
+
fetch: globalThis.fetch,
|
|
165
|
+
spawn
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function writeHelp(io) {
|
|
170
|
+
writeLine(io.stdout, `${PRODUCT_NAME} CLI (${PACKAGE_NAME})`);
|
|
171
|
+
writeLine(io.stdout, `Fallback npm package: ${FALLBACK_PACKAGE_NAME}; legacy command alias: ${LEGACY_COMMAND_NAME}`);
|
|
172
|
+
writeLine(io.stdout, '');
|
|
173
|
+
writeLine(io.stdout, 'Usage:');
|
|
174
|
+
writeLine(io.stdout, ` ${COMMAND_NAME} update [--dry-run] [--json]`);
|
|
175
|
+
writeLine(io.stdout, ` ${COMMAND_NAME} doctor [--base-url <https://api.example.com>] [--json]`);
|
|
176
|
+
writeLine(io.stdout, ` ${COMMAND_NAME} discovery show [--base-url <https://api.example.com>] [--json]`);
|
|
177
|
+
writeLine(io.stdout, ` ${COMMAND_NAME} setup <codex|cursor|copilot|gemini|antigravity> [--url <https://api.example.com>] [--dry-run] [--json]`);
|
|
178
|
+
writeLine(io.stdout, ` ${COMMAND_NAME} login [--from-stdin] [--base-url <url>] [--timeout-ms <ms>] [--http-timeout-ms <ms>] [--json]`);
|
|
179
|
+
writeLine(io.stdout, ` ${COMMAND_NAME} auth status [--verify] [--base-url <url>] [--json]`);
|
|
180
|
+
writeLine(io.stdout, ` ${COMMAND_NAME} status [--url <https://api.example.com>] [--json]`);
|
|
181
|
+
writeLine(io.stdout, ` ${COMMAND_NAME} token status [--verify]`);
|
|
182
|
+
writeLine(io.stdout, ` ${COMMAND_NAME} token add --from-stdin`);
|
|
183
|
+
writeLine(io.stdout, ` ${COMMAND_NAME} token set --from-stdin [--allow-plaintext]`);
|
|
184
|
+
writeLine(io.stdout, ` ${COMMAND_NAME} mcp list`);
|
|
185
|
+
writeLine(io.stdout, ` ${COMMAND_NAME} mcp config --client <codex|cursor|copilot-cli|antigravity|generic> [--base-url <url>] [--json]`);
|
|
186
|
+
writeLine(io.stdout, ` ${COMMAND_NAME} mcp proxy [--port ${DEFAULT_PROXY_PORT}]`);
|
|
187
|
+
writeLine(io.stdout, ` ${COMMAND_NAME} mcp profile codex [--json]`);
|
|
188
|
+
writeLine(io.stdout, ` ${COMMAND_NAME} profile install codex [--target AGENTS.md] [--dry-run|--json]`);
|
|
189
|
+
writeLine(io.stdout, ` ${COMMAND_NAME} profile uninstall codex [--target AGENTS.md] [--json]`);
|
|
190
|
+
writeLine(io.stdout, ` ${COMMAND_NAME} mcp add <${supportedMcpClientIds().join('|')}> [--url <https://api.example.com>] [--write] [--config <path>]`);
|
|
191
|
+
writeLine(io.stdout, ` ${COMMAND_NAME} smoke --client codex [--config <path>] [--json]`);
|
|
192
|
+
writeLine(io.stdout, ` ${COMMAND_NAME} env example [--shell bash|powershell|cmd] [--json]`);
|
|
193
|
+
writeLine(io.stdout, ` ${COMMAND_NAME} privacy`);
|
|
194
|
+
writeLine(io.stdout, '');
|
|
195
|
+
writeLine(io.stdout, `Default service URL: ${DEFAULT_SERVICE_URL}; use --url or XMEMO_URL for private deployments.`);
|
|
196
|
+
writeLine(io.stdout, '`login --timeout-ms` controls the full browser approval window; HTTP calls use `--http-timeout-ms`.');
|
|
197
|
+
writeLine(io.stdout, '');
|
|
198
|
+
writeLine(io.stdout, 'Privacy defaults: no telemetry, no token in project files, and no token is sent by `status`, `doctor`, or `discovery`.');
|
|
199
|
+
writeLine(io.stdout, '`login` and `token add` store credentials only in the user-scoped XMemo CLI config directory.');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function updateCommand(args, io) {
|
|
203
|
+
const outputJson = hasFlag(args, '--json');
|
|
204
|
+
const dryRun = hasFlag(args, '--dry-run');
|
|
205
|
+
const npmCommand = npmExecutable();
|
|
206
|
+
const npmArgs = ['install', '-g', `${PACKAGE_NAME}@latest`];
|
|
207
|
+
const report = {
|
|
208
|
+
package: PACKAGE_NAME,
|
|
209
|
+
command: [npmCommand, ...npmArgs],
|
|
210
|
+
dryRun,
|
|
211
|
+
tokenSent: false,
|
|
212
|
+
projectFilesModified: false
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
if (dryRun) {
|
|
216
|
+
if (outputJson) {
|
|
217
|
+
writeLine(io.stdout, JSON.stringify(report, null, 2));
|
|
218
|
+
} else {
|
|
219
|
+
writeLine(io.stdout, `Update command: ${report.command.join(' ')}`);
|
|
220
|
+
writeLine(io.stdout, 'Dry run only; no changes made.');
|
|
221
|
+
}
|
|
222
|
+
return 0;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (!outputJson) {
|
|
226
|
+
writeLine(io.stdout, `Updating ${PACKAGE_NAME} with: ${report.command.join(' ')}`);
|
|
227
|
+
}
|
|
228
|
+
const result = await runProcess(npmCommand, npmArgs, io, { stream: !outputJson });
|
|
229
|
+
report.exitCode = result.code;
|
|
230
|
+
report.completed = result.code === 0;
|
|
231
|
+
|
|
232
|
+
if (outputJson) {
|
|
233
|
+
writeLine(io.stdout, JSON.stringify(report, null, 2));
|
|
234
|
+
}
|
|
235
|
+
if (result.code !== 0) {
|
|
236
|
+
const detail = result.stderr.trim() || result.stdout.trim() || `exit code ${result.code}`;
|
|
237
|
+
throw new UsageError(`Update failed: ${detail}`);
|
|
238
|
+
}
|
|
239
|
+
if (!outputJson) {
|
|
240
|
+
writeLine(io.stdout, `Update complete. Run \`${COMMAND_NAME} --version\` to confirm.`);
|
|
241
|
+
}
|
|
242
|
+
return 0;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function doctorCommand(args, io) {
|
|
246
|
+
const baseUrl = normalizeBaseUrl(baseUrlOption(args, io.env));
|
|
247
|
+
const outputJson = hasFlag(args, '--json');
|
|
248
|
+
const timeoutMs = parsePositiveInteger(optionValue(args, '--timeout-ms') ?? '5000', '--timeout-ms');
|
|
249
|
+
const discoveryUrl = endpointUrl(baseUrl, '/.well-known/agent-discovery.json');
|
|
250
|
+
const discovery = await fetchJson(discoveryUrl, timeoutMs, io);
|
|
251
|
+
ensureDiscoveryService(discovery, discoveryUrl);
|
|
252
|
+
|
|
253
|
+
const rootVersion = await bestEffortRootVersion(discovery, timeoutMs, io);
|
|
254
|
+
const mcpUrl = discoveryMcpUrl(discovery, baseUrl);
|
|
255
|
+
const checks = [
|
|
256
|
+
{ name: 'node_version', ok: Number.parseInt(process.versions.node.split('.')[0], 10) >= 20, detail: process.versions.node },
|
|
257
|
+
{ name: 'discovery_reachable', ok: true, detail: discoveryUrl },
|
|
258
|
+
{ name: 'mcp_url_present', ok: Boolean(mcpUrl), detail: mcpUrl ?? 'missing' },
|
|
259
|
+
{ name: 'no_remote_code_execution', ok: booleanValue(discovery, ['security', 'no_remote_code_execution']) === true, detail: String(booleanValue(discovery, ['security', 'no_remote_code_execution'])) },
|
|
260
|
+
{
|
|
261
|
+
name: 'token_not_in_discovery',
|
|
262
|
+
ok: booleanValue(discovery, ['security', 'token_in_discovery']) === false && booleanValue(discovery, ['auth', 'token_in_discovery']) === false,
|
|
263
|
+
detail: `security=${booleanValue(discovery, ['security', 'token_in_discovery'])} auth=${booleanValue(discovery, ['auth', 'token_in_discovery'])}`
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
name: 'service_version_compatible',
|
|
267
|
+
ok: rootVersion.version ? sameMajorMinor(CLI_VERSION, rootVersion.version) : true,
|
|
268
|
+
detail: rootVersion.version ? `service=${rootVersion.version} cli=${CLI_VERSION}` : `service version unavailable${rootVersion.error ? `: ${rootVersion.error}` : ''}`
|
|
269
|
+
}
|
|
270
|
+
];
|
|
271
|
+
const report = {
|
|
272
|
+
ok: checks.every((check) => check.ok),
|
|
273
|
+
cli: { package: PACKAGE_NAME, version: CLI_VERSION, node: process.versions.node },
|
|
274
|
+
discovery: {
|
|
275
|
+
url: discoveryUrl,
|
|
276
|
+
schemaVersion: stringValue(discovery, ['schema_version']),
|
|
277
|
+
protocol: stringValue(discovery, ['protocol']),
|
|
278
|
+
service: stringValue(discovery, ['service']),
|
|
279
|
+
serviceVersion: rootVersion.version ?? null,
|
|
280
|
+
mcpUrl,
|
|
281
|
+
supportedClients: agentDiscoveryClientIds(discovery)
|
|
282
|
+
},
|
|
283
|
+
checks
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
if (outputJson) {
|
|
287
|
+
writeLine(io.stdout, JSON.stringify(report, null, 2));
|
|
288
|
+
return report.ok ? 0 : 1;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
writeLine(io.stdout, `${PRODUCT_NAME} CLI ${CLI_VERSION}`);
|
|
292
|
+
writeLine(io.stdout, `Discovery: ${discoveryUrl}`);
|
|
293
|
+
writeLine(io.stdout, `MCP: ${mcpUrl ?? 'missing'}`);
|
|
294
|
+
if (rootVersion.version) {
|
|
295
|
+
writeLine(io.stdout, `Service version: ${rootVersion.version}`);
|
|
296
|
+
}
|
|
297
|
+
writeLine(io.stdout, `Supported clients: ${report.discovery.supportedClients.join(', ') || 'unknown'}`);
|
|
298
|
+
for (const check of checks) {
|
|
299
|
+
writeLine(io.stdout, `${check.ok ? 'OK' : 'FAIL'} ${check.name}: ${check.detail}`);
|
|
300
|
+
}
|
|
301
|
+
return report.ok ? 0 : 1;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async function discoveryCommand(args, io) {
|
|
305
|
+
const subcommand = args[0] ?? 'help';
|
|
306
|
+
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
|
|
307
|
+
writeLine(io.stdout, 'Discovery commands:');
|
|
308
|
+
writeLine(io.stdout, ` ${COMMAND_NAME} discovery show [--base-url <https://api.example.com>] [--json]`);
|
|
309
|
+
return 0;
|
|
310
|
+
}
|
|
311
|
+
if (subcommand !== 'show') {
|
|
312
|
+
throw new UsageError(`Unknown discovery command: ${subcommand}`);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const baseUrl = normalizeBaseUrl(baseUrlOption(args.slice(1), io.env));
|
|
316
|
+
const outputJson = hasFlag(args, '--json');
|
|
317
|
+
const timeoutMs = parsePositiveInteger(optionValue(args, '--timeout-ms') ?? '5000', '--timeout-ms');
|
|
318
|
+
const discoveryUrl = endpointUrl(baseUrl, '/.well-known/agent-discovery.json');
|
|
319
|
+
const discovery = await fetchJson(discoveryUrl, timeoutMs, io);
|
|
320
|
+
ensureDiscoveryService(discovery, discoveryUrl);
|
|
321
|
+
|
|
322
|
+
if (outputJson) {
|
|
323
|
+
writeLine(io.stdout, JSON.stringify(discovery, null, 2));
|
|
324
|
+
return 0;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
writeLine(io.stdout, `${stringValue(discovery, ['name']) ?? PRODUCT_NAME} discovery`);
|
|
328
|
+
writeLine(io.stdout, `URL: ${discoveryUrl}`);
|
|
329
|
+
writeLine(io.stdout, `Protocol: ${stringValue(discovery, ['protocol']) ?? 'unknown'}`);
|
|
330
|
+
writeLine(io.stdout, `MCP: ${discoveryMcpUrl(discovery, baseUrl) ?? 'missing'}`);
|
|
331
|
+
writeLine(io.stdout, `Docs: ${stringValue(discovery, ['urls', 'docs']) ?? 'unknown'}`);
|
|
332
|
+
writeLine(io.stdout, `Clients: ${agentDiscoveryClientIds(discovery).join(', ') || 'unknown'}`);
|
|
333
|
+
writeLine(io.stdout, 'Security: read-only discovery; tokens are not returned; remote code execution is not advertised.');
|
|
334
|
+
return 0;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async function statusCommand(args, io) {
|
|
338
|
+
const baseUrl = normalizeBaseUrl(baseUrlOption(args, io.env));
|
|
339
|
+
const outputJson = hasFlag(args, '--json');
|
|
340
|
+
const timeoutMs = parsePositiveInteger(optionValue(args, '--timeout-ms') ?? '5000', '--timeout-ms');
|
|
341
|
+
const endpoints = [
|
|
342
|
+
endpointUrl(baseUrl, '/.well-known/memory-os.json'),
|
|
343
|
+
endpointUrl(baseUrl, '/health'),
|
|
344
|
+
endpointUrl(baseUrl, '/ready')
|
|
345
|
+
];
|
|
346
|
+
|
|
347
|
+
const probes = [];
|
|
348
|
+
for (const url of endpoints) {
|
|
349
|
+
probes.push(await probe(url, timeoutMs, io));
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const result = {
|
|
353
|
+
ok: probes.some((item) => item.ok),
|
|
354
|
+
baseUrl,
|
|
355
|
+
privacy: {
|
|
356
|
+
telemetry: false,
|
|
357
|
+
tokenSent: false,
|
|
358
|
+
tokenSource: 'not-used-by-status'
|
|
359
|
+
},
|
|
360
|
+
probes
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
if (outputJson) {
|
|
364
|
+
writeLine(io.stdout, JSON.stringify(result, null, 2));
|
|
365
|
+
return result.ok ? 0 : 1;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
writeLine(io.stdout, `${PRODUCT_NAME} status for ${baseUrl}`);
|
|
369
|
+
writeLine(io.stdout, 'Privacy: telemetry disabled; no token sent.');
|
|
370
|
+
for (const item of probes) {
|
|
371
|
+
if (item.ok) {
|
|
372
|
+
writeLine(io.stdout, ` OK ${item.status} ${item.url}`);
|
|
373
|
+
} else {
|
|
374
|
+
writeLine(io.stdout, ` FAIL ${item.status ?? 'ERR'} ${item.url} ${item.error ?? ''}`.trimEnd());
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return result.ok ? 0 : 1;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async function setupCommand(args, io) {
|
|
382
|
+
const positionalClientId = positionalClientArg(args);
|
|
383
|
+
const optionArgs = positionalClientId ? args.slice(1) : args;
|
|
384
|
+
const baseUrl = normalizeBaseUrl(baseUrlOption(optionArgs, io.env));
|
|
385
|
+
const outputJson = hasFlag(optionArgs, '--json');
|
|
386
|
+
const shortClientSetup = Boolean(positionalClientId);
|
|
387
|
+
const clientId = normalizeSetupClientId(positionalClientId ?? optionValue(optionArgs, '--client'));
|
|
388
|
+
const dryRun = hasFlag(optionArgs, '--dry-run') || hasFlag(optionArgs, '--preview');
|
|
389
|
+
const writeConfig = !dryRun && (hasFlag(optionArgs, '--write') || hasFlag(optionArgs, '--yes') || shortClientSetup);
|
|
390
|
+
const timeoutMs = parsePositiveInteger(optionValue(optionArgs, '--timeout-ms') ?? '5000', '--timeout-ms');
|
|
391
|
+
const installProfile = shortClientSetup
|
|
392
|
+
&& clientId === 'codex'
|
|
393
|
+
&& writeConfig
|
|
394
|
+
&& !hasFlag(optionArgs, '--no-profile');
|
|
395
|
+
|
|
396
|
+
if (writeConfig && !clientId) {
|
|
397
|
+
throw new UsageError(`Setup --write requires --client <${supportedSetupClientIds().join('|')}> so the CLI never writes broad config implicitly.`);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const discoveryUrl = endpointUrl(baseUrl, '/.well-known/memory-os.json');
|
|
401
|
+
const discovery = await fetchJson(discoveryUrl, timeoutMs, io);
|
|
402
|
+
ensureDiscoveryService(discovery, discoveryUrl);
|
|
403
|
+
|
|
404
|
+
const statusUrl = stringValue(discovery, ['urls', 'onboarding_status'])
|
|
405
|
+
?? stringValue(discovery, ['onboarding_status_url'])
|
|
406
|
+
?? endpointUrl(baseUrl, '/v1/onboarding/status');
|
|
407
|
+
const status = await fetchJson(statusUrl, timeoutMs, io);
|
|
408
|
+
const setupPlan = buildSetupPlan({ baseUrl, discoveryUrl, statusUrl, discovery, status });
|
|
409
|
+
|
|
410
|
+
if (clientId) {
|
|
411
|
+
if (clientId === 'copilot-cli') {
|
|
412
|
+
const proxyPort = parsePositiveInteger(optionValue(optionArgs, '--port') ?? String(DEFAULT_PROXY_PORT), '--port');
|
|
413
|
+
setupPlan.selectedClient = copilotSetupPlan(setupPlan.mcpUrl, proxyPort, io.env);
|
|
414
|
+
if (writeConfig) {
|
|
415
|
+
await mergeCopilotMcpConfig(setupPlan.selectedClient.configPath, setupPlan.selectedClient.proxyUrl);
|
|
416
|
+
setupPlan.selectedClient.written = true;
|
|
417
|
+
}
|
|
418
|
+
} else {
|
|
419
|
+
const client = MCP_CLIENTS.get(clientId);
|
|
420
|
+
if (!client) {
|
|
421
|
+
throw new UsageError(`Unsupported MCP client: ${clientId}. Supported clients: ${supportedSetupClientIds().join(', ')}.`);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const identity = writeConfig ? await agentIdentity(clientId, io.env) : envReferenceIdentity(clientId);
|
|
425
|
+
setupPlan.selectedClient = clientSetupPlan(clientId, client, setupPlan.mcpUrl, io.env, identity);
|
|
426
|
+
if (writeConfig) {
|
|
427
|
+
await client.writeConfig(setupPlan.selectedClient.configPath, setupPlan.mcpUrl, identity);
|
|
428
|
+
setupPlan.selectedClient.written = true;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (clientId === 'codex' && shortClientSetup) {
|
|
432
|
+
const profileTarget = optionValue(optionArgs, '--profile-target')
|
|
433
|
+
?? optionValue(optionArgs, '--target')
|
|
434
|
+
?? defaultCodexProfileTarget();
|
|
435
|
+
const profileResult = await codexProfileInstallResult(profileTarget, { write: installProfile });
|
|
436
|
+
setupPlan.selectedClient.codexProfile = profileResult;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (outputJson) {
|
|
442
|
+
writeLine(io.stdout, JSON.stringify(setupPlan, null, 2));
|
|
443
|
+
return 0;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
writeSetupSummary(setupPlan, io);
|
|
447
|
+
return 0;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
async function profileCommand(args, io) {
|
|
451
|
+
const subcommand = args[0] ?? 'help';
|
|
452
|
+
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
|
|
453
|
+
writeLine(io.stdout, 'Profile commands:');
|
|
454
|
+
writeLine(io.stdout, ` ${COMMAND_NAME} profile install codex [--target AGENTS.md] [--dry-run|--json]`);
|
|
455
|
+
writeLine(io.stdout, ` ${COMMAND_NAME} profile status codex [--target AGENTS.md] [--json]`);
|
|
456
|
+
writeLine(io.stdout, ` ${COMMAND_NAME} profile uninstall codex [--target AGENTS.md] [--json]`);
|
|
457
|
+
writeLine(io.stdout, '');
|
|
458
|
+
writeLine(io.stdout, 'Profile installs are marker-scoped and never write token values.');
|
|
459
|
+
return 0;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const clientId = args[1];
|
|
463
|
+
if (clientId !== 'codex') {
|
|
464
|
+
throw new UsageError(`Unsupported profile client: ${clientId ?? 'missing'}. Supported clients: codex.`);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const optionArgs = args.slice(2);
|
|
468
|
+
const outputJson = hasFlag(optionArgs, '--json');
|
|
469
|
+
const targetPath = optionValue(optionArgs, '--target') ?? defaultCodexProfileTarget();
|
|
470
|
+
let result;
|
|
471
|
+
|
|
472
|
+
if (subcommand === 'install') {
|
|
473
|
+
result = await codexProfileInstallResult(targetPath, { write: !hasFlag(optionArgs, '--dry-run') });
|
|
474
|
+
} else if (subcommand === 'status') {
|
|
475
|
+
result = await codexProfileStatusResult(targetPath);
|
|
476
|
+
} else if (subcommand === 'uninstall') {
|
|
477
|
+
result = await codexProfileUninstallResult(targetPath, { write: !hasFlag(optionArgs, '--dry-run') });
|
|
478
|
+
} else {
|
|
479
|
+
throw new UsageError(`Unknown profile command: ${subcommand}`);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (outputJson) {
|
|
483
|
+
writeLine(io.stdout, JSON.stringify(result, null, 2));
|
|
484
|
+
return 0;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
writeProfileResult(subcommand, result, io);
|
|
488
|
+
return 0;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
async function loginCommand(args, io) {
|
|
492
|
+
const outputJson = hasFlag(args, '--json');
|
|
493
|
+
const fromStdin = hasFlag(args, '--from-stdin') || hasFlag(args, '--token-stdin');
|
|
494
|
+
const baseUrl = normalizeBaseUrl(baseUrlOption(args, io.env));
|
|
495
|
+
const httpTimeoutMs = parsePositiveInteger(optionValue(args, '--http-timeout-ms') ?? '30000', '--http-timeout-ms');
|
|
496
|
+
const loginTimeoutOption = optionValue(args, '--timeout-ms');
|
|
497
|
+
const pollOnce = hasFlag(args, '--poll-once');
|
|
498
|
+
|
|
499
|
+
if (fromStdin) {
|
|
500
|
+
const result = await storeTokenFromStdin(io, { source: 'stdin' });
|
|
501
|
+
if (outputJson) {
|
|
502
|
+
writeLine(io.stdout, JSON.stringify(result, null, 2));
|
|
503
|
+
} else {
|
|
504
|
+
writeLine(io.stdout, `${PRODUCT_NAME} login complete.`);
|
|
505
|
+
writeLine(io.stdout, `Stored token in user-scoped credential file: ${result.credentialPath}`);
|
|
506
|
+
writeLine(io.stdout, 'Token value was not printed. Project files were not modified.');
|
|
507
|
+
}
|
|
508
|
+
return 0;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const start = await startDeviceLogin(baseUrl, httpTimeoutMs, io);
|
|
512
|
+
const loginTimeoutMs = loginTimeoutOption
|
|
513
|
+
? parsePositiveInteger(loginTimeoutOption, '--timeout-ms')
|
|
514
|
+
: Math.max(1000, start.expiresIn * 1000);
|
|
515
|
+
if (!outputJson) {
|
|
516
|
+
writeLine(io.stdout, `${PRODUCT_NAME} device login`);
|
|
517
|
+
writeLine(io.stdout, `Open: ${start.verificationUriComplete ?? start.verificationUri}`);
|
|
518
|
+
if (start.userCode) {
|
|
519
|
+
writeLine(io.stdout, `Code: ${start.userCode}`);
|
|
520
|
+
}
|
|
521
|
+
writeLine(io.stdout, 'Waiting for authorization...');
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const token = await pollDeviceLogin(baseUrl, start, loginTimeoutMs, httpTimeoutMs, io, { pollOnce });
|
|
525
|
+
const result = await storeTokenValue(token.accessToken, { source: 'device-login', account: token.account }, io.env);
|
|
526
|
+
const payload = {
|
|
527
|
+
...result,
|
|
528
|
+
baseUrl,
|
|
529
|
+
verificationUri: start.verificationUri,
|
|
530
|
+
account: token.account,
|
|
531
|
+
deviceLogin: true
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
if (outputJson) {
|
|
535
|
+
writeLine(io.stdout, JSON.stringify(payload, null, 2));
|
|
536
|
+
} else {
|
|
537
|
+
writeLine(io.stdout, 'Login complete. Token stored securely in the user-scoped XMemo CLI config directory.');
|
|
538
|
+
if (token.account) {
|
|
539
|
+
writeLine(io.stdout, `Signed in as: ${formatAccount(token.account)}`);
|
|
540
|
+
}
|
|
541
|
+
writeLine(io.stdout, `Credential path: ${result.credentialPath}`);
|
|
542
|
+
writeLine(io.stdout, 'No extra token configuration is required.');
|
|
543
|
+
writeLine(io.stdout, `Optional check: ${COMMAND_NAME} token status --verify`);
|
|
544
|
+
}
|
|
545
|
+
return 0;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
async function authCommand(args, io) {
|
|
549
|
+
const subcommand = args[0] ?? 'help';
|
|
550
|
+
|
|
551
|
+
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
|
|
552
|
+
writeLine(io.stdout, 'Auth commands:');
|
|
553
|
+
writeLine(io.stdout, ` ${COMMAND_NAME} auth status [--verify] [--base-url <url>] [--json]`);
|
|
554
|
+
writeLine(io.stdout, '');
|
|
555
|
+
writeLine(io.stdout, `Use \`${COMMAND_NAME} login\` to sign in and \`${COMMAND_NAME} token add --from-stdin\` to store an existing token.`);
|
|
556
|
+
return 0;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (subcommand === 'status') {
|
|
560
|
+
return await credentialStatusCommand(args.slice(1), io, { mode: 'auth' });
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
throw new UsageError(`Unknown auth command: ${subcommand}`);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
async function tokenCommand(args, io) {
|
|
567
|
+
const subcommand = args[0] ?? 'help';
|
|
568
|
+
|
|
569
|
+
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
|
|
570
|
+
writeLine(io.stdout, 'Token commands:');
|
|
571
|
+
writeLine(io.stdout, ` ${COMMAND_NAME} token status [--verify]`);
|
|
572
|
+
writeLine(io.stdout, ` ${COMMAND_NAME} token add --from-stdin`);
|
|
573
|
+
writeLine(io.stdout, ` ${COMMAND_NAME} token set --from-stdin [--allow-plaintext]`);
|
|
574
|
+
writeLine(io.stdout, '');
|
|
575
|
+
writeLine(io.stdout, `${COMMAND_NAME} login is the recommended personal-user path.`);
|
|
576
|
+
writeLine(io.stdout, `${COMMAND_NAME} token add --from-stdin stores a token in the user-scoped XMemo CLI config directory.`);
|
|
577
|
+
return 0;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (subcommand === 'status') {
|
|
581
|
+
return await credentialStatusCommand(args.slice(1), io, { mode: 'token' });
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (subcommand === 'add') {
|
|
585
|
+
if (!hasFlag(args, '--from-stdin')) {
|
|
586
|
+
throw new UsageError('Refusing command-line token input. Pipe the token through stdin with --from-stdin.');
|
|
587
|
+
}
|
|
588
|
+
const result = await storeTokenFromStdin(io, { source: 'token-add' });
|
|
589
|
+
if (hasFlag(args, '--json')) {
|
|
590
|
+
writeLine(io.stdout, JSON.stringify(result, null, 2));
|
|
591
|
+
} else {
|
|
592
|
+
writeLine(io.stdout, `Stored token in user-scoped credential file: ${result.credentialPath}`);
|
|
593
|
+
writeLine(io.stdout, 'Token value was not printed. Project files were not modified.');
|
|
594
|
+
}
|
|
595
|
+
return 0;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if (subcommand === 'set') {
|
|
599
|
+
if (!hasFlag(args, '--from-stdin')) {
|
|
600
|
+
throw new UsageError('Refusing command-line token input. Pipe the token through stdin with --from-stdin.');
|
|
601
|
+
}
|
|
602
|
+
const token = (await readAll(io.stdin)).trim();
|
|
603
|
+
validateToken(token);
|
|
604
|
+
if (!hasFlag(args, '--allow-plaintext')) {
|
|
605
|
+
writeLine(io.stderr, 'Token was read from stdin but was not stored.');
|
|
606
|
+
writeLine(io.stderr, 'Enterprise default refuses plaintext token storage without --allow-plaintext.');
|
|
607
|
+
writeLine(io.stderr, `Preferred personal-user path: ${COMMAND_NAME} login or ${COMMAND_NAME} token add --from-stdin.`);
|
|
608
|
+
return 2;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const result = await storeTokenValue(token, { source: 'token-set' }, io.env);
|
|
612
|
+
writeLine(io.stdout, `Stored token in user-scoped credential file: ${result.credentialPath}`);
|
|
613
|
+
writeLine(io.stdout, 'Token value was not printed. Do not commit this file.');
|
|
614
|
+
return 0;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
throw new UsageError(`Unknown token command: ${subcommand}`);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
async function credentialStatusCommand(args, io, { mode }) {
|
|
621
|
+
const outputJson = hasFlag(args, '--json');
|
|
622
|
+
const verify = hasFlag(args, '--verify');
|
|
623
|
+
const credential = await readStoredCredential(io.env);
|
|
624
|
+
const environmentToken = io.env[TOKEN_ENV_VAR] ?? io.env[LEGACY_TOKEN_ENV_VAR] ?? '';
|
|
625
|
+
const hasEnvironmentToken = Boolean(environmentToken);
|
|
626
|
+
const hasUserCredential = Boolean(credential.token);
|
|
627
|
+
const tokenSource = hasEnvironmentToken ? 'environment' : hasUserCredential ? 'user-credential-file' : 'missing';
|
|
628
|
+
const report = {
|
|
629
|
+
loggedIn: hasEnvironmentToken || hasUserCredential,
|
|
630
|
+
tokenSource,
|
|
631
|
+
environmentToken: {
|
|
632
|
+
present: hasEnvironmentToken,
|
|
633
|
+
variable: hasEnvironmentToken && io.env[TOKEN_ENV_VAR] ? TOKEN_ENV_VAR : hasEnvironmentToken ? LEGACY_TOKEN_ENV_VAR : TOKEN_ENV_VAR
|
|
634
|
+
},
|
|
635
|
+
userCredentialFile: {
|
|
636
|
+
present: hasUserCredential,
|
|
637
|
+
path: credential.path,
|
|
638
|
+
storage: credential.storage ?? null
|
|
639
|
+
},
|
|
640
|
+
account: credential.account ?? null,
|
|
641
|
+
privacy: {
|
|
642
|
+
tokenPrinted: false,
|
|
643
|
+
projectFilesModified: false
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
if (verify) {
|
|
648
|
+
const token = await resolveCredentialToken(io.env);
|
|
649
|
+
if (!token) {
|
|
650
|
+
if (outputJson) {
|
|
651
|
+
writeLine(io.stdout, JSON.stringify({ ...report, verification: { ok: false, detail: 'no token found' } }, null, 2));
|
|
652
|
+
} else {
|
|
653
|
+
writeCredentialStatus(report, io, { mode });
|
|
654
|
+
writeLine(io.stderr, `No token found. Run \`${COMMAND_NAME} login\` or \`${COMMAND_NAME} token add --from-stdin\`.`);
|
|
655
|
+
}
|
|
656
|
+
return 1;
|
|
657
|
+
}
|
|
658
|
+
const baseUrl = normalizeBaseUrl(baseUrlOption(args, io.env));
|
|
659
|
+
const timeoutMs = parsePositiveInteger(optionValue(args, '--timeout-ms') ?? '10000', '--timeout-ms');
|
|
660
|
+
const verification = await verifyTokenWithMcp(baseUrl, token, timeoutMs, io);
|
|
661
|
+
report.verification = verification;
|
|
662
|
+
if (outputJson) {
|
|
663
|
+
writeLine(io.stdout, JSON.stringify(report, null, 2));
|
|
664
|
+
return verification.ok ? 0 : 1;
|
|
665
|
+
}
|
|
666
|
+
writeCredentialStatus(report, io, { mode });
|
|
667
|
+
writeLine(io.stdout, `Remote token verification: ${verification.ok ? 'ok' : 'failed'} (${verification.detail})`);
|
|
668
|
+
return verification.ok ? 0 : 1;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
if (outputJson) {
|
|
672
|
+
writeLine(io.stdout, JSON.stringify(report, null, 2));
|
|
673
|
+
} else {
|
|
674
|
+
writeCredentialStatus(report, io, { mode });
|
|
675
|
+
}
|
|
676
|
+
return report.loggedIn ? 0 : 1;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function writeCredentialStatus(report, io, { mode }) {
|
|
680
|
+
if (mode === 'auth') {
|
|
681
|
+
writeLine(io.stdout, `${PRODUCT_NAME} auth status`);
|
|
682
|
+
writeLine(io.stdout, `Logged in: ${report.loggedIn ? 'yes' : 'no'}`);
|
|
683
|
+
writeLine(io.stdout, `Credential source: ${report.tokenSource}`);
|
|
684
|
+
if (report.account) {
|
|
685
|
+
writeLine(io.stdout, `Account: ${formatAccount(report.account)}`);
|
|
686
|
+
}
|
|
687
|
+
writeLine(io.stdout, report.loggedIn ? 'Credential is ready; token value remains hidden.' : `Run \`${COMMAND_NAME} login\` to sign in.`);
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
writeLine(io.stdout, `Environment token: ${report.environmentToken.present ? 'present' : 'missing'} (${report.environmentToken.variable})`);
|
|
691
|
+
writeLine(io.stdout, `User credential file: ${report.userCredentialFile.present ? 'present' : 'missing'} (${report.userCredentialFile.path})`);
|
|
692
|
+
if (report.account) {
|
|
693
|
+
writeLine(io.stdout, `Account: ${formatAccount(report.account)}`);
|
|
694
|
+
}
|
|
695
|
+
writeLine(io.stdout, report.loggedIn ? 'Credential is ready; token value remains hidden.' : `Run \`${COMMAND_NAME} login\` to sign in.`);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
async function mcpCommand(args, io) {
|
|
699
|
+
const subcommand = args[0] ?? 'help';
|
|
700
|
+
|
|
701
|
+
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
|
|
702
|
+
writeLine(io.stdout, 'MCP commands:');
|
|
703
|
+
writeLine(io.stdout, ` ${COMMAND_NAME} mcp list`);
|
|
704
|
+
writeLine(io.stdout, ` ${COMMAND_NAME} mcp config --client <codex|cursor|copilot-cli|antigravity|generic> [--base-url <url>] [--json]`);
|
|
705
|
+
writeLine(io.stdout, ` ${COMMAND_NAME} mcp proxy [--port ${DEFAULT_PROXY_PORT}] [--base-url <url>]`);
|
|
706
|
+
writeLine(io.stdout, ` ${COMMAND_NAME} mcp profile codex [--json]`);
|
|
707
|
+
writeLine(io.stdout, ` ${COMMAND_NAME} mcp add <${supportedMcpClientIds().join('|')}> [--url <https://api.example.com>]`);
|
|
708
|
+
writeLine(io.stdout, ` ${COMMAND_NAME} mcp add <${supportedMcpClientIds().join('|')}> [--url <https://api.example.com>] --write [--config <path>]`);
|
|
709
|
+
return 0;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
if (subcommand === 'list') {
|
|
713
|
+
if (hasFlag(args, '--json')) {
|
|
714
|
+
writeLine(io.stdout, JSON.stringify(supportedMcpClients(), null, 2));
|
|
715
|
+
return 0;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
writeLine(io.stdout, 'Supported MCP clients:');
|
|
719
|
+
for (const client of supportedMcpClients()) {
|
|
720
|
+
writeLine(io.stdout, ` ${client.id.padEnd(8)} ${client.label} (${client.configKind})`);
|
|
721
|
+
}
|
|
722
|
+
writeLine(io.stdout, `Generated configs never embed token values; OAuth clients do not require ${TOKEN_ENV_VAR} in their config.`);
|
|
723
|
+
return 0;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
if (subcommand === 'config') {
|
|
727
|
+
const clientId = optionValue(args, '--client') ?? args[1] ?? 'generic';
|
|
728
|
+
const baseUrl = normalizeBaseUrl(baseUrlOption(args, io.env));
|
|
729
|
+
const mcpUrl = endpointUrl(baseUrl, '/mcp');
|
|
730
|
+
const useLocalProxy = clientId === 'copilot-cli' && !hasFlag(args, '--remote-env');
|
|
731
|
+
const proxyPort = parsePositiveInteger(optionValue(args, '--port') ?? String(DEFAULT_PROXY_PORT), '--port');
|
|
732
|
+
const proxyUrl = `http://${DEFAULT_PROXY_HOST}:${proxyPort}/mcp`;
|
|
733
|
+
const template = useLocalProxy
|
|
734
|
+
? mcpLocalProxyTemplate(clientId, proxyUrl)
|
|
735
|
+
: mcpConfigTemplate(clientId, mcpUrl);
|
|
736
|
+
|
|
737
|
+
if (hasFlag(args, '--json')) {
|
|
738
|
+
writeLine(io.stdout, JSON.stringify(template, null, 2));
|
|
739
|
+
return 0;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
writeLine(io.stdout, `${PRODUCT_NAME} MCP config template for ${clientId}`);
|
|
743
|
+
if (useLocalProxy) {
|
|
744
|
+
writeLine(io.stdout, `Requires credential: ${COMMAND_NAME} login or ${COMMAND_NAME} token add --from-stdin`);
|
|
745
|
+
writeLine(io.stdout, `Run local proxy: ${template.requiresLocalCommand}`);
|
|
746
|
+
} else {
|
|
747
|
+
if (template.requiresEnv?.length > 0) {
|
|
748
|
+
writeLine(io.stdout, `Requires env: ${template.requiresEnv.join(', ')}`);
|
|
749
|
+
} else if (template.authentication === 'oauth') {
|
|
750
|
+
writeLine(io.stdout, 'Requires auth: complete the client MCP OAuth flow after setup.');
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
if (typeof template.snippet === 'string') {
|
|
754
|
+
writeLine(io.stdout, template.snippet.trimEnd());
|
|
755
|
+
} else {
|
|
756
|
+
writeLine(io.stdout, JSON.stringify(template.snippet, null, 2));
|
|
757
|
+
}
|
|
758
|
+
writeLine(io.stdout, 'Review the template before applying it. Token values are not included.');
|
|
759
|
+
return 0;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
if (subcommand === 'proxy') {
|
|
763
|
+
return await mcpProxyCommand(args.slice(1), io);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
if (subcommand === 'profile') {
|
|
767
|
+
const clientId = args[1] ?? 'codex';
|
|
768
|
+
if (clientId !== 'codex') {
|
|
769
|
+
throw new UsageError('Only the Codex memory behavior profile is available in this MCP-depth release.');
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
const profile = codexMemoryProfile();
|
|
773
|
+
if (hasFlag(args, '--json')) {
|
|
774
|
+
writeLine(io.stdout, JSON.stringify(profile, null, 2));
|
|
775
|
+
return 0;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
writeCodexMemoryProfile(profile, io);
|
|
779
|
+
return 0;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
const target = args[1] ?? '';
|
|
783
|
+
const client = MCP_CLIENTS.get(target);
|
|
784
|
+
|
|
785
|
+
if (subcommand !== 'add' || !client) {
|
|
786
|
+
throw new UsageError(`Supported MCP setup command: ${COMMAND_NAME} mcp add <${supportedMcpClientIds().join('|')}> [--url <url>]`);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
const baseUrl = normalizeBaseUrl(baseUrlOption(args, io.env));
|
|
790
|
+
const configPath = optionValue(args, '--config') ?? client.defaultConfigPath(io.env);
|
|
791
|
+
const mcpUrl = endpointUrl(baseUrl, '/mcp');
|
|
792
|
+
|
|
793
|
+
if (hasFlag(args, '--json')) {
|
|
794
|
+
const identity = envReferenceIdentity(target);
|
|
795
|
+
const oauthClient = usesClientOAuth(target);
|
|
796
|
+
writeLine(io.stdout, JSON.stringify({
|
|
797
|
+
client: target,
|
|
798
|
+
label: client.label,
|
|
799
|
+
configKind: client.configKind,
|
|
800
|
+
configPath,
|
|
801
|
+
serverName: MCP_SERVER_NAME,
|
|
802
|
+
url: mcpUrl,
|
|
803
|
+
tokenEnvVar: oauthClient ? null : TOKEN_ENV_VAR,
|
|
804
|
+
authentication: oauthClient ? 'oauth' : 'env-bearer',
|
|
805
|
+
agentId: identity.agentId,
|
|
806
|
+
agentInstanceId: identity.agentInstanceId,
|
|
807
|
+
agentInstanceIdPath: identity.path,
|
|
808
|
+
writesTokenValue: false
|
|
809
|
+
}, null, 2));
|
|
810
|
+
return 0;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
const identity = hasFlag(args, '--write') ? await agentIdentity(target, io.env) : envReferenceIdentity(target);
|
|
814
|
+
if (hasFlag(args, '--write')) {
|
|
815
|
+
await client.writeConfig(configPath, mcpUrl, identity);
|
|
816
|
+
writeLine(io.stdout, `Updated ${client.label} MCP config: ${configPath}`);
|
|
817
|
+
if (usesClientOAuth(target)) {
|
|
818
|
+
writeLine(io.stdout, `Token value was not written. ${client.label} will complete MCP OAuth on first use.`);
|
|
819
|
+
} else {
|
|
820
|
+
writeLine(io.stdout, `Token value was not written. ${client.label} will read ${TOKEN_ENV_VAR} from the environment.`);
|
|
821
|
+
}
|
|
822
|
+
writeLine(io.stdout, `Agent instance ID stored outside git: ${identity.path}`);
|
|
823
|
+
return 0;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
const snippet = client.buildSnippet(mcpUrl, identity);
|
|
827
|
+
writeLine(io.stdout, `Add this to your ${client.label} config (${configPath}):`);
|
|
828
|
+
writeLine(io.stdout, '');
|
|
829
|
+
writeLine(io.stdout, snippet.trimEnd());
|
|
830
|
+
writeLine(io.stdout, '');
|
|
831
|
+
if (usesClientOAuth(target)) {
|
|
832
|
+
writeLine(io.stdout, `Restart ${client.label} and complete its MCP OAuth flow. No token value is included here.`);
|
|
833
|
+
} else {
|
|
834
|
+
writeLine(io.stdout, `Set ${TOKEN_ENV_VAR} in your user environment or secret manager. The token value is not included here.`);
|
|
835
|
+
}
|
|
836
|
+
return 0;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
async function mcpProxyCommand(args, io) {
|
|
840
|
+
const baseUrl = normalizeBaseUrl(baseUrlOption(args, io.env));
|
|
841
|
+
const mcpUrl = endpointUrl(baseUrl, '/mcp');
|
|
842
|
+
const host = optionValue(args, '--host') ?? DEFAULT_PROXY_HOST;
|
|
843
|
+
const port = parsePositiveInteger(optionValue(args, '--port') ?? String(DEFAULT_PROXY_PORT), '--port');
|
|
844
|
+
const token = await resolveCredentialToken(io.env);
|
|
845
|
+
if (!token) {
|
|
846
|
+
throw new UsageError(`No token found. Run \`${COMMAND_NAME} login\` or \`${COMMAND_NAME} token add --from-stdin\` first.`);
|
|
847
|
+
}
|
|
848
|
+
validateToken(token);
|
|
849
|
+
const identity = await agentIdentity('copilot-cli', io.env);
|
|
850
|
+
|
|
851
|
+
const server = http.createServer(async (request, response) => {
|
|
852
|
+
try {
|
|
853
|
+
await handleMcpProxyRequest({ request, response, mcpUrl, token, identity, io });
|
|
854
|
+
} catch (error) {
|
|
855
|
+
response.statusCode = 502;
|
|
856
|
+
response.setHeader('content-type', 'application/json');
|
|
857
|
+
response.end(JSON.stringify({ error: 'mcp_proxy_error', message: error.message }));
|
|
858
|
+
}
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
await new Promise((resolve, reject) => {
|
|
862
|
+
server.once('error', reject);
|
|
863
|
+
server.listen(port, host, () => {
|
|
864
|
+
server.off('error', reject);
|
|
865
|
+
resolve();
|
|
866
|
+
});
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
writeLine(io.stdout, `${PRODUCT_NAME} MCP proxy listening on http://${host}:${port}/mcp`);
|
|
870
|
+
writeLine(io.stdout, `Forwarding to ${mcpUrl}`);
|
|
871
|
+
writeLine(io.stdout, `Credential source: ${TOKEN_ENV_VAR} or ${credentialsPath(io.env)}`);
|
|
872
|
+
return 0;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
async function handleMcpProxyRequest({ request, response, mcpUrl, token, identity, io }) {
|
|
876
|
+
const requestUrl = new URL(request.url ?? '/', `http://${request.headers.host ?? `${DEFAULT_PROXY_HOST}:${DEFAULT_PROXY_PORT}`}`);
|
|
877
|
+
if (request.method !== 'POST' || requestUrl.pathname !== '/mcp') {
|
|
878
|
+
response.statusCode = 404;
|
|
879
|
+
response.setHeader('content-type', 'application/json');
|
|
880
|
+
response.end(JSON.stringify({ error: 'not_found' }));
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
const body = await readAll(request);
|
|
885
|
+
const upstreamHeaders = {
|
|
886
|
+
accept: String(request.headers.accept || 'application/json, text/event-stream'),
|
|
887
|
+
'content-type': String(request.headers['content-type'] || 'application/json'),
|
|
888
|
+
authorization: `Bearer ${token}`,
|
|
889
|
+
[AGENT_ID_HEADER]: identity.agentId,
|
|
890
|
+
[AGENT_INSTANCE_HEADER]: identity.agentInstanceId,
|
|
891
|
+
'user-agent': `XMemo-CLI-Proxy/${CLI_VERSION} (+https://github.com/yonro/memory-os-cli)`
|
|
892
|
+
};
|
|
893
|
+
const sessionId = request.headers['mcp-session-id'];
|
|
894
|
+
if (sessionId) {
|
|
895
|
+
upstreamHeaders['mcp-session-id'] = Array.isArray(sessionId) ? sessionId[0] : sessionId;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
const upstream = await io.fetch(mcpUrl, {
|
|
899
|
+
method: 'POST',
|
|
900
|
+
headers: upstreamHeaders,
|
|
901
|
+
body
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
response.statusCode = upstream.status;
|
|
905
|
+
for (const header of ['content-type', 'mcp-session-id']) {
|
|
906
|
+
const value = upstream.headers.get(header);
|
|
907
|
+
if (value) {
|
|
908
|
+
response.setHeader(header, value);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
const buffer = Buffer.from(await upstream.arrayBuffer());
|
|
912
|
+
response.end(buffer);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
async function smokeCommand(args, io) {
|
|
916
|
+
const clientId = optionValue(args, '--client');
|
|
917
|
+
const outputJson = hasFlag(args, '--json');
|
|
918
|
+
if (!clientId) {
|
|
919
|
+
throw new UsageError('Smoke requires --client codex for this MCP-depth release.');
|
|
920
|
+
}
|
|
921
|
+
if (clientId !== 'codex') {
|
|
922
|
+
throw new UsageError('Only Codex smoke checks are available in this MCP-depth release.');
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
const configPath = optionValue(args, '--config') ?? defaultCodexConfigPath(io.env);
|
|
926
|
+
const report = await codexSmokeReport(configPath, io.env);
|
|
927
|
+
|
|
928
|
+
if (outputJson) {
|
|
929
|
+
writeLine(io.stdout, JSON.stringify(report, null, 2));
|
|
930
|
+
return report.ok ? 0 : 1;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
writeLine(io.stdout, `${PRODUCT_NAME} Codex MCP smoke: ${report.ok ? 'ok' : 'failed'}`);
|
|
934
|
+
writeLine(io.stdout, `Config: ${report.configPath}`);
|
|
935
|
+
writeLine(io.stdout, `Token env: ${report.tokenEnvVar}`);
|
|
936
|
+
for (const check of report.checks) {
|
|
937
|
+
const status = check.ok ? 'OK' : check.required ? 'FAIL' : 'WARN';
|
|
938
|
+
writeLine(io.stdout, ` ${status} ${check.name}: ${check.detail}`);
|
|
939
|
+
}
|
|
940
|
+
return report.ok ? 0 : 1;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
function envCommand(args, io) {
|
|
944
|
+
const subcommand = args[0] ?? 'help';
|
|
945
|
+
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
|
|
946
|
+
writeLine(io.stdout, 'Env commands:');
|
|
947
|
+
writeLine(io.stdout, ` ${COMMAND_NAME} env example [--shell bash|powershell|cmd] [--base-url <url>] [--json]`);
|
|
948
|
+
return 0;
|
|
949
|
+
}
|
|
950
|
+
if (subcommand !== 'example') {
|
|
951
|
+
throw new UsageError(`Unknown env command: ${subcommand}`);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
const baseUrl = normalizeBaseUrl(baseUrlOption(args.slice(1), io.env));
|
|
955
|
+
const outputJson = hasFlag(args, '--json');
|
|
956
|
+
const shell = optionValue(args, '--shell') ?? (process.platform === 'win32' ? 'powershell' : 'bash');
|
|
957
|
+
const placeholder = '<paste-token-from-your-secret-store>';
|
|
958
|
+
const payload = {
|
|
959
|
+
XMEMO_URL: baseUrl,
|
|
960
|
+
XMEMO_BASE_URL: baseUrl,
|
|
961
|
+
MEMORY_OS_URL: baseUrl,
|
|
962
|
+
MEMORY_OS_BASE_URL: baseUrl,
|
|
963
|
+
[TOKEN_ENV_VAR]: placeholder,
|
|
964
|
+
[AGENT_ID_ENV_VAR]: '<agent-family>',
|
|
965
|
+
[AGENT_INSTANCE_ENV_VAR]: '<stable-random-id-for-this-local-agent>'
|
|
966
|
+
};
|
|
967
|
+
|
|
968
|
+
if (outputJson) {
|
|
969
|
+
writeLine(io.stdout, JSON.stringify(payload, null, 2));
|
|
970
|
+
return 0;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
if (shell === 'powershell') {
|
|
974
|
+
writeLine(io.stdout, `[Environment]::SetEnvironmentVariable('XMEMO_URL', '${baseUrl}', 'User')`);
|
|
975
|
+
writeLine(io.stdout, `[Environment]::SetEnvironmentVariable('XMEMO_BASE_URL', '${baseUrl}', 'User')`);
|
|
976
|
+
writeLine(io.stdout, `[Environment]::SetEnvironmentVariable('MEMORY_OS_URL', '${baseUrl}', 'User')`);
|
|
977
|
+
writeLine(io.stdout, `[Environment]::SetEnvironmentVariable('MEMORY_OS_BASE_URL', '${baseUrl}', 'User')`);
|
|
978
|
+
writeLine(io.stdout, `[Environment]::SetEnvironmentVariable('${TOKEN_ENV_VAR}', '${placeholder}', 'User')`);
|
|
979
|
+
writeLine(io.stdout, `[Environment]::SetEnvironmentVariable('${AGENT_ID_ENV_VAR}', '<agent-family>', 'User')`);
|
|
980
|
+
writeLine(io.stdout, `[Environment]::SetEnvironmentVariable('${AGENT_INSTANCE_ENV_VAR}', '<stable-random-id-for-this-local-agent>', 'User')`);
|
|
981
|
+
} else if (shell === 'cmd') {
|
|
982
|
+
writeLine(io.stdout, `setx XMEMO_URL "${baseUrl}"`);
|
|
983
|
+
writeLine(io.stdout, `setx XMEMO_BASE_URL "${baseUrl}"`);
|
|
984
|
+
writeLine(io.stdout, `setx MEMORY_OS_URL "${baseUrl}"`);
|
|
985
|
+
writeLine(io.stdout, `setx MEMORY_OS_BASE_URL "${baseUrl}"`);
|
|
986
|
+
writeLine(io.stdout, `setx ${TOKEN_ENV_VAR} "${placeholder}"`);
|
|
987
|
+
writeLine(io.stdout, `setx ${AGENT_ID_ENV_VAR} "<agent-family>"`);
|
|
988
|
+
writeLine(io.stdout, `setx ${AGENT_INSTANCE_ENV_VAR} "<stable-random-id-for-this-local-agent>"`);
|
|
989
|
+
} else {
|
|
990
|
+
writeLine(io.stdout, `export XMEMO_URL="${baseUrl}"`);
|
|
991
|
+
writeLine(io.stdout, `export XMEMO_BASE_URL="${baseUrl}"`);
|
|
992
|
+
writeLine(io.stdout, `export MEMORY_OS_URL="${baseUrl}"`);
|
|
993
|
+
writeLine(io.stdout, `export MEMORY_OS_BASE_URL="${baseUrl}"`);
|
|
994
|
+
writeLine(io.stdout, `export ${TOKEN_ENV_VAR}="${placeholder}"`);
|
|
995
|
+
writeLine(io.stdout, `export ${AGENT_ID_ENV_VAR}="<agent-family>"`);
|
|
996
|
+
writeLine(io.stdout, `export ${AGENT_INSTANCE_ENV_VAR}="<stable-random-id-for-this-local-agent>"`);
|
|
997
|
+
}
|
|
998
|
+
return 0;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
function writePrivacy(io) {
|
|
1002
|
+
writeLine(io.stdout, `${PRODUCT_NAME} CLI privacy and security defaults:`);
|
|
1003
|
+
writeLine(io.stdout, '- No telemetry or analytics.');
|
|
1004
|
+
writeLine(io.stdout, '- `status` does not send tokens.');
|
|
1005
|
+
writeLine(io.stdout, `- MCP configs reference ${TOKEN_ENV_VAR}; token values are not embedded.`);
|
|
1006
|
+
writeLine(io.stdout, `- Agent instance IDs are non-secret and stored in user-scoped config outside git.`);
|
|
1007
|
+
writeLine(io.stdout, '- `login` and `token add` store credentials in the user-scoped XMemo CLI config directory.');
|
|
1008
|
+
writeLine(io.stdout, '- Legacy `token set` plaintext storage requires explicit --allow-plaintext.');
|
|
1009
|
+
writeLine(io.stdout, '- npm publishing is restricted by package.json files whitelist.');
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
async function startDeviceLogin(baseUrl, timeoutMs, io) {
|
|
1013
|
+
const payload = await postJson(endpointUrl(baseUrl, DEVICE_LOGIN_START_PATH), {
|
|
1014
|
+
client_id: PACKAGE_NAME,
|
|
1015
|
+
cli_version: CLI_VERSION,
|
|
1016
|
+
token_type: 'mcp_token',
|
|
1017
|
+
scopes: ['memory:read', 'memory:write']
|
|
1018
|
+
}, timeoutMs, io);
|
|
1019
|
+
|
|
1020
|
+
const deviceCode = stringValue(payload, ['device_code']);
|
|
1021
|
+
const verificationUri = stringValue(payload, ['verification_uri']);
|
|
1022
|
+
if (!deviceCode || !verificationUri) {
|
|
1023
|
+
throw new UsageError(`Device login did not return device_code and verification_uri from ${baseUrl}.`);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
return {
|
|
1027
|
+
deviceCode,
|
|
1028
|
+
userCode: stringValue(payload, ['user_code']),
|
|
1029
|
+
verificationUri,
|
|
1030
|
+
verificationUriComplete: stringValue(payload, ['verification_uri_complete']),
|
|
1031
|
+
expiresIn: Number.isFinite(Number(payload.expires_in)) ? Number(payload.expires_in) : 600,
|
|
1032
|
+
interval: Number.isFinite(Number(payload.interval)) ? Math.max(1, Number(payload.interval)) : 5
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
async function pollDeviceLogin(baseUrl, start, loginTimeoutMs, httpTimeoutMs, io, options = {}) {
|
|
1037
|
+
const deadline = Date.now() + Math.min(start.expiresIn * 1000, loginTimeoutMs);
|
|
1038
|
+
const sleepFn = io.sleep ?? sleep;
|
|
1039
|
+
let intervalSeconds = start.interval;
|
|
1040
|
+
while (Date.now() <= deadline) {
|
|
1041
|
+
const payload = await postJson(endpointUrl(baseUrl, DEVICE_LOGIN_TOKEN_PATH), {
|
|
1042
|
+
device_code: start.deviceCode,
|
|
1043
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code'
|
|
1044
|
+
}, httpTimeoutMs, io, { allowDevicePending: true });
|
|
1045
|
+
|
|
1046
|
+
const accessToken = stringValue(payload, ['access_token']) ?? stringValue(payload, ['token']);
|
|
1047
|
+
if (accessToken) {
|
|
1048
|
+
validateToken(accessToken);
|
|
1049
|
+
return {
|
|
1050
|
+
accessToken,
|
|
1051
|
+
account: accountFromPayload(payload)
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
const error = stringValue(payload, ['error']);
|
|
1056
|
+
if (error && error !== 'authorization_pending' && error !== 'slow_down') {
|
|
1057
|
+
throw new UsageError(`Device login failed: ${error}`);
|
|
1058
|
+
}
|
|
1059
|
+
if (options.pollOnce) {
|
|
1060
|
+
throw new UsageError('Device login is still pending.');
|
|
1061
|
+
}
|
|
1062
|
+
if (error === 'slow_down') {
|
|
1063
|
+
intervalSeconds += 5;
|
|
1064
|
+
}
|
|
1065
|
+
await sleepFn(intervalSeconds * 1000);
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
throw new UsageError('Device login expired before authorization completed.');
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
async function storeTokenFromStdin(io, metadata = {}) {
|
|
1072
|
+
const token = (await readAll(io.stdin)).trim();
|
|
1073
|
+
validateToken(token);
|
|
1074
|
+
return await storeTokenValue(token, metadata, io.env);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
async function storeTokenValue(token, metadata, env) {
|
|
1078
|
+
validateToken(token);
|
|
1079
|
+
const credentialPath = credentialsPath(env);
|
|
1080
|
+
await writePlaintextCredential(credentialPath, token, metadata);
|
|
1081
|
+
return {
|
|
1082
|
+
ok: true,
|
|
1083
|
+
credentialPath,
|
|
1084
|
+
tokenPresent: true,
|
|
1085
|
+
tokenPrinted: false,
|
|
1086
|
+
projectFilesModified: false,
|
|
1087
|
+
storage: 'user-scoped-credential-file'
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
async function readStoredCredential(env) {
|
|
1092
|
+
const credentialPath = credentialsPath(env);
|
|
1093
|
+
const content = await readTextIfExists(credentialPath);
|
|
1094
|
+
if (!content.trim()) {
|
|
1095
|
+
return { path: credentialPath, token: null };
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
const parsed = parseJsonConfig(content, credentialPath);
|
|
1099
|
+
return {
|
|
1100
|
+
path: credentialPath,
|
|
1101
|
+
token: stringValue(parsed, ['token']),
|
|
1102
|
+
storage: stringValue(parsed, ['storage']),
|
|
1103
|
+
account: accountFromPayload(parsed.metadata)
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
function accountFromPayload(payload) {
|
|
1108
|
+
const account = payload && typeof payload === 'object'
|
|
1109
|
+
? (payload.user && typeof payload.user === 'object' ? payload.user : payload.account)
|
|
1110
|
+
: null;
|
|
1111
|
+
if (!account || typeof account !== 'object') {
|
|
1112
|
+
return null;
|
|
1113
|
+
}
|
|
1114
|
+
const userId = stringValue(account, ['user_id']) ?? stringValue(account, ['id']) ?? stringValue(account, ['userId']);
|
|
1115
|
+
const email = stringValue(account, ['email']);
|
|
1116
|
+
const displayName = stringValue(account, ['display_name']) ?? stringValue(account, ['name']) ?? stringValue(account, ['displayName']);
|
|
1117
|
+
if (!userId && !email && !displayName) {
|
|
1118
|
+
return null;
|
|
1119
|
+
}
|
|
1120
|
+
return {
|
|
1121
|
+
userId: userId ?? null,
|
|
1122
|
+
email: email ?? null,
|
|
1123
|
+
displayName: displayName ?? null
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
function formatAccount(account) {
|
|
1128
|
+
const label = account.displayName || account.email || account.userId || 'XMemo account';
|
|
1129
|
+
return account.email && account.displayName ? `${account.displayName} <${account.email}>` : label;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
async function resolveCredentialToken(env) {
|
|
1133
|
+
const environmentToken = env[TOKEN_ENV_VAR] ?? env[LEGACY_TOKEN_ENV_VAR];
|
|
1134
|
+
if (environmentToken) {
|
|
1135
|
+
return environmentToken;
|
|
1136
|
+
}
|
|
1137
|
+
const credential = await readStoredCredential(env);
|
|
1138
|
+
return credential.token;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
async function verifyTokenWithMcp(baseUrl, token, timeoutMs, io) {
|
|
1142
|
+
const url = endpointUrl(baseUrl, '/mcp');
|
|
1143
|
+
const controller = new AbortController();
|
|
1144
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
1145
|
+
try {
|
|
1146
|
+
const response = await io.fetch(url, {
|
|
1147
|
+
method: 'POST',
|
|
1148
|
+
headers: {
|
|
1149
|
+
accept: 'application/json, text/event-stream',
|
|
1150
|
+
'content-type': 'application/json',
|
|
1151
|
+
authorization: `Bearer ${token}`,
|
|
1152
|
+
'user-agent': `XMemo-CLI/${CLI_VERSION} (+https://github.com/yonro/memory-os-cli)`
|
|
1153
|
+
},
|
|
1154
|
+
body: JSON.stringify({
|
|
1155
|
+
jsonrpc: '2.0',
|
|
1156
|
+
id: 1,
|
|
1157
|
+
method: 'initialize',
|
|
1158
|
+
params: {
|
|
1159
|
+
protocolVersion: '2024-11-05',
|
|
1160
|
+
capabilities: {},
|
|
1161
|
+
clientInfo: { name: COMMAND_NAME, version: CLI_VERSION }
|
|
1162
|
+
}
|
|
1163
|
+
}),
|
|
1164
|
+
signal: controller.signal
|
|
1165
|
+
});
|
|
1166
|
+
return {
|
|
1167
|
+
ok: response.ok,
|
|
1168
|
+
detail: response.ok ? `HTTP ${response.status}` : `HTTP ${response.status}`
|
|
1169
|
+
};
|
|
1170
|
+
} catch (error) {
|
|
1171
|
+
return {
|
|
1172
|
+
ok: false,
|
|
1173
|
+
detail: error.name === 'AbortError' ? `timeout after ${timeoutMs}ms` : error.message
|
|
1174
|
+
};
|
|
1175
|
+
} finally {
|
|
1176
|
+
clearTimeout(timeout);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
async function probe(url, timeoutMs, io) {
|
|
1181
|
+
if (typeof io.fetch !== 'function') {
|
|
1182
|
+
return { url, ok: false, error: 'fetch unavailable in this Node runtime' };
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
const controller = new AbortController();
|
|
1186
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
1187
|
+
|
|
1188
|
+
try {
|
|
1189
|
+
const response = await io.fetch(url, {
|
|
1190
|
+
headers: { accept: 'application/json' },
|
|
1191
|
+
signal: controller.signal
|
|
1192
|
+
});
|
|
1193
|
+
return { url, ok: response.ok, status: response.status };
|
|
1194
|
+
} catch (error) {
|
|
1195
|
+
return {
|
|
1196
|
+
url,
|
|
1197
|
+
ok: false,
|
|
1198
|
+
error: error.name === 'AbortError' ? `timeout after ${timeoutMs}ms` : error.message
|
|
1199
|
+
};
|
|
1200
|
+
} finally {
|
|
1201
|
+
clearTimeout(timeout);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
async function fetchJson(url, timeoutMs, io) {
|
|
1206
|
+
if (typeof io.fetch !== 'function') {
|
|
1207
|
+
throw new UsageError('This Node runtime does not provide fetch; use Node.js 20 or newer.');
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
const controller = new AbortController();
|
|
1211
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
1212
|
+
|
|
1213
|
+
try {
|
|
1214
|
+
const response = await io.fetch(url, {
|
|
1215
|
+
headers: { accept: 'application/json' },
|
|
1216
|
+
signal: controller.signal
|
|
1217
|
+
});
|
|
1218
|
+
if (!response.ok) {
|
|
1219
|
+
throw new UsageError(`Discovery request failed with HTTP ${response.status}: ${url}`);
|
|
1220
|
+
}
|
|
1221
|
+
return await response.json();
|
|
1222
|
+
} catch (error) {
|
|
1223
|
+
if (error instanceof UsageError) {
|
|
1224
|
+
throw error;
|
|
1225
|
+
}
|
|
1226
|
+
const reason = error.name === 'AbortError' ? `timeout after ${timeoutMs}ms` : error.message;
|
|
1227
|
+
throw new UsageError(`Discovery request failed: ${url} (${reason})`);
|
|
1228
|
+
} finally {
|
|
1229
|
+
clearTimeout(timeout);
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
async function postJson(url, payload, timeoutMs, io, options = {}) {
|
|
1234
|
+
if (typeof io.fetch !== 'function') {
|
|
1235
|
+
throw new UsageError('This Node runtime does not provide fetch; use Node.js 20 or newer.');
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
const controller = new AbortController();
|
|
1239
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
1240
|
+
|
|
1241
|
+
try {
|
|
1242
|
+
const response = await io.fetch(url, {
|
|
1243
|
+
method: 'POST',
|
|
1244
|
+
headers: {
|
|
1245
|
+
accept: 'application/json',
|
|
1246
|
+
'content-type': 'application/json'
|
|
1247
|
+
},
|
|
1248
|
+
body: JSON.stringify(payload),
|
|
1249
|
+
signal: controller.signal
|
|
1250
|
+
});
|
|
1251
|
+
const responsePayload = await response.json();
|
|
1252
|
+
if (!response.ok) {
|
|
1253
|
+
const error = stringValue(responsePayload, ['error']) ?? stringValue(responsePayload, ['detail']) ?? `HTTP ${response.status}`;
|
|
1254
|
+
if (options.allowDevicePending && (error === 'authorization_pending' || error === 'slow_down')) {
|
|
1255
|
+
return { error };
|
|
1256
|
+
}
|
|
1257
|
+
throw new UsageError(`Request failed with HTTP ${response.status}: ${url} (${error})`);
|
|
1258
|
+
}
|
|
1259
|
+
return responsePayload;
|
|
1260
|
+
} catch (error) {
|
|
1261
|
+
if (error instanceof UsageError) {
|
|
1262
|
+
throw error;
|
|
1263
|
+
}
|
|
1264
|
+
const reason = error.name === 'AbortError' ? `timeout after ${timeoutMs}ms` : error.message;
|
|
1265
|
+
throw new UsageError(`Request failed: ${url} (${reason})`);
|
|
1266
|
+
} finally {
|
|
1267
|
+
clearTimeout(timeout);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
function ensureDiscoveryService(discovery, discoveryUrl) {
|
|
1272
|
+
const service = stringValue(discovery, ['service']);
|
|
1273
|
+
if (service && service !== 'memory-os') {
|
|
1274
|
+
throw new UsageError(`Discovery document at ${discoveryUrl} is for '${service}', not 'memory-os'.`);
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
function buildSetupPlan({ baseUrl, discoveryUrl, statusUrl, discovery, status }) {
|
|
1279
|
+
const apiBase = stringValue(discovery, ['urls', 'api_base'])
|
|
1280
|
+
?? stringValue(discovery, ['api_base_url'])
|
|
1281
|
+
?? baseUrl;
|
|
1282
|
+
const mcpUrl = stringValue(discovery, ['urls', 'mcp'])
|
|
1283
|
+
?? stringValue(discovery, ['mcp_url'])
|
|
1284
|
+
?? endpointUrl(apiBase, '/mcp');
|
|
1285
|
+
const tokenPortalUrl = stringValue(discovery, ['urls', 'token_portal'])
|
|
1286
|
+
?? stringValue(discovery, ['token_portal_url'])
|
|
1287
|
+
?? stringValue(status, ['requirements', 'token_portal_url']);
|
|
1288
|
+
const tokenEnvVar = stringValue(discovery, ['auth', 'token_env_var'])
|
|
1289
|
+
?? stringValue(status, ['requirements', 'token_env_var'])
|
|
1290
|
+
?? TOKEN_ENV_VAR;
|
|
1291
|
+
|
|
1292
|
+
return {
|
|
1293
|
+
schemaVersion: '1.0',
|
|
1294
|
+
baseUrl,
|
|
1295
|
+
discoveryUrl,
|
|
1296
|
+
statusUrl,
|
|
1297
|
+
apiBase,
|
|
1298
|
+
mcpUrl,
|
|
1299
|
+
guideUrl: stringValue(discovery, ['urls', 'guide']) ?? endpointUrl(apiBase, '/guide'),
|
|
1300
|
+
docsUrl: stringValue(discovery, ['urls', 'docs']),
|
|
1301
|
+
tokenPortalUrl,
|
|
1302
|
+
tokenEnvVar,
|
|
1303
|
+
onboardingReady: booleanValue(status, ['ready']),
|
|
1304
|
+
supportedClients: discoveryMcpClients(discovery),
|
|
1305
|
+
localClients: supportedMcpClients(),
|
|
1306
|
+
privacy: {
|
|
1307
|
+
telemetry: false,
|
|
1308
|
+
tokenSent: false,
|
|
1309
|
+
tokenEmbeddedInConfig: false
|
|
1310
|
+
},
|
|
1311
|
+
boundaries: {
|
|
1312
|
+
clientAllowed: arrayValue(discovery, ['agent_boundary', 'client_allowed'])
|
|
1313
|
+
?? arrayValue(status, ['agent_boundary', 'client_allowed'])
|
|
1314
|
+
?? [],
|
|
1315
|
+
adminRequired: arrayValue(discovery, ['agent_boundary', 'admin_required'])
|
|
1316
|
+
?? arrayValue(status, ['agent_boundary', 'admin_required'])
|
|
1317
|
+
?? []
|
|
1318
|
+
}
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
async function bestEffortRootVersion(discovery, timeoutMs, io) {
|
|
1323
|
+
const rootDiscoveryUrl = stringValue(discovery, ['urls', 'root_discovery']);
|
|
1324
|
+
if (!rootDiscoveryUrl) {
|
|
1325
|
+
return {};
|
|
1326
|
+
}
|
|
1327
|
+
try {
|
|
1328
|
+
const rootDiscovery = await fetchJson(rootDiscoveryUrl, timeoutMs, io);
|
|
1329
|
+
return { version: stringValue(rootDiscovery, ['version']) ?? undefined };
|
|
1330
|
+
} catch (error) {
|
|
1331
|
+
return { error: error.message };
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
function discoveryMcpUrl(discovery, baseUrl) {
|
|
1336
|
+
return stringValue(discovery, ['api', 'mcp', 'url'])
|
|
1337
|
+
?? stringValue(discovery, ['urls', 'mcp'])
|
|
1338
|
+
?? endpointUrl(baseUrl, '/mcp');
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
function agentDiscoveryClientIds(discovery) {
|
|
1342
|
+
const clients = Array.isArray(discovery?.clients) ? discovery.clients : [];
|
|
1343
|
+
const ids = clients
|
|
1344
|
+
.filter((client) => isPlainObject(client) && typeof client.id === 'string')
|
|
1345
|
+
.map((client) => client.id);
|
|
1346
|
+
if (ids.length > 0) {
|
|
1347
|
+
return ids;
|
|
1348
|
+
}
|
|
1349
|
+
const supported = arrayValue(discovery, ['supported_clients']);
|
|
1350
|
+
return supported ?? [];
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
function mcpConfigTemplate(clientId, mcpUrl) {
|
|
1354
|
+
if (clientId === 'codex') {
|
|
1355
|
+
return {
|
|
1356
|
+
client: clientId,
|
|
1357
|
+
serverName: MCP_SERVER_NAME,
|
|
1358
|
+
snippetFormat: 'toml',
|
|
1359
|
+
snippet: codexTomlSnippet(mcpUrl),
|
|
1360
|
+
requiresEnv: [TOKEN_ENV_VAR],
|
|
1361
|
+
optionalEnv: [AGENT_INSTANCE_ENV_VAR],
|
|
1362
|
+
agentIdentity: {
|
|
1363
|
+
agentId: 'codex',
|
|
1364
|
+
agentIdHeader: AGENT_ID_HEADER,
|
|
1365
|
+
agentInstanceEnvVar: AGENT_INSTANCE_ENV_VAR,
|
|
1366
|
+
agentInstanceHeader: AGENT_INSTANCE_HEADER
|
|
1367
|
+
},
|
|
1368
|
+
writesTokenValue: false
|
|
1369
|
+
};
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
if (clientId === 'gemini-cli') {
|
|
1373
|
+
return oauthJsonMcpTemplate(clientId, mcpUrl, geminiJsonConfig(mcpUrl));
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
if (clientId === 'antigravity') {
|
|
1377
|
+
return oauthJsonMcpTemplate(clientId, mcpUrl, antigravityJsonConfig(mcpUrl));
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
const serverName = clientId === 'cursor' || clientId === 'gemini-cli' || clientId === 'antigravity' ? 'memory_os' : 'memory-os';
|
|
1381
|
+
return {
|
|
1382
|
+
client: clientId,
|
|
1383
|
+
serverName,
|
|
1384
|
+
snippetFormat: 'json',
|
|
1385
|
+
snippet: {
|
|
1386
|
+
mcpServers: {
|
|
1387
|
+
[serverName]: {
|
|
1388
|
+
type: 'http',
|
|
1389
|
+
url: mcpUrl,
|
|
1390
|
+
headers: {
|
|
1391
|
+
Authorization: `Bearer \${${TOKEN_ENV_VAR}}`,
|
|
1392
|
+
[AGENT_ID_HEADER]: clientId,
|
|
1393
|
+
[AGENT_INSTANCE_HEADER]: `\${${AGENT_INSTANCE_ENV_VAR}}`
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
},
|
|
1398
|
+
requiresEnv: [TOKEN_ENV_VAR],
|
|
1399
|
+
optionalEnv: [AGENT_INSTANCE_ENV_VAR],
|
|
1400
|
+
agentIdentity: {
|
|
1401
|
+
agentId: clientId,
|
|
1402
|
+
agentIdHeader: AGENT_ID_HEADER,
|
|
1403
|
+
agentInstanceEnvVar: AGENT_INSTANCE_ENV_VAR,
|
|
1404
|
+
agentInstanceHeader: AGENT_INSTANCE_HEADER
|
|
1405
|
+
},
|
|
1406
|
+
writesTokenValue: false
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
function oauthJsonMcpTemplate(clientId, mcpUrl, snippet) {
|
|
1411
|
+
return {
|
|
1412
|
+
client: clientId,
|
|
1413
|
+
serverName: MCP_SERVER_NAME,
|
|
1414
|
+
snippetFormat: 'json',
|
|
1415
|
+
snippet,
|
|
1416
|
+
requiresEnv: [],
|
|
1417
|
+
optionalEnv: [AGENT_INSTANCE_ENV_VAR],
|
|
1418
|
+
authentication: 'oauth',
|
|
1419
|
+
agentIdentity: {
|
|
1420
|
+
agentId: clientId,
|
|
1421
|
+
agentIdHeader: AGENT_ID_HEADER,
|
|
1422
|
+
agentInstanceEnvVar: AGENT_INSTANCE_ENV_VAR,
|
|
1423
|
+
agentInstanceHeader: AGENT_INSTANCE_HEADER
|
|
1424
|
+
},
|
|
1425
|
+
mcpUrl,
|
|
1426
|
+
writesTokenValue: false
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
function mcpLocalProxyTemplate(clientId, proxyUrl) {
|
|
1431
|
+
const serverName = clientId === 'cursor' || clientId === 'gemini-cli' || clientId === 'antigravity' ? 'memory_os' : 'memory-os';
|
|
1432
|
+
return {
|
|
1433
|
+
client: clientId,
|
|
1434
|
+
serverName,
|
|
1435
|
+
snippetFormat: 'json',
|
|
1436
|
+
snippet: {
|
|
1437
|
+
mcpServers: {
|
|
1438
|
+
[serverName]: {
|
|
1439
|
+
type: 'http',
|
|
1440
|
+
url: proxyUrl
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
},
|
|
1444
|
+
requiresCredential: [`${COMMAND_NAME} login`, `${COMMAND_NAME} token add --from-stdin`],
|
|
1445
|
+
requiresLocalCommand: `${COMMAND_NAME} mcp proxy --port ${new URL(proxyUrl).port || DEFAULT_PROXY_PORT}`,
|
|
1446
|
+
agentIdentity: {
|
|
1447
|
+
agentId: clientId,
|
|
1448
|
+
agentIdHeader: AGENT_ID_HEADER,
|
|
1449
|
+
agentInstanceEnvVar: AGENT_INSTANCE_ENV_VAR,
|
|
1450
|
+
agentInstanceHeader: AGENT_INSTANCE_HEADER
|
|
1451
|
+
},
|
|
1452
|
+
writesTokenValue: false
|
|
1453
|
+
};
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
function sameMajorMinor(left, right) {
|
|
1457
|
+
const leftParts = left.split('.');
|
|
1458
|
+
const rightParts = right.split('.');
|
|
1459
|
+
return leftParts[0] === rightParts[0] && leftParts[1] === rightParts[1];
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
function baseUrlOption(args, env) {
|
|
1463
|
+
return optionValue(args, '--base-url')
|
|
1464
|
+
?? optionValue(args, '--url')
|
|
1465
|
+
?? env.XMEMO_BASE_URL
|
|
1466
|
+
?? env.XMEMO_URL
|
|
1467
|
+
?? env.MEMORY_OS_BASE_URL
|
|
1468
|
+
?? env.MEMORY_OS_URL
|
|
1469
|
+
?? DEFAULT_SERVICE_URL;
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
function clientSetupPlan(clientId, client, mcpUrl, env, identity) {
|
|
1473
|
+
return {
|
|
1474
|
+
id: clientId,
|
|
1475
|
+
label: client.label,
|
|
1476
|
+
configKind: client.configKind,
|
|
1477
|
+
configPath: client.defaultConfigPath(env),
|
|
1478
|
+
serverName: MCP_SERVER_NAME,
|
|
1479
|
+
mcpUrl,
|
|
1480
|
+
tokenEnvVar: TOKEN_ENV_VAR,
|
|
1481
|
+
agentId: identity.agentId,
|
|
1482
|
+
agentInstanceId: identity.agentInstanceId,
|
|
1483
|
+
agentInstanceIdPath: identity.path,
|
|
1484
|
+
writesTokenValue: false,
|
|
1485
|
+
written: false
|
|
1486
|
+
};
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
function copilotSetupPlan(mcpUrl, proxyPort, env) {
|
|
1490
|
+
const proxyUrl = `http://${DEFAULT_PROXY_HOST}:${proxyPort}/mcp`;
|
|
1491
|
+
const template = mcpLocalProxyTemplate('copilot-cli', proxyUrl);
|
|
1492
|
+
return {
|
|
1493
|
+
id: 'copilot-cli',
|
|
1494
|
+
label: 'Copilot CLI',
|
|
1495
|
+
configKind: 'local-proxy',
|
|
1496
|
+
configPath: defaultCopilotConfigPath(env),
|
|
1497
|
+
serverName: template.serverName,
|
|
1498
|
+
mcpUrl,
|
|
1499
|
+
proxyUrl,
|
|
1500
|
+
tokenEnvVar: TOKEN_ENV_VAR,
|
|
1501
|
+
requiresCredential: template.requiresCredential,
|
|
1502
|
+
requiresLocalCommand: template.requiresLocalCommand,
|
|
1503
|
+
template: template.snippet,
|
|
1504
|
+
agentId: template.agentIdentity.agentId,
|
|
1505
|
+
writesTokenValue: false,
|
|
1506
|
+
writeSupported: true,
|
|
1507
|
+
written: false
|
|
1508
|
+
};
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
function writeSetupSummary(plan, io) {
|
|
1512
|
+
writeLine(io.stdout, `${PRODUCT_NAME} setup discovery: ${plan.baseUrl}`);
|
|
1513
|
+
writeLine(io.stdout, ` API: ${plan.apiBase}`);
|
|
1514
|
+
writeLine(io.stdout, ` MCP: ${plan.mcpUrl}`);
|
|
1515
|
+
writeLine(io.stdout, ` Guide: ${plan.guideUrl}`);
|
|
1516
|
+
if (plan.docsUrl) {
|
|
1517
|
+
writeLine(io.stdout, ` Docs: ${plan.docsUrl}`);
|
|
1518
|
+
}
|
|
1519
|
+
if (plan.tokenPortalUrl) {
|
|
1520
|
+
writeLine(io.stdout, ` Token portal: ${plan.tokenPortalUrl}`);
|
|
1521
|
+
}
|
|
1522
|
+
writeLine(io.stdout, ` Token env var: ${plan.tokenEnvVar}`);
|
|
1523
|
+
writeLine(io.stdout, ` Onboarding ready: ${plan.onboardingReady === null ? 'unknown' : plan.onboardingReady}`);
|
|
1524
|
+
writeLine(io.stdout, 'Privacy: telemetry disabled; no token sent; generated config references env vars only.');
|
|
1525
|
+
|
|
1526
|
+
if (plan.boundaries.adminRequired.length > 0) {
|
|
1527
|
+
writeLine(io.stdout, `Admin-only actions: ${plan.boundaries.adminRequired.join(', ')}`);
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
if (plan.selectedClient) {
|
|
1531
|
+
writeLine(io.stdout, '');
|
|
1532
|
+
writeLine(io.stdout, `Selected client: ${plan.selectedClient.label}`);
|
|
1533
|
+
writeLine(io.stdout, ` Config path: ${plan.selectedClient.configPath}`);
|
|
1534
|
+
writeLine(io.stdout, ` Written: ${plan.selectedClient.written}`);
|
|
1535
|
+
writeLine(io.stdout, ` Token value embedded: ${plan.selectedClient.writesTokenValue}`);
|
|
1536
|
+
writeLine(io.stdout, ` Agent ID: ${plan.selectedClient.agentId}`);
|
|
1537
|
+
if (plan.selectedClient.agentInstanceIdPath) {
|
|
1538
|
+
writeLine(io.stdout, ` Agent instance ID stored: ${plan.selectedClient.agentInstanceIdPath}`);
|
|
1539
|
+
}
|
|
1540
|
+
if (plan.selectedClient.configKind === 'local-proxy') {
|
|
1541
|
+
writeLine(io.stdout, ` Local proxy: ${plan.selectedClient.requiresLocalCommand}`);
|
|
1542
|
+
if (plan.selectedClient.written) {
|
|
1543
|
+
writeLine(io.stdout, ` Next: keep \`${plan.selectedClient.requiresLocalCommand}\` running while you use Copilot CLI.`);
|
|
1544
|
+
writeLine(io.stdout, ' If Copilot CLI is already open, reload MCP config or restart Copilot CLI.');
|
|
1545
|
+
} else {
|
|
1546
|
+
writeLine(io.stdout, ' MCP template:');
|
|
1547
|
+
writeLine(io.stdout, JSON.stringify(plan.selectedClient.template, null, 2));
|
|
1548
|
+
writeLine(io.stdout, ` Next: ${COMMAND_NAME} setup copilot --url ${plan.baseUrl}`);
|
|
1549
|
+
}
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
if (plan.selectedClient.codexProfile) {
|
|
1553
|
+
const profile = plan.selectedClient.codexProfile;
|
|
1554
|
+
writeLine(io.stdout, ` Codex profile target: ${profile.targetPath}`);
|
|
1555
|
+
writeLine(io.stdout, ` Codex profile installed: ${profile.written}`);
|
|
1556
|
+
writeLine(io.stdout, ` Codex profile changed: ${profile.changed}`);
|
|
1557
|
+
if (!profile.written) {
|
|
1558
|
+
writeLine(io.stdout, ` Profile preview: ${COMMAND_NAME} profile install codex --target ${profile.targetPath}`);
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
if (!plan.selectedClient.written) {
|
|
1562
|
+
writeLine(io.stdout, ` Next: ${COMMAND_NAME} setup ${plan.selectedClient.id} --url ${plan.baseUrl}`);
|
|
1563
|
+
}
|
|
1564
|
+
return;
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
writeLine(io.stdout, '');
|
|
1568
|
+
writeLine(io.stdout, 'Next steps:');
|
|
1569
|
+
writeLine(io.stdout, ` 1. Create a scoped token in the token portal and store it in ${plan.tokenEnvVar}.`);
|
|
1570
|
+
writeLine(io.stdout, ` 2. Configure a client, for example: ${COMMAND_NAME} setup codex --url ${plan.baseUrl}`);
|
|
1571
|
+
writeLine(io.stdout, ` 3. Run ${COMMAND_NAME} status to smoke-test the service without sending the token.`);
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
function discoveryMcpClients(discovery) {
|
|
1575
|
+
const clients = discovery?.clients?.mcp;
|
|
1576
|
+
if (!Array.isArray(clients)) {
|
|
1577
|
+
return [];
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
return clients
|
|
1581
|
+
.filter((client) => isPlainObject(client) && typeof client.id === 'string')
|
|
1582
|
+
.map((client) => ({
|
|
1583
|
+
id: client.id,
|
|
1584
|
+
configEndpoint: typeof client.config_endpoint === 'string' ? client.config_endpoint : null
|
|
1585
|
+
}));
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
function normalizeBaseUrl(input) {
|
|
1589
|
+
try {
|
|
1590
|
+
const parsed = new URL(input);
|
|
1591
|
+
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
|
|
1592
|
+
throw new UsageError('URL must use http or https.');
|
|
1593
|
+
}
|
|
1594
|
+
parsed.hash = '';
|
|
1595
|
+
parsed.search = '';
|
|
1596
|
+
return parsed.toString().replace(/\/$/, '');
|
|
1597
|
+
} catch (error) {
|
|
1598
|
+
if (error instanceof UsageError) {
|
|
1599
|
+
throw error;
|
|
1600
|
+
}
|
|
1601
|
+
throw new UsageError(`Invalid URL: ${input}`);
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
function endpointUrl(baseUrl, pathname) {
|
|
1606
|
+
const url = new URL(baseUrl);
|
|
1607
|
+
url.pathname = pathname;
|
|
1608
|
+
url.hash = '';
|
|
1609
|
+
url.search = '';
|
|
1610
|
+
return url.toString();
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
function codexTomlSnippet(mcpUrl) {
|
|
1614
|
+
return `[mcp_servers.${MCP_SERVER_NAME}]
|
|
1615
|
+
url = "${escapeTomlString(mcpUrl)}"
|
|
1616
|
+
bearer_token_env_var = "${TOKEN_ENV_VAR}"
|
|
1617
|
+
`;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
function codexMemoryProfile() {
|
|
1621
|
+
return {
|
|
1622
|
+
client: 'codex',
|
|
1623
|
+
profileVersion: 'codex-mcp-depth-v1',
|
|
1624
|
+
mcpServerName: MCP_SERVER_NAME,
|
|
1625
|
+
requiredTokenEnv: TOKEN_ENV_VAR,
|
|
1626
|
+
objective: 'Use XMemo deliberately through MCP for project context recall and high-signal write-back.',
|
|
1627
|
+
instructions: [
|
|
1628
|
+
'At the start of a non-trivial task, call XMemo recall/search for relevant project decisions, conventions, prior fixes, and active context unless the user explicitly asks not to use memory.',
|
|
1629
|
+
'Use recalled memories as evidence, not as unquestioned truth. Prefer current repository files when memory conflicts with code.',
|
|
1630
|
+
'After meaningful decisions, bug fixes, release steps, or durable conventions, write a concise XMemo memory with scope, source, and no secret values.',
|
|
1631
|
+
'Never store tokens, API keys, cookies, private keys, raw credentials, or sensitive customer data in XMemo.',
|
|
1632
|
+
'For routine or low-signal output, skip durable writes. Prefer summarized procedural or semantic memories over verbose logs.',
|
|
1633
|
+
'Keep XMemo authentication through the XMEMO_KEY environment variable; do not paste token values into prompts, config files, or logs.'
|
|
1634
|
+
],
|
|
1635
|
+
setupCommand: `${COMMAND_NAME} setup codex --url "$XMEMO_URL"`,
|
|
1636
|
+
smokeCommand: `${COMMAND_NAME} smoke --client codex`
|
|
1637
|
+
};
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
function writeCodexMemoryProfile(profile, io) {
|
|
1641
|
+
writeLine(io.stdout, `${PRODUCT_NAME} Codex memory behavior profile`);
|
|
1642
|
+
writeLine(io.stdout, `Profile: ${profile.profileVersion}`);
|
|
1643
|
+
writeLine(io.stdout, `MCP server: ${profile.mcpServerName}`);
|
|
1644
|
+
writeLine(io.stdout, `Token env: ${profile.requiredTokenEnv}`);
|
|
1645
|
+
writeLine(io.stdout, '');
|
|
1646
|
+
writeLine(io.stdout, 'Recommended Codex instructions:');
|
|
1647
|
+
for (const instruction of profile.instructions) {
|
|
1648
|
+
writeLine(io.stdout, `- ${instruction}`);
|
|
1649
|
+
}
|
|
1650
|
+
writeLine(io.stdout, '');
|
|
1651
|
+
writeLine(io.stdout, `Setup: ${profile.setupCommand}`);
|
|
1652
|
+
writeLine(io.stdout, `Smoke test: ${profile.smokeCommand}`);
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
function codexProfileInstructionText() {
|
|
1656
|
+
const profile = codexMemoryProfile();
|
|
1657
|
+
const lines = [
|
|
1658
|
+
'## XMemo Codex profile',
|
|
1659
|
+
'',
|
|
1660
|
+
`MCP server: \`${profile.mcpServerName}\``,
|
|
1661
|
+
`Token env var: \`${profile.requiredTokenEnv}\``,
|
|
1662
|
+
'',
|
|
1663
|
+
profile.objective,
|
|
1664
|
+
'',
|
|
1665
|
+
'Recommended Codex behavior:'
|
|
1666
|
+
];
|
|
1667
|
+
for (const instruction of profile.instructions) {
|
|
1668
|
+
lines.push(`- ${instruction}`);
|
|
1669
|
+
}
|
|
1670
|
+
lines.push('');
|
|
1671
|
+
return `${lines.join('\n')}\n`;
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
function codexProfileMarkerBlock() {
|
|
1675
|
+
return `${CODEX_PROFILE_MARKER_START}\n${codexProfileInstructionText()}${CODEX_PROFILE_MARKER_END}\n`;
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
function defaultCodexProfileTarget() {
|
|
1679
|
+
return path.resolve(process.cwd(), CODEX_PROFILE_TARGET);
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
async function codexProfileInstallResult(targetPath, options = {}) {
|
|
1683
|
+
const resolvedTarget = path.resolve(targetPath);
|
|
1684
|
+
const existing = await readTextIfExists(resolvedTarget);
|
|
1685
|
+
const marker = markerBounds(existing);
|
|
1686
|
+
const block = codexProfileMarkerBlock();
|
|
1687
|
+
let nextText;
|
|
1688
|
+
|
|
1689
|
+
if (marker.present) {
|
|
1690
|
+
nextText = `${existing.slice(0, marker.start)}${block}${existing.slice(marker.end)}`;
|
|
1691
|
+
} else if (existing.trim().length === 0) {
|
|
1692
|
+
nextText = block;
|
|
1693
|
+
} else {
|
|
1694
|
+
const separator = existing.endsWith('\n') ? '\n' : '\n\n';
|
|
1695
|
+
nextText = `${existing}${separator}${block}`;
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
const changed = nextText !== existing;
|
|
1699
|
+
const write = Boolean(options.write);
|
|
1700
|
+
if (write && changed) {
|
|
1701
|
+
await fs.mkdir(path.dirname(resolvedTarget), { recursive: true });
|
|
1702
|
+
await fs.writeFile(resolvedTarget, nextText);
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
return {
|
|
1706
|
+
client: 'codex',
|
|
1707
|
+
action: 'install',
|
|
1708
|
+
targetPath: resolvedTarget,
|
|
1709
|
+
markerStart: CODEX_PROFILE_MARKER_START,
|
|
1710
|
+
markerEnd: CODEX_PROFILE_MARKER_END,
|
|
1711
|
+
installed: marker.present || (write && changed),
|
|
1712
|
+
written: write,
|
|
1713
|
+
changed,
|
|
1714
|
+
markerPresent: marker.present,
|
|
1715
|
+
writesTokenValue: false
|
|
1716
|
+
};
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
async function codexProfileStatusResult(targetPath) {
|
|
1720
|
+
const resolvedTarget = path.resolve(targetPath);
|
|
1721
|
+
const existing = await readTextIfExists(resolvedTarget);
|
|
1722
|
+
const marker = markerBounds(existing);
|
|
1723
|
+
return {
|
|
1724
|
+
client: 'codex',
|
|
1725
|
+
action: 'status',
|
|
1726
|
+
targetPath: resolvedTarget,
|
|
1727
|
+
installed: marker.present,
|
|
1728
|
+
markerPresent: marker.present,
|
|
1729
|
+
markerStart: CODEX_PROFILE_MARKER_START,
|
|
1730
|
+
markerEnd: CODEX_PROFILE_MARKER_END,
|
|
1731
|
+
writesTokenValue: false
|
|
1732
|
+
};
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
async function codexProfileUninstallResult(targetPath, options = {}) {
|
|
1736
|
+
const resolvedTarget = path.resolve(targetPath);
|
|
1737
|
+
const existing = await readTextIfExists(resolvedTarget);
|
|
1738
|
+
const marker = markerBounds(existing);
|
|
1739
|
+
const write = Boolean(options.write);
|
|
1740
|
+
let changed = false;
|
|
1741
|
+
|
|
1742
|
+
if (marker.present) {
|
|
1743
|
+
let nextText = `${existing.slice(0, marker.start)}${existing.slice(marker.end)}`;
|
|
1744
|
+
nextText = nextText.replace(/\n{3,}/g, '\n\n');
|
|
1745
|
+
if (nextText.trim().length === 0) {
|
|
1746
|
+
nextText = '';
|
|
1747
|
+
} else if (!nextText.endsWith('\n')) {
|
|
1748
|
+
nextText = `${nextText}\n`;
|
|
1749
|
+
}
|
|
1750
|
+
changed = nextText !== existing;
|
|
1751
|
+
if (write && changed) {
|
|
1752
|
+
await fs.writeFile(resolvedTarget, nextText);
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
return {
|
|
1757
|
+
client: 'codex',
|
|
1758
|
+
action: 'uninstall',
|
|
1759
|
+
targetPath: resolvedTarget,
|
|
1760
|
+
installed: marker.present && !(write && changed),
|
|
1761
|
+
written: write,
|
|
1762
|
+
changed,
|
|
1763
|
+
markerPresent: marker.present,
|
|
1764
|
+
markerStart: CODEX_PROFILE_MARKER_START,
|
|
1765
|
+
markerEnd: CODEX_PROFILE_MARKER_END,
|
|
1766
|
+
writesTokenValue: false
|
|
1767
|
+
};
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
function markerBounds(content) {
|
|
1771
|
+
const start = content.indexOf(CODEX_PROFILE_MARKER_START);
|
|
1772
|
+
const end = content.indexOf(CODEX_PROFILE_MARKER_END);
|
|
1773
|
+
if (start === -1 && end === -1) {
|
|
1774
|
+
return { present: false, start: -1, end: -1 };
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
if (start === -1 || end === -1 || end < start) {
|
|
1778
|
+
throw new UsageError('Codex profile markers are incomplete or out of order; edit the target file manually before retrying.');
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
if (
|
|
1782
|
+
content.indexOf(CODEX_PROFILE_MARKER_START, start + CODEX_PROFILE_MARKER_START.length) !== -1
|
|
1783
|
+
|| content.indexOf(CODEX_PROFILE_MARKER_END, end + CODEX_PROFILE_MARKER_END.length) !== -1
|
|
1784
|
+
) {
|
|
1785
|
+
throw new UsageError('Codex profile markers appear more than once; edit the target file manually before retrying.');
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
const afterEnd = end + CODEX_PROFILE_MARKER_END.length;
|
|
1789
|
+
const trailingNewlineLength = content.slice(afterEnd, afterEnd + 2) === '\r\n'
|
|
1790
|
+
? 2
|
|
1791
|
+
: content.slice(afterEnd, afterEnd + 1) === '\n'
|
|
1792
|
+
? 1
|
|
1793
|
+
: 0;
|
|
1794
|
+
|
|
1795
|
+
return {
|
|
1796
|
+
present: true,
|
|
1797
|
+
start,
|
|
1798
|
+
end: afterEnd + trailingNewlineLength
|
|
1799
|
+
};
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
function writeProfileResult(action, result, io) {
|
|
1803
|
+
writeLine(io.stdout, `${PRODUCT_NAME} Codex profile ${action}`);
|
|
1804
|
+
writeLine(io.stdout, ` Target: ${result.targetPath}`);
|
|
1805
|
+
writeLine(io.stdout, ` Installed: ${result.installed}`);
|
|
1806
|
+
if ('written' in result) {
|
|
1807
|
+
writeLine(io.stdout, ` Written: ${result.written}`);
|
|
1808
|
+
writeLine(io.stdout, ` Changed: ${result.changed}`);
|
|
1809
|
+
}
|
|
1810
|
+
writeLine(io.stdout, ' Token value embedded: false');
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
async function codexSmokeReport(configPath, env) {
|
|
1814
|
+
const configText = await readTextIfExists(configPath);
|
|
1815
|
+
const block = tomlServerBlock(configText, MCP_SERVER_NAME);
|
|
1816
|
+
const mcpUrl = block ? tomlStringValue(block, 'url') : null;
|
|
1817
|
+
const bearerTokenEnvVar = block ? tomlStringValue(block, 'bearer_token_env_var') : null;
|
|
1818
|
+
const tokenValue = env[TOKEN_ENV_VAR] ?? '';
|
|
1819
|
+
const identityPath = agentInstanceIdentityPath(env, 'codex');
|
|
1820
|
+
const identityPresent = await fileExists(identityPath);
|
|
1821
|
+
const checks = [
|
|
1822
|
+
{
|
|
1823
|
+
name: 'config_present',
|
|
1824
|
+
ok: configText.trim().length > 0,
|
|
1825
|
+
required: true,
|
|
1826
|
+
detail: configText.trim().length > 0 ? 'found' : 'missing'
|
|
1827
|
+
},
|
|
1828
|
+
{
|
|
1829
|
+
name: 'memory_os_server_present',
|
|
1830
|
+
ok: Boolean(block),
|
|
1831
|
+
required: true,
|
|
1832
|
+
detail: block ? `[mcp_servers.${MCP_SERVER_NAME}]` : `missing [mcp_servers.${MCP_SERVER_NAME}]`
|
|
1833
|
+
},
|
|
1834
|
+
{
|
|
1835
|
+
name: 'mcp_url_present',
|
|
1836
|
+
ok: Boolean(mcpUrl),
|
|
1837
|
+
required: true,
|
|
1838
|
+
detail: mcpUrl ?? 'missing url'
|
|
1839
|
+
},
|
|
1840
|
+
{
|
|
1841
|
+
name: 'bearer_token_env_var',
|
|
1842
|
+
ok: bearerTokenEnvVar === TOKEN_ENV_VAR,
|
|
1843
|
+
required: true,
|
|
1844
|
+
detail: bearerTokenEnvVar ?? 'missing bearer_token_env_var'
|
|
1845
|
+
},
|
|
1846
|
+
{
|
|
1847
|
+
name: 'token_env_present',
|
|
1848
|
+
ok: Boolean(env[TOKEN_ENV_VAR]),
|
|
1849
|
+
required: true,
|
|
1850
|
+
detail: env[TOKEN_ENV_VAR] ? 'present' : `missing ${TOKEN_ENV_VAR}`
|
|
1851
|
+
},
|
|
1852
|
+
{
|
|
1853
|
+
name: 'token_not_embedded_in_config',
|
|
1854
|
+
ok: !tokenValue || !configText.includes(tokenValue),
|
|
1855
|
+
required: true,
|
|
1856
|
+
detail: 'token value not printed or embedded'
|
|
1857
|
+
},
|
|
1858
|
+
{
|
|
1859
|
+
name: 'agent_instance_identity_file',
|
|
1860
|
+
ok: identityPresent,
|
|
1861
|
+
required: false,
|
|
1862
|
+
detail: identityPresent ? identityPath : `optional; create with ${COMMAND_NAME} mcp add codex --write (${identityPath})`
|
|
1863
|
+
}
|
|
1864
|
+
];
|
|
1865
|
+
|
|
1866
|
+
return {
|
|
1867
|
+
ok: checks.every((check) => !check.required || check.ok),
|
|
1868
|
+
client: 'codex',
|
|
1869
|
+
configPath,
|
|
1870
|
+
serverName: MCP_SERVER_NAME,
|
|
1871
|
+
mcpUrl,
|
|
1872
|
+
tokenEnvVar: TOKEN_ENV_VAR,
|
|
1873
|
+
agentInstanceIdPath: identityPath,
|
|
1874
|
+
checks
|
|
1875
|
+
};
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
function tomlServerBlock(content, serverName) {
|
|
1879
|
+
const header = `[mcp_servers.${serverName}]`;
|
|
1880
|
+
const lines = content.split(/\r?\n/);
|
|
1881
|
+
const start = lines.findIndex((line) => line.trim() === header);
|
|
1882
|
+
if (start === -1) {
|
|
1883
|
+
return '';
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
const block = [];
|
|
1887
|
+
for (let index = start + 1; index < lines.length; index += 1) {
|
|
1888
|
+
const line = lines[index];
|
|
1889
|
+
if (/^\s*\[/.test(line)) {
|
|
1890
|
+
break;
|
|
1891
|
+
}
|
|
1892
|
+
block.push(line);
|
|
1893
|
+
}
|
|
1894
|
+
return block.join('\n');
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
function tomlStringValue(block, key) {
|
|
1898
|
+
const pattern = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=\\s*"((?:\\\\.|[^"\\\\])*)"\\s*$`, 'm');
|
|
1899
|
+
const match = block.match(pattern);
|
|
1900
|
+
return match ? unescapeTomlString(match[1]) : null;
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
function cursorJsonSnippet(mcpUrl, identity = envReferenceIdentity('cursor')) {
|
|
1904
|
+
return `${JSON.stringify(cursorJsonConfig(mcpUrl, identity), null, 2)}\n`;
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
async function appendTomlServerConfig(configPath, mcpUrl) {
|
|
1908
|
+
const snippet = codexTomlSnippet(mcpUrl);
|
|
1909
|
+
const existing = await readTextIfExists(configPath);
|
|
1910
|
+
if (existing.includes(`[mcp_servers.${MCP_SERVER_NAME}]`)) {
|
|
1911
|
+
throw new UsageError(`MCP config already contains [mcp_servers.${MCP_SERVER_NAME}]. Edit ${configPath} manually to avoid duplicate server definitions.`);
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
await fs.mkdir(path.dirname(configPath), { recursive: true, mode: 0o700 });
|
|
1915
|
+
const prefix = existing.trim().length === 0 ? '' : '\n\n';
|
|
1916
|
+
await fs.appendFile(configPath, `${prefix}${snippet}`, { mode: 0o600 });
|
|
1917
|
+
await bestEffortChmod(configPath, 0o600);
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
async function mergeJsonMcpConfig(configPath, mcpUrl, identity) {
|
|
1921
|
+
const existing = await readTextIfExists(configPath);
|
|
1922
|
+
const parsed = existing.trim().length === 0 ? {} : parseJsonConfig(existing, configPath);
|
|
1923
|
+
|
|
1924
|
+
if (!isPlainObject(parsed)) {
|
|
1925
|
+
throw new UsageError(`MCP JSON config must be an object: ${configPath}`);
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
if (!isPlainObject(parsed.mcpServers)) {
|
|
1929
|
+
parsed.mcpServers = {};
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
if (parsed.mcpServers[MCP_SERVER_NAME]) {
|
|
1933
|
+
throw new UsageError(`MCP config already contains mcpServers.${MCP_SERVER_NAME}. Edit ${configPath} manually to avoid duplicate server definitions.`);
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
parsed.mcpServers[MCP_SERVER_NAME] = cursorJsonServerConfig(mcpUrl, identity);
|
|
1937
|
+
await fs.mkdir(path.dirname(configPath), { recursive: true, mode: 0o700 });
|
|
1938
|
+
await fs.writeFile(configPath, `${JSON.stringify(parsed, null, 2)}\n`, { mode: 0o600 });
|
|
1939
|
+
await bestEffortChmod(configPath, 0o600);
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
function antigravityJsonServerConfig(mcpUrl, identity = envReferenceIdentity('antigravity')) {
|
|
1943
|
+
return {
|
|
1944
|
+
serverUrl: mcpUrl,
|
|
1945
|
+
headers: {
|
|
1946
|
+
[AGENT_ID_HEADER]: identity.agentId,
|
|
1947
|
+
[AGENT_INSTANCE_HEADER]: identity.agentInstanceId
|
|
1948
|
+
}
|
|
1949
|
+
};
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
function antigravityJsonConfig(mcpUrl, identity = envReferenceIdentity('antigravity')) {
|
|
1953
|
+
return {
|
|
1954
|
+
mcpServers: {
|
|
1955
|
+
[MCP_SERVER_NAME]: antigravityJsonServerConfig(mcpUrl, identity)
|
|
1956
|
+
}
|
|
1957
|
+
};
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
function antigravityJsonSnippet(mcpUrl, identity = envReferenceIdentity('antigravity')) {
|
|
1961
|
+
return `${JSON.stringify(antigravityJsonConfig(mcpUrl, identity), null, 2)}\n`;
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
async function mergeAntigravityMcpConfig(configPath, mcpUrl, identity) {
|
|
1965
|
+
const existing = await readTextIfExists(configPath);
|
|
1966
|
+
const parsed = existing.trim().length === 0 ? {} : parseJsonConfig(existing, configPath);
|
|
1967
|
+
|
|
1968
|
+
if (!isPlainObject(parsed)) {
|
|
1969
|
+
throw new UsageError(`MCP JSON config must be an object: ${configPath}`);
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
if (!isPlainObject(parsed.mcpServers)) {
|
|
1973
|
+
parsed.mcpServers = {};
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
if (parsed.mcpServers[MCP_SERVER_NAME]) {
|
|
1977
|
+
throw new UsageError(`MCP config already contains mcpServers.${MCP_SERVER_NAME}. Edit ${configPath} manually to avoid duplicate server definitions.`);
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
parsed.mcpServers[MCP_SERVER_NAME] = antigravityJsonServerConfig(mcpUrl, identity);
|
|
1981
|
+
await fs.mkdir(path.dirname(configPath), { recursive: true, mode: 0o700 });
|
|
1982
|
+
await fs.writeFile(configPath, `${JSON.stringify(parsed, null, 2)}\n`, { mode: 0o600 });
|
|
1983
|
+
await bestEffortChmod(configPath, 0o600);
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
|
|
1987
|
+
async function mergeGeminiMcpConfig(configPath, mcpUrl, identity) {
|
|
1988
|
+
const existing = await readTextIfExists(configPath);
|
|
1989
|
+
const parsed = existing.trim().length === 0 ? {} : parseJsonConfig(existing, configPath);
|
|
1990
|
+
|
|
1991
|
+
if (!isPlainObject(parsed)) {
|
|
1992
|
+
throw new UsageError(`MCP JSON config must be an object: ${configPath}`);
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
if (!isPlainObject(parsed.mcpServers)) {
|
|
1996
|
+
parsed.mcpServers = {};
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
if (parsed.mcpServers[MCP_SERVER_NAME]) {
|
|
2000
|
+
throw new UsageError(`MCP config already contains mcpServers.${MCP_SERVER_NAME}. Edit ${configPath} manually to avoid duplicate server definitions.`);
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
parsed.mcpServers[MCP_SERVER_NAME] = geminiJsonServerConfig(mcpUrl, identity);
|
|
2004
|
+
await fs.mkdir(path.dirname(configPath), { recursive: true, mode: 0o700 });
|
|
2005
|
+
await fs.writeFile(configPath, `${JSON.stringify(parsed, null, 2)}\n`, { mode: 0o600 });
|
|
2006
|
+
await bestEffortChmod(configPath, 0o600);
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
async function mergeCopilotMcpConfig(configPath, proxyUrl) {
|
|
2010
|
+
const existing = await readTextIfExists(configPath);
|
|
2011
|
+
const parsed = existing.trim().length === 0 ? {} : parseJsonConfig(existing, configPath);
|
|
2012
|
+
|
|
2013
|
+
if (!isPlainObject(parsed)) {
|
|
2014
|
+
throw new UsageError(`Copilot MCP JSON config must be an object: ${configPath}`);
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
if (!isPlainObject(parsed.mcpServers)) {
|
|
2018
|
+
parsed.mcpServers = {};
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
parsed.mcpServers['memory-os'] = copilotLocalProxyServerConfig(proxyUrl);
|
|
2022
|
+
await fs.mkdir(path.dirname(configPath), { recursive: true, mode: 0o700 });
|
|
2023
|
+
await fs.writeFile(configPath, `${JSON.stringify(parsed, null, 2)}\n`, { mode: 0o600 });
|
|
2024
|
+
await bestEffortChmod(configPath, 0o600);
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
function copilotLocalProxyServerConfig(proxyUrl) {
|
|
2028
|
+
return {
|
|
2029
|
+
type: 'http',
|
|
2030
|
+
url: proxyUrl
|
|
2031
|
+
};
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
function cursorJsonConfig(mcpUrl, identity = envReferenceIdentity('cursor')) {
|
|
2035
|
+
return {
|
|
2036
|
+
mcpServers: {
|
|
2037
|
+
[MCP_SERVER_NAME]: cursorJsonServerConfig(mcpUrl, identity)
|
|
2038
|
+
}
|
|
2039
|
+
};
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
function cursorJsonServerConfig(mcpUrl, identity = envReferenceIdentity('cursor')) {
|
|
2043
|
+
return {
|
|
2044
|
+
url: mcpUrl,
|
|
2045
|
+
headers: {
|
|
2046
|
+
Authorization: `Bearer \${env:${TOKEN_ENV_VAR}}`,
|
|
2047
|
+
[AGENT_ID_HEADER]: identity.agentId,
|
|
2048
|
+
[AGENT_INSTANCE_HEADER]: identity.agentInstanceId
|
|
2049
|
+
}
|
|
2050
|
+
};
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
function geminiJsonServerConfig(mcpUrl, identity = envReferenceIdentity('gemini-cli')) {
|
|
2054
|
+
return {
|
|
2055
|
+
httpUrl: mcpUrl,
|
|
2056
|
+
headers: {
|
|
2057
|
+
[AGENT_ID_HEADER]: identity.agentId,
|
|
2058
|
+
[AGENT_INSTANCE_HEADER]: identity.agentInstanceId
|
|
2059
|
+
}
|
|
2060
|
+
};
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
function geminiJsonConfig(mcpUrl, identity = envReferenceIdentity('gemini-cli')) {
|
|
2064
|
+
return {
|
|
2065
|
+
mcpServers: {
|
|
2066
|
+
[MCP_SERVER_NAME]: geminiJsonServerConfig(mcpUrl, identity)
|
|
2067
|
+
}
|
|
2068
|
+
};
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
function geminiJsonSnippet(mcpUrl, identity = envReferenceIdentity('gemini-cli')) {
|
|
2072
|
+
return `${JSON.stringify(geminiJsonConfig(mcpUrl, identity), null, 2)}\n`;
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
async function agentIdentity(clientId, env) {
|
|
2076
|
+
const configuredInstanceId = env[AGENT_INSTANCE_ENV_VAR];
|
|
2077
|
+
if (configuredInstanceId) {
|
|
2078
|
+
return {
|
|
2079
|
+
agentId: clientId,
|
|
2080
|
+
agentInstanceId: configuredInstanceId,
|
|
2081
|
+
path: `${AGENT_INSTANCE_ENV_VAR} environment variable`
|
|
2082
|
+
};
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
const identityPath = agentInstanceIdentityPath(env, clientId);
|
|
2086
|
+
const existing = await readAgentInstanceIdentity(identityPath);
|
|
2087
|
+
if (existing) {
|
|
2088
|
+
return { agentId: clientId, agentInstanceId: existing, path: identityPath };
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
const generated = `xmemo-${clientId}-${randomUUID()}`;
|
|
2092
|
+
await fs.mkdir(path.dirname(identityPath), { recursive: true, mode: 0o700 });
|
|
2093
|
+
await bestEffortChmod(path.dirname(identityPath), 0o700);
|
|
2094
|
+
await fs.writeFile(identityPath, `${JSON.stringify({ version: 1, agentId: clientId, agentInstanceId: generated }, null, 2)}\n`, { mode: 0o600 });
|
|
2095
|
+
await bestEffortChmod(identityPath, 0o600);
|
|
2096
|
+
return { agentId: clientId, agentInstanceId: generated, path: identityPath };
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
async function readAgentInstanceIdentity(identityPath) {
|
|
2100
|
+
const existing = await readTextIfExists(identityPath);
|
|
2101
|
+
if (!existing.trim()) {
|
|
2102
|
+
return null;
|
|
2103
|
+
}
|
|
2104
|
+
const parsed = parseJsonConfig(existing, identityPath);
|
|
2105
|
+
const value = stringValue(parsed, ['agentInstanceId']);
|
|
2106
|
+
return value || null;
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
function agentInstanceIdentityPath(env, clientId) {
|
|
2110
|
+
return path.join(configRoot(env), 'agent-instances', `${clientId}.json`);
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
function envReferenceIdentity(clientId) {
|
|
2114
|
+
return {
|
|
2115
|
+
agentId: clientId,
|
|
2116
|
+
agentInstanceId: `\${${AGENT_INSTANCE_ENV_VAR}}`,
|
|
2117
|
+
path: `${AGENT_INSTANCE_ENV_VAR} environment variable`
|
|
2118
|
+
};
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
function supportedMcpClients() {
|
|
2122
|
+
const clients = Array.from(MCP_CLIENTS.entries()).map(([id, client]) => ({
|
|
2123
|
+
id,
|
|
2124
|
+
label: client.label,
|
|
2125
|
+
configKind: client.configKind
|
|
2126
|
+
}));
|
|
2127
|
+
clients.push({ id: 'copilot-cli', label: 'Copilot CLI', configKind: 'local-proxy' });
|
|
2128
|
+
return clients;
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
function supportedMcpClientIds() {
|
|
2132
|
+
return Array.from(MCP_CLIENTS.keys());
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
function supportedSetupClientIds() {
|
|
2136
|
+
return ['codex', 'cursor', 'copilot', 'gemini', 'antigravity'];
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
function usesClientOAuth(clientId) {
|
|
2140
|
+
return clientId === 'gemini-cli' || clientId === 'antigravity';
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
function credentialsPath(env) {
|
|
2144
|
+
return path.join(configRoot(env), 'credentials.json');
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
function configRoot(env) {
|
|
2148
|
+
if (env.XMEMO_CONFIG_HOME) {
|
|
2149
|
+
return env.XMEMO_CONFIG_HOME;
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
if (env.MEMORY_OS_CONFIG_HOME) {
|
|
2153
|
+
return env.MEMORY_OS_CONFIG_HOME;
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
if (process.platform === 'win32' && env.LOCALAPPDATA) {
|
|
2157
|
+
return path.join(env.LOCALAPPDATA, 'XMemo', 'CLI');
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
if (env.XDG_CONFIG_HOME) {
|
|
2161
|
+
return path.join(env.XDG_CONFIG_HOME, 'xmemo');
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
const home = env.HOME || os.homedir();
|
|
2165
|
+
return path.join(home, '.config', 'xmemo');
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
function defaultCodexConfigPath(env) {
|
|
2169
|
+
const home = env.USERPROFILE || env.HOME || os.homedir();
|
|
2170
|
+
return path.join(home, '.codex', 'config.toml');
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
function defaultCursorConfigPath(env) {
|
|
2174
|
+
const home = env.USERPROFILE || env.HOME || os.homedir();
|
|
2175
|
+
return path.join(home, '.cursor', 'mcp.json');
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
function defaultGeminiConfigPath(env) {
|
|
2179
|
+
const home = env.USERPROFILE || env.HOME || os.homedir();
|
|
2180
|
+
return path.join(home, '.gemini', 'settings.json');
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
function defaultAntigravityConfigPath(env) {
|
|
2184
|
+
const home = env.USERPROFILE || env.HOME || os.homedir();
|
|
2185
|
+
return path.join(home, '.gemini', 'antigravity', 'mcp_config.json');
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
function defaultCopilotConfigPath(env) {
|
|
2189
|
+
const home = env.USERPROFILE || env.HOME || os.homedir();
|
|
2190
|
+
return path.join(env.COPILOT_HOME ?? path.join(home, '.copilot'), 'mcp-config.json');
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
async function writePlaintextCredential(credentialPath, token, metadata = {}) {
|
|
2194
|
+
await fs.mkdir(path.dirname(credentialPath), { recursive: true, mode: 0o700 });
|
|
2195
|
+
await bestEffortChmod(path.dirname(credentialPath), 0o700);
|
|
2196
|
+
const payload = {
|
|
2197
|
+
version: 1,
|
|
2198
|
+
tokenEnvVar: TOKEN_ENV_VAR,
|
|
2199
|
+
storage: 'user-scoped-credential-file',
|
|
2200
|
+
createdAt: new Date().toISOString(),
|
|
2201
|
+
metadata,
|
|
2202
|
+
token
|
|
2203
|
+
};
|
|
2204
|
+
await fs.writeFile(credentialPath, `${JSON.stringify(payload, null, 2)}\n`, { mode: 0o600 });
|
|
2205
|
+
await bestEffortChmod(credentialPath, 0o600);
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
async function bestEffortChmod(filePath, mode) {
|
|
2209
|
+
try {
|
|
2210
|
+
await fs.chmod(filePath, mode);
|
|
2211
|
+
} catch {
|
|
2212
|
+
// Windows and managed environments may ignore POSIX chmod.
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
function validateToken(token) {
|
|
2217
|
+
if (!token) {
|
|
2218
|
+
throw new UsageError('Token from stdin is empty.');
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
if (/\s/.test(token)) {
|
|
2222
|
+
throw new UsageError('Token must not contain whitespace.');
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
if (token.length < 16) {
|
|
2226
|
+
throw new UsageError('Token is too short to be a production credential.');
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
function requiredOption(args, name) {
|
|
2231
|
+
const value = optionValue(args, name);
|
|
2232
|
+
if (!value) {
|
|
2233
|
+
throw new UsageError(`Missing required option ${name}.`);
|
|
2234
|
+
}
|
|
2235
|
+
return value;
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
function positionalClientArg(args) {
|
|
2239
|
+
const candidate = args[0];
|
|
2240
|
+
if (!candidate || candidate.startsWith('--')) {
|
|
2241
|
+
return null;
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
return normalizeSetupClientId(candidate);
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
function normalizeSetupClientId(candidate) {
|
|
2248
|
+
if (!candidate) {
|
|
2249
|
+
return null;
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
const normalized = SETUP_CLIENT_ALIASES.get(candidate);
|
|
2253
|
+
if (!normalized) {
|
|
2254
|
+
throw new UsageError(`Unsupported setup client: ${candidate}. Supported clients: ${supportedSetupClientIds().join(', ')}.`);
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
return normalized;
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
function optionValue(args, name) {
|
|
2261
|
+
const index = args.indexOf(name);
|
|
2262
|
+
if (index === -1) {
|
|
2263
|
+
return null;
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
const value = args[index + 1];
|
|
2267
|
+
if (!value || value.startsWith('--')) {
|
|
2268
|
+
throw new UsageError(`Option ${name} requires a value.`);
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
return value;
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
function stringValue(source, keys) {
|
|
2275
|
+
const value = valueAtPath(source, keys);
|
|
2276
|
+
return typeof value === 'string' && value.length > 0 ? value : null;
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
function booleanValue(source, keys) {
|
|
2280
|
+
const value = valueAtPath(source, keys);
|
|
2281
|
+
return typeof value === 'boolean' ? value : null;
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
function arrayValue(source, keys) {
|
|
2285
|
+
const value = valueAtPath(source, keys);
|
|
2286
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === 'string') : null;
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
function valueAtPath(source, keys) {
|
|
2290
|
+
let current = source;
|
|
2291
|
+
for (const key of keys) {
|
|
2292
|
+
if (!isPlainObject(current) || !(key in current)) {
|
|
2293
|
+
return null;
|
|
2294
|
+
}
|
|
2295
|
+
current = current[key];
|
|
2296
|
+
}
|
|
2297
|
+
return current;
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2300
|
+
function hasFlag(args, name) {
|
|
2301
|
+
return args.includes(name);
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2304
|
+
function parsePositiveInteger(value, name) {
|
|
2305
|
+
const parsed = Number.parseInt(value, 10);
|
|
2306
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
2307
|
+
throw new UsageError(`${name} must be a positive integer.`);
|
|
2308
|
+
}
|
|
2309
|
+
return parsed;
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2312
|
+
async function sleep(ms) {
|
|
2313
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
|
|
2317
|
+
function npmExecutable() {
|
|
2318
|
+
return os.platform() === 'win32' ? 'npm.cmd' : 'npm';
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
|
|
2322
|
+
async function runProcess(command, args, io, { stream = true } = {}) {
|
|
2323
|
+
const spawnFn = io.spawn ?? spawn;
|
|
2324
|
+
return await new Promise((resolve, reject) => {
|
|
2325
|
+
const child = spawnFn(command, args, {
|
|
2326
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
2327
|
+
});
|
|
2328
|
+
let stdout = '';
|
|
2329
|
+
let stderr = '';
|
|
2330
|
+
child.stdout?.on('data', (chunk) => {
|
|
2331
|
+
const text = String(chunk);
|
|
2332
|
+
stdout += text;
|
|
2333
|
+
if (stream) {
|
|
2334
|
+
io.stdout.write(text);
|
|
2335
|
+
}
|
|
2336
|
+
});
|
|
2337
|
+
child.stderr?.on('data', (chunk) => {
|
|
2338
|
+
const text = String(chunk);
|
|
2339
|
+
stderr += text;
|
|
2340
|
+
if (stream) {
|
|
2341
|
+
io.stderr.write(text);
|
|
2342
|
+
}
|
|
2343
|
+
});
|
|
2344
|
+
child.on('error', reject);
|
|
2345
|
+
child.on('close', (code) => {
|
|
2346
|
+
resolve({ code: code ?? 0, stdout, stderr });
|
|
2347
|
+
});
|
|
2348
|
+
});
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
|
|
2352
|
+
async function readAll(stream) {
|
|
2353
|
+
let content = '';
|
|
2354
|
+
for await (const chunk of stream) {
|
|
2355
|
+
content += chunk;
|
|
2356
|
+
}
|
|
2357
|
+
return content;
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
async function fileExists(filePath) {
|
|
2361
|
+
try {
|
|
2362
|
+
await fs.access(filePath);
|
|
2363
|
+
return true;
|
|
2364
|
+
} catch {
|
|
2365
|
+
return false;
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
async function readTextIfExists(filePath) {
|
|
2370
|
+
try {
|
|
2371
|
+
return await fs.readFile(filePath, 'utf8');
|
|
2372
|
+
} catch (error) {
|
|
2373
|
+
if (error.code === 'ENOENT') {
|
|
2374
|
+
return '';
|
|
2375
|
+
}
|
|
2376
|
+
throw error;
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2380
|
+
function parseJsonConfig(content, configPath) {
|
|
2381
|
+
try {
|
|
2382
|
+
return JSON.parse(content);
|
|
2383
|
+
} catch (error) {
|
|
2384
|
+
throw new UsageError(`Invalid JSON in ${configPath}: ${error.message}`);
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
function isPlainObject(value) {
|
|
2389
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
2390
|
+
}
|
|
2391
|
+
|
|
2392
|
+
function escapeTomlString(value) {
|
|
2393
|
+
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
function unescapeTomlString(value) {
|
|
2397
|
+
return value.replace(/\\"/g, '"').replace(/\\\\/g, '\\');
|
|
2398
|
+
}
|
|
2399
|
+
|
|
2400
|
+
function escapeRegExp(value) {
|
|
2401
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
function writeLine(stream, line) {
|
|
2405
|
+
stream.write(`${line}\n`);
|
|
2406
|
+
}
|