@tokenbuddy/tokenbuddy 1.0.4 â 1.0.6
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/dist/src/buyer-store.d.ts +20 -0
- package/dist/src/buyer-store.d.ts.map +1 -1
- package/dist/src/buyer-store.js +73 -1
- package/dist/src/buyer-store.js.map +1 -1
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +390 -62
- package/dist/src/cli.js.map +1 -1
- package/dist/src/daemon.d.ts +6 -5
- package/dist/src/daemon.d.ts.map +1 -1
- package/dist/src/daemon.js +298 -92
- package/dist/src/daemon.js.map +1 -1
- package/dist/src/doctor-diagnostics.d.ts +97 -0
- package/dist/src/doctor-diagnostics.d.ts.map +1 -0
- package/dist/src/doctor-diagnostics.js +547 -0
- package/dist/src/doctor-diagnostics.js.map +1 -0
- package/dist/src/init-payment-options.d.ts +34 -0
- package/dist/src/init-payment-options.d.ts.map +1 -0
- package/dist/src/init-payment-options.js +90 -0
- package/dist/src/init-payment-options.js.map +1 -0
- package/dist/src/provider-install.d.ts +37 -2
- package/dist/src/provider-install.d.ts.map +1 -1
- package/dist/src/provider-install.js +317 -67
- package/dist/src/provider-install.js.map +1 -1
- package/dist/src/seller-catalog.d.ts +79 -0
- package/dist/src/seller-catalog.d.ts.map +1 -0
- package/dist/src/seller-catalog.js +126 -0
- package/dist/src/seller-catalog.js.map +1 -0
- package/dist/src/tb-proxyd.js +13 -2
- package/dist/src/tb-proxyd.js.map +1 -1
- package/package.json +4 -4
- package/src/buyer-store.ts +113 -1
- package/src/cli.ts +490 -67
- package/src/daemon.ts +346 -117
- package/src/doctor-diagnostics.ts +850 -0
- package/src/init-payment-options.ts +131 -0
- package/src/provider-install.ts +426 -76
- package/src/seller-catalog.ts +222 -0
- package/src/tb-proxyd.ts +14 -2
- package/tests/e2e.test.ts +9 -0
- package/tests/tokenbuddy.test.ts +628 -19
- package/bin/tb-proxyd.js +0 -2
- package/bin/tb.js +0 -3
package/src/cli.ts
CHANGED
|
@@ -6,10 +6,45 @@ import * as os from "os";
|
|
|
6
6
|
import { execSync, spawn } from "child_process";
|
|
7
7
|
import Table from "cli-table3";
|
|
8
8
|
import { BuyerStore, PaymentConfig } from "./buyer-store.js";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
applyProviderInstall,
|
|
11
|
+
detectProviders,
|
|
12
|
+
getProviderModelSelectionKind,
|
|
13
|
+
getProviderProtocolPreference,
|
|
14
|
+
type ClaudeCodeModelMappingConfig,
|
|
15
|
+
type ProviderId,
|
|
16
|
+
type ProviderSelections,
|
|
17
|
+
type SingleModelProviderRuntimeConfig,
|
|
18
|
+
} from "./provider-install.js";
|
|
10
19
|
import { createModuleLogger } from "@tokenbuddy/logging";
|
|
11
20
|
import * as crypto from "crypto";
|
|
12
21
|
import { fileURLToPath } from "url";
|
|
22
|
+
import {
|
|
23
|
+
discoverSellerBackedModels,
|
|
24
|
+
filterCatalogByProtocol,
|
|
25
|
+
filterCatalogBySeller,
|
|
26
|
+
type ModelCatalogEntry,
|
|
27
|
+
type SellerCatalogResult,
|
|
28
|
+
type SellerRoutingPreference,
|
|
29
|
+
} from "./seller-catalog.js";
|
|
30
|
+
import {
|
|
31
|
+
collectDoctorDiagnostics,
|
|
32
|
+
collectDoctorModelsSummary,
|
|
33
|
+
printDoctorProviders,
|
|
34
|
+
printDoctorModelsSummary,
|
|
35
|
+
readDoctorProviders,
|
|
36
|
+
renderDoctorDiagnosticsProgressively,
|
|
37
|
+
} from "./doctor-diagnostics.js";
|
|
38
|
+
import {
|
|
39
|
+
buildInitTerminalOptions,
|
|
40
|
+
buildInitSuccessMessage,
|
|
41
|
+
detectExistingClawtipBinding,
|
|
42
|
+
INIT_PAYMENT_OPTIONS,
|
|
43
|
+
noteInitComingSoonPayments,
|
|
44
|
+
OTHER_TERMINAL_OPTION,
|
|
45
|
+
type InitPaymentMethod,
|
|
46
|
+
validateInitTerminalSelection,
|
|
47
|
+
} from "./init-payment-options.js";
|
|
13
48
|
|
|
14
49
|
// @ts-ignore
|
|
15
50
|
import qrcode from "qrcode-terminal";
|
|
@@ -53,6 +88,12 @@ interface ClawtipBootstrapResponse {
|
|
|
53
88
|
};
|
|
54
89
|
}
|
|
55
90
|
|
|
91
|
+
interface SelectOption {
|
|
92
|
+
value: string;
|
|
93
|
+
label: string;
|
|
94
|
+
hint?: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
56
97
|
function isSupportedPaymentMethod(method: string): method is SupportedPaymentMethod {
|
|
57
98
|
return (SUPPORTED_PAYMENT_METHODS as readonly string[]).includes(method);
|
|
58
99
|
}
|
|
@@ -328,6 +369,251 @@ function readProof(options: { proofFile?: string; requireProof?: boolean }): str
|
|
|
328
369
|
return proof;
|
|
329
370
|
}
|
|
330
371
|
|
|
372
|
+
function sellerRegistryUrlForInit(): string {
|
|
373
|
+
return process.env.TB_PROXYD_SELLER_REGISTRY_URL || "https://tb-wallet-bootstrap.fly.dev/registry/sellers";
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function stableModelChoices(models: ModelCatalogEntry[]): SelectOption[] {
|
|
377
|
+
const grouped = new Map<string, ModelCatalogEntry[]>();
|
|
378
|
+
for (const entry of models) {
|
|
379
|
+
const list = grouped.get(entry.id) || [];
|
|
380
|
+
list.push(entry);
|
|
381
|
+
grouped.set(entry.id, list);
|
|
382
|
+
}
|
|
383
|
+
return Array.from(grouped.entries()).map(([modelId, entries]) => {
|
|
384
|
+
const sellerIds = Array.from(new Set(entries.map((entry) => entry.sellerId)));
|
|
385
|
+
const protocols = Array.from(
|
|
386
|
+
new Set(entries.flatMap((entry) => entry.supportedProtocols)),
|
|
387
|
+
);
|
|
388
|
+
return {
|
|
389
|
+
value: modelId,
|
|
390
|
+
label: modelId,
|
|
391
|
+
hint: `${sellerIds.join(",")} · ${protocols.join(",") || "no-protocol"}`,
|
|
392
|
+
};
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async function promptSellerRoutingPreference(catalog: SellerCatalogResult): Promise<SellerRoutingPreference> {
|
|
397
|
+
const healthySellers = catalog.sellers.filter((seller) => seller.status === "ok");
|
|
398
|
+
const mode = await p.select({
|
|
399
|
+
message: "Choose seller routing mode for tb-proxyd:",
|
|
400
|
+
options: [
|
|
401
|
+
{
|
|
402
|
+
value: "auto",
|
|
403
|
+
label: "Auto",
|
|
404
|
+
hint: "Automatically choose a compatible seller based on the requested model.",
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
value: "fixed",
|
|
408
|
+
label: "Fixed Seller",
|
|
409
|
+
hint: "Pin tb-proxyd to one seller and only use models from that seller.",
|
|
410
|
+
},
|
|
411
|
+
],
|
|
412
|
+
}) as SellerRoutingPreference["mode"] | symbol;
|
|
413
|
+
|
|
414
|
+
if (typeof mode !== "string") {
|
|
415
|
+
throw new Error("seller routing selection was cancelled");
|
|
416
|
+
}
|
|
417
|
+
if (mode === "auto") {
|
|
418
|
+
return { mode };
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (healthySellers.length === 0) {
|
|
422
|
+
throw new Error("no healthy sellers available for fixed routing");
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const sellerId = await p.select({
|
|
426
|
+
message: "Choose the seller to pin tb-proxyd to:",
|
|
427
|
+
options: healthySellers.map((seller) => ({
|
|
428
|
+
value: seller.id,
|
|
429
|
+
label: seller.name ? `${seller.name} (${seller.id})` : seller.id,
|
|
430
|
+
hint: [
|
|
431
|
+
seller.discountRatio != null ? `discount x${seller.discountRatio}` : null,
|
|
432
|
+
seller.modelCount != null ? `${seller.modelCount} models` : null,
|
|
433
|
+
seller.supportedProtocols?.length ? seller.supportedProtocols.join(",") : null,
|
|
434
|
+
seller.paymentMethods?.length ? seller.paymentMethods.join(",") : null,
|
|
435
|
+
]
|
|
436
|
+
.filter(Boolean)
|
|
437
|
+
.join(" · ") || seller.url,
|
|
438
|
+
})),
|
|
439
|
+
}) as string | symbol;
|
|
440
|
+
|
|
441
|
+
if (typeof sellerId !== "string") {
|
|
442
|
+
throw new Error("fixed seller selection was cancelled");
|
|
443
|
+
}
|
|
444
|
+
return {
|
|
445
|
+
mode,
|
|
446
|
+
sellerId,
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
async function promptSingleModelSelection(
|
|
451
|
+
providerId: ProviderId,
|
|
452
|
+
models: ModelCatalogEntry[],
|
|
453
|
+
sellerRouting: SellerRoutingPreference,
|
|
454
|
+
): Promise<SingleModelProviderRuntimeConfig> {
|
|
455
|
+
const protocolPreference = getProviderProtocolPreference(providerId);
|
|
456
|
+
const protocolFiltered = protocolPreference
|
|
457
|
+
? filterCatalogByProtocol(models, protocolPreference)
|
|
458
|
+
: models;
|
|
459
|
+
const choices = stableModelChoices(protocolFiltered);
|
|
460
|
+
if (choices.length === 0) {
|
|
461
|
+
throw new Error(`no compatible models available for ${providerId}`);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const labelMap: Record<string, string> = {
|
|
465
|
+
opencode: "OpenCode",
|
|
466
|
+
codex: "Codex",
|
|
467
|
+
openclaw: "OpenClaw",
|
|
468
|
+
hermes: "Hermes",
|
|
469
|
+
"claude-desktop": "Claude Desktop",
|
|
470
|
+
"claude-code": "Claude Code",
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
const selectedModel = await p.select({
|
|
474
|
+
message: `Choose the default model for ${labelMap[providerId] || providerId}:`,
|
|
475
|
+
options: choices,
|
|
476
|
+
}) as string | symbol;
|
|
477
|
+
|
|
478
|
+
if (typeof selectedModel !== "string") {
|
|
479
|
+
throw new Error(`default model selection was cancelled for ${providerId}`);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const selectedEntry = protocolFiltered.find((entry) => entry.id === selectedModel);
|
|
483
|
+
return {
|
|
484
|
+
selectionKind: "single-model",
|
|
485
|
+
protocolPreference,
|
|
486
|
+
defaultModel: selectedModel,
|
|
487
|
+
sellerId: sellerRouting.mode === "fixed" ? selectedEntry?.sellerId : undefined,
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function defaultClaudeDisplayName(modelId: string): string {
|
|
492
|
+
return modelId.trim();
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function makeClaudeRoleMapping(modelId: string): ClaudeCodeModelMappingConfig {
|
|
496
|
+
const displayName = defaultClaudeDisplayName(modelId);
|
|
497
|
+
return {
|
|
498
|
+
selectionKind: "claude-role-mapping",
|
|
499
|
+
protocolPreference: "messages",
|
|
500
|
+
fallbackModel: modelId,
|
|
501
|
+
roles: {
|
|
502
|
+
sonnet: {
|
|
503
|
+
upstreamModel: modelId,
|
|
504
|
+
displayName,
|
|
505
|
+
declareOneM: true,
|
|
506
|
+
},
|
|
507
|
+
opus: {
|
|
508
|
+
upstreamModel: modelId,
|
|
509
|
+
displayName,
|
|
510
|
+
declareOneM: true,
|
|
511
|
+
},
|
|
512
|
+
haiku: {
|
|
513
|
+
upstreamModel: modelId,
|
|
514
|
+
displayName,
|
|
515
|
+
declareOneM: false,
|
|
516
|
+
},
|
|
517
|
+
},
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
async function promptClaudeCodeModelSelection(
|
|
522
|
+
models: ModelCatalogEntry[],
|
|
523
|
+
): Promise<ClaudeCodeModelMappingConfig> {
|
|
524
|
+
const protocolFiltered = filterCatalogByProtocol(models, "messages");
|
|
525
|
+
const choices = stableModelChoices(protocolFiltered);
|
|
526
|
+
if (choices.length === 0) {
|
|
527
|
+
throw new Error("no compatible message models available for Claude Code");
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const sonnetModel = await p.select({
|
|
531
|
+
message: "Choose the default Sonnet model for Claude Code:",
|
|
532
|
+
options: choices,
|
|
533
|
+
}) as string | symbol;
|
|
534
|
+
|
|
535
|
+
if (typeof sonnetModel !== "string") {
|
|
536
|
+
throw new Error("Claude Code model selection was cancelled");
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const mirrorAllRoles = await p.confirm({
|
|
540
|
+
message: "Use the same model for Opus and Haiku as well?",
|
|
541
|
+
initialValue: true,
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
if (typeof mirrorAllRoles !== "boolean") {
|
|
545
|
+
throw new Error("Claude Code role mapping confirmation was cancelled");
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
if (mirrorAllRoles) {
|
|
549
|
+
return makeClaudeRoleMapping(sonnetModel);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const opusModel = await p.select({
|
|
553
|
+
message: "Choose the default Opus model for Claude Code:",
|
|
554
|
+
options: choices,
|
|
555
|
+
}) as string | symbol;
|
|
556
|
+
if (typeof opusModel !== "string") {
|
|
557
|
+
throw new Error("Claude Code Opus model selection was cancelled");
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const haikuModel = await p.select({
|
|
561
|
+
message: "Choose the default Haiku model for Claude Code:",
|
|
562
|
+
options: choices,
|
|
563
|
+
}) as string | symbol;
|
|
564
|
+
if (typeof haikuModel !== "string") {
|
|
565
|
+
throw new Error("Claude Code Haiku model selection was cancelled");
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
return {
|
|
569
|
+
selectionKind: "claude-role-mapping",
|
|
570
|
+
protocolPreference: "messages",
|
|
571
|
+
fallbackModel: sonnetModel,
|
|
572
|
+
roles: {
|
|
573
|
+
sonnet: {
|
|
574
|
+
upstreamModel: sonnetModel,
|
|
575
|
+
displayName: defaultClaudeDisplayName(sonnetModel),
|
|
576
|
+
declareOneM: true,
|
|
577
|
+
},
|
|
578
|
+
opus: {
|
|
579
|
+
upstreamModel: opusModel,
|
|
580
|
+
displayName: defaultClaudeDisplayName(opusModel),
|
|
581
|
+
declareOneM: true,
|
|
582
|
+
},
|
|
583
|
+
haiku: {
|
|
584
|
+
upstreamModel: haikuModel,
|
|
585
|
+
displayName: defaultClaudeDisplayName(haikuModel),
|
|
586
|
+
declareOneM: false,
|
|
587
|
+
},
|
|
588
|
+
},
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
async function promptProviderSelections(
|
|
593
|
+
providerIds: ProviderId[],
|
|
594
|
+
catalog: SellerCatalogResult,
|
|
595
|
+
sellerRouting: SellerRoutingPreference,
|
|
596
|
+
): Promise<ProviderSelections> {
|
|
597
|
+
const baseModels = sellerRouting.mode === "fixed"
|
|
598
|
+
? filterCatalogBySeller(catalog.models, sellerRouting.sellerId)
|
|
599
|
+
: catalog.models;
|
|
600
|
+
|
|
601
|
+
const selections: ProviderSelections = {};
|
|
602
|
+
for (const providerId of providerIds) {
|
|
603
|
+
const selectionKind = getProviderModelSelectionKind(providerId);
|
|
604
|
+
if (selectionKind === "claude-role-mapping") {
|
|
605
|
+
selections[providerId] = await promptClaudeCodeModelSelection(baseModels);
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
selections[providerId] = await promptSingleModelSelection(
|
|
609
|
+
providerId,
|
|
610
|
+
baseModels,
|
|
611
|
+
sellerRouting,
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
return selections;
|
|
615
|
+
}
|
|
616
|
+
|
|
331
617
|
export function buildCli(): Command {
|
|
332
618
|
const program = new Command();
|
|
333
619
|
program
|
|
@@ -352,7 +638,6 @@ export function buildCli(): Command {
|
|
|
352
638
|
const plistPath = process.platform === "darwin"
|
|
353
639
|
? path.join(os.homedir(), "Library", "LaunchAgents", "com.tokenbuddy.proxyd.plist")
|
|
354
640
|
: undefined;
|
|
355
|
-
const candidates = detectProviders();
|
|
356
641
|
let probe = await probeDaemonStatus(controlPort);
|
|
357
642
|
let repair: DaemonRepairResult = { attempted: false, fixed: false };
|
|
358
643
|
if (!probe.running && options.fix) {
|
|
@@ -363,11 +648,23 @@ export function buildCli(): Command {
|
|
|
363
648
|
const daemonInfo = probe.status;
|
|
364
649
|
const daemonRunning = probe.running;
|
|
365
650
|
const daemonError = probe.error;
|
|
651
|
+
const daemonStatus = daemonInfo && typeof daemonInfo === "object"
|
|
652
|
+
? daemonInfo as { selectionMode?: string; sellerRoutingMode?: string; selectedSellerId?: string; sellerRegistryUrl?: string }
|
|
653
|
+
: undefined;
|
|
654
|
+
const providers = readDoctorProviders();
|
|
366
655
|
if (options.fix && repair.attempted && !repair.fixed) {
|
|
367
656
|
process.exitCode = 1;
|
|
368
657
|
}
|
|
369
658
|
|
|
370
659
|
if (options.json) {
|
|
660
|
+
const diagnostics = await collectDoctorDiagnostics({
|
|
661
|
+
controlPort,
|
|
662
|
+
proxyPort,
|
|
663
|
+
daemonRunning,
|
|
664
|
+
daemonError,
|
|
665
|
+
providers,
|
|
666
|
+
sellerRegistryUrl: daemonStatus?.sellerRegistryUrl,
|
|
667
|
+
});
|
|
371
668
|
console.log(JSON.stringify({
|
|
372
669
|
daemon: {
|
|
373
670
|
running: daemonRunning,
|
|
@@ -387,7 +684,7 @@ export function buildCli(): Command {
|
|
|
387
684
|
plistPath,
|
|
388
685
|
plistExists: plistPath ? fs.existsSync(plistPath) : false
|
|
389
686
|
},
|
|
390
|
-
|
|
687
|
+
...diagnostics,
|
|
391
688
|
}, null, 2));
|
|
392
689
|
return;
|
|
393
690
|
}
|
|
@@ -422,12 +719,29 @@ export function buildCli(): Command {
|
|
|
422
719
|
}
|
|
423
720
|
}
|
|
424
721
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
722
|
+
if (daemonStatus) {
|
|
723
|
+
console.log(` Control Plane URL: ${controlUrl}`);
|
|
724
|
+
console.log(` Proxy Plane URL: http://127.0.0.1:${proxyPort}`);
|
|
725
|
+
if (daemonStatus.sellerRoutingMode || daemonStatus.selectionMode) {
|
|
726
|
+
console.log(` Routing Mode: ${daemonStatus.sellerRoutingMode || daemonStatus.selectionMode}`);
|
|
727
|
+
}
|
|
728
|
+
if (daemonStatus.selectedSellerId) {
|
|
729
|
+
console.log(` Selected Seller: ${daemonStatus.selectedSellerId}`);
|
|
730
|
+
}
|
|
731
|
+
if (daemonStatus.sellerRegistryUrl) {
|
|
732
|
+
console.log(` Registry URL: ${daemonStatus.sellerRegistryUrl}`);
|
|
733
|
+
}
|
|
430
734
|
}
|
|
735
|
+
|
|
736
|
+
printDoctorProviders(providers);
|
|
737
|
+
await renderDoctorDiagnosticsProgressively({
|
|
738
|
+
controlPort,
|
|
739
|
+
proxyPort,
|
|
740
|
+
daemonRunning,
|
|
741
|
+
daemonError,
|
|
742
|
+
sellerRegistryUrl: daemonStatus?.sellerRegistryUrl,
|
|
743
|
+
providers,
|
|
744
|
+
});
|
|
431
745
|
});
|
|
432
746
|
|
|
433
747
|
// 2. tb payment
|
|
@@ -555,22 +869,32 @@ export function buildCli(): Command {
|
|
|
555
869
|
.option("--json", "Output model list as JSON")
|
|
556
870
|
.action(async (options: { json?: boolean }) => {
|
|
557
871
|
try {
|
|
872
|
+
const controlPort = configuredControlPort();
|
|
873
|
+
const proxyPort = configuredProxyPort();
|
|
874
|
+
const status = await probeDaemonStatus(controlPort);
|
|
875
|
+
const daemonInfo = status.status && typeof status.status === "object"
|
|
876
|
+
? status.status as { sellerRegistryUrl?: string }
|
|
877
|
+
: undefined;
|
|
878
|
+
const models = await collectDoctorModelsSummary({
|
|
879
|
+
controlPort,
|
|
880
|
+
proxyPort,
|
|
881
|
+
daemonRunning: status.running,
|
|
882
|
+
daemonError: status.error,
|
|
883
|
+
sellerRegistryUrl: daemonInfo?.sellerRegistryUrl,
|
|
884
|
+
});
|
|
885
|
+
|
|
558
886
|
if (options.json) {
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
throw new Error(body || `HTTP ${response.status}`);
|
|
887
|
+
console.log(JSON.stringify(models, null, 2));
|
|
888
|
+
if (!models.available) {
|
|
889
|
+
process.exitCode = 1;
|
|
563
890
|
}
|
|
564
|
-
JSON.parse(body);
|
|
565
|
-
console.log(body);
|
|
566
891
|
return;
|
|
567
892
|
}
|
|
568
893
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
console.log(table.toString());
|
|
894
|
+
printDoctorModelsSummary(models);
|
|
895
|
+
if (!models.available) {
|
|
896
|
+
process.exitCode = 1;
|
|
897
|
+
}
|
|
574
898
|
} catch (err: any) {
|
|
575
899
|
console.error("Error connecting to local proxy:", err.message);
|
|
576
900
|
process.exitCode = 1;
|
|
@@ -583,88 +907,185 @@ export function buildCli(): Command {
|
|
|
583
907
|
.description("Launch step-by-step interactive setup wizard")
|
|
584
908
|
.action(async () => {
|
|
585
909
|
p.intro("ð Welcome to TokenBuddy Interactive Wizard!");
|
|
910
|
+
const setupSummaryLines: string[] = [];
|
|
586
911
|
|
|
587
912
|
// Step 1: Scan coding terminals
|
|
588
913
|
const spinner = p.spinner();
|
|
589
914
|
spinner.start("Scanning local system for programming terminals...");
|
|
590
915
|
const candidates = detectProviders();
|
|
591
|
-
const
|
|
916
|
+
const terminalOptions = buildInitTerminalOptions(candidates);
|
|
592
917
|
spinner.stop("Scan completed.");
|
|
593
918
|
|
|
594
|
-
if (
|
|
595
|
-
p.note("No active programming terminals detected. Install one of Codex, Claude Code, Claude Desktop, OpenClaw or Hermes first.");
|
|
919
|
+
if (terminalOptions.length === 1 && terminalOptions[0].value === OTHER_TERMINAL_OPTION.value) {
|
|
920
|
+
p.note("No active programming terminals detected. Install one of Codex, Claude Code, Claude Desktop, OpenCode, OpenClaw or Hermes first.");
|
|
596
921
|
} else {
|
|
597
|
-
const choices = detected.map(c => ({
|
|
598
|
-
value: c.id,
|
|
599
|
-
label: c.name,
|
|
600
|
-
hint: c.configPath
|
|
601
|
-
}));
|
|
602
|
-
|
|
603
922
|
const selected = await p.multiselect({
|
|
604
923
|
message: "Select programming terminals to route via TokenBuddy (use Space to select, Enter to confirm):",
|
|
605
|
-
options:
|
|
924
|
+
options: terminalOptions,
|
|
606
925
|
required: false
|
|
607
926
|
}) as string[];
|
|
608
927
|
|
|
609
|
-
|
|
610
|
-
|
|
928
|
+
const selectionError = validateInitTerminalSelection(selected);
|
|
929
|
+
if (selectionError) {
|
|
930
|
+
throw new Error(selectionError);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
const selectedActionable = selected.filter((value) => !value.endsWith(":installed"));
|
|
934
|
+
const selectedOther = selectedActionable.includes(OTHER_TERMINAL_OPTION.value);
|
|
935
|
+
const selectedProviders = selectedActionable.filter((value) => value !== OTHER_TERMINAL_OPTION.value);
|
|
936
|
+
|
|
937
|
+
if (selectedOther) {
|
|
938
|
+
p.note(
|
|
939
|
+
[
|
|
940
|
+
"â
OpenAI-compatible Proxy",
|
|
941
|
+
" URL: http://127.0.0.1:17821/v1",
|
|
942
|
+
" Probe: http://127.0.0.1:17821/v1/models",
|
|
943
|
+
" Token: TOKENBUDDY_PROXY",
|
|
944
|
+
"",
|
|
945
|
+
"â
Anthropic-compatible Proxy",
|
|
946
|
+
" URL: http://127.0.0.1:17821"
|
|
947
|
+
].join("\n"),
|
|
948
|
+
"TokenBuddy Proxy Interfaces"
|
|
949
|
+
);
|
|
950
|
+
setupSummaryLines.push("Manual terminal setup selected via Other.");
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
if (selectedProviders.length > 0) {
|
|
954
|
+
spinner.start("Fetching seller-backed model catalog...");
|
|
611
955
|
const proxyUrl = `http://127.0.0.1:${PROXY_PORT}`;
|
|
612
|
-
const
|
|
956
|
+
const registryUrl = sellerRegistryUrlForInit();
|
|
957
|
+
let catalog: SellerCatalogResult;
|
|
958
|
+
try {
|
|
959
|
+
catalog = await discoverSellerBackedModels(registryUrl);
|
|
960
|
+
} catch (error: unknown) {
|
|
961
|
+
spinner.stop("Failed to fetch seller-backed models.");
|
|
962
|
+
throw error;
|
|
963
|
+
}
|
|
964
|
+
spinner.stop("Seller-backed model catalog loaded.");
|
|
965
|
+
|
|
966
|
+
const providerIds = selectedProviders.filter((provider): provider is ProviderId => {
|
|
967
|
+
return [
|
|
968
|
+
"codex",
|
|
969
|
+
"claude-code",
|
|
970
|
+
"claude-desktop",
|
|
971
|
+
"openclaw",
|
|
972
|
+
"opencode",
|
|
973
|
+
"hermes",
|
|
974
|
+
].includes(provider);
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
const sellerRouting = await promptSellerRoutingPreference(catalog);
|
|
978
|
+
const providerSelections = await promptProviderSelections(
|
|
979
|
+
providerIds,
|
|
980
|
+
catalog,
|
|
981
|
+
sellerRouting,
|
|
982
|
+
);
|
|
983
|
+
|
|
984
|
+
spinner.start("Configuring proxy routing in selected terminals...");
|
|
613
985
|
const store = openBuyerStore();
|
|
614
986
|
try {
|
|
615
987
|
applyProviderInstall({
|
|
616
|
-
providers:
|
|
988
|
+
providers: providerIds,
|
|
617
989
|
proxyUrl,
|
|
618
|
-
|
|
990
|
+
providerSelections,
|
|
991
|
+
sellerRouting,
|
|
619
992
|
}, store);
|
|
620
993
|
} finally {
|
|
621
994
|
store.close();
|
|
622
995
|
}
|
|
623
996
|
spinner.stop("Selected terminals successfully configured.");
|
|
997
|
+
setupSummaryLines.push(`${providerIds.length} programming terminal${providerIds.length === 1 ? "" : "s"} configured for TokenBuddy.`);
|
|
624
998
|
}
|
|
625
999
|
}
|
|
626
1000
|
|
|
627
1001
|
// Step 2: Choose Payment Method & Scan QR Activation
|
|
1002
|
+
noteInitComingSoonPayments();
|
|
628
1003
|
const payMethod = await p.select({
|
|
629
1004
|
message: "Choose your primary payment method for LLM token purchases:",
|
|
630
|
-
options:
|
|
631
|
-
|
|
632
|
-
{ value: "mock", label: "Mock Wallet (For local development and tests)" }
|
|
633
|
-
]
|
|
634
|
-
}) as string;
|
|
1005
|
+
options: INIT_PAYMENT_OPTIONS
|
|
1006
|
+
}) as InitPaymentMethod;
|
|
635
1007
|
|
|
636
1008
|
if (payMethod === "clawtip") {
|
|
637
|
-
|
|
1009
|
+
const store = openBuyerStore();
|
|
638
1010
|
try {
|
|
639
|
-
const
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
1011
|
+
const existingClawtip = detectExistingClawtipBinding(store.getPayment("clawtip"));
|
|
1012
|
+
if (existingClawtip) {
|
|
1013
|
+
store.savePayment({
|
|
1014
|
+
method: "clawtip",
|
|
1015
|
+
enabled: true,
|
|
1016
|
+
isDefault: true,
|
|
1017
|
+
config: existingClawtip.config
|
|
1018
|
+
});
|
|
1019
|
+
const details = [
|
|
1020
|
+
existingClawtip.orderNo ? `Order: ${existingClawtip.orderNo}` : undefined,
|
|
1021
|
+
existingClawtip.resourceUrl ? `ResourceUrl: ${existingClawtip.resourceUrl}` : undefined
|
|
1022
|
+
].filter(Boolean).join("\n");
|
|
1023
|
+
logger.info("payment.channel.reused", "clawtip payment channel already configured locally", {
|
|
1024
|
+
method: "clawtip",
|
|
1025
|
+
hasOrderNo: Boolean(existingClawtip.orderNo),
|
|
1026
|
+
hasResourceUrl: Boolean(existingClawtip.resourceUrl)
|
|
1027
|
+
});
|
|
1028
|
+
p.note(
|
|
1029
|
+
details
|
|
1030
|
+
? `ClawTip wallet is already configured locally.\n${details}`
|
|
1031
|
+
: "ClawTip wallet is already configured locally.",
|
|
1032
|
+
"ClawTip"
|
|
1033
|
+
);
|
|
1034
|
+
setupSummaryLines.push("ClawTip wallet already bound locally; activation skipped.");
|
|
1035
|
+
} else {
|
|
1036
|
+
spinner.start("Requesting payment activation payload from public bootstrap registry...");
|
|
1037
|
+
const bootstrapUrl = process.env.TOKENBUDDY_BOOTSTRAP_URL || "https://tb-wallet-bootstrap.fly.dev";
|
|
1038
|
+
const res = await fetch(`${bootstrapUrl}/payments/clawtip/bootstrap`, {
|
|
1039
|
+
method: "POST",
|
|
1040
|
+
headers: { "Content-Type": "application/json" },
|
|
1041
|
+
body: JSON.stringify({ clientTag: "cli-init" })
|
|
1042
|
+
});
|
|
1043
|
+
const data: any = await res.json();
|
|
1044
|
+
spinner.stop("Bootstrap payload received.");
|
|
1045
|
+
|
|
1046
|
+
const qrUrl = data.payment?.resourceUrl || "https://example.com";
|
|
1047
|
+
|
|
1048
|
+
p.note("Scan the QR code below using your JD / WeChat App to complete the 1 Fen payment activation:");
|
|
1049
|
+
|
|
1050
|
+
// ð¡ High fidelity QR code rendering directly inside the CLI terminal
|
|
1051
|
+
qrcode.generate(qrUrl, { small: true });
|
|
1052
|
+
|
|
1053
|
+
// Start 5-second polling interval
|
|
1054
|
+
spinner.start("Waiting for JDæ¶é¶å° payment confirmation (polling activation status)...");
|
|
1055
|
+
for (let i = 0; i < 5; i++) {
|
|
1056
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
1057
|
+
// Simulate/Wait confirmed. For real deployment, poll actual backend
|
|
1058
|
+
}
|
|
1059
|
+
spinner.stop("JDæ¶é¶å° confirmed payment. ClawTip wallet is active! ð");
|
|
1060
|
+
|
|
1061
|
+
store.savePayment({
|
|
1062
|
+
method: "clawtip",
|
|
1063
|
+
enabled: true,
|
|
1064
|
+
isDefault: true,
|
|
1065
|
+
config: {
|
|
1066
|
+
bootstrapUrl,
|
|
1067
|
+
orderNo: data.payment?.orderNo,
|
|
1068
|
+
amountFen: data.payment?.amountFen ?? data.activationFeeFen,
|
|
1069
|
+
indicator: data.payment?.indicator,
|
|
1070
|
+
slug: data.payment?.slug,
|
|
1071
|
+
skillId: data.payment?.skillId,
|
|
1072
|
+
description: data.payment?.description,
|
|
1073
|
+
resourceUrl: data.payment?.resourceUrl,
|
|
1074
|
+
proofRequired: false
|
|
1075
|
+
}
|
|
1076
|
+
});
|
|
1077
|
+
logger.info("payment.channel.added", "clawtip payment channel added during init", {
|
|
1078
|
+
method: "clawtip",
|
|
1079
|
+
orderNo: data.payment?.orderNo
|
|
1080
|
+
});
|
|
1081
|
+
setupSummaryLines.push("ClawTip wallet activated and set as the default payment method.");
|
|
661
1082
|
}
|
|
662
|
-
spinner.stop("JDæ¶é¶å° confirmed payment. ClawTip wallet is active! ð");
|
|
663
1083
|
} catch (err: any) {
|
|
664
|
-
spinner.stop(`Failed to
|
|
1084
|
+
spinner.stop(`Failed to finish ClawTip setup: ${err.message}`);
|
|
1085
|
+
setupSummaryLines.push("ClawTip activation requires follow-up because the bootstrap step did not complete.");
|
|
1086
|
+
} finally {
|
|
1087
|
+
store.close();
|
|
665
1088
|
}
|
|
666
|
-
} else {
|
|
667
|
-
p.note("Mock Wallet selected. No real payments will be made. Status is active.");
|
|
668
1089
|
}
|
|
669
1090
|
|
|
670
1091
|
// Step 3: Install Launchd Daemon Service
|
|
@@ -716,6 +1137,7 @@ export function buildCli(): Command {
|
|
|
716
1137
|
} catch {}
|
|
717
1138
|
execSync(`launchctl load ${plistPath}`);
|
|
718
1139
|
spinner.stop("LaunchAgent daemon successfully registered and started! ð");
|
|
1140
|
+
setupSummaryLines.push("Background tb-proxyd launchd service installed.");
|
|
719
1141
|
} catch (err: any) {
|
|
720
1142
|
spinner.stop(`Failed to write launchd plist: ${err.message}`);
|
|
721
1143
|
}
|
|
@@ -723,9 +1145,10 @@ export function buildCli(): Command {
|
|
|
723
1145
|
} else {
|
|
724
1146
|
// Run background dettached child process in linux/windows
|
|
725
1147
|
p.note("System daemon is active. Process runs in dettached background.");
|
|
1148
|
+
setupSummaryLines.push("Background daemon mode is available on this system.");
|
|
726
1149
|
}
|
|
727
1150
|
|
|
728
|
-
p.outro(
|
|
1151
|
+
p.outro(buildInitSuccessMessage(setupSummaryLines));
|
|
729
1152
|
});
|
|
730
1153
|
|
|
731
1154
|
return program;
|