@tokenbuddy/tokenbuddy 1.0.5 → 1.0.7

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.
Files changed (56) hide show
  1. package/dist/src/buyer-store.d.ts +48 -1
  2. package/dist/src/buyer-store.d.ts.map +1 -1
  3. package/dist/src/buyer-store.js +144 -17
  4. package/dist/src/buyer-store.js.map +1 -1
  5. package/dist/src/cli.d.ts +17 -0
  6. package/dist/src/cli.d.ts.map +1 -1
  7. package/dist/src/cli.js +560 -63
  8. package/dist/src/cli.js.map +1 -1
  9. package/dist/src/daemon.d.ts +11 -5
  10. package/dist/src/daemon.d.ts.map +1 -1
  11. package/dist/src/daemon.js +574 -161
  12. package/dist/src/daemon.js.map +1 -1
  13. package/dist/src/doctor-clawtip-wallet.d.ts +14 -0
  14. package/dist/src/doctor-clawtip-wallet.d.ts.map +1 -0
  15. package/dist/src/doctor-clawtip-wallet.js +54 -0
  16. package/dist/src/doctor-clawtip-wallet.js.map +1 -0
  17. package/dist/src/doctor-diagnostics.d.ts +99 -0
  18. package/dist/src/doctor-diagnostics.d.ts.map +1 -0
  19. package/dist/src/doctor-diagnostics.js +552 -0
  20. package/dist/src/doctor-diagnostics.js.map +1 -0
  21. package/dist/src/init-clawtip-activation.d.ts +48 -0
  22. package/dist/src/init-clawtip-activation.d.ts.map +1 -0
  23. package/dist/src/init-clawtip-activation.js +395 -0
  24. package/dist/src/init-clawtip-activation.js.map +1 -0
  25. package/dist/src/init-payment-options.d.ts +56 -0
  26. package/dist/src/init-payment-options.d.ts.map +1 -0
  27. package/dist/src/init-payment-options.js +165 -0
  28. package/dist/src/init-payment-options.js.map +1 -0
  29. package/dist/src/provider-install.d.ts +37 -2
  30. package/dist/src/provider-install.d.ts.map +1 -1
  31. package/dist/src/provider-install.js +317 -67
  32. package/dist/src/provider-install.js.map +1 -1
  33. package/dist/src/seller-catalog.d.ts +79 -0
  34. package/dist/src/seller-catalog.d.ts.map +1 -0
  35. package/dist/src/seller-catalog.js +126 -0
  36. package/dist/src/seller-catalog.js.map +1 -0
  37. package/dist/src/tb-proxyd.js +13 -2
  38. package/dist/src/tb-proxyd.js.map +1 -1
  39. package/dist/src/terminal-image.d.ts +22 -0
  40. package/dist/src/terminal-image.d.ts.map +1 -0
  41. package/dist/src/terminal-image.js +135 -0
  42. package/dist/src/terminal-image.js.map +1 -0
  43. package/package.json +1 -1
  44. package/src/buyer-store.ts +253 -18
  45. package/src/cli.ts +709 -68
  46. package/src/daemon.ts +651 -167
  47. package/src/doctor-clawtip-wallet.ts +70 -0
  48. package/src/doctor-diagnostics.ts +861 -0
  49. package/src/init-clawtip-activation.ts +487 -0
  50. package/src/init-payment-options.ts +249 -0
  51. package/src/provider-install.ts +426 -76
  52. package/src/seller-catalog.ts +222 -0
  53. package/src/tb-proxyd.ts +14 -2
  54. package/src/terminal-image.ts +187 -0
  55. package/tests/e2e.test.ts +88 -5
  56. package/tests/tokenbuddy.test.ts +1362 -27
package/src/cli.ts CHANGED
@@ -6,10 +6,54 @@ 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 { applyProviderInstall, detectProviders } from "./provider-install.js";
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
+ buildInitSuccessMessage,
40
+ buildInitTerminalSelectionState,
41
+ buildInstalledTerminalMessage,
42
+ INIT_PAYMENT_OPTIONS,
43
+ inspectClawtipWalletReadiness,
44
+ inspectOpenClawWalletConfig,
45
+ noteInitComingSoonPayments,
46
+ OTHER_TERMINAL_OPTION,
47
+ type InitPaymentMethod,
48
+ validateInitTerminalSelection,
49
+ } from "./init-payment-options.js";
50
+ import {
51
+ checkOpenClawRuntime,
52
+ readClawtipPayCredential,
53
+ startClawtipWalletBootstrap,
54
+ waitForClawtipActivationConfirmation,
55
+ } from "./init-clawtip-activation.js";
56
+ import { displayTerminalImage } from "./terminal-image.js";
13
57
 
14
58
  // @ts-ignore
15
59
  import qrcode from "qrcode-terminal";
@@ -53,6 +97,12 @@ interface ClawtipBootstrapResponse {
53
97
  };
54
98
  }
55
99
 
100
+ interface SelectOption {
101
+ value: string;
102
+ label: string;
103
+ hint?: string;
104
+ }
105
+
56
106
  function isSupportedPaymentMethod(method: string): method is SupportedPaymentMethod {
57
107
  return (SUPPORTED_PAYMENT_METHODS as readonly string[]).includes(method);
58
108
  }
@@ -121,6 +171,20 @@ async function probeDaemonStatus(controlPort: number): Promise<DaemonProbeResult
121
171
  }
122
172
  }
123
173
 
174
+ interface NormalizedClawtipPaymentPayload {
175
+ orderNo: string;
176
+ amountFen?: number;
177
+ payTo?: string;
178
+ encryptedData?: string;
179
+ indicator: string;
180
+ slug?: string;
181
+ skillId?: string;
182
+ description?: string;
183
+ resourceUrl: string;
184
+ }
185
+
186
+ const CLAWTIP_BOOTSTRAP_PLACEHOLDER_PAY_TO = "bootstrap-pay-to";
187
+
124
188
  async function waitForDaemonStatus(controlPort: number, timeoutMs: number): Promise<DaemonProbeResult> {
125
189
  const deadline = Date.now() + timeoutMs;
126
190
  let latest: DaemonProbeResult = { running: false, error: "not checked" };
@@ -294,7 +358,7 @@ function printPaymentList(payments: PaymentConfig[], asJson: boolean): void {
294
358
  console.log(table.toString());
295
359
  }
296
360
 
297
- async function fetchClawtipBootstrap(bootstrapUrl: string): Promise<ClawtipBootstrapResponse> {
361
+ export async function fetchClawtipBootstrap(bootstrapUrl: string): Promise<ClawtipBootstrapResponse> {
298
362
  const response = await fetch(`${bootstrapUrl.replace(/\/+$/, "")}/payments/clawtip/bootstrap`, {
299
363
  method: "POST",
300
364
  headers: { "Content-Type": "application/json" },
@@ -307,9 +371,35 @@ async function fetchClawtipBootstrap(bootstrapUrl: string): Promise<ClawtipBoots
307
371
  if (!body.payment?.orderNo || !body.payment.indicator || !body.payment.resourceUrl) {
308
372
  throw new Error("ClawTip bootstrap response missing payment order fields");
309
373
  }
374
+ if ((body.payment.payTo || "").trim() === CLAWTIP_BOOTSTRAP_PLACEHOLDER_PAY_TO) {
375
+ throw new Error(
376
+ [
377
+ `ClawTip bootstrap service is misconfigured: payTo is still the placeholder \`${CLAWTIP_BOOTSTRAP_PLACEHOLDER_PAY_TO}\`.`,
378
+ `Bootstrap URL: ${bootstrapUrl}`,
379
+ "Configure the bootstrap service with the real ClawTip merchant pay_to before retrying `tb init`.",
380
+ ].join(" ")
381
+ );
382
+ }
383
+ body.payment.resourceUrl = normalizeClawtipBootstrapResourceUrl(bootstrapUrl, body.payment.resourceUrl);
310
384
  return body;
311
385
  }
312
386
 
387
+ export function normalizeClawtipBootstrapResourceUrl(bootstrapUrl: string, resourceUrl: string): string {
388
+ try {
389
+ const bootstrap = new URL(bootstrapUrl);
390
+ const resource = new URL(resourceUrl);
391
+ if (resource.origin === bootstrap.origin && resource.pathname === "/registry/sellers") {
392
+ resource.pathname = bootstrap.pathname.replace(/\/+$/, "") || "/";
393
+ resource.search = "";
394
+ resource.hash = "";
395
+ return resource.toString().replace(/\/$/, "");
396
+ }
397
+ } catch {
398
+ // Leave the server-provided value unchanged when URL parsing fails.
399
+ }
400
+ return resourceUrl;
401
+ }
402
+
313
403
  function readProof(options: { proofFile?: string; requireProof?: boolean }): string | undefined {
314
404
  const proofFile = options.proofFile || process.env.TOKENBUDDY_CLAWTIP_PROOF_FILE;
315
405
  if (!proofFile) {
@@ -328,6 +418,251 @@ function readProof(options: { proofFile?: string; requireProof?: boolean }): str
328
418
  return proof;
329
419
  }
330
420
 
421
+ function sellerRegistryUrlForInit(): string {
422
+ return process.env.TB_PROXYD_SELLER_REGISTRY_URL || "https://tb-wallet-bootstrap.fly.dev/registry/sellers";
423
+ }
424
+
425
+ function stableModelChoices(models: ModelCatalogEntry[]): SelectOption[] {
426
+ const grouped = new Map<string, ModelCatalogEntry[]>();
427
+ for (const entry of models) {
428
+ const list = grouped.get(entry.id) || [];
429
+ list.push(entry);
430
+ grouped.set(entry.id, list);
431
+ }
432
+ return Array.from(grouped.entries()).map(([modelId, entries]) => {
433
+ const sellerIds = Array.from(new Set(entries.map((entry) => entry.sellerId)));
434
+ const protocols = Array.from(
435
+ new Set(entries.flatMap((entry) => entry.supportedProtocols)),
436
+ );
437
+ return {
438
+ value: modelId,
439
+ label: modelId,
440
+ hint: `${sellerIds.join(",")} · ${protocols.join(",") || "no-protocol"}`,
441
+ };
442
+ });
443
+ }
444
+
445
+ async function promptSellerRoutingPreference(catalog: SellerCatalogResult): Promise<SellerRoutingPreference> {
446
+ const healthySellers = catalog.sellers.filter((seller) => seller.status === "ok");
447
+ const mode = await p.select({
448
+ message: "Choose seller routing mode for tb-proxyd:",
449
+ options: [
450
+ {
451
+ value: "auto",
452
+ label: "Auto",
453
+ hint: "Automatically choose a compatible seller based on the requested model.",
454
+ },
455
+ {
456
+ value: "fixed",
457
+ label: "Fixed Seller",
458
+ hint: "Pin tb-proxyd to one seller and only use models from that seller.",
459
+ },
460
+ ],
461
+ }) as SellerRoutingPreference["mode"] | symbol;
462
+
463
+ if (typeof mode !== "string") {
464
+ throw new Error("seller routing selection was cancelled");
465
+ }
466
+ if (mode === "auto") {
467
+ return { mode };
468
+ }
469
+
470
+ if (healthySellers.length === 0) {
471
+ throw new Error("no healthy sellers available for fixed routing");
472
+ }
473
+
474
+ const sellerId = await p.select({
475
+ message: "Choose the seller to pin tb-proxyd to:",
476
+ options: healthySellers.map((seller) => ({
477
+ value: seller.id,
478
+ label: seller.name ? `${seller.name} (${seller.id})` : seller.id,
479
+ hint: [
480
+ seller.discountRatio != null ? `discount x${seller.discountRatio}` : null,
481
+ seller.modelCount != null ? `${seller.modelCount} models` : null,
482
+ seller.supportedProtocols?.length ? seller.supportedProtocols.join(",") : null,
483
+ seller.paymentMethods?.length ? seller.paymentMethods.join(",") : null,
484
+ ]
485
+ .filter(Boolean)
486
+ .join(" · ") || seller.url,
487
+ })),
488
+ }) as string | symbol;
489
+
490
+ if (typeof sellerId !== "string") {
491
+ throw new Error("fixed seller selection was cancelled");
492
+ }
493
+ return {
494
+ mode,
495
+ sellerId,
496
+ };
497
+ }
498
+
499
+ async function promptSingleModelSelection(
500
+ providerId: ProviderId,
501
+ models: ModelCatalogEntry[],
502
+ sellerRouting: SellerRoutingPreference,
503
+ ): Promise<SingleModelProviderRuntimeConfig> {
504
+ const protocolPreference = getProviderProtocolPreference(providerId);
505
+ const protocolFiltered = protocolPreference
506
+ ? filterCatalogByProtocol(models, protocolPreference)
507
+ : models;
508
+ const choices = stableModelChoices(protocolFiltered);
509
+ if (choices.length === 0) {
510
+ throw new Error(`no compatible models available for ${providerId}`);
511
+ }
512
+
513
+ const labelMap: Record<string, string> = {
514
+ opencode: "OpenCode",
515
+ codex: "Codex",
516
+ openclaw: "OpenClaw",
517
+ hermes: "Hermes",
518
+ "claude-desktop": "Claude Desktop",
519
+ "claude-code": "Claude Code",
520
+ };
521
+
522
+ const selectedModel = await p.select({
523
+ message: `Choose the default model for ${labelMap[providerId] || providerId}:`,
524
+ options: choices,
525
+ }) as string | symbol;
526
+
527
+ if (typeof selectedModel !== "string") {
528
+ throw new Error(`default model selection was cancelled for ${providerId}`);
529
+ }
530
+
531
+ const selectedEntry = protocolFiltered.find((entry) => entry.id === selectedModel);
532
+ return {
533
+ selectionKind: "single-model",
534
+ protocolPreference,
535
+ defaultModel: selectedModel,
536
+ sellerId: sellerRouting.mode === "fixed" ? selectedEntry?.sellerId : undefined,
537
+ };
538
+ }
539
+
540
+ function defaultClaudeDisplayName(modelId: string): string {
541
+ return modelId.trim();
542
+ }
543
+
544
+ function makeClaudeRoleMapping(modelId: string): ClaudeCodeModelMappingConfig {
545
+ const displayName = defaultClaudeDisplayName(modelId);
546
+ return {
547
+ selectionKind: "claude-role-mapping",
548
+ protocolPreference: "messages",
549
+ fallbackModel: modelId,
550
+ roles: {
551
+ sonnet: {
552
+ upstreamModel: modelId,
553
+ displayName,
554
+ declareOneM: true,
555
+ },
556
+ opus: {
557
+ upstreamModel: modelId,
558
+ displayName,
559
+ declareOneM: true,
560
+ },
561
+ haiku: {
562
+ upstreamModel: modelId,
563
+ displayName,
564
+ declareOneM: false,
565
+ },
566
+ },
567
+ };
568
+ }
569
+
570
+ async function promptClaudeCodeModelSelection(
571
+ models: ModelCatalogEntry[],
572
+ ): Promise<ClaudeCodeModelMappingConfig> {
573
+ const protocolFiltered = filterCatalogByProtocol(models, "messages");
574
+ const choices = stableModelChoices(protocolFiltered);
575
+ if (choices.length === 0) {
576
+ throw new Error("no compatible message models available for Claude Code");
577
+ }
578
+
579
+ const sonnetModel = await p.select({
580
+ message: "Choose the default Sonnet model for Claude Code:",
581
+ options: choices,
582
+ }) as string | symbol;
583
+
584
+ if (typeof sonnetModel !== "string") {
585
+ throw new Error("Claude Code model selection was cancelled");
586
+ }
587
+
588
+ const mirrorAllRoles = await p.confirm({
589
+ message: "Use the same model for Opus and Haiku as well?",
590
+ initialValue: true,
591
+ });
592
+
593
+ if (typeof mirrorAllRoles !== "boolean") {
594
+ throw new Error("Claude Code role mapping confirmation was cancelled");
595
+ }
596
+
597
+ if (mirrorAllRoles) {
598
+ return makeClaudeRoleMapping(sonnetModel);
599
+ }
600
+
601
+ const opusModel = await p.select({
602
+ message: "Choose the default Opus model for Claude Code:",
603
+ options: choices,
604
+ }) as string | symbol;
605
+ if (typeof opusModel !== "string") {
606
+ throw new Error("Claude Code Opus model selection was cancelled");
607
+ }
608
+
609
+ const haikuModel = await p.select({
610
+ message: "Choose the default Haiku model for Claude Code:",
611
+ options: choices,
612
+ }) as string | symbol;
613
+ if (typeof haikuModel !== "string") {
614
+ throw new Error("Claude Code Haiku model selection was cancelled");
615
+ }
616
+
617
+ return {
618
+ selectionKind: "claude-role-mapping",
619
+ protocolPreference: "messages",
620
+ fallbackModel: sonnetModel,
621
+ roles: {
622
+ sonnet: {
623
+ upstreamModel: sonnetModel,
624
+ displayName: defaultClaudeDisplayName(sonnetModel),
625
+ declareOneM: true,
626
+ },
627
+ opus: {
628
+ upstreamModel: opusModel,
629
+ displayName: defaultClaudeDisplayName(opusModel),
630
+ declareOneM: true,
631
+ },
632
+ haiku: {
633
+ upstreamModel: haikuModel,
634
+ displayName: defaultClaudeDisplayName(haikuModel),
635
+ declareOneM: false,
636
+ },
637
+ },
638
+ };
639
+ }
640
+
641
+ async function promptProviderSelections(
642
+ providerIds: ProviderId[],
643
+ catalog: SellerCatalogResult,
644
+ sellerRouting: SellerRoutingPreference,
645
+ ): Promise<ProviderSelections> {
646
+ const baseModels = sellerRouting.mode === "fixed"
647
+ ? filterCatalogBySeller(catalog.models, sellerRouting.sellerId)
648
+ : catalog.models;
649
+
650
+ const selections: ProviderSelections = {};
651
+ for (const providerId of providerIds) {
652
+ const selectionKind = getProviderModelSelectionKind(providerId);
653
+ if (selectionKind === "claude-role-mapping") {
654
+ selections[providerId] = await promptClaudeCodeModelSelection(baseModels);
655
+ continue;
656
+ }
657
+ selections[providerId] = await promptSingleModelSelection(
658
+ providerId,
659
+ baseModels,
660
+ sellerRouting,
661
+ );
662
+ }
663
+ return selections;
664
+ }
665
+
331
666
  export function buildCli(): Command {
332
667
  const program = new Command();
333
668
  program
@@ -352,7 +687,6 @@ export function buildCli(): Command {
352
687
  const plistPath = process.platform === "darwin"
353
688
  ? path.join(os.homedir(), "Library", "LaunchAgents", "com.tokenbuddy.proxyd.plist")
354
689
  : undefined;
355
- const candidates = detectProviders();
356
690
  let probe = await probeDaemonStatus(controlPort);
357
691
  let repair: DaemonRepairResult = { attempted: false, fixed: false };
358
692
  if (!probe.running && options.fix) {
@@ -363,11 +697,23 @@ export function buildCli(): Command {
363
697
  const daemonInfo = probe.status;
364
698
  const daemonRunning = probe.running;
365
699
  const daemonError = probe.error;
700
+ const daemonStatus = daemonInfo && typeof daemonInfo === "object"
701
+ ? daemonInfo as { selectionMode?: string; sellerRoutingMode?: string; selectedSellerId?: string; sellerRegistryUrl?: string }
702
+ : undefined;
703
+ const providers = readDoctorProviders();
366
704
  if (options.fix && repair.attempted && !repair.fixed) {
367
705
  process.exitCode = 1;
368
706
  }
369
707
 
370
708
  if (options.json) {
709
+ const diagnostics = await collectDoctorDiagnostics({
710
+ controlPort,
711
+ proxyPort,
712
+ daemonRunning,
713
+ daemonError,
714
+ providers,
715
+ sellerRegistryUrl: daemonStatus?.sellerRegistryUrl,
716
+ });
371
717
  console.log(JSON.stringify({
372
718
  daemon: {
373
719
  running: daemonRunning,
@@ -387,7 +733,7 @@ export function buildCli(): Command {
387
733
  plistPath,
388
734
  plistExists: plistPath ? fs.existsSync(plistPath) : false
389
735
  },
390
- providers: candidates
736
+ ...diagnostics,
391
737
  }, null, 2));
392
738
  return;
393
739
  }
@@ -422,12 +768,29 @@ export function buildCli(): Command {
422
768
  }
423
769
  }
424
770
 
425
- // 3. Detect terminals
426
- console.log("\n--- Programming Terminals Detection ---");
427
- for (const c of candidates) {
428
- const icon = c.detected ? "✅" : "🔘";
429
- console.log(`${icon} ${c.name}: ${c.reason}`);
771
+ if (daemonStatus) {
772
+ console.log(` Control Plane URL: ${controlUrl}`);
773
+ console.log(` Proxy Plane URL: http://127.0.0.1:${proxyPort}`);
774
+ if (daemonStatus.sellerRoutingMode || daemonStatus.selectionMode) {
775
+ console.log(` Routing Mode: ${daemonStatus.sellerRoutingMode || daemonStatus.selectionMode}`);
776
+ }
777
+ if (daemonStatus.selectedSellerId) {
778
+ console.log(` Selected Seller: ${daemonStatus.selectedSellerId}`);
779
+ }
780
+ if (daemonStatus.sellerRegistryUrl) {
781
+ console.log(` Registry URL: ${daemonStatus.sellerRegistryUrl}`);
782
+ }
430
783
  }
784
+
785
+ printDoctorProviders(providers);
786
+ await renderDoctorDiagnosticsProgressively({
787
+ controlPort,
788
+ proxyPort,
789
+ daemonRunning,
790
+ daemonError,
791
+ sellerRegistryUrl: daemonStatus?.sellerRegistryUrl,
792
+ providers,
793
+ });
431
794
  });
432
795
 
433
796
  // 2. tb payment
@@ -555,22 +918,32 @@ export function buildCli(): Command {
555
918
  .option("--json", "Output model list as JSON")
556
919
  .action(async (options: { json?: boolean }) => {
557
920
  try {
921
+ const controlPort = configuredControlPort();
922
+ const proxyPort = configuredProxyPort();
923
+ const status = await probeDaemonStatus(controlPort);
924
+ const daemonInfo = status.status && typeof status.status === "object"
925
+ ? status.status as { sellerRegistryUrl?: string }
926
+ : undefined;
927
+ const models = await collectDoctorModelsSummary({
928
+ controlPort,
929
+ proxyPort,
930
+ daemonRunning: status.running,
931
+ daemonError: status.error,
932
+ sellerRegistryUrl: daemonInfo?.sellerRegistryUrl,
933
+ });
934
+
558
935
  if (options.json) {
559
- const response = await fetch(`http://127.0.0.1:${configuredControlPort()}/models`);
560
- const body = await response.text();
561
- if (!response.ok) {
562
- throw new Error(body || `HTTP ${response.status}`);
936
+ console.log(JSON.stringify(models, null, 2));
937
+ if (!models.available) {
938
+ process.exitCode = 1;
563
939
  }
564
- JSON.parse(body);
565
- console.log(body);
566
940
  return;
567
941
  }
568
942
 
569
- const table = new Table({ head: ["Model ID", "Input Price/1M", "Output Price/1M", "Supported Protocols"] });
570
- // Sample static model config from seller mock
571
- table.push(["gpt-4", "1.0 USD (or equivalent points)", "3.0 USD", "OpenAI, Direct"]);
572
- console.log("=== Available LLM Models Matrix ===");
573
- console.log(table.toString());
943
+ printDoctorModelsSummary(models);
944
+ if (!models.available) {
945
+ process.exitCode = 1;
946
+ }
574
947
  } catch (err: any) {
575
948
  console.error("Error connecting to local proxy:", err.message);
576
949
  process.exitCode = 1;
@@ -583,88 +956,354 @@ export function buildCli(): Command {
583
956
  .description("Launch step-by-step interactive setup wizard")
584
957
  .action(async () => {
585
958
  p.intro("🚀 Welcome to TokenBuddy Interactive Wizard!");
959
+ const setupSummaryLines: string[] = [];
586
960
 
587
961
  // Step 1: Scan coding terminals
588
962
  const spinner = p.spinner();
589
963
  spinner.start("Scanning local system for programming terminals...");
590
964
  const candidates = detectProviders();
591
- const detected = candidates.filter(c => c.detected);
965
+ const terminalSelection = buildInitTerminalSelectionState(candidates);
592
966
  spinner.stop("Scan completed.");
593
967
 
594
- if (detected.length === 0) {
595
- p.note("No active programming terminals detected. Install one of Codex, Claude Code, Claude Desktop, OpenClaw or Hermes first.");
596
- } else {
597
- const choices = detected.map(c => ({
598
- value: c.id,
599
- label: c.name,
600
- hint: c.configPath
601
- }));
968
+ const installedTerminalMessage = buildInstalledTerminalMessage(terminalSelection.installed);
969
+ if (installedTerminalMessage) {
970
+ p.note(installedTerminalMessage, "Already Configured");
971
+ setupSummaryLines.push(`${terminalSelection.installed.length} terminal${terminalSelection.installed.length === 1 ? "" : "s"} already configured.`);
972
+ }
602
973
 
974
+ if (terminalSelection.options.length === 1 && terminalSelection.options[0].value === OTHER_TERMINAL_OPTION.value) {
975
+ p.note("No active programming terminals detected. Install one of Codex, Claude Code, Claude Desktop, OpenCode, OpenClaw or Hermes first.");
976
+ } else {
603
977
  const selected = await p.multiselect({
604
978
  message: "Select programming terminals to route via TokenBuddy (use Space to select, Enter to confirm):",
605
- options: choices,
979
+ options: terminalSelection.options,
606
980
  required: false
607
981
  }) as string[];
608
982
 
609
- if (selected && selected.length > 0) {
610
- spinner.start("Configuring proxy routing in selected terminals...");
983
+ const selectionError = validateInitTerminalSelection(selected);
984
+ if (selectionError) {
985
+ throw new Error(selectionError);
986
+ }
987
+
988
+ const selectedActionable = selected.filter((value) => !value.endsWith(":installed"));
989
+ const selectedOther = selectedActionable.includes(OTHER_TERMINAL_OPTION.value);
990
+ const selectedProviders = selectedActionable.filter((value) => value !== OTHER_TERMINAL_OPTION.value);
991
+
992
+ if (selectedOther) {
993
+ p.note(
994
+ [
995
+ "✅ OpenAI-compatible Proxy",
996
+ " URL: http://127.0.0.1:17821/v1",
997
+ " Probe: http://127.0.0.1:17821/v1/models",
998
+ " Token: TOKENBUDDY_PROXY",
999
+ "",
1000
+ "✅ Anthropic-compatible Proxy",
1001
+ " URL: http://127.0.0.1:17821"
1002
+ ].join("\n"),
1003
+ "TokenBuddy Proxy Interfaces"
1004
+ );
1005
+ setupSummaryLines.push("Manual terminal setup selected via Other.");
1006
+ }
1007
+
1008
+ if (selectedProviders.length > 0) {
1009
+ spinner.start("Fetching seller-backed model catalog...");
611
1010
  const proxyUrl = `http://127.0.0.1:${PROXY_PORT}`;
612
- const defaultModel = "gpt-4";
1011
+ const registryUrl = sellerRegistryUrlForInit();
1012
+ let catalog: SellerCatalogResult;
1013
+ try {
1014
+ catalog = await discoverSellerBackedModels(registryUrl);
1015
+ } catch (error: unknown) {
1016
+ spinner.stop("Failed to fetch seller-backed models.");
1017
+ throw error;
1018
+ }
1019
+ spinner.stop("Seller-backed model catalog loaded.");
1020
+
1021
+ const providerIds = selectedProviders.filter((provider): provider is ProviderId => {
1022
+ return [
1023
+ "codex",
1024
+ "claude-code",
1025
+ "claude-desktop",
1026
+ "openclaw",
1027
+ "opencode",
1028
+ "hermes",
1029
+ ].includes(provider);
1030
+ });
1031
+
1032
+ const sellerRouting = await promptSellerRoutingPreference(catalog);
1033
+ const providerSelections = await promptProviderSelections(
1034
+ providerIds,
1035
+ catalog,
1036
+ sellerRouting,
1037
+ );
1038
+
1039
+ spinner.start("Configuring proxy routing in selected terminals...");
613
1040
  const store = openBuyerStore();
614
1041
  try {
615
1042
  applyProviderInstall({
616
- providers: selected,
1043
+ providers: providerIds,
617
1044
  proxyUrl,
618
- model: defaultModel
1045
+ providerSelections,
1046
+ sellerRouting,
619
1047
  }, store);
620
1048
  } finally {
621
1049
  store.close();
622
1050
  }
623
1051
  spinner.stop("Selected terminals successfully configured.");
1052
+ setupSummaryLines.push(`${providerIds.length} programming terminal${providerIds.length === 1 ? "" : "s"} configured for TokenBuddy.`);
624
1053
  }
625
1054
  }
626
1055
 
627
1056
  // Step 2: Choose Payment Method & Scan QR Activation
1057
+ noteInitComingSoonPayments();
628
1058
  const payMethod = await p.select({
629
1059
  message: "Choose your primary payment method for LLM token purchases:",
630
- options: [
631
- { value: "clawtip", label: "JD ClawTip Pay (Scan QR Code to activate)", hint: "1 Fen activation fee" },
632
- { value: "mock", label: "Mock Wallet (For local development and tests)" }
633
- ]
634
- }) as string;
1060
+ options: INIT_PAYMENT_OPTIONS
1061
+ }) as InitPaymentMethod;
635
1062
 
636
1063
  if (payMethod === "clawtip") {
637
- spinner.start("Requesting payment activation payload from public bootstrap registry...");
1064
+ const store = openBuyerStore();
638
1065
  try {
639
- const bootstrapUrl = process.env.TOKENBUDDY_BOOTSTRAP_URL || "https://tb-wallet-bootstrap.fly.dev";
640
- const res = await fetch(`${bootstrapUrl}/payments/clawtip/bootstrap`, {
641
- method: "POST",
642
- headers: { "Content-Type": "application/json" },
643
- body: JSON.stringify({ clientTag: "cli-init" })
644
- });
645
- const data: any = await res.json();
646
- spinner.stop("Bootstrap payload received.");
647
-
648
- const qrUrl = data.payment?.resourceUrl || "https://example.com";
649
-
650
- p.note("Scan the QR code below using your JD / WeChat App to complete the 1 Fen payment activation:");
651
-
652
- // 💡 High fidelity QR code rendering directly inside the CLI terminal
653
- qrcode.generate(qrUrl, { small: true });
654
-
655
- // Start 5-second polling interval
656
- spinner.start("Waiting for JD收银台 payment confirmation (polling activation status)...");
657
- let activated = false;
658
- for (let i = 0; i < 5; i++) {
659
- await new Promise(resolve => setTimeout(resolve, 3000));
660
- // Simulate/Wait confirmed. For real deployment, poll actual backend
1066
+ let walletConfig = inspectOpenClawWalletConfig();
1067
+ const clawtipReadiness = inspectClawtipWalletReadiness(store.getPayment("clawtip"), walletConfig);
1068
+ const existingClawtip = clawtipReadiness.reusableBinding;
1069
+ if (existingClawtip) {
1070
+ store.savePayment({
1071
+ method: "clawtip",
1072
+ enabled: true,
1073
+ isDefault: true,
1074
+ config: existingClawtip.config
1075
+ });
1076
+ const details = [
1077
+ existingClawtip.orderNo ? `Order: ${existingClawtip.orderNo}` : undefined,
1078
+ existingClawtip.resourceUrl ? `ResourceUrl: ${existingClawtip.resourceUrl}` : undefined
1079
+ ].filter(Boolean).join("\n");
1080
+ logger.info("payment.channel.reused", "clawtip payment channel already configured locally", {
1081
+ method: "clawtip",
1082
+ hasOrderNo: Boolean(existingClawtip.orderNo),
1083
+ hasResourceUrl: Boolean(existingClawtip.resourceUrl)
1084
+ });
1085
+ p.note(
1086
+ details
1087
+ ? `ClawTip wallet is already configured locally.\n${details}`
1088
+ : "ClawTip wallet is already configured locally.",
1089
+ "ClawTip"
1090
+ );
1091
+ setupSummaryLines.push("ClawTip wallet already bound locally; activation skipped.");
1092
+ } else {
1093
+ if (clawtipReadiness.status === "metadata_missing_wallet") {
1094
+ p.note(
1095
+ [
1096
+ clawtipReadiness.message,
1097
+ `Expected: ${walletConfig.expectedPath}`,
1098
+ walletConfig.alternatePaths.length > 0
1099
+ ? `Alternates: ${walletConfig.alternatePaths.join(", ")}`
1100
+ : "Alternates: -"
1101
+ ].join("\n"),
1102
+ "ClawTip"
1103
+ );
1104
+ setupSummaryLines.push("Saved ClawTip metadata found, but local wallet config is missing; activation restarted.");
1105
+ }
1106
+
1107
+ const walletReadyBeforePay = walletConfig.exists;
1108
+ let openClawVersion: string | undefined;
1109
+ if (!walletReadyBeforePay) {
1110
+ spinner.start("Checking OpenClaw CLI before ClawTip wallet bootstrap...");
1111
+ openClawVersion = await checkOpenClawRuntime();
1112
+ spinner.stop("OpenClaw CLI detected.");
1113
+ }
1114
+
1115
+ spinner.start("Requesting payment activation payload from public bootstrap registry...");
1116
+ const bootstrapUrl = process.env.TOKENBUDDY_BOOTSTRAP_URL || "https://tb-wallet-bootstrap.fly.dev";
1117
+ const bootstrap = await fetchClawtipBootstrap(bootstrapUrl);
1118
+ spinner.stop("Bootstrap payload received.");
1119
+
1120
+ if (!bootstrap.payment?.orderNo || !bootstrap.payment?.indicator) {
1121
+ throw new Error("ClawTip bootstrap response missing orderNo or indicator.");
1122
+ }
1123
+
1124
+ const activationPayment = {
1125
+ orderNo: bootstrap.payment.orderNo,
1126
+ amountFen: bootstrap.payment.amountFen ?? bootstrap.activationFeeFen ?? 1,
1127
+ payTo: bootstrap.payment.payTo,
1128
+ encryptedData: bootstrap.payment.encryptedData,
1129
+ indicator: bootstrap.payment.indicator,
1130
+ slug: bootstrap.payment.slug,
1131
+ skillId: bootstrap.payment.skillId,
1132
+ description: bootstrap.payment.description,
1133
+ resourceUrl: bootstrap.payment.resourceUrl,
1134
+ };
1135
+
1136
+ spinner.start("Starting the ClawTip payment activation flow...");
1137
+ let activation = await startClawtipWalletBootstrap(activationPayment);
1138
+ spinner.stop("ClawTip payment activation finished.");
1139
+
1140
+ let payCredential = activation.payCredential;
1141
+ for (
1142
+ let authAttempt = 1;
1143
+ (walletReadyBeforePay ? !payCredential : !walletConfig.exists)
1144
+ && activation.parsedOutput.requiresWalletAuth
1145
+ && authAttempt <= 3;
1146
+ authAttempt += 1
1147
+ ) {
1148
+ let qrDisplayMessage: string | undefined;
1149
+ let manualOpenCommand: string | undefined;
1150
+ if (activation.parsedOutput.mediaPath) {
1151
+ const qrDisplay = await displayTerminalImage(activation.parsedOutput.mediaPath);
1152
+ qrDisplayMessage = qrDisplay.message;
1153
+ manualOpenCommand = qrDisplay.fallbackCommand;
1154
+ }
1155
+ if (!activation.parsedOutput.mediaPath && !activation.parsedOutput.authUrl) {
1156
+ throw new Error(
1157
+ `ClawTip pay requested authorization but did not return QR media or authUrl. Order file: ${activation.orderFile}`
1158
+ );
1159
+ }
1160
+ p.note(
1161
+ [
1162
+ activation.parsedOutput.mediaPath
1163
+ ? `Open or scan this ClawTip wallet QR image with the JD app: ${activation.parsedOutput.mediaPath}`
1164
+ : undefined,
1165
+ activation.parsedOutput.authUrl
1166
+ ? `Open or scan this ClawTip wallet auth URL with the JD app: ${activation.parsedOutput.authUrl}`
1167
+ : undefined,
1168
+ qrDisplayMessage,
1169
+ manualOpenCommand ? `Manual open command: ${manualOpenCommand}` : undefined,
1170
+ activation.parsedOutput.clawtipId ? `clawtipId: ${activation.parsedOutput.clawtipId}` : undefined,
1171
+ activation.parsedOutput.clawtipId
1172
+ ? "After scanning, ClawTip CLI will register the local agent wallet."
1173
+ : "After scanning, TokenBuddy will retry ClawTip payment activation.",
1174
+ `Expected wallet config: ${walletConfig.expectedPath}`,
1175
+ openClawVersion ? `OpenClaw: ${openClawVersion}` : undefined,
1176
+ ].filter(Boolean).join("\n"),
1177
+ activation.parsedOutput.clawtipId ? "ClawTip Wallet QR" : "ClawTip Authorization QR"
1178
+ );
1179
+
1180
+ if (activation.parsedOutput.clawtipId && !walletReadyBeforePay) {
1181
+ spinner.start("Waiting for ClawTip wallet registration. Press Ctrl+C to cancel.");
1182
+ const walletRegistered = await waitForClawtipActivationConfirmation({
1183
+ clawtipId: activation.parsedOutput.clawtipId,
1184
+ });
1185
+ spinner.stop(walletRegistered
1186
+ ? "Detected local OpenClaw ClawTip wallet config."
1187
+ : "ClawTip wallet registration cancelled.");
1188
+ if (!walletRegistered) {
1189
+ setupSummaryLines.push("ClawTip wallet registration was cancelled before local wallet config was saved.");
1190
+ process.exitCode = 1;
1191
+ return;
1192
+ }
1193
+
1194
+ walletConfig = inspectOpenClawWalletConfig();
1195
+ if (!walletConfig.exists) {
1196
+ throw new Error(`ClawTip wallet registration finished but wallet config is still missing: ${walletConfig.expectedPath}`);
1197
+ }
1198
+ setupSummaryLines.push("ClawTip wallet registered locally.");
1199
+ } else {
1200
+ const authorized = await p.confirm({
1201
+ message: "Scan or authorize this ClawTip QR in the JD app, then press Enter to retry payment activation.",
1202
+ initialValue: true,
1203
+ });
1204
+ if (authorized !== true) {
1205
+ setupSummaryLines.push("ClawTip authorization was cancelled before payment activation completed.");
1206
+ process.exitCode = 1;
1207
+ return;
1208
+ }
1209
+ walletConfig = inspectOpenClawWalletConfig();
1210
+ }
1211
+
1212
+ payCredential = readClawtipPayCredential(activation.orderFile);
1213
+ if (!payCredential) {
1214
+ spinner.start(`Retrying ClawTip payment activation after authorization (${authAttempt}/3)...`);
1215
+ activation = await startClawtipWalletBootstrap(activationPayment);
1216
+ spinner.stop("ClawTip payment activation retry finished.");
1217
+ payCredential = activation.payCredential;
1218
+ }
1219
+ }
1220
+
1221
+ const refreshedWalletConfig = inspectOpenClawWalletConfig();
1222
+ if (!walletReadyBeforePay && !refreshedWalletConfig.exists) {
1223
+ const paymentQrMessage = activation.parsedOutput.mediaPath
1224
+ ? ` ClawTip pay returned QR media: ${activation.parsedOutput.mediaPath}`
1225
+ : " ClawTip pay did not return QR media.";
1226
+ throw new Error(
1227
+ [
1228
+ `ClawTip wallet config is still missing after payment activation: ${refreshedWalletConfig.expectedPath}.`,
1229
+ `Order file: ${activation.orderFile}.`,
1230
+ paymentQrMessage.trim(),
1231
+ ].join(" ")
1232
+ );
1233
+ }
1234
+ const completedByWalletConfig = !payCredential && !walletReadyBeforePay && refreshedWalletConfig.exists;
1235
+ if (!payCredential && !completedByWalletConfig) {
1236
+ const paymentQrMessage = activation.parsedOutput.mediaPath
1237
+ ? ` ClawTip pay returned QR media: ${activation.parsedOutput.mediaPath}`
1238
+ : "";
1239
+ throw new Error(
1240
+ `ClawTip pay did not write payCredential to the order file. Order file: ${activation.orderFile}.${paymentQrMessage}`
1241
+ );
1242
+ }
1243
+ const activationCompletedBy = payCredential
1244
+ ? (refreshedWalletConfig.exists ? "payCredential+wallet-config" : "payCredential")
1245
+ : "wallet-config";
1246
+
1247
+ store.savePayment({
1248
+ method: "clawtip",
1249
+ enabled: true,
1250
+ isDefault: true,
1251
+ config: {
1252
+ bootstrapUrl,
1253
+ orderNo: bootstrap.payment?.orderNo,
1254
+ amountFen: bootstrap.payment?.amountFen ?? bootstrap.activationFeeFen,
1255
+ indicator: bootstrap.payment?.indicator,
1256
+ slug: bootstrap.payment?.slug,
1257
+ skillId: bootstrap.payment?.skillId,
1258
+ description: bootstrap.payment?.description,
1259
+ resourceUrl: bootstrap.payment?.resourceUrl,
1260
+ proofRequired: false,
1261
+ activationOrderFile: activation.orderFile,
1262
+ walletConfigPath: refreshedWalletConfig.expectedPath,
1263
+ walletConfigPresent: refreshedWalletConfig.exists,
1264
+ payCredentialWritten: Boolean(payCredential),
1265
+ activationCompletedBy
1266
+ }
1267
+ });
1268
+ logger.info("payment.channel.added", "clawtip payment channel added during init", {
1269
+ method: "clawtip",
1270
+ orderNo: bootstrap.payment?.orderNo,
1271
+ payCredentialWritten: Boolean(payCredential),
1272
+ activationCompletedBy
1273
+ });
1274
+ if (refreshedWalletConfig.exists) {
1275
+ if (!payCredential) {
1276
+ p.note(
1277
+ [
1278
+ "OpenClaw saved the local ClawTip wallet config, but the ClawTip order file did not contain payCredential.",
1279
+ `Order file: ${activation.orderFile}`,
1280
+ "TokenBuddy saved the wallet binding metadata and will rely on the local wallet for future ClawTip purchases."
1281
+ ].join("\n"),
1282
+ "ClawTip"
1283
+ );
1284
+ }
1285
+ setupSummaryLines.push("ClawTip wallet activated and set as the default payment method.");
1286
+ } else {
1287
+ p.note(
1288
+ [
1289
+ "ClawTip payment metadata was saved, but the local OpenClaw wallet config is still missing.",
1290
+ `Expected: ${refreshedWalletConfig.expectedPath}`,
1291
+ refreshedWalletConfig.alternatePaths.length > 0
1292
+ ? `Nearby files: ${refreshedWalletConfig.alternatePaths.join(", ")}`
1293
+ : "Nearby files: -",
1294
+ "Bind or restore the local wallet before using ClawTip-backed purchases."
1295
+ ].join("\n"),
1296
+ "ClawTip Wallet Required"
1297
+ );
1298
+ setupSummaryLines.push("ClawTip payment metadata saved; local wallet config still needs binding before use.");
1299
+ }
661
1300
  }
662
- spinner.stop("JD收银台 confirmed payment. ClawTip wallet is active! 🚀");
663
1301
  } catch (err: any) {
664
- spinner.stop(`Failed to fetch activation QR: ${err.message}`);
1302
+ spinner.stop(`Failed to finish ClawTip setup: ${err.message}`);
1303
+ setupSummaryLines.push("ClawTip activation requires follow-up because the bootstrap step did not complete.");
1304
+ } finally {
1305
+ store.close();
665
1306
  }
666
- } else {
667
- p.note("Mock Wallet selected. No real payments will be made. Status is active.");
668
1307
  }
669
1308
 
670
1309
  // Step 3: Install Launchd Daemon Service
@@ -716,6 +1355,7 @@ export function buildCli(): Command {
716
1355
  } catch {}
717
1356
  execSync(`launchctl load ${plistPath}`);
718
1357
  spinner.stop("LaunchAgent daemon successfully registered and started! 🚀");
1358
+ setupSummaryLines.push("Background tb-proxyd launchd service installed.");
719
1359
  } catch (err: any) {
720
1360
  spinner.stop(`Failed to write launchd plist: ${err.message}`);
721
1361
  }
@@ -723,9 +1363,10 @@ export function buildCli(): Command {
723
1363
  } else {
724
1364
  // Run background dettached child process in linux/windows
725
1365
  p.note("System daemon is active. Process runs in dettached background.");
1366
+ setupSummaryLines.push("Background daemon mode is available on this system.");
726
1367
  }
727
1368
 
728
- p.outro("🎉 Setup complete! Run `tb doctor` to audit status anytime. Let's code!");
1369
+ p.outro(buildInitSuccessMessage(setupSummaryLines));
729
1370
  });
730
1371
 
731
1372
  return program;