@tokenbuddy/tokenbuddy 1.0.4
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/bin/tb-proxyd.js +2 -0
- package/bin/tb.js +3 -0
- package/bin/tokenbuddy-proxyd.js +2 -0
- package/bin/tokenbuddy.js +3 -0
- package/dist/src/buyer-store.d.ts +118 -0
- package/dist/src/buyer-store.d.ts.map +1 -0
- package/dist/src/buyer-store.js +296 -0
- package/dist/src/buyer-store.js.map +1 -0
- package/dist/src/cli.d.ts +3 -0
- package/dist/src/cli.d.ts.map +1 -0
- package/dist/src/cli.js +648 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/daemon.d.ts +48 -0
- package/dist/src/daemon.d.ts.map +1 -0
- package/dist/src/daemon.js +998 -0
- package/dist/src/daemon.js.map +1 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +12 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/provider-install.d.ts +44 -0
- package/dist/src/provider-install.d.ts.map +1 -0
- package/dist/src/provider-install.js +286 -0
- package/dist/src/provider-install.js.map +1 -0
- package/dist/src/tb-proxyd.d.ts +2 -0
- package/dist/src/tb-proxyd.d.ts.map +1 -0
- package/dist/src/tb-proxyd.js +54 -0
- package/dist/src/tb-proxyd.js.map +1 -0
- package/dist/src/terminal-detect.d.ts +29 -0
- package/dist/src/terminal-detect.d.ts.map +1 -0
- package/dist/src/terminal-detect.js +209 -0
- package/dist/src/terminal-detect.js.map +1 -0
- package/package.json +29 -0
- package/src/buyer-store.ts +536 -0
- package/src/cli.ts +732 -0
- package/src/daemon.ts +1158 -0
- package/src/index.ts +12 -0
- package/src/provider-install.ts +363 -0
- package/src/tb-proxyd.ts +60 -0
- package/src/terminal-detect.ts +225 -0
- package/tests/e2e.test.ts +264 -0
- package/tests/tokenbuddy.test.ts +1186 -0
- package/tsconfig.json +8 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { buildCli } from "./cli.js";
|
|
2
|
+
|
|
3
|
+
export function run() {
|
|
4
|
+
const program = buildCli();
|
|
5
|
+
program.parseAsync(process.argv).catch((error: unknown) => {
|
|
6
|
+
const commandError = error as { code?: string; exitCode?: number; message?: string };
|
|
7
|
+
if (commandError.code !== "tokenbuddy.daemon_not_running") {
|
|
8
|
+
console.error(commandError.message || String(error));
|
|
9
|
+
}
|
|
10
|
+
process.exitCode = commandError.exitCode || 1;
|
|
11
|
+
});
|
|
12
|
+
}
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as os from "os";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { BuyerStore, ProviderInstallSnapshot } from "./buyer-store.js";
|
|
5
|
+
|
|
6
|
+
const PLACEHOLDER_API_KEY = "TOKENBUDDY_PROXY";
|
|
7
|
+
const DESKTOP_PROFILE_ID = "00000000-0000-4000-8000-000000178210";
|
|
8
|
+
|
|
9
|
+
export const SUPPORTED_PROVIDER_IDS = [
|
|
10
|
+
"codex",
|
|
11
|
+
"claude-code",
|
|
12
|
+
"claude-desktop",
|
|
13
|
+
"openclaw",
|
|
14
|
+
"hermes"
|
|
15
|
+
] as const;
|
|
16
|
+
|
|
17
|
+
export type ProviderId = typeof SUPPORTED_PROVIDER_IDS[number];
|
|
18
|
+
|
|
19
|
+
export interface ProviderDetectOptions {
|
|
20
|
+
home?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ProviderInstallOptions extends ProviderDetectOptions {
|
|
24
|
+
providers: string[];
|
|
25
|
+
proxyUrl: string;
|
|
26
|
+
model: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ProviderRollbackOptions extends ProviderDetectOptions {
|
|
30
|
+
providers: string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ProviderCandidate {
|
|
34
|
+
id: ProviderId;
|
|
35
|
+
name: string;
|
|
36
|
+
detected: boolean;
|
|
37
|
+
configPath: string;
|
|
38
|
+
reason: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ProviderFileChange {
|
|
42
|
+
providerId: ProviderId;
|
|
43
|
+
path: string;
|
|
44
|
+
action: "create" | "update";
|
|
45
|
+
existed: boolean;
|
|
46
|
+
summary: string;
|
|
47
|
+
content: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface ProviderApplyResult {
|
|
51
|
+
providerId: ProviderId;
|
|
52
|
+
path: string;
|
|
53
|
+
action: "created" | "updated";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface ProviderRollbackResult {
|
|
57
|
+
providerId: ProviderId;
|
|
58
|
+
path: string;
|
|
59
|
+
action: "restored" | "removed" | "missing_snapshot";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface ProviderDefinition {
|
|
63
|
+
id: ProviderId;
|
|
64
|
+
name: string;
|
|
65
|
+
configPath(home: string): string;
|
|
66
|
+
changes(home: string, proxyUrl: string, model: string): ProviderFileChange[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function resolveHome(home?: string): string {
|
|
70
|
+
return home && home.trim() ? home : os.homedir();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isProviderId(value: string): value is ProviderId {
|
|
74
|
+
return (SUPPORTED_PROVIDER_IDS as readonly string[]).includes(value);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function assertProviderIds(providers: string[]): ProviderId[] {
|
|
78
|
+
if (!Array.isArray(providers) || providers.length === 0) {
|
|
79
|
+
throw new Error("providers must include at least one provider id");
|
|
80
|
+
}
|
|
81
|
+
const out: ProviderId[] = [];
|
|
82
|
+
for (const provider of providers) {
|
|
83
|
+
if (!isProviderId(provider)) {
|
|
84
|
+
throw new Error(`unsupported provider: ${provider}`);
|
|
85
|
+
}
|
|
86
|
+
if (!out.includes(provider)) {
|
|
87
|
+
out.push(provider);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function readText(filePath: string): string | undefined {
|
|
94
|
+
if (!fs.existsSync(filePath)) {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
return fs.readFileSync(filePath, "utf8");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function readJsonObject(filePath: string): Record<string, unknown> {
|
|
101
|
+
const text = readText(filePath);
|
|
102
|
+
if (!text) {
|
|
103
|
+
return {};
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
const parsed = JSON.parse(text) as unknown;
|
|
107
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
108
|
+
? parsed as Record<string, unknown>
|
|
109
|
+
: {};
|
|
110
|
+
} catch {
|
|
111
|
+
return {};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function jsonContent(value: unknown): string {
|
|
116
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function escapeTomlString(value: string): string {
|
|
120
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function replaceTomlSection(existing: string, sectionName: string, sectionBody: string): string {
|
|
124
|
+
const sectionPattern = new RegExp(`^\\[${sectionName}\\]\\n[\\s\\S]*?(?=^\\[|\\s*$)`, "m");
|
|
125
|
+
const normalized = existing.trimEnd();
|
|
126
|
+
const nextSection = `[${sectionName}]\n${sectionBody.trimEnd()}\n`;
|
|
127
|
+
if (sectionPattern.test(normalized)) {
|
|
128
|
+
return `${normalized.replace(sectionPattern, nextSection).trimEnd()}\n`;
|
|
129
|
+
}
|
|
130
|
+
return `${normalized}${normalized ? "\n\n" : ""}${nextSection}`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function codexConfig(home: string, proxyUrl: string, model: string): ProviderFileChange[] {
|
|
134
|
+
const configPath = path.join(home, ".codex", "config.toml");
|
|
135
|
+
const existing = readText(configPath) || "";
|
|
136
|
+
const content = replaceTomlSection(existing, "tokenbuddy", [
|
|
137
|
+
`proxy_url = "${escapeTomlString(proxyUrl)}"`,
|
|
138
|
+
`api_key = "${PLACEHOLDER_API_KEY}"`,
|
|
139
|
+
`model = "${escapeTomlString(model)}"`
|
|
140
|
+
].join("\n"));
|
|
141
|
+
return [makeChange("codex", configPath, "configure TokenBuddy proxy for Codex", content)];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function claudeCodeConfig(home: string, proxyUrl: string, model: string): ProviderFileChange[] {
|
|
145
|
+
const configPath = path.join(home, ".claude", "settings.json");
|
|
146
|
+
const config = readJsonObject(configPath);
|
|
147
|
+
const env = config.env && typeof config.env === "object" && !Array.isArray(config.env)
|
|
148
|
+
? config.env as Record<string, unknown>
|
|
149
|
+
: {};
|
|
150
|
+
config.env = {
|
|
151
|
+
...env,
|
|
152
|
+
ANTHROPIC_BASE_URL: proxyUrl,
|
|
153
|
+
ANTHROPIC_AUTH_TOKEN: PLACEHOLDER_API_KEY,
|
|
154
|
+
ANTHROPIC_MODEL: model,
|
|
155
|
+
ANTHROPIC_DEFAULT_SONNET_MODEL: model
|
|
156
|
+
};
|
|
157
|
+
return [makeChange("claude-code", configPath, "configure Anthropic proxy env for Claude Code", jsonContent(config))];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function claudeDesktopConfig(home: string, proxyUrl: string, model: string): ProviderFileChange[] {
|
|
161
|
+
const configDir = path.join(home, "Library", "Application Support", "Claude");
|
|
162
|
+
const configPath = path.join(configDir, "claude_desktop_config.json");
|
|
163
|
+
const threepDir = path.join(home, "Library", "Application Support", "Claude-3p");
|
|
164
|
+
const threepConfigPath = path.join(threepDir, "claude_desktop_config.json");
|
|
165
|
+
const libraryPath = path.join(threepDir, "configLibrary");
|
|
166
|
+
const profilePath = path.join(libraryPath, `${DESKTOP_PROFILE_ID}.json`);
|
|
167
|
+
const metaPath = path.join(libraryPath, "_meta.json");
|
|
168
|
+
|
|
169
|
+
const primary = readJsonObject(configPath);
|
|
170
|
+
primary.deploymentMode = "3p";
|
|
171
|
+
const threep = readJsonObject(threepConfigPath);
|
|
172
|
+
threep.deploymentMode = "3p";
|
|
173
|
+
|
|
174
|
+
const profile = {
|
|
175
|
+
disableDeploymentModeChooser: true,
|
|
176
|
+
inferenceGatewayApiKey: PLACEHOLDER_API_KEY,
|
|
177
|
+
inferenceGatewayAuthScheme: "bearer",
|
|
178
|
+
inferenceGatewayBaseUrl: proxyUrl,
|
|
179
|
+
inferenceProvider: "gateway",
|
|
180
|
+
inferenceModels: [{ name: model }]
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const meta = readJsonObject(metaPath);
|
|
184
|
+
const existingEntries = Array.isArray(meta.entries) ? meta.entries : [];
|
|
185
|
+
meta.appliedId = DESKTOP_PROFILE_ID;
|
|
186
|
+
meta.entries = [
|
|
187
|
+
...existingEntries.filter((entry) => {
|
|
188
|
+
return !(entry && typeof entry === "object" && "id" in entry && entry.id === DESKTOP_PROFILE_ID);
|
|
189
|
+
}),
|
|
190
|
+
{ id: DESKTOP_PROFILE_ID, name: "TokenBuddy" }
|
|
191
|
+
];
|
|
192
|
+
|
|
193
|
+
return [
|
|
194
|
+
makeChange("claude-desktop", configPath, "enable Claude Desktop 3p deployment mode", jsonContent(primary)),
|
|
195
|
+
makeChange("claude-desktop", threepConfigPath, "enable Claude Desktop 3p config", jsonContent(threep)),
|
|
196
|
+
makeChange("claude-desktop", profilePath, "write TokenBuddy Claude Desktop profile", jsonContent(profile)),
|
|
197
|
+
makeChange("claude-desktop", metaPath, "select TokenBuddy Claude Desktop profile", jsonContent(meta))
|
|
198
|
+
];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function openclawConfig(home: string, proxyUrl: string, model: string): ProviderFileChange[] {
|
|
202
|
+
const configPath = path.join(home, ".openclaw", "config.json");
|
|
203
|
+
const config = readJsonObject(configPath);
|
|
204
|
+
config.api_url = proxyUrl;
|
|
205
|
+
config.api_key = PLACEHOLDER_API_KEY;
|
|
206
|
+
config.model = model;
|
|
207
|
+
return [makeChange("openclaw", configPath, "configure OpenClaw proxy settings", jsonContent(config))];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function hermesConfig(home: string, proxyUrl: string, model: string): ProviderFileChange[] {
|
|
211
|
+
const configPath = path.join(home, ".hermes", "settings.json");
|
|
212
|
+
const config = readJsonObject(configPath);
|
|
213
|
+
const openai = config.openai && typeof config.openai === "object" && !Array.isArray(config.openai)
|
|
214
|
+
? config.openai as Record<string, unknown>
|
|
215
|
+
: {};
|
|
216
|
+
config.openai = {
|
|
217
|
+
...openai,
|
|
218
|
+
base_url: proxyUrl,
|
|
219
|
+
api_key: PLACEHOLDER_API_KEY,
|
|
220
|
+
model
|
|
221
|
+
};
|
|
222
|
+
return [makeChange("hermes", configPath, "configure Hermes OpenAI proxy settings", jsonContent(config))];
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function makeChange(providerId: ProviderId, filePath: string, summary: string, content: string): ProviderFileChange {
|
|
226
|
+
const existed = fs.existsSync(filePath);
|
|
227
|
+
return {
|
|
228
|
+
providerId,
|
|
229
|
+
path: filePath,
|
|
230
|
+
action: existed ? "update" : "create",
|
|
231
|
+
existed,
|
|
232
|
+
summary,
|
|
233
|
+
content
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const PROVIDERS: ProviderDefinition[] = [
|
|
238
|
+
{
|
|
239
|
+
id: "codex",
|
|
240
|
+
name: "Codex CLI",
|
|
241
|
+
configPath: (home) => path.join(home, ".codex", "config.toml"),
|
|
242
|
+
changes: codexConfig
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
id: "claude-code",
|
|
246
|
+
name: "Claude Code CLI",
|
|
247
|
+
configPath: (home) => path.join(home, ".claude", "settings.json"),
|
|
248
|
+
changes: claudeCodeConfig
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
id: "claude-desktop",
|
|
252
|
+
name: "Claude Desktop App",
|
|
253
|
+
configPath: (home) => path.join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json"),
|
|
254
|
+
changes: claudeDesktopConfig
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
id: "openclaw",
|
|
258
|
+
name: "OpenClaw Agent",
|
|
259
|
+
configPath: (home) => path.join(home, ".openclaw", "config.json"),
|
|
260
|
+
changes: openclawConfig
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
id: "hermes",
|
|
264
|
+
name: "Hermes Terminal",
|
|
265
|
+
configPath: (home) => path.join(home, ".hermes", "settings.json"),
|
|
266
|
+
changes: hermesConfig
|
|
267
|
+
}
|
|
268
|
+
];
|
|
269
|
+
|
|
270
|
+
export function detectProviders(options: ProviderDetectOptions = {}): ProviderCandidate[] {
|
|
271
|
+
const home = resolveHome(options.home);
|
|
272
|
+
return PROVIDERS.map((provider) => {
|
|
273
|
+
const configPath = provider.configPath(home);
|
|
274
|
+
const detected = fs.existsSync(configPath);
|
|
275
|
+
return {
|
|
276
|
+
id: provider.id,
|
|
277
|
+
name: provider.name,
|
|
278
|
+
detected,
|
|
279
|
+
configPath,
|
|
280
|
+
reason: detected ? `Found ${configPath}` : `Missing ${configPath}`
|
|
281
|
+
};
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function previewProviderInstall(options: ProviderInstallOptions): ProviderFileChange[] {
|
|
286
|
+
const home = resolveHome(options.home);
|
|
287
|
+
const providerIds = assertProviderIds(options.providers);
|
|
288
|
+
if (!options.proxyUrl || !options.proxyUrl.trim()) {
|
|
289
|
+
throw new Error("proxyUrl is required");
|
|
290
|
+
}
|
|
291
|
+
if (!options.model || !options.model.trim()) {
|
|
292
|
+
throw new Error("model is required");
|
|
293
|
+
}
|
|
294
|
+
return providerIds.flatMap((providerId) => {
|
|
295
|
+
const provider = PROVIDERS.find((entry) => entry.id === providerId);
|
|
296
|
+
if (!provider) {
|
|
297
|
+
throw new Error(`unsupported provider: ${providerId}`);
|
|
298
|
+
}
|
|
299
|
+
return provider.changes(home, options.proxyUrl, options.model);
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export function applyProviderInstall(options: ProviderInstallOptions, store: BuyerStore): ProviderApplyResult[] {
|
|
304
|
+
const changes = previewProviderInstall(options);
|
|
305
|
+
const byProvider = new Map<ProviderId, ProviderFileChange[]>();
|
|
306
|
+
for (const change of changes) {
|
|
307
|
+
byProvider.set(change.providerId, [...(byProvider.get(change.providerId) || []), change]);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
for (const [providerId, providerChanges] of byProvider) {
|
|
311
|
+
const snapshot: ProviderInstallSnapshot = {
|
|
312
|
+
providerId,
|
|
313
|
+
files: providerChanges.map((change) => ({
|
|
314
|
+
path: change.path,
|
|
315
|
+
existed: fs.existsSync(change.path),
|
|
316
|
+
content: readText(change.path)
|
|
317
|
+
}))
|
|
318
|
+
};
|
|
319
|
+
store.saveProviderInstallSnapshot(snapshot);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const applied: ProviderApplyResult[] = [];
|
|
323
|
+
for (const change of changes) {
|
|
324
|
+
const dir = path.dirname(change.path);
|
|
325
|
+
if (!fs.existsSync(dir)) {
|
|
326
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
327
|
+
}
|
|
328
|
+
fs.writeFileSync(change.path, change.content, "utf8");
|
|
329
|
+
applied.push({
|
|
330
|
+
providerId: change.providerId,
|
|
331
|
+
path: change.path,
|
|
332
|
+
action: change.existed ? "updated" : "created"
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
return applied;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export function rollbackProviderInstall(options: ProviderRollbackOptions, store: BuyerStore): ProviderRollbackResult[] {
|
|
339
|
+
const providerIds = assertProviderIds(options.providers);
|
|
340
|
+
const results: ProviderRollbackResult[] = [];
|
|
341
|
+
for (const providerId of providerIds) {
|
|
342
|
+
const snapshot = store.getProviderInstallSnapshot(providerId);
|
|
343
|
+
if (!snapshot) {
|
|
344
|
+
results.push({ providerId, path: "", action: "missing_snapshot" });
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
for (const file of snapshot.files) {
|
|
348
|
+
if (file.existed) {
|
|
349
|
+
const dir = path.dirname(file.path);
|
|
350
|
+
if (!fs.existsSync(dir)) {
|
|
351
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
352
|
+
}
|
|
353
|
+
fs.writeFileSync(file.path, file.content || "", "utf8");
|
|
354
|
+
results.push({ providerId, path: file.path, action: "restored" });
|
|
355
|
+
} else {
|
|
356
|
+
fs.rmSync(file.path, { force: true });
|
|
357
|
+
results.push({ providerId, path: file.path, action: "removed" });
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
store.removeProviderInstallSnapshot(providerId);
|
|
361
|
+
}
|
|
362
|
+
return results;
|
|
363
|
+
}
|
package/src/tb-proxyd.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { TokenbuddyDaemon } from "./daemon.js";
|
|
2
|
+
import { createModuleLogger } from "@tokenbuddy/logging";
|
|
3
|
+
import { resolveBuyerStorePath } from "./buyer-store.js";
|
|
4
|
+
|
|
5
|
+
const logger = createModuleLogger("tb-proxyd");
|
|
6
|
+
|
|
7
|
+
const dbPath = resolveBuyerStorePath();
|
|
8
|
+
const controlPort = parsePortEnv("TB_PROXYD_CONTROL_PORT", 17820);
|
|
9
|
+
const proxyPort = parsePortEnv("TB_PROXYD_PROXY_PORT", 17821);
|
|
10
|
+
const sellerRegistryUrl = process.env.TB_PROXYD_SELLER_REGISTRY_URL || "https://tb-wallet-bootstrap.fly.dev/registry/sellers";
|
|
11
|
+
const selectionMode = parseSelectionModeEnv();
|
|
12
|
+
|
|
13
|
+
const daemon = new TokenbuddyDaemon({
|
|
14
|
+
controlPort,
|
|
15
|
+
proxyPort,
|
|
16
|
+
dbPath,
|
|
17
|
+
sellerRegistryUrl,
|
|
18
|
+
selectionMode
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
logger.info("proxy.process.initializing", "tb-proxyd process initializing", {
|
|
22
|
+
dbPath,
|
|
23
|
+
controlPort,
|
|
24
|
+
proxyPort,
|
|
25
|
+
sellerRegistryUrl,
|
|
26
|
+
selectionMode
|
|
27
|
+
});
|
|
28
|
+
daemon.start();
|
|
29
|
+
|
|
30
|
+
// Handle graceful stop
|
|
31
|
+
process.on("SIGTERM", () => {
|
|
32
|
+
logger.info("proxy.shutdown", "tb-proxyd received shutdown signal", { signal: "SIGTERM" });
|
|
33
|
+
daemon.stop();
|
|
34
|
+
process.exit(0);
|
|
35
|
+
});
|
|
36
|
+
process.on("SIGINT", () => {
|
|
37
|
+
logger.info("proxy.shutdown", "tb-proxyd received shutdown signal", { signal: "SIGINT" });
|
|
38
|
+
daemon.stop();
|
|
39
|
+
process.exit(0);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
function parsePortEnv(name: string, fallback: number): number {
|
|
43
|
+
const rawValue = process.env[name];
|
|
44
|
+
if (!rawValue) {
|
|
45
|
+
return fallback;
|
|
46
|
+
}
|
|
47
|
+
const port = Number(rawValue);
|
|
48
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
49
|
+
throw new Error(`${name} must be an integer port between 0 and 65535`);
|
|
50
|
+
}
|
|
51
|
+
return port;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parseSelectionModeEnv(): "auto" | "manual" {
|
|
55
|
+
const rawValue = process.env.TB_PROXYD_SELECTION_MODE || "auto";
|
|
56
|
+
if (rawValue === "auto" || rawValue === "manual") {
|
|
57
|
+
return rawValue;
|
|
58
|
+
}
|
|
59
|
+
throw new Error("TB_PROXYD_SELECTION_MODE must be auto or manual");
|
|
60
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
|
|
5
|
+
export interface TerminalCandidate {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
detected: boolean;
|
|
9
|
+
configPath: string;
|
|
10
|
+
reason: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const PLACEHOLDER_API_KEY = "TOKENBUDDY_PROXY";
|
|
14
|
+
const DESKTOP_PROFILE_ID = "00000000-0000-4000-8000-000000178210";
|
|
15
|
+
|
|
16
|
+
export function getHomeDir(): string {
|
|
17
|
+
return os.homedir();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Detect which coding terminals are installed on the local system.
|
|
22
|
+
*/
|
|
23
|
+
export function detectTerminals(): TerminalCandidate[] {
|
|
24
|
+
const home = getHomeDir();
|
|
25
|
+
const candidates: TerminalCandidate[] = [];
|
|
26
|
+
|
|
27
|
+
// 1. Claude Code
|
|
28
|
+
const claudePath = path.join(home, ".claude", "settings.json");
|
|
29
|
+
const claudeDetected = fs.existsSync(claudePath);
|
|
30
|
+
candidates.push({
|
|
31
|
+
id: "claude-code",
|
|
32
|
+
name: "Claude Code CLI",
|
|
33
|
+
detected: claudeDetected,
|
|
34
|
+
configPath: claudePath,
|
|
35
|
+
reason: claudeDetected ? "Found `~/.claude/settings.json`" : "Missing `~/.claude/settings.json`"
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// 2. Claude Desktop
|
|
39
|
+
let desktopPath = "";
|
|
40
|
+
if (process.platform === "darwin") {
|
|
41
|
+
desktopPath = path.join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
42
|
+
} else if (process.platform === "win32") {
|
|
43
|
+
const localAppData = process.env.LOCALAPPDATA || path.join(home, "AppData", "Local");
|
|
44
|
+
desktopPath = path.join(localAppData, "Claude", "claude_desktop_config.json");
|
|
45
|
+
}
|
|
46
|
+
const desktopDetected = desktopPath !== "" && fs.existsSync(desktopPath);
|
|
47
|
+
candidates.push({
|
|
48
|
+
id: "claude-desktop",
|
|
49
|
+
name: "Claude Desktop App",
|
|
50
|
+
detected: desktopDetected,
|
|
51
|
+
configPath: desktopPath,
|
|
52
|
+
reason: desktopDetected ? "Found Claude desktop config" : "Missing Claude desktop config"
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// 3. Openclaw
|
|
56
|
+
const openclawPath = path.join(home, ".openclaw", "config.json");
|
|
57
|
+
const openclawDetected = fs.existsSync(openclawPath);
|
|
58
|
+
candidates.push({
|
|
59
|
+
id: "openclaw",
|
|
60
|
+
name: "Openclaw Agent",
|
|
61
|
+
detected: openclawDetected,
|
|
62
|
+
configPath: openclawPath,
|
|
63
|
+
reason: openclawDetected ? "Found `~/.openclaw/config.json`" : "Missing `~/.openclaw/config.json`"
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// 4. Hermes
|
|
67
|
+
const hermesPath = path.join(home, ".hermes", "settings.json");
|
|
68
|
+
const hermesDetected = fs.existsSync(hermesPath);
|
|
69
|
+
candidates.push({
|
|
70
|
+
id: "hermes",
|
|
71
|
+
name: "Hermes Terminal",
|
|
72
|
+
detected: hermesDetected,
|
|
73
|
+
configPath: hermesPath,
|
|
74
|
+
reason: hermesDetected ? "Found `~/.hermes/settings.json`" : "Missing `~/.hermes/settings.json`"
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return candidates;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Safely rewrite Claude Code settings to route requests through our proxy.
|
|
82
|
+
*/
|
|
83
|
+
export function rewriteClaudeCode(configPath: string, proxyUrl: string, model: string): void {
|
|
84
|
+
try {
|
|
85
|
+
const parent = path.dirname(configPath);
|
|
86
|
+
if (!fs.existsSync(parent)) {
|
|
87
|
+
fs.mkdirSync(parent, { recursive: true });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let config: any = {};
|
|
91
|
+
if (fs.existsSync(configPath)) {
|
|
92
|
+
try {
|
|
93
|
+
config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
94
|
+
} catch {}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!config.env || typeof config.env !== "object") {
|
|
98
|
+
config.env = {};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
config.env.ANTHROPIC_BASE_URL = proxyUrl;
|
|
102
|
+
config.env.ANTHROPIC_AUTH_TOKEN = PLACEHOLDER_API_KEY;
|
|
103
|
+
config.env.ANTHROPIC_MODEL = model;
|
|
104
|
+
config.env.ANTHROPIC_DEFAULT_SONNET_MODEL = model;
|
|
105
|
+
|
|
106
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf8");
|
|
107
|
+
console.log(`[terminal-detect] Claude Code successfully routed to ${proxyUrl}`);
|
|
108
|
+
} catch (err: any) {
|
|
109
|
+
console.error("[terminal-detect] Failed to rewrite Claude Code config:", err.message);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Safely rewrite Claude Desktop configuration.
|
|
115
|
+
*/
|
|
116
|
+
export function rewriteClaudeDesktop(configPath: string, proxyUrl: string, model: string): void {
|
|
117
|
+
try {
|
|
118
|
+
const dir = path.dirname(configPath);
|
|
119
|
+
const threepDir = dir.replace(/Claude$/, "Claude-3p");
|
|
120
|
+
|
|
121
|
+
const writeMode = (filePath: string) => {
|
|
122
|
+
const parent = path.dirname(filePath);
|
|
123
|
+
if (!fs.existsSync(parent)) {
|
|
124
|
+
fs.mkdirSync(parent, { recursive: true });
|
|
125
|
+
}
|
|
126
|
+
let config: any = {};
|
|
127
|
+
if (fs.existsSync(filePath)) {
|
|
128
|
+
try { config = JSON.parse(fs.readFileSync(filePath, "utf8")); } catch {}
|
|
129
|
+
}
|
|
130
|
+
config.deploymentMode = "3p";
|
|
131
|
+
fs.writeFileSync(filePath, JSON.stringify(config, null, 2), "utf8");
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// 1. Write deployment modes
|
|
135
|
+
writeMode(configPath);
|
|
136
|
+
const threepConfigPath = configPath.replace(/Claude/, "Claude-3p");
|
|
137
|
+
writeMode(threepConfigPath);
|
|
138
|
+
|
|
139
|
+
// 2. Write Profile
|
|
140
|
+
const configLibraryPath = path.join(threepDir, "configLibrary");
|
|
141
|
+
if (!fs.existsSync(configLibraryPath)) {
|
|
142
|
+
fs.mkdirSync(configLibraryPath, { recursive: true });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const profilePath = path.join(configLibraryPath, `${DESKTOP_PROFILE_ID}.json`);
|
|
146
|
+
const profile = {
|
|
147
|
+
disableDeploymentModeChooser: true,
|
|
148
|
+
inferenceGatewayApiKey: PLACEHOLDER_API_KEY,
|
|
149
|
+
inferenceGatewayAuthScheme: "bearer",
|
|
150
|
+
inferenceGatewayBaseUrl: proxyUrl,
|
|
151
|
+
inferenceProvider: "gateway",
|
|
152
|
+
inferenceModels: [{ name: model }]
|
|
153
|
+
};
|
|
154
|
+
fs.writeFileSync(profilePath, JSON.stringify(profile, null, 2), "utf8");
|
|
155
|
+
|
|
156
|
+
// 3. Write _meta.json
|
|
157
|
+
const metaPath = path.join(configLibraryPath, "_meta.json");
|
|
158
|
+
let meta: any = {};
|
|
159
|
+
if (fs.existsSync(metaPath)) {
|
|
160
|
+
try { meta = JSON.parse(fs.readFileSync(metaPath, "utf8")); } catch {}
|
|
161
|
+
}
|
|
162
|
+
let entries = Array.isArray(meta.entries) ? meta.entries : [];
|
|
163
|
+
entries = entries.filter((e: any) => e.id !== DESKTOP_PROFILE_ID);
|
|
164
|
+
entries.push({ id: DESKTOP_PROFILE_ID, name: "TokenBuddy" });
|
|
165
|
+
|
|
166
|
+
meta.appliedId = DESKTOP_PROFILE_ID;
|
|
167
|
+
meta.entries = entries;
|
|
168
|
+
fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2), "utf8");
|
|
169
|
+
|
|
170
|
+
console.log(`[terminal-detect] Claude Desktop successfully routed to ${proxyUrl}`);
|
|
171
|
+
} catch (err: any) {
|
|
172
|
+
console.error("[terminal-detect] Failed to rewrite Claude Desktop config:", err.message);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Rewrite Openclaw settings.
|
|
178
|
+
*/
|
|
179
|
+
export function rewriteOpenclaw(configPath: string, proxyUrl: string, model: string): void {
|
|
180
|
+
try {
|
|
181
|
+
const parent = path.dirname(configPath);
|
|
182
|
+
if (!fs.existsSync(parent)) fs.mkdirSync(parent, { recursive: true });
|
|
183
|
+
|
|
184
|
+
let config: any = {};
|
|
185
|
+
if (fs.existsSync(configPath)) {
|
|
186
|
+
try { config = JSON.parse(fs.readFileSync(configPath, "utf8")); } catch {}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
config.api_url = proxyUrl;
|
|
190
|
+
config.api_key = PLACEHOLDER_API_KEY;
|
|
191
|
+
config.model = model;
|
|
192
|
+
|
|
193
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf8");
|
|
194
|
+
console.log(`[terminal-detect] Openclaw successfully routed to ${proxyUrl}`);
|
|
195
|
+
} catch (err: any) {
|
|
196
|
+
console.error("[terminal-detect] Failed to rewrite Openclaw config:", err.message);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Rewrite Hermes settings.
|
|
202
|
+
*/
|
|
203
|
+
export function rewriteHermes(configPath: string, proxyUrl: string, model: string): void {
|
|
204
|
+
try {
|
|
205
|
+
const parent = path.dirname(configPath);
|
|
206
|
+
if (!fs.existsSync(parent)) fs.mkdirSync(parent, { recursive: true });
|
|
207
|
+
|
|
208
|
+
let config: any = {};
|
|
209
|
+
if (fs.existsSync(configPath)) {
|
|
210
|
+
try { config = JSON.parse(fs.readFileSync(configPath, "utf8")); } catch {}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (!config.openai || typeof config.openai !== "object") {
|
|
214
|
+
config.openai = {};
|
|
215
|
+
}
|
|
216
|
+
config.openai.base_url = proxyUrl;
|
|
217
|
+
config.openai.api_key = PLACEHOLDER_API_KEY;
|
|
218
|
+
config.openai.model = model;
|
|
219
|
+
|
|
220
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf8");
|
|
221
|
+
console.log(`[terminal-detect] Hermes successfully routed to ${proxyUrl}`);
|
|
222
|
+
} catch (err: any) {
|
|
223
|
+
console.error("[terminal-detect] Failed to rewrite Hermes config:", err.message);
|
|
224
|
+
}
|
|
225
|
+
}
|