@h1d3rone/claude-proxy 0.1.0

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/src/config.js ADDED
@@ -0,0 +1,494 @@
1
+ const fs = require("fs/promises");
2
+ const path = require("path");
3
+ const os = require("os");
4
+ const readline = require("readline/promises");
5
+ const toml = require("@iarna/toml");
6
+
7
+ const DEFAULT_CLIENT_API_KEY = "arbitrary value";
8
+ const DEFAULT_BIG_MODEL = "gpt-5.4";
9
+ const DEFAULT_MIDDLE_MODEL = "gpt-5.3-codex";
10
+ const DEFAULT_SMALL_MODEL = "gpt-5.2-codex";
11
+ const DEFAULT_CLAUDE_MODEL = "opus[1m]";
12
+ const LEGACY_TOP_LEVEL_FIELDS = ["hosts", "type", "name", "enabled", "host", "user", "sync_project"];
13
+ const CONFIG_SECTION_FIELDS = {
14
+ claude: ["server_port", "big_model", "middle_model", "small_model", "default_claude_model", "claude_dir"],
15
+ openai: ["base_url", "api_key", "codex_dir", "codex_provider"]
16
+ };
17
+
18
+ function expandHome(inputPath) {
19
+ if (!inputPath || typeof inputPath !== "string") {
20
+ return inputPath;
21
+ }
22
+
23
+ if (inputPath === "~") {
24
+ return os.homedir();
25
+ }
26
+
27
+ if (inputPath.startsWith("~/")) {
28
+ return path.join(os.homedir(), inputPath.slice(2));
29
+ }
30
+
31
+ return inputPath;
32
+ }
33
+
34
+ function normalizeLocalPath(inputPath, baseDir) {
35
+ const expanded = expandHome(inputPath);
36
+ if (!expanded) {
37
+ return expanded;
38
+ }
39
+
40
+ if (path.isAbsolute(expanded)) {
41
+ return expanded;
42
+ }
43
+
44
+ return path.resolve(baseDir, expanded);
45
+ }
46
+
47
+ function getDefaultConfigDir() {
48
+ return path.join(os.homedir(), ".claude-proxy");
49
+ }
50
+
51
+ function getDefaultConfigPath() {
52
+ return path.join(getDefaultConfigDir(), "config.toml");
53
+ }
54
+
55
+ function getDefaultConfigDisplayPath() {
56
+ return "<home>/.claude-proxy/config.toml";
57
+ }
58
+
59
+ function createDefaultConfigDocument() {
60
+ return {
61
+ server_host: "127.0.0.1",
62
+ server_port: 8082,
63
+ base_url: "",
64
+ api_key: "",
65
+ big_model: DEFAULT_BIG_MODEL,
66
+ middle_model: DEFAULT_MIDDLE_MODEL,
67
+ small_model: DEFAULT_SMALL_MODEL,
68
+ default_claude_model: DEFAULT_CLAUDE_MODEL,
69
+ home_dir: "~",
70
+ claude_dir: "~/.claude",
71
+ codex_dir: "~/.codex"
72
+ };
73
+ }
74
+
75
+ function isPlaceholderValue(value) {
76
+ if (value == null) {
77
+ return true;
78
+ }
79
+
80
+ const normalized = String(value).trim();
81
+ if (!normalized) {
82
+ return true;
83
+ }
84
+
85
+ const lowered = normalized.toLowerCase();
86
+ return (
87
+ lowered.startsWith("replace-with-") ||
88
+ lowered.startsWith("your-") ||
89
+ lowered === "changeme" ||
90
+ lowered === "todo"
91
+ );
92
+ }
93
+
94
+ function applyHostRuntimePaths(host, runtime = {}) {
95
+ const pathApi = runtime.pathApi || path;
96
+ const projectRoot = runtime.projectRoot || host.project_root;
97
+ const claudeDir = runtime.claudeDir || host.claude_dir;
98
+ const codexDir = runtime.codexDir || host.codex_dir;
99
+ const configPath = runtime.configPath || host.config_path || pathApi.join(projectRoot, "config.toml");
100
+ const runtimeDir = pathApi.join(claudeDir, "claude-proxy");
101
+ const stateDir = pathApi.join(runtimeDir, "state");
102
+ const logsDir = pathApi.join(runtimeDir, "logs");
103
+
104
+ return {
105
+ ...host,
106
+ project_root: projectRoot,
107
+ claude_dir: claudeDir,
108
+ codex_dir: codexDir,
109
+ runtime_dir: runtimeDir,
110
+ state_dir: stateDir,
111
+ logs_dir: logsDir,
112
+ settings_path: pathApi.join(claudeDir, "settings.json"),
113
+ backups_dir: pathApi.join(runtimeDir, "backups"),
114
+ sessions_file: pathApi.join(stateDir, "sessions.txt"),
115
+ pid_file: pathApi.join(stateDir, "proxy.pid"),
116
+ managed_state_file: pathApi.join(stateDir, "managed-files.json"),
117
+ server_log_file: pathApi.join(logsDir, "server.log"),
118
+ codex_config_path: pathApi.join(codexDir, "config.toml"),
119
+ codex_auth_path: pathApi.join(codexDir, "auth.json"),
120
+ config_path: configPath
121
+ };
122
+ }
123
+
124
+ function validateConfigDocument(document) {
125
+ const legacyFields = LEGACY_TOP_LEVEL_FIELDS.filter((key) =>
126
+ Object.prototype.hasOwnProperty.call(document, key)
127
+ );
128
+
129
+ if (legacyFields.length > 0) {
130
+ throw new Error(
131
+ "Only the single-machine top-level config format is supported now. " +
132
+ `Remove legacy fields: ${legacyFields.join(", ")}.`
133
+ );
134
+ }
135
+ }
136
+
137
+ function getPromptValue(document, key, fallback) {
138
+ const value = document[key];
139
+ if (value == null || value === "") {
140
+ return fallback;
141
+ }
142
+ return String(value);
143
+ }
144
+
145
+ function collectConfigPrompts(document = {}) {
146
+ return [
147
+ {
148
+ target: "server_host",
149
+ question: "Server host",
150
+ defaultValue: getPromptValue(document, "server_host", "127.0.0.1"),
151
+ required: true
152
+ },
153
+ {
154
+ target: "server_port",
155
+ question: "Server port",
156
+ defaultValue: String(document.server_port || 8082),
157
+ required: true
158
+ },
159
+ {
160
+ target: "base_url",
161
+ question: "Upstream OpenAI-compatible base_url (provider root or explicit /v1)",
162
+ defaultValue: getPromptValue(document, "base_url", ""),
163
+ required: true
164
+ },
165
+ {
166
+ target: "api_key",
167
+ question: "Upstream API key",
168
+ defaultValue: getPromptValue(document, "api_key", ""),
169
+ required: true
170
+ },
171
+ {
172
+ target: "big_model",
173
+ question: "Large model mapping",
174
+ defaultValue: getPromptValue(document, "big_model", DEFAULT_BIG_MODEL),
175
+ required: true
176
+ },
177
+ {
178
+ target: "middle_model",
179
+ question: "Middle model mapping",
180
+ defaultValue: getPromptValue(document, "middle_model", DEFAULT_MIDDLE_MODEL),
181
+ required: true
182
+ },
183
+ {
184
+ target: "small_model",
185
+ question: "Small model mapping",
186
+ defaultValue: getPromptValue(document, "small_model", DEFAULT_SMALL_MODEL),
187
+ required: true
188
+ },
189
+ {
190
+ target: "default_claude_model",
191
+ question: "Default Claude model",
192
+ defaultValue: getPromptValue(document, "default_claude_model", DEFAULT_CLAUDE_MODEL),
193
+ required: true
194
+ },
195
+ {
196
+ target: "home_dir",
197
+ question: "Home directory",
198
+ defaultValue: getPromptValue(document, "home_dir", "~"),
199
+ required: true
200
+ },
201
+ {
202
+ target: "claude_dir",
203
+ question: "Claude config directory",
204
+ defaultValue: getPromptValue(document, "claude_dir", "~/.claude"),
205
+ required: true
206
+ },
207
+ {
208
+ target: "codex_dir",
209
+ question: "Codex config directory",
210
+ defaultValue: getPromptValue(document, "codex_dir", "~/.codex"),
211
+ required: true
212
+ },
213
+ {
214
+ target: "codex_provider",
215
+ question: "Codex provider name (optional)",
216
+ defaultValue: getPromptValue(document, "codex_provider", ""),
217
+ required: false
218
+ }
219
+ ];
220
+ }
221
+
222
+ function collectConfigPromptsForSection(document = {}, section) {
223
+ if (!section || section === "all") {
224
+ return collectConfigPrompts(document);
225
+ }
226
+
227
+ const targets = new Set(CONFIG_SECTION_FIELDS[section] || []);
228
+ if (targets.size === 0) {
229
+ throw new Error(`Unknown config section: ${section}`);
230
+ }
231
+
232
+ return collectConfigPrompts(document).filter((entry) => targets.has(entry.target));
233
+ }
234
+
235
+ function normalizeConfigDocument(document, resolvedConfigPath, options = {}) {
236
+ const configDir = path.dirname(resolvedConfigPath);
237
+ const runtimeProjectRoot = options.runtimeProjectRoot || configDir;
238
+ const homeDir = normalizeLocalPath(document.home_dir || "~", configDir);
239
+ const claudeDir = normalizeLocalPath(document.claude_dir || path.join(homeDir, ".claude"), configDir);
240
+ const codexDir = normalizeLocalPath(document.codex_dir || path.join(homeDir, ".codex"), configDir);
241
+ const localConfig = applyHostRuntimePaths(
242
+ {
243
+ name: "local",
244
+ type: "local",
245
+ base_url: document.base_url || "",
246
+ api_key: document.api_key || "",
247
+ client_api_key: DEFAULT_CLIENT_API_KEY,
248
+ big_model: document.big_model || DEFAULT_BIG_MODEL,
249
+ middle_model: document.middle_model || DEFAULT_MIDDLE_MODEL,
250
+ small_model: document.small_model || DEFAULT_SMALL_MODEL,
251
+ default_claude_model: document.default_claude_model || DEFAULT_CLAUDE_MODEL,
252
+ codex_provider: document.codex_provider || null,
253
+ home_dir: homeDir,
254
+ project_root: runtimeProjectRoot,
255
+ claude_dir: claudeDir,
256
+ codex_dir: codexDir,
257
+ config_path: resolvedConfigPath
258
+ },
259
+ {
260
+ projectRoot: runtimeProjectRoot,
261
+ claudeDir,
262
+ codexDir,
263
+ configPath: resolvedConfigPath
264
+ }
265
+ );
266
+
267
+ return {
268
+ server_host: document.server_host || "127.0.0.1",
269
+ server_port: Number(document.server_port || 8082),
270
+ __configPath: resolvedConfigPath,
271
+ __projectRoot: runtimeProjectRoot,
272
+ ...localConfig
273
+ };
274
+ }
275
+
276
+ async function readConfigDocument(configPath, options = {}) {
277
+ const resolvedConfigPath = path.resolve(configPath || getDefaultConfigPath());
278
+ let raw;
279
+ try {
280
+ raw = await fs.readFile(resolvedConfigPath, "utf8");
281
+ } catch (error) {
282
+ if (error.code === "ENOENT" && options.allowMissing) {
283
+ return {
284
+ resolvedConfigPath,
285
+ document: createDefaultConfigDocument()
286
+ };
287
+ }
288
+ if (error.code === "ENOENT") {
289
+ const defaultConfigPath = getDefaultConfigPath();
290
+ const defaultConfigDisplayPath = getDefaultConfigDisplayPath();
291
+ const usesDefaultConfigPath = !configPath || resolvedConfigPath === defaultConfigPath;
292
+ const displayPath = usesDefaultConfigPath ? defaultConfigDisplayPath : resolvedConfigPath;
293
+ const hint = usesDefaultConfigPath
294
+ ? `Run "claude-proxy config" first to create the default config at ${defaultConfigDisplayPath}.`
295
+ : `Please check whether the file exists, or rerun with the same --config path used during "claude-proxy config".`;
296
+ throw new Error(
297
+ `Config file not found: ${displayPath}. ${hint} You can use "config_example.toml" in the project root as a reference.`
298
+ );
299
+ }
300
+ throw error;
301
+ }
302
+
303
+ return {
304
+ resolvedConfigPath,
305
+ document: toml.parse(raw)
306
+ };
307
+ }
308
+
309
+ async function writeConfigDocument(configPath, document) {
310
+ await fs.mkdir(path.dirname(configPath), { recursive: true });
311
+ await fs.writeFile(configPath, toml.stringify(document), "utf8");
312
+ }
313
+
314
+ function clearConfigDocumentSection(document, section) {
315
+ const fields = CONFIG_SECTION_FIELDS[section];
316
+ if (!fields) {
317
+ throw new Error(`Unknown config section: ${section}`);
318
+ }
319
+
320
+ const cleaned = { ...document };
321
+ for (const field of fields) {
322
+ delete cleaned[field];
323
+ }
324
+ return cleaned;
325
+ }
326
+
327
+ async function clearConfigSection(configPath, section) {
328
+ const resolvedConfigPath = path.resolve(configPath || getDefaultConfigPath());
329
+
330
+ try {
331
+ await fs.access(resolvedConfigPath);
332
+ } catch (error) {
333
+ if (error.code === "ENOENT") {
334
+ return false;
335
+ }
336
+ throw error;
337
+ }
338
+
339
+ const { document } = await readConfigDocument(resolvedConfigPath);
340
+ validateConfigDocument(document);
341
+ await writeConfigDocument(resolvedConfigPath, clearConfigDocumentSection(document, section));
342
+ return true;
343
+ }
344
+
345
+ async function loadConfig(configPath, options = {}) {
346
+ const { resolvedConfigPath, document } = await readConfigDocument(configPath, {
347
+ allowMissing: options.allowMissing
348
+ });
349
+ validateConfigDocument(document);
350
+ const config = normalizeConfigDocument(document, resolvedConfigPath, {
351
+ runtimeProjectRoot: options.runtimeProjectRoot
352
+ });
353
+
354
+ if (
355
+ !options.allowIncomplete &&
356
+ (isPlaceholderValue(config.base_url) || isPlaceholderValue(config.api_key))
357
+ ) {
358
+ throw new Error("Config must include non-empty base_url and api_key.");
359
+ }
360
+
361
+ return config;
362
+ }
363
+
364
+ function setDocumentValue(document, target, value) {
365
+ if (target === "server_port") {
366
+ document.server_port = Number(value);
367
+ return;
368
+ }
369
+
370
+ if (target === "codex_provider") {
371
+ if (value) {
372
+ document.codex_provider = value;
373
+ } else {
374
+ delete document.codex_provider;
375
+ }
376
+ return;
377
+ }
378
+
379
+ document[target] = value;
380
+ }
381
+
382
+ function isValidPromptValue(target, value) {
383
+ if (target === "codex_provider") {
384
+ return true;
385
+ }
386
+
387
+ if (!value) {
388
+ return false;
389
+ }
390
+
391
+ if (target === "server_port") {
392
+ const port = Number(value);
393
+ return Number.isInteger(port) && port > 0;
394
+ }
395
+
396
+ return true;
397
+ }
398
+
399
+ async function promptForConfig(configPath, options = {}) {
400
+ const { resolvedConfigPath, document } = await readConfigDocument(configPath, {
401
+ allowMissing: true
402
+ });
403
+ validateConfigDocument(document);
404
+ const prompts = collectConfigPromptsForSection(document, "all");
405
+ const rl = readline.createInterface({
406
+ input: process.stdin,
407
+ output: process.stdout
408
+ });
409
+
410
+ try {
411
+ for (const entry of prompts) {
412
+ for (;;) {
413
+ const suffix = entry.defaultValue ? ` [${entry.defaultValue}]` : "";
414
+ const answer = await rl.question(`${entry.question}${suffix}: `);
415
+ const value = answer.trim() || entry.defaultValue;
416
+
417
+ if (!isValidPromptValue(entry.target, value)) {
418
+ console.log(`Invalid value for ${entry.target}. Please try again.`);
419
+ continue;
420
+ }
421
+
422
+ setDocumentValue(document, entry.target, value);
423
+ break;
424
+ }
425
+ }
426
+ } finally {
427
+ rl.close();
428
+ }
429
+
430
+ await writeConfigDocument(resolvedConfigPath, document);
431
+ return normalizeConfigDocument(document, resolvedConfigPath, {
432
+ runtimeProjectRoot: options.runtimeProjectRoot
433
+ });
434
+ }
435
+
436
+ async function promptForConfigSection(configPath, section, options = {}) {
437
+ const { resolvedConfigPath, document } = await readConfigDocument(configPath, {
438
+ allowMissing: true
439
+ });
440
+ validateConfigDocument(document);
441
+ const prompts = collectConfigPromptsForSection(document, section);
442
+ const rl = readline.createInterface({
443
+ input: process.stdin,
444
+ output: process.stdout
445
+ });
446
+
447
+ try {
448
+ for (const entry of prompts) {
449
+ for (;;) {
450
+ const suffix = entry.defaultValue ? ` [${entry.defaultValue}]` : "";
451
+ const answer = await rl.question(`${entry.question}${suffix}: `);
452
+ const value = answer.trim() || entry.defaultValue;
453
+
454
+ if (!isValidPromptValue(entry.target, value)) {
455
+ console.log(`Invalid value for ${entry.target}. Please try again.`);
456
+ continue;
457
+ }
458
+
459
+ setDocumentValue(document, entry.target, value);
460
+ break;
461
+ }
462
+ }
463
+ } finally {
464
+ rl.close();
465
+ }
466
+
467
+ await writeConfigDocument(resolvedConfigPath, document);
468
+ return normalizeConfigDocument(document, resolvedConfigPath, {
469
+ runtimeProjectRoot: options.runtimeProjectRoot
470
+ });
471
+ }
472
+
473
+ module.exports = {
474
+ DEFAULT_BIG_MODEL,
475
+ DEFAULT_CLAUDE_MODEL,
476
+ DEFAULT_CLIENT_API_KEY,
477
+ DEFAULT_MIDDLE_MODEL,
478
+ DEFAULT_SMALL_MODEL,
479
+ applyHostRuntimePaths,
480
+ clearConfigSection,
481
+ collectConfigPrompts,
482
+ collectConfigPromptsForSection,
483
+ expandHome,
484
+ getDefaultConfigDir,
485
+ getDefaultConfigDisplayPath,
486
+ getDefaultConfigPath,
487
+ isPlaceholderValue,
488
+ loadConfig,
489
+ normalizeLocalPath,
490
+ promptForConfig,
491
+ promptForConfigSection,
492
+ readConfigDocument,
493
+ writeConfigDocument
494
+ };