kinetic-mcp 1.3.1 → 1.3.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/LICENSE +21 -21
- package/README.md +138 -138
- package/package.json +53 -53
- package/scripts/setup.mjs +779 -760
package/scripts/setup.mjs
CHANGED
|
@@ -1,760 +1,779 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Interactive setup wizard for Kinetic MCP Server.
|
|
5
|
-
*
|
|
6
|
-
* Run: npx kinetic-mcp setup
|
|
7
|
-
*
|
|
8
|
-
* This is a standalone .mjs file — no compilation required.
|
|
9
|
-
* Uses @clack/prompts for beautiful terminal UI.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import {
|
|
13
|
-
readFileSync,
|
|
14
|
-
writeFileSync,
|
|
15
|
-
existsSync,
|
|
16
|
-
mkdirSync,
|
|
17
|
-
chmodSync,
|
|
18
|
-
copyFileSync,
|
|
19
|
-
} from "node:fs";
|
|
20
|
-
import { homedir } from "node:os";
|
|
21
|
-
import { join, resolve, dirname } from "node:path";
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
import
|
|
28
|
-
import
|
|
29
|
-
import
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
// ---------------------------------------------------------------------------
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
function
|
|
88
|
-
if (
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
return
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
case "
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
parsed
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if (
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
message:
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
const
|
|
352
|
-
await p.
|
|
353
|
-
message: "
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
if (
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
if (
|
|
484
|
-
p.note(
|
|
485
|
-
[
|
|
486
|
-
"
|
|
487
|
-
"",
|
|
488
|
-
|
|
489
|
-
"",
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
if (
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
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
|
-
} else {
|
|
685
|
-
p.log.
|
|
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
|
-
// Step
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
const
|
|
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
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Interactive setup wizard for Kinetic MCP Server.
|
|
5
|
+
*
|
|
6
|
+
* Run: npx kinetic-mcp setup
|
|
7
|
+
*
|
|
8
|
+
* This is a standalone .mjs file — no compilation required.
|
|
9
|
+
* Uses @clack/prompts for beautiful terminal UI.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
readFileSync,
|
|
14
|
+
writeFileSync,
|
|
15
|
+
existsSync,
|
|
16
|
+
mkdirSync,
|
|
17
|
+
chmodSync,
|
|
18
|
+
copyFileSync,
|
|
19
|
+
} from "node:fs";
|
|
20
|
+
import { homedir } from "node:os";
|
|
21
|
+
import { join, resolve, dirname } from "node:path";
|
|
22
|
+
import { fileURLToPath } from "node:url";
|
|
23
|
+
import { execSync } from "node:child_process";
|
|
24
|
+
import { platform, execPath } from "node:process";
|
|
25
|
+
import { createRequire } from "node:module";
|
|
26
|
+
|
|
27
|
+
import { Keypair } from "@solana/web3.js";
|
|
28
|
+
import bs58 from "bs58";
|
|
29
|
+
import * as p from "@clack/prompts";
|
|
30
|
+
import pc from "picocolors";
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Version
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
const require = createRequire(import.meta.url);
|
|
37
|
+
const { version } = require("../package.json");
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Package root detection
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
44
|
+
const isNpmInstall = /node_modules|[/\\]_npx[/\\]/.test(packageRoot);
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Non-TTY check
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
if (!process.stdin.isTTY) {
|
|
51
|
+
console.error("Error: Setup wizard requires an interactive terminal.");
|
|
52
|
+
console.error("Run: npx kinetic-mcp setup");
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// ASCII Banner
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
const BANNER = `
|
|
61
|
+
██╗ ██╗██╗███╗ ██╗███████╗████████╗██╗ ██████╗
|
|
62
|
+
██║ ██╔╝██║████╗ ██║██╔════╝╚══██╔══╝██║██╔════╝
|
|
63
|
+
█████╔╝ ██║██╔██╗ ██║█████╗ ██║ ██║██║
|
|
64
|
+
██╔═██╗ ██║██║╚██╗██║██╔══╝ ██║ ██║██║
|
|
65
|
+
██║ ██╗██║██║ ╚████║███████╗ ██║ ██║╚██████╗
|
|
66
|
+
╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝╚══════╝ ╚═╝ ╚═╝ ╚═════╝`;
|
|
67
|
+
|
|
68
|
+
async function renderBanner() {
|
|
69
|
+
let rendered;
|
|
70
|
+
try {
|
|
71
|
+
const gradient = (await import("gradient-string")).default;
|
|
72
|
+
const kineticGradient = gradient(["#FEDA75", "#FA7E1E", "#D62976", "#962FBF", "#4F5BD5"]);
|
|
73
|
+
rendered = kineticGradient.multiline(BANNER);
|
|
74
|
+
} catch {
|
|
75
|
+
rendered = pc.bold(pc.magenta(BANNER));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
console.log(rendered);
|
|
79
|
+
console.log(pc.dim(` v${version} — Solana Trading via Claude Desktop`));
|
|
80
|
+
console.log();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Helpers
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
function handleCancel(value) {
|
|
88
|
+
if (p.isCancel(value)) {
|
|
89
|
+
p.cancel("Setup cancelled.");
|
|
90
|
+
process.exit(0);
|
|
91
|
+
}
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function expandHome(filePath) {
|
|
96
|
+
if (filePath.startsWith("~/") || filePath.startsWith("~\\")) {
|
|
97
|
+
return resolve(homedir(), filePath.slice(2));
|
|
98
|
+
}
|
|
99
|
+
return filePath;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function isWSL() {
|
|
103
|
+
try {
|
|
104
|
+
const ver = readFileSync("/proc/version", "utf-8");
|
|
105
|
+
return /microsoft|wsl/i.test(ver);
|
|
106
|
+
} catch {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function getClaudeConfigPath() {
|
|
112
|
+
const home = homedir();
|
|
113
|
+
switch (platform) {
|
|
114
|
+
case "darwin":
|
|
115
|
+
return join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
116
|
+
case "win32":
|
|
117
|
+
return join(
|
|
118
|
+
process.env.APPDATA || join(home, "AppData", "Roaming"),
|
|
119
|
+
"Claude",
|
|
120
|
+
"claude_desktop_config.json",
|
|
121
|
+
);
|
|
122
|
+
case "linux":
|
|
123
|
+
if (isWSL()) return null;
|
|
124
|
+
return join(home, ".config", "Claude", "claude_desktop_config.json");
|
|
125
|
+
default:
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function getDefaultKeypairPath() {
|
|
131
|
+
return join(homedir(), ".config", "solana", "trading-keypair.json");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function validateKeypairFile(filePath) {
|
|
135
|
+
const expanded = expandHome(filePath);
|
|
136
|
+
if (!existsSync(expanded)) {
|
|
137
|
+
throw new Error(`File not found: ${expanded}`);
|
|
138
|
+
}
|
|
139
|
+
const raw = readFileSync(expanded, "utf-8");
|
|
140
|
+
let parsed;
|
|
141
|
+
try {
|
|
142
|
+
parsed = JSON.parse(raw);
|
|
143
|
+
} catch {
|
|
144
|
+
throw new Error(
|
|
145
|
+
"File is not valid JSON. Expected a JSON array of 64 integers (Solana CLI format).",
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
if (
|
|
149
|
+
!Array.isArray(parsed) ||
|
|
150
|
+
parsed.length !== 64 ||
|
|
151
|
+
!parsed.every((n) => typeof n === "number" && Number.isInteger(n))
|
|
152
|
+
) {
|
|
153
|
+
throw new Error("Invalid Solana keypair. Expected a JSON array of exactly 64 integers.");
|
|
154
|
+
}
|
|
155
|
+
return Keypair.fromSecretKey(Uint8Array.from(parsed));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
// Step 2: Build Check
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
async function checkBuild() {
|
|
163
|
+
// npm/npx installs ship with build/ already included
|
|
164
|
+
if (isNpmInstall) return;
|
|
165
|
+
|
|
166
|
+
const buildExists = existsSync(join(packageRoot, "build", "index.js"));
|
|
167
|
+
if (buildExists) return;
|
|
168
|
+
|
|
169
|
+
p.log.warn("Project hasn't been built yet — build/index.js not found.");
|
|
170
|
+
|
|
171
|
+
const runBuild = handleCancel(
|
|
172
|
+
await p.confirm({ message: "Run npm run build now?", initialValue: true }),
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
if (runBuild) {
|
|
176
|
+
const s = p.spinner();
|
|
177
|
+
s.start("Building project...");
|
|
178
|
+
try {
|
|
179
|
+
execSync("npm run build", { stdio: "pipe", cwd: packageRoot });
|
|
180
|
+
s.stop("Build complete");
|
|
181
|
+
} catch {
|
|
182
|
+
s.stop("Build failed");
|
|
183
|
+
p.log.warn("You can try again later with: npm run build");
|
|
184
|
+
}
|
|
185
|
+
} else {
|
|
186
|
+
p.log.info("Skipped. Remember to run npm run build before using the server.");
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
// Step 3: Keypair Setup
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
|
|
194
|
+
async function importFromPhantom() {
|
|
195
|
+
p.note(
|
|
196
|
+
[
|
|
197
|
+
`${pc.bold("How to export from Phantom:")}`,
|
|
198
|
+
"",
|
|
199
|
+
"1. Open Phantom → Settings → Manage Accounts → [Your Account] → Show Private Key",
|
|
200
|
+
"2. Enter your password and copy the base58 string",
|
|
201
|
+
"",
|
|
202
|
+
`${pc.dim("Your key will be masked. It is NOT saved to shell history.")}`,
|
|
203
|
+
].join("\n"),
|
|
204
|
+
"Import Private Key",
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
const key = handleCancel(
|
|
208
|
+
await p.password({
|
|
209
|
+
message: "Paste your private key:",
|
|
210
|
+
mask: "*",
|
|
211
|
+
validate(value) {
|
|
212
|
+
if (!value || value.trim().length === 0) return "Private key is required.";
|
|
213
|
+
|
|
214
|
+
let decoded;
|
|
215
|
+
try {
|
|
216
|
+
decoded = bs58.decode(value.trim());
|
|
217
|
+
} catch {
|
|
218
|
+
return "Invalid format. Expected a base58-encoded string from Phantom.";
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (decoded.length !== 64) {
|
|
222
|
+
if (decoded.length === 32) {
|
|
223
|
+
return "This looks like a 32-byte seed. Use the Solana CLI to generate the full keypair.";
|
|
224
|
+
}
|
|
225
|
+
return `Expected 64-byte secret key, got ${decoded.length} bytes.`;
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
}),
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
const decoded = bs58.decode(key.trim());
|
|
232
|
+
const keypair = Keypair.fromSecretKey(decoded);
|
|
233
|
+
|
|
234
|
+
const publicKey = keypair.publicKey.toBase58();
|
|
235
|
+
p.log.info(`Wallet address: ${pc.cyan(publicKey)}`);
|
|
236
|
+
p.log.message(pc.dim("Verify this matches your Phantom wallet address."));
|
|
237
|
+
|
|
238
|
+
// Save keypair to default path
|
|
239
|
+
const defaultPath = getDefaultKeypairPath();
|
|
240
|
+
|
|
241
|
+
// Check if file exists
|
|
242
|
+
if (existsSync(defaultPath)) {
|
|
243
|
+
const overwrite = handleCancel(
|
|
244
|
+
await p.confirm({
|
|
245
|
+
message: `Keypair file already exists at ${pc.dim(defaultPath)}. Overwrite?`,
|
|
246
|
+
initialValue: false,
|
|
247
|
+
}),
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
if (!overwrite) {
|
|
251
|
+
p.log.info("Keeping existing keypair file.");
|
|
252
|
+
return { publicKey, path: defaultPath };
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
mkdirSync(dirname(defaultPath), { recursive: true, mode: 0o700 });
|
|
257
|
+
writeFileSync(defaultPath, JSON.stringify(Array.from(keypair.secretKey)) + "\n", { mode: 0o600 });
|
|
258
|
+
|
|
259
|
+
if (platform !== "win32") {
|
|
260
|
+
try {
|
|
261
|
+
chmodSync(defaultPath, 0o600);
|
|
262
|
+
} catch (err) {
|
|
263
|
+
p.log.warn(`Could not set file permissions: ${err.message}. Run: chmod 600 ${defaultPath}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
p.log.success(`Keypair saved to ${pc.dim(defaultPath)}`);
|
|
268
|
+
return { publicKey, path: defaultPath };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async function useExistingKeypair() {
|
|
272
|
+
const filePath = handleCancel(
|
|
273
|
+
await p.text({
|
|
274
|
+
message: "Path to keypair file:",
|
|
275
|
+
placeholder: "~/.config/solana/id.json",
|
|
276
|
+
validate(value) {
|
|
277
|
+
if (!value || value.trim().length === 0) return "Path is required.";
|
|
278
|
+
try {
|
|
279
|
+
const expanded = expandHome(value.trim());
|
|
280
|
+
validateKeypairFile(expanded);
|
|
281
|
+
} catch (err) {
|
|
282
|
+
return err.message;
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
}),
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
const expanded = expandHome(filePath.trim());
|
|
289
|
+
const keypair = validateKeypairFile(expanded);
|
|
290
|
+
const publicKey = keypair.publicKey.toBase58();
|
|
291
|
+
|
|
292
|
+
p.log.info(`Wallet address: ${pc.cyan(publicKey)}`);
|
|
293
|
+
|
|
294
|
+
return { publicKey, path: expanded };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async function setupKeypair() {
|
|
298
|
+
const method = handleCancel(
|
|
299
|
+
await p.select({
|
|
300
|
+
message: "How would you like to set up your Solana keypair?",
|
|
301
|
+
options: [
|
|
302
|
+
{
|
|
303
|
+
value: "phantom",
|
|
304
|
+
label: "Import from Phantom",
|
|
305
|
+
hint: "paste your base58 private key",
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
value: "existing",
|
|
309
|
+
label: "Use existing keypair file",
|
|
310
|
+
hint: "Solana CLI JSON format",
|
|
311
|
+
},
|
|
312
|
+
],
|
|
313
|
+
}),
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
if (method === "phantom") {
|
|
317
|
+
return importFromPhantom();
|
|
318
|
+
} else {
|
|
319
|
+
return useExistingKeypair();
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ---------------------------------------------------------------------------
|
|
324
|
+
// Step 4: API Key
|
|
325
|
+
// ---------------------------------------------------------------------------
|
|
326
|
+
|
|
327
|
+
async function setupApiKey() {
|
|
328
|
+
const apiKey = handleCancel(
|
|
329
|
+
await p.password({
|
|
330
|
+
message: "Kinetic API key (optional) (press Enter to skip):",
|
|
331
|
+
mask: "*",
|
|
332
|
+
}),
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
return apiKey?.trim() || null;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ---------------------------------------------------------------------------
|
|
339
|
+
// Step 5: Advanced Settings
|
|
340
|
+
// ---------------------------------------------------------------------------
|
|
341
|
+
|
|
342
|
+
const DEFAULTS = {
|
|
343
|
+
rpcUrl: "https://api.mainnet-beta.solana.com",
|
|
344
|
+
maxTradeSol: "10",
|
|
345
|
+
priorityFeeLamports: "400000",
|
|
346
|
+
jitoEnabled: false,
|
|
347
|
+
dynamicSlippageMaxBps: "300",
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
async function advancedSettings() {
|
|
351
|
+
const configure = handleCancel(
|
|
352
|
+
await p.confirm({
|
|
353
|
+
message: "Configure advanced settings?",
|
|
354
|
+
initialValue: false,
|
|
355
|
+
}),
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
if (!configure) return {};
|
|
359
|
+
|
|
360
|
+
p.log.info(pc.dim("Press Enter to keep defaults shown."));
|
|
361
|
+
|
|
362
|
+
const rpcUrl = handleCancel(
|
|
363
|
+
await p.text({
|
|
364
|
+
message: "Solana RPC URL:",
|
|
365
|
+
placeholder: DEFAULTS.rpcUrl,
|
|
366
|
+
defaultValue: DEFAULTS.rpcUrl,
|
|
367
|
+
validate(value) {
|
|
368
|
+
if (!value) return;
|
|
369
|
+
try {
|
|
370
|
+
const url = new URL(value);
|
|
371
|
+
if (url.protocol !== "https:") return "URL must use https://";
|
|
372
|
+
} catch {
|
|
373
|
+
return "Invalid URL format.";
|
|
374
|
+
}
|
|
375
|
+
},
|
|
376
|
+
}),
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
const maxTradeSol = handleCancel(
|
|
380
|
+
await p.text({
|
|
381
|
+
message: "Max trade size (SOL):",
|
|
382
|
+
placeholder: DEFAULTS.maxTradeSol,
|
|
383
|
+
defaultValue: DEFAULTS.maxTradeSol,
|
|
384
|
+
validate(value) {
|
|
385
|
+
if (!value) return;
|
|
386
|
+
const num = Number(value);
|
|
387
|
+
if (isNaN(num) || num <= 0) return "Must be a positive number.";
|
|
388
|
+
},
|
|
389
|
+
}),
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
const priorityFeeLamports = handleCancel(
|
|
393
|
+
await p.text({
|
|
394
|
+
message: `Priority fee (lamports): ${pc.dim("400000 = ~0.0004 SOL")}`,
|
|
395
|
+
placeholder: DEFAULTS.priorityFeeLamports,
|
|
396
|
+
defaultValue: DEFAULTS.priorityFeeLamports,
|
|
397
|
+
validate(value) {
|
|
398
|
+
if (!value) return;
|
|
399
|
+
const num = Number(value);
|
|
400
|
+
if (isNaN(num) || !Number.isInteger(num) || num < 0)
|
|
401
|
+
return "Must be a non-negative integer.";
|
|
402
|
+
},
|
|
403
|
+
}),
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
const jitoEnabled = handleCancel(
|
|
407
|
+
await p.confirm({
|
|
408
|
+
message: "Enable Jito MEV protection?",
|
|
409
|
+
initialValue: DEFAULTS.jitoEnabled,
|
|
410
|
+
}),
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
const dynamicSlippageMaxBps = handleCancel(
|
|
414
|
+
await p.text({
|
|
415
|
+
message: "Max dynamic slippage (BPS):",
|
|
416
|
+
placeholder: DEFAULTS.dynamicSlippageMaxBps,
|
|
417
|
+
defaultValue: DEFAULTS.dynamicSlippageMaxBps,
|
|
418
|
+
validate(value) {
|
|
419
|
+
if (!value) return;
|
|
420
|
+
const num = Number(value);
|
|
421
|
+
if (isNaN(num) || !Number.isInteger(num) || num <= 0) return "Must be a positive integer.";
|
|
422
|
+
if (num > 1000) return "Cannot exceed 1000 BPS (10%).";
|
|
423
|
+
},
|
|
424
|
+
}),
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
// Return only non-default values
|
|
428
|
+
const settings = {};
|
|
429
|
+
if (rpcUrl !== DEFAULTS.rpcUrl) settings.SOLANA_RPC_URL = rpcUrl;
|
|
430
|
+
if (maxTradeSol !== DEFAULTS.maxTradeSol) settings.MAX_TRADE_SOL = maxTradeSol;
|
|
431
|
+
if (priorityFeeLamports !== DEFAULTS.priorityFeeLamports)
|
|
432
|
+
settings.PRIORITY_FEE_LAMPORTS = priorityFeeLamports;
|
|
433
|
+
if (jitoEnabled !== DEFAULTS.jitoEnabled) settings.JITO_ENABLED = jitoEnabled ? "true" : "false";
|
|
434
|
+
if (dynamicSlippageMaxBps !== DEFAULTS.dynamicSlippageMaxBps)
|
|
435
|
+
settings.DYNAMIC_SLIPPAGE_MAX_BPS = dynamicSlippageMaxBps;
|
|
436
|
+
|
|
437
|
+
return settings;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ---------------------------------------------------------------------------
|
|
441
|
+
// Step 6: Claude Desktop Config
|
|
442
|
+
// ---------------------------------------------------------------------------
|
|
443
|
+
|
|
444
|
+
function generateConfigEntry(keypairPath, apiKey, advancedEnv) {
|
|
445
|
+
const entry = {
|
|
446
|
+
env: {
|
|
447
|
+
SOLANA_KEYPAIR_PATH: keypairPath.replace(/\\/g, "/"),
|
|
448
|
+
},
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
if (isNpmInstall) {
|
|
452
|
+
// npm/npx install — use portable npx command
|
|
453
|
+
entry.command = "npx";
|
|
454
|
+
entry.args = ["-y", "kinetic-mcp"];
|
|
455
|
+
} else {
|
|
456
|
+
// Local source checkout — use direct node path
|
|
457
|
+
entry.command = execPath;
|
|
458
|
+
entry.args = [join(packageRoot, "build", "index.js").replace(/\\/g, "/")];
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (apiKey) {
|
|
462
|
+
entry.env.API_KEY = apiKey;
|
|
463
|
+
}
|
|
464
|
+
// Add non-default advanced settings
|
|
465
|
+
if (advancedEnv) {
|
|
466
|
+
Object.assign(entry.env, advancedEnv);
|
|
467
|
+
}
|
|
468
|
+
return entry;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function formatConfigSnippet(entry) {
|
|
472
|
+
const snippet = {
|
|
473
|
+
mcpServers: {
|
|
474
|
+
"solana-trading": entry,
|
|
475
|
+
},
|
|
476
|
+
};
|
|
477
|
+
return JSON.stringify(snippet, null, 2);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
async function configureClaudeDesktop(keypairPath, apiKey, advancedEnv) {
|
|
481
|
+
const entry = generateConfigEntry(keypairPath, apiKey, advancedEnv);
|
|
482
|
+
|
|
483
|
+
if (isWSL()) {
|
|
484
|
+
p.note(
|
|
485
|
+
[
|
|
486
|
+
"WSL detected. Claude Desktop runs on the Windows host.",
|
|
487
|
+
"",
|
|
488
|
+
`Config file: ${pc.dim("%APPDATA%\\Claude\\claude_desktop_config.json")}`,
|
|
489
|
+
"",
|
|
490
|
+
"Add this to your config:",
|
|
491
|
+
"",
|
|
492
|
+
formatConfigSnippet(entry),
|
|
493
|
+
"",
|
|
494
|
+
pc.dim('Merge "solana-trading" into your existing "mcpServers" object.'),
|
|
495
|
+
].join("\n"),
|
|
496
|
+
"Claude Desktop Config",
|
|
497
|
+
);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const configPath = getClaudeConfigPath();
|
|
502
|
+
if (!configPath) {
|
|
503
|
+
p.note(
|
|
504
|
+
[
|
|
505
|
+
"Could not detect Claude Desktop config path.",
|
|
506
|
+
"",
|
|
507
|
+
"Add this to your claude_desktop_config.json:",
|
|
508
|
+
"",
|
|
509
|
+
formatConfigSnippet(entry),
|
|
510
|
+
].join("\n"),
|
|
511
|
+
"Claude Desktop Config",
|
|
512
|
+
);
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
p.log.info(`Config file: ${pc.dim(configPath)}`);
|
|
517
|
+
|
|
518
|
+
const autoModify = handleCancel(
|
|
519
|
+
await p.confirm({
|
|
520
|
+
message: "Automatically add this MCP server to Claude Desktop config?",
|
|
521
|
+
initialValue: true,
|
|
522
|
+
}),
|
|
523
|
+
);
|
|
524
|
+
|
|
525
|
+
if (!autoModify) {
|
|
526
|
+
p.note(
|
|
527
|
+
[
|
|
528
|
+
"Add this to your claude_desktop_config.json:",
|
|
529
|
+
"",
|
|
530
|
+
formatConfigSnippet(entry),
|
|
531
|
+
"",
|
|
532
|
+
pc.dim('Merge "solana-trading" into your existing "mcpServers" object.'),
|
|
533
|
+
].join("\n"),
|
|
534
|
+
"Manual Config",
|
|
535
|
+
);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const s = p.spinner();
|
|
540
|
+
s.start("Writing Claude Desktop config...");
|
|
541
|
+
|
|
542
|
+
// Read existing config
|
|
543
|
+
let config = {};
|
|
544
|
+
if (existsSync(configPath)) {
|
|
545
|
+
try {
|
|
546
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
547
|
+
config = JSON.parse(raw);
|
|
548
|
+
} catch {
|
|
549
|
+
s.stop("Existing config has invalid JSON");
|
|
550
|
+
p.note(
|
|
551
|
+
[
|
|
552
|
+
"Could not parse existing config file.",
|
|
553
|
+
"",
|
|
554
|
+
"Add this manually to your claude_desktop_config.json:",
|
|
555
|
+
"",
|
|
556
|
+
formatConfigSnippet(entry),
|
|
557
|
+
].join("\n"),
|
|
558
|
+
"Manual Config",
|
|
559
|
+
);
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Backup
|
|
564
|
+
const backupPath = configPath + ".bak";
|
|
565
|
+
copyFileSync(configPath, backupPath);
|
|
566
|
+
p.log.info(`Backup created: ${pc.dim(backupPath)}`);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Check for existing entry
|
|
570
|
+
if (config.mcpServers?.["solana-trading"]) {
|
|
571
|
+
s.stop("Existing entry found");
|
|
572
|
+
const overwrite = handleCancel(
|
|
573
|
+
await p.confirm({
|
|
574
|
+
message: "A 'solana-trading' entry already exists. Overwrite?",
|
|
575
|
+
initialValue: false,
|
|
576
|
+
}),
|
|
577
|
+
);
|
|
578
|
+
|
|
579
|
+
if (!overwrite) {
|
|
580
|
+
p.log.info("Existing config preserved.");
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
s.start("Writing Claude Desktop config...");
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Merge
|
|
588
|
+
if (!config.mcpServers) {
|
|
589
|
+
config.mcpServers = {};
|
|
590
|
+
}
|
|
591
|
+
config.mcpServers["solana-trading"] = entry;
|
|
592
|
+
|
|
593
|
+
// Write
|
|
594
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
595
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
596
|
+
|
|
597
|
+
s.stop("Claude Desktop config updated");
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// ---------------------------------------------------------------------------
|
|
601
|
+
// Step 7: Verification
|
|
602
|
+
// ---------------------------------------------------------------------------
|
|
603
|
+
|
|
604
|
+
const MAX_VERIFICATION_RETRIES = 3;
|
|
605
|
+
|
|
606
|
+
async function runVerification(keypairPath, rpcUrl, apiKey) {
|
|
607
|
+
for (let attempt = 0; attempt <= MAX_VERIFICATION_RETRIES; attempt++) {
|
|
608
|
+
const s = p.spinner();
|
|
609
|
+
let hasFailure = false;
|
|
610
|
+
|
|
611
|
+
// 1. Load keypair
|
|
612
|
+
s.start("Loading keypair...");
|
|
613
|
+
let keypair;
|
|
614
|
+
try {
|
|
615
|
+
keypair = validateKeypairFile(keypairPath);
|
|
616
|
+
s.stop(`Keypair loaded — ${pc.cyan(keypair.publicKey.toBase58())}`);
|
|
617
|
+
} catch (err) {
|
|
618
|
+
s.stop(`Keypair load failed: ${err.message}`);
|
|
619
|
+
hasFailure = true;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// 2. Check SOL balance (only if keypair loaded)
|
|
623
|
+
if (keypair) {
|
|
624
|
+
const effectiveRpc = rpcUrl || DEFAULTS.rpcUrl;
|
|
625
|
+
s.start("Checking SOL balance...");
|
|
626
|
+
try {
|
|
627
|
+
const response = await fetch(effectiveRpc, {
|
|
628
|
+
method: "POST",
|
|
629
|
+
headers: { "Content-Type": "application/json" },
|
|
630
|
+
body: JSON.stringify({
|
|
631
|
+
jsonrpc: "2.0",
|
|
632
|
+
id: 1,
|
|
633
|
+
method: "getBalance",
|
|
634
|
+
params: [keypair.publicKey.toBase58()],
|
|
635
|
+
}),
|
|
636
|
+
signal: AbortSignal.timeout(10_000),
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
const data = await response.json();
|
|
640
|
+
const lamports = data.result?.value ?? 0;
|
|
641
|
+
const sol = (lamports / 1e9).toFixed(4);
|
|
642
|
+
|
|
643
|
+
if (lamports === 0) {
|
|
644
|
+
s.stop(`Balance: ${pc.yellow("0 SOL")}`);
|
|
645
|
+
p.log.warn("Fund this wallet with SOL before trading.");
|
|
646
|
+
} else {
|
|
647
|
+
s.stop(`Balance: ${pc.green(sol + " SOL")}`);
|
|
648
|
+
}
|
|
649
|
+
} catch (err) {
|
|
650
|
+
const msg =
|
|
651
|
+
err.name === "TimeoutError" || err.name === "AbortError"
|
|
652
|
+
? "Request timed out"
|
|
653
|
+
: err.message;
|
|
654
|
+
s.stop(`Balance check failed: ${pc.yellow(msg)}`);
|
|
655
|
+
hasFailure = true;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// 3. Test Kinetic API (only if API key provided)
|
|
660
|
+
if (apiKey) {
|
|
661
|
+
s.start("Testing Kinetic API...");
|
|
662
|
+
try {
|
|
663
|
+
const response = await fetch("https://auth.kinetic.xyz/v1/token", {
|
|
664
|
+
method: "POST",
|
|
665
|
+
headers: { "Content-Type": "application/json" },
|
|
666
|
+
body: JSON.stringify({ apiKey }),
|
|
667
|
+
signal: AbortSignal.timeout(10_000),
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
if (response.ok) {
|
|
671
|
+
s.stop(`Kinetic API: ${pc.green("connected")}`);
|
|
672
|
+
} else {
|
|
673
|
+
s.stop(`Kinetic API: ${pc.yellow(`${response.status} ${response.statusText}`)}`);
|
|
674
|
+
hasFailure = true;
|
|
675
|
+
}
|
|
676
|
+
} catch (err) {
|
|
677
|
+
const msg =
|
|
678
|
+
err.name === "TimeoutError" || err.name === "AbortError"
|
|
679
|
+
? "Request timed out"
|
|
680
|
+
: err.message;
|
|
681
|
+
s.stop(`Kinetic API: ${pc.yellow(msg)}`);
|
|
682
|
+
hasFailure = true;
|
|
683
|
+
}
|
|
684
|
+
} else {
|
|
685
|
+
p.log.info(pc.dim("Skipping Kinetic API test (no API key)."));
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
if (!hasFailure) return;
|
|
689
|
+
|
|
690
|
+
// Retry on failure (up to MAX_VERIFICATION_RETRIES)
|
|
691
|
+
if (attempt < MAX_VERIFICATION_RETRIES) {
|
|
692
|
+
const retry = handleCancel(
|
|
693
|
+
await p.confirm({
|
|
694
|
+
message: "Some checks failed. Retry verification?",
|
|
695
|
+
initialValue: true,
|
|
696
|
+
}),
|
|
697
|
+
);
|
|
698
|
+
|
|
699
|
+
if (!retry) {
|
|
700
|
+
p.log.warn("Continuing with warnings. You can verify connectivity later.");
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
} else {
|
|
704
|
+
p.log.warn("Maximum retries reached. Continuing with warnings.");
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// ---------------------------------------------------------------------------
|
|
711
|
+
// Main
|
|
712
|
+
// ---------------------------------------------------------------------------
|
|
713
|
+
|
|
714
|
+
async function main() {
|
|
715
|
+
await renderBanner();
|
|
716
|
+
p.intro(pc.bold("Setup Wizard"));
|
|
717
|
+
|
|
718
|
+
// Step 2: Build check
|
|
719
|
+
await checkBuild();
|
|
720
|
+
|
|
721
|
+
// Step 3: Keypair setup
|
|
722
|
+
const keypairResult = await setupKeypair();
|
|
723
|
+
|
|
724
|
+
// Step 4: API key
|
|
725
|
+
const apiKey = await setupApiKey();
|
|
726
|
+
|
|
727
|
+
// Step 5: Advanced settings
|
|
728
|
+
const advancedEnv = await advancedSettings();
|
|
729
|
+
|
|
730
|
+
// Step 6: Claude Desktop config
|
|
731
|
+
await configureClaudeDesktop(keypairResult.path, apiKey, advancedEnv);
|
|
732
|
+
|
|
733
|
+
// Step 7: Verification
|
|
734
|
+
const rpcUrl = advancedEnv.SOLANA_RPC_URL || null;
|
|
735
|
+
await runVerification(keypairResult.path, rpcUrl, apiKey);
|
|
736
|
+
|
|
737
|
+
// Step 8: Outro
|
|
738
|
+
const summaryLines = [
|
|
739
|
+
`Wallet: ${pc.cyan(keypairResult.publicKey)}`,
|
|
740
|
+
`Keypair: ${pc.dim(keypairResult.path)}`,
|
|
741
|
+
];
|
|
742
|
+
if (apiKey) summaryLines.push(`API Key: ${pc.green("configured")}`);
|
|
743
|
+
|
|
744
|
+
const advancedKeys = Object.keys(advancedEnv);
|
|
745
|
+
if (advancedKeys.length > 0) {
|
|
746
|
+
summaryLines.push("");
|
|
747
|
+
summaryLines.push(pc.bold("Custom settings:"));
|
|
748
|
+
for (const [key, val] of Object.entries(advancedEnv)) {
|
|
749
|
+
summaryLines.push(` ${pc.dim(key)}: ${val}`);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
p.note(summaryLines.join("\n"), "Setup Complete");
|
|
754
|
+
|
|
755
|
+
p.note(
|
|
756
|
+
[
|
|
757
|
+
`1. ${pc.bold("Restart Claude Desktop")} (fully quit and reopen)`,
|
|
758
|
+
`2. Look for the ${pc.bold("hammer icon")} in the chat input`,
|
|
759
|
+
`3. Try: ${pc.cyan('"What\'s my SOL balance?"')}`,
|
|
760
|
+
"",
|
|
761
|
+
pc.dim("Safety guardrails:"),
|
|
762
|
+
pc.dim(` • Swaps always show a preview first`),
|
|
763
|
+
pc.dim(` • Max trade size: ${advancedEnv.MAX_TRADE_SOL || "10"} SOL`),
|
|
764
|
+
pc.dim(` • Slippage capped at ${advancedEnv.DYNAMIC_SLIPPAGE_MAX_BPS || "300"} BPS`),
|
|
765
|
+
].join("\n"),
|
|
766
|
+
"Next Steps",
|
|
767
|
+
);
|
|
768
|
+
|
|
769
|
+
p.outro(pc.green("You're all set! Happy trading."));
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
main().catch((err) => {
|
|
773
|
+
if (err.message === "Cancelled by user") {
|
|
774
|
+
p.cancel("Setup cancelled.");
|
|
775
|
+
} else {
|
|
776
|
+
p.log.error(`Setup failed: ${err.message}`);
|
|
777
|
+
}
|
|
778
|
+
process.exit(1);
|
|
779
|
+
});
|