@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/dist/src/cli.js CHANGED
@@ -6,10 +6,15 @@ 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 { buildInitSuccessMessage, buildInitTerminalSelectionState, buildInstalledTerminalMessage, INIT_PAYMENT_OPTIONS, inspectClawtipWalletReadiness, inspectOpenClawWalletConfig, noteInitComingSoonPayments, OTHER_TERMINAL_OPTION, validateInitTerminalSelection, } from "./init-payment-options.js";
16
+ import { checkOpenClawRuntime, readClawtipPayCredential, startClawtipWalletBootstrap, waitForClawtipActivationConfirmation, } from "./init-clawtip-activation.js";
17
+ import { displayTerminalImage } from "./terminal-image.js";
13
18
  // @ts-ignore
14
19
  import qrcode from "qrcode-terminal";
15
20
  const CONTROL_PORT = 17820;
@@ -75,6 +80,7 @@ async function probeDaemonStatus(controlPort) {
75
80
  };
76
81
  }
77
82
  }
83
+ const CLAWTIP_BOOTSTRAP_PLACEHOLDER_PAY_TO = "bootstrap-pay-to";
78
84
  async function waitForDaemonStatus(controlPort, timeoutMs) {
79
85
  const deadline = Date.now() + timeoutMs;
80
86
  let latest = { running: false, error: "not checked" };
@@ -230,7 +236,7 @@ function printPaymentList(payments, asJson) {
230
236
  console.log("=== TokenBuddy Payment Methods ===");
231
237
  console.log(table.toString());
232
238
  }
233
- async function fetchClawtipBootstrap(bootstrapUrl) {
239
+ export async function fetchClawtipBootstrap(bootstrapUrl) {
234
240
  const response = await fetch(`${bootstrapUrl.replace(/\/+$/, "")}/payments/clawtip/bootstrap`, {
235
241
  method: "POST",
236
242
  headers: { "Content-Type": "application/json" },
@@ -243,8 +249,32 @@ async function fetchClawtipBootstrap(bootstrapUrl) {
243
249
  if (!body.payment?.orderNo || !body.payment.indicator || !body.payment.resourceUrl) {
244
250
  throw new Error("ClawTip bootstrap response missing payment order fields");
245
251
  }
252
+ if ((body.payment.payTo || "").trim() === CLAWTIP_BOOTSTRAP_PLACEHOLDER_PAY_TO) {
253
+ throw new Error([
254
+ `ClawTip bootstrap service is misconfigured: payTo is still the placeholder \`${CLAWTIP_BOOTSTRAP_PLACEHOLDER_PAY_TO}\`.`,
255
+ `Bootstrap URL: ${bootstrapUrl}`,
256
+ "Configure the bootstrap service with the real ClawTip merchant pay_to before retrying `tb init`.",
257
+ ].join(" "));
258
+ }
259
+ body.payment.resourceUrl = normalizeClawtipBootstrapResourceUrl(bootstrapUrl, body.payment.resourceUrl);
246
260
  return body;
247
261
  }
262
+ export function normalizeClawtipBootstrapResourceUrl(bootstrapUrl, resourceUrl) {
263
+ try {
264
+ const bootstrap = new URL(bootstrapUrl);
265
+ const resource = new URL(resourceUrl);
266
+ if (resource.origin === bootstrap.origin && resource.pathname === "/registry/sellers") {
267
+ resource.pathname = bootstrap.pathname.replace(/\/+$/, "") || "/";
268
+ resource.search = "";
269
+ resource.hash = "";
270
+ return resource.toString().replace(/\/$/, "");
271
+ }
272
+ }
273
+ catch {
274
+ // Leave the server-provided value unchanged when URL parsing fails.
275
+ }
276
+ return resourceUrl;
277
+ }
248
278
  function readProof(options) {
249
279
  const proofFile = options.proofFile || process.env.TOKENBUDDY_CLAWTIP_PROOF_FILE;
250
280
  if (!proofFile) {
@@ -262,6 +292,210 @@ function readProof(options) {
262
292
  }
263
293
  return proof;
264
294
  }
295
+ function sellerRegistryUrlForInit() {
296
+ return process.env.TB_PROXYD_SELLER_REGISTRY_URL || "https://tb-wallet-bootstrap.fly.dev/registry/sellers";
297
+ }
298
+ function stableModelChoices(models) {
299
+ const grouped = new Map();
300
+ for (const entry of models) {
301
+ const list = grouped.get(entry.id) || [];
302
+ list.push(entry);
303
+ grouped.set(entry.id, list);
304
+ }
305
+ return Array.from(grouped.entries()).map(([modelId, entries]) => {
306
+ const sellerIds = Array.from(new Set(entries.map((entry) => entry.sellerId)));
307
+ const protocols = Array.from(new Set(entries.flatMap((entry) => entry.supportedProtocols)));
308
+ return {
309
+ value: modelId,
310
+ label: modelId,
311
+ hint: `${sellerIds.join(",")} · ${protocols.join(",") || "no-protocol"}`,
312
+ };
313
+ });
314
+ }
315
+ async function promptSellerRoutingPreference(catalog) {
316
+ const healthySellers = catalog.sellers.filter((seller) => seller.status === "ok");
317
+ const mode = await p.select({
318
+ message: "Choose seller routing mode for tb-proxyd:",
319
+ options: [
320
+ {
321
+ value: "auto",
322
+ label: "Auto",
323
+ hint: "Automatically choose a compatible seller based on the requested model.",
324
+ },
325
+ {
326
+ value: "fixed",
327
+ label: "Fixed Seller",
328
+ hint: "Pin tb-proxyd to one seller and only use models from that seller.",
329
+ },
330
+ ],
331
+ });
332
+ if (typeof mode !== "string") {
333
+ throw new Error("seller routing selection was cancelled");
334
+ }
335
+ if (mode === "auto") {
336
+ return { mode };
337
+ }
338
+ if (healthySellers.length === 0) {
339
+ throw new Error("no healthy sellers available for fixed routing");
340
+ }
341
+ const sellerId = await p.select({
342
+ message: "Choose the seller to pin tb-proxyd to:",
343
+ options: healthySellers.map((seller) => ({
344
+ value: seller.id,
345
+ label: seller.name ? `${seller.name} (${seller.id})` : seller.id,
346
+ hint: [
347
+ seller.discountRatio != null ? `discount x${seller.discountRatio}` : null,
348
+ seller.modelCount != null ? `${seller.modelCount} models` : null,
349
+ seller.supportedProtocols?.length ? seller.supportedProtocols.join(",") : null,
350
+ seller.paymentMethods?.length ? seller.paymentMethods.join(",") : null,
351
+ ]
352
+ .filter(Boolean)
353
+ .join(" · ") || seller.url,
354
+ })),
355
+ });
356
+ if (typeof sellerId !== "string") {
357
+ throw new Error("fixed seller selection was cancelled");
358
+ }
359
+ return {
360
+ mode,
361
+ sellerId,
362
+ };
363
+ }
364
+ async function promptSingleModelSelection(providerId, models, sellerRouting) {
365
+ const protocolPreference = getProviderProtocolPreference(providerId);
366
+ const protocolFiltered = protocolPreference
367
+ ? filterCatalogByProtocol(models, protocolPreference)
368
+ : models;
369
+ const choices = stableModelChoices(protocolFiltered);
370
+ if (choices.length === 0) {
371
+ throw new Error(`no compatible models available for ${providerId}`);
372
+ }
373
+ const labelMap = {
374
+ opencode: "OpenCode",
375
+ codex: "Codex",
376
+ openclaw: "OpenClaw",
377
+ hermes: "Hermes",
378
+ "claude-desktop": "Claude Desktop",
379
+ "claude-code": "Claude Code",
380
+ };
381
+ const selectedModel = await p.select({
382
+ message: `Choose the default model for ${labelMap[providerId] || providerId}:`,
383
+ options: choices,
384
+ });
385
+ if (typeof selectedModel !== "string") {
386
+ throw new Error(`default model selection was cancelled for ${providerId}`);
387
+ }
388
+ const selectedEntry = protocolFiltered.find((entry) => entry.id === selectedModel);
389
+ return {
390
+ selectionKind: "single-model",
391
+ protocolPreference,
392
+ defaultModel: selectedModel,
393
+ sellerId: sellerRouting.mode === "fixed" ? selectedEntry?.sellerId : undefined,
394
+ };
395
+ }
396
+ function defaultClaudeDisplayName(modelId) {
397
+ return modelId.trim();
398
+ }
399
+ function makeClaudeRoleMapping(modelId) {
400
+ const displayName = defaultClaudeDisplayName(modelId);
401
+ return {
402
+ selectionKind: "claude-role-mapping",
403
+ protocolPreference: "messages",
404
+ fallbackModel: modelId,
405
+ roles: {
406
+ sonnet: {
407
+ upstreamModel: modelId,
408
+ displayName,
409
+ declareOneM: true,
410
+ },
411
+ opus: {
412
+ upstreamModel: modelId,
413
+ displayName,
414
+ declareOneM: true,
415
+ },
416
+ haiku: {
417
+ upstreamModel: modelId,
418
+ displayName,
419
+ declareOneM: false,
420
+ },
421
+ },
422
+ };
423
+ }
424
+ async function promptClaudeCodeModelSelection(models) {
425
+ const protocolFiltered = filterCatalogByProtocol(models, "messages");
426
+ const choices = stableModelChoices(protocolFiltered);
427
+ if (choices.length === 0) {
428
+ throw new Error("no compatible message models available for Claude Code");
429
+ }
430
+ const sonnetModel = await p.select({
431
+ message: "Choose the default Sonnet model for Claude Code:",
432
+ options: choices,
433
+ });
434
+ if (typeof sonnetModel !== "string") {
435
+ throw new Error("Claude Code model selection was cancelled");
436
+ }
437
+ const mirrorAllRoles = await p.confirm({
438
+ message: "Use the same model for Opus and Haiku as well?",
439
+ initialValue: true,
440
+ });
441
+ if (typeof mirrorAllRoles !== "boolean") {
442
+ throw new Error("Claude Code role mapping confirmation was cancelled");
443
+ }
444
+ if (mirrorAllRoles) {
445
+ return makeClaudeRoleMapping(sonnetModel);
446
+ }
447
+ const opusModel = await p.select({
448
+ message: "Choose the default Opus model for Claude Code:",
449
+ options: choices,
450
+ });
451
+ if (typeof opusModel !== "string") {
452
+ throw new Error("Claude Code Opus model selection was cancelled");
453
+ }
454
+ const haikuModel = await p.select({
455
+ message: "Choose the default Haiku model for Claude Code:",
456
+ options: choices,
457
+ });
458
+ if (typeof haikuModel !== "string") {
459
+ throw new Error("Claude Code Haiku model selection was cancelled");
460
+ }
461
+ return {
462
+ selectionKind: "claude-role-mapping",
463
+ protocolPreference: "messages",
464
+ fallbackModel: sonnetModel,
465
+ roles: {
466
+ sonnet: {
467
+ upstreamModel: sonnetModel,
468
+ displayName: defaultClaudeDisplayName(sonnetModel),
469
+ declareOneM: true,
470
+ },
471
+ opus: {
472
+ upstreamModel: opusModel,
473
+ displayName: defaultClaudeDisplayName(opusModel),
474
+ declareOneM: true,
475
+ },
476
+ haiku: {
477
+ upstreamModel: haikuModel,
478
+ displayName: defaultClaudeDisplayName(haikuModel),
479
+ declareOneM: false,
480
+ },
481
+ },
482
+ };
483
+ }
484
+ async function promptProviderSelections(providerIds, catalog, sellerRouting) {
485
+ const baseModels = sellerRouting.mode === "fixed"
486
+ ? filterCatalogBySeller(catalog.models, sellerRouting.sellerId)
487
+ : catalog.models;
488
+ const selections = {};
489
+ for (const providerId of providerIds) {
490
+ const selectionKind = getProviderModelSelectionKind(providerId);
491
+ if (selectionKind === "claude-role-mapping") {
492
+ selections[providerId] = await promptClaudeCodeModelSelection(baseModels);
493
+ continue;
494
+ }
495
+ selections[providerId] = await promptSingleModelSelection(providerId, baseModels, sellerRouting);
496
+ }
497
+ return selections;
498
+ }
265
499
  export function buildCli() {
266
500
  const program = new Command();
267
501
  program
@@ -284,7 +518,6 @@ export function buildCli() {
284
518
  const plistPath = process.platform === "darwin"
285
519
  ? path.join(os.homedir(), "Library", "LaunchAgents", "com.tokenbuddy.proxyd.plist")
286
520
  : undefined;
287
- const candidates = detectProviders();
288
521
  let probe = await probeDaemonStatus(controlPort);
289
522
  let repair = { attempted: false, fixed: false };
290
523
  if (!probe.running && options.fix) {
@@ -295,10 +528,22 @@ export function buildCli() {
295
528
  const daemonInfo = probe.status;
296
529
  const daemonRunning = probe.running;
297
530
  const daemonError = probe.error;
531
+ const daemonStatus = daemonInfo && typeof daemonInfo === "object"
532
+ ? daemonInfo
533
+ : undefined;
534
+ const providers = readDoctorProviders();
298
535
  if (options.fix && repair.attempted && !repair.fixed) {
299
536
  process.exitCode = 1;
300
537
  }
301
538
  if (options.json) {
539
+ const diagnostics = await collectDoctorDiagnostics({
540
+ controlPort,
541
+ proxyPort,
542
+ daemonRunning,
543
+ daemonError,
544
+ providers,
545
+ sellerRegistryUrl: daemonStatus?.sellerRegistryUrl,
546
+ });
302
547
  console.log(JSON.stringify({
303
548
  daemon: {
304
549
  running: daemonRunning,
@@ -318,7 +563,7 @@ export function buildCli() {
318
563
  plistPath,
319
564
  plistExists: plistPath ? fs.existsSync(plistPath) : false
320
565
  },
321
- providers: candidates
566
+ ...diagnostics,
322
567
  }, null, 2));
323
568
  return;
324
569
  }
@@ -351,12 +596,28 @@ export function buildCli() {
351
596
  console.log("⚠️ LaunchAgent plist does NOT exist. Run `tb init` to install it as service.");
352
597
  }
353
598
  }
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}`);
599
+ if (daemonStatus) {
600
+ console.log(` Control Plane URL: ${controlUrl}`);
601
+ console.log(` Proxy Plane URL: http://127.0.0.1:${proxyPort}`);
602
+ if (daemonStatus.sellerRoutingMode || daemonStatus.selectionMode) {
603
+ console.log(` Routing Mode: ${daemonStatus.sellerRoutingMode || daemonStatus.selectionMode}`);
604
+ }
605
+ if (daemonStatus.selectedSellerId) {
606
+ console.log(` Selected Seller: ${daemonStatus.selectedSellerId}`);
607
+ }
608
+ if (daemonStatus.sellerRegistryUrl) {
609
+ console.log(` Registry URL: ${daemonStatus.sellerRegistryUrl}`);
610
+ }
359
611
  }
612
+ printDoctorProviders(providers);
613
+ await renderDoctorDiagnosticsProgressively({
614
+ controlPort,
615
+ proxyPort,
616
+ daemonRunning,
617
+ daemonError,
618
+ sellerRegistryUrl: daemonStatus?.sellerRegistryUrl,
619
+ providers,
620
+ });
360
621
  });
361
622
  // 2. tb payment
362
623
  const payment = program.command("payment").description("Manage payment methods");
@@ -481,21 +742,30 @@ export function buildCli() {
481
742
  .option("--json", "Output model list as JSON")
482
743
  .action(async (options) => {
483
744
  try {
745
+ const controlPort = configuredControlPort();
746
+ const proxyPort = configuredProxyPort();
747
+ const status = await probeDaemonStatus(controlPort);
748
+ const daemonInfo = status.status && typeof status.status === "object"
749
+ ? status.status
750
+ : undefined;
751
+ const models = await collectDoctorModelsSummary({
752
+ controlPort,
753
+ proxyPort,
754
+ daemonRunning: status.running,
755
+ daemonError: status.error,
756
+ sellerRegistryUrl: daemonInfo?.sellerRegistryUrl,
757
+ });
484
758
  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}`);
759
+ console.log(JSON.stringify(models, null, 2));
760
+ if (!models.available) {
761
+ process.exitCode = 1;
489
762
  }
490
- JSON.parse(body);
491
- console.log(body);
492
763
  return;
493
764
  }
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());
765
+ printDoctorModelsSummary(models);
766
+ if (!models.available) {
767
+ process.exitCode = 1;
768
+ }
499
769
  }
500
770
  catch (err) {
501
771
  console.error("Error connecting to local proxy:", err.message);
@@ -508,82 +778,307 @@ export function buildCli() {
508
778
  .description("Launch step-by-step interactive setup wizard")
509
779
  .action(async () => {
510
780
  p.intro("🚀 Welcome to TokenBuddy Interactive Wizard!");
781
+ const setupSummaryLines = [];
511
782
  // Step 1: Scan coding terminals
512
783
  const spinner = p.spinner();
513
784
  spinner.start("Scanning local system for programming terminals...");
514
785
  const candidates = detectProviders();
515
- const detected = candidates.filter(c => c.detected);
786
+ const terminalSelection = buildInitTerminalSelectionState(candidates);
516
787
  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.");
788
+ const installedTerminalMessage = buildInstalledTerminalMessage(terminalSelection.installed);
789
+ if (installedTerminalMessage) {
790
+ p.note(installedTerminalMessage, "Already Configured");
791
+ setupSummaryLines.push(`${terminalSelection.installed.length} terminal${terminalSelection.installed.length === 1 ? "" : "s"} already configured.`);
792
+ }
793
+ if (terminalSelection.options.length === 1 && terminalSelection.options[0].value === OTHER_TERMINAL_OPTION.value) {
794
+ p.note("No active programming terminals detected. Install one of Codex, Claude Code, Claude Desktop, OpenCode, OpenClaw or Hermes first.");
519
795
  }
520
796
  else {
521
- const choices = detected.map(c => ({
522
- value: c.id,
523
- label: c.name,
524
- hint: c.configPath
525
- }));
526
797
  const selected = await p.multiselect({
527
798
  message: "Select programming terminals to route via TokenBuddy (use Space to select, Enter to confirm):",
528
- options: choices,
799
+ options: terminalSelection.options,
529
800
  required: false
530
801
  });
531
- if (selected && selected.length > 0) {
532
- spinner.start("Configuring proxy routing in selected terminals...");
802
+ const selectionError = validateInitTerminalSelection(selected);
803
+ if (selectionError) {
804
+ throw new Error(selectionError);
805
+ }
806
+ const selectedActionable = selected.filter((value) => !value.endsWith(":installed"));
807
+ const selectedOther = selectedActionable.includes(OTHER_TERMINAL_OPTION.value);
808
+ const selectedProviders = selectedActionable.filter((value) => value !== OTHER_TERMINAL_OPTION.value);
809
+ if (selectedOther) {
810
+ p.note([
811
+ "✅ OpenAI-compatible Proxy",
812
+ " URL: http://127.0.0.1:17821/v1",
813
+ " Probe: http://127.0.0.1:17821/v1/models",
814
+ " Token: TOKENBUDDY_PROXY",
815
+ "",
816
+ "✅ Anthropic-compatible Proxy",
817
+ " URL: http://127.0.0.1:17821"
818
+ ].join("\n"), "TokenBuddy Proxy Interfaces");
819
+ setupSummaryLines.push("Manual terminal setup selected via Other.");
820
+ }
821
+ if (selectedProviders.length > 0) {
822
+ spinner.start("Fetching seller-backed model catalog...");
533
823
  const proxyUrl = `http://127.0.0.1:${PROXY_PORT}`;
534
- const defaultModel = "gpt-4";
824
+ const registryUrl = sellerRegistryUrlForInit();
825
+ let catalog;
826
+ try {
827
+ catalog = await discoverSellerBackedModels(registryUrl);
828
+ }
829
+ catch (error) {
830
+ spinner.stop("Failed to fetch seller-backed models.");
831
+ throw error;
832
+ }
833
+ spinner.stop("Seller-backed model catalog loaded.");
834
+ const providerIds = selectedProviders.filter((provider) => {
835
+ return [
836
+ "codex",
837
+ "claude-code",
838
+ "claude-desktop",
839
+ "openclaw",
840
+ "opencode",
841
+ "hermes",
842
+ ].includes(provider);
843
+ });
844
+ const sellerRouting = await promptSellerRoutingPreference(catalog);
845
+ const providerSelections = await promptProviderSelections(providerIds, catalog, sellerRouting);
846
+ spinner.start("Configuring proxy routing in selected terminals...");
535
847
  const store = openBuyerStore();
536
848
  try {
537
849
  applyProviderInstall({
538
- providers: selected,
850
+ providers: providerIds,
539
851
  proxyUrl,
540
- model: defaultModel
852
+ providerSelections,
853
+ sellerRouting,
541
854
  }, store);
542
855
  }
543
856
  finally {
544
857
  store.close();
545
858
  }
546
859
  spinner.stop("Selected terminals successfully configured.");
860
+ setupSummaryLines.push(`${providerIds.length} programming terminal${providerIds.length === 1 ? "" : "s"} configured for TokenBuddy.`);
547
861
  }
548
862
  }
549
863
  // Step 2: Choose Payment Method & Scan QR Activation
864
+ noteInitComingSoonPayments();
550
865
  const payMethod = await p.select({
551
866
  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
- ]
867
+ options: INIT_PAYMENT_OPTIONS
556
868
  });
557
869
  if (payMethod === "clawtip") {
558
- spinner.start("Requesting payment activation payload from public bootstrap registry...");
870
+ const store = openBuyerStore();
559
871
  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
872
+ let walletConfig = inspectOpenClawWalletConfig();
873
+ const clawtipReadiness = inspectClawtipWalletReadiness(store.getPayment("clawtip"), walletConfig);
874
+ const existingClawtip = clawtipReadiness.reusableBinding;
875
+ if (existingClawtip) {
876
+ store.savePayment({
877
+ method: "clawtip",
878
+ enabled: true,
879
+ isDefault: true,
880
+ config: existingClawtip.config
881
+ });
882
+ const details = [
883
+ existingClawtip.orderNo ? `Order: ${existingClawtip.orderNo}` : undefined,
884
+ existingClawtip.resourceUrl ? `ResourceUrl: ${existingClawtip.resourceUrl}` : undefined
885
+ ].filter(Boolean).join("\n");
886
+ logger.info("payment.channel.reused", "clawtip payment channel already configured locally", {
887
+ method: "clawtip",
888
+ hasOrderNo: Boolean(existingClawtip.orderNo),
889
+ hasResourceUrl: Boolean(existingClawtip.resourceUrl)
890
+ });
891
+ p.note(details
892
+ ? `ClawTip wallet is already configured locally.\n${details}`
893
+ : "ClawTip wallet is already configured locally.", "ClawTip");
894
+ setupSummaryLines.push("ClawTip wallet already bound locally; activation skipped.");
895
+ }
896
+ else {
897
+ if (clawtipReadiness.status === "metadata_missing_wallet") {
898
+ p.note([
899
+ clawtipReadiness.message,
900
+ `Expected: ${walletConfig.expectedPath}`,
901
+ walletConfig.alternatePaths.length > 0
902
+ ? `Alternates: ${walletConfig.alternatePaths.join(", ")}`
903
+ : "Alternates: -"
904
+ ].join("\n"), "ClawTip");
905
+ setupSummaryLines.push("Saved ClawTip metadata found, but local wallet config is missing; activation restarted.");
906
+ }
907
+ const walletReadyBeforePay = walletConfig.exists;
908
+ let openClawVersion;
909
+ if (!walletReadyBeforePay) {
910
+ spinner.start("Checking OpenClaw CLI before ClawTip wallet bootstrap...");
911
+ openClawVersion = await checkOpenClawRuntime();
912
+ spinner.stop("OpenClaw CLI detected.");
913
+ }
914
+ spinner.start("Requesting payment activation payload from public bootstrap registry...");
915
+ const bootstrapUrl = process.env.TOKENBUDDY_BOOTSTRAP_URL || "https://tb-wallet-bootstrap.fly.dev";
916
+ const bootstrap = await fetchClawtipBootstrap(bootstrapUrl);
917
+ spinner.stop("Bootstrap payload received.");
918
+ if (!bootstrap.payment?.orderNo || !bootstrap.payment?.indicator) {
919
+ throw new Error("ClawTip bootstrap response missing orderNo or indicator.");
920
+ }
921
+ const activationPayment = {
922
+ orderNo: bootstrap.payment.orderNo,
923
+ amountFen: bootstrap.payment.amountFen ?? bootstrap.activationFeeFen ?? 1,
924
+ payTo: bootstrap.payment.payTo,
925
+ encryptedData: bootstrap.payment.encryptedData,
926
+ indicator: bootstrap.payment.indicator,
927
+ slug: bootstrap.payment.slug,
928
+ skillId: bootstrap.payment.skillId,
929
+ description: bootstrap.payment.description,
930
+ resourceUrl: bootstrap.payment.resourceUrl,
931
+ };
932
+ spinner.start("Starting the ClawTip payment activation flow...");
933
+ let activation = await startClawtipWalletBootstrap(activationPayment);
934
+ spinner.stop("ClawTip payment activation finished.");
935
+ let payCredential = activation.payCredential;
936
+ for (let authAttempt = 1; (walletReadyBeforePay ? !payCredential : !walletConfig.exists)
937
+ && activation.parsedOutput.requiresWalletAuth
938
+ && authAttempt <= 3; authAttempt += 1) {
939
+ let qrDisplayMessage;
940
+ let manualOpenCommand;
941
+ if (activation.parsedOutput.mediaPath) {
942
+ const qrDisplay = await displayTerminalImage(activation.parsedOutput.mediaPath);
943
+ qrDisplayMessage = qrDisplay.message;
944
+ manualOpenCommand = qrDisplay.fallbackCommand;
945
+ }
946
+ if (!activation.parsedOutput.mediaPath && !activation.parsedOutput.authUrl) {
947
+ throw new Error(`ClawTip pay requested authorization but did not return QR media or authUrl. Order file: ${activation.orderFile}`);
948
+ }
949
+ p.note([
950
+ activation.parsedOutput.mediaPath
951
+ ? `Open or scan this ClawTip wallet QR image with the JD app: ${activation.parsedOutput.mediaPath}`
952
+ : undefined,
953
+ activation.parsedOutput.authUrl
954
+ ? `Open or scan this ClawTip wallet auth URL with the JD app: ${activation.parsedOutput.authUrl}`
955
+ : undefined,
956
+ qrDisplayMessage,
957
+ manualOpenCommand ? `Manual open command: ${manualOpenCommand}` : undefined,
958
+ activation.parsedOutput.clawtipId ? `clawtipId: ${activation.parsedOutput.clawtipId}` : undefined,
959
+ activation.parsedOutput.clawtipId
960
+ ? "After scanning, ClawTip CLI will register the local agent wallet."
961
+ : "After scanning, TokenBuddy will retry ClawTip payment activation.",
962
+ `Expected wallet config: ${walletConfig.expectedPath}`,
963
+ openClawVersion ? `OpenClaw: ${openClawVersion}` : undefined,
964
+ ].filter(Boolean).join("\n"), activation.parsedOutput.clawtipId ? "ClawTip Wallet QR" : "ClawTip Authorization QR");
965
+ if (activation.parsedOutput.clawtipId && !walletReadyBeforePay) {
966
+ spinner.start("Waiting for ClawTip wallet registration. Press Ctrl+C to cancel.");
967
+ const walletRegistered = await waitForClawtipActivationConfirmation({
968
+ clawtipId: activation.parsedOutput.clawtipId,
969
+ });
970
+ spinner.stop(walletRegistered
971
+ ? "Detected local OpenClaw ClawTip wallet config."
972
+ : "ClawTip wallet registration cancelled.");
973
+ if (!walletRegistered) {
974
+ setupSummaryLines.push("ClawTip wallet registration was cancelled before local wallet config was saved.");
975
+ process.exitCode = 1;
976
+ return;
977
+ }
978
+ walletConfig = inspectOpenClawWalletConfig();
979
+ if (!walletConfig.exists) {
980
+ throw new Error(`ClawTip wallet registration finished but wallet config is still missing: ${walletConfig.expectedPath}`);
981
+ }
982
+ setupSummaryLines.push("ClawTip wallet registered locally.");
983
+ }
984
+ else {
985
+ const authorized = await p.confirm({
986
+ message: "Scan or authorize this ClawTip QR in the JD app, then press Enter to retry payment activation.",
987
+ initialValue: true,
988
+ });
989
+ if (authorized !== true) {
990
+ setupSummaryLines.push("ClawTip authorization was cancelled before payment activation completed.");
991
+ process.exitCode = 1;
992
+ return;
993
+ }
994
+ walletConfig = inspectOpenClawWalletConfig();
995
+ }
996
+ payCredential = readClawtipPayCredential(activation.orderFile);
997
+ if (!payCredential) {
998
+ spinner.start(`Retrying ClawTip payment activation after authorization (${authAttempt}/3)...`);
999
+ activation = await startClawtipWalletBootstrap(activationPayment);
1000
+ spinner.stop("ClawTip payment activation retry finished.");
1001
+ payCredential = activation.payCredential;
1002
+ }
1003
+ }
1004
+ const refreshedWalletConfig = inspectOpenClawWalletConfig();
1005
+ if (!walletReadyBeforePay && !refreshedWalletConfig.exists) {
1006
+ const paymentQrMessage = activation.parsedOutput.mediaPath
1007
+ ? ` ClawTip pay returned QR media: ${activation.parsedOutput.mediaPath}`
1008
+ : " ClawTip pay did not return QR media.";
1009
+ throw new Error([
1010
+ `ClawTip wallet config is still missing after payment activation: ${refreshedWalletConfig.expectedPath}.`,
1011
+ `Order file: ${activation.orderFile}.`,
1012
+ paymentQrMessage.trim(),
1013
+ ].join(" "));
1014
+ }
1015
+ const completedByWalletConfig = !payCredential && !walletReadyBeforePay && refreshedWalletConfig.exists;
1016
+ if (!payCredential && !completedByWalletConfig) {
1017
+ const paymentQrMessage = activation.parsedOutput.mediaPath
1018
+ ? ` ClawTip pay returned QR media: ${activation.parsedOutput.mediaPath}`
1019
+ : "";
1020
+ throw new Error(`ClawTip pay did not write payCredential to the order file. Order file: ${activation.orderFile}.${paymentQrMessage}`);
1021
+ }
1022
+ const activationCompletedBy = payCredential
1023
+ ? (refreshedWalletConfig.exists ? "payCredential+wallet-config" : "payCredential")
1024
+ : "wallet-config";
1025
+ store.savePayment({
1026
+ method: "clawtip",
1027
+ enabled: true,
1028
+ isDefault: true,
1029
+ config: {
1030
+ bootstrapUrl,
1031
+ orderNo: bootstrap.payment?.orderNo,
1032
+ amountFen: bootstrap.payment?.amountFen ?? bootstrap.activationFeeFen,
1033
+ indicator: bootstrap.payment?.indicator,
1034
+ slug: bootstrap.payment?.slug,
1035
+ skillId: bootstrap.payment?.skillId,
1036
+ description: bootstrap.payment?.description,
1037
+ resourceUrl: bootstrap.payment?.resourceUrl,
1038
+ proofRequired: false,
1039
+ activationOrderFile: activation.orderFile,
1040
+ walletConfigPath: refreshedWalletConfig.expectedPath,
1041
+ walletConfigPresent: refreshedWalletConfig.exists,
1042
+ payCredentialWritten: Boolean(payCredential),
1043
+ activationCompletedBy
1044
+ }
1045
+ });
1046
+ logger.info("payment.channel.added", "clawtip payment channel added during init", {
1047
+ method: "clawtip",
1048
+ orderNo: bootstrap.payment?.orderNo,
1049
+ payCredentialWritten: Boolean(payCredential),
1050
+ activationCompletedBy
1051
+ });
1052
+ if (refreshedWalletConfig.exists) {
1053
+ if (!payCredential) {
1054
+ p.note([
1055
+ "OpenClaw saved the local ClawTip wallet config, but the ClawTip order file did not contain payCredential.",
1056
+ `Order file: ${activation.orderFile}`,
1057
+ "TokenBuddy saved the wallet binding metadata and will rely on the local wallet for future ClawTip purchases."
1058
+ ].join("\n"), "ClawTip");
1059
+ }
1060
+ setupSummaryLines.push("ClawTip wallet activated and set as the default payment method.");
1061
+ }
1062
+ else {
1063
+ p.note([
1064
+ "ClawTip payment metadata was saved, but the local OpenClaw wallet config is still missing.",
1065
+ `Expected: ${refreshedWalletConfig.expectedPath}`,
1066
+ refreshedWalletConfig.alternatePaths.length > 0
1067
+ ? `Nearby files: ${refreshedWalletConfig.alternatePaths.join(", ")}`
1068
+ : "Nearby files: -",
1069
+ "Bind or restore the local wallet before using ClawTip-backed purchases."
1070
+ ].join("\n"), "ClawTip Wallet Required");
1071
+ setupSummaryLines.push("ClawTip payment metadata saved; local wallet config still needs binding before use.");
1072
+ }
578
1073
  }
579
- spinner.stop("JD收银台 confirmed payment. ClawTip wallet is active! 🚀");
580
1074
  }
581
1075
  catch (err) {
582
- spinner.stop(`Failed to fetch activation QR: ${err.message}`);
1076
+ spinner.stop(`Failed to finish ClawTip setup: ${err.message}`);
1077
+ setupSummaryLines.push("ClawTip activation requires follow-up because the bootstrap step did not complete.");
1078
+ }
1079
+ finally {
1080
+ store.close();
583
1081
  }
584
- }
585
- else {
586
- p.note("Mock Wallet selected. No real payments will be made. Status is active.");
587
1082
  }
588
1083
  // Step 3: Install Launchd Daemon Service
589
1084
  if (process.platform === "darwin") {
@@ -631,6 +1126,7 @@ export function buildCli() {
631
1126
  catch { }
632
1127
  execSync(`launchctl load ${plistPath}`);
633
1128
  spinner.stop("LaunchAgent daemon successfully registered and started! 🚀");
1129
+ setupSummaryLines.push("Background tb-proxyd launchd service installed.");
634
1130
  }
635
1131
  catch (err) {
636
1132
  spinner.stop(`Failed to write launchd plist: ${err.message}`);
@@ -640,8 +1136,9 @@ export function buildCli() {
640
1136
  else {
641
1137
  // Run background dettached child process in linux/windows
642
1138
  p.note("System daemon is active. Process runs in dettached background.");
1139
+ setupSummaryLines.push("Background daemon mode is available on this system.");
643
1140
  }
644
- p.outro("🎉 Setup complete! Run `tb doctor` to audit status anytime. Let's code!");
1141
+ p.outro(buildInitSuccessMessage(setupSummaryLines));
645
1142
  });
646
1143
  return program;
647
1144
  }