@pingagent/sdk 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/pingagent.js +1417 -1124
- package/dist/chunk-MDGELIR5.js +1317 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1 -1
- package/dist/web-server.d.ts +2 -0
- package/dist/web-server.js +80 -40
- package/package.json +5 -5
- package/src/client.ts +2 -0
- package/src/web-server.ts +87 -45
- package/src/ws-subscription.ts +3 -0
package/bin/pingagent.js
CHANGED
|
@@ -1,1125 +1,1418 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { Command } from 'commander';
|
|
3
|
-
import * as fs from 'node:fs';
|
|
4
|
-
import * as path from 'node:path';
|
|
5
|
-
import * as os from 'node:os';
|
|
6
|
-
import {
|
|
7
|
-
PingAgentClient,
|
|
8
|
-
generateIdentity,
|
|
9
|
-
saveIdentity,
|
|
10
|
-
loadIdentity,
|
|
11
|
-
identityExists,
|
|
12
|
-
updateStoredToken,
|
|
13
|
-
ensureTokenValid,
|
|
14
|
-
LocalStore,
|
|
15
|
-
ContactManager,
|
|
16
|
-
HistoryManager,
|
|
17
|
-
A2AAdapter,
|
|
18
|
-
} from '../dist/index.js';
|
|
19
|
-
import { ERROR_HINTS, SCHEMA_TEXT } from '@pingagent/schemas';
|
|
20
|
-
|
|
21
|
-
const DEFAULT_SERVER = 'http://localhost:8787';
|
|
22
|
-
const UPGRADE_URL = 'https://pingagent.chat';
|
|
23
|
-
const DEFAULT_IDENTITY_PATH = path.join(os.homedir(), '.pingagent', 'identity.json');
|
|
24
|
-
|
|
25
|
-
function resolvePath(p) {
|
|
26
|
-
if (!p) return p;
|
|
27
|
-
if (p.startsWith('~')) return path.join(os.homedir(), p.slice(1));
|
|
28
|
-
return p;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function getEffectiveIdentityPath() {
|
|
32
|
-
const dir = program.opts().identityDir;
|
|
33
|
-
if (dir) return path.join(resolvePath(dir), 'identity.json');
|
|
34
|
-
const envPath = process.env.PINGAGENT_IDENTITY_PATH;
|
|
35
|
-
return envPath ? resolvePath(envPath) : DEFAULT_IDENTITY_PATH;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (
|
|
41
|
-
return
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function
|
|
45
|
-
if (
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
console.log(
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
if (
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
console.log(
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
.
|
|
311
|
-
.
|
|
312
|
-
.option('--
|
|
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
|
-
const
|
|
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
|
-
.option('--
|
|
371
|
-
.
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
if (opts.json) {
|
|
494
|
-
console.log(JSON.stringify(
|
|
495
|
-
} else {
|
|
496
|
-
console.log(
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
if (
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
.
|
|
516
|
-
.
|
|
517
|
-
.requiredOption('--
|
|
518
|
-
.option('--
|
|
519
|
-
.
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
if (
|
|
580
|
-
console.log(
|
|
581
|
-
} else {
|
|
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
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
.command('
|
|
764
|
-
.description('
|
|
765
|
-
.
|
|
766
|
-
.option('--
|
|
767
|
-
.
|
|
768
|
-
|
|
769
|
-
const
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
const
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
const
|
|
794
|
-
console.log(` ${c.conversation_id} ${c.
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
});
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
.command('
|
|
802
|
-
.description('
|
|
803
|
-
.
|
|
804
|
-
.
|
|
805
|
-
.option('--
|
|
806
|
-
.
|
|
807
|
-
|
|
808
|
-
const
|
|
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
|
-
|
|
845
|
-
|
|
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
|
-
store
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
console.log(
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
.
|
|
935
|
-
.
|
|
936
|
-
.
|
|
937
|
-
.
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
const
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
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
|
-
const
|
|
1084
|
-
|
|
1085
|
-
if (!
|
|
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
|
-
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import * as fs from 'node:fs';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import * as os from 'node:os';
|
|
6
|
+
import {
|
|
7
|
+
PingAgentClient,
|
|
8
|
+
generateIdentity,
|
|
9
|
+
saveIdentity,
|
|
10
|
+
loadIdentity,
|
|
11
|
+
identityExists,
|
|
12
|
+
updateStoredToken,
|
|
13
|
+
ensureTokenValid,
|
|
14
|
+
LocalStore,
|
|
15
|
+
ContactManager,
|
|
16
|
+
HistoryManager,
|
|
17
|
+
A2AAdapter,
|
|
18
|
+
} from '../dist/index.js';
|
|
19
|
+
import { ERROR_HINTS, SCHEMA_TEXT } from '@pingagent/schemas';
|
|
20
|
+
|
|
21
|
+
const DEFAULT_SERVER = 'http://localhost:8787';
|
|
22
|
+
const UPGRADE_URL = 'https://pingagent.chat';
|
|
23
|
+
const DEFAULT_IDENTITY_PATH = path.join(os.homedir(), '.pingagent', 'identity.json');
|
|
24
|
+
|
|
25
|
+
function resolvePath(p) {
|
|
26
|
+
if (!p) return p;
|
|
27
|
+
if (p.startsWith('~')) return path.join(os.homedir(), p.slice(1));
|
|
28
|
+
return p;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getEffectiveIdentityPath() {
|
|
32
|
+
const dir = program.opts().identityDir;
|
|
33
|
+
if (dir) return path.join(resolvePath(dir), 'identity.json');
|
|
34
|
+
const envPath = process.env.PINGAGENT_IDENTITY_PATH;
|
|
35
|
+
return envPath ? resolvePath(envPath) : DEFAULT_IDENTITY_PATH;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Resolve identity path from command opts (--profile) or global (--identity-dir) or default. */
|
|
39
|
+
function getIdentityPathForCommand(opts) {
|
|
40
|
+
if (opts && opts.profile) return path.join(os.homedir(), '.pingagent', opts.profile, 'identity.json');
|
|
41
|
+
return getEffectiveIdentityPath();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getStorePath(identityPath) {
|
|
45
|
+
if (process.env.PINGAGENT_STORE_PATH) return resolvePath(process.env.PINGAGENT_STORE_PATH);
|
|
46
|
+
const base = identityPath != null ? path.dirname(identityPath) : path.dirname(getEffectiveIdentityPath());
|
|
47
|
+
return path.join(base, 'store.db');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function printError(err) {
|
|
51
|
+
if (!err) return;
|
|
52
|
+
console.error(`Error: ${err.code ?? 'unknown'} - ${err.message ?? ''}`);
|
|
53
|
+
const hint = err.hint ?? (err.code && ERROR_HINTS[err.code]);
|
|
54
|
+
if (hint) console.error(`Hint: ${hint}`);
|
|
55
|
+
if (err.code && err.code.startsWith('E_PAYWALL_')) {
|
|
56
|
+
console.error(`Upgrade: ${UPGRADE_URL}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function openStore(identityPath) {
|
|
61
|
+
return new LocalStore(getStorePath(identityPath));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Format timestamp for display: human-readable by default, raw ms when --raw. */
|
|
65
|
+
function formatTs(tsMs, raw) {
|
|
66
|
+
if (raw) return String(tsMs);
|
|
67
|
+
if (tsMs == null || Number.isNaN(tsMs)) return '-';
|
|
68
|
+
return new Date(tsMs).toLocaleString();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function makeClient(id, identityPath) {
|
|
72
|
+
const p = identityPath ?? getEffectiveIdentityPath();
|
|
73
|
+
return new PingAgentClient({
|
|
74
|
+
serverUrl: id.serverUrl ?? DEFAULT_SERVER,
|
|
75
|
+
identity: id,
|
|
76
|
+
accessToken: id.accessToken ?? '',
|
|
77
|
+
onTokenRefreshed: (token, expiresAt) => updateStoredToken(token, expiresAt, p),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Load identity, proactively refresh token if near/past expiry, then return client. */
|
|
82
|
+
async function getClient(identityPath) {
|
|
83
|
+
const p = identityPath ?? getEffectiveIdentityPath();
|
|
84
|
+
let id = loadIdentity(p);
|
|
85
|
+
await ensureTokenValid(p, id.serverUrl);
|
|
86
|
+
id = loadIdentity(p);
|
|
87
|
+
return makeClient(id, p);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const program = new Command();
|
|
91
|
+
program
|
|
92
|
+
.name('pingagent')
|
|
93
|
+
.description('PingAgent Chat CLI')
|
|
94
|
+
.version('0.1.0')
|
|
95
|
+
.option('--identity-dir <dir>', 'Use identity and store from this directory (e.g. ~/.pingagent/agent1). Overrides PINGAGENT_IDENTITY_PATH for identity file; store is <dir>/store.db unless PINGAGENT_STORE_PATH is set.')
|
|
96
|
+
.option('--raw', 'Show raw timestamps (milliseconds) instead of human-readable in non-JSON output.');
|
|
97
|
+
|
|
98
|
+
program
|
|
99
|
+
.command('init')
|
|
100
|
+
.description('Initialize PingAgent Chat identity')
|
|
101
|
+
.option('--server <url>', 'Server URL', DEFAULT_SERVER)
|
|
102
|
+
.option('--token <token>', 'Developer token for production mode')
|
|
103
|
+
.option('--profile <name>', 'Profile name: use ~/.pingagent/<name>/ for identity and set server display_name to <name>')
|
|
104
|
+
.option('--bio <text>', 'Set profile bio on the server after registration')
|
|
105
|
+
.option('--cursor', 'Auto-configure Cursor MCP')
|
|
106
|
+
.action(async (opts) => {
|
|
107
|
+
const identityPath = opts.profile
|
|
108
|
+
? path.join(os.homedir(), '.pingagent', opts.profile, 'identity.json')
|
|
109
|
+
: getEffectiveIdentityPath();
|
|
110
|
+
const withProfile = opts.profile != null || opts.bio != null;
|
|
111
|
+
|
|
112
|
+
if (identityExists(identityPath)) {
|
|
113
|
+
const existing = loadIdentity(identityPath);
|
|
114
|
+
console.log(`Identity already exists: ${existing.did}`);
|
|
115
|
+
if (withProfile) {
|
|
116
|
+
await ensureTokenValid(identityPath, existing.serverUrl ?? opts.server);
|
|
117
|
+
const id = loadIdentity(identityPath);
|
|
118
|
+
const client = makeClient(id, identityPath);
|
|
119
|
+
const profilePayload = {};
|
|
120
|
+
if (opts.profile != null) profilePayload.display_name = opts.profile;
|
|
121
|
+
if (opts.bio != null) profilePayload.bio = opts.bio;
|
|
122
|
+
console.log('Updating profile (display_name, bio)...');
|
|
123
|
+
const profileRes = await client.updateProfile(profilePayload);
|
|
124
|
+
if (!profileRes.ok) {
|
|
125
|
+
if (profileRes.error) printError(profileRes.error);
|
|
126
|
+
else console.error('Profile update failed.');
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
console.log('Profile updated.');
|
|
130
|
+
}
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const totalSteps = withProfile ? 4 : 3;
|
|
135
|
+
|
|
136
|
+
console.log('PingAgent Chat Setup');
|
|
137
|
+
console.log('====================');
|
|
138
|
+
console.log(`[1/${totalSteps}] Generating Ed25519 keypair... done`);
|
|
139
|
+
|
|
140
|
+
const identity = generateIdentity();
|
|
141
|
+
|
|
142
|
+
console.log(`[2/${totalSteps}] Registering with relay server...`);
|
|
143
|
+
const client = new PingAgentClient({
|
|
144
|
+
serverUrl: opts.server,
|
|
145
|
+
identity,
|
|
146
|
+
accessToken: '',
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const regRes = await client.register(opts.token);
|
|
150
|
+
if (!regRes.ok || !regRes.data) {
|
|
151
|
+
if (regRes.error) printError(regRes.error);
|
|
152
|
+
else console.error('Registration failed:', regRes.error?.message);
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const { did, access_token, expires_ms, mode } = regRes.data;
|
|
157
|
+
console.log(` Server: ${opts.server}`);
|
|
158
|
+
console.log(` Mode: ${mode}`);
|
|
159
|
+
console.log(` Your DID: ${did}`);
|
|
160
|
+
|
|
161
|
+
console.log(`[3/${totalSteps}] Saving identity... done`);
|
|
162
|
+
saveIdentity(identity, {
|
|
163
|
+
serverUrl: opts.server,
|
|
164
|
+
accessToken: access_token,
|
|
165
|
+
tokenExpiresAt: Date.now() + expires_ms,
|
|
166
|
+
mode,
|
|
167
|
+
}, identityPath);
|
|
168
|
+
|
|
169
|
+
if (withProfile) {
|
|
170
|
+
const profilePayload = {};
|
|
171
|
+
if (opts.profile != null) profilePayload.display_name = opts.profile;
|
|
172
|
+
if (opts.bio != null) profilePayload.bio = opts.bio;
|
|
173
|
+
console.log(`[4/${totalSteps}] Setting profile (display_name, bio)...`);
|
|
174
|
+
const clientWithToken = new PingAgentClient({
|
|
175
|
+
serverUrl: opts.server,
|
|
176
|
+
identity,
|
|
177
|
+
accessToken: access_token,
|
|
178
|
+
});
|
|
179
|
+
const profileRes = await clientWithToken.updateProfile(profilePayload);
|
|
180
|
+
if (!profileRes.ok) {
|
|
181
|
+
if (profileRes.error) printError(profileRes.error);
|
|
182
|
+
else console.error('Profile update failed (identity was saved).');
|
|
183
|
+
} else {
|
|
184
|
+
console.log(' done');
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
console.log(`\nReady! Share your DID with agents you want to communicate with.`);
|
|
189
|
+
if (identityPath !== DEFAULT_IDENTITY_PATH) {
|
|
190
|
+
console.log(`Identity: ${identityPath}`);
|
|
191
|
+
console.log(`Store: ${getStorePath()}`);
|
|
192
|
+
}
|
|
193
|
+
console.log(`\nRun 'pingagent doctor' to verify setup.`);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
program
|
|
197
|
+
.command('doctor')
|
|
198
|
+
.description('Check identity, token, permissions and server; use --fix to renew token if expired')
|
|
199
|
+
.option('--profile <name>', 'Use profile from ~/.pingagent/<name>')
|
|
200
|
+
.option('--fix', 'Attempt to fix token if expired (runs renew-token)')
|
|
201
|
+
.option('--json', 'Output as JSON')
|
|
202
|
+
.action(async (opts) => {
|
|
203
|
+
const identityPath = getIdentityPathForCommand(opts);
|
|
204
|
+
const storePath = getStorePath(identityPath);
|
|
205
|
+
const identityDir = path.dirname(identityPath);
|
|
206
|
+
const storeDir = path.dirname(storePath);
|
|
207
|
+
const issues = [];
|
|
208
|
+
const ok = [];
|
|
209
|
+
|
|
210
|
+
if (!identityExists(identityPath)) {
|
|
211
|
+
issues.push({ check: 'identity', message: 'No identity found. Run: pingagent init' });
|
|
212
|
+
} else {
|
|
213
|
+
ok.push('identity');
|
|
214
|
+
try {
|
|
215
|
+
const id = loadIdentity(identityPath);
|
|
216
|
+
const now = Date.now();
|
|
217
|
+
const expiresAt = id.tokenExpiresAt;
|
|
218
|
+
const graceMs = 5 * 60 * 1000;
|
|
219
|
+
if (expiresAt == null || expiresAt <= now + graceMs) {
|
|
220
|
+
issues.push({ check: 'token', message: 'Token expired or expiring soon. Run: pingagent renew-token', fixable: true });
|
|
221
|
+
} else {
|
|
222
|
+
ok.push('token');
|
|
223
|
+
}
|
|
224
|
+
} catch (e) {
|
|
225
|
+
issues.push({ check: 'identity', message: `Cannot load identity: ${e.message}` });
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
if (!fs.existsSync(identityDir)) {
|
|
231
|
+
fs.mkdirSync(identityDir, { recursive: true, mode: 0o700 });
|
|
232
|
+
}
|
|
233
|
+
const testFile = path.join(identityDir, '.pingagent-write-test');
|
|
234
|
+
fs.writeFileSync(testFile, '');
|
|
235
|
+
fs.unlinkSync(testFile);
|
|
236
|
+
ok.push('identity_dir_writable');
|
|
237
|
+
} catch (e) {
|
|
238
|
+
issues.push({ check: 'identity_dir', message: `Identity directory not writable: ${identityDir} (${e.message})` });
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
if (!fs.existsSync(storeDir)) {
|
|
243
|
+
fs.mkdirSync(storeDir, { recursive: true, mode: 0o700 });
|
|
244
|
+
}
|
|
245
|
+
const store = openStore(identityPath);
|
|
246
|
+
store.close();
|
|
247
|
+
ok.push('store_writable');
|
|
248
|
+
} catch (e) {
|
|
249
|
+
issues.push({ check: 'store', message: `Store path not writable: ${storePath} (${e.message})` });
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (identityExists(identityPath)) {
|
|
253
|
+
const id = loadIdentity(identityPath);
|
|
254
|
+
const serverUrl = (id.serverUrl ?? DEFAULT_SERVER).replace(/\/$/, '');
|
|
255
|
+
try {
|
|
256
|
+
const res = await fetch(`${serverUrl}/.well-known/agent.json`, { method: 'GET' });
|
|
257
|
+
if (res.ok) ok.push('server_reachable');
|
|
258
|
+
else issues.push({ check: 'server', message: `Server returned ${res.status}: ${serverUrl}` });
|
|
259
|
+
} catch (e) {
|
|
260
|
+
issues.push({ check: 'server', message: `Server not reachable: ${serverUrl} (${e.message})` });
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (opts.fix && issues.some(i => i.fixable)) {
|
|
265
|
+
const tokenIssue = issues.find(i => i.check === 'token');
|
|
266
|
+
if (tokenIssue && identityExists(identityPath)) {
|
|
267
|
+
const existing = loadIdentity(identityPath);
|
|
268
|
+
const serverUrl = existing.serverUrl ?? DEFAULT_SERVER;
|
|
269
|
+
|
|
270
|
+
// Try refresh first, then fallback to re-register
|
|
271
|
+
const refreshed = await ensureTokenValid(identityPath, serverUrl);
|
|
272
|
+
if (refreshed) {
|
|
273
|
+
console.log('Fixed: token refreshed (via /v1/auth/refresh).');
|
|
274
|
+
issues.splice(issues.findIndex(i => i.check === 'token'), 1);
|
|
275
|
+
ok.push('token');
|
|
276
|
+
} else {
|
|
277
|
+
const client = new PingAgentClient({ serverUrl, identity: existing, accessToken: '' });
|
|
278
|
+
const regRes = await client.register();
|
|
279
|
+
if (regRes.ok && regRes.data) {
|
|
280
|
+
saveIdentity(existing, {
|
|
281
|
+
serverUrl,
|
|
282
|
+
accessToken: regRes.data.access_token,
|
|
283
|
+
tokenExpiresAt: Date.now() + regRes.data.expires_ms,
|
|
284
|
+
mode: regRes.data.mode,
|
|
285
|
+
}, identityPath);
|
|
286
|
+
console.log('Fixed: token renewed (via re-register).');
|
|
287
|
+
issues.splice(issues.findIndex(i => i.check === 'token'), 1);
|
|
288
|
+
ok.push('token');
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (opts.json) {
|
|
295
|
+
console.log(JSON.stringify({ ok, issues }, null, 2));
|
|
296
|
+
process.exit(issues.length > 0 ? 1 : 0);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (ok.length) console.log('OK:', ok.join(', '));
|
|
300
|
+
if (issues.length) {
|
|
301
|
+
for (const i of issues) console.error(`Issue [${i.check}]: ${i.message}`);
|
|
302
|
+
if (issues.some(i => i.fixable)) console.error('\nRun with --fix to attempt token renewal.');
|
|
303
|
+
process.exit(1);
|
|
304
|
+
}
|
|
305
|
+
console.log('All checks passed.');
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
program
|
|
309
|
+
.command('renew-token')
|
|
310
|
+
.description('Renew access token using existing identity (keeps DID and keypair). Tries /v1/auth/refresh first, falls back to re-register.')
|
|
311
|
+
.option('--profile <name>', 'Use profile from ~/.pingagent/<name> (e.g. agent1, receiver)')
|
|
312
|
+
.option('--server <url>', 'Server URL (defaults to identity server or localhost:8787)')
|
|
313
|
+
.option('--token <token>', 'Developer token for production mode (optional)')
|
|
314
|
+
.action(async (opts) => {
|
|
315
|
+
const identityPath = program.opts().identityDir
|
|
316
|
+
? path.join(resolvePath(program.opts().identityDir), 'identity.json')
|
|
317
|
+
: opts.profile
|
|
318
|
+
? path.join(os.homedir(), '.pingagent', opts.profile, 'identity.json')
|
|
319
|
+
: getEffectiveIdentityPath();
|
|
320
|
+
if (!identityExists(identityPath)) {
|
|
321
|
+
console.error('No identity found. Run: pingagent init (or use --identity-dir / --profile / PINGAGENT_IDENTITY_PATH) first.');
|
|
322
|
+
process.exit(1);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const existing = loadIdentity(identityPath);
|
|
326
|
+
const serverUrl = opts.server ?? existing.serverUrl ?? DEFAULT_SERVER;
|
|
327
|
+
|
|
328
|
+
// Step 1: try lightweight /v1/auth/refresh (works within grace period)
|
|
329
|
+
console.log('Attempting token refresh via /v1/auth/refresh...');
|
|
330
|
+
const refreshed = await ensureTokenValid(identityPath, serverUrl);
|
|
331
|
+
if (refreshed) {
|
|
332
|
+
const updated = loadIdentity(identityPath);
|
|
333
|
+
console.log('Token refreshed successfully (via /v1/auth/refresh).');
|
|
334
|
+
console.log(`DID: ${updated.did}`);
|
|
335
|
+
console.log(`Server: ${serverUrl}`);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Step 2: fallback to re-register with existing keypair
|
|
340
|
+
console.log('Refresh not possible (token beyond grace period). Falling back to re-register...');
|
|
341
|
+
const client = new PingAgentClient({
|
|
342
|
+
serverUrl,
|
|
343
|
+
identity: existing,
|
|
344
|
+
accessToken: '',
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
const regRes = await client.register(opts.token);
|
|
348
|
+
if (!regRes.ok || !regRes.data) {
|
|
349
|
+
if (regRes.error) printError(regRes.error);
|
|
350
|
+
else console.error('Token renewal failed:', regRes.error?.message);
|
|
351
|
+
process.exit(1);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const { access_token, expires_ms, mode } = regRes.data;
|
|
355
|
+
saveIdentity(existing, {
|
|
356
|
+
serverUrl,
|
|
357
|
+
accessToken: access_token,
|
|
358
|
+
tokenExpiresAt: Date.now() + expires_ms,
|
|
359
|
+
mode,
|
|
360
|
+
}, identityPath);
|
|
361
|
+
|
|
362
|
+
console.log('Token renewed successfully (via re-register).');
|
|
363
|
+
console.log(`DID: ${existing.did}`);
|
|
364
|
+
console.log(`Server: ${serverUrl}`);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
program
|
|
368
|
+
.command('status')
|
|
369
|
+
.description('Show current identity and subscription status')
|
|
370
|
+
.option('--profile <name>', 'Use profile from ~/.pingagent/<name>')
|
|
371
|
+
.option('--json', 'Output as JSON')
|
|
372
|
+
.action(async (opts) => {
|
|
373
|
+
const identityPath = getIdentityPathForCommand(opts);
|
|
374
|
+
if (!identityExists(identityPath)) {
|
|
375
|
+
console.log('No identity found. Run: pingagent init (or use --identity-dir / --profile / PINGAGENT_IDENTITY_PATH)');
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
const client = await getClient(identityPath);
|
|
379
|
+
const id = loadIdentity(identityPath);
|
|
380
|
+
|
|
381
|
+
const tokenStatus = id.tokenExpiresAt && id.tokenExpiresAt > Date.now() ? 'valid' : 'expired';
|
|
382
|
+
|
|
383
|
+
if (opts.json) {
|
|
384
|
+
const out = { did: id.did, device_id: id.deviceId, server: id.serverUrl ?? null, mode: id.mode ?? null, token: tokenStatus };
|
|
385
|
+
const subRes = await client.getSubscription();
|
|
386
|
+
if (subRes.ok && subRes.data) {
|
|
387
|
+
out.tier = subRes.data.tier;
|
|
388
|
+
out.usage = subRes.data.usage;
|
|
389
|
+
out.limits = subRes.data.limits;
|
|
390
|
+
}
|
|
391
|
+
console.log(JSON.stringify(out, null, 2));
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
console.log(`DID: ${id.did}`);
|
|
396
|
+
console.log(`Device: ${id.deviceId}`);
|
|
397
|
+
console.log(`Server: ${id.serverUrl ?? 'not set'}`);
|
|
398
|
+
console.log(`Mode: ${id.mode ?? 'unknown'}`);
|
|
399
|
+
console.log(`Token: ${tokenStatus}`);
|
|
400
|
+
|
|
401
|
+
const subRes = await client.getSubscription();
|
|
402
|
+
if (subRes.ok && subRes.data) {
|
|
403
|
+
const d = subRes.data;
|
|
404
|
+
console.log(`Tier: ${d.tier}`);
|
|
405
|
+
console.log(`Relay: ${d.usage.relay_today} / ${d.usage.relay_limit} today`);
|
|
406
|
+
console.log(`Artifact: ${(d.usage.artifact_bytes / 1024 / 1024).toFixed(2)} MB / ${d.limits.artifact_storage_mb} MB`);
|
|
407
|
+
console.log(`Alias: ${d.usage.alias_count} / ${d.usage.alias_limit}`);
|
|
408
|
+
if (d.billing_primary_did && d.billing_primary_did !== id.did) {
|
|
409
|
+
console.log(`Billing: linked device (primary: ${d.billing_primary_did})`);
|
|
410
|
+
console.log(' Subscription managed on primary. Use primary to create link codes or manage billing.');
|
|
411
|
+
} else if (d.linked_device_count > 0) {
|
|
412
|
+
console.log(`Billing: primary device (${d.linked_device_count} linked)`);
|
|
413
|
+
}
|
|
414
|
+
if (d.usage.relay_today >= d.usage.relay_limit) {
|
|
415
|
+
console.log(`\nQuota reached. Upgrade: ${UPGRADE_URL}`);
|
|
416
|
+
}
|
|
417
|
+
} else {
|
|
418
|
+
console.log('Subscription: (unable to fetch - check server and token)');
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
program
|
|
423
|
+
.command('send')
|
|
424
|
+
.description('Send a task to a remote agent')
|
|
425
|
+
.option('--profile <name>', 'Use profile from ~/.pingagent/<name>')
|
|
426
|
+
.option('--to <did>', 'Target DID or alias')
|
|
427
|
+
.option('--target <did>', 'Target DID or alias (alias of --to)')
|
|
428
|
+
.requiredOption('--task <title>', 'Task title')
|
|
429
|
+
.option('--description <desc>', 'Task description')
|
|
430
|
+
.option('--wait', 'Wait for result')
|
|
431
|
+
.option('--timeout <seconds>', 'Timeout in seconds', '120')
|
|
432
|
+
.option('--json', 'Output as JSON')
|
|
433
|
+
.action(async (opts) => {
|
|
434
|
+
const identityPath = getIdentityPathForCommand(opts);
|
|
435
|
+
let targetDid = opts.to ?? opts.target;
|
|
436
|
+
if (!targetDid) {
|
|
437
|
+
console.error('Provide --to or --target (e.g. --to did:agent:xxx or --to @alias)');
|
|
438
|
+
process.exit(1);
|
|
439
|
+
}
|
|
440
|
+
if (!identityExists(identityPath)) {
|
|
441
|
+
console.error('No identity found. Run: pingagent init (or use --identity-dir / --profile / PINGAGENT_IDENTITY_PATH)');
|
|
442
|
+
process.exit(1);
|
|
443
|
+
}
|
|
444
|
+
const client = await getClient(identityPath);
|
|
445
|
+
if (targetDid.startsWith('@')) {
|
|
446
|
+
const resolved = await client.resolveAlias(targetDid);
|
|
447
|
+
if (!resolved.ok || !resolved.data) {
|
|
448
|
+
if (resolved.error) printError(resolved.error);
|
|
449
|
+
else console.error(`Could not resolve alias: ${targetDid}`);
|
|
450
|
+
process.exit(1);
|
|
451
|
+
}
|
|
452
|
+
targetDid = resolved.data.did;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
let trusted = false;
|
|
456
|
+
if (opts.wait) {
|
|
457
|
+
const result = await client.sendTaskAndWait(
|
|
458
|
+
targetDid,
|
|
459
|
+
{ title: opts.task, description: opts.description },
|
|
460
|
+
{ timeoutMs: parseInt(opts.timeout) * 1000 },
|
|
461
|
+
);
|
|
462
|
+
trusted = result.status === 'ok';
|
|
463
|
+
if (opts.json) {
|
|
464
|
+
console.log(JSON.stringify(result, null, 2));
|
|
465
|
+
} else {
|
|
466
|
+
console.log(`Task ${result.task_id}: ${result.status}`);
|
|
467
|
+
if (result.result?.summary) console.log(`Summary: ${result.result.summary}`);
|
|
468
|
+
if (result.error) printError(result.error);
|
|
469
|
+
console.log(`Elapsed: ${result.elapsed_ms}ms`);
|
|
470
|
+
}
|
|
471
|
+
} else {
|
|
472
|
+
const openRes = await client.openConversation(targetDid);
|
|
473
|
+
if (!openRes.ok || !openRes.data) {
|
|
474
|
+
if (openRes.error) printError(openRes.error);
|
|
475
|
+
else console.error('Failed to open conversation');
|
|
476
|
+
process.exit(1);
|
|
477
|
+
}
|
|
478
|
+
trusted = openRes.data.trusted;
|
|
479
|
+
|
|
480
|
+
const { v7: uuidv7 } = await import('uuid');
|
|
481
|
+
const sendRes = await client.sendTask(openRes.data.conversation_id, {
|
|
482
|
+
task_id: `t_${uuidv7()}`,
|
|
483
|
+
title: opts.task,
|
|
484
|
+
description: opts.description,
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
if (!sendRes.ok) {
|
|
488
|
+
if (sendRes.error) printError(sendRes.error);
|
|
489
|
+
else console.error('Failed to send task');
|
|
490
|
+
process.exit(1);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (opts.json) {
|
|
494
|
+
console.log(JSON.stringify(sendRes, null, 2));
|
|
495
|
+
} else {
|
|
496
|
+
console.log(`Sent: ${sendRes.data?.message_id} (seq: ${sendRes.data?.seq})`);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const store = openStore(identityPath);
|
|
501
|
+
try {
|
|
502
|
+
const mgr = new ContactManager(store);
|
|
503
|
+
if (!mgr.get(targetDid)) {
|
|
504
|
+
mgr.add({ did: targetDid, trusted, notes: 'Added via pingagent send' });
|
|
505
|
+
}
|
|
506
|
+
} finally {
|
|
507
|
+
store.close();
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
program
|
|
512
|
+
.command('chat')
|
|
513
|
+
.description('Send a text message (same as MCP pingagent_chat). Use "send" for tasks.')
|
|
514
|
+
.option('--profile <name>', 'Use profile from ~/.pingagent/<name>')
|
|
515
|
+
.option('--to <did>', 'Target DID or alias')
|
|
516
|
+
.option('--target <did>', 'Target DID or alias (alias of --to)')
|
|
517
|
+
.requiredOption('--message <text>', 'Message text')
|
|
518
|
+
.option('--json', 'Output as JSON')
|
|
519
|
+
.action(async (opts) => {
|
|
520
|
+
const identityPath = getIdentityPathForCommand(opts);
|
|
521
|
+
let targetDid = opts.to ?? opts.target;
|
|
522
|
+
if (!targetDid) {
|
|
523
|
+
console.error('Provide --to or --target (e.g. --to did:agent:xxx or --to @alias)');
|
|
524
|
+
process.exit(1);
|
|
525
|
+
}
|
|
526
|
+
if (!identityExists(identityPath)) {
|
|
527
|
+
console.error('No identity found. Run: pingagent init (or use --identity-dir / --profile / PINGAGENT_IDENTITY_PATH)');
|
|
528
|
+
process.exit(1);
|
|
529
|
+
}
|
|
530
|
+
const client = await getClient(identityPath);
|
|
531
|
+
if (targetDid.startsWith('@')) {
|
|
532
|
+
const resolved = await client.resolveAlias(targetDid);
|
|
533
|
+
if (!resolved.ok || !resolved.data) {
|
|
534
|
+
if (resolved.error) printError(resolved.error);
|
|
535
|
+
else console.error(`Could not resolve alias: ${targetDid}`);
|
|
536
|
+
process.exit(1);
|
|
537
|
+
}
|
|
538
|
+
targetDid = resolved.data.did;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const openRes = await client.openConversation(targetDid);
|
|
542
|
+
if (!openRes.ok || !openRes.data) {
|
|
543
|
+
if (openRes.error) printError(openRes.error);
|
|
544
|
+
else console.error('Failed to open conversation');
|
|
545
|
+
process.exit(1);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const store = openStore(identityPath);
|
|
549
|
+
try {
|
|
550
|
+
const mgr = new ContactManager(store);
|
|
551
|
+
if (!mgr.get(targetDid)) {
|
|
552
|
+
mgr.add({
|
|
553
|
+
did: targetDid,
|
|
554
|
+
trusted: openRes.data.trusted,
|
|
555
|
+
conversation_id: openRes.data.conversation_id,
|
|
556
|
+
notes: openRes.data.trusted ? 'Added via pingagent chat' : 'Added via pingagent chat (pending)',
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
} finally {
|
|
560
|
+
store.close();
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (!openRes.data.trusted) {
|
|
564
|
+
await client.sendContactRequest(openRes.data.conversation_id, opts.message);
|
|
565
|
+
if (opts.json) {
|
|
566
|
+
console.log(JSON.stringify({ sent: 'contact_request', conversation_id: openRes.data.conversation_id }));
|
|
567
|
+
} else {
|
|
568
|
+
console.log('Contact request sent with your message. They need to approve before further messages are delivered.');
|
|
569
|
+
}
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const sendRes = await client.sendMessage(openRes.data.conversation_id, SCHEMA_TEXT, { text: opts.message });
|
|
574
|
+
if (!sendRes.ok) {
|
|
575
|
+
if (sendRes.error) printError(sendRes.error);
|
|
576
|
+
else console.error('Failed to send message');
|
|
577
|
+
process.exit(1);
|
|
578
|
+
}
|
|
579
|
+
if (opts.json) {
|
|
580
|
+
console.log(JSON.stringify({ message_id: sendRes.data?.message_id, seq: sendRes.data?.seq }));
|
|
581
|
+
} else {
|
|
582
|
+
console.log(`Sent: ${sendRes.data?.message_id} (seq: ${sendRes.data?.seq})`);
|
|
583
|
+
}
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
program
|
|
587
|
+
.command('inbox')
|
|
588
|
+
.description('Fetch inbox messages. Without --conversation, lists conversations with recent activity.')
|
|
589
|
+
.option('--profile <name>', 'Use profile from ~/.pingagent/<name>')
|
|
590
|
+
.option('--conversation <id>', 'Conversation ID (omit to list all conversations with new messages)')
|
|
591
|
+
.option('--since-seq <n>', 'Since sequence number', '0')
|
|
592
|
+
.option('--limit <n>', 'Limit', '20')
|
|
593
|
+
.option('--box <box>', "Inbox box: ready, strangers, or all (merge both; use 'all' if Web messages don't show)", 'ready')
|
|
594
|
+
.option('--json', 'Output as JSON')
|
|
595
|
+
.action(async (opts) => {
|
|
596
|
+
const identityPath = getIdentityPathForCommand(opts);
|
|
597
|
+
if (!identityExists(identityPath)) {
|
|
598
|
+
console.error('No identity found. Run: pingagent init (or use --identity-dir / --profile / PINGAGENT_IDENTITY_PATH)');
|
|
599
|
+
process.exit(1);
|
|
600
|
+
}
|
|
601
|
+
const client = await getClient(identityPath);
|
|
602
|
+
const sinceSeq = parseInt(opts.sinceSeq);
|
|
603
|
+
const limit = parseInt(opts.limit);
|
|
604
|
+
|
|
605
|
+
if (!opts.conversation) {
|
|
606
|
+
const listRes = await client.listConversations({ type: 'dm' });
|
|
607
|
+
if (!listRes.ok || !listRes.data?.conversations?.length) {
|
|
608
|
+
if (opts.json) {
|
|
609
|
+
console.log(JSON.stringify({ ok: true, data: { conversations: [] } }, null, 2));
|
|
610
|
+
} else {
|
|
611
|
+
console.log('No conversations. Send a message (pingagent chat --to <did>) or approve a contact request first.');
|
|
612
|
+
}
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
const raw = program.opts().raw;
|
|
616
|
+
const convos = listRes.data.conversations;
|
|
617
|
+
if (opts.json) {
|
|
618
|
+
const withPreview = [];
|
|
619
|
+
for (const c of convos) {
|
|
620
|
+
const resReady = await client.fetchInbox(c.conversation_id, { sinceSeq: 0, limit: 1, box: 'ready' });
|
|
621
|
+
const resStrangers = await client.fetchInbox(c.conversation_id, { sinceSeq: 0, limit: 1, box: 'strangers' });
|
|
622
|
+
const lastReady = resReady.ok && resReady.data?.messages?.[0];
|
|
623
|
+
const lastStrangers = resStrangers.ok && resStrangers.data?.messages?.[0];
|
|
624
|
+
withPreview.push({
|
|
625
|
+
conversation_id: c.conversation_id,
|
|
626
|
+
type: c.type,
|
|
627
|
+
target_did: c.target_did,
|
|
628
|
+
trusted: c.trusted,
|
|
629
|
+
last_ready: lastReady ? { seq: lastReady.seq, schema: lastReady.schema, ts_ms: lastReady.ts_ms } : null,
|
|
630
|
+
last_strangers: lastStrangers ? { seq: lastStrangers.seq, schema: lastStrangers.schema, ts_ms: lastStrangers.ts_ms } : null,
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
console.log(JSON.stringify({ ok: true, data: { conversations: withPreview } }, null, 2));
|
|
634
|
+
} else {
|
|
635
|
+
for (const c of convos) {
|
|
636
|
+
const resReady = await client.fetchInbox(c.conversation_id, { sinceSeq: 0, limit: 1, box: 'ready' });
|
|
637
|
+
const resStrangers = await client.fetchInbox(c.conversation_id, { sinceSeq: 0, limit: 1, box: 'strangers' });
|
|
638
|
+
const lastReady = resReady.ok && resReady.data?.messages?.[0];
|
|
639
|
+
const lastStrangers = resStrangers.ok && resStrangers.data?.messages?.[0];
|
|
640
|
+
const preview = lastReady?.payload?.text ?? lastStrangers?.payload?.text ?? lastReady?.payload?.title ?? lastStrangers?.payload?.title ?? lastReady?.schema ?? lastStrangers?.schema ?? '-';
|
|
641
|
+
const ts = (lastReady ?? lastStrangers)?.ts_ms != null ? formatTs((lastReady ?? lastStrangers).ts_ms, raw) : '-';
|
|
642
|
+
const from = (lastReady ?? lastStrangers)?.sender_did?.slice(0, 16) ?? '?';
|
|
643
|
+
console.log(` ${c.conversation_id} ${c.trusted ? 'ready' : 'pending'} last: ${ts} from ${from}... "${String(preview).slice(0, 40)}${String(preview).length > 40 ? '...' : ''}"`);
|
|
644
|
+
}
|
|
645
|
+
console.log('');
|
|
646
|
+
console.log('To view messages: pingagent inbox --conversation <conversation_id>');
|
|
647
|
+
}
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
let messages = [];
|
|
652
|
+
let hasMore = false;
|
|
653
|
+
if (opts.box === 'all') {
|
|
654
|
+
const seen = new Set();
|
|
655
|
+
for (const box of ['ready', 'strangers']) {
|
|
656
|
+
const res = await client.fetchInbox(opts.conversation, { sinceSeq, limit, box });
|
|
657
|
+
if (res.ok && res.data) {
|
|
658
|
+
for (const m of res.data.messages) {
|
|
659
|
+
const id = m.message_id ?? `${opts.conversation}-${m.seq}`;
|
|
660
|
+
if (!seen.has(id)) { seen.add(id); messages.push(m); }
|
|
661
|
+
}
|
|
662
|
+
if (res.data.has_more) hasMore = true;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
messages.sort((a, b) => (a.ts_ms ?? 0) - (b.ts_ms ?? 0));
|
|
666
|
+
} else {
|
|
667
|
+
const res = await client.fetchInbox(opts.conversation, { sinceSeq, limit, box: opts.box });
|
|
668
|
+
if (!res.ok) {
|
|
669
|
+
if (res.error) printError(res.error);
|
|
670
|
+
else console.error('Failed to fetch inbox');
|
|
671
|
+
process.exit(1);
|
|
672
|
+
}
|
|
673
|
+
messages = res.data?.messages ?? [];
|
|
674
|
+
hasMore = res.data?.has_more ?? false;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
if (opts.json) {
|
|
678
|
+
console.log(JSON.stringify({ ok: true, data: { messages, has_more: hasMore } }, null, 2));
|
|
679
|
+
} else {
|
|
680
|
+
const raw = program.opts().raw;
|
|
681
|
+
for (const msg of messages) {
|
|
682
|
+
const ts = formatTs(msg.ts_ms, raw);
|
|
683
|
+
console.log(` ${ts} [${msg.seq}] ${msg.schema} from ${msg.sender_did?.slice(0, 20)}... (${msg.status})`);
|
|
684
|
+
}
|
|
685
|
+
console.log(`${messages.length} message(s), has_more: ${hasMore}`);
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
program
|
|
690
|
+
.command('approve')
|
|
691
|
+
.description('Approve a contact request')
|
|
692
|
+
.option('--profile <name>', 'Use profile from ~/.pingagent/<name>')
|
|
693
|
+
.argument('<conversation_id>', 'Pending DM conversation ID')
|
|
694
|
+
.action(async (conversationId, opts = {}) => {
|
|
695
|
+
const identityPath = getIdentityPathForCommand(opts);
|
|
696
|
+
if (!identityExists(identityPath)) {
|
|
697
|
+
console.error('No identity found. Run: pingagent init (or use --identity-dir / --profile / PINGAGENT_IDENTITY_PATH)');
|
|
698
|
+
process.exit(1);
|
|
699
|
+
}
|
|
700
|
+
const client = await getClient(identityPath);
|
|
701
|
+
|
|
702
|
+
const res = await client.approveContact(conversationId);
|
|
703
|
+
if (res.ok && res.data) {
|
|
704
|
+
console.log(`Approved. DM conversation: ${res.data.dm_conversation_id}`);
|
|
705
|
+
} else {
|
|
706
|
+
if (res.error) printError(res.error);
|
|
707
|
+
else console.error('Failed:', res.error?.message);
|
|
708
|
+
process.exit(1);
|
|
709
|
+
}
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
program
|
|
713
|
+
.command('cancel')
|
|
714
|
+
.description('Cancel a running task')
|
|
715
|
+
.option('--profile <name>', 'Use profile from ~/.pingagent/<name>')
|
|
716
|
+
.argument('<conversation_id>', 'Conversation ID')
|
|
717
|
+
.argument('<task_id>', 'Task ID')
|
|
718
|
+
.action(async (conversationId, taskId, opts = {}) => {
|
|
719
|
+
const identityPath = getIdentityPathForCommand(opts);
|
|
720
|
+
if (!identityExists(identityPath)) {
|
|
721
|
+
console.error('No identity found. Run: pingagent init (or use --identity-dir / --profile / PINGAGENT_IDENTITY_PATH)');
|
|
722
|
+
process.exit(1);
|
|
723
|
+
}
|
|
724
|
+
const client = await getClient(identityPath);
|
|
725
|
+
|
|
726
|
+
const res = await client.cancelTask(conversationId, taskId);
|
|
727
|
+
if (res.ok && res.data) {
|
|
728
|
+
console.log(`Task state: ${res.data.task_state}`);
|
|
729
|
+
} else {
|
|
730
|
+
if (res.error) printError(res.error);
|
|
731
|
+
else console.error('Cancel failed');
|
|
732
|
+
process.exit(1);
|
|
733
|
+
}
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
program
|
|
737
|
+
.command('resolve')
|
|
738
|
+
.description('Resolve alias to DID')
|
|
739
|
+
.option('--profile <name>', 'Use profile from ~/.pingagent/<name>')
|
|
740
|
+
.argument('<alias>', 'Alias (e.g. @my/bot)')
|
|
741
|
+
.action(async (alias, opts = {}) => {
|
|
742
|
+
const identityPath = getIdentityPathForCommand(opts);
|
|
743
|
+
if (!identityExists(identityPath)) {
|
|
744
|
+
console.error('No identity found. Run: pingagent init (or use --identity-dir / --profile / PINGAGENT_IDENTITY_PATH)');
|
|
745
|
+
process.exit(1);
|
|
746
|
+
}
|
|
747
|
+
const client = await getClient(identityPath);
|
|
748
|
+
|
|
749
|
+
const res = await client.resolveAlias(alias);
|
|
750
|
+
if (res.ok && res.data) {
|
|
751
|
+
console.log(`${res.data.alias} → ${res.data.did}`);
|
|
752
|
+
} else {
|
|
753
|
+
if (res.error) printError(res.error);
|
|
754
|
+
else console.error('Not found');
|
|
755
|
+
process.exit(1);
|
|
756
|
+
}
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
// === Conversations (local history management + server view) ===
|
|
760
|
+
const conversations = program.command('conversations').description('Manage conversations (local history and server-side metadata)');
|
|
761
|
+
|
|
762
|
+
conversations
|
|
763
|
+
.command('list')
|
|
764
|
+
.description('List server conversations for the current identity (optionally by profile). Use --type pending_dm to see only pending contact requests.')
|
|
765
|
+
.option('--profile <name>', 'Use profile from ~/.pingagent/<name>')
|
|
766
|
+
.option('--type <dm|pending_dm|all>', 'Filter: dm (established only), pending_dm (waiting for approval), or all (default)', 'all')
|
|
767
|
+
.option('--json', 'Output as JSON')
|
|
768
|
+
.action(async (opts = {}) => {
|
|
769
|
+
const identityPath = getIdentityPathForCommand(opts);
|
|
770
|
+
if (!identityExists(identityPath)) {
|
|
771
|
+
console.error('No identity found. Run: pingagent init (or use --identity-dir / --profile / PINGAGENT_IDENTITY_PATH)');
|
|
772
|
+
process.exit(1);
|
|
773
|
+
}
|
|
774
|
+
const client = await getClient(identityPath);
|
|
775
|
+
const typeArg = opts.type === 'pending_dm' ? 'pending_dm' : opts.type === 'dm' ? 'dm' : undefined;
|
|
776
|
+
const res = await client.listConversations(typeArg ? { type: typeArg } : undefined);
|
|
777
|
+
const convos = res.ok && res.data?.conversations ? res.data.conversations : [];
|
|
778
|
+
const raw = program.opts().raw;
|
|
779
|
+
if (opts.json) {
|
|
780
|
+
console.log(JSON.stringify({ ok: res.ok, conversations: convos }, null, 2));
|
|
781
|
+
} else if (!res.ok) {
|
|
782
|
+
if (res.error) printError(res.error);
|
|
783
|
+
else console.error('Failed to list conversations');
|
|
784
|
+
process.exit(1);
|
|
785
|
+
} else if (convos.length === 0) {
|
|
786
|
+
const hint = typeArg === 'pending_dm'
|
|
787
|
+
? 'No pending contact requests.'
|
|
788
|
+
: 'No conversations. Send a message (pingagent chat --to <did>) or approve a contact request first.';
|
|
789
|
+
console.log(hint);
|
|
790
|
+
} else {
|
|
791
|
+
for (const c of convos) {
|
|
792
|
+
const ts = c.last_message_ts_ms != null ? formatTs(c.last_message_ts_ms, raw) : '-';
|
|
793
|
+
const target = c.target_did?.slice(0, 30) ?? '?';
|
|
794
|
+
console.log(` ${c.conversation_id} ${c.trusted ? 'ready' : 'pending'} last: ${ts} target: ${target}`);
|
|
795
|
+
}
|
|
796
|
+
console.log(`\n${convos.length} conversation(s)`);
|
|
797
|
+
}
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
conversations
|
|
801
|
+
.command('show')
|
|
802
|
+
.description('Show details and recent messages for a conversation')
|
|
803
|
+
.option('--profile <name>', 'Use profile from ~/.pingagent/<name>')
|
|
804
|
+
.argument('<conversation_id>', 'Conversation ID')
|
|
805
|
+
.option('--limit <n>', 'Number of recent messages to fetch', '20')
|
|
806
|
+
.option('--json', 'Output as JSON')
|
|
807
|
+
.action(async (conversationId, opts = {}) => {
|
|
808
|
+
const identityPath = getIdentityPathForCommand(opts);
|
|
809
|
+
if (!identityExists(identityPath)) {
|
|
810
|
+
console.error('No identity found. Run: pingagent init (or use --identity-dir / --profile / PINGAGENT_IDENTITY_PATH)');
|
|
811
|
+
process.exit(1);
|
|
812
|
+
}
|
|
813
|
+
const client = await getClient(identityPath);
|
|
814
|
+
const metaRes = await client.listConversations({ type: 'dm' });
|
|
815
|
+
const convo = metaRes.ok && metaRes.data?.conversations
|
|
816
|
+
? metaRes.data.conversations.find(c => c.conversation_id === conversationId)
|
|
817
|
+
: null;
|
|
818
|
+
const limit = parseInt(opts.limit, 10) || 20;
|
|
819
|
+
const inboxRes = await client.fetchInbox(conversationId, { sinceSeq: 0, limit, box: 'all' });
|
|
820
|
+
const messages = inboxRes.ok && inboxRes.data?.messages ? inboxRes.data.messages : [];
|
|
821
|
+
const raw = program.opts().raw;
|
|
822
|
+
|
|
823
|
+
if (opts.json) {
|
|
824
|
+
console.log(JSON.stringify({
|
|
825
|
+
ok: metaRes.ok && inboxRes.ok,
|
|
826
|
+
conversation: convo ?? null,
|
|
827
|
+
messages,
|
|
828
|
+
}, null, 2));
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
if (!inboxRes.ok) {
|
|
833
|
+
if (inboxRes.error) printError(inboxRes.error);
|
|
834
|
+
else console.error('Failed to fetch messages for conversation');
|
|
835
|
+
process.exit(1);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
if (convo) {
|
|
839
|
+
console.log(`Conversation: ${convo.conversation_id}`);
|
|
840
|
+
console.log(`Type: ${convo.type}`);
|
|
841
|
+
console.log(`Trusted: ${convo.trusted}`);
|
|
842
|
+
console.log(`Target DID: ${convo.target_did ?? '-'}`);
|
|
843
|
+
if (convo.last_message_ts_ms != null) {
|
|
844
|
+
console.log(`Last message: ${formatTs(convo.last_message_ts_ms, raw)}`);
|
|
845
|
+
}
|
|
846
|
+
console.log('');
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
if (messages.length === 0) {
|
|
850
|
+
console.log('No messages found for this conversation.');
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
for (const msg of messages) {
|
|
855
|
+
const ts = formatTs(msg.ts_ms, raw);
|
|
856
|
+
const from = msg.sender_did?.slice(0, 20) ?? '?';
|
|
857
|
+
const text = msg.payload?.text ?? msg.payload?.title ?? msg.schema;
|
|
858
|
+
console.log(` ${ts} [${msg.seq}] ${msg.schema} from ${from}... ${text}`);
|
|
859
|
+
}
|
|
860
|
+
console.log(`\n${messages.length} message(s)`);
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
conversations
|
|
864
|
+
.command('delete')
|
|
865
|
+
.description('Delete local history for a conversation (server conversation unchanged)')
|
|
866
|
+
.option('--profile <name>', 'Use profile from ~/.pingagent/<name>')
|
|
867
|
+
.argument('<conversation_id>', 'Conversation ID')
|
|
868
|
+
.action((conversationId, opts = {}) => {
|
|
869
|
+
const identityPath = getIdentityPathForCommand(opts);
|
|
870
|
+
const store = openStore(identityPath);
|
|
871
|
+
const mgr = new HistoryManager(store);
|
|
872
|
+
const count = mgr.delete(conversationId);
|
|
873
|
+
console.log(`Deleted ${count} message(s) for conversation ${conversationId}`);
|
|
874
|
+
store.close();
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
conversations
|
|
878
|
+
.command('clear')
|
|
879
|
+
.description('Clear all local history (use --force to skip confirmation)')
|
|
880
|
+
.option('--profile <name>', 'Use profile from ~/.pingagent/<name>')
|
|
881
|
+
.option('--force', 'Skip confirmation')
|
|
882
|
+
.action((opts = {}) => {
|
|
883
|
+
const identityPath = getIdentityPathForCommand(opts);
|
|
884
|
+
const store = openStore(identityPath);
|
|
885
|
+
const mgr = new HistoryManager(store);
|
|
886
|
+
const convos = mgr.listConversations();
|
|
887
|
+
if (convos.length === 0) {
|
|
888
|
+
console.log('No local history to clear.');
|
|
889
|
+
store.close();
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
if (!opts.force) {
|
|
893
|
+
console.log(`This will delete local history for ${convos.length} conversation(s). Run with --force to confirm.`);
|
|
894
|
+
store.close();
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
let total = 0;
|
|
898
|
+
for (const c of convos) {
|
|
899
|
+
total += mgr.delete(c.conversation_id);
|
|
900
|
+
}
|
|
901
|
+
console.log(`Cleared ${total} message(s) from ${convos.length} conversation(s).`);
|
|
902
|
+
store.close();
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
// === Contacts ===
|
|
906
|
+
const contacts = program.command('contacts').description('Manage local contacts');
|
|
907
|
+
|
|
908
|
+
contacts
|
|
909
|
+
.command('list')
|
|
910
|
+
.description('List saved contacts')
|
|
911
|
+
.option('--tag <tag>', 'Filter by tag')
|
|
912
|
+
.option('--trusted', 'Show only trusted contacts')
|
|
913
|
+
.option('--json', 'Output as JSON')
|
|
914
|
+
.action((opts) => {
|
|
915
|
+
const store = openStore();
|
|
916
|
+
const mgr = new ContactManager(store);
|
|
917
|
+
const list = mgr.list({ tag: opts.tag, trusted: opts.trusted ? true : undefined });
|
|
918
|
+
if (opts.json) {
|
|
919
|
+
console.log(JSON.stringify(list, null, 2));
|
|
920
|
+
} else if (list.length === 0) {
|
|
921
|
+
console.log('No contacts found.');
|
|
922
|
+
} else {
|
|
923
|
+
for (const c of list) {
|
|
924
|
+
const name = c.display_name ?? c.alias ?? c.did.slice(0, 30) + '...';
|
|
925
|
+
const trust = c.trusted ? ' [trusted]' : '';
|
|
926
|
+
console.log(` ${name}${trust} ${c.did}`);
|
|
927
|
+
}
|
|
928
|
+
console.log(`\n${list.length} contact(s)`);
|
|
929
|
+
}
|
|
930
|
+
store.close();
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
contacts
|
|
934
|
+
.command('add')
|
|
935
|
+
.description('Add a contact')
|
|
936
|
+
.argument('<did>', 'Agent DID')
|
|
937
|
+
.option('--alias <alias>', 'Alias (e.g. @my/bot)')
|
|
938
|
+
.option('--name <name>', 'Display name')
|
|
939
|
+
.option('--notes <notes>', 'Notes')
|
|
940
|
+
.option('--tag <tag>', 'Tag')
|
|
941
|
+
.action((did, opts) => {
|
|
942
|
+
const store = openStore();
|
|
943
|
+
const mgr = new ContactManager(store);
|
|
944
|
+
mgr.add({
|
|
945
|
+
did,
|
|
946
|
+
alias: opts.alias,
|
|
947
|
+
display_name: opts.name,
|
|
948
|
+
notes: opts.notes,
|
|
949
|
+
trusted: false,
|
|
950
|
+
tags: opts.tag ? [opts.tag] : undefined,
|
|
951
|
+
});
|
|
952
|
+
console.log(`Contact added: ${did}`);
|
|
953
|
+
store.close();
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
contacts
|
|
957
|
+
.command('remove')
|
|
958
|
+
.description('Remove a contact')
|
|
959
|
+
.argument('<did>', 'Agent DID')
|
|
960
|
+
.action((did) => {
|
|
961
|
+
const store = openStore();
|
|
962
|
+
const mgr = new ContactManager(store);
|
|
963
|
+
if (mgr.remove(did)) {
|
|
964
|
+
console.log(`Contact removed: ${did}`);
|
|
965
|
+
} else {
|
|
966
|
+
console.log('Contact not found.');
|
|
967
|
+
}
|
|
968
|
+
store.close();
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
contacts
|
|
972
|
+
.command('update')
|
|
973
|
+
.description('Update a contact')
|
|
974
|
+
.argument('<did>', 'Agent DID')
|
|
975
|
+
.option('--alias <alias>', 'Alias')
|
|
976
|
+
.option('--name <name>', 'Display name')
|
|
977
|
+
.option('--notes <notes>', 'Notes')
|
|
978
|
+
.option('--tag <tag>', 'Tag (replaces existing tags)')
|
|
979
|
+
.action((did, opts) => {
|
|
980
|
+
const store = openStore();
|
|
981
|
+
const mgr = new ContactManager(store);
|
|
982
|
+
const updates = {};
|
|
983
|
+
if (opts.alias) updates.alias = opts.alias;
|
|
984
|
+
if (opts.name) updates.display_name = opts.name;
|
|
985
|
+
if (opts.notes) updates.notes = opts.notes;
|
|
986
|
+
if (opts.tag) updates.tags = [opts.tag];
|
|
987
|
+
const result = mgr.update(did, updates);
|
|
988
|
+
if (result) {
|
|
989
|
+
console.log(`Contact updated: ${did}`);
|
|
990
|
+
} else {
|
|
991
|
+
console.log('Contact not found.');
|
|
992
|
+
}
|
|
993
|
+
store.close();
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
contacts
|
|
997
|
+
.command('search')
|
|
998
|
+
.description('Search contacts')
|
|
999
|
+
.argument('<query>', 'Search query')
|
|
1000
|
+
.option('--json', 'Output as JSON')
|
|
1001
|
+
.action((query, opts) => {
|
|
1002
|
+
const store = openStore();
|
|
1003
|
+
const mgr = new ContactManager(store);
|
|
1004
|
+
const results = mgr.search(query);
|
|
1005
|
+
if (opts.json) {
|
|
1006
|
+
console.log(JSON.stringify(results, null, 2));
|
|
1007
|
+
} else if (results.length === 0) {
|
|
1008
|
+
console.log('No contacts match.');
|
|
1009
|
+
} else {
|
|
1010
|
+
for (const c of results) {
|
|
1011
|
+
console.log(` ${c.display_name ?? c.alias ?? c.did.slice(0, 30)} ${c.did}`);
|
|
1012
|
+
}
|
|
1013
|
+
console.log(`\n${results.length} match(es)`);
|
|
1014
|
+
}
|
|
1015
|
+
store.close();
|
|
1016
|
+
});
|
|
1017
|
+
|
|
1018
|
+
contacts
|
|
1019
|
+
.command('export')
|
|
1020
|
+
.description('Export contacts')
|
|
1021
|
+
.option('--format <format>', 'json or csv', 'json')
|
|
1022
|
+
.option('--output <file>', 'Output file path')
|
|
1023
|
+
.action((opts) => {
|
|
1024
|
+
const store = openStore();
|
|
1025
|
+
const mgr = new ContactManager(store);
|
|
1026
|
+
const data = mgr.export(opts.format);
|
|
1027
|
+
if (opts.output) {
|
|
1028
|
+
fs.writeFileSync(opts.output, data);
|
|
1029
|
+
console.log(`Exported to ${opts.output}`);
|
|
1030
|
+
} else {
|
|
1031
|
+
console.log(data);
|
|
1032
|
+
}
|
|
1033
|
+
store.close();
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
contacts
|
|
1037
|
+
.command('import')
|
|
1038
|
+
.description('Import contacts from file')
|
|
1039
|
+
.argument('<file>', 'File to import')
|
|
1040
|
+
.option('--format <format>', 'json or csv', 'json')
|
|
1041
|
+
.action((file, opts) => {
|
|
1042
|
+
const store = openStore();
|
|
1043
|
+
const mgr = new ContactManager(store);
|
|
1044
|
+
const data = fs.readFileSync(file, 'utf-8');
|
|
1045
|
+
const result = mgr.import(data, opts.format);
|
|
1046
|
+
console.log(`Imported: ${result.imported}, Skipped: ${result.skipped}`);
|
|
1047
|
+
store.close();
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
// === History ===
|
|
1051
|
+
const history = program.command('history').description('Manage local chat history');
|
|
1052
|
+
|
|
1053
|
+
history
|
|
1054
|
+
.command('conversations')
|
|
1055
|
+
.description('List conversations with local history')
|
|
1056
|
+
.option('--json', 'Output as JSON')
|
|
1057
|
+
.action((opts) => {
|
|
1058
|
+
const raw = program.opts().raw;
|
|
1059
|
+
const store = openStore();
|
|
1060
|
+
const mgr = new HistoryManager(store);
|
|
1061
|
+
const convos = mgr.listConversations();
|
|
1062
|
+
if (opts.json) {
|
|
1063
|
+
console.log(JSON.stringify(convos, null, 2));
|
|
1064
|
+
} else if (convos.length === 0) {
|
|
1065
|
+
console.log('No local history.');
|
|
1066
|
+
} else {
|
|
1067
|
+
for (const c of convos) {
|
|
1068
|
+
const date = formatTs(c.last_message_at, raw);
|
|
1069
|
+
console.log(` ${c.conversation_id} ${c.message_count} msg(s) last: ${date}`);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
store.close();
|
|
1073
|
+
});
|
|
1074
|
+
|
|
1075
|
+
history
|
|
1076
|
+
.command('list')
|
|
1077
|
+
.description('List messages in a conversation (default: most recent conversation)')
|
|
1078
|
+
.argument('[conversation_id]', 'Conversation ID (omit to use most recent from local history)')
|
|
1079
|
+
.option('--limit <n>', 'Limit', '50')
|
|
1080
|
+
.option('--json', 'Output as JSON')
|
|
1081
|
+
.action((conversationId, opts) => {
|
|
1082
|
+
const store = openStore();
|
|
1083
|
+
const mgr = new HistoryManager(store);
|
|
1084
|
+
let cid = conversationId;
|
|
1085
|
+
if (!cid) {
|
|
1086
|
+
const convos = mgr.listConversations();
|
|
1087
|
+
if (convos.length === 0) {
|
|
1088
|
+
console.log('No local history. Run "pingagent inbox" then "pingagent history sync <conversation_id>" to sync, or pass a conversation_id.');
|
|
1089
|
+
store.close();
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
cid = convos[0].conversation_id;
|
|
1093
|
+
if (!opts.json) {
|
|
1094
|
+
console.log(`Using most recent conversation: ${cid}\n`);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
const messages = mgr.list(cid, { limit: parseInt(opts.limit) });
|
|
1098
|
+
if (opts.json) {
|
|
1099
|
+
console.log(JSON.stringify(messages, null, 2));
|
|
1100
|
+
} else if (messages.length === 0) {
|
|
1101
|
+
console.log('No messages found.');
|
|
1102
|
+
} else {
|
|
1103
|
+
const raw = program.opts().raw;
|
|
1104
|
+
for (const m of messages) {
|
|
1105
|
+
const dir = m.direction === 'sent' ? '→' : '←';
|
|
1106
|
+
const text = m.payload?.text ?? m.payload?.title ?? m.schema;
|
|
1107
|
+
const ts = formatTs(m.ts_ms, raw);
|
|
1108
|
+
console.log(` ${ts} ${dir} [${m.seq ?? '-'}] ${m.schema} ${text}`);
|
|
1109
|
+
}
|
|
1110
|
+
console.log(`\n${messages.length} message(s)`);
|
|
1111
|
+
}
|
|
1112
|
+
store.close();
|
|
1113
|
+
});
|
|
1114
|
+
|
|
1115
|
+
history
|
|
1116
|
+
.command('sync')
|
|
1117
|
+
.description('Sync messages from server')
|
|
1118
|
+
.argument('<conversation_id>', 'Conversation ID')
|
|
1119
|
+
.option('--full', 'Full sync from beginning')
|
|
1120
|
+
.action(async (conversationId, opts) => {
|
|
1121
|
+
if (!identityExists(getEffectiveIdentityPath())) {
|
|
1122
|
+
console.error('No identity found. Run: pingagent init (or use --identity-dir / PINGAGENT_IDENTITY_PATH)');
|
|
1123
|
+
process.exit(1);
|
|
1124
|
+
}
|
|
1125
|
+
const client = await getClient();
|
|
1126
|
+
const store = openStore();
|
|
1127
|
+
const mgr = new HistoryManager(store);
|
|
1128
|
+
const result = await mgr.syncFromServer(client, conversationId, { full: opts.full });
|
|
1129
|
+
console.log(`Synced ${result.synced} message(s)`);
|
|
1130
|
+
store.close();
|
|
1131
|
+
});
|
|
1132
|
+
|
|
1133
|
+
history
|
|
1134
|
+
.command('search')
|
|
1135
|
+
.description('Search message history')
|
|
1136
|
+
.argument('<query>', 'Search query')
|
|
1137
|
+
.option('--conversation <id>', 'Limit to conversation')
|
|
1138
|
+
.option('--json', 'Output as JSON')
|
|
1139
|
+
.action((query, opts) => {
|
|
1140
|
+
const store = openStore();
|
|
1141
|
+
const mgr = new HistoryManager(store);
|
|
1142
|
+
const results = mgr.search(query, { conversationId: opts.conversation });
|
|
1143
|
+
if (opts.json) {
|
|
1144
|
+
console.log(JSON.stringify(results, null, 2));
|
|
1145
|
+
} else if (results.length === 0) {
|
|
1146
|
+
console.log('No messages match.');
|
|
1147
|
+
} else {
|
|
1148
|
+
for (const m of results) {
|
|
1149
|
+
const dir = m.direction === 'sent' ? '→' : '←';
|
|
1150
|
+
const text = m.payload?.text ?? m.payload?.title ?? m.schema;
|
|
1151
|
+
console.log(` ${dir} ${m.conversation_id.slice(0, 15)} ${text}`);
|
|
1152
|
+
}
|
|
1153
|
+
console.log(`\n${results.length} match(es)`);
|
|
1154
|
+
}
|
|
1155
|
+
store.close();
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1158
|
+
history
|
|
1159
|
+
.command('export')
|
|
1160
|
+
.description('Export chat history')
|
|
1161
|
+
.option('--conversation <id>', 'Conversation ID (all if omitted)')
|
|
1162
|
+
.option('--format <format>', 'json or csv', 'json')
|
|
1163
|
+
.option('--output <file>', 'Output file path')
|
|
1164
|
+
.action((opts) => {
|
|
1165
|
+
const store = openStore();
|
|
1166
|
+
const mgr = new HistoryManager(store);
|
|
1167
|
+
const data = mgr.export({ conversationId: opts.conversation, format: opts.format });
|
|
1168
|
+
if (opts.output) {
|
|
1169
|
+
fs.writeFileSync(opts.output, data);
|
|
1170
|
+
console.log(`Exported to ${opts.output}`);
|
|
1171
|
+
} else {
|
|
1172
|
+
console.log(data);
|
|
1173
|
+
}
|
|
1174
|
+
store.close();
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
history
|
|
1178
|
+
.command('delete')
|
|
1179
|
+
.description('Delete history for a conversation')
|
|
1180
|
+
.argument('<conversation_id>', 'Conversation ID')
|
|
1181
|
+
.action((conversationId) => {
|
|
1182
|
+
const store = openStore();
|
|
1183
|
+
const mgr = new HistoryManager(store);
|
|
1184
|
+
const count = mgr.delete(conversationId);
|
|
1185
|
+
console.log(`Deleted ${count} message(s)`);
|
|
1186
|
+
store.close();
|
|
1187
|
+
});
|
|
1188
|
+
|
|
1189
|
+
// === A2A (Agent-to-Agent Protocol) ===
|
|
1190
|
+
const a2a = program.command('a2a').description('Interact with external A2A-compatible agents');
|
|
1191
|
+
|
|
1192
|
+
a2a
|
|
1193
|
+
.command('discover')
|
|
1194
|
+
.description('Fetch and display an external agent\'s AgentCard')
|
|
1195
|
+
.argument('<url>', 'Agent URL (e.g. https://agent.example.com)')
|
|
1196
|
+
.option('--json', 'Output as JSON')
|
|
1197
|
+
.action(async (url, opts) => {
|
|
1198
|
+
const adapter = new A2AAdapter({ agentUrl: url });
|
|
1199
|
+
const card = await adapter.getAgentCard();
|
|
1200
|
+
if (opts.json) {
|
|
1201
|
+
console.log(JSON.stringify(card, null, 2));
|
|
1202
|
+
} else {
|
|
1203
|
+
console.log(`Agent: ${card.name}`);
|
|
1204
|
+
console.log(`Description: ${card.description}`);
|
|
1205
|
+
console.log(`URL: ${card.url}`);
|
|
1206
|
+
console.log(`Version: ${card.version} (protocol ${card.protocolVersion})`);
|
|
1207
|
+
if (card.provider) console.log(`Provider: ${card.provider.organization}`);
|
|
1208
|
+
console.log(`Capabilities: streaming=${card.capabilities.streaming ?? false}, push=${card.capabilities.pushNotifications ?? false}`);
|
|
1209
|
+
console.log(`Skills:`);
|
|
1210
|
+
for (const s of card.skills) {
|
|
1211
|
+
console.log(` [${s.id}] ${s.name}: ${s.description}`);
|
|
1212
|
+
if (s.tags.length) console.log(` tags: ${s.tags.join(', ')}`);
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
});
|
|
1216
|
+
|
|
1217
|
+
a2a
|
|
1218
|
+
.command('send')
|
|
1219
|
+
.description('Send a task to an external A2A agent')
|
|
1220
|
+
.argument('<url>', 'Agent URL')
|
|
1221
|
+
.requiredOption('--task <title>', 'Task title/prompt')
|
|
1222
|
+
.option('--description <desc>', 'Task description')
|
|
1223
|
+
.option('--wait', 'Wait for task completion')
|
|
1224
|
+
.option('--timeout <seconds>', 'Timeout in seconds', '120')
|
|
1225
|
+
.option('--auth <token>', 'Bearer token for the remote agent')
|
|
1226
|
+
.option('--json', 'Output as JSON')
|
|
1227
|
+
.action(async (url, opts) => {
|
|
1228
|
+
const adapter = new A2AAdapter({
|
|
1229
|
+
agentUrl: url,
|
|
1230
|
+
authToken: opts.auth,
|
|
1231
|
+
});
|
|
1232
|
+
|
|
1233
|
+
const result = await adapter.sendTask({
|
|
1234
|
+
title: opts.task,
|
|
1235
|
+
description: opts.description,
|
|
1236
|
+
wait: opts.wait,
|
|
1237
|
+
timeoutMs: parseInt(opts.timeout) * 1000,
|
|
1238
|
+
});
|
|
1239
|
+
|
|
1240
|
+
if (opts.json) {
|
|
1241
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1242
|
+
} else {
|
|
1243
|
+
console.log(`Task ID: ${result.taskId}`);
|
|
1244
|
+
console.log(`State: ${result.state}`);
|
|
1245
|
+
if (result.summary) console.log(`Summary: ${result.summary}`);
|
|
1246
|
+
if (result.output) console.log(`Output: ${JSON.stringify(result.output, null, 2)}`);
|
|
1247
|
+
}
|
|
1248
|
+
});
|
|
1249
|
+
|
|
1250
|
+
a2a
|
|
1251
|
+
.command('status')
|
|
1252
|
+
.description('Get task status from an external A2A agent')
|
|
1253
|
+
.argument('<url>', 'Agent URL')
|
|
1254
|
+
.requiredOption('--task-id <id>', 'Task ID')
|
|
1255
|
+
.option('--auth <token>', 'Bearer token')
|
|
1256
|
+
.option('--json', 'Output as JSON')
|
|
1257
|
+
.action(async (url, opts) => {
|
|
1258
|
+
const adapter = new A2AAdapter({
|
|
1259
|
+
agentUrl: url,
|
|
1260
|
+
authToken: opts.auth,
|
|
1261
|
+
});
|
|
1262
|
+
|
|
1263
|
+
const result = await adapter.getTaskStatus(opts.taskId);
|
|
1264
|
+
if (opts.json) {
|
|
1265
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1266
|
+
} else {
|
|
1267
|
+
console.log(`Task ID: ${result.taskId}`);
|
|
1268
|
+
console.log(`State: ${result.state}`);
|
|
1269
|
+
if (result.summary) console.log(`Summary: ${result.summary}`);
|
|
1270
|
+
}
|
|
1271
|
+
});
|
|
1272
|
+
|
|
1273
|
+
a2a
|
|
1274
|
+
.command('cancel')
|
|
1275
|
+
.description('Cancel a task on an external A2A agent')
|
|
1276
|
+
.argument('<url>', 'Agent URL')
|
|
1277
|
+
.requiredOption('--task-id <id>', 'Task ID')
|
|
1278
|
+
.option('--auth <token>', 'Bearer token')
|
|
1279
|
+
.action(async (url, opts) => {
|
|
1280
|
+
const adapter = new A2AAdapter({
|
|
1281
|
+
agentUrl: url,
|
|
1282
|
+
authToken: opts.auth,
|
|
1283
|
+
});
|
|
1284
|
+
const result = await adapter.cancelTask(opts.taskId);
|
|
1285
|
+
console.log(`Task ${result.taskId}: ${result.state}`);
|
|
1286
|
+
});
|
|
1287
|
+
|
|
1288
|
+
// === Billing ===
|
|
1289
|
+
const billing = program.command('billing').description('Manage billing group (primary + linked devices)');
|
|
1290
|
+
|
|
1291
|
+
billing
|
|
1292
|
+
.command('link-code')
|
|
1293
|
+
.description('Generate a link code for adding a device to your subscription (primary only)')
|
|
1294
|
+
.option('--json', 'Output as JSON')
|
|
1295
|
+
.action(async (opts) => {
|
|
1296
|
+
const client = await getClient();
|
|
1297
|
+
const res = await client.createBillingLinkCode();
|
|
1298
|
+
if (!res.ok) {
|
|
1299
|
+
if (opts.json) { console.log(JSON.stringify(res, null, 2)); } else {
|
|
1300
|
+
console.error(`Error: ${res.error?.message ?? 'Failed to create link code'}`);
|
|
1301
|
+
if (res.error?.hint) console.error(`Hint: ${res.error.hint}`);
|
|
1302
|
+
}
|
|
1303
|
+
process.exit(1);
|
|
1304
|
+
}
|
|
1305
|
+
if (opts.json) {
|
|
1306
|
+
console.log(JSON.stringify(res.data, null, 2));
|
|
1307
|
+
} else {
|
|
1308
|
+
console.log(`Link code: ${res.data.code}`);
|
|
1309
|
+
console.log(`Expires in ${res.data.expires_in_seconds}s`);
|
|
1310
|
+
console.log(`\nRun on the new device:\n pingagent billing link --code ${res.data.code}`);
|
|
1311
|
+
}
|
|
1312
|
+
});
|
|
1313
|
+
|
|
1314
|
+
billing
|
|
1315
|
+
.command('link')
|
|
1316
|
+
.description('Link this device to a primary subscription using a link code')
|
|
1317
|
+
.requiredOption('--code <code>', 'Link code from primary device')
|
|
1318
|
+
.option('--json', 'Output as JSON')
|
|
1319
|
+
.action(async (opts) => {
|
|
1320
|
+
const client = await getClient();
|
|
1321
|
+
const res = await client.redeemBillingLink(opts.code);
|
|
1322
|
+
if (!res.ok) {
|
|
1323
|
+
if (opts.json) { console.log(JSON.stringify(res, null, 2)); } else {
|
|
1324
|
+
console.error(`Error: ${res.error?.message ?? 'Failed to link device'}`);
|
|
1325
|
+
if (res.error?.hint) console.error(`Hint: ${res.error.hint}`);
|
|
1326
|
+
}
|
|
1327
|
+
process.exit(1);
|
|
1328
|
+
}
|
|
1329
|
+
if (opts.json) {
|
|
1330
|
+
console.log(JSON.stringify(res.data, null, 2));
|
|
1331
|
+
} else {
|
|
1332
|
+
console.log(`Linked to primary: ${res.data.primary_did}`);
|
|
1333
|
+
console.log('This device now shares the primary subscription tier and quotas.');
|
|
1334
|
+
}
|
|
1335
|
+
});
|
|
1336
|
+
|
|
1337
|
+
billing
|
|
1338
|
+
.command('linked-devices')
|
|
1339
|
+
.description('List all devices in your billing group')
|
|
1340
|
+
.option('--json', 'Output as JSON')
|
|
1341
|
+
.action(async (opts) => {
|
|
1342
|
+
const client = await getClient();
|
|
1343
|
+
const res = await client.getLinkedDevices();
|
|
1344
|
+
if (!res.ok) {
|
|
1345
|
+
if (opts.json) { console.log(JSON.stringify(res, null, 2)); } else {
|
|
1346
|
+
console.error(`Error: ${res.error?.message ?? 'Failed to get linked devices'}`);
|
|
1347
|
+
}
|
|
1348
|
+
process.exit(1);
|
|
1349
|
+
}
|
|
1350
|
+
if (opts.json) {
|
|
1351
|
+
console.log(JSON.stringify(res.data, null, 2));
|
|
1352
|
+
} else {
|
|
1353
|
+
const d = res.data;
|
|
1354
|
+
console.log(`Primary DID: ${d.primary_did}${d.is_primary ? ' (this device)' : ''}`);
|
|
1355
|
+
if (d.linked_dids.length === 0) {
|
|
1356
|
+
console.log('No linked devices.');
|
|
1357
|
+
} else {
|
|
1358
|
+
console.log(`Linked devices (${d.linked_dids.length}):`);
|
|
1359
|
+
for (const did of d.linked_dids) {
|
|
1360
|
+
const marker = did === client.getDid() ? ' (this device)' : '';
|
|
1361
|
+
console.log(` - ${did}${marker}`);
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
});
|
|
1366
|
+
|
|
1367
|
+
billing
|
|
1368
|
+
.command('unlink')
|
|
1369
|
+
.description('Remove a linked device from your billing group (primary only)')
|
|
1370
|
+
.requiredOption('--did <did>', 'DID of the device to unlink')
|
|
1371
|
+
.option('--json', 'Output as JSON')
|
|
1372
|
+
.action(async (opts) => {
|
|
1373
|
+
const client = await getClient();
|
|
1374
|
+
const res = await client.unlinkBillingDevice(opts.did);
|
|
1375
|
+
if (!res.ok) {
|
|
1376
|
+
if (opts.json) { console.log(JSON.stringify(res, null, 2)); } else {
|
|
1377
|
+
console.error(`Error: ${res.error?.message ?? 'Failed to unlink device'}`);
|
|
1378
|
+
if (res.error?.hint) console.error(`Hint: ${res.error.hint}`);
|
|
1379
|
+
}
|
|
1380
|
+
process.exit(1);
|
|
1381
|
+
}
|
|
1382
|
+
if (opts.json) {
|
|
1383
|
+
console.log(JSON.stringify({ ok: true }, null, 2));
|
|
1384
|
+
} else {
|
|
1385
|
+
console.log(`Unlinked: ${opts.did}`);
|
|
1386
|
+
console.log('The device will revert to ghost tier.');
|
|
1387
|
+
}
|
|
1388
|
+
});
|
|
1389
|
+
|
|
1390
|
+
program
|
|
1391
|
+
.command('web')
|
|
1392
|
+
.description('Start local web UI for debugging and audit. By default scans ~/.pingagent for profiles; use --identity-dir to lock to one profile.')
|
|
1393
|
+
.option('--port <port>', 'Port for the web server', '3846')
|
|
1394
|
+
.action(async (opts) => {
|
|
1395
|
+
const serverUrl = process.env.PINGAGENT_SERVER_URL || DEFAULT_SERVER;
|
|
1396
|
+
const port = parseInt(opts.port, 10) || 3846;
|
|
1397
|
+
const identityDir = program.opts().identityDir;
|
|
1398
|
+
const defaultRoot = path.join(os.homedir(), '.pingagent');
|
|
1399
|
+
const resolvedRoot = resolvePath(defaultRoot);
|
|
1400
|
+
const { startWebServer } = await import('../dist/web-server.js');
|
|
1401
|
+
const useSingleProfile = identityDir && path.resolve(resolvePath(identityDir)) !== resolvedRoot;
|
|
1402
|
+
if (useSingleProfile) {
|
|
1403
|
+
const identityPath = path.join(resolvePath(identityDir), 'identity.json');
|
|
1404
|
+
if (!identityExists(identityPath)) {
|
|
1405
|
+
console.error('No identity found at', identityPath, '. Run: pingagent init');
|
|
1406
|
+
process.exit(1);
|
|
1407
|
+
}
|
|
1408
|
+
const storePath = process.env.PINGAGENT_STORE_PATH
|
|
1409
|
+
? resolvePath(process.env.PINGAGENT_STORE_PATH)
|
|
1410
|
+
: path.join(resolvePath(identityDir), 'store.db');
|
|
1411
|
+
await startWebServer({ fixedIdentityPath: identityPath, fixedStorePath: storePath, serverUrl, port });
|
|
1412
|
+
} else {
|
|
1413
|
+
const rootDir = process.env.PINGAGENT_ROOT_DIR || defaultRoot;
|
|
1414
|
+
await startWebServer({ rootDir, serverUrl, port });
|
|
1415
|
+
}
|
|
1416
|
+
});
|
|
1417
|
+
|
|
1418
|
+
program.parse();
|