@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/cli.js ADDED
@@ -0,0 +1,458 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("fs/promises");
4
+ const path = require("path");
5
+ const toml = require("@iarna/toml");
6
+ const { Command } = require("commander");
7
+ const {
8
+ clearConfigSection,
9
+ getDefaultConfigDisplayPath,
10
+ getDefaultConfigPath,
11
+ loadConfig,
12
+ promptForConfig,
13
+ promptForConfigSection,
14
+ readConfigDocument
15
+ } = require("./config");
16
+ const {
17
+ applyClaudeManagedHostConfig,
18
+ applyManagedHostConfig,
19
+ applyOpenAIManagedHostConfig,
20
+ cleanClaudeManagedHostConfig,
21
+ cleanManagedHostConfig,
22
+ cleanOpenAIManagedHostConfig
23
+ } = require("./services/client-config-manager");
24
+ const { ensureProxy, stopProxy } = require("./services/host-manager");
25
+ const { startServer } = require("./proxy/server");
26
+ const { pathExists, readJson } = require("./utils");
27
+
28
+ const PROJECT_ROOT = path.resolve(__dirname, "..");
29
+
30
+ function formatErrorMessage(error) {
31
+ return error && error.message ? error.message : String(error);
32
+ }
33
+
34
+ function getConfigOptionDescription() {
35
+ return `Path to config.toml (default: ${getDefaultConfigDisplayPath()})`;
36
+ }
37
+
38
+ function resolveConfigPath(options, command) {
39
+ return command?.optsWithGlobals?.().config || options?.config || getDefaultConfigPath();
40
+ }
41
+
42
+ function createStepLogger(config) {
43
+ return (event) => {
44
+ const suffix = event.error ? ` (${formatErrorMessage(event.error)})` : "";
45
+ console.log(`[local] ${event.label}: ${event.status}${suffix}`);
46
+ };
47
+ }
48
+
49
+ async function applyLocalConfig(config) {
50
+ const logStep = createStepLogger(config);
51
+ await applyManagedHostConfig(config, config, { onProgress: logStep });
52
+ console.log("Configured local environment");
53
+ }
54
+
55
+ async function applyClaudeConfig(config) {
56
+ const logStep = createStepLogger(config);
57
+ await applyClaudeManagedHostConfig(config, config, { onProgress: logStep });
58
+ console.log("Configured Claude environment");
59
+ }
60
+
61
+ async function applyOpenAIConfig(config) {
62
+ const logStep = createStepLogger(config);
63
+ await applyOpenAIManagedHostConfig(config, config, { onProgress: logStep });
64
+ console.log("Configured OpenAI environment");
65
+ }
66
+
67
+ async function clearConfigSectionsWithLog(config, sections, prefix) {
68
+ const logStep = createStepLogger(config);
69
+
70
+ for (const section of sections) {
71
+ const label = `Clear ${prefix || section} config fields (${config.__configPath})`;
72
+ logStep({ label, status: "started" });
73
+ const changed = await clearConfigSection(config.__configPath, section);
74
+ logStep({ label, status: changed ? "completed" : "skipped" });
75
+ }
76
+ }
77
+
78
+ async function cleanClaudeConfig(config) {
79
+ const logStep = createStepLogger(config);
80
+ await cleanClaudeManagedHostConfig(config, config, { onProgress: logStep });
81
+ await clearConfigSectionsWithLog(config, ["claude"], "Claude");
82
+ console.log("Restored Claude configuration");
83
+ }
84
+
85
+ async function cleanOpenAIConfig(config) {
86
+ const logStep = createStepLogger(config);
87
+ await cleanOpenAIManagedHostConfig(config, config, { onProgress: logStep });
88
+ await clearConfigSectionsWithLog(config, ["openai"], "OpenAI");
89
+ console.log("Restored OpenAI configuration");
90
+ }
91
+
92
+ async function cleanAllConfig(config) {
93
+ const logStep = createStepLogger(config);
94
+ await cleanManagedHostConfig(config, config, { onProgress: logStep });
95
+ await clearConfigSectionsWithLog(config, ["claude", "openai"]);
96
+ console.log("Restored local configuration");
97
+ }
98
+
99
+ function formatValue(value) {
100
+ if (value == null || value === "") {
101
+ return "(not set)";
102
+ }
103
+ return String(value);
104
+ }
105
+
106
+ function flattenHooks(groups) {
107
+ return Array.isArray(groups) ? groups.flatMap((group) => group?.hooks || []) : [];
108
+ }
109
+
110
+ function hasManagedHook(settings, actionToken) {
111
+ const groups =
112
+ actionToken === "ensure-proxy"
113
+ ? settings?.hooks?.SessionStart
114
+ : settings?.hooks?.SessionEnd;
115
+ return flattenHooks(groups).some((hook) =>
116
+ typeof hook?.command === "string" && hook.command.includes(`internal ${actionToken}`)
117
+ );
118
+ }
119
+
120
+ function getCodexProviderName(document, preferredProvider) {
121
+ const providers =
122
+ document.model_providers && typeof document.model_providers === "object"
123
+ ? document.model_providers
124
+ : null;
125
+
126
+ if (providers && preferredProvider && providers[preferredProvider]) {
127
+ return preferredProvider;
128
+ }
129
+
130
+ if (providers && document.model_provider && providers[document.model_provider]) {
131
+ return document.model_provider;
132
+ }
133
+
134
+ if (!providers) {
135
+ return null;
136
+ }
137
+
138
+ return Object.keys(providers)[0] || null;
139
+ }
140
+
141
+ async function readToml(targetPath, fallback = {}) {
142
+ try {
143
+ const raw = await fs.readFile(targetPath, "utf8");
144
+ return toml.parse(raw);
145
+ } catch (error) {
146
+ if (error.code === "ENOENT") {
147
+ return fallback;
148
+ }
149
+ throw error;
150
+ }
151
+ }
152
+
153
+ function formatSection(title, lines) {
154
+ return [title, ...lines.map((line) => ` ${line}`)].join("\n");
155
+ }
156
+
157
+ async function buildConfigSummary(configPath) {
158
+ const resolvedConfigPath = path.resolve(configPath || getDefaultConfigPath());
159
+ const configExists = await pathExists(resolvedConfigPath);
160
+ const { document } = await readConfigDocument(resolvedConfigPath, {
161
+ allowMissing: true
162
+ });
163
+ const config = await loadConfig(resolvedConfigPath, {
164
+ allowIncomplete: true,
165
+ allowMissing: true,
166
+ runtimeProjectRoot: PROJECT_ROOT
167
+ });
168
+
169
+ const claudeExists = await pathExists(config.settings_path);
170
+ const claudeSettings = await readJson(config.settings_path, {});
171
+
172
+ const codexConfigExists = await pathExists(config.codex_config_path);
173
+ const codexConfig = await readToml(config.codex_config_path, {});
174
+ const codexProvider = getCodexProviderName(codexConfig, config.codex_provider || null);
175
+ const codexBaseUrl = codexProvider
176
+ ? codexConfig.model_providers?.[codexProvider]?.base_url
177
+ : codexConfig.base_url;
178
+
179
+ const codexAuthExists = await pathExists(config.codex_auth_path);
180
+ const codexAuth = await readJson(config.codex_auth_path, {});
181
+
182
+ return [
183
+ formatSection("Config File", [
184
+ `path: ${resolvedConfigPath}`,
185
+ `exists: ${configExists ? "yes" : "no"}`,
186
+ `server_host: ${formatValue(document.server_host)}`,
187
+ `server_port: ${formatValue(document.server_port)}`,
188
+ `base_url: ${formatValue(document.base_url)}`,
189
+ `api_key: ${formatValue(document.api_key)}`,
190
+ `big_model: ${formatValue(document.big_model)}`,
191
+ `middle_model: ${formatValue(document.middle_model)}`,
192
+ `small_model: ${formatValue(document.small_model)}`,
193
+ `default_claude_model: ${formatValue(document.default_claude_model)}`,
194
+ `home_dir: ${formatValue(document.home_dir)}`,
195
+ `claude_dir: ${formatValue(document.claude_dir)}`,
196
+ `codex_dir: ${formatValue(document.codex_dir)}`,
197
+ `codex_provider: ${formatValue(document.codex_provider)}`
198
+ ]),
199
+ formatSection("Claude", [
200
+ `path: ${config.settings_path}`,
201
+ `exists: ${claudeExists ? "yes" : "no"}`,
202
+ `ANTHROPIC_BASE_URL: ${formatValue(claudeSettings?.env?.ANTHROPIC_BASE_URL)}`,
203
+ `ANTHROPIC_API_KEY: ${formatValue(claudeSettings?.env?.ANTHROPIC_API_KEY)}`,
204
+ `ANTHROPIC_MODEL: ${formatValue(claudeSettings?.env?.ANTHROPIC_MODEL)}`,
205
+ `ANTHROPIC_REASONING_MODEL: ${formatValue(claudeSettings?.env?.ANTHROPIC_REASONING_MODEL)}`,
206
+ `model: ${formatValue(claudeSettings?.model)}`,
207
+ `ensure-proxy hook: ${hasManagedHook(claudeSettings, "ensure-proxy") ? "installed" : "missing"}`,
208
+ `stop-proxy hook: ${hasManagedHook(claudeSettings, "stop-proxy") ? "installed" : "missing"}`
209
+ ]),
210
+ formatSection("Codex Config", [
211
+ `path: ${config.codex_config_path}`,
212
+ `exists: ${codexConfigExists ? "yes" : "no"}`,
213
+ `provider: ${formatValue(codexProvider)}`,
214
+ `base_url: ${formatValue(codexBaseUrl)}`
215
+ ]),
216
+ formatSection("Codex Auth", [
217
+ `path: ${config.codex_auth_path}`,
218
+ `exists: ${codexAuthExists ? "yes" : "no"}`,
219
+ `OPENAI_API_KEY: ${formatValue(codexAuth?.OPENAI_API_KEY)}`
220
+ ])
221
+ ].join("\n\n");
222
+ }
223
+
224
+ async function main() {
225
+ const program = new Command();
226
+ program.name("claude-proxy");
227
+ program.addHelpCommand("help [command]", "Show help for the main command or a subcommand");
228
+ program.addHelpText(
229
+ "after",
230
+ [
231
+ "",
232
+ "Config:",
233
+ ` Default config path: ${getDefaultConfigDisplayPath()}`,
234
+ "",
235
+ "Common usage:",
236
+ " claude-proxy config",
237
+ " Interactively configure the local proxy and apply local Claude/Codex settings",
238
+ " claude-proxy config claude",
239
+ " Interactively configure Claude settings only",
240
+ " claude-proxy config openai",
241
+ " Interactively configure OpenAI/Codex settings only",
242
+ " claude-proxy config get",
243
+ " Show the current local proxy, Claude, and Codex configuration summary",
244
+ " claude-proxy clean",
245
+ " Restore local Claude/Codex settings and clear config.toml owned fields",
246
+ " claude-proxy clean claude",
247
+ " Restore Claude settings from backups",
248
+ " claude-proxy clean openai",
249
+ " Restore OpenAI/Codex settings from backups",
250
+ " claude-proxy start",
251
+ " Start the local proxy server only",
252
+ " claude-proxy stop",
253
+ " Stop the managed local proxy server",
254
+ "",
255
+ "More help:",
256
+ " claude-proxy help",
257
+ " claude-proxy help config",
258
+ " claude-proxy help start",
259
+ " claude-proxy help stop",
260
+ " claude-proxy help clean",
261
+ ""
262
+ ].join("\n")
263
+ );
264
+
265
+ program
266
+ .command("start")
267
+ .description("Start the local proxy server only")
268
+ .option("--config <path>", getConfigOptionDescription())
269
+ .addHelpText(
270
+ "after",
271
+ [
272
+ "",
273
+ "Help:",
274
+ " claude-proxy help start",
275
+ ""
276
+ ].join("\n")
277
+ )
278
+ .action(async (options, command) => {
279
+ const configPath = resolveConfigPath(options, command);
280
+ const config = await loadConfig(configPath, {
281
+ runtimeProjectRoot: PROJECT_ROOT
282
+ });
283
+ await startServer(config, config);
284
+ console.log(`Proxy listening on ${config.server_host}:${config.server_port}`);
285
+ });
286
+
287
+ program
288
+ .command("stop")
289
+ .description("Stop the managed local proxy server")
290
+ .option("--config <path>", getConfigOptionDescription())
291
+ .addHelpText(
292
+ "after",
293
+ [
294
+ "",
295
+ "Help:",
296
+ " claude-proxy help stop",
297
+ ""
298
+ ].join("\n")
299
+ )
300
+ .action(async (options, command) => {
301
+ const configPath = resolveConfigPath(options, command);
302
+ const config = await loadConfig(configPath, {
303
+ allowIncomplete: true,
304
+ allowMissing: true,
305
+ runtimeProjectRoot: PROJECT_ROOT
306
+ });
307
+ await stopProxy(config, config, { force: true });
308
+ console.log("Stopped local proxy");
309
+ });
310
+
311
+ const configCommand = new Command("config")
312
+ .description("Manage the local proxy configuration")
313
+ .option("--config <path>", getConfigOptionDescription())
314
+ .addHelpText(
315
+ "after",
316
+ [
317
+ "",
318
+ "Subcommands:",
319
+ " claude-proxy config",
320
+ " claude-proxy config claude",
321
+ " claude-proxy config openai",
322
+ " claude-proxy config get",
323
+ ""
324
+ ].join("\n")
325
+ )
326
+ .action(async (options, command) => {
327
+ const configPath = resolveConfigPath(options, command);
328
+ const config = await promptForConfig(configPath, {
329
+ runtimeProjectRoot: PROJECT_ROOT
330
+ });
331
+ await applyLocalConfig(config);
332
+ });
333
+
334
+ configCommand
335
+ .command("claude")
336
+ .description("Interactively configure Claude settings only")
337
+ .option("--config <path>", getConfigOptionDescription())
338
+ .action(async (options, command) => {
339
+ const configPath = resolveConfigPath(options, command);
340
+ const config = await promptForConfigSection(configPath, "claude", {
341
+ runtimeProjectRoot: PROJECT_ROOT
342
+ });
343
+ await applyClaudeConfig(config);
344
+ });
345
+
346
+ configCommand
347
+ .command("openai")
348
+ .description("Interactively configure OpenAI/Codex settings only")
349
+ .option("--config <path>", getConfigOptionDescription())
350
+ .action(async (options, command) => {
351
+ const configPath = resolveConfigPath(options, command);
352
+ const config = await promptForConfigSection(configPath, "openai", {
353
+ runtimeProjectRoot: PROJECT_ROOT
354
+ });
355
+ await applyOpenAIConfig(config);
356
+ });
357
+
358
+ configCommand
359
+ .command("get")
360
+ .description("Show the current local proxy, Claude, and Codex configuration summary")
361
+ .option("--config <path>", getConfigOptionDescription())
362
+ .action(async (options, command) => {
363
+ const configPath = resolveConfigPath(options, command);
364
+ console.log(await buildConfigSummary(configPath));
365
+ });
366
+
367
+ program.addCommand(configCommand);
368
+
369
+ const cleanCommand = new Command("clean")
370
+ .description("Restore local Claude or OpenAI settings from backups")
371
+ .option("--config <path>", getConfigOptionDescription())
372
+ .addHelpText(
373
+ "after",
374
+ [
375
+ "",
376
+ "Subcommands:",
377
+ " claude-proxy clean",
378
+ " claude-proxy clean claude",
379
+ " claude-proxy clean openai",
380
+ "",
381
+ "Behavior:",
382
+ " Does not stop the running proxy server",
383
+ ""
384
+ ].join("\n")
385
+ )
386
+ .action(async (options, command) => {
387
+ const configPath = resolveConfigPath(options, command);
388
+ const config = await loadConfig(configPath, {
389
+ allowIncomplete: true,
390
+ allowMissing: true,
391
+ runtimeProjectRoot: PROJECT_ROOT
392
+ });
393
+ await cleanAllConfig(config);
394
+ });
395
+
396
+ cleanCommand
397
+ .command("claude")
398
+ .description("Restore Claude settings from backups")
399
+ .option("--config <path>", getConfigOptionDescription())
400
+ .action(async (options, command) => {
401
+ const configPath = resolveConfigPath(options, command);
402
+ const config = await loadConfig(configPath, {
403
+ allowIncomplete: true,
404
+ allowMissing: true,
405
+ runtimeProjectRoot: PROJECT_ROOT
406
+ });
407
+ await cleanClaudeConfig(config);
408
+ });
409
+
410
+ cleanCommand
411
+ .command("openai")
412
+ .description("Restore OpenAI/Codex settings from backups")
413
+ .option("--config <path>", getConfigOptionDescription())
414
+ .action(async (options, command) => {
415
+ const configPath = resolveConfigPath(options, command);
416
+ const config = await loadConfig(configPath, {
417
+ allowIncomplete: true,
418
+ allowMissing: true,
419
+ runtimeProjectRoot: PROJECT_ROOT
420
+ });
421
+ await cleanOpenAIConfig(config);
422
+ });
423
+
424
+ program.addCommand(cleanCommand);
425
+
426
+ const internalCommand = new Command("internal")
427
+ .addCommand(
428
+ new Command("ensure-proxy")
429
+ .requiredOption("--config <path>")
430
+ .action(async (options) => {
431
+ const config = await loadConfig(options.config, {
432
+ runtimeProjectRoot: PROJECT_ROOT
433
+ });
434
+ await ensureProxy(config, config, options.config);
435
+ })
436
+ )
437
+ .addCommand(
438
+ new Command("stop-proxy")
439
+ .requiredOption("--config <path>")
440
+ .action(async (options) => {
441
+ const config = await loadConfig(options.config, {
442
+ allowIncomplete: true,
443
+ allowMissing: true,
444
+ runtimeProjectRoot: PROJECT_ROOT
445
+ });
446
+ await stopProxy(config, config);
447
+ })
448
+ );
449
+
450
+ program.addCommand(internalCommand, { hidden: true });
451
+
452
+ await program.parseAsync(process.argv);
453
+ }
454
+
455
+ main().catch((error) => {
456
+ console.error(error.message || error);
457
+ process.exit(1);
458
+ });