@tokenbuddy/tokenbuddy 1.0.35 → 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.
- package/dist/src/buyer-store.d.ts +6 -1
- package/dist/src/buyer-store.js +43 -4
- package/dist/src/cli.js +2 -2
- package/dist/src/daemon.d.ts +12 -0
- package/dist/src/daemon.js +791 -61
- package/dist/src/doctor-diagnostics.js +1 -6
- package/dist/src/provider-install.d.ts +2 -2
- package/dist/src/provider-install.js +248 -2
- package/dist/src/seller-catalog.d.ts +21 -0
- package/dist/src/seller-catalog.js +17 -0
- package/dist/src/seller-route-planner.d.ts +4 -1
- package/dist/src/seller-route-planner.js +3 -0
- package/dist/src/seller-routing-strategy.d.ts +3 -0
- package/dist/src/terminal-detect.d.ts +1 -1
- package/dist/src/terminal-detect.js +3 -2
- package/package.json +15 -2
- package/static/ui/assets/index-Djfl9tw5.js +271 -0
- package/static/ui/assets/index-DkfztCkn.css +1 -0
- package/static/ui/index.html +2 -2
- package/dist/src/buyer-store.d.ts.map +0 -1
- package/dist/src/buyer-store.js.map +0 -1
- package/dist/src/clawtip-bootstrap.d.ts.map +0 -1
- package/dist/src/clawtip-bootstrap.js.map +0 -1
- package/dist/src/cli.d.ts.map +0 -1
- package/dist/src/cli.js.map +0 -1
- package/dist/src/credit-tracker.d.ts.map +0 -1
- package/dist/src/credit-tracker.js.map +0 -1
- package/dist/src/daemon.d.ts.map +0 -1
- package/dist/src/daemon.js.map +0 -1
- package/dist/src/doctor-clawtip-wallet.d.ts.map +0 -1
- package/dist/src/doctor-clawtip-wallet.js.map +0 -1
- package/dist/src/doctor-diagnostics.d.ts.map +0 -1
- package/dist/src/doctor-diagnostics.js.map +0 -1
- package/dist/src/index.d.ts.map +0 -1
- package/dist/src/index.js.map +0 -1
- package/dist/src/init-clawtip-activation.d.ts.map +0 -1
- package/dist/src/init-clawtip-activation.js.map +0 -1
- package/dist/src/init-payment-options.d.ts.map +0 -1
- package/dist/src/init-payment-options.js.map +0 -1
- package/dist/src/init-setup.d.ts.map +0 -1
- package/dist/src/init-setup.js.map +0 -1
- package/dist/src/model-index.d.ts.map +0 -1
- package/dist/src/model-index.js.map +0 -1
- package/dist/src/package-update.d.ts.map +0 -1
- package/dist/src/package-update.js.map +0 -1
- package/dist/src/prewarm-cache.d.ts.map +0 -1
- package/dist/src/prewarm-cache.js.map +0 -1
- package/dist/src/prewarm-scheduler.d.ts.map +0 -1
- package/dist/src/prewarm-scheduler.js.map +0 -1
- package/dist/src/provider-install.d.ts.map +0 -1
- package/dist/src/provider-install.js.map +0 -1
- package/dist/src/provider-routing-config.d.ts.map +0 -1
- package/dist/src/provider-routing-config.js.map +0 -1
- package/dist/src/registry-trust.d.ts.map +0 -1
- package/dist/src/registry-trust.js.map +0 -1
- package/dist/src/route-failover.d.ts.map +0 -1
- package/dist/src/route-failover.js.map +0 -1
- package/dist/src/seller-catalog.d.ts.map +0 -1
- package/dist/src/seller-catalog.js.map +0 -1
- package/dist/src/seller-concurrency-limiter.d.ts.map +0 -1
- package/dist/src/seller-concurrency-limiter.js.map +0 -1
- package/dist/src/seller-metadata-cache.d.ts.map +0 -1
- package/dist/src/seller-metadata-cache.js.map +0 -1
- package/dist/src/seller-pool.d.ts.map +0 -1
- package/dist/src/seller-pool.js.map +0 -1
- package/dist/src/seller-route-planner.d.ts.map +0 -1
- package/dist/src/seller-route-planner.js.map +0 -1
- package/dist/src/seller-routing-config.d.ts.map +0 -1
- package/dist/src/seller-routing-config.js.map +0 -1
- package/dist/src/seller-routing-strategy.d.ts.map +0 -1
- package/dist/src/seller-routing-strategy.js.map +0 -1
- package/dist/src/stream-failover.d.ts.map +0 -1
- package/dist/src/stream-failover.js.map +0 -1
- package/dist/src/tb-clawtip-proof.d.ts.map +0 -1
- package/dist/src/tb-clawtip-proof.js.map +0 -1
- package/dist/src/tb-proxyd.d.ts.map +0 -1
- package/dist/src/tb-proxyd.js.map +0 -1
- package/dist/src/terminal-detect.d.ts.map +0 -1
- package/dist/src/terminal-detect.js.map +0 -1
- package/dist/src/terminal-image.d.ts.map +0 -1
- package/dist/src/terminal-image.js.map +0 -1
- package/src/buyer-store.ts +0 -1090
- package/src/clawtip-bootstrap.ts +0 -65
- package/src/cli.ts +0 -2243
- package/src/credit-tracker.ts +0 -295
- package/src/daemon.ts +0 -5475
- package/src/doctor-clawtip-wallet.ts +0 -95
- package/src/doctor-diagnostics.ts +0 -1026
- package/src/index.ts +0 -16
- package/src/init-clawtip-activation.ts +0 -695
- package/src/init-payment-options.ts +0 -373
- package/src/init-setup.ts +0 -165
- package/src/model-index.ts +0 -278
- package/src/package-update.ts +0 -311
- package/src/prewarm-cache.ts +0 -485
- package/src/prewarm-scheduler.ts +0 -675
- package/src/provider-install.ts +0 -1006
- package/src/provider-routing-config.ts +0 -410
- package/src/registry-trust.ts +0 -51
- package/src/route-failover.ts +0 -304
- package/src/seller-catalog.ts +0 -505
- package/src/seller-concurrency-limiter.ts +0 -161
- package/src/seller-metadata-cache.ts +0 -91
- package/src/seller-pool.ts +0 -557
- package/src/seller-route-planner.ts +0 -513
- package/src/seller-routing-config.ts +0 -211
- package/src/seller-routing-strategy.ts +0 -362
- package/src/stream-failover.ts +0 -152
- package/src/tb-clawtip-proof.ts +0 -28
- package/src/tb-proxyd.ts +0 -101
- package/src/terminal-detect.ts +0 -333
- package/src/terminal-image.ts +0 -228
- package/static/ui/assets/index-0MVXD7bH.css +0 -1
- package/static/ui/assets/index-BVbeDEwq.js +0 -271
- package/static/ui/assets/index-BVbeDEwq.js.map +0 -1
- package/tests/cli-routing.test.ts +0 -363
- package/tests/control-plane-ui-endpoints.test.ts +0 -1630
- package/tests/credit-tracker.test.ts +0 -165
- package/tests/daemon-413-fallback.test.ts +0 -92
- package/tests/daemon-classify.test.ts +0 -452
- package/tests/daemon-roles.test.ts +0 -92
- package/tests/daemon-trusted-registry-cache.test.ts +0 -132
- package/tests/e2e.test.ts +0 -366
- package/tests/image-generation-e2e.test.ts +0 -230
- package/tests/model-index.test.ts +0 -198
- package/tests/package-update.test.ts +0 -147
- package/tests/prewarm-cache.test.ts +0 -296
- package/tests/prewarm-scheduler.test.ts +0 -367
- package/tests/provider-routing-config.test.ts +0 -150
- package/tests/registry-trust.test.ts +0 -28
- package/tests/route-failover.test.ts +0 -222
- package/tests/seller-catalog-413.test.ts +0 -120
- package/tests/seller-catalog-utilities.test.ts +0 -124
- package/tests/seller-concurrency-limiter.test.ts +0 -83
- package/tests/seller-metadata-cache.test.ts +0 -89
- package/tests/seller-pool.test.ts +0 -365
- package/tests/seller-route-planner.test.ts +0 -312
- package/tests/seller-routing-config.test.ts +0 -124
- package/tests/seller-routing-strategy.test.ts +0 -167
- package/tests/stream-failover.test.ts +0 -52
- package/tests/thousand-seller.test.ts +0 -151
- package/tests/tokenbuddy.test.ts +0 -4043
- 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, "&")
|
|
280
|
-
.replace(/</g, "<")
|
|
281
|
-
.replace(/>/g, ">");
|
|
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
|
-
}
|