@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.
Files changed (42) 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 +4 -4
  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
  41. package/bin/tb-proxyd.js +0 -2
  42. package/bin/tb.js +0 -3
package/dist/src/cli.js CHANGED
@@ -6,10 +6,13 @@ import * as os from "os";
6
6
  import { execSync, spawn } from "child_process";
7
7
  import Table from "cli-table3";
8
8
  import { BuyerStore } from "./buyer-store.js";
9
- import { applyProviderInstall, detectProviders } from "./provider-install.js";
9
+ import { applyProviderInstall, detectProviders, getProviderModelSelectionKind, getProviderProtocolPreference, } from "./provider-install.js";
10
10
  import { createModuleLogger } from "@tokenbuddy/logging";
11
11
  import * as crypto from "crypto";
12
12
  import { fileURLToPath } from "url";
13
+ import { discoverSellerBackedModels, filterCatalogByProtocol, filterCatalogBySeller, } from "./seller-catalog.js";
14
+ import { collectDoctorDiagnostics, collectDoctorModelsSummary, printDoctorProviders, printDoctorModelsSummary, readDoctorProviders, renderDoctorDiagnosticsProgressively, } from "./doctor-diagnostics.js";
15
+ import { buildInitTerminalOptions, buildInitSuccessMessage, detectExistingClawtipBinding, INIT_PAYMENT_OPTIONS, noteInitComingSoonPayments, OTHER_TERMINAL_OPTION, validateInitTerminalSelection, } from "./init-payment-options.js";
13
16
  // @ts-ignore
14
17
  import qrcode from "qrcode-terminal";
15
18
  const CONTROL_PORT = 17820;
@@ -262,6 +265,210 @@ function readProof(options) {
262
265
  }
263
266
  return proof;
264
267
  }
268
+ function sellerRegistryUrlForInit() {
269
+ return process.env.TB_PROXYD_SELLER_REGISTRY_URL || "https://tb-wallet-bootstrap.fly.dev/registry/sellers";
270
+ }
271
+ function stableModelChoices(models) {
272
+ const grouped = new Map();
273
+ for (const entry of models) {
274
+ const list = grouped.get(entry.id) || [];
275
+ list.push(entry);
276
+ grouped.set(entry.id, list);
277
+ }
278
+ return Array.from(grouped.entries()).map(([modelId, entries]) => {
279
+ const sellerIds = Array.from(new Set(entries.map((entry) => entry.sellerId)));
280
+ const protocols = Array.from(new Set(entries.flatMap((entry) => entry.supportedProtocols)));
281
+ return {
282
+ value: modelId,
283
+ label: modelId,
284
+ hint: `${sellerIds.join(",")} · ${protocols.join(",") || "no-protocol"}`,
285
+ };
286
+ });
287
+ }
288
+ async function promptSellerRoutingPreference(catalog) {
289
+ const healthySellers = catalog.sellers.filter((seller) => seller.status === "ok");
290
+ const mode = await p.select({
291
+ message: "Choose seller routing mode for tb-proxyd:",
292
+ options: [
293
+ {
294
+ value: "auto",
295
+ label: "Auto",
296
+ hint: "Automatically choose a compatible seller based on the requested model.",
297
+ },
298
+ {
299
+ value: "fixed",
300
+ label: "Fixed Seller",
301
+ hint: "Pin tb-proxyd to one seller and only use models from that seller.",
302
+ },
303
+ ],
304
+ });
305
+ if (typeof mode !== "string") {
306
+ throw new Error("seller routing selection was cancelled");
307
+ }
308
+ if (mode === "auto") {
309
+ return { mode };
310
+ }
311
+ if (healthySellers.length === 0) {
312
+ throw new Error("no healthy sellers available for fixed routing");
313
+ }
314
+ const sellerId = await p.select({
315
+ message: "Choose the seller to pin tb-proxyd to:",
316
+ options: healthySellers.map((seller) => ({
317
+ value: seller.id,
318
+ label: seller.name ? `${seller.name} (${seller.id})` : seller.id,
319
+ hint: [
320
+ seller.discountRatio != null ? `discount x${seller.discountRatio}` : null,
321
+ seller.modelCount != null ? `${seller.modelCount} models` : null,
322
+ seller.supportedProtocols?.length ? seller.supportedProtocols.join(",") : null,
323
+ seller.paymentMethods?.length ? seller.paymentMethods.join(",") : null,
324
+ ]
325
+ .filter(Boolean)
326
+ .join(" · ") || seller.url,
327
+ })),
328
+ });
329
+ if (typeof sellerId !== "string") {
330
+ throw new Error("fixed seller selection was cancelled");
331
+ }
332
+ return {
333
+ mode,
334
+ sellerId,
335
+ };
336
+ }
337
+ async function promptSingleModelSelection(providerId, models, sellerRouting) {
338
+ const protocolPreference = getProviderProtocolPreference(providerId);
339
+ const protocolFiltered = protocolPreference
340
+ ? filterCatalogByProtocol(models, protocolPreference)
341
+ : models;
342
+ const choices = stableModelChoices(protocolFiltered);
343
+ if (choices.length === 0) {
344
+ throw new Error(`no compatible models available for ${providerId}`);
345
+ }
346
+ const labelMap = {
347
+ opencode: "OpenCode",
348
+ codex: "Codex",
349
+ openclaw: "OpenClaw",
350
+ hermes: "Hermes",
351
+ "claude-desktop": "Claude Desktop",
352
+ "claude-code": "Claude Code",
353
+ };
354
+ const selectedModel = await p.select({
355
+ message: `Choose the default model for ${labelMap[providerId] || providerId}:`,
356
+ options: choices,
357
+ });
358
+ if (typeof selectedModel !== "string") {
359
+ throw new Error(`default model selection was cancelled for ${providerId}`);
360
+ }
361
+ const selectedEntry = protocolFiltered.find((entry) => entry.id === selectedModel);
362
+ return {
363
+ selectionKind: "single-model",
364
+ protocolPreference,
365
+ defaultModel: selectedModel,
366
+ sellerId: sellerRouting.mode === "fixed" ? selectedEntry?.sellerId : undefined,
367
+ };
368
+ }
369
+ function defaultClaudeDisplayName(modelId) {
370
+ return modelId.trim();
371
+ }
372
+ function makeClaudeRoleMapping(modelId) {
373
+ const displayName = defaultClaudeDisplayName(modelId);
374
+ return {
375
+ selectionKind: "claude-role-mapping",
376
+ protocolPreference: "messages",
377
+ fallbackModel: modelId,
378
+ roles: {
379
+ sonnet: {
380
+ upstreamModel: modelId,
381
+ displayName,
382
+ declareOneM: true,
383
+ },
384
+ opus: {
385
+ upstreamModel: modelId,
386
+ displayName,
387
+ declareOneM: true,
388
+ },
389
+ haiku: {
390
+ upstreamModel: modelId,
391
+ displayName,
392
+ declareOneM: false,
393
+ },
394
+ },
395
+ };
396
+ }
397
+ async function promptClaudeCodeModelSelection(models) {
398
+ const protocolFiltered = filterCatalogByProtocol(models, "messages");
399
+ const choices = stableModelChoices(protocolFiltered);
400
+ if (choices.length === 0) {
401
+ throw new Error("no compatible message models available for Claude Code");
402
+ }
403
+ const sonnetModel = await p.select({
404
+ message: "Choose the default Sonnet model for Claude Code:",
405
+ options: choices,
406
+ });
407
+ if (typeof sonnetModel !== "string") {
408
+ throw new Error("Claude Code model selection was cancelled");
409
+ }
410
+ const mirrorAllRoles = await p.confirm({
411
+ message: "Use the same model for Opus and Haiku as well?",
412
+ initialValue: true,
413
+ });
414
+ if (typeof mirrorAllRoles !== "boolean") {
415
+ throw new Error("Claude Code role mapping confirmation was cancelled");
416
+ }
417
+ if (mirrorAllRoles) {
418
+ return makeClaudeRoleMapping(sonnetModel);
419
+ }
420
+ const opusModel = await p.select({
421
+ message: "Choose the default Opus model for Claude Code:",
422
+ options: choices,
423
+ });
424
+ if (typeof opusModel !== "string") {
425
+ throw new Error("Claude Code Opus model selection was cancelled");
426
+ }
427
+ const haikuModel = await p.select({
428
+ message: "Choose the default Haiku model for Claude Code:",
429
+ options: choices,
430
+ });
431
+ if (typeof haikuModel !== "string") {
432
+ throw new Error("Claude Code Haiku model selection was cancelled");
433
+ }
434
+ return {
435
+ selectionKind: "claude-role-mapping",
436
+ protocolPreference: "messages",
437
+ fallbackModel: sonnetModel,
438
+ roles: {
439
+ sonnet: {
440
+ upstreamModel: sonnetModel,
441
+ displayName: defaultClaudeDisplayName(sonnetModel),
442
+ declareOneM: true,
443
+ },
444
+ opus: {
445
+ upstreamModel: opusModel,
446
+ displayName: defaultClaudeDisplayName(opusModel),
447
+ declareOneM: true,
448
+ },
449
+ haiku: {
450
+ upstreamModel: haikuModel,
451
+ displayName: defaultClaudeDisplayName(haikuModel),
452
+ declareOneM: false,
453
+ },
454
+ },
455
+ };
456
+ }
457
+ async function promptProviderSelections(providerIds, catalog, sellerRouting) {
458
+ const baseModels = sellerRouting.mode === "fixed"
459
+ ? filterCatalogBySeller(catalog.models, sellerRouting.sellerId)
460
+ : catalog.models;
461
+ const selections = {};
462
+ for (const providerId of providerIds) {
463
+ const selectionKind = getProviderModelSelectionKind(providerId);
464
+ if (selectionKind === "claude-role-mapping") {
465
+ selections[providerId] = await promptClaudeCodeModelSelection(baseModels);
466
+ continue;
467
+ }
468
+ selections[providerId] = await promptSingleModelSelection(providerId, baseModels, sellerRouting);
469
+ }
470
+ return selections;
471
+ }
265
472
  export function buildCli() {
266
473
  const program = new Command();
267
474
  program
@@ -284,7 +491,6 @@ export function buildCli() {
284
491
  const plistPath = process.platform === "darwin"
285
492
  ? path.join(os.homedir(), "Library", "LaunchAgents", "com.tokenbuddy.proxyd.plist")
286
493
  : undefined;
287
- const candidates = detectProviders();
288
494
  let probe = await probeDaemonStatus(controlPort);
289
495
  let repair = { attempted: false, fixed: false };
290
496
  if (!probe.running && options.fix) {
@@ -295,10 +501,22 @@ export function buildCli() {
295
501
  const daemonInfo = probe.status;
296
502
  const daemonRunning = probe.running;
297
503
  const daemonError = probe.error;
504
+ const daemonStatus = daemonInfo && typeof daemonInfo === "object"
505
+ ? daemonInfo
506
+ : undefined;
507
+ const providers = readDoctorProviders();
298
508
  if (options.fix && repair.attempted && !repair.fixed) {
299
509
  process.exitCode = 1;
300
510
  }
301
511
  if (options.json) {
512
+ const diagnostics = await collectDoctorDiagnostics({
513
+ controlPort,
514
+ proxyPort,
515
+ daemonRunning,
516
+ daemonError,
517
+ providers,
518
+ sellerRegistryUrl: daemonStatus?.sellerRegistryUrl,
519
+ });
302
520
  console.log(JSON.stringify({
303
521
  daemon: {
304
522
  running: daemonRunning,
@@ -318,7 +536,7 @@ export function buildCli() {
318
536
  plistPath,
319
537
  plistExists: plistPath ? fs.existsSync(plistPath) : false
320
538
  },
321
- providers: candidates
539
+ ...diagnostics,
322
540
  }, null, 2));
323
541
  return;
324
542
  }
@@ -351,12 +569,28 @@ export function buildCli() {
351
569
  console.log("⚠ LaunchAgent plist does NOT exist. Run `tb init` to install it as service.");
352
570
  }
353
571
  }
354
- // 3. Detect terminals
355
- console.log("\n--- Programming Terminals Detection ---");
356
- for (const c of candidates) {
357
- const icon = c.detected ? "✅" : "🔘";
358
- console.log(`${icon} ${c.name}: ${c.reason}`);
572
+ if (daemonStatus) {
573
+ console.log(` Control Plane URL: ${controlUrl}`);
574
+ console.log(` Proxy Plane URL: http://127.0.0.1:${proxyPort}`);
575
+ if (daemonStatus.sellerRoutingMode || daemonStatus.selectionMode) {
576
+ console.log(` Routing Mode: ${daemonStatus.sellerRoutingMode || daemonStatus.selectionMode}`);
577
+ }
578
+ if (daemonStatus.selectedSellerId) {
579
+ console.log(` Selected Seller: ${daemonStatus.selectedSellerId}`);
580
+ }
581
+ if (daemonStatus.sellerRegistryUrl) {
582
+ console.log(` Registry URL: ${daemonStatus.sellerRegistryUrl}`);
583
+ }
359
584
  }
585
+ printDoctorProviders(providers);
586
+ await renderDoctorDiagnosticsProgressively({
587
+ controlPort,
588
+ proxyPort,
589
+ daemonRunning,
590
+ daemonError,
591
+ sellerRegistryUrl: daemonStatus?.sellerRegistryUrl,
592
+ providers,
593
+ });
360
594
  });
361
595
  // 2. tb payment
362
596
  const payment = program.command("payment").description("Manage payment methods");
@@ -481,21 +715,30 @@ export function buildCli() {
481
715
  .option("--json", "Output model list as JSON")
482
716
  .action(async (options) => {
483
717
  try {
718
+ const controlPort = configuredControlPort();
719
+ const proxyPort = configuredProxyPort();
720
+ const status = await probeDaemonStatus(controlPort);
721
+ const daemonInfo = status.status && typeof status.status === "object"
722
+ ? status.status
723
+ : undefined;
724
+ const models = await collectDoctorModelsSummary({
725
+ controlPort,
726
+ proxyPort,
727
+ daemonRunning: status.running,
728
+ daemonError: status.error,
729
+ sellerRegistryUrl: daemonInfo?.sellerRegistryUrl,
730
+ });
484
731
  if (options.json) {
485
- const response = await fetch(`http://127.0.0.1:${configuredControlPort()}/models`);
486
- const body = await response.text();
487
- if (!response.ok) {
488
- throw new Error(body || `HTTP ${response.status}`);
732
+ console.log(JSON.stringify(models, null, 2));
733
+ if (!models.available) {
734
+ process.exitCode = 1;
489
735
  }
490
- JSON.parse(body);
491
- console.log(body);
492
736
  return;
493
737
  }
494
- const table = new Table({ head: ["Model ID", "Input Price/1M", "Output Price/1M", "Supported Protocols"] });
495
- // Sample static model config from seller mock
496
- table.push(["gpt-4", "1.0 USD (or equivalent points)", "3.0 USD", "OpenAI, Direct"]);
497
- console.log("=== Available LLM Models Matrix ===");
498
- console.log(table.toString());
738
+ printDoctorModelsSummary(models);
739
+ if (!models.available) {
740
+ process.exitCode = 1;
741
+ }
499
742
  }
500
743
  catch (err) {
501
744
  console.error("Error connecting to local proxy:", err.message);
@@ -508,82 +751,165 @@ export function buildCli() {
508
751
  .description("Launch step-by-step interactive setup wizard")
509
752
  .action(async () => {
510
753
  p.intro("🚀 Welcome to TokenBuddy Interactive Wizard!");
754
+ const setupSummaryLines = [];
511
755
  // Step 1: Scan coding terminals
512
756
  const spinner = p.spinner();
513
757
  spinner.start("Scanning local system for programming terminals...");
514
758
  const candidates = detectProviders();
515
- const detected = candidates.filter(c => c.detected);
759
+ const terminalOptions = buildInitTerminalOptions(candidates);
516
760
  spinner.stop("Scan completed.");
517
- if (detected.length === 0) {
518
- p.note("No active programming terminals detected. Install one of Codex, Claude Code, Claude Desktop, OpenClaw or Hermes first.");
761
+ if (terminalOptions.length === 1 && terminalOptions[0].value === OTHER_TERMINAL_OPTION.value) {
762
+ p.note("No active programming terminals detected. Install one of Codex, Claude Code, Claude Desktop, OpenCode, OpenClaw or Hermes first.");
519
763
  }
520
764
  else {
521
- const choices = detected.map(c => ({
522
- value: c.id,
523
- label: c.name,
524
- hint: c.configPath
525
- }));
526
765
  const selected = await p.multiselect({
527
766
  message: "Select programming terminals to route via TokenBuddy (use Space to select, Enter to confirm):",
528
- options: choices,
767
+ options: terminalOptions,
529
768
  required: false
530
769
  });
531
- if (selected && selected.length > 0) {
532
- spinner.start("Configuring proxy routing in selected terminals...");
770
+ const selectionError = validateInitTerminalSelection(selected);
771
+ if (selectionError) {
772
+ throw new Error(selectionError);
773
+ }
774
+ const selectedActionable = selected.filter((value) => !value.endsWith(":installed"));
775
+ const selectedOther = selectedActionable.includes(OTHER_TERMINAL_OPTION.value);
776
+ const selectedProviders = selectedActionable.filter((value) => value !== OTHER_TERMINAL_OPTION.value);
777
+ if (selectedOther) {
778
+ p.note([
779
+ "✅ OpenAI-compatible Proxy",
780
+ " URL: http://127.0.0.1:17821/v1",
781
+ " Probe: http://127.0.0.1:17821/v1/models",
782
+ " Token: TOKENBUDDY_PROXY",
783
+ "",
784
+ "✅ Anthropic-compatible Proxy",
785
+ " URL: http://127.0.0.1:17821"
786
+ ].join("\n"), "TokenBuddy Proxy Interfaces");
787
+ setupSummaryLines.push("Manual terminal setup selected via Other.");
788
+ }
789
+ if (selectedProviders.length > 0) {
790
+ spinner.start("Fetching seller-backed model catalog...");
533
791
  const proxyUrl = `http://127.0.0.1:${PROXY_PORT}`;
534
- const defaultModel = "gpt-4";
792
+ const registryUrl = sellerRegistryUrlForInit();
793
+ let catalog;
794
+ try {
795
+ catalog = await discoverSellerBackedModels(registryUrl);
796
+ }
797
+ catch (error) {
798
+ spinner.stop("Failed to fetch seller-backed models.");
799
+ throw error;
800
+ }
801
+ spinner.stop("Seller-backed model catalog loaded.");
802
+ const providerIds = selectedProviders.filter((provider) => {
803
+ return [
804
+ "codex",
805
+ "claude-code",
806
+ "claude-desktop",
807
+ "openclaw",
808
+ "opencode",
809
+ "hermes",
810
+ ].includes(provider);
811
+ });
812
+ const sellerRouting = await promptSellerRoutingPreference(catalog);
813
+ const providerSelections = await promptProviderSelections(providerIds, catalog, sellerRouting);
814
+ spinner.start("Configuring proxy routing in selected terminals...");
535
815
  const store = openBuyerStore();
536
816
  try {
537
817
  applyProviderInstall({
538
- providers: selected,
818
+ providers: providerIds,
539
819
  proxyUrl,
540
- model: defaultModel
820
+ providerSelections,
821
+ sellerRouting,
541
822
  }, store);
542
823
  }
543
824
  finally {
544
825
  store.close();
545
826
  }
546
827
  spinner.stop("Selected terminals successfully configured.");
828
+ setupSummaryLines.push(`${providerIds.length} programming terminal${providerIds.length === 1 ? "" : "s"} configured for TokenBuddy.`);
547
829
  }
548
830
  }
549
831
  // Step 2: Choose Payment Method & Scan QR Activation
832
+ noteInitComingSoonPayments();
550
833
  const payMethod = await p.select({
551
834
  message: "Choose your primary payment method for LLM token purchases:",
552
- options: [
553
- { value: "clawtip", label: "JD ClawTip Pay (Scan QR Code to activate)", hint: "1 Fen activation fee" },
554
- { value: "mock", label: "Mock Wallet (For local development and tests)" }
555
- ]
835
+ options: INIT_PAYMENT_OPTIONS
556
836
  });
557
837
  if (payMethod === "clawtip") {
558
- spinner.start("Requesting payment activation payload from public bootstrap registry...");
838
+ const store = openBuyerStore();
559
839
  try {
560
- const bootstrapUrl = process.env.TOKENBUDDY_BOOTSTRAP_URL || "https://tb-wallet-bootstrap.fly.dev";
561
- const res = await fetch(`${bootstrapUrl}/payments/clawtip/bootstrap`, {
562
- method: "POST",
563
- headers: { "Content-Type": "application/json" },
564
- body: JSON.stringify({ clientTag: "cli-init" })
565
- });
566
- const data = await res.json();
567
- spinner.stop("Bootstrap payload received.");
568
- const qrUrl = data.payment?.resourceUrl || "https://example.com";
569
- p.note("Scan the QR code below using your JD / WeChat App to complete the 1 Fen payment activation:");
570
- // 💡 High fidelity QR code rendering directly inside the CLI terminal
571
- qrcode.generate(qrUrl, { small: true });
572
- // Start 5-second polling interval
573
- spinner.start("Waiting for JD收银台 payment confirmation (polling activation status)...");
574
- let activated = false;
575
- for (let i = 0; i < 5; i++) {
576
- await new Promise(resolve => setTimeout(resolve, 3000));
577
- // Simulate/Wait confirmed. For real deployment, poll actual backend
840
+ const existingClawtip = detectExistingClawtipBinding(store.getPayment("clawtip"));
841
+ if (existingClawtip) {
842
+ store.savePayment({
843
+ method: "clawtip",
844
+ enabled: true,
845
+ isDefault: true,
846
+ config: existingClawtip.config
847
+ });
848
+ const details = [
849
+ existingClawtip.orderNo ? `Order: ${existingClawtip.orderNo}` : undefined,
850
+ existingClawtip.resourceUrl ? `ResourceUrl: ${existingClawtip.resourceUrl}` : undefined
851
+ ].filter(Boolean).join("\n");
852
+ logger.info("payment.channel.reused", "clawtip payment channel already configured locally", {
853
+ method: "clawtip",
854
+ hasOrderNo: Boolean(existingClawtip.orderNo),
855
+ hasResourceUrl: Boolean(existingClawtip.resourceUrl)
856
+ });
857
+ p.note(details
858
+ ? `ClawTip wallet is already configured locally.\n${details}`
859
+ : "ClawTip wallet is already configured locally.", "ClawTip");
860
+ setupSummaryLines.push("ClawTip wallet already bound locally; activation skipped.");
861
+ }
862
+ else {
863
+ spinner.start("Requesting payment activation payload from public bootstrap registry...");
864
+ const bootstrapUrl = process.env.TOKENBUDDY_BOOTSTRAP_URL || "https://tb-wallet-bootstrap.fly.dev";
865
+ const res = await fetch(`${bootstrapUrl}/payments/clawtip/bootstrap`, {
866
+ method: "POST",
867
+ headers: { "Content-Type": "application/json" },
868
+ body: JSON.stringify({ clientTag: "cli-init" })
869
+ });
870
+ const data = await res.json();
871
+ spinner.stop("Bootstrap payload received.");
872
+ const qrUrl = data.payment?.resourceUrl || "https://example.com";
873
+ p.note("Scan the QR code below using your JD / WeChat App to complete the 1 Fen payment activation:");
874
+ // 💡 High fidelity QR code rendering directly inside the CLI terminal
875
+ qrcode.generate(qrUrl, { small: true });
876
+ // Start 5-second polling interval
877
+ spinner.start("Waiting for JD收银台 payment confirmation (polling activation status)...");
878
+ for (let i = 0; i < 5; i++) {
879
+ await new Promise(resolve => setTimeout(resolve, 3000));
880
+ // Simulate/Wait confirmed. For real deployment, poll actual backend
881
+ }
882
+ spinner.stop("JD收银台 confirmed payment. ClawTip wallet is active! 🚀");
883
+ store.savePayment({
884
+ method: "clawtip",
885
+ enabled: true,
886
+ isDefault: true,
887
+ config: {
888
+ bootstrapUrl,
889
+ orderNo: data.payment?.orderNo,
890
+ amountFen: data.payment?.amountFen ?? data.activationFeeFen,
891
+ indicator: data.payment?.indicator,
892
+ slug: data.payment?.slug,
893
+ skillId: data.payment?.skillId,
894
+ description: data.payment?.description,
895
+ resourceUrl: data.payment?.resourceUrl,
896
+ proofRequired: false
897
+ }
898
+ });
899
+ logger.info("payment.channel.added", "clawtip payment channel added during init", {
900
+ method: "clawtip",
901
+ orderNo: data.payment?.orderNo
902
+ });
903
+ setupSummaryLines.push("ClawTip wallet activated and set as the default payment method.");
578
904
  }
579
- spinner.stop("JD收银台 confirmed payment. ClawTip wallet is active! 🚀");
580
905
  }
581
906
  catch (err) {
582
- spinner.stop(`Failed to fetch activation QR: ${err.message}`);
907
+ spinner.stop(`Failed to finish ClawTip setup: ${err.message}`);
908
+ setupSummaryLines.push("ClawTip activation requires follow-up because the bootstrap step did not complete.");
909
+ }
910
+ finally {
911
+ store.close();
583
912
  }
584
- }
585
- else {
586
- p.note("Mock Wallet selected. No real payments will be made. Status is active.");
587
913
  }
588
914
  // Step 3: Install Launchd Daemon Service
589
915
  if (process.platform === "darwin") {
@@ -631,6 +957,7 @@ export function buildCli() {
631
957
  catch { }
632
958
  execSync(`launchctl load ${plistPath}`);
633
959
  spinner.stop("LaunchAgent daemon successfully registered and started! 🚀");
960
+ setupSummaryLines.push("Background tb-proxyd launchd service installed.");
634
961
  }
635
962
  catch (err) {
636
963
  spinner.stop(`Failed to write launchd plist: ${err.message}`);
@@ -640,8 +967,9 @@ export function buildCli() {
640
967
  else {
641
968
  // Run background dettached child process in linux/windows
642
969
  p.note("System daemon is active. Process runs in dettached background.");
970
+ setupSummaryLines.push("Background daemon mode is available on this system.");
643
971
  }
644
- p.outro("🎉 Setup complete! Run `tb doctor` to audit status anytime. Let's code!");
972
+ p.outro(buildInitSuccessMessage(setupSummaryLines));
645
973
  });
646
974
  return program;
647
975
  }