pennyrouter 0.2.4 → 0.2.8
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 +225 -185
- package/bin/penny.js +9 -9
- package/bin/pennyrouter.js +7 -7
- package/package.json +30 -30
- package/src/cli.js +1727 -1584
- package/src/harnesses/aider.js +48 -48
- package/src/harnesses/claude-code.js +318 -295
- package/src/harnesses/claude-code.test.js +57 -57
- package/src/harnesses/cline.js +77 -77
- package/src/harnesses/codegpt.js +76 -76
- package/src/harnesses/codex.js +180 -180
- package/src/harnesses/codex.test.js +119 -119
- package/src/harnesses/opencode.js +151 -151
- package/src/harnesses/shared.js +51 -51
- package/src/harnesses/windsurf.js +57 -57
- package/src/harnesses/zed.js +61 -61
- package/src/launch.js +467 -389
- package/src/launch.test.js +153 -153
- package/src/session.js +157 -157
- package/src/session.test.js +44 -44
- package/src/state.js +103 -103
- package/src/statusline.js +202 -202
- package/src/statusline.test.js +72 -72
package/src/cli.js
CHANGED
|
@@ -1,1584 +1,1727 @@
|
|
|
1
|
-
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
-
import { emitKeypressEvents } from "node:readline";
|
|
3
|
-
import { createInterface } from "node:readline/promises";
|
|
4
|
-
import { stdin as input, stdout as output } from "node:process";
|
|
5
|
-
import { aiderHarness } from "./harnesses/aider.js";
|
|
6
|
-
import {
|
|
7
|
-
CLAUDE_CODE_MODELS,
|
|
8
|
-
claudeCodeHarness,
|
|
9
|
-
readClaudeCodeGatewayAuth,
|
|
10
|
-
readClaudeCodePennyRouterKey,
|
|
11
|
-
} from "./harnesses/claude-code.js";
|
|
12
|
-
import { clineHarness } from "./harnesses/cline.js";
|
|
13
|
-
import { codeGptHarness } from "./harnesses/codegpt.js";
|
|
14
|
-
import { codexHarness, detectCodexChatgptAuth } from "./harnesses/codex.js";
|
|
15
|
-
import { opencodeHarness } from "./harnesses/opencode.js";
|
|
16
|
-
import { windsurfHarness } from "./harnesses/windsurf.js";
|
|
17
|
-
import { zedHarness } from "./harnesses/zed.js";
|
|
18
|
-
import {
|
|
19
|
-
APP_URL,
|
|
20
|
-
createCliSession,
|
|
21
|
-
forgetAnthropicToken,
|
|
22
|
-
maskKey,
|
|
23
|
-
pollForKey,
|
|
24
|
-
probeClaudeCodeGateway,
|
|
25
|
-
sleep,
|
|
26
|
-
storeAnthropicToken,
|
|
27
|
-
storeUpstreamCredential,
|
|
28
|
-
} from "./session.js";
|
|
29
|
-
import {
|
|
30
|
-
backupFile,
|
|
31
|
-
compactHome,
|
|
32
|
-
loadManifest,
|
|
33
|
-
restoreFile,
|
|
34
|
-
saveManifest,
|
|
35
|
-
statePath,
|
|
36
|
-
} from "./state.js";
|
|
37
|
-
import {
|
|
38
|
-
claudeLaunchSettingsPath,
|
|
39
|
-
codexLaunchProfilePath,
|
|
40
|
-
configurePennyHarness,
|
|
41
|
-
hasConfiguredPennyHarnesses,
|
|
42
|
-
installPennyRuntime,
|
|
43
|
-
loadPennyConfig,
|
|
44
|
-
pennyConfigPath,
|
|
45
|
-
removePennyHarness,
|
|
46
|
-
removePennyRuntime,
|
|
47
|
-
savePennyConfig,
|
|
48
|
-
setPennyHarnessEnabled,
|
|
49
|
-
} from "./launch.js";
|
|
50
|
-
|
|
51
|
-
const HARNESS_BY_ID = {
|
|
52
|
-
"claude-code": claudeCodeHarness,
|
|
53
|
-
cline: clineHarness,
|
|
54
|
-
opencode: opencodeHarness,
|
|
55
|
-
codex: codexHarness,
|
|
56
|
-
windsurf: windsurfHarness,
|
|
57
|
-
zed: zedHarness,
|
|
58
|
-
codegpt: codeGptHarness,
|
|
59
|
-
aider: aiderHarness,
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
const SUPPORTED_HARNESS_IDS = Object.keys(HARNESS_BY_ID).join(",");
|
|
63
|
-
const DEFAULT_INSTALL_HARNESS_IDS = ["claude-code", "codex"];
|
|
64
|
-
const CLAUDE_PROVIDER_OPTIONS = [
|
|
65
|
-
{ id: "subscription", name: "Claude subscription" },
|
|
66
|
-
{ id: "anthropic-api", name: "Anthropic API key" },
|
|
67
|
-
{ id: "openrouter", name: "OpenRouter API key" },
|
|
68
|
-
];
|
|
69
|
-
const CODEX_PROVIDER_OPTIONS = [
|
|
70
|
-
{ id: "subscription", name: "ChatGPT subscription" },
|
|
71
|
-
{ id: "openai-api", name: "OpenAI API key" },
|
|
72
|
-
{ id: "openrouter", name: "OpenRouter API key" },
|
|
73
|
-
];
|
|
74
|
-
|
|
75
|
-
export async function main(argv = process.argv.slice(2)) {
|
|
76
|
-
const { command, flags } = parseArgs(argv);
|
|
77
|
-
|
|
78
|
-
if (flags.help || command === "help") {
|
|
79
|
-
printHelp();
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (command === "install") {
|
|
84
|
-
await install(flags);
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (command === "
|
|
89
|
-
await
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (command === "
|
|
94
|
-
await
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (command === "
|
|
99
|
-
await
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (command === "
|
|
104
|
-
await
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if (command === "
|
|
109
|
-
await
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
else if (arg === "--
|
|
131
|
-
else if (arg === "--
|
|
132
|
-
else if (arg === "--
|
|
133
|
-
else if (arg === "--
|
|
134
|
-
else if (arg === "--
|
|
135
|
-
else if (arg === "--
|
|
136
|
-
else if (arg === "--
|
|
137
|
-
else if (arg === "--
|
|
138
|
-
else if (arg
|
|
139
|
-
else if (arg === "--
|
|
140
|
-
else if (arg
|
|
141
|
-
else if (arg === "--
|
|
142
|
-
else if (arg
|
|
143
|
-
else if (arg
|
|
144
|
-
else if (arg
|
|
145
|
-
else if (arg
|
|
146
|
-
else if (arg
|
|
147
|
-
else if (arg
|
|
148
|
-
else if (arg
|
|
149
|
-
else if (arg
|
|
150
|
-
else if (arg
|
|
151
|
-
else if (arg
|
|
152
|
-
else if (arg
|
|
153
|
-
else if (arg
|
|
154
|
-
else if (arg
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
else if (arg
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
else
|
|
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
|
-
const
|
|
190
|
-
if (
|
|
191
|
-
console.log(
|
|
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
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
await
|
|
249
|
-
|
|
250
|
-
await
|
|
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
|
-
console.log(`
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
return { token:
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
//
|
|
319
|
-
|
|
320
|
-
//
|
|
321
|
-
//
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
//
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
//
|
|
328
|
-
//
|
|
329
|
-
//
|
|
330
|
-
//
|
|
331
|
-
//
|
|
332
|
-
//
|
|
333
|
-
//
|
|
334
|
-
// token
|
|
335
|
-
//
|
|
336
|
-
//
|
|
337
|
-
//
|
|
338
|
-
//
|
|
339
|
-
//
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
//
|
|
347
|
-
//
|
|
348
|
-
//
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
child.on("
|
|
362
|
-
child.on("
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
if (
|
|
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
|
-
console.log("");
|
|
425
|
-
console.log(
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
console.log(
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
const
|
|
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
|
-
const
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
record.
|
|
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
|
-
const
|
|
555
|
-
const
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
console.log("");
|
|
559
|
-
console.log(
|
|
560
|
-
console.log(
|
|
561
|
-
|
|
562
|
-
console.log(`
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
console.log("
|
|
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
|
-
|
|
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
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
if (
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
if (
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
if (
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
console.log(
|
|
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
|
-
|
|
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
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
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
|
-
if (
|
|
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
|
-
console.log(`Skipping ${
|
|
1068
|
-
continue;
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
const
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
};
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
}
|
|
1216
|
-
}
|
|
1217
|
-
|
|
1218
|
-
async function
|
|
1219
|
-
const
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
console.log(
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
}
|
|
1256
|
-
}
|
|
1257
|
-
|
|
1258
|
-
async function
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
const
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
}
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
}
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
}
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
}
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
}
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
}
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
}
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
return
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
}
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
function
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import { emitKeypressEvents } from "node:readline";
|
|
3
|
+
import { createInterface } from "node:readline/promises";
|
|
4
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
5
|
+
import { aiderHarness } from "./harnesses/aider.js";
|
|
6
|
+
import {
|
|
7
|
+
CLAUDE_CODE_MODELS,
|
|
8
|
+
claudeCodeHarness,
|
|
9
|
+
readClaudeCodeGatewayAuth,
|
|
10
|
+
readClaudeCodePennyRouterKey,
|
|
11
|
+
} from "./harnesses/claude-code.js";
|
|
12
|
+
import { clineHarness } from "./harnesses/cline.js";
|
|
13
|
+
import { codeGptHarness } from "./harnesses/codegpt.js";
|
|
14
|
+
import { codexHarness, detectCodexChatgptAuth } from "./harnesses/codex.js";
|
|
15
|
+
import { opencodeHarness } from "./harnesses/opencode.js";
|
|
16
|
+
import { windsurfHarness } from "./harnesses/windsurf.js";
|
|
17
|
+
import { zedHarness } from "./harnesses/zed.js";
|
|
18
|
+
import {
|
|
19
|
+
APP_URL,
|
|
20
|
+
createCliSession,
|
|
21
|
+
forgetAnthropicToken,
|
|
22
|
+
maskKey,
|
|
23
|
+
pollForKey,
|
|
24
|
+
probeClaudeCodeGateway,
|
|
25
|
+
sleep,
|
|
26
|
+
storeAnthropicToken,
|
|
27
|
+
storeUpstreamCredential,
|
|
28
|
+
} from "./session.js";
|
|
29
|
+
import {
|
|
30
|
+
backupFile,
|
|
31
|
+
compactHome,
|
|
32
|
+
loadManifest,
|
|
33
|
+
restoreFile,
|
|
34
|
+
saveManifest,
|
|
35
|
+
statePath,
|
|
36
|
+
} from "./state.js";
|
|
37
|
+
import {
|
|
38
|
+
claudeLaunchSettingsPath,
|
|
39
|
+
codexLaunchProfilePath,
|
|
40
|
+
configurePennyHarness,
|
|
41
|
+
hasConfiguredPennyHarnesses,
|
|
42
|
+
installPennyRuntime,
|
|
43
|
+
loadPennyConfig,
|
|
44
|
+
pennyConfigPath,
|
|
45
|
+
removePennyHarness,
|
|
46
|
+
removePennyRuntime,
|
|
47
|
+
savePennyConfig,
|
|
48
|
+
setPennyHarnessEnabled,
|
|
49
|
+
} from "./launch.js";
|
|
50
|
+
|
|
51
|
+
const HARNESS_BY_ID = {
|
|
52
|
+
"claude-code": claudeCodeHarness,
|
|
53
|
+
cline: clineHarness,
|
|
54
|
+
opencode: opencodeHarness,
|
|
55
|
+
codex: codexHarness,
|
|
56
|
+
windsurf: windsurfHarness,
|
|
57
|
+
zed: zedHarness,
|
|
58
|
+
codegpt: codeGptHarness,
|
|
59
|
+
aider: aiderHarness,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const SUPPORTED_HARNESS_IDS = Object.keys(HARNESS_BY_ID).join(",");
|
|
63
|
+
const DEFAULT_INSTALL_HARNESS_IDS = ["claude-code", "codex"];
|
|
64
|
+
const CLAUDE_PROVIDER_OPTIONS = [
|
|
65
|
+
{ id: "subscription", name: "Claude subscription" },
|
|
66
|
+
{ id: "anthropic-api", name: "Anthropic API key" },
|
|
67
|
+
{ id: "openrouter", name: "OpenRouter API key" },
|
|
68
|
+
];
|
|
69
|
+
const CODEX_PROVIDER_OPTIONS = [
|
|
70
|
+
{ id: "subscription", name: "ChatGPT subscription" },
|
|
71
|
+
{ id: "openai-api", name: "OpenAI API key" },
|
|
72
|
+
{ id: "openrouter", name: "OpenRouter API key" },
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
76
|
+
const { command, flags } = parseArgs(argv);
|
|
77
|
+
|
|
78
|
+
if (flags.help || command === "help") {
|
|
79
|
+
printHelp();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (command === "install") {
|
|
84
|
+
await install(flags);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (command === "update") {
|
|
89
|
+
await update(flags);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (command === "auth") {
|
|
94
|
+
await authProvider(flags);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (command === "uninstall") {
|
|
99
|
+
await uninstall(flags);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (command === "disable") {
|
|
104
|
+
await disable(flags);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (command === "enable") {
|
|
109
|
+
await enable(flags);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (command === "status") {
|
|
114
|
+
await status();
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
throw new Error(`Unknown command "${command}". Run "pennyrouter help".`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function parseArgs(argv) {
|
|
122
|
+
const command = argv[0] && !argv[0].startsWith("-") ? argv[0] : "install";
|
|
123
|
+
const rest = command === argv[0] ? argv.slice(1) : argv;
|
|
124
|
+
const flags = {};
|
|
125
|
+
flags.args = [];
|
|
126
|
+
|
|
127
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
128
|
+
const arg = rest[i];
|
|
129
|
+
if (arg === "--help" || arg === "-h") flags.help = true;
|
|
130
|
+
else if (arg === "--yes" || arg === "-y") flags.yes = true;
|
|
131
|
+
else if (arg === "--all") flags.all = true;
|
|
132
|
+
else if (arg === "--dry-run") flags.dryRun = true;
|
|
133
|
+
else if (arg === "--allow-running-claude") flags.allowRunningClaude = true;
|
|
134
|
+
else if (arg === "--allow-running-harnesses") flags.allowRunningHarnesses = true;
|
|
135
|
+
else if (arg === "--make-default") flags.makeDefault = true;
|
|
136
|
+
else if (arg === "--penny-only") flags.pennyOnly = true;
|
|
137
|
+
else if (arg === "--no-path-update") flags.noPathUpdate = true;
|
|
138
|
+
else if (arg === "--no-browser") flags.noBrowser = true;
|
|
139
|
+
else if (arg === "--existing-account") flags.existingAccount = true;
|
|
140
|
+
else if (arg === "--anthropic-auth") flags.anthropicAuth = true;
|
|
141
|
+
else if (arg === "--no-anthropic-auth") flags.noAnthropicAuth = true;
|
|
142
|
+
else if (arg === "--penny-key") flags.pennyKey = rest[++i] || "";
|
|
143
|
+
else if (arg.startsWith("--penny-key=")) flags.pennyKey = arg.slice("--penny-key=".length);
|
|
144
|
+
else if (arg === "--token") flags.token = rest[++i] || "";
|
|
145
|
+
else if (arg.startsWith("--token=")) flags.token = arg.slice("--token=".length);
|
|
146
|
+
else if (arg === "--token-command") flags.tokenCommand = rest[++i] || "";
|
|
147
|
+
else if (arg.startsWith("--token-command=")) flags.tokenCommand = arg.slice("--token-command=".length);
|
|
148
|
+
else if (arg === "--anthropic-api-key") flags.anthropicApiKey = rest[++i] || "";
|
|
149
|
+
else if (arg.startsWith("--anthropic-api-key=")) flags.anthropicApiKey = arg.slice("--anthropic-api-key=".length);
|
|
150
|
+
else if (arg === "--openai-api-key") flags.openaiApiKey = rest[++i] || "";
|
|
151
|
+
else if (arg.startsWith("--openai-api-key=")) flags.openaiApiKey = arg.slice("--openai-api-key=".length);
|
|
152
|
+
else if (arg === "--openrouter-api-key") flags.openrouterApiKey = rest[++i] || "";
|
|
153
|
+
else if (arg.startsWith("--openrouter-api-key=")) flags.openrouterApiKey = arg.slice("--openrouter-api-key=".length);
|
|
154
|
+
else if (arg === "--harness") flags.harness = rest[++i] || "";
|
|
155
|
+
else if (arg.startsWith("--harness=")) flags.harness = arg.slice("--harness=".length);
|
|
156
|
+
else if (arg === "--app-url") flags.appUrl = rest[++i] || "";
|
|
157
|
+
else if (arg.startsWith("--app-url=")) flags.appUrl = arg.slice("--app-url=".length);
|
|
158
|
+
else if (arg === "--gateway-base-url") flags.gatewayBaseUrl = rest[++i] || "";
|
|
159
|
+
else if (arg.startsWith("--gateway-base-url=")) flags.gatewayBaseUrl = arg.slice("--gateway-base-url=".length);
|
|
160
|
+
// --local: point the installed harness at a locally-running gateway (default
|
|
161
|
+
// http://localhost:8400), for developing/testing against your own gateway. Auth still goes
|
|
162
|
+
// to prod (pennyrouter.com) so you get a real key; only the harness's serving URL is local.
|
|
163
|
+
// `--local=<url>` overrides the port/host.
|
|
164
|
+
else if (arg === "--local") flags.gatewayBaseUrl = "http://localhost:8400";
|
|
165
|
+
else if (arg.startsWith("--local=")) flags.gatewayBaseUrl = arg.slice("--local=".length);
|
|
166
|
+
else if (arg === "--forget") flags.forget = true;
|
|
167
|
+
else if (arg === "--forget-token") flags.forgetToken = true;
|
|
168
|
+
else if (!arg.startsWith("-")) flags.args.push(arg);
|
|
169
|
+
else throw new Error(`Unknown option "${arg}". Run "pennyrouter help".`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return { command, flags };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function authProvider(flags) {
|
|
176
|
+
const provider = (flags.args[0] || "anthropic").toLowerCase();
|
|
177
|
+
if (provider !== "anthropic" && provider !== "claude") {
|
|
178
|
+
throw new Error(`Unknown auth provider "${provider}". Try "pennyrouter auth anthropic".`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (flags.forget) {
|
|
182
|
+
const launchConfig = await loadPennyConfig();
|
|
183
|
+
const pennyKey = flags.pennyKey || launchConfig.api_key || await readClaudeCodePennyRouterKey();
|
|
184
|
+
if (!pennyKey) {
|
|
185
|
+
throw new Error(
|
|
186
|
+
"No PennyRouter key found in Claude Code settings. Pass `--penny-key pr-...`.",
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
const gatewayBaseUrl = flags.gatewayBaseUrl || process.env.PENNYROUTER_GATEWAY_BASE_URL || "https://api.pennyrouter.com";
|
|
190
|
+
if (flags.dryRun) {
|
|
191
|
+
console.log(`Will delete stored Anthropic token from gateway: ${gatewayBaseUrl}`);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const ok = await forgetAnthropicToken({ gatewayBaseUrl, apiKey: pennyKey });
|
|
195
|
+
if (ok) {
|
|
196
|
+
console.log("Successfully forgot stored Anthropic token on server.");
|
|
197
|
+
} else {
|
|
198
|
+
console.log("Failed to forget stored Anthropic token on server.");
|
|
199
|
+
}
|
|
200
|
+
const manifest = await loadManifest();
|
|
201
|
+
let updated = false;
|
|
202
|
+
for (const record of (manifest.installations || [])) {
|
|
203
|
+
if (record.harness === "claude-code" && record.anthropic_token_stored) {
|
|
204
|
+
record.anthropic_token_stored = false;
|
|
205
|
+
record.anthropic_oauth = false;
|
|
206
|
+
updated = true;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (updated) {
|
|
210
|
+
await saveManifest(manifest);
|
|
211
|
+
}
|
|
212
|
+
if (launchConfig.harnesses?.claude) {
|
|
213
|
+
launchConfig.harnesses.claude.subscription_auth = false;
|
|
214
|
+
await savePennyConfig(launchConfig);
|
|
215
|
+
}
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (flags.dryRun) {
|
|
220
|
+
console.log("Will configure Claude Code for PennyRouter + Anthropic OAuth dual auth.");
|
|
221
|
+
console.log("Dry run only. No files changed.");
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const launchConfig = await loadPennyConfig();
|
|
226
|
+
const pennyKey = flags.pennyKey || launchConfig.api_key || await readClaudeCodePennyRouterKey();
|
|
227
|
+
if (!pennyKey) {
|
|
228
|
+
throw new Error(
|
|
229
|
+
"No PennyRouter key found in Claude Code settings. Run `pennyrouter install --harness claude-code` first, or pass `--penny-key pr-...`.",
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const tokenResult = readAnthropicOAuthToken(flags);
|
|
234
|
+
const token = tokenResult.token;
|
|
235
|
+
if (!token) {
|
|
236
|
+
throw new Error(
|
|
237
|
+
"No Anthropic OAuth token found. Run `claude setup-token` and pass it with `--token`, set ANTHROPIC_AUTH_TOKEN, or pass `--token-command <cmd>`.",
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const gatewayBaseUrl = flags.gatewayBaseUrl || process.env.PENNYROUTER_GATEWAY_BASE_URL || "https://api.pennyrouter.com";
|
|
242
|
+
const manifest = await loadManifest();
|
|
243
|
+
const existingRecord = (manifest.installations || []).find(
|
|
244
|
+
(candidate) => candidate.harness === "claude-code",
|
|
245
|
+
);
|
|
246
|
+
const ownsGlobalRouting = existingRecord
|
|
247
|
+
? existingRecord.default_routing !== false
|
|
248
|
+
: Boolean(await readClaudeCodePennyRouterKey());
|
|
249
|
+
if (ownsGlobalRouting) assertNoRunningHarness("claude-code", flags);
|
|
250
|
+
await storeAnthropicToken({ gatewayBaseUrl, apiKey: pennyKey, token });
|
|
251
|
+
await probeClaudeCodeGateway({ gatewayBaseUrl, apiKey: pennyKey });
|
|
252
|
+
await installPennyRuntime({ updatePath: !flags.noPathUpdate });
|
|
253
|
+
await configurePennyHarness("claude-code", {
|
|
254
|
+
apiKey: pennyKey,
|
|
255
|
+
gatewayBaseUrl,
|
|
256
|
+
subscriptionAuth: true,
|
|
257
|
+
models: CLAUDE_CODE_MODELS,
|
|
258
|
+
});
|
|
259
|
+
let record = existingRecord || {
|
|
260
|
+
harness: "claude-code",
|
|
261
|
+
config_path: compactHome((await claudeCodeHarness.planInstall()).configPath),
|
|
262
|
+
installed_at: new Date().toISOString(),
|
|
263
|
+
launch_available: true,
|
|
264
|
+
launch_config_path: compactHome(claudeLaunchSettingsPath()),
|
|
265
|
+
default_routing: ownsGlobalRouting,
|
|
266
|
+
};
|
|
267
|
+
if (ownsGlobalRouting) {
|
|
268
|
+
const plan = await claudeCodeHarness.planInstall();
|
|
269
|
+
record = {
|
|
270
|
+
...record,
|
|
271
|
+
...(await claudeCodeHarness.installStoredToken({
|
|
272
|
+
apiKey: pennyKey, gatewayBaseUrl, plan, existingRecord,
|
|
273
|
+
})),
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
upsertManifestRecord(manifest, {
|
|
278
|
+
...record,
|
|
279
|
+
launch_available: true,
|
|
280
|
+
launch_config_path: compactHome(claudeLaunchSettingsPath()),
|
|
281
|
+
default_routing: ownsGlobalRouting,
|
|
282
|
+
anthropic_oauth: true,
|
|
283
|
+
anthropic_token_stored: true,
|
|
284
|
+
token_source: tokenResult.source,
|
|
285
|
+
});
|
|
286
|
+
await saveManifest(manifest);
|
|
287
|
+
|
|
288
|
+
console.log("Configured `penny claude` for PennyRouter + your Claude subscription.");
|
|
289
|
+
console.log("Your Claude token is stored encrypted; Claude Code holds only your PennyRouter key.");
|
|
290
|
+
console.log("If Claude auth expires, refresh the token source and run `pennyrouter auth anthropic` again.");
|
|
291
|
+
console.log(`Penny config: ${pennyConfigPath()}`);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function readAnthropicOAuthToken(flags = {}) {
|
|
295
|
+
if (flags.token) return { token: normalizeAnthropicToken(flags.token), source: "flag" };
|
|
296
|
+
if (flags.tokenCommand) {
|
|
297
|
+
const result = spawnSync(flags.tokenCommand, {
|
|
298
|
+
encoding: "utf8",
|
|
299
|
+
shell: true,
|
|
300
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
301
|
+
});
|
|
302
|
+
if (result.status === 0) {
|
|
303
|
+
return { token: normalizeAnthropicToken(result.stdout), source: "token-command" };
|
|
304
|
+
}
|
|
305
|
+
return { token: "", source: "token-command" };
|
|
306
|
+
}
|
|
307
|
+
if (process.env.ANTHROPIC_AUTH_TOKEN) {
|
|
308
|
+
return { token: normalizeAnthropicToken(process.env.ANTHROPIC_AUTH_TOKEN), source: "env" };
|
|
309
|
+
}
|
|
310
|
+
const result = spawnSync("ant", ["auth", "print-credentials", "--access-token"], {
|
|
311
|
+
encoding: "utf8",
|
|
312
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
313
|
+
});
|
|
314
|
+
if (result.status !== 0) return { token: "", source: "none" };
|
|
315
|
+
return { token: normalizeAnthropicToken(result.stdout), source: "ant" };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ANSI/CSI escape sequences that `claude setup-token`'s TUI emits (colors, cursor
|
|
319
|
+
// moves, bracketed-paste). We strip these before scanning the captured stream for the
|
|
320
|
+
// token so a match isn't split by control bytes. Kept deliberately broad.
|
|
321
|
+
// eslint-disable-next-line no-control-regex
|
|
322
|
+
const ANSI_ESCAPE_RE = /\x1b\[[0-9;?]*[ -/]*[@-~]/g;
|
|
323
|
+
// The token line printed by `claude setup-token`. Match any sk-ant-* token so we don't
|
|
324
|
+
// depend on the exact variant/version (e.g. sk-ant-oat01-...).
|
|
325
|
+
const ANTHROPIC_SETUP_TOKEN_RE = /sk-ant-[A-Za-z0-9_-]+/;
|
|
326
|
+
|
|
327
|
+
// Run `claude setup-token` inline and extract the token it prints, WITHOUT echoing any
|
|
328
|
+
// of the command's own output to the terminal. `claude setup-token` is a full-screen
|
|
329
|
+
// Ink TUI: it redraws with carriage returns and cursor-move escapes (not newlines), so
|
|
330
|
+
// its banner ("Welcome to Claude Code…"), spinner frames, the OAuth URL, the real token
|
|
331
|
+
// ("sk-ant-oat01-…") and the "Store this / export" chrome all arrive interleaved with
|
|
332
|
+
// no reliable line boundaries. There's no safe way to tee that stream line-by-line, and
|
|
333
|
+
// none of it needs to reach the user: the browser auto-opens for approval, and the
|
|
334
|
+
// token goes server-side (encrypted). So we fully suppress the child's output — we pipe
|
|
335
|
+
// BOTH stdout and stderr only to capture them in memory for token extraction, and print
|
|
336
|
+
// our own clean status lines around the call instead. The Ink TUI renders to stdout (the
|
|
337
|
+
// token line arrives there — that's what we scan), but we also silence stderr so no
|
|
338
|
+
// spinner/banner frame can leak from the other stream. Only stdin inherits the TTY, which
|
|
339
|
+
// is all that's needed for the interactive approval handoff; the browser is opened by the
|
|
340
|
+
// OS, not the terminal. After exit we ANSI-strip the captured output and regex out the
|
|
341
|
+
// token. Returns "" on non-zero exit or if no token appeared, so the caller falls back to
|
|
342
|
+
// manual paste.
|
|
343
|
+
function runClaudeSetupToken() {
|
|
344
|
+
return new Promise((resolve) => {
|
|
345
|
+
let captured = "";
|
|
346
|
+
// On Windows, npm installs `claude` as a `claude.cmd` shim which Node's spawn will
|
|
347
|
+
// not resolve without a shell (it looks for a bare executable and throws ENOENT).
|
|
348
|
+
// Route through the shell there so the shim resolves; the args are fixed literals
|
|
349
|
+
// (no user input), so there's no shell-injection surface. POSIX spawns `claude`
|
|
350
|
+
// directly. Works from both PowerShell and cmd.exe host terminals since Node invokes
|
|
351
|
+
// cmd.exe itself for the shell.
|
|
352
|
+
const child = spawn("claude", ["setup-token"], {
|
|
353
|
+
// stdin keeps the TTY; stdout+stderr are piped (captured, never echoed).
|
|
354
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
355
|
+
shell: process.platform === "win32",
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
const capture = (chunk) => {
|
|
359
|
+
captured += chunk.toString("utf8"); // in-memory only — nothing is written to the terminal
|
|
360
|
+
};
|
|
361
|
+
child.stdout.on("data", capture);
|
|
362
|
+
child.stderr.on("data", capture);
|
|
363
|
+
|
|
364
|
+
child.on("error", () => resolve(""));
|
|
365
|
+
child.on("close", (code) => {
|
|
366
|
+
if (code !== 0) return resolve("");
|
|
367
|
+
const match = captured.replace(ANSI_ESCAPE_RE, "").match(ANTHROPIC_SETUP_TOKEN_RE);
|
|
368
|
+
resolve(match ? match[0] : "");
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function normalizeAnthropicToken(value) {
|
|
374
|
+
let text = String(value || "").trim();
|
|
375
|
+
if (!text) return "";
|
|
376
|
+
text = text.replace(/^export\s+/, "");
|
|
377
|
+
const match = text.match(/^ANTHROPIC_AUTH_TOKEN=(.*)$/);
|
|
378
|
+
if (match) text = match[1].trim();
|
|
379
|
+
if ((text.startsWith('"') && text.endsWith('"')) || (text.startsWith("'") && text.endsWith("'"))) {
|
|
380
|
+
text = text.slice(1, -1);
|
|
381
|
+
}
|
|
382
|
+
return text.replace(/\s+/g, "").trim();
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async function install(flags) {
|
|
386
|
+
if (flags.makeDefault && flags.pennyOnly) {
|
|
387
|
+
throw new Error("--make-default and --penny-only cannot be used together.");
|
|
388
|
+
}
|
|
389
|
+
const selected = await selectHarnesses(flags);
|
|
390
|
+
if (selected.length === 0) {
|
|
391
|
+
console.log("No supported harnesses selected.");
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
console.log("PennyRouter install");
|
|
396
|
+
console.log("");
|
|
397
|
+
|
|
398
|
+
const manifest = await loadManifest();
|
|
399
|
+
|
|
400
|
+
if (flags.dryRun) {
|
|
401
|
+
const defaultRouting = await chooseDefaultRouting(flags, selected, manifest);
|
|
402
|
+
for (const harness of selected) {
|
|
403
|
+
const explicit = ["claude-code", "codex"].includes(harness.id);
|
|
404
|
+
const command = harness.id === "claude-code" ? "penny claude" : "penny codex";
|
|
405
|
+
console.log(`Will install on ${harness.name}${explicit ? ` via ${command}` : installNoteForHarness(harness.id)}.`);
|
|
406
|
+
if (defaultRouting.get(harness.id)) {
|
|
407
|
+
console.log(`Will also route bare ${harness.id === "claude-code" ? "claude" : harness.id} through PennyRouter.`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
if (flags.gatewayBaseUrl) console.log(`Serving gateway: ${flags.gatewayBaseUrl}`);
|
|
411
|
+
if (selected.some((harness) => harness.id === "claude-code")) {
|
|
412
|
+
console.log("Claude Code provider picker would be shown unless providers were supplied by flags or environment.");
|
|
413
|
+
}
|
|
414
|
+
console.log("Dry run only. No files changed.");
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const session = await createCliSession({
|
|
419
|
+
appUrl: flags.appUrl || APP_URL,
|
|
420
|
+
harnesses: selected.map((harness) => harness.id),
|
|
421
|
+
accountMode: "existing",
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
console.log("Opening your browser to log in and authorize this machine with your PennyRouter account...");
|
|
425
|
+
console.log(`If your browser did not open, visit:\n${session.authorize_url}`);
|
|
426
|
+
if (!flags.noBrowser) openBrowser(session.authorize_url);
|
|
427
|
+
console.log("");
|
|
428
|
+
console.log("Waiting for approval...");
|
|
429
|
+
|
|
430
|
+
const claim = await pollForKey(session);
|
|
431
|
+
console.log(`Authorized. Received key ${maskKey(claim.api_key)}.`);
|
|
432
|
+
console.log("");
|
|
433
|
+
|
|
434
|
+
const claudeProviders = await selectClaudeFundingProviders(flags, selected);
|
|
435
|
+
const codexProviders = await selectCodexFundingProviders(flags, selected);
|
|
436
|
+
const claudeAnthropicAuth = await maybeConfigureClaudeAnthropicAuth(
|
|
437
|
+
flags, selected, claudeProviders.has("subscription"));
|
|
438
|
+
const funding = await configureUserFunding(
|
|
439
|
+
flags, selected, claudeAnthropicAuth, claudeProviders, codexProviders);
|
|
440
|
+
const gatewayBaseUrl = flags.gatewayBaseUrl || claim.gateway_base_url;
|
|
441
|
+
|
|
442
|
+
for (const credential of funding.credentials) {
|
|
443
|
+
await storeUpstreamCredential({
|
|
444
|
+
gatewayBaseUrl,
|
|
445
|
+
apiKey: claim.api_key,
|
|
446
|
+
provider: credential.provider,
|
|
447
|
+
token: credential.token,
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (selected.some((harness) => harness.id === "claude-code")) {
|
|
452
|
+
console.log("Verifying modern Claude Code compatibility before changing local settings...");
|
|
453
|
+
await probeClaudeCodeGateway({ gatewayBaseUrl, apiKey: claim.api_key });
|
|
454
|
+
console.log("");
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Keep this as the final interactive choice: users first authorize their account,
|
|
458
|
+
// optionally connect their subscription, and pass compatibility checks. Then they
|
|
459
|
+
// choose the visible command behavior with all relevant context in front of them.
|
|
460
|
+
const defaultRouting = await chooseDefaultRouting(flags, selected, manifest);
|
|
461
|
+
|
|
462
|
+
const launchable = selected.filter((harness) => ["claude-code", "codex"].includes(harness.id));
|
|
463
|
+
let runtimeResult = null;
|
|
464
|
+
if (launchable.length > 0) {
|
|
465
|
+
runtimeResult = await installPennyRuntime({ updatePath: !flags.noPathUpdate });
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const records = [];
|
|
469
|
+
const failures = [];
|
|
470
|
+
for (const harness of selected) {
|
|
471
|
+
await runHarnessJob(failures, harness.id, "install", async () => {
|
|
472
|
+
const plan = await harness.planInstall();
|
|
473
|
+
const existingRecord = (manifest.installations || []).find(
|
|
474
|
+
(candidate) => candidate.harness === harness.id,
|
|
475
|
+
);
|
|
476
|
+
const supportsPennyCommand = ["claude-code", "codex"].includes(harness.id);
|
|
477
|
+
const makeDefault = defaultRouting.get(harness.id) ?? true;
|
|
478
|
+
const hadGlobalRouting = Boolean(existingRecord && existingRecord.default_routing !== false);
|
|
479
|
+
if (["claude-code", "codex"].includes(harness.id) && (makeDefault || hadGlobalRouting)) {
|
|
480
|
+
assertNoRunningHarness(harness.id, flags);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const codexSubscriptionAuth = harness.id === "codex"
|
|
484
|
+
? funding.codexSubscriptionAuth
|
|
485
|
+
: false;
|
|
486
|
+
const claudeSubscriptionAuth = harness.id === "claude-code"
|
|
487
|
+
&& Boolean(claudeAnthropicAuth || existingRecord?.anthropic_token_stored);
|
|
488
|
+
|
|
489
|
+
if (supportsPennyCommand) {
|
|
490
|
+
await configurePennyHarness(harness.id, {
|
|
491
|
+
apiKey: claim.api_key,
|
|
492
|
+
gatewayBaseUrl,
|
|
493
|
+
subscriptionAuth: harness.id === "claude-code"
|
|
494
|
+
? claudeSubscriptionAuth
|
|
495
|
+
: codexSubscriptionAuth,
|
|
496
|
+
models: harness.id === "claude-code" ? CLAUDE_CODE_MODELS : null,
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
let record;
|
|
501
|
+
if (!supportsPennyCommand || makeDefault) {
|
|
502
|
+
record = harness.id === "claude-code" && claudeSubscriptionAuth
|
|
503
|
+
? await harness.installStoredToken({
|
|
504
|
+
apiKey: claim.api_key, gatewayBaseUrl, plan, existingRecord,
|
|
505
|
+
})
|
|
506
|
+
: await harness.install({
|
|
507
|
+
apiKey: claim.api_key, gatewayBaseUrl, plan, existingRecord,
|
|
508
|
+
});
|
|
509
|
+
} else {
|
|
510
|
+
if (hadGlobalRouting) {
|
|
511
|
+
await harness.uninstall(existingRecord);
|
|
512
|
+
}
|
|
513
|
+
record = {
|
|
514
|
+
harness: harness.id,
|
|
515
|
+
config_path: compactHome(plan.configPath),
|
|
516
|
+
backup_path: existingRecord?.backup_path || null,
|
|
517
|
+
installed_at: new Date().toISOString(),
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
if (supportsPennyCommand) {
|
|
521
|
+
record.launch_available = true;
|
|
522
|
+
record.launch_config_path = compactHome(
|
|
523
|
+
harness.id === "claude-code" ? claudeLaunchSettingsPath() : codexLaunchProfilePath(),
|
|
524
|
+
);
|
|
525
|
+
record.default_routing = Boolean(makeDefault);
|
|
526
|
+
}
|
|
527
|
+
if (harness.id === "claude-code" && claudeSubscriptionAuth) {
|
|
528
|
+
record.anthropic_oauth = true;
|
|
529
|
+
record.anthropic_token_stored = true;
|
|
530
|
+
record.token_source = claudeAnthropicAuth?.source || existingRecord?.token_source || "stored";
|
|
531
|
+
}
|
|
532
|
+
if (harness.id === "claude-code" && funding.claudeCredentialProvider) {
|
|
533
|
+
record.user_funded_provider = funding.claudeCredentialProvider;
|
|
534
|
+
}
|
|
535
|
+
if (harness.id === "codex" && funding.codexCredentialProvider) {
|
|
536
|
+
record.user_funded_provider = funding.codexCredentialProvider;
|
|
537
|
+
}
|
|
538
|
+
if (harness.id === "codex") record.openai_subscription_auth = codexSubscriptionAuth;
|
|
539
|
+
records.push(record);
|
|
540
|
+
upsertManifestRecord(manifest, record);
|
|
541
|
+
await saveManifest(manifest);
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
await saveManifest(manifest);
|
|
546
|
+
console.log("");
|
|
547
|
+
console.log(records.length > 0
|
|
548
|
+
? "PennyRouter installation complete."
|
|
549
|
+
: "PennyRouter was not installed on any selected tool.");
|
|
550
|
+
console.log(`Manifest: ${statePath("installations.json")}`);
|
|
551
|
+
for (const record of records) {
|
|
552
|
+
const harness = HARNESS_BY_ID[record.harness];
|
|
553
|
+
if (record.launch_available) {
|
|
554
|
+
const command = record.harness === "claude-code" ? "penny claude" : "penny codex";
|
|
555
|
+
const native = record.harness === "claude-code" ? "claude" : "codex";
|
|
556
|
+
const harnessName = record.harness === "claude-code" ? "Claude Code" : "Codex";
|
|
557
|
+
const commandWidth = Math.max(command.length, native.length) + 2;
|
|
558
|
+
console.log("");
|
|
559
|
+
console.log(color(`How to Run ${harnessName}`, "bold"));
|
|
560
|
+
console.log("");
|
|
561
|
+
console.log(` ${color(command.padEnd(commandWidth), "bold")}→ ${harnessName} with PennyRouter`);
|
|
562
|
+
console.log(` ${color(native.padEnd(commandWidth), "bold")}→ ${record.default_routing ? `${harnessName} with PennyRouter (default routing enabled)` : `${harnessName} native`}`);
|
|
563
|
+
} else {
|
|
564
|
+
console.log(`Installed on ${harness?.name || record.harness}${installNoteForRecord(record)}.`);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
if (runtimeResult && !runtimeResult.available) {
|
|
568
|
+
console.log("Add the PennyRouter launcher directory to PATH before using the `penny` command.");
|
|
569
|
+
} else if (runtimeResult?.changed) {
|
|
570
|
+
console.log("Open a new terminal (or reload your shell) before using the `penny` command.");
|
|
571
|
+
}
|
|
572
|
+
console.log("");
|
|
573
|
+
reportFailures(failures);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Refresh the managed local files to this package version WITHOUT re-running any of the
|
|
577
|
+
// account/credential flow. Everything an update needs is already on disk: the pr- key and
|
|
578
|
+
// gateway URL live in the penny config (and, for older installs, in the harness config the
|
|
579
|
+
// installer wrote), and the upstream provider tokens live server-side, encrypted against the
|
|
580
|
+
// account. So no browser claim, no provider checkboxes, no token prompts — this command is
|
|
581
|
+
// non-interactive by construction and never touches a credential.
|
|
582
|
+
//
|
|
583
|
+
// What it DOES rewrite is everything regenerated from code constants, which is exactly what
|
|
584
|
+
// goes stale on a version bump: the copied runtime + `penny` shim, the /penny skill, the
|
|
585
|
+
// Claude launch settings overlay, the Codex provider profile, and the model labels +
|
|
586
|
+
// context-window env in the harness configs.
|
|
587
|
+
//
|
|
588
|
+
// Per-user choices are read back from the manifest, never re-asked: `default_routing` decides
|
|
589
|
+
// whether the bare `claude`/`codex` command keeps pointing at the gateway, and
|
|
590
|
+
// `anthropic_token_stored`/`openai_subscription_auth` decide which auth shape gets rewritten.
|
|
591
|
+
// Re-deriving either from prompt defaults would silently flip a --penny-only user into
|
|
592
|
+
// default routing, or drop subscription auth from a working install.
|
|
593
|
+
async function update(flags) {
|
|
594
|
+
const manifest = await loadManifest();
|
|
595
|
+
if (pruneUnsupportedManifestRecords(manifest).length > 0) await saveManifest(manifest);
|
|
596
|
+
const records = manifest.installations || [];
|
|
597
|
+
|
|
598
|
+
const requested = parseHarnessList(flags.harness);
|
|
599
|
+
const targets = records.filter((record) => {
|
|
600
|
+
if (record.disabled) return false;
|
|
601
|
+
if (flags.all || requested.length === 0) return true;
|
|
602
|
+
return requested.includes(record.harness);
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
if (records.length === 0) {
|
|
606
|
+
throw new Error("No PennyRouter installation found. Run `npx pennyrouter install` first.");
|
|
607
|
+
}
|
|
608
|
+
if (targets.length === 0) {
|
|
609
|
+
console.log("No enabled PennyRouter installations matched. Nothing to update.");
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const launchConfig = await loadPennyConfig();
|
|
614
|
+
const globalAuth = await readClaudeCodeGatewayAuth();
|
|
615
|
+
const apiKey = flags.pennyKey || launchConfig.api_key || globalAuth.apiKey;
|
|
616
|
+
const gatewayBaseUrl = flags.gatewayBaseUrl
|
|
617
|
+
|| launchConfig.gateway_base_url
|
|
618
|
+
|| globalAuth.gatewayBaseUrl
|
|
619
|
+
|| process.env.PENNYROUTER_GATEWAY_BASE_URL
|
|
620
|
+
|| "https://api.pennyrouter.com";
|
|
621
|
+
|
|
622
|
+
if (!apiKey) {
|
|
623
|
+
throw new Error(
|
|
624
|
+
"Could not find your PennyRouter key in the local config. Pass `--penny-key pr-...`, "
|
|
625
|
+
+ "or run `npx pennyrouter install` to re-authorize.",
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
console.log("PennyRouter update");
|
|
630
|
+
console.log("");
|
|
631
|
+
console.log(`Key ${maskKey(apiKey)} · gateway ${gatewayBaseUrl}`);
|
|
632
|
+
console.log("Subscription and API keys are left untouched.");
|
|
633
|
+
console.log("");
|
|
634
|
+
|
|
635
|
+
if (flags.dryRun) {
|
|
636
|
+
for (const record of targets) {
|
|
637
|
+
const routing = record.default_routing === false ? "penny command only" : "default routing";
|
|
638
|
+
console.log(`Will refresh ${record.harness} (${routing}).`);
|
|
639
|
+
}
|
|
640
|
+
console.log("Will refresh the penny runtime and shim.");
|
|
641
|
+
console.log("Dry run only. No files changed.");
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const failures = [];
|
|
646
|
+
let runtimeResult = null;
|
|
647
|
+
if (targets.some((record) => ["claude-code", "codex"].includes(record.harness))) {
|
|
648
|
+
runtimeResult = await installPennyRuntime({ updatePath: !flags.noPathUpdate });
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
for (const record of targets) {
|
|
652
|
+
const harness = HARNESS_BY_ID[record.harness];
|
|
653
|
+
if (!harness) continue;
|
|
654
|
+
|
|
655
|
+
await runHarnessJob(failures, record.harness, "update", async () => {
|
|
656
|
+
const supportsPennyCommand = ["claude-code", "codex"].includes(record.harness);
|
|
657
|
+
// An update rewrites the harness's own config only when that config is ours to
|
|
658
|
+
// rewrite — i.e. the bare command still routes through the gateway. In penny-only
|
|
659
|
+
// mode the harness config belongs to the user and we touch just the launch profile.
|
|
660
|
+
const ownsGlobalRouting = !record.launch_available || record.default_routing !== false;
|
|
661
|
+
if (ownsGlobalRouting) assertNoRunningHarness(record.harness, flags);
|
|
662
|
+
|
|
663
|
+
const claudeSubscriptionAuth = record.harness === "claude-code"
|
|
664
|
+
&& Boolean(record.anthropic_token_stored);
|
|
665
|
+
const codexSubscriptionAuth = record.harness === "codex"
|
|
666
|
+
? await detectCodexChatgptAuth()
|
|
667
|
+
: false;
|
|
668
|
+
|
|
669
|
+
if (supportsPennyCommand) {
|
|
670
|
+
await configurePennyHarness(record.harness, {
|
|
671
|
+
apiKey,
|
|
672
|
+
gatewayBaseUrl,
|
|
673
|
+
subscriptionAuth: record.harness === "claude-code"
|
|
674
|
+
? claudeSubscriptionAuth
|
|
675
|
+
: codexSubscriptionAuth,
|
|
676
|
+
models: record.harness === "claude-code" ? CLAUDE_CODE_MODELS : null,
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
let next = record;
|
|
681
|
+
if (ownsGlobalRouting) {
|
|
682
|
+
const plan = await harness.planInstall();
|
|
683
|
+
// Pass the existing record through so install-time state the harness stashed
|
|
684
|
+
// (previous_model_provider, claude_ai_connectors_before) survives the rewrite
|
|
685
|
+
// and a later uninstall still restores the user's original values.
|
|
686
|
+
const installed = record.harness === "claude-code" && claudeSubscriptionAuth
|
|
687
|
+
? await harness.installStoredToken({ apiKey, gatewayBaseUrl, plan, existingRecord: record })
|
|
688
|
+
: await harness.install({ apiKey, gatewayBaseUrl, plan, existingRecord: record });
|
|
689
|
+
next = { ...record, ...installed, installed_at: record.installed_at };
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
next.updated_at = new Date().toISOString();
|
|
693
|
+
if (record.harness === "codex") next.openai_subscription_auth = codexSubscriptionAuth;
|
|
694
|
+
upsertManifestRecord(manifest, next);
|
|
695
|
+
await saveManifest(manifest);
|
|
696
|
+
console.log(`Updated ${record.harness}.`);
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
await saveManifest(manifest);
|
|
701
|
+
console.log("");
|
|
702
|
+
if (runtimeResult && !runtimeResult.available) {
|
|
703
|
+
console.log("Add the PennyRouter launcher directory to PATH before using the `penny` command.");
|
|
704
|
+
} else if (runtimeResult?.changed) {
|
|
705
|
+
console.log("Open a new terminal (or reload your shell) before using the `penny` command.");
|
|
706
|
+
}
|
|
707
|
+
console.log("Restart any running Claude Code or Codex sessions to pick up the update.");
|
|
708
|
+
reportFailures(failures);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
async function chooseDefaultRouting(flags, selected, manifest) {
|
|
712
|
+
const out = new Map();
|
|
713
|
+
const launchable = selected.filter((harness) => ["claude-code", "codex"].includes(harness.id));
|
|
714
|
+
let newInstallChoice = null;
|
|
715
|
+
for (const harness of launchable) {
|
|
716
|
+
if (flags.makeDefault) {
|
|
717
|
+
out.set(harness.id, true);
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
if (flags.pennyOnly) {
|
|
721
|
+
out.set(harness.id, false);
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
const existing = (manifest.installations || []).find((item) => item.harness === harness.id);
|
|
725
|
+
if (existing) {
|
|
726
|
+
const currentlyDefault = existing.default_routing !== false;
|
|
727
|
+
if (flags.dryRun || !currentlyDefault || flags.yes || !input.isTTY || !output.isTTY) {
|
|
728
|
+
out.set(harness.id, currentlyDefault);
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
731
|
+
const native = harness.id === "claude-code" ? "claude" : "codex";
|
|
732
|
+
const change = await confirm(
|
|
733
|
+
`PennyRouter currently runs when you type ${color(native, "bold")}. Change it to run only with ${color(`penny ${native}`, "bold")}?`,
|
|
734
|
+
);
|
|
735
|
+
out.set(harness.id, !change);
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
if (newInstallChoice === null) {
|
|
739
|
+
// Auto-load-by-default prompt is disabled for now — always install as `penny <native>`
|
|
740
|
+
// only. Flip ENABLE_AUTOLOAD_PROMPT to true to restore asking the user.
|
|
741
|
+
const ENABLE_AUTOLOAD_PROMPT = false;
|
|
742
|
+
if (!ENABLE_AUTOLOAD_PROMPT || flags.dryRun || flags.yes || !input.isTTY || !output.isTTY) {
|
|
743
|
+
newInstallChoice = false;
|
|
744
|
+
} else {
|
|
745
|
+
for (const item of launchable) {
|
|
746
|
+
const native = item.id === "claude-code" ? "claude" : "codex";
|
|
747
|
+
const harnessName = item.id === "claude-code" ? "Claude Code" : "Codex";
|
|
748
|
+
const command = `penny ${native}`;
|
|
749
|
+
const commandWidth = Math.max(command.length, native.length) + 2;
|
|
750
|
+
console.log("");
|
|
751
|
+
console.log(color(`How to Run ${harnessName}`, "bold"));
|
|
752
|
+
console.log("");
|
|
753
|
+
console.log(` ${color(command.padEnd(commandWidth), "bold")}→ ${harnessName} with PennyRouter`);
|
|
754
|
+
console.log(` ${color(native.padEnd(commandWidth), "bold")}→ ${harnessName} native (recommended)`);
|
|
755
|
+
}
|
|
756
|
+
console.log("");
|
|
757
|
+
const bare = launchable.map((item) => item.id === "claude-code" ? "claude" : "codex");
|
|
758
|
+
const quoted = bare.map((command) => `'${command}'`);
|
|
759
|
+
const label = quoted.length === 1
|
|
760
|
+
? quoted[0]
|
|
761
|
+
: `${quoted.slice(0, -1).join(", ")} and ${quoted.at(-1)}`;
|
|
762
|
+
newInstallChoice = await confirm(
|
|
763
|
+
`Set ${label} to automatically load PennyRouter with every session?`,
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
out.set(harness.id, newInstallChoice);
|
|
768
|
+
}
|
|
769
|
+
return out;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
async function selectClaudeFundingProviders(flags, selected) {
|
|
773
|
+
if (!selected.some((harness) => harness.id === "claude-code")) return new Set();
|
|
774
|
+
|
|
775
|
+
const supplied = new Set();
|
|
776
|
+
if (!flags.noAnthropicAuth && (flags.anthropicAuth || flags.token || flags.tokenCommand
|
|
777
|
+
|| process.env.ANTHROPIC_AUTH_TOKEN)) supplied.add("subscription");
|
|
778
|
+
if (flags.anthropicApiKey || process.env.ANTHROPIC_API_KEY) supplied.add("anthropic-api");
|
|
779
|
+
if (flags.openrouterApiKey || process.env.OPENROUTER_API_KEY) supplied.add("openrouter");
|
|
780
|
+
|
|
781
|
+
// Explicit flags are the automation selector. Interactive installs always default to the
|
|
782
|
+
// subscription alone; environment keys are used if the user checks their provider.
|
|
783
|
+
const explicit = flags.anthropicAuth || flags.noAnthropicAuth || flags.token
|
|
784
|
+
|| flags.tokenCommand || flags.anthropicApiKey || flags.openrouterApiKey;
|
|
785
|
+
if (explicit || flags.yes || !input.isTTY || !output.isTTY) return supplied;
|
|
786
|
+
|
|
787
|
+
console.log("PennyRouter can use your Claude subscription, an Anthropic API key, or an OpenRouter API key to run Claude Code.");
|
|
788
|
+
console.log("Choose one or more providers. PennyRouter will try them in order when available.");
|
|
789
|
+
return promptFundingProviderCheckboxes(
|
|
790
|
+
CLAUDE_PROVIDER_OPTIONS,
|
|
791
|
+
"Which providers should PennyRouter use for Claude Code?",
|
|
792
|
+
new Set(["subscription"]),
|
|
793
|
+
);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
async function selectCodexFundingProviders(flags, selected) {
|
|
797
|
+
if (!selected.some((harness) => harness.id === "codex")) return new Set();
|
|
798
|
+
|
|
799
|
+
const supplied = new Set();
|
|
800
|
+
if (flags.openaiApiKey || process.env.OPENAI_API_KEY) supplied.add("openai-api");
|
|
801
|
+
if (flags.openrouterApiKey || process.env.OPENROUTER_API_KEY) supplied.add("openrouter");
|
|
802
|
+
const hasChatgptLogin = await detectCodexChatgptAuth();
|
|
803
|
+
if (hasChatgptLogin) supplied.add("subscription");
|
|
804
|
+
|
|
805
|
+
const explicit = flags.openaiApiKey || flags.openrouterApiKey;
|
|
806
|
+
if (explicit || flags.yes || !input.isTTY || !output.isTTY) return supplied;
|
|
807
|
+
|
|
808
|
+
console.log("PennyRouter can use your ChatGPT subscription, an OpenAI API key, or an OpenRouter API key to run Codex.");
|
|
809
|
+
console.log("Choose one or more providers. PennyRouter will try them in order when available.");
|
|
810
|
+
return promptFundingProviderCheckboxes(
|
|
811
|
+
CODEX_PROVIDER_OPTIONS,
|
|
812
|
+
"Which providers should PennyRouter use for Codex?",
|
|
813
|
+
new Set(["subscription"]),
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
async function maybeConfigureClaudeAnthropicAuth(flags, selected, enabled = true) {
|
|
818
|
+
if (!selected.some((harness) => harness.id === "claude-code")) return null;
|
|
819
|
+
if (!enabled || flags.noAnthropicAuth) return null;
|
|
820
|
+
|
|
821
|
+
if (flags.token || flags.tokenCommand || process.env.ANTHROPIC_AUTH_TOKEN) {
|
|
822
|
+
const tokenResult = readAnthropicOAuthToken(flags);
|
|
823
|
+
if (tokenResult.token) return tokenResult;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
if (flags.yes || !input.isTTY || !output.isTTY) {
|
|
827
|
+
if (flags.anthropicAuth) {
|
|
828
|
+
throw new Error(
|
|
829
|
+
"Claude subscription auth was requested, but no Anthropic bearer token was found. Pass --token, --token-command, or ANTHROPIC_AUTH_TOKEN.",
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
return null;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
const tokenResult = readAnthropicOAuthToken({ tokenCommand: flags.tokenCommand });
|
|
836
|
+
if (tokenResult.token) return tokenResult;
|
|
837
|
+
|
|
838
|
+
console.log("");
|
|
839
|
+
if (commandExists("claude")) {
|
|
840
|
+
// The user already opted into subscription auth above, so run `claude setup-token`
|
|
841
|
+
// directly and capture the token — no copy-paste, no second confirm. The command
|
|
842
|
+
// opens the browser for approval inline. On failure we fall through to manual paste.
|
|
843
|
+
console.log("Launching Claude sign-in — approve in your browser…");
|
|
844
|
+
const captured = await runClaudeSetupToken();
|
|
845
|
+
if (captured) {
|
|
846
|
+
console.log("✓ Claude subscription auth enabled.");
|
|
847
|
+
console.log("");
|
|
848
|
+
return { token: captured, source: "setup-token" };
|
|
849
|
+
}
|
|
850
|
+
console.log("");
|
|
851
|
+
console.log("Couldn't capture a token automatically. Paste it below, or leave blank to skip.");
|
|
852
|
+
} else {
|
|
853
|
+
console.log("Claude Code was not found on PATH. Paste an Anthropic bearer token, or leave blank to skip.");
|
|
854
|
+
}
|
|
855
|
+
const token = normalizeAnthropicToken(await promptSecretish("Anthropic token (blank to skip): "));
|
|
856
|
+
if (!token) {
|
|
857
|
+
console.log("Skipped Claude subscription auth. Enable later with `@penny auth` or `pennyrouter auth anthropic`.");
|
|
858
|
+
console.log("");
|
|
859
|
+
return null;
|
|
860
|
+
}
|
|
861
|
+
console.log("");
|
|
862
|
+
return { token, source: "prompt" };
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
async function configureUserFunding(flags, selected, claudeAnthropicAuth,
|
|
866
|
+
claudeProviders = new Set(), codexProviders = new Set()) {
|
|
867
|
+
const wantsClaude = selected.some((harness) => harness.id === "claude-code");
|
|
868
|
+
const wantsCodex = selected.some((harness) => harness.id === "codex");
|
|
869
|
+
const interactive = !flags.yes && input.isTTY && output.isTTY;
|
|
870
|
+
const credentials = [];
|
|
871
|
+
|
|
872
|
+
let anthropicApiKey = String(
|
|
873
|
+
flags.anthropicApiKey || process.env.ANTHROPIC_API_KEY || "",
|
|
874
|
+
).trim();
|
|
875
|
+
let openaiApiKey = String(
|
|
876
|
+
flags.openaiApiKey || process.env.OPENAI_API_KEY || "",
|
|
877
|
+
).trim();
|
|
878
|
+
let openrouterApiKey = String(
|
|
879
|
+
flags.openrouterApiKey || process.env.OPENROUTER_API_KEY || "",
|
|
880
|
+
).trim();
|
|
881
|
+
const codexSubscriptionAuth = wantsCodex && codexProviders.has("subscription")
|
|
882
|
+
? await detectCodexChatgptAuth()
|
|
883
|
+
: false;
|
|
884
|
+
|
|
885
|
+
if (wantsClaude && claudeProviders.has("anthropic-api") && !anthropicApiKey) {
|
|
886
|
+
if (!interactive) throw new Error("Anthropic API was selected, but no key was provided.");
|
|
887
|
+
anthropicApiKey = (await promptSecretish("Anthropic API key: ")).trim();
|
|
888
|
+
}
|
|
889
|
+
if (wantsClaude && claudeProviders.has("openrouter") && !openrouterApiKey) {
|
|
890
|
+
if (!interactive) throw new Error("OpenRouter was selected, but no key was provided.");
|
|
891
|
+
openrouterApiKey = (await promptSecretish("OpenRouter API key: ")).trim();
|
|
892
|
+
}
|
|
893
|
+
if (wantsCodex && codexProviders.has("subscription") && !codexSubscriptionAuth) {
|
|
894
|
+
throw new Error(
|
|
895
|
+
"ChatGPT subscription was selected, but Codex is not logged in. Run `codex login`, or select an API-key provider.",
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
if (wantsCodex && codexProviders.has("openai-api") && !openaiApiKey) {
|
|
899
|
+
if (!interactive) throw new Error("OpenAI API was selected, but no key was provided.");
|
|
900
|
+
openaiApiKey = (await promptSecretish("OpenAI API key: ")).trim();
|
|
901
|
+
}
|
|
902
|
+
if (wantsCodex && codexProviders.has("openrouter") && !openrouterApiKey) {
|
|
903
|
+
if (!interactive) throw new Error("OpenRouter was selected, but no key was provided.");
|
|
904
|
+
openrouterApiKey = (await promptSecretish("OpenRouter API key: ")).trim();
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
if (wantsClaude && !claudeAnthropicAuth && !anthropicApiKey && !openrouterApiKey) {
|
|
908
|
+
if (!interactive) {
|
|
909
|
+
throw new Error(
|
|
910
|
+
"Claude Code requires a Claude subscription, --anthropic-api-key (or ANTHROPIC_API_KEY), "
|
|
911
|
+
+ "or --openrouter-api-key (or OPENROUTER_API_KEY). Nothing was installed.",
|
|
912
|
+
);
|
|
913
|
+
}
|
|
914
|
+
console.log("Claude Code needs a user-funded provider. PennyRouter will never fall back to its own API keys.");
|
|
915
|
+
anthropicApiKey = (await promptSecretish(
|
|
916
|
+
"Anthropic API key (leave blank to use OpenRouter): ",
|
|
917
|
+
)).trim();
|
|
918
|
+
if (!anthropicApiKey) {
|
|
919
|
+
openrouterApiKey = (await promptSecretish("OpenRouter API key: ")).trim();
|
|
920
|
+
}
|
|
921
|
+
if (!anthropicApiKey && !openrouterApiKey) {
|
|
922
|
+
throw new Error("Claude Code installation cancelled: no subscription or API key was provided.");
|
|
923
|
+
}
|
|
924
|
+
console.log("");
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
if (wantsCodex && !codexSubscriptionAuth && !openaiApiKey && !openrouterApiKey) {
|
|
928
|
+
if (!interactive) {
|
|
929
|
+
throw new Error(
|
|
930
|
+
"Codex requires ChatGPT login, --openai-api-key (or OPENAI_API_KEY), "
|
|
931
|
+
+ "or --openrouter-api-key (or OPENROUTER_API_KEY). Nothing was installed.",
|
|
932
|
+
);
|
|
933
|
+
}
|
|
934
|
+
console.log("Codex is not logged into ChatGPT and needs a user-funded provider.");
|
|
935
|
+
console.log("PennyRouter will never fall back to its own API keys.");
|
|
936
|
+
openaiApiKey = (await promptSecretish(
|
|
937
|
+
"OpenAI API key (leave blank to use OpenRouter): ",
|
|
938
|
+
)).trim();
|
|
939
|
+
if (!openaiApiKey) {
|
|
940
|
+
openrouterApiKey = (await promptSecretish("OpenRouter API key: ")).trim();
|
|
941
|
+
}
|
|
942
|
+
if (!openaiApiKey && !openrouterApiKey) {
|
|
943
|
+
throw new Error("Codex installation cancelled: no subscription or API key was provided.");
|
|
944
|
+
}
|
|
945
|
+
console.log("");
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
if (claudeAnthropicAuth) {
|
|
949
|
+
credentials.push({ provider: "anthropic_oauth", token: claudeAnthropicAuth.token });
|
|
950
|
+
}
|
|
951
|
+
if (wantsClaude && anthropicApiKey) {
|
|
952
|
+
credentials.push({ provider: "anthropic_api", token: anthropicApiKey });
|
|
953
|
+
}
|
|
954
|
+
if (wantsCodex && openaiApiKey) {
|
|
955
|
+
credentials.push({ provider: "openai_api", token: openaiApiKey });
|
|
956
|
+
}
|
|
957
|
+
if ((wantsClaude || wantsCodex) && openrouterApiKey) {
|
|
958
|
+
credentials.push({ provider: "openrouter", token: openrouterApiKey });
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
return {
|
|
962
|
+
credentials,
|
|
963
|
+
codexSubscriptionAuth,
|
|
964
|
+
claudeCredentialProvider: claudeAnthropicAuth
|
|
965
|
+
? "claude-subscription"
|
|
966
|
+
: anthropicApiKey ? "anthropic-api" : openrouterApiKey ? "openrouter" : null,
|
|
967
|
+
codexCredentialProvider: codexSubscriptionAuth
|
|
968
|
+
? "chatgpt-subscription"
|
|
969
|
+
: openaiApiKey ? "openai-api" : openrouterApiKey ? "openrouter" : null,
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
async function disable(flags) {
|
|
974
|
+
const manifest = await loadManifest();
|
|
975
|
+
if (pruneUnsupportedManifestRecords(manifest).length > 0) await saveManifest(manifest);
|
|
976
|
+
const records = manifest.installations || [];
|
|
977
|
+
const { selectedIds, selectedRecords } = await resolveLifecycleTargets(flags, records);
|
|
978
|
+
|
|
979
|
+
if (selectedRecords.length === 0) {
|
|
980
|
+
console.log("No matching PennyRouter installations found.");
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
console.log("PennyRouter disable");
|
|
985
|
+
console.log("");
|
|
986
|
+
for (const record of selectedRecords) {
|
|
987
|
+
console.log(` - ${record.harness}: ${record.config_path || "manual setup"}`);
|
|
988
|
+
}
|
|
989
|
+
console.log("");
|
|
990
|
+
|
|
991
|
+
if (!flags.yes) {
|
|
992
|
+
const ok = await confirm("Temporarily turn off these PennyRouter local integrations?");
|
|
993
|
+
if (!ok) return;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
const failures = [];
|
|
997
|
+
for (const record of selectedRecords) {
|
|
998
|
+
if (record.disabled) {
|
|
999
|
+
console.log(`${record.harness} is already disabled.`);
|
|
1000
|
+
continue;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
const harness = HARNESS_BY_ID[record.harness];
|
|
1004
|
+
if (!harness) {
|
|
1005
|
+
console.log(`Skipping ${record.harness}: this CLI no longer knows how to disable it.`);
|
|
1006
|
+
continue;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
await runHarnessJob(failures, record.harness, "disable", async () => {
|
|
1010
|
+
const ownsGlobalRouting = !record.launch_available || record.default_routing;
|
|
1011
|
+
if (record.config_path && ownsGlobalRouting) {
|
|
1012
|
+
const snapshotPath = await backupFile(record.config_path, `${record.harness}-disabled`);
|
|
1013
|
+
record.disabled_snapshot_path = snapshotPath;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
if (record.launch_available) await setPennyHarnessEnabled(record.harness, false);
|
|
1017
|
+
if (ownsGlobalRouting) {
|
|
1018
|
+
if (typeof harness.disable === "function") await harness.disable(record);
|
|
1019
|
+
else await harness.uninstall(record);
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
record.disabled = true;
|
|
1023
|
+
record.disabled_at = new Date().toISOString();
|
|
1024
|
+
upsertManifestRecord(manifest, record);
|
|
1025
|
+
await saveManifest(manifest);
|
|
1026
|
+
console.log(`Disabled ${record.harness}.`);
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
await saveManifest(manifest);
|
|
1031
|
+
reportFailures(failures);
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
async function enable(flags) {
|
|
1035
|
+
const manifest = await loadManifest();
|
|
1036
|
+
if (pruneUnsupportedManifestRecords(manifest).length > 0) await saveManifest(manifest);
|
|
1037
|
+
const records = manifest.installations || [];
|
|
1038
|
+
const selectedIds = resolveRequestedHarnessIds(flags, records.map((record) => record.harness));
|
|
1039
|
+
const selectedRecords = records.filter((record) => selectedIds.includes(record.harness));
|
|
1040
|
+
|
|
1041
|
+
if (selectedRecords.length === 0) {
|
|
1042
|
+
console.log("No matching PennyRouter installations found.");
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
console.log("PennyRouter enable");
|
|
1047
|
+
console.log("");
|
|
1048
|
+
for (const record of selectedRecords) {
|
|
1049
|
+
console.log(` - ${record.harness}: ${record.config_path || "manual setup"}`);
|
|
1050
|
+
}
|
|
1051
|
+
console.log("");
|
|
1052
|
+
|
|
1053
|
+
if (!flags.yes) {
|
|
1054
|
+
const ok = await confirm("Turn these PennyRouter local integrations back on?");
|
|
1055
|
+
if (!ok) return;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
const failures = [];
|
|
1059
|
+
for (const record of selectedRecords) {
|
|
1060
|
+
if (!record.disabled) {
|
|
1061
|
+
console.log(`${record.harness} is already enabled.`);
|
|
1062
|
+
continue;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
const harness = HARNESS_BY_ID[record.harness];
|
|
1066
|
+
if (!harness) {
|
|
1067
|
+
console.log(`Skipping ${record.harness}: this CLI no longer knows how to enable it.`);
|
|
1068
|
+
continue;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
await runHarnessJob(failures, record.harness, "enable", async () => {
|
|
1072
|
+
const ownsGlobalRouting = !record.launch_available || record.default_routing;
|
|
1073
|
+
if (record.launch_available) await setPennyHarnessEnabled(record.harness, true);
|
|
1074
|
+
if (ownsGlobalRouting) {
|
|
1075
|
+
if (typeof harness.enable === "function") await harness.enable(record);
|
|
1076
|
+
else if (record.config_path && record.disabled_snapshot_path) {
|
|
1077
|
+
await restoreFile(record.disabled_snapshot_path, record.config_path);
|
|
1078
|
+
} else if (record.manual_required) {
|
|
1079
|
+
console.log(`${record.harness}: re-enable manually in the tool UI using the status details.`);
|
|
1080
|
+
} else {
|
|
1081
|
+
throw new Error("no disabled snapshot found; run install again");
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
delete record.disabled;
|
|
1086
|
+
delete record.disabled_at;
|
|
1087
|
+
upsertManifestRecord(manifest, record);
|
|
1088
|
+
await saveManifest(manifest);
|
|
1089
|
+
console.log(`Enabled ${record.harness}.`);
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
await saveManifest(manifest);
|
|
1094
|
+
reportFailures(failures);
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
async function uninstall(flags) {
|
|
1098
|
+
const manifest = await loadManifest();
|
|
1099
|
+
if (pruneUnsupportedManifestRecords(manifest).length > 0) await saveManifest(manifest);
|
|
1100
|
+
const records = manifest.installations || [];
|
|
1101
|
+
const { selectedIds, selectedRecords } = await resolveLifecycleTargets(flags, records);
|
|
1102
|
+
|
|
1103
|
+
if (selectedRecords.length === 0) {
|
|
1104
|
+
console.log("No matching PennyRouter installations found.");
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
console.log("PennyRouter uninstall");
|
|
1109
|
+
console.log("");
|
|
1110
|
+
for (const record of selectedRecords) {
|
|
1111
|
+
console.log(` - ${record.harness}: ${record.config_path}`);
|
|
1112
|
+
}
|
|
1113
|
+
console.log("");
|
|
1114
|
+
|
|
1115
|
+
if (!flags.yes) {
|
|
1116
|
+
const ok = await confirm("Remove these PennyRouter local integrations?");
|
|
1117
|
+
if (!ok) return;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
const kept = [];
|
|
1121
|
+
const failures = [];
|
|
1122
|
+
let anthropicTokenNotice = false;
|
|
1123
|
+
for (const record of records) {
|
|
1124
|
+
if (!selectedIds.includes(record.harness)) {
|
|
1125
|
+
kept.push(record);
|
|
1126
|
+
continue;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
const harness = HARNESS_BY_ID[record.harness];
|
|
1130
|
+
if (!harness) {
|
|
1131
|
+
console.log(`Skipping ${record.harness}: this CLI no longer knows how to uninstall it.`);
|
|
1132
|
+
kept.push(record);
|
|
1133
|
+
continue;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
const ok = await runHarnessJob(failures, record.harness, "uninstall", async () => {
|
|
1137
|
+
if (flags.forgetToken) {
|
|
1138
|
+
await forgetStoredAnthropicToken(record);
|
|
1139
|
+
} else if (record.anthropic_token_stored) {
|
|
1140
|
+
anthropicTokenNotice = true;
|
|
1141
|
+
}
|
|
1142
|
+
if (record.launch_available) await removePennyHarness(record.harness);
|
|
1143
|
+
if (!record.launch_available || record.default_routing) await harness.uninstall(record);
|
|
1144
|
+
console.log(`Removed ${record.harness}.`);
|
|
1145
|
+
});
|
|
1146
|
+
if (!ok) kept.push(record);
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
for (const record of selectedRecords) {
|
|
1150
|
+
if (records.some((existing) => existing.harness === record.harness)) continue;
|
|
1151
|
+
const harness = HARNESS_BY_ID[record.harness];
|
|
1152
|
+
if (!harness) {
|
|
1153
|
+
console.log(`Skipping ${record.harness}: this CLI does not know how to uninstall it.`);
|
|
1154
|
+
continue;
|
|
1155
|
+
}
|
|
1156
|
+
await runHarnessJob(failures, record.harness, "uninstall", async () => {
|
|
1157
|
+
if (flags.forgetToken) {
|
|
1158
|
+
await forgetStoredAnthropicToken(record);
|
|
1159
|
+
} else if (record.anthropic_token_stored) {
|
|
1160
|
+
anthropicTokenNotice = true;
|
|
1161
|
+
}
|
|
1162
|
+
if (record.launch_available) await removePennyHarness(record.harness);
|
|
1163
|
+
if (!record.launch_available || record.default_routing) await harness.uninstall(record);
|
|
1164
|
+
console.log(`Removed ${record.harness}.`);
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
if (anthropicTokenNotice) {
|
|
1169
|
+
console.log("");
|
|
1170
|
+
console.log("Note: Your stored Anthropic token remains on the server for other machines.");
|
|
1171
|
+
console.log("To delete it from the server, run: pennyrouter auth anthropic --forget");
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
manifest.installations = kept;
|
|
1175
|
+
await saveManifest(manifest);
|
|
1176
|
+
if (!(await hasConfiguredPennyHarnesses())) await removePennyRuntime();
|
|
1177
|
+
reportFailures(failures);
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
async function resolveLifecycleTargets(flags, records) {
|
|
1181
|
+
const requested = parseHarnessList(flags.harness);
|
|
1182
|
+
const ids = flags.all
|
|
1183
|
+
? Object.keys(HARNESS_BY_ID)
|
|
1184
|
+
: requested.length > 0
|
|
1185
|
+
? requested
|
|
1186
|
+
: records.length > 0
|
|
1187
|
+
? records.map((record) => record.harness)
|
|
1188
|
+
: await detectHarnessIds();
|
|
1189
|
+
|
|
1190
|
+
const selectedIds = [...new Set(ids)];
|
|
1191
|
+
const selectedRecords = records.filter((record) => selectedIds.includes(record.harness));
|
|
1192
|
+
const manifestIds = new Set(selectedRecords.map((record) => record.harness));
|
|
1193
|
+
|
|
1194
|
+
for (const id of selectedIds) {
|
|
1195
|
+
if (manifestIds.has(id)) continue;
|
|
1196
|
+
const harness = HARNESS_BY_ID[id];
|
|
1197
|
+
if (!harness) continue;
|
|
1198
|
+
if (!(await safeDetect(harness))) continue;
|
|
1199
|
+
let plan;
|
|
1200
|
+
try {
|
|
1201
|
+
plan = await harness.planInstall();
|
|
1202
|
+
} catch (error) {
|
|
1203
|
+
console.log(`Skipping ${id}: ${formatError(error)}`);
|
|
1204
|
+
continue;
|
|
1205
|
+
}
|
|
1206
|
+
selectedRecords.push({
|
|
1207
|
+
harness: id,
|
|
1208
|
+
config_path: plan.configPath ? compactHome(plan.configPath) : null,
|
|
1209
|
+
backup_path: null,
|
|
1210
|
+
installed_at: null,
|
|
1211
|
+
recovered_from_detected_config: true,
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
return { selectedIds, selectedRecords };
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
async function detectHarnessIds() {
|
|
1219
|
+
const ids = [];
|
|
1220
|
+
for (const harness of Object.values(HARNESS_BY_ID)) {
|
|
1221
|
+
if (await safeDetect(harness)) ids.push(harness.id);
|
|
1222
|
+
}
|
|
1223
|
+
return ids;
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
async function status() {
|
|
1227
|
+
const manifest = await loadManifest();
|
|
1228
|
+
const pruned = pruneUnsupportedManifestRecords(manifest);
|
|
1229
|
+
if (pruned.length > 0) await saveManifest(manifest);
|
|
1230
|
+
const records = manifest.installations || [];
|
|
1231
|
+
console.log("PennyRouter local setup");
|
|
1232
|
+
console.log("");
|
|
1233
|
+
|
|
1234
|
+
if (records.length === 0) {
|
|
1235
|
+
console.log("No local PennyRouter integrations recorded.");
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
for (const record of records) {
|
|
1240
|
+
const state = record.disabled ? "disabled" : record.manual_required ? "manual setup shown" : "installed";
|
|
1241
|
+
console.log(`${record.harness}: ${state}`);
|
|
1242
|
+
if (record.config_path) console.log(` config: ${record.config_path}`);
|
|
1243
|
+
if (record.backup_path) console.log(` backup: ${record.backup_path}`);
|
|
1244
|
+
if (record.disabled_snapshot_path) console.log(` disabled snapshot: ${record.disabled_snapshot_path}`);
|
|
1245
|
+
if (record.installed_at) console.log(` installed: ${record.installed_at}`);
|
|
1246
|
+
if (record.disabled_at) console.log(` disabled: ${record.disabled_at}`);
|
|
1247
|
+
if (record.anthropic_oauth) console.log(` Claude subscription auth: enabled (${record.token_source || "unknown"} token)`);
|
|
1248
|
+
if (record.openai_subscription_auth) console.log(" OpenAI subscription auth: enabled (Codex-managed login)");
|
|
1249
|
+
if (record.launch_available) {
|
|
1250
|
+
const command = record.harness === "claude-code" ? "penny claude" : "penny codex";
|
|
1251
|
+
console.log(` command: ${command}`);
|
|
1252
|
+
console.log(` bare command: ${record.default_routing ? "PennyRouter" : "native"}`);
|
|
1253
|
+
}
|
|
1254
|
+
if (record.manual_required) console.log(" note: finish or remove this integration in the tool UI");
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
async function selectHarnesses(flags) {
|
|
1259
|
+
const requested = flags.all ? Object.keys(HARNESS_BY_ID) : parseHarnessList(flags.harness);
|
|
1260
|
+
if (requested.length > 0) {
|
|
1261
|
+
return requested.map((id) => HARNESS_BY_ID[id]).filter(Boolean);
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
const detected = [];
|
|
1265
|
+
for (const id of DEFAULT_INSTALL_HARNESS_IDS) {
|
|
1266
|
+
const harness = HARNESS_BY_ID[id];
|
|
1267
|
+
if (await safeDetect(harness)) detected.push(harness);
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
if (flags.yes) return detected;
|
|
1271
|
+
|
|
1272
|
+
return promptHarnessCheckboxes(detected);
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
async function promptHarnessCheckboxes(detected) {
|
|
1276
|
+
const detectedIds = new Set(detected.map((harness) => harness.id));
|
|
1277
|
+
const selectedIds = new Set(detectedIds);
|
|
1278
|
+
const harnesses = orderedHarnessesForPrompt(detectedIds);
|
|
1279
|
+
if (!input.isTTY || !output.isTTY) return promptHarnessCheckboxesByText(harnesses, selectedIds);
|
|
1280
|
+
|
|
1281
|
+
return promptHarnessCheckboxesByKeys(harnesses, selectedIds);
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
async function promptHarnessCheckboxesByKeys(harnesses, selectedIds) {
|
|
1285
|
+
let cursor = 0;
|
|
1286
|
+
const previousRawMode = input.isRaw;
|
|
1287
|
+
|
|
1288
|
+
emitKeypressEvents(input);
|
|
1289
|
+
input.setRawMode(true);
|
|
1290
|
+
input.resume();
|
|
1291
|
+
output.write("\x1b[?25l");
|
|
1292
|
+
|
|
1293
|
+
const render = () => {
|
|
1294
|
+
output.write("\x1b[2J\x1b[H");
|
|
1295
|
+
output.write("\n\n");
|
|
1296
|
+
output.write(`${color("Where should PennyRouter be installed?", "bold")}\n\n\n`);
|
|
1297
|
+
harnesses.forEach((harness, index) => {
|
|
1298
|
+
const pointer = index === cursor ? color(">", "cyan") : " ";
|
|
1299
|
+
const checked = selectedIds.has(harness.id) ? color("x", "green") : " ";
|
|
1300
|
+
const name = index === cursor ? color(harness.name, "cyan") : harness.name;
|
|
1301
|
+
output.write(` ${pointer} [${checked}] ${name}\n`);
|
|
1302
|
+
});
|
|
1303
|
+
output.write("\nEnter to continue.\n");
|
|
1304
|
+
};
|
|
1305
|
+
|
|
1306
|
+
render();
|
|
1307
|
+
|
|
1308
|
+
return new Promise((resolve, reject) => {
|
|
1309
|
+
const cleanup = () => {
|
|
1310
|
+
input.off("keypress", onKeypress);
|
|
1311
|
+
input.setRawMode(Boolean(previousRawMode));
|
|
1312
|
+
input.pause();
|
|
1313
|
+
output.write("\x1b[?25h");
|
|
1314
|
+
output.write("\n");
|
|
1315
|
+
};
|
|
1316
|
+
|
|
1317
|
+
const onKeypress = (_str, key = {}) => {
|
|
1318
|
+
if (key.ctrl && key.name === "c") {
|
|
1319
|
+
cleanup();
|
|
1320
|
+
reject(new Error("Cancelled."));
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
if (key.name === "up" || key.name === "k") {
|
|
1325
|
+
cursor = (cursor - 1 + harnesses.length) % harnesses.length;
|
|
1326
|
+
render();
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
if (key.name === "down" || key.name === "j") {
|
|
1331
|
+
cursor = (cursor + 1) % harnesses.length;
|
|
1332
|
+
render();
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
if (key.name === "space") {
|
|
1337
|
+
const id = harnesses[cursor].id;
|
|
1338
|
+
if (selectedIds.has(id)) selectedIds.delete(id);
|
|
1339
|
+
else selectedIds.add(id);
|
|
1340
|
+
render();
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
if (key.name === "return" || key.name === "enter") {
|
|
1345
|
+
cleanup();
|
|
1346
|
+
resolve(harnesses.filter((harness) => selectedIds.has(harness.id)));
|
|
1347
|
+
}
|
|
1348
|
+
};
|
|
1349
|
+
|
|
1350
|
+
input.on("keypress", onKeypress);
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
async function promptHarnessCheckboxesByText(harnesses, selectedIds) {
|
|
1355
|
+
const rl = createInterface({ input, output });
|
|
1356
|
+
|
|
1357
|
+
try {
|
|
1358
|
+
while (true) {
|
|
1359
|
+
console.log("");
|
|
1360
|
+
console.log("");
|
|
1361
|
+
console.log(color("Where should PennyRouter be installed?", "bold"));
|
|
1362
|
+
for (const harness of harnesses) {
|
|
1363
|
+
const checked = selectedIds.has(harness.id) ? color("x", "green") : " ";
|
|
1364
|
+
console.log(` [${checked}] ${harness.name}`);
|
|
1365
|
+
}
|
|
1366
|
+
console.log("");
|
|
1367
|
+
const answer = await rl.question(
|
|
1368
|
+
"Press Enter to continue, or type names to toggle (comma-separated, 'all', or 'none'): ",
|
|
1369
|
+
);
|
|
1370
|
+
const value = answer.trim().toLowerCase();
|
|
1371
|
+
if (!value) return harnesses.filter((harness) => selectedIds.has(harness.id));
|
|
1372
|
+
if (value === "all") {
|
|
1373
|
+
for (const harness of harnesses) selectedIds.add(harness.id);
|
|
1374
|
+
} else if (value === "none") {
|
|
1375
|
+
selectedIds.clear();
|
|
1376
|
+
} else {
|
|
1377
|
+
for (const token of parseHarnessList(value)) {
|
|
1378
|
+
const id = resolveHarnessToken(token);
|
|
1379
|
+
if (!HARNESS_BY_ID[id]) {
|
|
1380
|
+
console.log(`Unknown tool "${token}".`);
|
|
1381
|
+
continue;
|
|
1382
|
+
}
|
|
1383
|
+
if (selectedIds.has(id)) selectedIds.delete(id);
|
|
1384
|
+
else selectedIds.add(id);
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
console.log("");
|
|
1388
|
+
}
|
|
1389
|
+
} finally {
|
|
1390
|
+
rl.close();
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
async function promptFundingProviderCheckboxes(providerOptions, title,
|
|
1395
|
+
initialIds = new Set(["subscription"])) {
|
|
1396
|
+
const selectedIds = new Set(initialIds);
|
|
1397
|
+
if (!input.isTTY || !output.isTTY) return selectedIds;
|
|
1398
|
+
|
|
1399
|
+
let cursor = 0;
|
|
1400
|
+
let validation = "";
|
|
1401
|
+
const previousRawMode = input.isRaw;
|
|
1402
|
+
emitKeypressEvents(input);
|
|
1403
|
+
input.setRawMode(true);
|
|
1404
|
+
input.resume();
|
|
1405
|
+
output.write("\x1b[?25l");
|
|
1406
|
+
|
|
1407
|
+
const render = () => {
|
|
1408
|
+
output.write("\x1b[2J\x1b[H");
|
|
1409
|
+
output.write("\n\n");
|
|
1410
|
+
output.write(`${color(title, "bold")}\n\n`);
|
|
1411
|
+
providerOptions.forEach((provider, index) => {
|
|
1412
|
+
const pointer = index === cursor ? color(">", "cyan") : " ";
|
|
1413
|
+
const checked = selectedIds.has(provider.id) ? color("x", "green") : " ";
|
|
1414
|
+
const name = index === cursor ? color(provider.name, "cyan") : provider.name;
|
|
1415
|
+
output.write(` ${pointer} [${checked}] ${name}\n`);
|
|
1416
|
+
});
|
|
1417
|
+
output.write("\nSpace to toggle · Enter to continue.\n");
|
|
1418
|
+
if (validation) output.write(`${validation}\n`);
|
|
1419
|
+
};
|
|
1420
|
+
render();
|
|
1421
|
+
|
|
1422
|
+
return new Promise((resolve, reject) => {
|
|
1423
|
+
const cleanup = () => {
|
|
1424
|
+
input.off("keypress", onKeypress);
|
|
1425
|
+
input.setRawMode(Boolean(previousRawMode));
|
|
1426
|
+
input.pause();
|
|
1427
|
+
output.write("\x1b[?25h\n");
|
|
1428
|
+
};
|
|
1429
|
+
const onKeypress = (_str, key = {}) => {
|
|
1430
|
+
if (key.ctrl && key.name === "c") {
|
|
1431
|
+
cleanup();
|
|
1432
|
+
reject(new Error("Cancelled."));
|
|
1433
|
+
} else if (key.name === "up" || key.name === "k") {
|
|
1434
|
+
cursor = (cursor - 1 + providerOptions.length) % providerOptions.length;
|
|
1435
|
+
validation = "";
|
|
1436
|
+
render();
|
|
1437
|
+
} else if (key.name === "down" || key.name === "j") {
|
|
1438
|
+
cursor = (cursor + 1) % providerOptions.length;
|
|
1439
|
+
validation = "";
|
|
1440
|
+
render();
|
|
1441
|
+
} else if (key.name === "space") {
|
|
1442
|
+
const id = providerOptions[cursor].id;
|
|
1443
|
+
if (selectedIds.has(id)) selectedIds.delete(id); else selectedIds.add(id);
|
|
1444
|
+
validation = "";
|
|
1445
|
+
render();
|
|
1446
|
+
} else if (key.name === "return" || key.name === "enter") {
|
|
1447
|
+
if (!selectedIds.size) {
|
|
1448
|
+
validation = "Select at least one provider.";
|
|
1449
|
+
render();
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
cleanup();
|
|
1453
|
+
resolve(selectedIds);
|
|
1454
|
+
}
|
|
1455
|
+
};
|
|
1456
|
+
input.on("keypress", onKeypress);
|
|
1457
|
+
});
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
function orderedHarnessesForPrompt(detectedIds) {
|
|
1461
|
+
return DEFAULT_INSTALL_HARNESS_IDS.map((id) => HARNESS_BY_ID[id]).sort((left, right) => {
|
|
1462
|
+
const leftDetected = detectedIds.has(left.id);
|
|
1463
|
+
const rightDetected = detectedIds.has(right.id);
|
|
1464
|
+
if (leftDetected !== rightDetected) return leftDetected ? -1 : 1;
|
|
1465
|
+
return left.name.localeCompare(right.name);
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
function resolveHarnessToken(token) {
|
|
1470
|
+
const normalized = normalizeHarnessToken(token);
|
|
1471
|
+
for (const harness of Object.values(HARNESS_BY_ID)) {
|
|
1472
|
+
if (normalizeHarnessToken(harness.id) === normalized) return harness.id;
|
|
1473
|
+
if (normalizeHarnessToken(harness.name) === normalized) return harness.id;
|
|
1474
|
+
}
|
|
1475
|
+
return token;
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
function normalizeHarnessToken(value) {
|
|
1479
|
+
return String(value).toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
function color(text, style) {
|
|
1483
|
+
if (!output.isTTY || process.env.NO_COLOR) return text;
|
|
1484
|
+
const codes = {
|
|
1485
|
+
bold: ["\x1b[1m", "\x1b[22m"],
|
|
1486
|
+
cyan: ["\x1b[36m", "\x1b[39m"],
|
|
1487
|
+
green: ["\x1b[32m", "\x1b[39m"],
|
|
1488
|
+
};
|
|
1489
|
+
const pair = codes[style];
|
|
1490
|
+
return pair ? `${pair[0]}${text}${pair[1]}` : text;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
function parseHarnessList(value = "") {
|
|
1494
|
+
return value
|
|
1495
|
+
.split(",")
|
|
1496
|
+
.map((part) => part.trim())
|
|
1497
|
+
.filter(Boolean);
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
function resolveRequestedHarnessIds(flags, defaults) {
|
|
1501
|
+
if (flags.all) return defaults;
|
|
1502
|
+
const requested = parseHarnessList(flags.harness);
|
|
1503
|
+
return requested.length > 0 ? requested : defaults;
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
function upsertManifestRecord(manifest, record) {
|
|
1507
|
+
manifest.installations ||= [];
|
|
1508
|
+
manifest.installations = manifest.installations.filter((existing) => existing.harness !== record.harness);
|
|
1509
|
+
manifest.installations.push(record);
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
function pruneUnsupportedManifestRecords(manifest) {
|
|
1513
|
+
manifest.installations ||= [];
|
|
1514
|
+
const kept = manifest.installations.filter((record) => HARNESS_BY_ID[record.harness]);
|
|
1515
|
+
const pruned = manifest.installations.filter((record) => !HARNESS_BY_ID[record.harness]);
|
|
1516
|
+
manifest.installations = kept;
|
|
1517
|
+
return pruned;
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
async function safeDetect(harness) {
|
|
1521
|
+
try {
|
|
1522
|
+
return await harness.detect();
|
|
1523
|
+
} catch (error) {
|
|
1524
|
+
console.log(`Skipping ${harness.id}: detect failed: ${formatError(error)}`);
|
|
1525
|
+
return false;
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
async function runHarnessJob(failures, harnessId, action, job) {
|
|
1530
|
+
try {
|
|
1531
|
+
await job();
|
|
1532
|
+
return true;
|
|
1533
|
+
} catch (error) {
|
|
1534
|
+
failures.push({ harness: harnessId, action, error });
|
|
1535
|
+
console.error(`${harnessId} ${action} failed: ${formatError(error)}`);
|
|
1536
|
+
return false;
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
function reportFailures(failures) {
|
|
1541
|
+
if (failures.length === 0) return;
|
|
1542
|
+
console.error("");
|
|
1543
|
+
console.error("Some PennyRouter integrations failed:");
|
|
1544
|
+
for (const failure of failures) {
|
|
1545
|
+
console.error(` - ${failure.harness} ${failure.action}: ${formatError(failure.error)}`);
|
|
1546
|
+
}
|
|
1547
|
+
process.exitCode = 1;
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
function formatError(error) {
|
|
1551
|
+
return error?.message || String(error);
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
// Best-effort: tell the gateway to forget a stored Claude token when uninstalling a
|
|
1555
|
+
// claude-code integration that stored one. Reads the gateway URL + pr- key from the
|
|
1556
|
+
// live config BEFORE harness.uninstall strips them. Never throws — a local uninstall
|
|
1557
|
+
// must not be blocked by a network hiccup; the record is going away regardless.
|
|
1558
|
+
async function forgetStoredAnthropicToken(record) {
|
|
1559
|
+
if (record.harness !== "claude-code" || !record.anthropic_token_stored) return;
|
|
1560
|
+
try {
|
|
1561
|
+
const launchConfig = await loadPennyConfig();
|
|
1562
|
+
const globalAuth = await readClaudeCodeGatewayAuth();
|
|
1563
|
+
const gatewayBaseUrl = launchConfig.gateway_base_url || globalAuth.gatewayBaseUrl;
|
|
1564
|
+
const apiKey = launchConfig.api_key || globalAuth.apiKey;
|
|
1565
|
+
if (!gatewayBaseUrl || !apiKey) return;
|
|
1566
|
+
await forgetAnthropicToken({ gatewayBaseUrl, apiKey });
|
|
1567
|
+
} catch {
|
|
1568
|
+
// swallow — see note above
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
async function confirm(question) {
|
|
1573
|
+
const rl = createInterface({ input, output });
|
|
1574
|
+
const answer = await rl.question(`${question} [y/N] `);
|
|
1575
|
+
rl.close();
|
|
1576
|
+
return answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes";
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
async function promptSecretish(question) {
|
|
1580
|
+
if (!input.isTTY || !output.isTTY) {
|
|
1581
|
+
const rl = createInterface({ input, output });
|
|
1582
|
+
const answer = await rl.question(question);
|
|
1583
|
+
rl.close();
|
|
1584
|
+
return answer.trim();
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
const previousRawMode = input.isRaw;
|
|
1588
|
+
emitKeypressEvents(input);
|
|
1589
|
+
input.setRawMode(true);
|
|
1590
|
+
input.resume();
|
|
1591
|
+
output.write(question);
|
|
1592
|
+
|
|
1593
|
+
return new Promise((resolve, reject) => {
|
|
1594
|
+
let value = "";
|
|
1595
|
+
const cleanup = () => {
|
|
1596
|
+
input.off("keypress", onKeypress);
|
|
1597
|
+
input.setRawMode(Boolean(previousRawMode));
|
|
1598
|
+
input.pause();
|
|
1599
|
+
output.write("\n");
|
|
1600
|
+
};
|
|
1601
|
+
const onKeypress = (str, key = {}) => {
|
|
1602
|
+
if (key.ctrl && key.name === "c") {
|
|
1603
|
+
cleanup();
|
|
1604
|
+
reject(new Error("Cancelled."));
|
|
1605
|
+
return;
|
|
1606
|
+
}
|
|
1607
|
+
if (key.name === "return" || key.name === "enter") {
|
|
1608
|
+
cleanup();
|
|
1609
|
+
resolve(value.trim());
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1612
|
+
if (key.name === "backspace") {
|
|
1613
|
+
value = value.slice(0, -1);
|
|
1614
|
+
return;
|
|
1615
|
+
}
|
|
1616
|
+
if (str) value += str;
|
|
1617
|
+
};
|
|
1618
|
+
input.on("keypress", onKeypress);
|
|
1619
|
+
});
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
function commandExists(command) {
|
|
1623
|
+
// Windows installs CLIs as `.cmd`/`.ps1` shims that Node won't resolve without a
|
|
1624
|
+
// shell, so probe through the shell there (else this false-negatives on `claude` and
|
|
1625
|
+
// the auto setup-token path gets skipped). `command` is an internal literal.
|
|
1626
|
+
const result = spawnSync(command, ["--version"], {
|
|
1627
|
+
stdio: "ignore",
|
|
1628
|
+
shell: process.platform === "win32",
|
|
1629
|
+
});
|
|
1630
|
+
return result.status === 0;
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
function installNoteForRecord(record) {
|
|
1634
|
+
if (record.manual_required) return " (manual setup shown)";
|
|
1635
|
+
if (record.anthropic_oauth) return " (Claude subscription auth)";
|
|
1636
|
+
return installNoteForHarness(record.harness);
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
function installNoteForHarness(harnessId) {
|
|
1640
|
+
if (harnessId === "cline" || harnessId === "codegpt") return " (manual setup)";
|
|
1641
|
+
if (harnessId === "opencode") return " (select a Penny model from /model)";
|
|
1642
|
+
return " (auto-selected)";
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
function openBrowser(url) {
|
|
1646
|
+
const platform = process.platform;
|
|
1647
|
+
const command =
|
|
1648
|
+
platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
|
|
1649
|
+
const args = platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
1650
|
+
spawnSync(command, args, { stdio: "ignore", detached: true });
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
function runningClaudeCodeProcesses() {
|
|
1654
|
+
if (process.platform === "win32") {
|
|
1655
|
+
const result = spawnSync("tasklist", ["/FO", "CSV", "/NH"], { encoding: "utf8" });
|
|
1656
|
+
if (result.status !== 0) return [];
|
|
1657
|
+
return String(result.stdout || "").split(/\r?\n/)
|
|
1658
|
+
.filter((line) => /^"claude(?:\.exe)?"/i.test(line.trim()));
|
|
1659
|
+
}
|
|
1660
|
+
const result = spawnSync("ps", ["-ax", "-o", "pid=,command="], { encoding: "utf8" });
|
|
1661
|
+
if (result.status !== 0) return [];
|
|
1662
|
+
return String(result.stdout || "").split(/\r?\n/)
|
|
1663
|
+
.map((line) => line.trim())
|
|
1664
|
+
// Case-sensitive on purpose: match the Claude Code executable, not Claude.app.
|
|
1665
|
+
.filter((line) => /(^|[\/\s])claude(?:\s|$)/.test(line));
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
function runningCodexProcesses() {
|
|
1669
|
+
if (process.platform === "win32") return [];
|
|
1670
|
+
const result = spawnSync("ps", ["-ax", "-o", "pid=,command="], { encoding: "utf8" });
|
|
1671
|
+
if (result.status !== 0) return [];
|
|
1672
|
+
return String(result.stdout || "").split(/\r?\n/)
|
|
1673
|
+
.map((line) => line.trim())
|
|
1674
|
+
.filter((line) => /(^|[\/\s])codex(?:\s|$)/.test(line));
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
function assertNoRunningHarness(harnessId, flags) {
|
|
1678
|
+
if (flags.allowRunningHarnesses || (harnessId === "claude-code" && flags.allowRunningClaude)) {
|
|
1679
|
+
return;
|
|
1680
|
+
}
|
|
1681
|
+
const running = harnessId === "claude-code"
|
|
1682
|
+
? runningClaudeCodeProcesses()
|
|
1683
|
+
: runningCodexProcesses();
|
|
1684
|
+
if (running.length === 0) return;
|
|
1685
|
+
const name = harnessId === "claude-code" ? "Claude Code" : "Codex";
|
|
1686
|
+
throw new Error(
|
|
1687
|
+
`${name} is currently running. Close all ${name} sessions before changing its global routing. `
|
|
1688
|
+
+ "Installing only the explicit `penny` command does not require this. If you intentionally "
|
|
1689
|
+
+ "accept the risk, pass --allow-running-harnesses.\nDetected:\n "
|
|
1690
|
+
+ running.slice(0, 5).join("\n "),
|
|
1691
|
+
);
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
function printHelp() {
|
|
1695
|
+
console.log(`PennyRouter CLI
|
|
1696
|
+
|
|
1697
|
+
Usage:
|
|
1698
|
+
pennyrouter install [--harness ${SUPPORTED_HARNESS_IDS}] [--all] [--yes] [--dry-run] [--make-default|--penny-only] [--no-path-update] [--anthropic-auth|--no-anthropic-auth] [--anthropic-api-key KEY] [--openai-api-key KEY] [--openrouter-api-key KEY] [--local[=URL]] [--allow-running-harnesses]
|
|
1699
|
+
pennyrouter update [--harness ${SUPPORTED_HARNESS_IDS}] [--all] [--dry-run] [--penny-key pr-...] [--gateway-base-url URL] [--allow-running-harnesses]
|
|
1700
|
+
pennyrouter auth anthropic [--penny-key pr-...] [--token oauth-token] [--token-command CMD] [--gateway-base-url URL] [--forget]
|
|
1701
|
+
pennyrouter disable [--harness ${SUPPORTED_HARNESS_IDS}] [--all] [--yes]
|
|
1702
|
+
pennyrouter enable [--harness ${SUPPORTED_HARNESS_IDS}] [--all] [--yes]
|
|
1703
|
+
pennyrouter uninstall [--harness ${SUPPORTED_HARNESS_IDS}] [--all] [--yes] [--forget-token]
|
|
1704
|
+
pennyrouter status
|
|
1705
|
+
|
|
1706
|
+
Update refreshes the managed local config and the penny runtime to this package version.
|
|
1707
|
+
It reuses your existing key and never re-asks for subscription or API credentials.
|
|
1708
|
+
|
|
1709
|
+
Examples:
|
|
1710
|
+
npx pennyrouter update
|
|
1711
|
+
npx pennyrouter update --dry-run
|
|
1712
|
+
npx pennyrouter update --harness claude-code
|
|
1713
|
+
npx pennyrouter install
|
|
1714
|
+
npx pennyrouter install --make-default
|
|
1715
|
+
npx pennyrouter install --penny-only
|
|
1716
|
+
npx pennyrouter install --harness claude-code --anthropic-auth --token <token>
|
|
1717
|
+
npx pennyrouter install --harness claude-code --anthropic-api-key <key>
|
|
1718
|
+
npx pennyrouter install --harness codex --openai-api-key <key>
|
|
1719
|
+
npx pennyrouter install --openrouter-api-key <key>
|
|
1720
|
+
npx pennyrouter auth anthropic
|
|
1721
|
+
npx pennyrouter install --all
|
|
1722
|
+
npx pennyrouter install --harness windsurf,zed,codegpt,aider --yes
|
|
1723
|
+
npx pennyrouter disable
|
|
1724
|
+
npx pennyrouter enable
|
|
1725
|
+
npx pennyrouter uninstall
|
|
1726
|
+
`);
|
|
1727
|
+
}
|