@tokenbuddy/tokenbuddy 1.0.36 → 1.0.37

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 (143) hide show
  1. package/dist/src/buyer-store.d.ts +6 -1
  2. package/dist/src/buyer-store.js +43 -4
  3. package/dist/src/cli.js +2 -2
  4. package/dist/src/daemon.d.ts +12 -0
  5. package/dist/src/daemon.js +791 -61
  6. package/dist/src/doctor-diagnostics.js +1 -6
  7. package/dist/src/provider-install.d.ts +2 -2
  8. package/dist/src/provider-install.js +248 -2
  9. package/dist/src/seller-catalog.d.ts +21 -0
  10. package/dist/src/seller-catalog.js +17 -0
  11. package/dist/src/seller-route-planner.d.ts +4 -1
  12. package/dist/src/seller-route-planner.js +3 -0
  13. package/dist/src/seller-routing-strategy.d.ts +3 -0
  14. package/dist/src/terminal-detect.d.ts +1 -1
  15. package/dist/src/terminal-detect.js +3 -2
  16. package/package.json +15 -2
  17. package/static/ui/assets/index-Djfl9tw5.js +271 -0
  18. package/static/ui/assets/index-DkfztCkn.css +1 -0
  19. package/static/ui/index.html +2 -2
  20. package/dist/src/buyer-store.d.ts.map +0 -1
  21. package/dist/src/buyer-store.js.map +0 -1
  22. package/dist/src/clawtip-bootstrap.d.ts.map +0 -1
  23. package/dist/src/clawtip-bootstrap.js.map +0 -1
  24. package/dist/src/cli.d.ts.map +0 -1
  25. package/dist/src/cli.js.map +0 -1
  26. package/dist/src/credit-tracker.d.ts.map +0 -1
  27. package/dist/src/credit-tracker.js.map +0 -1
  28. package/dist/src/daemon.d.ts.map +0 -1
  29. package/dist/src/daemon.js.map +0 -1
  30. package/dist/src/doctor-clawtip-wallet.d.ts.map +0 -1
  31. package/dist/src/doctor-clawtip-wallet.js.map +0 -1
  32. package/dist/src/doctor-diagnostics.d.ts.map +0 -1
  33. package/dist/src/doctor-diagnostics.js.map +0 -1
  34. package/dist/src/index.d.ts.map +0 -1
  35. package/dist/src/index.js.map +0 -1
  36. package/dist/src/init-clawtip-activation.d.ts.map +0 -1
  37. package/dist/src/init-clawtip-activation.js.map +0 -1
  38. package/dist/src/init-payment-options.d.ts.map +0 -1
  39. package/dist/src/init-payment-options.js.map +0 -1
  40. package/dist/src/init-setup.d.ts.map +0 -1
  41. package/dist/src/init-setup.js.map +0 -1
  42. package/dist/src/model-index.d.ts.map +0 -1
  43. package/dist/src/model-index.js.map +0 -1
  44. package/dist/src/package-update.d.ts.map +0 -1
  45. package/dist/src/package-update.js.map +0 -1
  46. package/dist/src/prewarm-cache.d.ts.map +0 -1
  47. package/dist/src/prewarm-cache.js.map +0 -1
  48. package/dist/src/prewarm-scheduler.d.ts.map +0 -1
  49. package/dist/src/prewarm-scheduler.js.map +0 -1
  50. package/dist/src/provider-install.d.ts.map +0 -1
  51. package/dist/src/provider-install.js.map +0 -1
  52. package/dist/src/provider-routing-config.d.ts.map +0 -1
  53. package/dist/src/provider-routing-config.js.map +0 -1
  54. package/dist/src/registry-trust.d.ts.map +0 -1
  55. package/dist/src/registry-trust.js.map +0 -1
  56. package/dist/src/route-failover.d.ts.map +0 -1
  57. package/dist/src/route-failover.js.map +0 -1
  58. package/dist/src/seller-catalog.d.ts.map +0 -1
  59. package/dist/src/seller-catalog.js.map +0 -1
  60. package/dist/src/seller-concurrency-limiter.d.ts.map +0 -1
  61. package/dist/src/seller-concurrency-limiter.js.map +0 -1
  62. package/dist/src/seller-metadata-cache.d.ts.map +0 -1
  63. package/dist/src/seller-metadata-cache.js.map +0 -1
  64. package/dist/src/seller-pool.d.ts.map +0 -1
  65. package/dist/src/seller-pool.js.map +0 -1
  66. package/dist/src/seller-route-planner.d.ts.map +0 -1
  67. package/dist/src/seller-route-planner.js.map +0 -1
  68. package/dist/src/seller-routing-config.d.ts.map +0 -1
  69. package/dist/src/seller-routing-config.js.map +0 -1
  70. package/dist/src/seller-routing-strategy.d.ts.map +0 -1
  71. package/dist/src/seller-routing-strategy.js.map +0 -1
  72. package/dist/src/stream-failover.d.ts.map +0 -1
  73. package/dist/src/stream-failover.js.map +0 -1
  74. package/dist/src/tb-clawtip-proof.d.ts.map +0 -1
  75. package/dist/src/tb-clawtip-proof.js.map +0 -1
  76. package/dist/src/tb-proxyd.d.ts.map +0 -1
  77. package/dist/src/tb-proxyd.js.map +0 -1
  78. package/dist/src/terminal-detect.d.ts.map +0 -1
  79. package/dist/src/terminal-detect.js.map +0 -1
  80. package/dist/src/terminal-image.d.ts.map +0 -1
  81. package/dist/src/terminal-image.js.map +0 -1
  82. package/src/buyer-store.ts +0 -1090
  83. package/src/clawtip-bootstrap.ts +0 -65
  84. package/src/cli.ts +0 -2243
  85. package/src/credit-tracker.ts +0 -295
  86. package/src/daemon.ts +0 -5475
  87. package/src/doctor-clawtip-wallet.ts +0 -95
  88. package/src/doctor-diagnostics.ts +0 -1026
  89. package/src/index.ts +0 -16
  90. package/src/init-clawtip-activation.ts +0 -695
  91. package/src/init-payment-options.ts +0 -373
  92. package/src/init-setup.ts +0 -165
  93. package/src/model-index.ts +0 -278
  94. package/src/package-update.ts +0 -311
  95. package/src/prewarm-cache.ts +0 -485
  96. package/src/prewarm-scheduler.ts +0 -675
  97. package/src/provider-install.ts +0 -1006
  98. package/src/provider-routing-config.ts +0 -410
  99. package/src/registry-trust.ts +0 -51
  100. package/src/route-failover.ts +0 -304
  101. package/src/seller-catalog.ts +0 -505
  102. package/src/seller-concurrency-limiter.ts +0 -161
  103. package/src/seller-metadata-cache.ts +0 -91
  104. package/src/seller-pool.ts +0 -557
  105. package/src/seller-route-planner.ts +0 -513
  106. package/src/seller-routing-config.ts +0 -211
  107. package/src/seller-routing-strategy.ts +0 -362
  108. package/src/stream-failover.ts +0 -152
  109. package/src/tb-clawtip-proof.ts +0 -28
  110. package/src/tb-proxyd.ts +0 -101
  111. package/src/terminal-detect.ts +0 -333
  112. package/src/terminal-image.ts +0 -228
  113. package/static/ui/assets/index-0MVXD7bH.css +0 -1
  114. package/static/ui/assets/index-BVbeDEwq.js +0 -271
  115. package/static/ui/assets/index-BVbeDEwq.js.map +0 -1
  116. package/tests/cli-routing.test.ts +0 -363
  117. package/tests/control-plane-ui-endpoints.test.ts +0 -1630
  118. package/tests/credit-tracker.test.ts +0 -165
  119. package/tests/daemon-413-fallback.test.ts +0 -92
  120. package/tests/daemon-classify.test.ts +0 -452
  121. package/tests/daemon-roles.test.ts +0 -92
  122. package/tests/daemon-trusted-registry-cache.test.ts +0 -132
  123. package/tests/e2e.test.ts +0 -366
  124. package/tests/image-generation-e2e.test.ts +0 -230
  125. package/tests/model-index.test.ts +0 -198
  126. package/tests/package-update.test.ts +0 -147
  127. package/tests/prewarm-cache.test.ts +0 -296
  128. package/tests/prewarm-scheduler.test.ts +0 -367
  129. package/tests/provider-routing-config.test.ts +0 -150
  130. package/tests/registry-trust.test.ts +0 -28
  131. package/tests/route-failover.test.ts +0 -222
  132. package/tests/seller-catalog-413.test.ts +0 -120
  133. package/tests/seller-catalog-utilities.test.ts +0 -124
  134. package/tests/seller-concurrency-limiter.test.ts +0 -83
  135. package/tests/seller-metadata-cache.test.ts +0 -89
  136. package/tests/seller-pool.test.ts +0 -365
  137. package/tests/seller-route-planner.test.ts +0 -312
  138. package/tests/seller-routing-config.test.ts +0 -124
  139. package/tests/seller-routing-strategy.test.ts +0 -167
  140. package/tests/stream-failover.test.ts +0 -52
  141. package/tests/thousand-seller.test.ts +0 -151
  142. package/tests/tokenbuddy.test.ts +0 -4043
  143. package/tsconfig.json +0 -8
package/src/cli.ts DELETED
@@ -1,2243 +0,0 @@
1
- import { Command } from "commander";
2
- import * as p from "@clack/prompts";
3
- import * as fs from "fs";
4
- import * as path from "path";
5
- import * as os from "os";
6
- import { execFileSync, spawn } from "child_process";
7
- import Table from "cli-table3";
8
- import { BuyerStore, PaymentConfig } from "./buyer-store.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";
19
- import { createModuleLogger } from "@tokenbuddy/logging";
20
- import * as crypto from "crypto";
21
- import { fileURLToPath } from "url";
22
- import {
23
- discoverSellerBackedModels,
24
- filterCatalogByProtocol,
25
- filterCatalogBySeller,
26
- type ModelCatalogEntry,
27
- type SellerCatalogResult,
28
- } from "./seller-catalog.js";
29
- import {
30
- assertSellerRoutingConfig,
31
- defaultSellerRoutingConfig,
32
- normalizeSellerRoutingConfig,
33
- parseSellerIdList,
34
- ROUTING_CONFIG_KEY,
35
- type BuyerSellerRoutingConfig,
36
- } from "./seller-routing-config.js";
37
- import type { SellerRoutingMode, SellerRoutingScorer } from "./seller-routing-strategy.js";
38
- import {
39
- collectDoctorDiagnostics,
40
- collectDoctorModelsSummary,
41
- printDoctorProviders,
42
- printDoctorModelsSummary,
43
- readDoctorProviders,
44
- renderDoctorDiagnosticsProgressively,
45
- } from "./doctor-diagnostics.js";
46
- import {
47
- buildInitSuccessMessage,
48
- buildInitTerminalSelectionState,
49
- buildInstalledTerminalMessage,
50
- INIT_PAYMENT_OPTIONS,
51
- inspectClawtipWalletReadiness,
52
- inspectOpenClawWalletConfig,
53
- noteInitComingSoonPayments,
54
- OTHER_TERMINAL_OPTION,
55
- type InitPaymentMethod,
56
- validateInitTerminalSelection,
57
- } from "./init-payment-options.js";
58
- import {
59
- checkOpenClawRuntime,
60
- readClawtipPayCredential,
61
- startClawtipWalletBootstrap,
62
- waitForClawtipActivationConfirmation,
63
- } from "./init-clawtip-activation.js";
64
- import {
65
- DEFAULT_CLAWTIP_BOOTSTRAP_URL,
66
- fetchClawtipBootstrap,
67
- normalizeClawtipBootstrapResourceUrl,
68
- } from "./clawtip-bootstrap.js";
69
- import { displayTerminalImage } from "./terminal-image.js";
70
- import {
71
- checkPackageUpdate,
72
- readInstalledPackageManifest,
73
- runPackageUpdate,
74
- type PackageRestartResult,
75
- type PackageUpdateCheck,
76
- type PackageUpdateResult,
77
- } from "./package-update.js";
78
- import { DEFAULT_SELLER_REGISTRY_URL } from "./registry-trust.js";
79
-
80
- // @ts-ignore
81
- import qrcode from "qrcode-terminal";
82
-
83
- const CONTROL_PORT = 17820;
84
- const PROXY_PORT = 17821;
85
- const TOKENBUDDY_LAUNCHD_LABEL = "com.tokenbuddy.proxyd";
86
- const STALE_TOKENBUDDY_LAUNCHD_LABELS = [
87
- "com.tokenbuddy.tb-proxyd",
88
- "homebrew.mxcl.tokenbuddy",
89
- ] as const;
90
- const logger = createModuleLogger("tokenbuddy-cli");
91
-
92
- function readCliPackageVersion(): string {
93
- return readInstalledPackageManifest().version;
94
- }
95
- const SUPPORTED_PAYMENT_METHODS = ["mock", "clawtip"] as const;
96
- type SupportedPaymentMethod = typeof SUPPORTED_PAYMENT_METHODS[number];
97
-
98
- export interface DaemonProbeResult {
99
- running: boolean;
100
- status?: unknown;
101
- error?: string;
102
- }
103
-
104
- export interface DaemonRepairResult {
105
- attempted: boolean;
106
- fixed: boolean;
107
- pid?: number;
108
- error?: string;
109
- }
110
-
111
- interface DaemonRestartResult {
112
- attempted: boolean;
113
- restarted: boolean;
114
- method: "launchd";
115
- plistPath: string;
116
- target?: string;
117
- before?: DaemonProbeResult;
118
- after?: DaemonProbeResult;
119
- error?: string;
120
- }
121
-
122
- interface CommandFailure extends Error {
123
- code?: string;
124
- exitCode?: number;
125
- }
126
-
127
- interface SelectOption {
128
- value: string;
129
- label: string;
130
- hint?: string;
131
- }
132
-
133
- function isSupportedPaymentMethod(method: string): method is SupportedPaymentMethod {
134
- return (SUPPORTED_PAYMENT_METHODS as readonly string[]).includes(method);
135
- }
136
-
137
- function configuredControlPort(): number {
138
- return parsePortEnv("TB_PROXYD_CONTROL_PORT", CONTROL_PORT);
139
- }
140
-
141
- function configuredProxyPort(): number {
142
- return parsePortEnv("TB_PROXYD_PROXY_PORT", PROXY_PORT);
143
- }
144
-
145
- function parsePortEnv(name: string, fallback: number): number {
146
- const rawValue = process.env[name];
147
- if (!rawValue) {
148
- return fallback;
149
- }
150
- const port = Number(rawValue);
151
- if (!Number.isInteger(port) || port < 0 || port > 65535) {
152
- throw new Error(`${name} must be an integer port between 0 and 65535`);
153
- }
154
- return port;
155
- }
156
-
157
- function openBuyerStore(): BuyerStore {
158
- return new BuyerStore();
159
- }
160
-
161
- function currentModuleDir(): string {
162
- if (typeof __dirname !== "undefined") {
163
- return __dirname;
164
- }
165
-
166
- const stack = new Error().stack || "";
167
- const fileUrlMatch = stack.match(/(file:\/\/\/[^)\n]+\/cli\.js):\d+:\d+/);
168
- if (fileUrlMatch) {
169
- return path.dirname(fileURLToPath(fileUrlMatch[1]));
170
- }
171
-
172
- const filePathMatch = stack.match(/(\/[^)\n]+\/cli\.(?:js|ts)):\d+:\d+/);
173
- if (filePathMatch) {
174
- return path.dirname(filePathMatch[1]);
175
- }
176
-
177
- return process.cwd();
178
- }
179
-
180
- async function probeDaemonStatus(controlPort: number): Promise<DaemonProbeResult> {
181
- try {
182
- const res = await fetch(`http://127.0.0.1:${controlPort}/status`);
183
- if (res.ok) {
184
- return {
185
- running: true,
186
- status: await res.json()
187
- };
188
- }
189
- return {
190
- running: false,
191
- error: `HTTP ${res.status}`
192
- };
193
- } catch (error: unknown) {
194
- return {
195
- running: false,
196
- error: error instanceof Error ? error.message : String(error)
197
- };
198
- }
199
- }
200
-
201
- interface NormalizedClawtipPaymentPayload {
202
- orderNo: string;
203
- amountFen?: number;
204
- payTo?: string;
205
- encryptedData?: string;
206
- indicator: string;
207
- slug?: string;
208
- skillId?: string;
209
- description?: string;
210
- resourceUrl: string;
211
- }
212
-
213
- async function waitForDaemonStatus(controlPort: number, timeoutMs: number): Promise<DaemonProbeResult> {
214
- const deadline = Date.now() + timeoutMs;
215
- let latest: DaemonProbeResult = { running: false, error: "not checked" };
216
- while (Date.now() < deadline) {
217
- latest = await probeDaemonStatus(controlPort);
218
- if (latest.running) {
219
- return latest;
220
- }
221
- await new Promise(resolve => setTimeout(resolve, 150));
222
- }
223
- return latest;
224
- }
225
-
226
- function launchControlUi(controlPort: number, pathname = "/"): string {
227
- const normalizedPathname = pathname.startsWith("/") ? pathname : `/${pathname}`;
228
- const url = `http://127.0.0.1:${controlPort}${normalizedPathname}`;
229
- const platform = process.platform;
230
- const args: string[] = platform === "darwin" ? [url] : platform === "win32" ? ["/c", "start", "", url] : [url];
231
- const cmd = platform === "win32" ? "cmd" : platform === "darwin" ? "open" : "xdg-open";
232
- const child = spawn(cmd, args, { detached: true, stdio: "ignore" });
233
- child.on("error", (err) => {
234
- console.error(`Failed to launch browser: ${err.message}`);
235
- process.exitCode = 1;
236
- });
237
- child.unref();
238
- return url;
239
- }
240
-
241
- function defaultProxydLogPath(kind: "stdout" | "stderr"): string {
242
- const logDir = path.join(os.homedir(), ".tokenbuddy-store");
243
- fs.mkdirSync(logDir, { recursive: true });
244
- return path.join(logDir, `tb-proxyd.${kind}.log`);
245
- }
246
-
247
- function tbProxydScriptPath(): string {
248
- return path.resolve(currentModuleDir(), "./tb-proxyd.js");
249
- }
250
-
251
- function tbClawtipProofScriptPath(): string {
252
- return path.resolve(currentModuleDir(), "../../bin/tb-clawtip-proof.js");
253
- }
254
-
255
- function defaultClawtipProofCommand(): string {
256
- return process.env.TB_PROXYD_CLAWTIP_PROOF_COMMAND || tbClawtipProofScriptPath();
257
- }
258
-
259
- /**
260
- * `buildLaunchdPlistContent` 的输入。
261
- * 用于生成 macOS launchd plist,把 `tb-proxyd` 注册成 LaunchAgent。
262
- */
263
- export interface LaunchdPlistOptions {
264
- label: string;
265
- nodePath: string;
266
- scriptPath: string;
267
- stdoutPath: string;
268
- stderrPath: string;
269
- controlPort: number;
270
- proxyPort: number;
271
- sellerRegistryUrl: string;
272
- pathEnv?: string;
273
- clawtipProofCommand?: string;
274
- clawtipProofTimeoutMs?: number;
275
- }
276
-
277
- function escapeXmlText(value: string): string {
278
- return value
279
- .replace(/&/g, "&amp;")
280
- .replace(/</g, "&lt;")
281
- .replace(/>/g, "&gt;");
282
- }
283
-
284
- function buildLaunchdPathEnv(nodePath: string, pathEnv?: string): string {
285
- const entries = [
286
- path.dirname(nodePath),
287
- ...(pathEnv || process.env.PATH || "").split(path.delimiter),
288
- ]
289
- .map((entry) => entry.trim())
290
- .filter((entry) => entry.length > 0);
291
- return Array.from(new Set(entries)).join(path.delimiter);
292
- }
293
-
294
- /**
295
- * 构造 macOS launchd plist 的 XML 文本,用于 `launchctl load` 注册 `tb-proxyd`。
296
- * 已转义所有插值字段,避免 XML 注入;env 块只输出非空项。
297
- *
298
- * @param options plist 字段
299
- * @returns 完整的 plist XML 字符串
300
- */
301
- export function buildLaunchdPlistContent(options: LaunchdPlistOptions): string {
302
- const env: Record<string, string> = {
303
- TB_PROXYD_CONTROL_PORT: String(options.controlPort),
304
- TB_PROXYD_PROXY_PORT: String(options.proxyPort),
305
- TB_PROXYD_SELLER_REGISTRY_URL: options.sellerRegistryUrl,
306
- PATH: buildLaunchdPathEnv(options.nodePath, options.pathEnv),
307
- };
308
- if (options.clawtipProofCommand?.trim()) {
309
- env.TB_PROXYD_CLAWTIP_PROOF_COMMAND = options.clawtipProofCommand.trim();
310
- }
311
- if (options.clawtipProofTimeoutMs !== undefined) {
312
- env.TB_PROXYD_CLAWTIP_PROOF_TIMEOUT_MS = String(options.clawtipProofTimeoutMs);
313
- }
314
- const envEntries = Object.entries(env)
315
- .map(([key, value]) => ` <key>${escapeXmlText(key)}</key>\n <string>${escapeXmlText(value)}</string>`)
316
- .join("\n");
317
-
318
- return `<?xml version="1.0" encoding="UTF-8"?>
319
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
320
- <plist version="1.0">
321
- <dict>
322
- <key>Label</key>
323
- <string>${escapeXmlText(options.label)}</string>
324
- <key>ProgramArguments</key>
325
- <array>
326
- <string>${escapeXmlText(options.nodePath)}</string>
327
- <string>${escapeXmlText(options.scriptPath)}</string>
328
- </array>
329
- <key>EnvironmentVariables</key>
330
- <dict>
331
- ${envEntries}
332
- </dict>
333
- <key>RunAtLoad</key>
334
- <true/>
335
- <key>KeepAlive</key>
336
- <true/>
337
- <key>StandardOutPath</key>
338
- <string>${escapeXmlText(options.stdoutPath)}</string>
339
- <key>StandardErrorPath</key>
340
- <string>${escapeXmlText(options.stderrPath)}</string>
341
- </dict>
342
- </plist>`;
343
- }
344
-
345
- function launchdUserDomain(): string {
346
- if (typeof process.getuid === "function") {
347
- return `gui/${process.getuid()}`;
348
- }
349
- return "gui/501";
350
- }
351
-
352
- function launchAgentPlistPath(label: string): string {
353
- return path.join(os.homedir(), "Library", "LaunchAgents", `${label}.plist`);
354
- }
355
-
356
- function launchdServiceTarget(label: string): string {
357
- return `${launchdUserDomain()}/${label}`;
358
- }
359
-
360
- function runLaunchctl(args: string[], ignoreFailure = false): void {
361
- try {
362
- execFileSync("launchctl", args, { stdio: "ignore" });
363
- } catch (error) {
364
- if (!ignoreFailure) {
365
- throw error;
366
- }
367
- }
368
- }
369
-
370
- function sleepSync(ms: number): void {
371
- const shared = new Int32Array(new SharedArrayBuffer(4));
372
- Atomics.wait(shared, 0, 0, ms);
373
- }
374
-
375
- function runLaunchctlWithRetry(
376
- args: string[],
377
- run: (args: string[], ignoreFailure?: boolean) => void,
378
- attempts = 3
379
- ): void {
380
- let latestError: unknown;
381
- for (let attempt = 0; attempt < attempts; attempt += 1) {
382
- try {
383
- run(args);
384
- return;
385
- } catch (error: unknown) {
386
- latestError = error;
387
- if (attempt < attempts - 1) {
388
- sleepSync(250 * (attempt + 1));
389
- }
390
- }
391
- }
392
- throw latestError;
393
- }
394
-
395
- function launchAgentLoaded(label: string, run: (args: string[], ignoreFailure?: boolean) => void = runLaunchctl): boolean {
396
- try {
397
- run(["print", launchdServiceTarget(label)]);
398
- return true;
399
- } catch {
400
- return false;
401
- }
402
- }
403
-
404
- export function installLaunchAgentWithRunner(
405
- plistPath: string,
406
- label: string,
407
- run: (args: string[], ignoreFailure?: boolean) => void
408
- ): void {
409
- const domain = launchdUserDomain();
410
- for (const staleLabel of STALE_TOKENBUDDY_LAUNCHD_LABELS) {
411
- run(["bootout", `${domain}/${staleLabel}`], true);
412
- }
413
- const target = launchdServiceTarget(label);
414
- if (launchAgentLoaded(label, run)) {
415
- run(["bootout", target], true);
416
- }
417
- runLaunchctlWithRetry(["bootstrap", domain, plistPath], run);
418
- run(["kickstart", "-k", target]);
419
- }
420
-
421
- function installLaunchAgent(plistPath: string, label: string): void {
422
- installLaunchAgentWithRunner(plistPath, label, runLaunchctl);
423
- }
424
-
425
- async function repairDaemon(controlPort: number): Promise<{ repair: DaemonRepairResult; probe: DaemonProbeResult }> {
426
- const existing = await probeDaemonStatus(controlPort);
427
- if (existing.running) {
428
- return {
429
- repair: { attempted: false, fixed: false },
430
- probe: existing
431
- };
432
- }
433
-
434
- const stdoutPath = process.env.TB_PROXYD_STDOUT_LOG_FILE || defaultProxydLogPath("stdout");
435
- const stderrPath = process.env.TB_PROXYD_STDERR_LOG_FILE || defaultProxydLogPath("stderr");
436
- const stdout = fs.openSync(stdoutPath, "a");
437
- const stderr = fs.openSync(stderrPath, "a");
438
- const child = spawn(process.execPath, [tbProxydScriptPath()], {
439
- detached: true,
440
- stdio: ["ignore", stdout, stderr],
441
- env: process.env
442
- });
443
- child.unref();
444
- fs.closeSync(stdout);
445
- fs.closeSync(stderr);
446
-
447
- const probe = await waitForDaemonStatus(controlPort, 8000);
448
- if (probe.running) {
449
- return {
450
- repair: { attempted: true, fixed: true, pid: child.pid },
451
- probe
452
- };
453
- }
454
-
455
- return {
456
- repair: {
457
- attempted: true,
458
- fixed: false,
459
- pid: child.pid,
460
- error: probe.error || "tb-proxyd did not become ready"
461
- },
462
- probe
463
- };
464
- }
465
-
466
- interface RestartLaunchAgentDeps {
467
- platform: NodeJS.Platform;
468
- plistPath: string;
469
- existsSync(filePath: string): boolean;
470
- runLaunchctl(args: string[]): void;
471
- probeDaemonStatus(controlPort: number): Promise<DaemonProbeResult>;
472
- waitForDaemonStatus(controlPort: number, timeoutMs: number): Promise<DaemonProbeResult>;
473
- }
474
-
475
- export async function restartLaunchAgent(
476
- controlPort: number,
477
- deps: RestartLaunchAgentDeps = {
478
- platform: process.platform,
479
- plistPath: launchAgentPlistPath(TOKENBUDDY_LAUNCHD_LABEL),
480
- existsSync: fs.existsSync,
481
- runLaunchctl: (args) => runLaunchctl(args),
482
- probeDaemonStatus,
483
- waitForDaemonStatus,
484
- },
485
- ): Promise<DaemonRestartResult> {
486
- const before = await deps.probeDaemonStatus(controlPort);
487
- const baseResult = {
488
- attempted: false,
489
- restarted: false,
490
- method: "launchd" as const,
491
- plistPath: deps.plistPath,
492
- before,
493
- };
494
-
495
- if (deps.platform !== "darwin") {
496
- return {
497
- ...baseResult,
498
- error: "tb daemon restart is only supported for the macOS LaunchAgent service. Run `tb doctor --fix` to start tb-proxyd in the background."
499
- };
500
- }
501
-
502
- if (!deps.existsSync(deps.plistPath)) {
503
- return {
504
- ...baseResult,
505
- error: "LaunchAgent plist is missing. Run `tb init` to install tb-proxyd as a service first."
506
- };
507
- }
508
-
509
- const target = launchdServiceTarget(TOKENBUDDY_LAUNCHD_LABEL);
510
- try {
511
- deps.runLaunchctl(["kickstart", "-k", target]);
512
- } catch (error: unknown) {
513
- return {
514
- ...baseResult,
515
- attempted: true,
516
- target,
517
- error: error instanceof Error ? error.message : String(error)
518
- };
519
- }
520
-
521
- const after = await deps.waitForDaemonStatus(controlPort, 8000);
522
- return {
523
- ...baseResult,
524
- attempted: true,
525
- restarted: after.running,
526
- target,
527
- after,
528
- error: after.running ? undefined : after.error || "tb-proxyd did not become ready after restart"
529
- };
530
- }
531
-
532
- export interface WebInitLauncherResult {
533
- method: "launchd" | "detached";
534
- controlPort: number;
535
- proxyPort: number;
536
- url: string;
537
- probe: DaemonProbeResult;
538
- repair?: DaemonRepairResult;
539
- plistPath?: string;
540
- serviceInstalled?: boolean;
541
- error?: string;
542
- }
543
-
544
- interface WebInitStateForLaunch {
545
- freshMachine?: boolean;
546
- repairMode?: boolean;
547
- setup?: {
548
- status?: string;
549
- };
550
- }
551
-
552
- export interface WebInitLauncherDeps {
553
- platform?: NodeJS.Platform;
554
- controlPort?: number;
555
- proxyPort?: number;
556
- sellerRegistryUrl?: string;
557
- homeDir?: string;
558
- nodePath?: string;
559
- scriptPath?: string;
560
- pathEnv?: string;
561
- stdoutPath?: string;
562
- stderrPath?: string;
563
- clawtipProofCommand?: string;
564
- clawtipProofTimeoutMs?: number;
565
- openBrowser?: boolean;
566
- mkdirSync?(dirPath: string, options: { recursive: boolean }): void;
567
- writeFileSync?(filePath: string, content: string, encoding: BufferEncoding): void;
568
- installLaunchAgent?(plistPath: string, label: string): void;
569
- repairDaemon?(controlPort: number): Promise<{ repair: DaemonRepairResult; probe: DaemonProbeResult }>;
570
- waitForDaemonStatus?(controlPort: number, timeoutMs: number): Promise<DaemonProbeResult>;
571
- fetchInitState?(controlPort: number): Promise<WebInitStateForLaunch>;
572
- launchControlUi?(controlPort: number, pathname?: string): string;
573
- }
574
-
575
- async function fetchInitStateForLaunch(controlPort: number): Promise<WebInitStateForLaunch> {
576
- const res = await fetch(`http://127.0.0.1:${controlPort}/init/state`);
577
- if (!res.ok) {
578
- throw new Error(`/init/state returned ${res.status}`);
579
- }
580
- return await res.json() as WebInitStateForLaunch;
581
- }
582
-
583
- async function resolveWebInitPath(
584
- controlPort: number,
585
- fetchInitState: (controlPort: number) => Promise<WebInitStateForLaunch>
586
- ): Promise<"/init" | "/overview"> {
587
- try {
588
- const state = await fetchInitState(controlPort);
589
- if (state.repairMode === true) {
590
- return "/init";
591
- }
592
- return state.freshMachine === false || state.setup?.status === "completed" ? "/overview" : "/init";
593
- } catch (error: unknown) {
594
- logger.warn("init.web.state_probe.failed", "web init state probe failed", {
595
- errorMessage: error instanceof Error ? error.message : String(error)
596
- });
597
- return "/init";
598
- }
599
- }
600
-
601
- export async function runWebInitLauncher(deps: WebInitLauncherDeps = {}): Promise<WebInitLauncherResult> {
602
- const platform = deps.platform ?? process.platform;
603
- const controlPort = deps.controlPort ?? configuredControlPort();
604
- const proxyPort = deps.proxyPort ?? configuredProxyPort();
605
- const sellerRegistryUrl = deps.sellerRegistryUrl ?? sellerRegistryUrlForInit();
606
- const home = deps.homeDir ?? os.homedir();
607
- const launchUi = deps.launchControlUi ?? launchControlUi;
608
- const fetchInitState = deps.fetchInitState ?? fetchInitStateForLaunch;
609
- const expectedUrl = `http://127.0.0.1:${controlPort}/init`;
610
-
611
- if (platform === "darwin") {
612
- const plistDir = path.join(home, "Library", "LaunchAgents");
613
- const plistPath = path.join(plistDir, `${TOKENBUDDY_LAUNCHD_LABEL}.plist`);
614
- const stdoutPath = deps.stdoutPath ?? path.join(home, ".tokenbuddy-store", "tb-proxyd.stdout.log");
615
- const stderrPath = deps.stderrPath ?? path.join(home, ".tokenbuddy-store", "tb-proxyd.stderr.log");
616
- try {
617
- (deps.mkdirSync ?? fs.mkdirSync)(plistDir, { recursive: true });
618
- (deps.mkdirSync ?? fs.mkdirSync)(path.dirname(stdoutPath), { recursive: true });
619
- (deps.mkdirSync ?? fs.mkdirSync)(path.dirname(stderrPath), { recursive: true });
620
- const plistContent = buildLaunchdPlistContent({
621
- label: TOKENBUDDY_LAUNCHD_LABEL,
622
- nodePath: deps.nodePath ?? process.execPath,
623
- scriptPath: deps.scriptPath ?? tbProxydScriptPath(),
624
- pathEnv: deps.pathEnv,
625
- stdoutPath,
626
- stderrPath,
627
- controlPort,
628
- proxyPort,
629
- sellerRegistryUrl,
630
- clawtipProofCommand: deps.clawtipProofCommand ?? defaultClawtipProofCommand(),
631
- clawtipProofTimeoutMs: deps.clawtipProofTimeoutMs ?? (
632
- process.env.TB_PROXYD_CLAWTIP_PROOF_TIMEOUT_MS
633
- ? Number(process.env.TB_PROXYD_CLAWTIP_PROOF_TIMEOUT_MS)
634
- : undefined
635
- ),
636
- });
637
- (deps.writeFileSync ?? fs.writeFileSync)(plistPath, plistContent, "utf8");
638
- (deps.installLaunchAgent ?? installLaunchAgent)(plistPath, TOKENBUDDY_LAUNCHD_LABEL);
639
- } catch (error: unknown) {
640
- const errorMessage = error instanceof Error ? error.message : String(error);
641
- const probe = await (deps.waitForDaemonStatus ?? waitForDaemonStatus)(controlPort, 3_000);
642
- if (probe.running) {
643
- const pathname = await resolveWebInitPath(controlPort, fetchInitState);
644
- const url = deps.openBrowser !== false
645
- ? launchUi(controlPort, pathname)
646
- : `http://127.0.0.1:${controlPort}${pathname}`;
647
- logger.warn("init.web.launcher.recovered", "web init launchd install failed but service is ready", {
648
- method: "launchd",
649
- plistPath,
650
- errorMessage,
651
- });
652
- return {
653
- method: "launchd",
654
- controlPort,
655
- proxyPort,
656
- plistPath,
657
- serviceInstalled: true,
658
- url,
659
- probe,
660
- };
661
- }
662
- logger.warn("init.web.launcher.failed", "web init launchd install failed", {
663
- method: "launchd",
664
- plistPath,
665
- errorMessage
666
- });
667
- return {
668
- method: "launchd",
669
- controlPort,
670
- proxyPort,
671
- plistPath,
672
- serviceInstalled: false,
673
- url: expectedUrl,
674
- probe: { ...probe, error: probe.error || errorMessage },
675
- error: errorMessage,
676
- };
677
- }
678
-
679
- const probe = await (deps.waitForDaemonStatus ?? waitForDaemonStatus)(controlPort, 12_000);
680
- const pathname = probe.running ? await resolveWebInitPath(controlPort, fetchInitState) : "/init";
681
- const url = probe.running && deps.openBrowser !== false
682
- ? launchUi(controlPort, pathname)
683
- : `http://127.0.0.1:${controlPort}${pathname}`;
684
- return {
685
- method: "launchd",
686
- controlPort,
687
- proxyPort,
688
- plistPath,
689
- serviceInstalled: true,
690
- url,
691
- probe,
692
- error: probe.running ? undefined : probe.error || "tb-proxyd did not become ready"
693
- };
694
- }
695
-
696
- const repaired = await (deps.repairDaemon ?? repairDaemon)(controlPort);
697
- const pathname = repaired.probe.running ? await resolveWebInitPath(controlPort, fetchInitState) : "/init";
698
- const url = repaired.probe.running && deps.openBrowser !== false
699
- ? launchUi(controlPort, pathname)
700
- : `http://127.0.0.1:${controlPort}${pathname}`;
701
- return {
702
- method: "detached",
703
- controlPort,
704
- proxyPort,
705
- url,
706
- probe: repaired.probe,
707
- repair: repaired.repair,
708
- serviceInstalled: repaired.probe.running,
709
- error: repaired.probe.running ? undefined : repaired.repair.error || repaired.probe.error || "tb-proxyd did not become ready"
710
- };
711
- }
712
-
713
- function commandPath(command: Command): string {
714
- const names: string[] = [];
715
- let current: Command | null = command;
716
- while (current) {
717
- const name = current.name();
718
- if (name) {
719
- names.unshift(name);
720
- }
721
- current = current.parent || null;
722
- }
723
- return names.join(" ");
724
- }
725
-
726
- function rootActionName(command: Command): string {
727
- let current = command;
728
- while (current.parent && current.parent.parent) {
729
- current = current.parent;
730
- }
731
- return current.name();
732
- }
733
-
734
- function commandRequiresDaemon(command: Command): boolean {
735
- const rootName = rootActionName(command);
736
- return rootName !== "doctor" && rootName !== "init" && rootName !== "routing" && rootName !== "daemon" && rootName !== "ui" && rootName !== "update";
737
- }
738
-
739
- async function enforceDaemonGate(command: Command): Promise<void> {
740
- if (!commandRequiresDaemon(command)) {
741
- return;
742
- }
743
-
744
- const controlPort = configuredControlPort();
745
- const probe = await probeDaemonStatus(controlPort);
746
- if (probe.running) {
747
- return;
748
- }
749
-
750
- const commandName = commandPath(command);
751
- logger.warn("daemon.gate.blocked", "tb command blocked because tb-proxyd is not running", {
752
- command: commandName,
753
- controlPort,
754
- errorMessage: probe.error
755
- });
756
- console.error(`tb-proxyd is not running for \`${commandName}\`.`);
757
- console.error(`Checked: http://127.0.0.1:${controlPort}/status`);
758
- console.error("Run `tb doctor --fix` to repair tb-proxyd automatically, or run `tb init` to initialize TokenBuddy.");
759
- process.exitCode = 1;
760
- const error = new Error("tb-proxyd is not running") as CommandFailure;
761
- error.code = "tokenbuddy.daemon_not_running";
762
- error.exitCode = 1;
763
- throw error;
764
- }
765
-
766
- function hashText(value: string): string {
767
- return crypto.createHash("sha256").update(value).digest("hex");
768
- }
769
-
770
- function safePaymentView(payment: PaymentConfig) {
771
- return {
772
- method: payment.method,
773
- enabled: payment.enabled,
774
- isDefault: payment.isDefault,
775
- updatedAt: payment.updatedAt,
776
- config: payment.config ? {
777
- ...payment.config,
778
- proof: undefined,
779
- paymentProof: undefined,
780
- payCredential: undefined,
781
- encryptedData: undefined
782
- } : undefined
783
- };
784
- }
785
-
786
- function supportedPaymentRows(payments: PaymentConfig[]) {
787
- return SUPPORTED_PAYMENT_METHODS.map((method) => {
788
- const configured = payments.find((payment) => payment.method === method);
789
- return {
790
- method,
791
- supported: true,
792
- configured: Boolean(configured),
793
- enabled: configured?.enabled || false,
794
- isDefault: configured?.isDefault || false,
795
- updatedAt: configured?.updatedAt,
796
- config: configured ? safePaymentView(configured).config : undefined
797
- };
798
- });
799
- }
800
-
801
- function printPaymentList(payments: PaymentConfig[], asJson: boolean): void {
802
- const rows = supportedPaymentRows(payments);
803
- if (asJson) {
804
- console.log(JSON.stringify({ payments: rows }, null, 2));
805
- return;
806
- }
807
-
808
- const table = new Table({ head: ["Method", "Supported", "Configured", "Enabled", "Default"] });
809
- for (const row of rows) {
810
- table.push([
811
- row.method,
812
- row.supported ? "yes" : "no",
813
- row.configured ? "yes" : "no",
814
- row.enabled ? "yes" : "no",
815
- row.isDefault ? "yes" : "no"
816
- ]);
817
- }
818
- console.log("=== TokenBuddy Payment Methods ===");
819
- console.log(table.toString());
820
- }
821
-
822
- async function safeCheckPackageUpdate(): Promise<{ check?: PackageUpdateCheck; error?: string }> {
823
- if (process.env.NODE_ENV === "test" && process.env.TOKENBUDDY_TEST_UPDATE_CHECK !== "1") {
824
- return { error: "version check skipped in tests" };
825
- }
826
- try {
827
- return { check: await checkPackageUpdate() };
828
- } catch (error: unknown) {
829
- return { error: error instanceof Error ? error.message : String(error) };
830
- }
831
- }
832
-
833
- async function runDoctorPackageUpdate(
834
- fix: boolean,
835
- controlPort: number,
836
- ): Promise<{ check?: PackageUpdateCheck; error?: string } | PackageUpdateResult> {
837
- if (process.env.NODE_ENV === "test" && process.env.TOKENBUDDY_TEST_UPDATE_CHECK !== "1") {
838
- return { error: "version check skipped in tests" };
839
- }
840
- if (!fix) {
841
- return safeCheckPackageUpdate();
842
- }
843
- return await runPackageUpdate({ apply: true, controlPort }, { restartProxyd: restartProxydForPackageUpdate });
844
- }
845
-
846
- function printPackageUpdateCheck(result: { check?: PackageUpdateCheck; error?: string }): void {
847
- if (result.error) {
848
- console.log(`⚠️ Version check unavailable: ${result.error}`);
849
- return;
850
- }
851
- const check = result.check!;
852
- if (check.updateAvailable) {
853
- console.log(`⬆️ TokenBuddy update available: ${check.currentVersion} -> ${check.latestVersion}`);
854
- console.log(` Run \`tb update\` to install and restart tb-proxyd.`);
855
- } else {
856
- console.log(`✅ TokenBuddy package is current (${check.currentVersion}).`);
857
- }
858
- }
859
-
860
- function packageUpdateExitCode(result: PackageUpdateResult): number {
861
- if (!result.check.updateAvailable) {
862
- return 0;
863
- }
864
- if (!result.install.succeeded || !result.restart.restarted) {
865
- return 1;
866
- }
867
- return 0;
868
- }
869
-
870
- function printPackageUpdateResult(result: PackageUpdateResult): void {
871
- const { check, install, restart } = result;
872
- if (!check.updateAvailable) {
873
- console.log(`TokenBuddy is already current (${check.currentVersion}).`);
874
- return;
875
- }
876
-
877
- console.log(`TokenBuddy update available: ${check.currentVersion} -> ${check.latestVersion}`);
878
- if (!install.attempted) {
879
- console.log(`Run \`${check.installCommand}\` to install it.`);
880
- return;
881
- }
882
- if (!install.succeeded) {
883
- console.error(`Failed to install ${check.packageName}@${check.latestVersion}: ${install.error || "unknown error"}`);
884
- return;
885
- }
886
-
887
- console.log(`Installed ${check.packageName}@${check.latestVersion}.`);
888
- if (!restart.restarted) {
889
- console.error(`Failed to restart tb-proxyd: ${restart.error || "unknown error"}`);
890
- return;
891
- }
892
- console.log("tb-proxyd restarted.");
893
- }
894
-
895
- async function restartProxydForPackageUpdate(controlPort: number): Promise<PackageRestartResult> {
896
- const result = await restartLaunchAgent(controlPort);
897
- return {
898
- attempted: result.attempted,
899
- restarted: result.restarted,
900
- method: result.method,
901
- plistPath: result.plistPath,
902
- target: result.target,
903
- error: result.error,
904
- };
905
- }
906
-
907
- export {
908
- fetchClawtipBootstrap,
909
- normalizeClawtipBootstrapResourceUrl,
910
- } from "./clawtip-bootstrap.js";
911
-
912
- function readProof(options: { proofFile?: string; requireProof?: boolean }): string | undefined {
913
- const proofFile = options.proofFile || process.env.TOKENBUDDY_CLAWTIP_PROOF_FILE;
914
- if (!proofFile) {
915
- if (options.requireProof) {
916
- throw new Error("ClawTip proof is required; pass --proof-file or TOKENBUDDY_CLAWTIP_PROOF_FILE");
917
- }
918
- return undefined;
919
- }
920
- if (!fs.existsSync(proofFile)) {
921
- throw new Error(`ClawTip proof file does not exist: ${proofFile}`);
922
- }
923
- const proof = fs.readFileSync(proofFile, "utf8").trim();
924
- if (!proof) {
925
- throw new Error(`ClawTip proof file is empty: ${proofFile}`);
926
- }
927
- return proof;
928
- }
929
-
930
- function sellerRegistryUrlForInit(): string {
931
- return process.env.TB_PROXYD_SELLER_REGISTRY_URL || DEFAULT_SELLER_REGISTRY_URL;
932
- }
933
-
934
- function stableModelChoices(models: ModelCatalogEntry[]): SelectOption[] {
935
- const grouped = new Map<string, ModelCatalogEntry[]>();
936
- for (const entry of models) {
937
- const list = grouped.get(entry.id) || [];
938
- list.push(entry);
939
- grouped.set(entry.id, list);
940
- }
941
- return Array.from(grouped.entries()).map(([modelId, entries]) => {
942
- const sellerIds = Array.from(new Set(entries.map((entry) => entry.sellerId)));
943
- const protocols = Array.from(
944
- new Set(entries.flatMap((entry) => entry.supportedProtocols)),
945
- );
946
- return {
947
- value: modelId,
948
- label: modelId,
949
- hint: `${sellerIds.join(",")} · ${protocols.join(",") || "no-protocol"}`,
950
- };
951
- });
952
- }
953
-
954
- async function promptSellerRoutingPreference(catalog: SellerCatalogResult): Promise<BuyerSellerRoutingConfig> {
955
- const healthySellers = catalog.sellers.filter((seller) => seller.status === "ok");
956
- const mode = await p.select({
957
- message: "Choose seller routing mode for tb-proxyd:",
958
- options: [
959
- {
960
- value: "fullAuto",
961
- label: "Full Auto",
962
- hint: "Use all compatible sellers and rank them by the selected scorer.",
963
- },
964
- {
965
- value: "fixed",
966
- label: "Fixed Seller",
967
- hint: "Pin tb-proxyd to one seller and only use models from that seller.",
968
- },
969
- {
970
- value: "fixedSet",
971
- label: "Fixed Seller Set",
972
- hint: "Use only a selected seller pool and rank within that pool.",
973
- },
974
- ],
975
- }) as SellerRoutingMode | symbol;
976
-
977
- if (typeof mode !== "string") {
978
- throw new Error("seller routing selection was cancelled");
979
- }
980
- if ((mode === "fixed" || mode === "fixedSet") && healthySellers.length === 0) {
981
- throw new Error("no healthy sellers available for fixed routing");
982
- }
983
- const scorer = await promptSellerRoutingScorer();
984
- if (mode === "fullAuto") {
985
- return { mode, scorer };
986
- }
987
-
988
- if (mode === "fixed") {
989
- const sellerId = await p.select({
990
- message: "Choose the seller to pin tb-proxyd to:",
991
- options: sellerChoices(healthySellers),
992
- }) as string | symbol;
993
-
994
- if (typeof sellerId !== "string") {
995
- throw new Error("fixed seller selection was cancelled");
996
- }
997
- return {
998
- mode,
999
- sellerId,
1000
- scorer,
1001
- };
1002
- }
1003
-
1004
- const sellerIds = await p.multiselect({
1005
- message: "Choose the seller pool for fixedSet routing:",
1006
- options: sellerChoices(healthySellers),
1007
- required: true
1008
- }) as string[] | symbol;
1009
-
1010
- if (!Array.isArray(sellerIds)) {
1011
- throw new Error("fixedSet seller selection was cancelled");
1012
- }
1013
- return {
1014
- mode,
1015
- sellerIds,
1016
- scorer,
1017
- };
1018
- }
1019
-
1020
- async function promptSellerRoutingScorer(): Promise<SellerRoutingScorer> {
1021
- const scorer = await p.select({
1022
- message: "Choose seller ranking preference:",
1023
- options: [
1024
- { value: "balanced", label: "Balanced", hint: "Balance health, latency, and discount." },
1025
- { value: "speed", label: "Speed", hint: "Prefer healthier, faster sellers." },
1026
- { value: "discount", label: "Discount", hint: "Prefer lower discount ratio first." },
1027
- ],
1028
- }) as SellerRoutingScorer | symbol;
1029
- if (typeof scorer !== "string") {
1030
- throw new Error("seller scorer selection was cancelled");
1031
- }
1032
- return scorer;
1033
- }
1034
-
1035
- function sellerChoices(sellers: SellerCatalogResult["sellers"]): SelectOption[] {
1036
- return sellers.map((seller) => ({
1037
- value: seller.id,
1038
- label: seller.name ? `${seller.name} (${seller.id})` : seller.id,
1039
- hint: [
1040
- seller.discountRatio != null ? `discount x${seller.discountRatio}` : null,
1041
- seller.modelCount != null ? `${seller.modelCount} models` : null,
1042
- seller.supportedProtocols?.length ? seller.supportedProtocols.join(",") : null,
1043
- seller.paymentMethods?.length ? seller.paymentMethods.join(",") : null,
1044
- ]
1045
- .filter(Boolean)
1046
- .join(" · ") || seller.url,
1047
- }));
1048
- }
1049
-
1050
- function buildRoutingConfigFromOptions(
1051
- modeValue: string,
1052
- options: { seller?: string; sellerSet?: string; scorer?: string }
1053
- ): BuyerSellerRoutingConfig {
1054
- const mode = parseRoutingModeValue(modeValue);
1055
- const scorer = parseRoutingScorerValue(options.scorer || "balanced");
1056
- if (mode === "fixed") {
1057
- return {
1058
- mode,
1059
- sellerId: options.seller?.trim(),
1060
- scorer
1061
- };
1062
- }
1063
- if (mode === "fixedSet") {
1064
- return {
1065
- mode,
1066
- sellerIds: parseSellerIdList(options.sellerSet || ""),
1067
- scorer
1068
- };
1069
- }
1070
- return {
1071
- mode,
1072
- scorer
1073
- };
1074
- }
1075
-
1076
- function parseRoutingModeValue(value: string): SellerRoutingMode {
1077
- if (value === "fixed" || value === "fixedSet" || value === "fullAuto") {
1078
- return value;
1079
- }
1080
- throw new Error("routing mode must be fixed, fixedSet, or fullAuto");
1081
- }
1082
-
1083
- function parseRoutingScorerValue(value: string): SellerRoutingScorer {
1084
- if (value === "balanced" || value === "speed" || value === "discount") {
1085
- return value;
1086
- }
1087
- throw new Error("routing scorer must be balanced, speed, or discount");
1088
- }
1089
-
1090
- function printRoutingConfig(config: BuyerSellerRoutingConfig, updatedAt?: string): void {
1091
- console.log("=== TokenBuddy Seller Routing ===");
1092
- console.log(`Mode: ${config.mode}`);
1093
- console.log(`Scorer: ${config.scorer}`);
1094
- if (config.mode === "fixed") {
1095
- console.log(`Seller: ${config.sellerId || "(not configured)"}`);
1096
- }
1097
- if (config.mode === "fixedSet") {
1098
- console.log(`Seller Set: ${config.sellerIds?.join(",") || "(empty)"}`);
1099
- }
1100
- if (updatedAt) {
1101
- console.log(`Updated: ${updatedAt}`);
1102
- }
1103
- }
1104
-
1105
- async function promptSingleModelSelection(
1106
- providerId: ProviderId,
1107
- models: ModelCatalogEntry[],
1108
- ): Promise<SingleModelProviderRuntimeConfig> {
1109
- const protocolPreference = getProviderProtocolPreference(providerId);
1110
- const protocolFiltered = protocolPreference
1111
- ? filterCatalogByProtocol(models, protocolPreference)
1112
- : models;
1113
- const choices = stableModelChoices(protocolFiltered);
1114
- if (choices.length === 0) {
1115
- throw new Error(`no compatible models available for ${providerId}`);
1116
- }
1117
-
1118
- const labelMap: Record<string, string> = {
1119
- opencode: "OpenCode",
1120
- codex: "Codex",
1121
- openclaw: "OpenClaw",
1122
- hermes: "Hermes",
1123
- "claude-desktop": "Claude Desktop",
1124
- "claude-code": "Claude Code",
1125
- };
1126
-
1127
- const selectedModel = await p.select({
1128
- message: `Choose the default model for ${labelMap[providerId] || providerId}:`,
1129
- options: choices,
1130
- }) as string | symbol;
1131
-
1132
- if (typeof selectedModel !== "string") {
1133
- throw new Error(`default model selection was cancelled for ${providerId}`);
1134
- }
1135
-
1136
- return {
1137
- selectionKind: "single-model",
1138
- protocolPreference,
1139
- defaultModel: selectedModel,
1140
- };
1141
- }
1142
-
1143
- function defaultClaudeDisplayName(modelId: string): string {
1144
- return modelId.trim();
1145
- }
1146
-
1147
- function makeClaudeRoleMapping(modelId: string): ClaudeCodeModelMappingConfig {
1148
- const displayName = defaultClaudeDisplayName(modelId);
1149
- return {
1150
- selectionKind: "claude-role-mapping",
1151
- protocolPreference: "messages",
1152
- fallbackModel: modelId,
1153
- roles: {
1154
- sonnet: {
1155
- upstreamModel: modelId,
1156
- displayName,
1157
- declareOneM: true,
1158
- },
1159
- opus: {
1160
- upstreamModel: modelId,
1161
- displayName,
1162
- declareOneM: true,
1163
- },
1164
- haiku: {
1165
- upstreamModel: modelId,
1166
- displayName,
1167
- declareOneM: false,
1168
- },
1169
- },
1170
- };
1171
- }
1172
-
1173
- async function promptClaudeCodeModelSelection(
1174
- models: ModelCatalogEntry[],
1175
- ): Promise<ClaudeCodeModelMappingConfig> {
1176
- const protocolFiltered = filterCatalogByProtocol(models, "messages");
1177
- const choices = stableModelChoices(protocolFiltered);
1178
- if (choices.length === 0) {
1179
- throw new Error("no compatible message models available for Claude Code");
1180
- }
1181
-
1182
- const sonnetModel = await p.select({
1183
- message: "Choose the default Sonnet model for Claude Code:",
1184
- options: choices,
1185
- }) as string | symbol;
1186
-
1187
- if (typeof sonnetModel !== "string") {
1188
- throw new Error("Claude Code model selection was cancelled");
1189
- }
1190
-
1191
- const mirrorAllRoles = await p.confirm({
1192
- message: "Use the same model for Opus and Haiku as well?",
1193
- initialValue: true,
1194
- });
1195
-
1196
- if (typeof mirrorAllRoles !== "boolean") {
1197
- throw new Error("Claude Code role mapping confirmation was cancelled");
1198
- }
1199
-
1200
- if (mirrorAllRoles) {
1201
- return makeClaudeRoleMapping(sonnetModel);
1202
- }
1203
-
1204
- const opusModel = await p.select({
1205
- message: "Choose the default Opus model for Claude Code:",
1206
- options: choices,
1207
- }) as string | symbol;
1208
- if (typeof opusModel !== "string") {
1209
- throw new Error("Claude Code Opus model selection was cancelled");
1210
- }
1211
-
1212
- const haikuModel = await p.select({
1213
- message: "Choose the default Haiku model for Claude Code:",
1214
- options: choices,
1215
- }) as string | symbol;
1216
- if (typeof haikuModel !== "string") {
1217
- throw new Error("Claude Code Haiku model selection was cancelled");
1218
- }
1219
-
1220
- return {
1221
- selectionKind: "claude-role-mapping",
1222
- protocolPreference: "messages",
1223
- fallbackModel: sonnetModel,
1224
- roles: {
1225
- sonnet: {
1226
- upstreamModel: sonnetModel,
1227
- displayName: defaultClaudeDisplayName(sonnetModel),
1228
- declareOneM: true,
1229
- },
1230
- opus: {
1231
- upstreamModel: opusModel,
1232
- displayName: defaultClaudeDisplayName(opusModel),
1233
- declareOneM: true,
1234
- },
1235
- haiku: {
1236
- upstreamModel: haikuModel,
1237
- displayName: defaultClaudeDisplayName(haikuModel),
1238
- declareOneM: false,
1239
- },
1240
- },
1241
- };
1242
- }
1243
-
1244
- async function promptProviderSelections(
1245
- providerIds: ProviderId[],
1246
- catalog: SellerCatalogResult,
1247
- sellerRouting: BuyerSellerRoutingConfig,
1248
- ): Promise<ProviderSelections> {
1249
- const baseModels = sellerRouting.mode === "fixed"
1250
- ? filterCatalogBySeller(catalog.models, sellerRouting.sellerId)
1251
- : sellerRouting.mode === "fixedSet"
1252
- ? catalog.models.filter((model) => sellerRouting.sellerIds?.includes(model.sellerId))
1253
- : catalog.models;
1254
-
1255
- const selections: ProviderSelections = {};
1256
- for (const providerId of providerIds) {
1257
- const selectionKind = getProviderModelSelectionKind(providerId);
1258
- if (selectionKind === "claude-role-mapping") {
1259
- selections[providerId] = await promptClaudeCodeModelSelection(baseModels);
1260
- continue;
1261
- }
1262
- selections[providerId] = await promptSingleModelSelection(
1263
- providerId,
1264
- baseModels,
1265
- );
1266
- }
1267
- return selections;
1268
- }
1269
-
1270
- /**
1271
- * 构造 commander program,绑定所有 `tb` 子命令(doctor / init / config / provider 等)。
1272
- * 在 `preAction` 钩子里做 daemon gate:非 daemon-only 命令跳过,其余命令需要本地 `tb-proxyd` 在跑。
1273
- *
1274
- * @returns 配置完整的 commander Command 实例
1275
- */
1276
- export function buildCli(): Command {
1277
- const program = new Command();
1278
- program
1279
- .name("tb")
1280
- .description("Buyer CLI for TokenBuddy")
1281
- .version(readCliPackageVersion());
1282
-
1283
- program.hook("preAction", async (_thisCommand, actionCommand) => {
1284
- await enforceDaemonGate(actionCommand);
1285
- });
1286
-
1287
- program
1288
- .command("update")
1289
- .description("Check for a TokenBuddy package update, install it, and restart tb-proxyd")
1290
- .option("--check", "Only check for an available update")
1291
- .option("--json", "Output update state as JSON")
1292
- .action(async (options: { check?: boolean; json?: boolean }) => {
1293
- const controlPort = configuredControlPort();
1294
- try {
1295
- const result = await runPackageUpdate(
1296
- { apply: !options.check, controlPort },
1297
- { restartProxyd: restartProxydForPackageUpdate },
1298
- );
1299
- if (options.json) {
1300
- console.log(JSON.stringify(result, null, 2));
1301
- } else if (options.check) {
1302
- printPackageUpdateCheck({ check: result.check });
1303
- } else {
1304
- printPackageUpdateResult(result);
1305
- }
1306
- if (!options.check) {
1307
- process.exitCode = packageUpdateExitCode(result);
1308
- }
1309
- } catch (error: unknown) {
1310
- const errorMessage = error instanceof Error ? error.message : String(error);
1311
- process.exitCode = 1;
1312
- if (options.json) {
1313
- console.log(JSON.stringify({ error: { code: "package_update_failed", message: errorMessage } }, null, 2));
1314
- } else {
1315
- console.error(`TokenBuddy update failed: ${errorMessage}`);
1316
- }
1317
- }
1318
- });
1319
-
1320
- // 1. tb doctor
1321
- program
1322
- .command("doctor")
1323
- .description("Check running status, system agents, and network diagnostics")
1324
- .option("--json", "Output diagnostics as JSON")
1325
- .option("--fix", "Start tb-proxyd when needed, install available package updates, and restart the service")
1326
- .action(async (options: { json?: boolean; fix?: boolean }) => {
1327
- const controlPort = configuredControlPort();
1328
- const proxyPort = configuredProxyPort();
1329
- const controlUrl = `http://127.0.0.1:${controlPort}`;
1330
- const plistPath = process.platform === "darwin"
1331
- ? path.join(os.homedir(), "Library", "LaunchAgents", "com.tokenbuddy.proxyd.plist")
1332
- : undefined;
1333
- let probe = await probeDaemonStatus(controlPort);
1334
- let repair: DaemonRepairResult = { attempted: false, fixed: false };
1335
- if (!probe.running && options.fix) {
1336
- const repaired = await repairDaemon(controlPort);
1337
- repair = repaired.repair;
1338
- probe = repaired.probe;
1339
- }
1340
- const daemonInfo = probe.status;
1341
- const daemonRunning = probe.running;
1342
- const daemonError = probe.error;
1343
- const daemonStatus = daemonInfo && typeof daemonInfo === "object"
1344
- ? daemonInfo as { selectionMode?: string; sellerRoutingMode?: string; selectedSellerId?: string; sellerRegistryUrl?: string }
1345
- : undefined;
1346
- const providers = readDoctorProviders();
1347
- const updateState = await runDoctorPackageUpdate(Boolean(options.fix), controlPort).catch((error: unknown) => ({
1348
- error: error instanceof Error ? error.message : String(error)
1349
- }));
1350
- if (options.fix && repair.attempted && !repair.fixed) {
1351
- process.exitCode = 1;
1352
- }
1353
- if (options.fix && "check" in updateState && updateState.check?.updateAvailable) {
1354
- process.exitCode = packageUpdateExitCode(updateState as PackageUpdateResult);
1355
- }
1356
- if (options.fix && "error" in updateState && updateState.error) {
1357
- process.exitCode = 1;
1358
- }
1359
-
1360
- if (options.json) {
1361
- const diagnostics = await collectDoctorDiagnostics({
1362
- controlPort,
1363
- proxyPort,
1364
- daemonRunning,
1365
- daemonError,
1366
- providers,
1367
- sellerRegistryUrl: daemonStatus?.sellerRegistryUrl,
1368
- });
1369
- const v12Snapshot = daemonRunning
1370
- ? await fetchV12Snapshot(controlUrl)
1371
- : null;
1372
- console.log(JSON.stringify({
1373
- daemon: {
1374
- running: daemonRunning,
1375
- controlPort,
1376
- proxyPort,
1377
- controlUrl,
1378
- status: daemonInfo,
1379
- error: daemonError,
1380
- fixAvailable: true
1381
- },
1382
- repair: {
1383
- requested: Boolean(options.fix),
1384
- ...repair
1385
- },
1386
- packageUpdate: updateState,
1387
- service: {
1388
- platform: process.platform,
1389
- plistPath,
1390
- plistExists: plistPath ? fs.existsSync(plistPath) : false
1391
- },
1392
- v12: v12Snapshot,
1393
- ...diagnostics,
1394
- }, null, 2));
1395
- return;
1396
- }
1397
-
1398
- console.log("=== TokenBuddy System Diagnostics ===");
1399
- printPackageUpdateCheck("check" in updateState ? { check: updateState.check } : updateState);
1400
- if (options.fix && "install" in updateState) {
1401
- printPackageUpdateResult(updateState);
1402
- }
1403
- console.log("");
1404
-
1405
- // 1. Detect if daemon is listening
1406
- if (daemonRunning) {
1407
- const info = daemonInfo as { pid?: number; controlPort?: number; proxyPort?: number };
1408
- console.log(`✅ Daemon tb-proxyd is running (PID: ${info.pid})`);
1409
- console.log(` Control Plane Port: ${info.controlPort}`);
1410
- console.log(` Proxy Plane Port: ${info.proxyPort}`);
1411
- } else {
1412
- console.log("❌ Daemon tb-proxyd is NOT running.");
1413
- if (options.fix && repair.attempted) {
1414
- console.log(`❌ Automatic repair failed: ${repair.error || daemonError || "unknown error"}`);
1415
- } else {
1416
- console.log(" Run `tb doctor --fix` to start tb-proxyd in the background.");
1417
- }
1418
- }
1419
-
1420
- if (options.fix && repair.fixed) {
1421
- console.log(`✅ tb-proxyd was started in the background (PID: ${repair.pid}).`);
1422
- }
1423
-
1424
- // 2. Detect plist launchd status on Darwin
1425
- if (plistPath) {
1426
- if (fs.existsSync(plistPath)) {
1427
- console.log(`✅ LaunchAgent plist exists at: ${plistPath}`);
1428
- } else {
1429
- console.log("⚠️ LaunchAgent plist does NOT exist. Run `tb init` to install it as service.");
1430
- }
1431
- }
1432
-
1433
- if (daemonStatus) {
1434
- console.log(` Control Plane URL: ${controlUrl}`);
1435
- console.log(` Proxy Plane URL: http://127.0.0.1:${proxyPort}`);
1436
- if (daemonStatus.sellerRoutingMode || daemonStatus.selectionMode) {
1437
- console.log(` Routing Mode: ${daemonStatus.sellerRoutingMode || daemonStatus.selectionMode}`);
1438
- }
1439
- if (daemonStatus.selectedSellerId) {
1440
- console.log(` Selected Seller: ${daemonStatus.selectedSellerId}`);
1441
- }
1442
- if (daemonStatus.sellerRegistryUrl) {
1443
- console.log(` Registry URL: ${daemonStatus.sellerRegistryUrl}`);
1444
- }
1445
- }
1446
-
1447
- printDoctorProviders(providers);
1448
- await renderDoctorDiagnosticsProgressively({
1449
- controlPort,
1450
- proxyPort,
1451
- daemonRunning,
1452
- daemonError,
1453
- sellerRegistryUrl: daemonStatus?.sellerRegistryUrl,
1454
- providers,
1455
- });
1456
-
1457
- // v1.2 §18.11: append the prewarm cache / pool / credit summary
1458
- // block so users can see at a glance how much of their budget is
1459
- // being burned by failover and which sellers are in circuit_open.
1460
- if (daemonRunning) {
1461
- await renderDoctorV12Section(controlUrl);
1462
- }
1463
- });
1464
-
1465
- // tb ui — 探测 daemon + 用系统默认浏览器打开控制台
1466
- program
1467
- .command("ui")
1468
- .description("Open the local TokenBuddy control console in your default browser")
1469
- .action(async () => {
1470
- const controlPort = configuredControlPort();
1471
- const url = `http://127.0.0.1:${controlPort}/`;
1472
- const probe = await probeDaemonStatus(controlPort);
1473
- if (!probe.running) {
1474
- console.error(`tb-proxyd is not running.`);
1475
- console.error(`Checked: ${url}status`);
1476
- console.error(`Run \`tb init\` to complete first-time setup, then \`tb-proxyd\` to start the daemon.`);
1477
- process.exitCode = 1;
1478
- const err = new Error("tb-proxyd is not running") as CommandFailure;
1479
- err.code = "tokenbuddy.daemon_not_running";
1480
- err.exitCode = 1;
1481
- throw err;
1482
- }
1483
- console.log(`Opening ${launchControlUi(controlPort)} in your default browser…`);
1484
- });
1485
-
1486
- const daemon = program
1487
- .command("daemon")
1488
- .description("Manage the local tb-proxyd service");
1489
-
1490
- daemon
1491
- .command("restart")
1492
- .description("Restart tb-proxyd through the installed macOS LaunchAgent")
1493
- .option("--json", "Output restart result as JSON")
1494
- .action(async (options: { json?: boolean }) => {
1495
- const controlPort = configuredControlPort();
1496
- const proxyPort = configuredProxyPort();
1497
- if (!options.json) {
1498
- console.log("Restarting tb-proxyd LaunchAgent...");
1499
- }
1500
- const result = await restartLaunchAgent(controlPort);
1501
- if (!result.restarted) {
1502
- process.exitCode = 1;
1503
- }
1504
-
1505
- if (options.json) {
1506
- const daemonProbe = result.after || result.before;
1507
- console.log(JSON.stringify({
1508
- restart: result,
1509
- daemon: {
1510
- running: Boolean(daemonProbe?.running),
1511
- controlPort,
1512
- proxyPort,
1513
- controlUrl: `http://127.0.0.1:${controlPort}`,
1514
- proxyUrl: `http://127.0.0.1:${proxyPort}`,
1515
- status: daemonProbe?.status,
1516
- error: daemonProbe?.error || result.error
1517
- }
1518
- }, null, 2));
1519
- return;
1520
- }
1521
-
1522
- if (!result.restarted) {
1523
- console.error(`Failed to restart tb-proxyd: ${result.error || "unknown error"}`);
1524
- if (!result.after?.running) {
1525
- console.error(`Checked: http://127.0.0.1:${controlPort}/status`);
1526
- }
1527
- return;
1528
- }
1529
-
1530
- const status = result.after?.status && typeof result.after.status === "object"
1531
- ? result.after.status as { pid?: number; controlPort?: number; proxyPort?: number }
1532
- : undefined;
1533
- console.log(`✅ tb-proxyd restarted${status?.pid ? ` (PID: ${status.pid})` : ""}.`);
1534
- console.log(` Control Plane URL: http://127.0.0.1:${status?.controlPort || controlPort}`);
1535
- console.log(` Proxy Plane URL: http://127.0.0.1:${status?.proxyPort || proxyPort}`);
1536
- });
1537
-
1538
- // v1.2 §18.11 helpers for `tb doctor` / `tb doctor --json`.
1539
- async function fetchV12Snapshot(controlUrl: string): Promise<unknown | null> {
1540
- try {
1541
- const res = await fetch(`${controlUrl}/v1.2/prewarm`);
1542
- if (!res.ok) {
1543
- return null;
1544
- }
1545
- return await res.json();
1546
- } catch {
1547
- return null;
1548
- }
1549
- }
1550
-
1551
- async function renderDoctorV12Section(controlUrl: string): Promise<void> {
1552
- try {
1553
- const res = await fetch(`${controlUrl}/v1.2/prewarm`);
1554
- if (!res.ok) {
1555
- return;
1556
- }
1557
- const snapshot = (await res.json()) as {
1558
- prewarm: { entries: Array<{ modelId: string; state: string; candidateCount: number; warmedAt: number; ttlMs: number; consecutiveWarmingFailures: number }>; size: number };
1559
- pool: { size: number; entries: Array<{ sellerId: string; circuit: string; healthScore: number }> };
1560
- credit: { totalWastedMicros: number; wastedSinceLastDoctorRun: number; purchasesInLastMinute: number; purchaseBudgetPerMinute: number; perSeller: Array<{ sellerId: string; currentBalanceMicros: number; leftoverCreditMicros: number }> };
1561
- focusSet: string[];
1562
- scheduler: { inFlight: number; queueDepth: number; totalSucceeded: number; totalFailed: number };
1563
- };
1564
- console.log("");
1565
- console.log("=== v1.2 Fallback Pipeline ===");
1566
- console.log(`Focus Set: ${snapshot.focusSet.length === 0 ? "(empty; using lazy prewarms)" : snapshot.focusSet.join(", ")}`);
1567
- console.log(`Prewarm Cache: ${snapshot.prewarm.size} entries`);
1568
- for (const entry of snapshot.prewarm.entries.slice(0, 10)) {
1569
- console.log(` - ${entry.modelId} [${entry.state}] ${entry.candidateCount} candidates, age ${Math.max(0, Date.now() - entry.warmedAt)}ms / ttl ${entry.ttlMs}ms`);
1570
- }
1571
- const open = snapshot.pool.entries.filter((e) => e.circuit !== "closed");
1572
- console.log(`Seller Pool: ${snapshot.pool.size} entries, ${open.length} non-closed`);
1573
- for (const entry of open.slice(0, 5)) {
1574
- console.log(` - ${entry.sellerId} [${entry.circuit}] healthScore=${entry.healthScore}`);
1575
- }
1576
- console.log(`Credit: totalWasted=${snapshot.credit.totalWastedMicros}μ, sinceLastDoctor=${snapshot.credit.wastedSinceLastDoctorRun}μ, purchasesInLastMinute=${snapshot.credit.purchasesInLastMinute}/${snapshot.credit.purchaseBudgetPerMinute}`);
1577
- for (const seller of snapshot.credit.perSeller.slice(0, 5)) {
1578
- console.log(` - ${seller.sellerId} balance=${seller.currentBalanceMicros}μ, leftover=${seller.leftoverCreditMicros}μ`);
1579
- }
1580
- console.log(`Scheduler: inFlight=${snapshot.scheduler.inFlight}, queueDepth=${snapshot.scheduler.queueDepth}, succeeded=${snapshot.scheduler.totalSucceeded}, failed=${snapshot.scheduler.totalFailed}`);
1581
- } catch (err) {
1582
- // Doctor must not fail because of an optional section.
1583
- const message = err instanceof Error ? err.message : String(err);
1584
- console.log(`\n(v1.2 snapshot unavailable: ${message})`);
1585
- }
1586
- }
1587
-
1588
- // 2. tb payment
1589
- const payment = program.command("payment").description("Manage payment methods");
1590
-
1591
- payment
1592
- .command("list")
1593
- .description("List configured and available payment methods")
1594
- .option("--json", "Output payment state as JSON")
1595
- .action(async (options: { json?: boolean }) => {
1596
- const store = openBuyerStore();
1597
- try {
1598
- printPaymentList(store.listPayments(), Boolean(options.json));
1599
- } finally {
1600
- store.close();
1601
- }
1602
- });
1603
-
1604
- payment
1605
- .command("add <method>")
1606
- .description("Add/Configure a payment method")
1607
- .option("--bootstrap-url <url>", "Wallet bootstrap URL for ClawTip activation")
1608
- .option("--proof-file <file>", "File containing ClawTip payment proof")
1609
- .option("--require-proof", "Require ClawTip payment proof before saving the method")
1610
- .action(async (method: string, options: { bootstrapUrl?: string; proofFile?: string; requireProof?: boolean }) => {
1611
- if (!isSupportedPaymentMethod(method)) {
1612
- console.error(`Unsupported payment method: ${method}`);
1613
- process.exitCode = 1;
1614
- return;
1615
- }
1616
-
1617
- const store = openBuyerStore();
1618
- try {
1619
- if (method === "mock") {
1620
- store.savePayment({
1621
- method: "mock",
1622
- enabled: true,
1623
- isDefault: true,
1624
- config: { channel: "developer", explicitOptIn: true }
1625
- });
1626
- logger.info("payment.channel.added", "payment channel added", {
1627
- method: "mock",
1628
- isDefault: true
1629
- });
1630
- console.log("Mock payment method registered and set as default.");
1631
- return;
1632
- }
1633
-
1634
- const bootstrapUrl = options.bootstrapUrl || process.env.TOKENBUDDY_BOOTSTRAP_URL || DEFAULT_CLAWTIP_BOOTSTRAP_URL;
1635
- const proof = readProof({
1636
- proofFile: options.proofFile,
1637
- requireProof: options.requireProof
1638
- });
1639
- const bootstrap = await fetchClawtipBootstrap(bootstrapUrl);
1640
- const paymentPayload = bootstrap.payment!;
1641
- const proofHash = proof ? hashText(proof) : undefined;
1642
- store.savePayment({
1643
- method: "clawtip",
1644
- enabled: true,
1645
- isDefault: true,
1646
- config: {
1647
- bootstrapUrl,
1648
- orderNo: paymentPayload.orderNo,
1649
- amountFen: paymentPayload.amountFen ?? bootstrap.activationFeeFen,
1650
- indicator: paymentPayload.indicator,
1651
- slug: paymentPayload.slug,
1652
- skillId: paymentPayload.skillId,
1653
- description: paymentPayload.description,
1654
- resourceUrl: paymentPayload.resourceUrl,
1655
- proofHash,
1656
- proofRequired: Boolean(options.requireProof)
1657
- }
1658
- });
1659
- logger.info("payment.channel.added", "payment channel added", {
1660
- method: "clawtip",
1661
- isDefault: true,
1662
- proofProvided: Boolean(proofHash),
1663
- orderNo: paymentPayload.orderNo
1664
- });
1665
- console.log("ClawTip payment method registered and set as default.");
1666
- console.log(`Order: ${paymentPayload.orderNo}`);
1667
- console.log(`AmountFen: ${paymentPayload.amountFen ?? bootstrap.activationFeeFen}`);
1668
- console.log(`Indicator: ${paymentPayload.indicator}`);
1669
- console.log(`ResourceUrl: ${paymentPayload.resourceUrl}`);
1670
- if (paymentPayload.resourceUrl) {
1671
- qrcode.generate(paymentPayload.resourceUrl, { small: true });
1672
- }
1673
- } catch (error: unknown) {
1674
- console.error(error instanceof Error ? error.message : String(error));
1675
- process.exitCode = 1;
1676
- } finally {
1677
- store.close();
1678
- }
1679
- });
1680
-
1681
- payment
1682
- .command("remove <method>")
1683
- .description("Remove a payment method")
1684
- .action(async (method: string) => {
1685
- if (!isSupportedPaymentMethod(method)) {
1686
- console.error(`Unsupported payment method: ${method}`);
1687
- process.exitCode = 1;
1688
- return;
1689
- }
1690
- const store = openBuyerStore();
1691
- try {
1692
- const removed = store.removePayment(method);
1693
- logger.info("payment.channel.removed", "payment channel removed", {
1694
- method,
1695
- removed
1696
- });
1697
- console.log(`Payment method \`${method}\` ${removed ? "removed" : "was not configured"}.`);
1698
- } finally {
1699
- store.close();
1700
- }
1701
- });
1702
-
1703
- // 3. tb routing
1704
- const routing = program.command("routing").description("Manage buyer-side seller routing strategy");
1705
-
1706
- routing
1707
- .command("show")
1708
- .description("Show the configured seller routing strategy")
1709
- .option("--json", "Output routing config as JSON")
1710
- .action(async (options: { json?: boolean }) => {
1711
- const store = openBuyerStore();
1712
- try {
1713
- const record = store.getDaemonRuntimeConfig<unknown>(ROUTING_CONFIG_KEY);
1714
- const config = record ? normalizeSellerRoutingConfig(record.config) : defaultSellerRoutingConfig();
1715
- if (options.json) {
1716
- console.log(JSON.stringify({ routing: config, updatedAt: record?.updatedAt }, null, 2));
1717
- return;
1718
- }
1719
- printRoutingConfig(config, record?.updatedAt);
1720
- } finally {
1721
- store.close();
1722
- }
1723
- });
1724
-
1725
- routing
1726
- .command("set <mode>")
1727
- .description("Set seller routing strategy: fullAuto, fixed, or fixedSet")
1728
- .option("--seller <id>", "Seller id for fixed routing")
1729
- .option("--seller-set <ids>", "Comma-separated seller ids for fixedSet routing")
1730
- .option("--scorer <scorer>", "Ranking scorer: balanced, speed, or discount", "balanced")
1731
- .action(async (mode: string, options: { seller?: string; sellerSet?: string; scorer?: string }) => {
1732
- const config = buildRoutingConfigFromOptions(mode, options);
1733
- assertSellerRoutingConfig(config);
1734
- const store = openBuyerStore();
1735
- try {
1736
- store.saveDaemonRuntimeConfig(ROUTING_CONFIG_KEY, config);
1737
- } finally {
1738
- store.close();
1739
- }
1740
- printRoutingConfig(config);
1741
- });
1742
-
1743
- // 3. tb models
1744
- program
1745
- .command("models")
1746
- .description("Show available LLM models through local proxy")
1747
- .option("--json", "Output model list as JSON")
1748
- .action(async (options: { json?: boolean }) => {
1749
- try {
1750
- const controlPort = configuredControlPort();
1751
- const proxyPort = configuredProxyPort();
1752
- const status = await probeDaemonStatus(controlPort);
1753
- const daemonInfo = status.status && typeof status.status === "object"
1754
- ? status.status as { sellerRegistryUrl?: string }
1755
- : undefined;
1756
- const models = await collectDoctorModelsSummary({
1757
- controlPort,
1758
- proxyPort,
1759
- daemonRunning: status.running,
1760
- daemonError: status.error,
1761
- sellerRegistryUrl: daemonInfo?.sellerRegistryUrl,
1762
- });
1763
-
1764
- if (options.json) {
1765
- console.log(JSON.stringify(models, null, 2));
1766
- if (!models.available) {
1767
- process.exitCode = 1;
1768
- }
1769
- return;
1770
- }
1771
-
1772
- printDoctorModelsSummary(models);
1773
- if (!models.available) {
1774
- process.exitCode = 1;
1775
- }
1776
- } catch (err: any) {
1777
- console.error("Error connecting to local proxy:", err.message);
1778
- process.exitCode = 1;
1779
- }
1780
- });
1781
-
1782
- // 4. tb init (WOW terminal guide向导)
1783
- program
1784
- .command("init")
1785
- .description("Start TokenBuddy and open the web setup wizard")
1786
- .option("--terminal", "Run the legacy terminal setup wizard")
1787
- .action(async (options: { terminal?: boolean }) => {
1788
- if (!options.terminal) {
1789
- const result = await runWebInitLauncher();
1790
- if (result.probe.running) {
1791
- console.log(`TokenBuddy local service is ready on :${result.controlPort}.`);
1792
- console.log(`Opening ${result.url} in your default browser…`);
1793
- console.log("Run `tb init --terminal` if you need the legacy terminal wizard.");
1794
- return;
1795
- }
1796
-
1797
- console.error("TokenBuddy local service did not become ready.");
1798
- console.error(`Checked: http://127.0.0.1:${result.controlPort}/status`);
1799
- if (result.method === "launchd" && result.plistPath) {
1800
- console.error(`LaunchAgent: ${result.plistPath}`);
1801
- }
1802
- if (result.error) {
1803
- console.error(`Reason: ${result.error}`);
1804
- }
1805
- console.error("Run `tb doctor --fix` to retry background startup, or `tb init --terminal` for the legacy terminal wizard.");
1806
- process.exitCode = 1;
1807
- const error = new Error("TokenBuddy local service did not become ready") as CommandFailure;
1808
- error.code = "tokenbuddy.init_daemon_not_ready";
1809
- error.exitCode = 1;
1810
- throw error;
1811
- }
1812
-
1813
- p.intro("🚀 Welcome to TokenBuddy Interactive Wizard!");
1814
- const setupSummaryLines: string[] = [];
1815
-
1816
- // Step 1: Scan coding terminals
1817
- const spinner = p.spinner();
1818
- spinner.start("Scanning local system for programming terminals...");
1819
- const candidates = detectProviders();
1820
- const terminalSelection = buildInitTerminalSelectionState(candidates);
1821
- spinner.stop("Scan completed.");
1822
-
1823
- const installedTerminalMessage = buildInstalledTerminalMessage(terminalSelection.installed);
1824
- if (installedTerminalMessage) {
1825
- p.note(installedTerminalMessage, "Already Configured");
1826
- setupSummaryLines.push(`${terminalSelection.installed.length} terminal${terminalSelection.installed.length === 1 ? "" : "s"} already configured.`);
1827
- }
1828
-
1829
- if (terminalSelection.options.length === 1 && terminalSelection.options[0].value === OTHER_TERMINAL_OPTION.value) {
1830
- p.note("No active programming terminals detected. Install one of Codex, Claude Code, Claude Desktop, OpenCode, OpenClaw or Hermes first.");
1831
- } else {
1832
- const selected = await p.multiselect({
1833
- message: "Select programming terminals to route via TokenBuddy (use Space to select, Enter to confirm):",
1834
- options: terminalSelection.options,
1835
- required: false
1836
- }) as string[];
1837
-
1838
- const selectionError = validateInitTerminalSelection(selected);
1839
- if (selectionError) {
1840
- throw new Error(selectionError);
1841
- }
1842
-
1843
- const selectedActionable = selected.filter((value) => !value.endsWith(":installed"));
1844
- const selectedOther = selectedActionable.includes(OTHER_TERMINAL_OPTION.value);
1845
- const selectedProviders = selectedActionable.filter((value) => value !== OTHER_TERMINAL_OPTION.value);
1846
-
1847
- if (selectedOther) {
1848
- p.note(
1849
- [
1850
- "✅ OpenAI-compatible Proxy",
1851
- " URL: http://127.0.0.1:17821/v1",
1852
- " Probe: http://127.0.0.1:17821/v1/models",
1853
- " Token: TOKENBUDDY_PROXY",
1854
- "",
1855
- "✅ Anthropic-compatible Proxy",
1856
- " URL: http://127.0.0.1:17821"
1857
- ].join("\n"),
1858
- "TokenBuddy Proxy Interfaces"
1859
- );
1860
- setupSummaryLines.push("Manual terminal setup selected via Other.");
1861
- }
1862
-
1863
- if (selectedProviders.length > 0) {
1864
- spinner.start("Fetching seller-backed model catalog...");
1865
- const proxyUrl = `http://127.0.0.1:${configuredProxyPort()}`;
1866
- const registryUrl = sellerRegistryUrlForInit();
1867
- let catalog: SellerCatalogResult;
1868
- try {
1869
- catalog = await discoverSellerBackedModels(registryUrl);
1870
- } catch (error: unknown) {
1871
- spinner.stop("Failed to fetch seller-backed models.");
1872
- throw error;
1873
- }
1874
- spinner.stop("Seller-backed model catalog loaded.");
1875
-
1876
- const providerIds = selectedProviders.filter((provider): provider is ProviderId => {
1877
- return [
1878
- "codex",
1879
- "claude-code",
1880
- "claude-desktop",
1881
- "openclaw",
1882
- "opencode",
1883
- "hermes",
1884
- ].includes(provider);
1885
- });
1886
-
1887
- const sellerRouting = await promptSellerRoutingPreference(catalog);
1888
- const providerSelections = await promptProviderSelections(
1889
- providerIds,
1890
- catalog,
1891
- sellerRouting,
1892
- );
1893
-
1894
- spinner.start("Configuring proxy routing in selected terminals...");
1895
- const store = openBuyerStore();
1896
- try {
1897
- store.saveDaemonRuntimeConfig(ROUTING_CONFIG_KEY, sellerRouting);
1898
- applyProviderInstall({
1899
- providers: providerIds,
1900
- proxyUrl,
1901
- providerSelections,
1902
- }, store);
1903
- } finally {
1904
- store.close();
1905
- }
1906
- spinner.stop("Selected terminals successfully configured.");
1907
- setupSummaryLines.push(`${providerIds.length} programming terminal${providerIds.length === 1 ? "" : "s"} configured for TokenBuddy.`);
1908
- }
1909
- }
1910
-
1911
- // Step 2: Choose Payment Method & Scan QR Activation
1912
- noteInitComingSoonPayments();
1913
- const payMethod = await p.select({
1914
- message: "Choose your primary payment method for LLM token purchases:",
1915
- options: INIT_PAYMENT_OPTIONS
1916
- }) as InitPaymentMethod;
1917
-
1918
- if (payMethod === "clawtip") {
1919
- const store = openBuyerStore();
1920
- try {
1921
- let walletConfig = inspectOpenClawWalletConfig();
1922
- const clawtipReadiness = inspectClawtipWalletReadiness(store.getPayment("clawtip"), walletConfig);
1923
- const existingClawtip = clawtipReadiness.reusableBinding;
1924
- if (existingClawtip) {
1925
- store.savePayment({
1926
- method: "clawtip",
1927
- enabled: true,
1928
- isDefault: true,
1929
- config: existingClawtip.config
1930
- });
1931
- const details = [
1932
- existingClawtip.orderNo ? `Order: ${existingClawtip.orderNo}` : undefined,
1933
- existingClawtip.resourceUrl ? `ResourceUrl: ${existingClawtip.resourceUrl}` : undefined
1934
- ].filter(Boolean).join("\n");
1935
- logger.info("payment.channel.reused", "clawtip payment channel already configured locally", {
1936
- method: "clawtip",
1937
- hasOrderNo: Boolean(existingClawtip.orderNo),
1938
- hasResourceUrl: Boolean(existingClawtip.resourceUrl)
1939
- });
1940
- p.note(
1941
- details
1942
- ? `ClawTip wallet is already configured locally.\n${details}`
1943
- : "ClawTip wallet is already configured locally.",
1944
- "ClawTip"
1945
- );
1946
- setupSummaryLines.push("ClawTip wallet already bound locally; activation skipped.");
1947
- } else {
1948
- if (clawtipReadiness.status === "metadata_missing_wallet") {
1949
- p.note(
1950
- [
1951
- clawtipReadiness.message,
1952
- `Expected: ${walletConfig.expectedPath}`,
1953
- walletConfig.alternatePaths.length > 0
1954
- ? `Alternates: ${walletConfig.alternatePaths.join(", ")}`
1955
- : "Alternates: -"
1956
- ].join("\n"),
1957
- "ClawTip"
1958
- );
1959
- setupSummaryLines.push("Saved ClawTip metadata found, but local wallet config is missing; activation restarted.");
1960
- }
1961
-
1962
- const walletReadyBeforePay = walletConfig.exists;
1963
- let openClawVersion: string | undefined;
1964
- if (!walletReadyBeforePay) {
1965
- spinner.start("Checking OpenClaw CLI before ClawTip wallet bootstrap...");
1966
- openClawVersion = await checkOpenClawRuntime();
1967
- spinner.stop("OpenClaw CLI detected.");
1968
- }
1969
-
1970
- spinner.start("Requesting payment activation payload from public bootstrap registry...");
1971
- const bootstrapUrl = process.env.TOKENBUDDY_BOOTSTRAP_URL || DEFAULT_CLAWTIP_BOOTSTRAP_URL;
1972
- const bootstrap = await fetchClawtipBootstrap(bootstrapUrl);
1973
- spinner.stop("Bootstrap payload received.");
1974
-
1975
- if (!bootstrap.payment?.orderNo || !bootstrap.payment?.indicator) {
1976
- throw new Error("ClawTip bootstrap response missing orderNo or indicator.");
1977
- }
1978
-
1979
- const activationPayment = {
1980
- orderNo: bootstrap.payment.orderNo,
1981
- amountFen: bootstrap.payment.amountFen ?? bootstrap.activationFeeFen ?? 1,
1982
- payTo: bootstrap.payment.payTo,
1983
- encryptedData: bootstrap.payment.encryptedData,
1984
- indicator: bootstrap.payment.indicator,
1985
- slug: bootstrap.payment.slug,
1986
- skillId: bootstrap.payment.skillId,
1987
- description: bootstrap.payment.description,
1988
- resourceUrl: bootstrap.payment.resourceUrl,
1989
- };
1990
-
1991
- spinner.start("Starting the ClawTip payment activation flow...");
1992
- let activation = await startClawtipWalletBootstrap(activationPayment);
1993
- spinner.stop("ClawTip payment activation finished.");
1994
-
1995
- let payCredential = activation.payCredential;
1996
- for (
1997
- let authAttempt = 1;
1998
- (walletReadyBeforePay ? !payCredential : !walletConfig.exists)
1999
- && activation.parsedOutput.requiresWalletAuth
2000
- && authAttempt <= 3;
2001
- authAttempt += 1
2002
- ) {
2003
- let qrDisplayMessage: string | undefined;
2004
- let manualOpenCommand: string | undefined;
2005
- if (activation.parsedOutput.mediaPath) {
2006
- const qrDisplay = await displayTerminalImage(activation.parsedOutput.mediaPath);
2007
- qrDisplayMessage = qrDisplay.message;
2008
- manualOpenCommand = qrDisplay.fallbackCommand;
2009
- }
2010
- if (!activation.parsedOutput.mediaPath && !activation.parsedOutput.authUrl) {
2011
- throw new Error(
2012
- `ClawTip pay requested authorization but did not return QR media or authUrl. Order file: ${activation.orderFile}`
2013
- );
2014
- }
2015
- p.note(
2016
- [
2017
- activation.parsedOutput.mediaPath
2018
- ? `Open or scan this ClawTip wallet QR image with the JD app: ${activation.parsedOutput.mediaPath}`
2019
- : undefined,
2020
- activation.parsedOutput.authUrl
2021
- ? `Open or scan this ClawTip wallet auth URL with the JD app: ${activation.parsedOutput.authUrl}`
2022
- : undefined,
2023
- qrDisplayMessage,
2024
- manualOpenCommand ? `Manual open command: ${manualOpenCommand}` : undefined,
2025
- activation.parsedOutput.clawtipId ? `clawtipId: ${activation.parsedOutput.clawtipId}` : undefined,
2026
- activation.parsedOutput.clawtipId
2027
- ? "After scanning, ClawTip CLI will register the local agent wallet."
2028
- : "After scanning, TokenBuddy will retry ClawTip payment activation.",
2029
- `Expected wallet config: ${walletConfig.expectedPath}`,
2030
- openClawVersion ? `OpenClaw: ${openClawVersion}` : undefined,
2031
- ].filter(Boolean).join("\n"),
2032
- activation.parsedOutput.clawtipId ? "ClawTip Wallet QR" : "ClawTip Authorization QR"
2033
- );
2034
-
2035
- if (activation.parsedOutput.clawtipId && !walletReadyBeforePay) {
2036
- spinner.start("Waiting for ClawTip wallet registration. Press Ctrl+C to cancel.");
2037
- const walletRegistered = await waitForClawtipActivationConfirmation({
2038
- clawtipId: activation.parsedOutput.clawtipId,
2039
- });
2040
- spinner.stop(walletRegistered
2041
- ? "Detected local OpenClaw ClawTip wallet config."
2042
- : "ClawTip wallet registration cancelled.");
2043
- if (!walletRegistered) {
2044
- setupSummaryLines.push("ClawTip wallet registration was cancelled before local wallet config was saved.");
2045
- process.exitCode = 1;
2046
- return;
2047
- }
2048
-
2049
- walletConfig = inspectOpenClawWalletConfig();
2050
- if (!walletConfig.exists) {
2051
- throw new Error(`ClawTip wallet registration finished but wallet config is still missing: ${walletConfig.expectedPath}`);
2052
- }
2053
- setupSummaryLines.push("ClawTip wallet registered locally.");
2054
- } else {
2055
- const authorized = await p.confirm({
2056
- message: "Scan or authorize this ClawTip QR in the JD app, then press Enter to retry payment activation.",
2057
- initialValue: true,
2058
- });
2059
- if (authorized !== true) {
2060
- setupSummaryLines.push("ClawTip authorization was cancelled before payment activation completed.");
2061
- process.exitCode = 1;
2062
- return;
2063
- }
2064
- walletConfig = inspectOpenClawWalletConfig();
2065
- }
2066
-
2067
- payCredential = readClawtipPayCredential(activation.orderFile);
2068
- if (!payCredential) {
2069
- spinner.start(`Retrying ClawTip payment activation after authorization (${authAttempt}/3)...`);
2070
- activation = await startClawtipWalletBootstrap(activationPayment);
2071
- spinner.stop("ClawTip payment activation retry finished.");
2072
- payCredential = activation.payCredential;
2073
- }
2074
- }
2075
-
2076
- const refreshedWalletConfig = inspectOpenClawWalletConfig();
2077
- if (!walletReadyBeforePay && !refreshedWalletConfig.exists) {
2078
- const paymentQrMessage = activation.parsedOutput.mediaPath
2079
- ? ` ClawTip pay returned QR media: ${activation.parsedOutput.mediaPath}`
2080
- : " ClawTip pay did not return QR media.";
2081
- throw new Error(
2082
- [
2083
- `ClawTip wallet config is still missing after payment activation: ${refreshedWalletConfig.expectedPath}.`,
2084
- `Order file: ${activation.orderFile}.`,
2085
- paymentQrMessage.trim(),
2086
- ].join(" ")
2087
- );
2088
- }
2089
- const completedByWalletConfig = !payCredential && !walletReadyBeforePay && refreshedWalletConfig.exists;
2090
- if (!payCredential && !completedByWalletConfig) {
2091
- const paymentQrMessage = activation.parsedOutput.mediaPath
2092
- ? ` ClawTip pay returned QR media: ${activation.parsedOutput.mediaPath}`
2093
- : "";
2094
- throw new Error(
2095
- `ClawTip pay did not write payCredential to the order file. Order file: ${activation.orderFile}.${paymentQrMessage}`
2096
- );
2097
- }
2098
- const activationCompletedBy = payCredential
2099
- ? (refreshedWalletConfig.exists ? "payCredential+wallet-config" : "payCredential")
2100
- : "wallet-config";
2101
-
2102
- store.savePayment({
2103
- method: "clawtip",
2104
- enabled: true,
2105
- isDefault: true,
2106
- config: {
2107
- bootstrapUrl,
2108
- orderNo: bootstrap.payment?.orderNo,
2109
- amountFen: bootstrap.payment?.amountFen ?? bootstrap.activationFeeFen,
2110
- indicator: bootstrap.payment?.indicator,
2111
- slug: bootstrap.payment?.slug,
2112
- skillId: bootstrap.payment?.skillId,
2113
- description: bootstrap.payment?.description,
2114
- resourceUrl: bootstrap.payment?.resourceUrl,
2115
- proofRequired: false,
2116
- activationOrderFile: activation.orderFile,
2117
- walletConfigPath: refreshedWalletConfig.expectedPath,
2118
- walletConfigPresent: refreshedWalletConfig.exists,
2119
- payCredentialWritten: Boolean(payCredential),
2120
- activationCompletedBy
2121
- }
2122
- });
2123
- logger.info("payment.channel.added", "clawtip payment channel added during init", {
2124
- method: "clawtip",
2125
- orderNo: bootstrap.payment?.orderNo,
2126
- payCredentialWritten: Boolean(payCredential),
2127
- activationCompletedBy
2128
- });
2129
- if (refreshedWalletConfig.exists) {
2130
- if (!payCredential) {
2131
- p.note(
2132
- [
2133
- "OpenClaw saved the local ClawTip wallet config, but the ClawTip order file did not contain payCredential.",
2134
- `Order file: ${activation.orderFile}`,
2135
- "TokenBuddy saved the wallet binding metadata and will rely on the local wallet for future ClawTip purchases."
2136
- ].join("\n"),
2137
- "ClawTip"
2138
- );
2139
- }
2140
- setupSummaryLines.push("ClawTip wallet activated and set as the default payment method.");
2141
- } else {
2142
- p.note(
2143
- [
2144
- "ClawTip payment metadata was saved, but the local OpenClaw wallet config is still missing.",
2145
- `Expected: ${refreshedWalletConfig.expectedPath}`,
2146
- refreshedWalletConfig.alternatePaths.length > 0
2147
- ? `Nearby files: ${refreshedWalletConfig.alternatePaths.join(", ")}`
2148
- : "Nearby files: -",
2149
- "Bind or restore the local wallet before using ClawTip-backed purchases."
2150
- ].join("\n"),
2151
- "ClawTip Wallet Required"
2152
- );
2153
- setupSummaryLines.push("ClawTip payment metadata saved; local wallet config still needs binding before use.");
2154
- }
2155
- }
2156
- } catch (err: any) {
2157
- spinner.stop(`Failed to finish ClawTip setup: ${err.message}`);
2158
- setupSummaryLines.push("ClawTip activation requires follow-up because the bootstrap step did not complete.");
2159
- } finally {
2160
- store.close();
2161
- }
2162
- }
2163
-
2164
- // Step 3: Install Launchd Daemon Service
2165
- if (process.platform === "darwin") {
2166
- const installDaemon = await p.confirm({
2167
- message: "Would you like to install tb-proxyd as a launchd service to automatically run in the background on startup?",
2168
- initialValue: true
2169
- });
2170
-
2171
- if (installDaemon) {
2172
- spinner.start("Registering launchd daemon plist agent...");
2173
- try {
2174
- const home = os.homedir();
2175
- const plistDir = path.join(home, "Library", "LaunchAgents");
2176
- if (!fs.existsSync(plistDir)) fs.mkdirSync(plistDir, { recursive: true });
2177
-
2178
- const plistPath = path.join(plistDir, `${TOKENBUDDY_LAUNCHD_LABEL}.plist`);
2179
- const controlPort = configuredControlPort();
2180
- const proxyPort = configuredProxyPort();
2181
- const sellerRegistryUrl = sellerRegistryUrlForInit();
2182
-
2183
- const nodePath = process.execPath;
2184
- const scriptPath = tbProxydScriptPath();
2185
- const stdoutPath = path.join(home, ".tokenbuddy-store", "tb-proxyd.stdout.log");
2186
- const stderrPath = path.join(home, ".tokenbuddy-store", "tb-proxyd.stderr.log");
2187
- fs.mkdirSync(path.dirname(stdoutPath), { recursive: true });
2188
- fs.mkdirSync(path.dirname(stderrPath), { recursive: true });
2189
- const plistContent = buildLaunchdPlistContent({
2190
- label: TOKENBUDDY_LAUNCHD_LABEL,
2191
- nodePath,
2192
- scriptPath,
2193
- stdoutPath,
2194
- stderrPath,
2195
- controlPort,
2196
- proxyPort,
2197
- sellerRegistryUrl,
2198
- pathEnv: process.env.PATH,
2199
- clawtipProofCommand: defaultClawtipProofCommand(),
2200
- clawtipProofTimeoutMs: process.env.TB_PROXYD_CLAWTIP_PROOF_TIMEOUT_MS
2201
- ? Number(process.env.TB_PROXYD_CLAWTIP_PROOF_TIMEOUT_MS)
2202
- : undefined,
2203
- });
2204
- fs.writeFileSync(plistPath, plistContent, "utf8");
2205
-
2206
- installLaunchAgent(plistPath, TOKENBUDDY_LAUNCHD_LABEL);
2207
- spinner.stop("LaunchAgent daemon successfully registered and started! 🚀");
2208
- setupSummaryLines.push("Background tb-proxyd launchd service installed.");
2209
-
2210
- spinner.start("Checking tb-proxyd control plane...");
2211
- const daemonProbe = await waitForDaemonStatus(controlPort, 8000);
2212
- spinner.stop(daemonProbe.running ? "tb-proxyd control plane is ready." : "tb-proxyd control plane is still starting.");
2213
-
2214
- if (daemonProbe.running) {
2215
- const nextStep = await p.select({
2216
- message: "tb-proxyd is running. What would you like to do next?",
2217
- options: [
2218
- { value: "continue", label: "Continue in terminal", hint: "Finish the CLI setup summary" },
2219
- { value: "ui", label: "Open tb ui", hint: "Launch the local graphical console" }
2220
- ]
2221
- }) as string;
2222
- if (nextStep === "ui") {
2223
- const uiUrl = launchControlUi(controlPort);
2224
- setupSummaryLines.push(`Opened TokenBuddy UI at ${uiUrl}.`);
2225
- p.outro(buildInitSuccessMessage(setupSummaryLines));
2226
- return;
2227
- }
2228
- }
2229
- } catch (err: any) {
2230
- spinner.stop(`Failed to write launchd plist: ${err.message}`);
2231
- }
2232
- }
2233
- } else {
2234
- // Run background dettached child process in linux/windows
2235
- p.note("System daemon is active. Process runs in dettached background.");
2236
- setupSummaryLines.push("Background daemon mode is available on this system.");
2237
- }
2238
-
2239
- p.outro(buildInitSuccessMessage(setupSummaryLines));
2240
- });
2241
-
2242
- return program;
2243
- }