@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.
Files changed (143) hide show
  1. package/dist/src/buyer-store.d.ts +6 -1
  2. package/dist/src/buyer-store.js +43 -4
  3. package/dist/src/cli.js +2 -2
  4. package/dist/src/daemon.d.ts +12 -0
  5. package/dist/src/daemon.js +791 -61
  6. package/dist/src/doctor-diagnostics.js +1 -6
  7. package/dist/src/provider-install.d.ts +2 -2
  8. package/dist/src/provider-install.js +248 -2
  9. package/dist/src/seller-catalog.d.ts +21 -0
  10. package/dist/src/seller-catalog.js +17 -0
  11. package/dist/src/seller-route-planner.d.ts +4 -1
  12. package/dist/src/seller-route-planner.js +3 -0
  13. package/dist/src/seller-routing-strategy.d.ts +3 -0
  14. package/dist/src/terminal-detect.d.ts +1 -1
  15. package/dist/src/terminal-detect.js +3 -2
  16. package/package.json +15 -2
  17. package/static/ui/assets/index-Djfl9tw5.js +271 -0
  18. package/static/ui/assets/index-DkfztCkn.css +1 -0
  19. package/static/ui/index.html +2 -2
  20. package/dist/src/buyer-store.d.ts.map +0 -1
  21. package/dist/src/buyer-store.js.map +0 -1
  22. package/dist/src/clawtip-bootstrap.d.ts.map +0 -1
  23. package/dist/src/clawtip-bootstrap.js.map +0 -1
  24. package/dist/src/cli.d.ts.map +0 -1
  25. package/dist/src/cli.js.map +0 -1
  26. package/dist/src/credit-tracker.d.ts.map +0 -1
  27. package/dist/src/credit-tracker.js.map +0 -1
  28. package/dist/src/daemon.d.ts.map +0 -1
  29. package/dist/src/daemon.js.map +0 -1
  30. package/dist/src/doctor-clawtip-wallet.d.ts.map +0 -1
  31. package/dist/src/doctor-clawtip-wallet.js.map +0 -1
  32. package/dist/src/doctor-diagnostics.d.ts.map +0 -1
  33. package/dist/src/doctor-diagnostics.js.map +0 -1
  34. package/dist/src/index.d.ts.map +0 -1
  35. package/dist/src/index.js.map +0 -1
  36. package/dist/src/init-clawtip-activation.d.ts.map +0 -1
  37. package/dist/src/init-clawtip-activation.js.map +0 -1
  38. package/dist/src/init-payment-options.d.ts.map +0 -1
  39. package/dist/src/init-payment-options.js.map +0 -1
  40. package/dist/src/init-setup.d.ts.map +0 -1
  41. package/dist/src/init-setup.js.map +0 -1
  42. package/dist/src/model-index.d.ts.map +0 -1
  43. package/dist/src/model-index.js.map +0 -1
  44. package/dist/src/package-update.d.ts.map +0 -1
  45. package/dist/src/package-update.js.map +0 -1
  46. package/dist/src/prewarm-cache.d.ts.map +0 -1
  47. package/dist/src/prewarm-cache.js.map +0 -1
  48. package/dist/src/prewarm-scheduler.d.ts.map +0 -1
  49. package/dist/src/prewarm-scheduler.js.map +0 -1
  50. package/dist/src/provider-install.d.ts.map +0 -1
  51. package/dist/src/provider-install.js.map +0 -1
  52. package/dist/src/provider-routing-config.d.ts.map +0 -1
  53. package/dist/src/provider-routing-config.js.map +0 -1
  54. package/dist/src/registry-trust.d.ts.map +0 -1
  55. package/dist/src/registry-trust.js.map +0 -1
  56. package/dist/src/route-failover.d.ts.map +0 -1
  57. package/dist/src/route-failover.js.map +0 -1
  58. package/dist/src/seller-catalog.d.ts.map +0 -1
  59. package/dist/src/seller-catalog.js.map +0 -1
  60. package/dist/src/seller-concurrency-limiter.d.ts.map +0 -1
  61. package/dist/src/seller-concurrency-limiter.js.map +0 -1
  62. package/dist/src/seller-metadata-cache.d.ts.map +0 -1
  63. package/dist/src/seller-metadata-cache.js.map +0 -1
  64. package/dist/src/seller-pool.d.ts.map +0 -1
  65. package/dist/src/seller-pool.js.map +0 -1
  66. package/dist/src/seller-route-planner.d.ts.map +0 -1
  67. package/dist/src/seller-route-planner.js.map +0 -1
  68. package/dist/src/seller-routing-config.d.ts.map +0 -1
  69. package/dist/src/seller-routing-config.js.map +0 -1
  70. package/dist/src/seller-routing-strategy.d.ts.map +0 -1
  71. package/dist/src/seller-routing-strategy.js.map +0 -1
  72. package/dist/src/stream-failover.d.ts.map +0 -1
  73. package/dist/src/stream-failover.js.map +0 -1
  74. package/dist/src/tb-clawtip-proof.d.ts.map +0 -1
  75. package/dist/src/tb-clawtip-proof.js.map +0 -1
  76. package/dist/src/tb-proxyd.d.ts.map +0 -1
  77. package/dist/src/tb-proxyd.js.map +0 -1
  78. package/dist/src/terminal-detect.d.ts.map +0 -1
  79. package/dist/src/terminal-detect.js.map +0 -1
  80. package/dist/src/terminal-image.d.ts.map +0 -1
  81. package/dist/src/terminal-image.js.map +0 -1
  82. package/src/buyer-store.ts +0 -1090
  83. package/src/clawtip-bootstrap.ts +0 -65
  84. package/src/cli.ts +0 -2243
  85. package/src/credit-tracker.ts +0 -295
  86. package/src/daemon.ts +0 -5475
  87. package/src/doctor-clawtip-wallet.ts +0 -95
  88. package/src/doctor-diagnostics.ts +0 -1026
  89. package/src/index.ts +0 -16
  90. package/src/init-clawtip-activation.ts +0 -695
  91. package/src/init-payment-options.ts +0 -373
  92. package/src/init-setup.ts +0 -165
  93. package/src/model-index.ts +0 -278
  94. package/src/package-update.ts +0 -311
  95. package/src/prewarm-cache.ts +0 -485
  96. package/src/prewarm-scheduler.ts +0 -675
  97. package/src/provider-install.ts +0 -1006
  98. package/src/provider-routing-config.ts +0 -410
  99. package/src/registry-trust.ts +0 -51
  100. package/src/route-failover.ts +0 -304
  101. package/src/seller-catalog.ts +0 -505
  102. package/src/seller-concurrency-limiter.ts +0 -161
  103. package/src/seller-metadata-cache.ts +0 -91
  104. package/src/seller-pool.ts +0 -557
  105. package/src/seller-route-planner.ts +0 -513
  106. package/src/seller-routing-config.ts +0 -211
  107. package/src/seller-routing-strategy.ts +0 -362
  108. package/src/stream-failover.ts +0 -152
  109. package/src/tb-clawtip-proof.ts +0 -28
  110. package/src/tb-proxyd.ts +0 -101
  111. package/src/terminal-detect.ts +0 -333
  112. package/src/terminal-image.ts +0 -228
  113. package/static/ui/assets/index-0MVXD7bH.css +0 -1
  114. package/static/ui/assets/index-BVbeDEwq.js +0 -271
  115. package/static/ui/assets/index-BVbeDEwq.js.map +0 -1
  116. package/tests/cli-routing.test.ts +0 -363
  117. package/tests/control-plane-ui-endpoints.test.ts +0 -1630
  118. package/tests/credit-tracker.test.ts +0 -165
  119. package/tests/daemon-413-fallback.test.ts +0 -92
  120. package/tests/daemon-classify.test.ts +0 -452
  121. package/tests/daemon-roles.test.ts +0 -92
  122. package/tests/daemon-trusted-registry-cache.test.ts +0 -132
  123. package/tests/e2e.test.ts +0 -366
  124. package/tests/image-generation-e2e.test.ts +0 -230
  125. package/tests/model-index.test.ts +0 -198
  126. package/tests/package-update.test.ts +0 -147
  127. package/tests/prewarm-cache.test.ts +0 -296
  128. package/tests/prewarm-scheduler.test.ts +0 -367
  129. package/tests/provider-routing-config.test.ts +0 -150
  130. package/tests/registry-trust.test.ts +0 -28
  131. package/tests/route-failover.test.ts +0 -222
  132. package/tests/seller-catalog-413.test.ts +0 -120
  133. package/tests/seller-catalog-utilities.test.ts +0 -124
  134. package/tests/seller-concurrency-limiter.test.ts +0 -83
  135. package/tests/seller-metadata-cache.test.ts +0 -89
  136. package/tests/seller-pool.test.ts +0 -365
  137. package/tests/seller-route-planner.test.ts +0 -312
  138. package/tests/seller-routing-config.test.ts +0 -124
  139. package/tests/seller-routing-strategy.test.ts +0 -167
  140. package/tests/stream-failover.test.ts +0 -52
  141. package/tests/thousand-seller.test.ts +0 -151
  142. package/tests/tokenbuddy.test.ts +0 -4043
  143. package/tsconfig.json +0 -8
@@ -1,152 +0,0 @@
1
- import { createModuleLogger } from "@tokenbuddy/logging";
2
-
3
- const logger = createModuleLogger("tb-proxyd:stream-failover");
4
-
5
- /**
6
- * v1.2 §6 / §18.10: stream-failover policy. The buyer honors the
7
- * "abort + client retry" contract: once the first SSE byte has been
8
- * written to the client, an upstream stream failure is surfaced as an
9
- * abrupt close plus a `X-TokenBuddy-Retry-Hint: 1` trailer. The client
10
- * (OpenAI / Anthropic SDK or any consumer honoring the OpenAI retry
11
- * contract) re-issues the request and the buyer serves it from a
12
- * healthy seller.
13
- *
14
- * The decisions in this module are intentionally one-way: the buyer
15
- * never tries to splice two streams together (option B in the design
16
- * doc) because that would double-charge and would require non-trivial
17
- * idempotency re-design. v1.2 = abort + retry; v2 may revisit.
18
- */
19
- /**
20
- * v1.2 §6 / §18.10:stream-failover 策略。
21
- * buyer 端遵循"abort + client retry"契约:一旦首字节 SSE 写入客户端,上游流失败只能 abrupt close
22
- * 并附带 `X-TokenBuddy-Retry-Hint: 1` trailer;由 OpenAI / Anthropic SDK 重新发起请求,
23
- * buyer 切到健康的 seller 重发。
24
- *
25
- * 该模块的决策是单向的:buyer 不会把两条流拼接(design doc 里的 option B),
26
- * 因为会双重计费且需要重做幂等。v1.2 = abort + retry;v2 再考虑。
27
- */
28
- export interface StreamFailoverOptions {
29
- /** abort 响应中携带的 retry hint 响应头名,默认 `X-TokenBuddy-Retry-Hint` */
30
- retryHintHeader?: string;
31
- /** 时间源,默认 `Date.now`;测试可注入 */
32
- now?: () => number;
33
- }
34
-
35
- /**
36
- * `StreamFailover.decideOnStreamAbort` 的返回结果。
37
- */
38
- export interface StreamFailoverDecision {
39
- /** 决策动作:abort_with_retry_hint(已写首字节,abrupt close) / let_stream_complete(路由层可换 seller) */
40
- action: "abort_with_retry_hint" | "let_stream_complete";
41
- /** 决策原因(用于日志和稳定事件断言) */
42
- reason: string;
43
- /** retry hint 头的取值(`"0"` 或 `"1"`) */
44
- retryHintValue: string;
45
- /** 决策时是否已写过首字节(决策的输入状态) */
46
- firstChunkCommitted: boolean;
47
- /** 决策时已写入客户端的字节数 */
48
- bytesFlushed: number;
49
- }
50
-
51
- /**
52
- * buyer 端 SSE 流失败决策器。
53
- * 跟踪"首字节是否已写客户端"和"累计写入字节数",在 upstream 中断时决定是 abort+retry 还是切 seller 重试。
54
- */
55
- export class StreamFailover {
56
- private readonly retryHintHeader: string;
57
- private readonly now: () => number;
58
- private firstChunkCommitted = false;
59
- private bytesFlushed = 0;
60
-
61
- constructor(options: StreamFailoverOptions = {}) {
62
- this.retryHintHeader = options.retryHintHeader ?? "X-TokenBuddy-Retry-Hint";
63
- this.now = options.now ?? Date.now;
64
- }
65
-
66
- /**
67
- * Record that the buyer's response stream has written its first chunk
68
- * to the client. From this point on, the route-failover controller
69
- * cannot switch sellers without the client's knowledge; failures
70
- * must abort the stream and rely on the client to retry.
71
- */
72
- markFirstChunkCommitted(): void {
73
- if (this.firstChunkCommitted) {
74
- return;
75
- }
76
- this.firstChunkCommitted = true;
77
- }
78
-
79
- /**
80
- * Track total bytes written to the client. Used by `tb doctor` and
81
- * the inference ledger to attribute partial-stream usage.
82
- */
83
- recordBytesWritten(bytes: number): void {
84
- this.bytesFlushed += bytes;
85
- }
86
-
87
- /**
88
- * Decide what to do when the upstream stream breaks. If the first
89
- * chunk has already been written, the only option is to abort and
90
- * surface the retry hint. Otherwise the controller is free to fail
91
- * over to the next seller.
92
- */
93
- decideOnStreamAbort(reason: string): StreamFailoverDecision {
94
- if (!this.firstChunkCommitted) {
95
- return {
96
- action: "let_stream_complete",
97
- reason: "no_chunks_yet_committed",
98
- retryHintValue: "0",
99
- firstChunkCommitted: false,
100
- bytesFlushed: 0
101
- };
102
- }
103
- logger.warn("stream.failover.aborted", "upstream stream broke after first chunk; aborting client with retry hint", {
104
- reason,
105
- bytesFlushed: this.bytesFlushed
106
- });
107
- return {
108
- action: "abort_with_retry_hint",
109
- reason,
110
- retryHintValue: "1",
111
- firstChunkCommitted: true,
112
- bytesFlushed: this.bytesFlushed
113
- };
114
- }
115
-
116
- /**
117
- * Read-only snapshot of the current stream state. The route-failover
118
- * controller calls this to decide whether the next chunk is the first
119
- * one (failover still possible) or a follow-up (abort required).
120
- */
121
- snapshot(): { firstChunkCommitted: boolean; bytesFlushed: number } {
122
- return {
123
- firstChunkCommitted: this.firstChunkCommitted,
124
- bytesFlushed: this.bytesFlushed
125
- };
126
- }
127
-
128
- /**
129
- * Reset the failover state when a brand-new request starts. The
130
- * `forwardProxyRequest` controller calls this before each new
131
- * inference request.
132
- */
133
- reset(): void {
134
- this.firstChunkCommitted = false;
135
- this.bytesFlushed = 0;
136
- }
137
-
138
- /**
139
- * The HTTP header to set on the abort response so the client knows
140
- * it should retry. Exposed so the controller and the test fixtures
141
- * can refer to the same constant.
142
- */
143
- get headerName(): string {
144
- return this.retryHintHeader;
145
- }
146
- }
147
-
148
- /**
149
- * Constant for the "retry hint value" used on stream-abort responses.
150
- * Exposed so callers can refer to the same value in tests.
151
- */
152
- export const STREAM_FAILOVER_RETRY_HINT = "1";
@@ -1,28 +0,0 @@
1
- #!/usr/bin/env node
2
- import { createClawtipPaymentProof } from "./init-clawtip-activation.js";
3
-
4
- async function readStdin(): Promise<string> {
5
- const chunks: Buffer[] = [];
6
- for await (const chunk of process.stdin) {
7
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
8
- }
9
- return Buffer.concat(chunks).toString("utf8");
10
- }
11
-
12
- async function main(): Promise<void> {
13
- const input = (await readStdin()).trim();
14
- if (!input) {
15
- throw new Error("ClawTip proof provider requires a JSON payload on stdin");
16
- }
17
- const payload = JSON.parse(input) as unknown;
18
- const proof = await createClawtipPaymentProof(
19
- payload && typeof payload === "object" ? payload as { paymentInstructions?: unknown; quote?: unknown } : {},
20
- );
21
- process.stdout.write(`${proof}\n`);
22
- }
23
-
24
- main().catch((error: unknown) => {
25
- const message = error instanceof Error ? error.message : String(error);
26
- process.stderr.write(`${message}\n`);
27
- process.exitCode = 1;
28
- });
package/src/tb-proxyd.ts DELETED
@@ -1,101 +0,0 @@
1
- import { TokenbuddyDaemon } from "./daemon.js";
2
- import { createModuleLogger } from "@tokenbuddy/logging";
3
- import { resolveBuyerStorePath } from "./buyer-store.js";
4
- import { normalizeSellerRoutingConfig, parseSellerRoutingEnv } from "./seller-routing-config.js";
5
- import { DEFAULT_SELLER_REGISTRY_URL } from "./registry-trust.js";
6
-
7
- const logger = createModuleLogger("tb-proxyd");
8
-
9
- const dbPath = resolveBuyerStorePath();
10
- const controlPort = parsePortEnv("TB_PROXYD_CONTROL_PORT", 17820);
11
- const proxyPort = parsePortEnv("TB_PROXYD_PROXY_PORT", 17821);
12
- const sellerRegistryUrl = process.env.TB_PROXYD_SELLER_REGISTRY_URL || DEFAULT_SELLER_REGISTRY_URL;
13
- const sellerRoutingEnv = parseSellerRoutingEnv();
14
- const sellerRouting = sellerRoutingEnv ? normalizeSellerRoutingConfig(sellerRoutingEnv) : undefined;
15
- const sellerConcurrency = parseSellerConcurrencyEnv();
16
-
17
- const daemon = new TokenbuddyDaemon({
18
- controlPort,
19
- proxyPort,
20
- dbPath,
21
- sellerRegistryUrl,
22
- ...(sellerRouting ? { sellerRouting } : {}),
23
- ...(sellerConcurrency ? { sellerConcurrency } : {})
24
- });
25
-
26
- logger.info("proxy.process.initializing", "tb-proxyd process initializing", {
27
- dbPath,
28
- controlPort,
29
- proxyPort,
30
- sellerRegistryUrl,
31
- sellerRoutingEnvOverride: Boolean(sellerRouting),
32
- sellerRoutingMode: sellerRouting?.mode,
33
- sellerRoutingScorer: sellerRouting?.scorer,
34
- sellerConcurrencyEnabled: sellerConcurrency?.enabled ?? false,
35
- sellerMaxInFlight: sellerConcurrency?.maxInFlightPerSeller
36
- });
37
- daemon.start();
38
-
39
- // Handle graceful stop
40
- process.on("SIGTERM", () => {
41
- logger.info("proxy.shutdown", "tb-proxyd received shutdown signal", { signal: "SIGTERM" });
42
- daemon.stop();
43
- process.exit(0);
44
- });
45
- process.on("SIGINT", () => {
46
- logger.info("proxy.shutdown", "tb-proxyd received shutdown signal", { signal: "SIGINT" });
47
- daemon.stop();
48
- process.exit(0);
49
- });
50
-
51
- function parsePortEnv(name: string, fallback: number): number {
52
- const rawValue = process.env[name];
53
- if (!rawValue) {
54
- return fallback;
55
- }
56
- const port = Number(rawValue);
57
- if (!Number.isInteger(port) || port < 0 || port > 65535) {
58
- throw new Error(`${name} must be an integer port between 0 and 65535`);
59
- }
60
- return port;
61
- }
62
-
63
- function parseSellerConcurrencyEnv(): { enabled: boolean; maxInFlightPerSeller?: number; leaseTtlMs?: number } | undefined {
64
- const enabled = parseBooleanEnv("TB_PROXYD_SELLER_CONCURRENCY_ENABLED");
65
- const maxInFlightPerSeller = parsePositiveIntegerEnv("TB_PROXYD_SELLER_MAX_IN_FLIGHT");
66
- const leaseTtlMs = parsePositiveIntegerEnv("TB_PROXYD_SELLER_LEASE_TTL_MS");
67
- if (enabled === undefined && maxInFlightPerSeller === undefined && leaseTtlMs === undefined) {
68
- return undefined;
69
- }
70
- return {
71
- enabled: enabled ?? false,
72
- ...(maxInFlightPerSeller !== undefined ? { maxInFlightPerSeller } : {}),
73
- ...(leaseTtlMs !== undefined ? { leaseTtlMs } : {})
74
- };
75
- }
76
-
77
- function parseBooleanEnv(name: string): boolean | undefined {
78
- const rawValue = process.env[name]?.trim().toLowerCase();
79
- if (!rawValue) {
80
- return undefined;
81
- }
82
- if (["1", "true", "yes", "on"].includes(rawValue)) {
83
- return true;
84
- }
85
- if (["0", "false", "no", "off"].includes(rawValue)) {
86
- return false;
87
- }
88
- throw new Error(`${name} must be one of 1, true, yes, on, 0, false, no, off`);
89
- }
90
-
91
- function parsePositiveIntegerEnv(name: string): number | undefined {
92
- const rawValue = process.env[name];
93
- if (!rawValue) {
94
- return undefined;
95
- }
96
- const parsed = Number(rawValue);
97
- if (!Number.isInteger(parsed) || parsed <= 0) {
98
- throw new Error(`${name} must be a positive integer`);
99
- }
100
- return parsed;
101
- }
@@ -1,333 +0,0 @@
1
- import * as fs from "fs";
2
- import * as path from "path";
3
- import * as os from "os";
4
-
5
- /**
6
- * `detectTerminals()` 的输出单元:描述一个可被 rewrite 的 coding terminal。
7
- * `detected=false` 时仍会返回,UI 用其渲染"未安装"标签而不是直接跳过。
8
- */
9
- export interface TerminalCandidate {
10
- /** terminal 内部 ID,例:`claude-code` / `claude-desktop` / `openclaw` / `hermes` */
11
- id: string;
12
- /** 人类可读名称,例:`Claude Code CLI` */
13
- name: string;
14
- /** 本机是否检测到该 terminal 的配置文件 */
15
- detected: boolean;
16
- /** 配置文件绝对路径(即使未检测到也会拼出预期路径) */
17
- configPath: string;
18
- /** 检测结果的说明,用于 UI 展示 */
19
- reason: string;
20
- }
21
-
22
- const PLACEHOLDER_API_KEY = "TOKENBUDDY_PROXY";
23
- const DESKTOP_PROFILE_ID = "00000000-0000-4000-8000-000000178210";
24
-
25
- /**
26
- * 获取当前用户的 home 目录,作为拼装 terminal 配置路径的基准。
27
- *
28
- * @returns `os.homedir()` 返回的 home 路径
29
- */
30
- export function getHomeDir(): string {
31
- return os.homedir();
32
- }
33
-
34
- /**
35
- * Detect which coding terminals are installed on the local system.
36
- *
37
- * 通过 `~/.claude/settings.json`、`Library/Application Support/Claude/...`、
38
- * `~/.openclaw/openclaw.json`、`~/.hermes/config.yaml` 等约定路径判断。
39
- * Claude Desktop 在非 macOS 平台上 `configPath` 可能为空字符串,
40
- * 调用方需以 `detected` 字段为准。
41
- *
42
- * @returns terminal 候选列表(固定四个 ID 顺序)
43
- */
44
- export function detectTerminals(): TerminalCandidate[] {
45
- const home = getHomeDir();
46
- const candidates: TerminalCandidate[] = [];
47
-
48
- // 1. Claude Code
49
- const claudePath = path.join(home, ".claude", "settings.json");
50
- const claudeDetected = fs.existsSync(claudePath);
51
- candidates.push({
52
- id: "claude-code",
53
- name: "Claude Code CLI",
54
- detected: claudeDetected,
55
- configPath: claudePath,
56
- reason: claudeDetected ? "Found `~/.claude/settings.json`" : "Missing `~/.claude/settings.json`"
57
- });
58
-
59
- // 2. Claude Desktop
60
- let desktopPath = "";
61
- if (process.platform === "darwin") {
62
- desktopPath = path.join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
63
- } else if (process.platform === "win32") {
64
- const localAppData = process.env.LOCALAPPDATA || path.join(home, "AppData", "Local");
65
- desktopPath = path.join(localAppData, "Claude", "claude_desktop_config.json");
66
- }
67
- const desktopDetected = desktopPath !== "" && fs.existsSync(desktopPath);
68
- candidates.push({
69
- id: "claude-desktop",
70
- name: "Claude Desktop App",
71
- detected: desktopDetected,
72
- configPath: desktopPath,
73
- reason: desktopDetected ? "Found Claude desktop config" : "Missing Claude desktop config"
74
- });
75
-
76
- // 3. Openclaw
77
- const openclawPath = path.join(home, ".openclaw", "openclaw.json");
78
- const openclawDetected = fs.existsSync(openclawPath);
79
- candidates.push({
80
- id: "openclaw",
81
- name: "Openclaw Agent",
82
- detected: openclawDetected,
83
- configPath: openclawPath,
84
- reason: openclawDetected ? "Found `~/.openclaw/openclaw.json`" : "Missing `~/.openclaw/openclaw.json`"
85
- });
86
-
87
- // 4. Hermes
88
- const hermesPath = path.join(home, ".hermes", "config.yaml");
89
- const hermesDetected = fs.existsSync(hermesPath);
90
- candidates.push({
91
- id: "hermes",
92
- name: "Hermes Terminal",
93
- detected: hermesDetected,
94
- configPath: hermesPath,
95
- reason: hermesDetected ? "Found `~/.hermes/config.yaml`" : "Missing `~/.hermes/config.yaml`"
96
- });
97
-
98
- return candidates;
99
- }
100
-
101
- /**
102
- * Safely rewrite Claude Code settings to route requests through our proxy.
103
- *
104
- * 写入 `ANTHROPIC_BASE_URL` / `ANTHROPIC_AUTH_TOKEN`(占位 `TOKENBUDDY_PROXY`)/
105
- * `ANTHROPIC_MODEL` / `ANTHROPIC_DEFAULT_SONNET_MODEL`。失败时仅打印日志,
106
- * 不会抛出异常;调用方无需 try/catch。
107
- *
108
- * @param configPath `~/.claude/settings.json` 的绝对路径
109
- * @param proxyUrl TokenBuddy proxy 的 base URL
110
- * @param model Claude Code 默认模型 ID
111
- */
112
- export function rewriteClaudeCode(configPath: string, proxyUrl: string, model: string): void {
113
- try {
114
- const parent = path.dirname(configPath);
115
- if (!fs.existsSync(parent)) {
116
- fs.mkdirSync(parent, { recursive: true });
117
- }
118
-
119
- let config: any = {};
120
- if (fs.existsSync(configPath)) {
121
- try {
122
- config = JSON.parse(fs.readFileSync(configPath, "utf8"));
123
- } catch {}
124
- }
125
-
126
- if (!config.env || typeof config.env !== "object") {
127
- config.env = {};
128
- }
129
-
130
- config.env.ANTHROPIC_BASE_URL = proxyUrl;
131
- config.env.ANTHROPIC_AUTH_TOKEN = PLACEHOLDER_API_KEY;
132
- config.env.ANTHROPIC_MODEL = model;
133
- config.env.ANTHROPIC_DEFAULT_SONNET_MODEL = model;
134
-
135
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf8");
136
- console.log(`[terminal-detect] Claude Code successfully routed to ${proxyUrl}`);
137
- } catch (err: any) {
138
- console.error("[terminal-detect] Failed to rewrite Claude Code config:", err.message);
139
- }
140
- }
141
-
142
- /**
143
- * Safely rewrite Claude Desktop configuration.
144
- *
145
- * 写入 `deploymentMode = "3p"` 到 Claude 与 Claude-3p 两个目录,
146
- * 并在 `configLibrary/<DESKTOP_PROFILE_ID>.json` 写入 TokenBuddy profile。
147
- * 最后更新 `_meta.json` 把 `appliedId` 指向该 profile。
148
- *
149
- * @param configPath Claude Desktop 主配置(`claude_desktop_config.json`)绝对路径
150
- * @param proxyUrl TokenBuddy proxy 的 base URL
151
- * @param model Claude Desktop 默认模型 ID
152
- */
153
- export function rewriteClaudeDesktop(configPath: string, proxyUrl: string, model: string): void {
154
- try {
155
- const dir = path.dirname(configPath);
156
- const threepDir = dir.replace(/Claude$/, "Claude-3p");
157
-
158
- const writeMode = (filePath: string) => {
159
- const parent = path.dirname(filePath);
160
- if (!fs.existsSync(parent)) {
161
- fs.mkdirSync(parent, { recursive: true });
162
- }
163
- let config: any = {};
164
- if (fs.existsSync(filePath)) {
165
- try { config = JSON.parse(fs.readFileSync(filePath, "utf8")); } catch {}
166
- }
167
- config.deploymentMode = "3p";
168
- fs.writeFileSync(filePath, JSON.stringify(config, null, 2), "utf8");
169
- };
170
-
171
- // 1. Write deployment modes
172
- writeMode(configPath);
173
- const threepConfigPath = configPath.replace(/Claude/, "Claude-3p");
174
- writeMode(threepConfigPath);
175
-
176
- // 2. Write Profile
177
- const configLibraryPath = path.join(threepDir, "configLibrary");
178
- if (!fs.existsSync(configLibraryPath)) {
179
- fs.mkdirSync(configLibraryPath, { recursive: true });
180
- }
181
-
182
- const profilePath = path.join(configLibraryPath, `${DESKTOP_PROFILE_ID}.json`);
183
- const profile = {
184
- disableDeploymentModeChooser: true,
185
- inferenceGatewayApiKey: PLACEHOLDER_API_KEY,
186
- inferenceGatewayAuthScheme: "bearer",
187
- inferenceGatewayBaseUrl: proxyUrl,
188
- inferenceProvider: "gateway",
189
- inferenceModels: [{ name: model }]
190
- };
191
- fs.writeFileSync(profilePath, JSON.stringify(profile, null, 2), "utf8");
192
-
193
- // 3. Write _meta.json
194
- const metaPath = path.join(configLibraryPath, "_meta.json");
195
- let meta: any = {};
196
- if (fs.existsSync(metaPath)) {
197
- try { meta = JSON.parse(fs.readFileSync(metaPath, "utf8")); } catch {}
198
- }
199
- let entries = Array.isArray(meta.entries) ? meta.entries : [];
200
- entries = entries.filter((e: any) => e.id !== DESKTOP_PROFILE_ID);
201
- entries.push({ id: DESKTOP_PROFILE_ID, name: "TokenBuddy" });
202
-
203
- meta.appliedId = DESKTOP_PROFILE_ID;
204
- meta.entries = entries;
205
- fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2), "utf8");
206
-
207
- console.log(`[terminal-detect] Claude Desktop successfully routed to ${proxyUrl}`);
208
- } catch (err: any) {
209
- console.error("[terminal-detect] Failed to rewrite Claude Desktop config:", err.message);
210
- }
211
- }
212
-
213
- /**
214
- * Rewrite Openclaw settings.
215
- *
216
- * 写入 `models.providers.tokenbuddy`,并把 `agents.defaults.model` 指向 TokenBuddy。
217
- * 失败仅打印日志,不抛出。
218
- *
219
- * @param configPath `~/.openclaw/openclaw.json` 的绝对路径
220
- * @param proxyUrl TokenBuddy proxy 的 base URL
221
- * @param model Openclaw 默认模型 ID
222
- */
223
- export function rewriteOpenclaw(configPath: string, proxyUrl: string, model: string): void {
224
- try {
225
- const parent = path.dirname(configPath);
226
- if (!fs.existsSync(parent)) fs.mkdirSync(parent, { recursive: true });
227
-
228
- let config: any = {};
229
- if (fs.existsSync(configPath)) {
230
- try { config = JSON.parse(fs.readFileSync(configPath, "utf8")); } catch {}
231
- }
232
-
233
- const providers = config.models && typeof config.models === "object" && !Array.isArray(config.models)
234
- ? config.models.providers && typeof config.models.providers === "object" && !Array.isArray(config.models.providers)
235
- ? config.models.providers
236
- : {}
237
- : {};
238
- const tokenbuddy = providers.tokenbuddy && typeof providers.tokenbuddy === "object" && !Array.isArray(providers.tokenbuddy)
239
- ? providers.tokenbuddy
240
- : {};
241
- const existingModels = Array.isArray(tokenbuddy.models) ? tokenbuddy.models : [];
242
- providers.tokenbuddy = {
243
- ...tokenbuddy,
244
- baseUrl: openAiBaseUrl(proxyUrl),
245
- apiKey: PLACEHOLDER_API_KEY,
246
- auth: "api-key",
247
- api: "openai-completions",
248
- models: [
249
- ...existingModels.filter((entry: any) => !(entry && typeof entry === "object" && entry.id === model)),
250
- {
251
- id: model,
252
- name: model,
253
- api: "openai-completions",
254
- input: ["text", "image"],
255
- },
256
- ],
257
- };
258
- config.models = {
259
- ...(config.models && typeof config.models === "object" && !Array.isArray(config.models) ? config.models : {}),
260
- providers,
261
- };
262
- const agents = config.agents && typeof config.agents === "object" && !Array.isArray(config.agents) ? config.agents : {};
263
- const defaults = agents.defaults && typeof agents.defaults === "object" && !Array.isArray(agents.defaults) ? agents.defaults : {};
264
- defaults.model = `tokenbuddy/${model}`;
265
- agents.defaults = defaults;
266
- config.agents = agents;
267
-
268
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf8");
269
- console.log(`[terminal-detect] Openclaw successfully routed to ${proxyUrl}`);
270
- } catch (err: any) {
271
- console.error("[terminal-detect] Failed to rewrite Openclaw config:", err.message);
272
- }
273
- }
274
-
275
- /**
276
- * Rewrite Hermes settings.
277
- *
278
- * 在 `model` 块下写入 TokenBuddy 的默认模型、custom provider、base_url、api_key 和 api_mode。
279
- * 失败仅打印日志,不抛出。
280
- *
281
- * @param configPath `~/.hermes/config.yaml` 的绝对路径
282
- * @param proxyUrl TokenBuddy proxy 的 base URL
283
- * @param model Hermes 默认模型 ID
284
- */
285
- export function rewriteHermes(configPath: string, proxyUrl: string, model: string): void {
286
- try {
287
- const parent = path.dirname(configPath);
288
- if (!fs.existsSync(parent)) fs.mkdirSync(parent, { recursive: true });
289
-
290
- const existing = fs.existsSync(configPath) ? fs.readFileSync(configPath, "utf8") : "";
291
- const modelSection = [
292
- "model:",
293
- ` default: ${model}`,
294
- " provider: custom",
295
- ` base_url: ${JSON.stringify(openAiBaseUrl(proxyUrl))}`,
296
- ` api_key: ${PLACEHOLDER_API_KEY}`,
297
- " api_mode: chat_completions",
298
- ].join("\n");
299
-
300
- fs.writeFileSync(configPath, replaceTopLevelYamlSection(existing, "model", modelSection), "utf8");
301
- console.log(`[terminal-detect] Hermes successfully routed to ${proxyUrl}`);
302
- } catch (err: any) {
303
- console.error("[terminal-detect] Failed to rewrite Hermes config:", err.message);
304
- }
305
- }
306
-
307
- function openAiBaseUrl(proxyUrl: string): string {
308
- const normalized = proxyUrl.replace(/\/+$/, "");
309
- return normalized.endsWith("/v1") ? normalized : `${normalized}/v1`;
310
- }
311
-
312
- function replaceTopLevelYamlSection(existing: string, sectionName: string, sectionBody: string): string {
313
- const lines = existing.split(/\r?\n/);
314
- const sectionStart = lines.findIndex((line) => line === `${sectionName}:` || line.startsWith(`${sectionName}: `));
315
- const bodyLines = sectionBody.trimEnd().split(/\r?\n/);
316
- if (sectionStart < 0) {
317
- const prefix = existing.trimEnd();
318
- return `${prefix}${prefix ? "\n" : ""}${bodyLines.join("\n")}\n`;
319
- }
320
- let sectionEnd = sectionStart + 1;
321
- while (sectionEnd < lines.length) {
322
- const line = lines[sectionEnd];
323
- if (line.trim() && !line.startsWith(" ") && !line.startsWith("\t")) {
324
- break;
325
- }
326
- sectionEnd += 1;
327
- }
328
- return `${[
329
- ...lines.slice(0, sectionStart),
330
- ...bodyLines,
331
- ...lines.slice(sectionEnd),
332
- ].join("\n").replace(/\n*$/, "")}\n`;
333
- }