@tokenbuddy/tokenbuddy 1.0.5 → 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.
Files changed (40) hide show
  1. package/dist/src/buyer-store.d.ts +20 -0
  2. package/dist/src/buyer-store.d.ts.map +1 -1
  3. package/dist/src/buyer-store.js +73 -1
  4. package/dist/src/buyer-store.js.map +1 -1
  5. package/dist/src/cli.d.ts.map +1 -1
  6. package/dist/src/cli.js +390 -62
  7. package/dist/src/cli.js.map +1 -1
  8. package/dist/src/daemon.d.ts +6 -5
  9. package/dist/src/daemon.d.ts.map +1 -1
  10. package/dist/src/daemon.js +298 -92
  11. package/dist/src/daemon.js.map +1 -1
  12. package/dist/src/doctor-diagnostics.d.ts +97 -0
  13. package/dist/src/doctor-diagnostics.d.ts.map +1 -0
  14. package/dist/src/doctor-diagnostics.js +547 -0
  15. package/dist/src/doctor-diagnostics.js.map +1 -0
  16. package/dist/src/init-payment-options.d.ts +34 -0
  17. package/dist/src/init-payment-options.d.ts.map +1 -0
  18. package/dist/src/init-payment-options.js +90 -0
  19. package/dist/src/init-payment-options.js.map +1 -0
  20. package/dist/src/provider-install.d.ts +37 -2
  21. package/dist/src/provider-install.d.ts.map +1 -1
  22. package/dist/src/provider-install.js +317 -67
  23. package/dist/src/provider-install.js.map +1 -1
  24. package/dist/src/seller-catalog.d.ts +79 -0
  25. package/dist/src/seller-catalog.d.ts.map +1 -0
  26. package/dist/src/seller-catalog.js +126 -0
  27. package/dist/src/seller-catalog.js.map +1 -0
  28. package/dist/src/tb-proxyd.js +13 -2
  29. package/dist/src/tb-proxyd.js.map +1 -1
  30. package/package.json +1 -1
  31. package/src/buyer-store.ts +113 -1
  32. package/src/cli.ts +490 -67
  33. package/src/daemon.ts +346 -117
  34. package/src/doctor-diagnostics.ts +850 -0
  35. package/src/init-payment-options.ts +131 -0
  36. package/src/provider-install.ts +426 -76
  37. package/src/seller-catalog.ts +222 -0
  38. package/src/tb-proxyd.ts +14 -2
  39. package/tests/e2e.test.ts +9 -0
  40. package/tests/tokenbuddy.test.ts +628 -19
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 { 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
+ 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
- providers: candidates
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
- // 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}`);
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
- 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}`);
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
- 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());
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 detected = candidates.filter(c => c.detected);
916
+ const terminalOptions = buildInitTerminalOptions(candidates);
592
917
  spinner.stop("Scan completed.");
593
918
 
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.");
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: choices,
924
+ options: terminalOptions,
606
925
  required: false
607
926
  }) as string[];
608
927
 
609
- if (selected && selected.length > 0) {
610
- spinner.start("Configuring proxy routing in selected terminals...");
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 defaultModel = "gpt-4";
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: selected,
988
+ providers: providerIds,
617
989
  proxyUrl,
618
- model: defaultModel
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
- { 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;
1005
+ options: INIT_PAYMENT_OPTIONS
1006
+ }) as InitPaymentMethod;
635
1007
 
636
1008
  if (payMethod === "clawtip") {
637
- spinner.start("Requesting payment activation payload from public bootstrap registry...");
1009
+ const store = openBuyerStore();
638
1010
  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
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 fetch activation QR: ${err.message}`);
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("🎉 Setup complete! Run `tb doctor` to audit status anytime. Let's code!");
1151
+ p.outro(buildInitSuccessMessage(setupSummaryLines));
729
1152
  });
730
1153
 
731
1154
  return program;