@cbuk100011/claude-mode 1.2.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/dist/index.js ADDED
@@ -0,0 +1,1024 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+ import chalk2 from "chalk";
6
+ import { select, input } from "@inquirer/prompts";
7
+ import { spawn, execSync } from "child_process";
8
+ import { config } from "dotenv";
9
+ import { fileURLToPath } from "url";
10
+ import path2 from "path";
11
+
12
+ // src/config.ts
13
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
14
+ import { homedir } from "os";
15
+ import path from "path";
16
+ function getConfigDir() {
17
+ return path.join(homedir(), ".claude-mode");
18
+ }
19
+ function getConfigPaths() {
20
+ return [
21
+ path.join(getConfigDir(), "claude-mode.json")
22
+ // ~/.claude-mode/claude-mode.json (primary)
23
+ ];
24
+ }
25
+ function getCacheDir() {
26
+ const configDir = getConfigDir();
27
+ const cacheDir = path.join(configDir, "cache");
28
+ if (!existsSync(cacheDir)) {
29
+ mkdirSync(cacheDir, { recursive: true });
30
+ }
31
+ return cacheDir;
32
+ }
33
+ var DEFAULT_CONFIG = {
34
+ defaultProvider: "",
35
+ defaultModel: "",
36
+ modelDiscoveryTimeout: 5e3,
37
+ healthCheckTimeout: 2e3,
38
+ cacheTTL: 3e4,
39
+ customProviders: [],
40
+ skipHealthCheck: false,
41
+ offlineMode: false,
42
+ headlessAllowedTools: "Read,Edit,Write,Bash,Glob,Grep"
43
+ };
44
+ var _cachedConfig = null;
45
+ function loadConfig() {
46
+ if (_cachedConfig) {
47
+ return _cachedConfig;
48
+ }
49
+ const paths = getConfigPaths();
50
+ for (const configPath of paths) {
51
+ if (existsSync(configPath)) {
52
+ try {
53
+ const content = readFileSync(configPath, "utf-8");
54
+ const parsed = JSON.parse(content);
55
+ _cachedConfig = { ...DEFAULT_CONFIG, ...parsed };
56
+ return _cachedConfig;
57
+ } catch (error) {
58
+ console.error(`Warning: Failed to parse config at ${configPath}:`, error);
59
+ }
60
+ }
61
+ }
62
+ _cachedConfig = DEFAULT_CONFIG;
63
+ return _cachedConfig;
64
+ }
65
+ function getConfig(key) {
66
+ const config2 = loadConfig();
67
+ return config2[key] ?? DEFAULT_CONFIG[key];
68
+ }
69
+ function initConfig() {
70
+ const configDir = getConfigDir();
71
+ const configPath = path.join(configDir, "claude-mode.json");
72
+ if (existsSync(configPath)) {
73
+ return configPath;
74
+ }
75
+ if (!existsSync(configDir)) {
76
+ mkdirSync(configDir, { recursive: true });
77
+ }
78
+ const defaultConfig = {
79
+ defaultProvider: "",
80
+ defaultModel: "",
81
+ modelDiscoveryTimeout: 5e3,
82
+ healthCheckTimeout: 2e3,
83
+ cacheTTL: 3e4,
84
+ customProviders: [
85
+ // Example custom provider (commented out in JSON)
86
+ ],
87
+ skipHealthCheck: false,
88
+ offlineMode: false,
89
+ headlessAllowedTools: "Read,Edit,Write,Bash,Glob,Grep"
90
+ };
91
+ writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2), "utf-8");
92
+ return configPath;
93
+ }
94
+ function loadModelCache() {
95
+ const cachePath = path.join(getCacheDir(), "models.json");
96
+ if (!existsSync(cachePath)) {
97
+ return {};
98
+ }
99
+ try {
100
+ const content = readFileSync(cachePath, "utf-8");
101
+ return JSON.parse(content);
102
+ } catch {
103
+ return {};
104
+ }
105
+ }
106
+ function saveModelCache(providerKey, models3) {
107
+ const cache = loadModelCache();
108
+ cache[providerKey] = {
109
+ models: models3,
110
+ timestamp: Date.now()
111
+ };
112
+ const cachePath = path.join(getCacheDir(), "models.json");
113
+ writeFileSync(cachePath, JSON.stringify(cache, null, 2), "utf-8");
114
+ }
115
+ function getCachedModels(providerKey) {
116
+ const cache = loadModelCache();
117
+ const entry = cache[providerKey];
118
+ if (!entry) {
119
+ return null;
120
+ }
121
+ return entry.models;
122
+ }
123
+
124
+ // src/errors.ts
125
+ import chalk from "chalk";
126
+ function classifyError(error) {
127
+ if (error instanceof Error) {
128
+ const msg = error.message.toLowerCase();
129
+ if (msg.includes("econnrefused") || msg.includes("connection refused")) {
130
+ return {
131
+ code: "CONNECTION_REFUSED" /* CONNECTION_REFUSED */,
132
+ message: "Connection refused",
133
+ hint: "Check that the server is running and the URL is correct.",
134
+ cause: error
135
+ };
136
+ }
137
+ if (msg.includes("etimedout") || msg.includes("timeout") || msg.includes("timed out")) {
138
+ return {
139
+ code: "CONNECTION_TIMEOUT" /* CONNECTION_TIMEOUT */,
140
+ message: "Connection timed out",
141
+ hint: "The server took too long to respond. Check network connectivity or increase timeout in config.",
142
+ cause: error
143
+ };
144
+ }
145
+ if (msg.includes("enotfound") || msg.includes("getaddrinfo")) {
146
+ return {
147
+ code: "DNS_RESOLUTION_FAILED" /* DNS_RESOLUTION_FAILED */,
148
+ message: "DNS resolution failed",
149
+ hint: "Could not resolve hostname. Check the URL and your internet connection.",
150
+ cause: error
151
+ };
152
+ }
153
+ if (msg.includes("401") || msg.includes("unauthorized")) {
154
+ return {
155
+ code: "AUTH_INVALID" /* AUTH_INVALID */,
156
+ message: "Authentication failed",
157
+ hint: "Check that your API key is valid and correctly set in .env file.",
158
+ cause: error
159
+ };
160
+ }
161
+ if (msg.includes("403") || msg.includes("forbidden")) {
162
+ return {
163
+ code: "AUTH_INVALID" /* AUTH_INVALID */,
164
+ message: "Access forbidden",
165
+ hint: "Your API key may not have access to this resource.",
166
+ cause: error
167
+ };
168
+ }
169
+ if (msg.includes("model") && (msg.includes("not found") || msg.includes("404"))) {
170
+ return {
171
+ code: "MODEL_NOT_FOUND" /* MODEL_NOT_FOUND */,
172
+ message: "Model not found",
173
+ hint: "The specified model does not exist on this provider. Use --list to see available models.",
174
+ cause: error
175
+ };
176
+ }
177
+ if (msg.includes("enoent") && msg.includes("claude")) {
178
+ return {
179
+ code: "CLAUDE_NOT_FOUND" /* CLAUDE_NOT_FOUND */,
180
+ message: "Claude CLI not found",
181
+ hint: "Install Claude Code CLI: npm install -g @anthropic-ai/claude-code",
182
+ cause: error
183
+ };
184
+ }
185
+ }
186
+ return {
187
+ code: "UNKNOWN" /* UNKNOWN */,
188
+ message: error instanceof Error ? error.message : String(error),
189
+ cause: error instanceof Error ? error : void 0
190
+ };
191
+ }
192
+ function formatError(error) {
193
+ const lines = [];
194
+ lines.push(chalk.red(`Error: ${error.message}`));
195
+ if (error.hint) {
196
+ lines.push(chalk.yellow(`Hint: ${error.hint}`));
197
+ }
198
+ if (process.env.DEBUG && error.cause) {
199
+ lines.push(chalk.gray(`Debug: ${error.cause.stack || error.cause.message}`));
200
+ }
201
+ return lines.join("\n");
202
+ }
203
+ function printError(error) {
204
+ console.error(formatError(error));
205
+ }
206
+ function claudeNotFoundError() {
207
+ return {
208
+ code: "CLAUDE_NOT_FOUND" /* CLAUDE_NOT_FOUND */,
209
+ message: "Claude CLI not found in PATH",
210
+ hint: "Install Claude Code CLI: npm install -g @anthropic-ai/claude-code\nOr check that it is in your PATH."
211
+ };
212
+ }
213
+
214
+ // src/providers.ts
215
+ var builtInProviders = {
216
+ openrouter: {
217
+ name: "OpenRouter",
218
+ key: "openrouter",
219
+ getBaseUrl: () => process.env.ANTHROPIC_BASE_URL || "https://openrouter.ai/api",
220
+ getAuthToken: () => process.env.ANTHROPIC_AUTH_TOKEN || process.env.OPEN_ROUTER_API_KEY || "",
221
+ getDescription: () => {
222
+ const url = process.env.ANTHROPIC_BASE_URL || "https://openrouter.ai/api";
223
+ return `OpenRouter API (${url})`;
224
+ },
225
+ isBuiltIn: true
226
+ },
227
+ "ollama-cloud": {
228
+ name: "Ollama Cloud",
229
+ key: "ollama-cloud",
230
+ getBaseUrl: () => process.env.OLLAMA_HOST || "https://ollama.com",
231
+ getAuthToken: () => process.env.OLLAMA_API_KEY || "",
232
+ getDescription: () => {
233
+ const url = process.env.OLLAMA_HOST || "https://ollama.com";
234
+ return `Ollama Cloud (${url})`;
235
+ },
236
+ isBuiltIn: true
237
+ },
238
+ "ollama-local": {
239
+ name: "Ollama Local",
240
+ key: "ollama-local",
241
+ getBaseUrl: () => process.env.OLLAMA_BASE_URL_LOCAL || "http://localhost:11434",
242
+ getAuthToken: () => "ollama",
243
+ getDescription: () => {
244
+ const url = process.env.OLLAMA_BASE_URL_LOCAL || "http://localhost:11434";
245
+ return `Ollama Local (${url})`;
246
+ },
247
+ isBuiltIn: true
248
+ },
249
+ "ollama-custom": {
250
+ name: "Ollama Custom",
251
+ key: "ollama-custom",
252
+ getBaseUrl: () => process.env.OLLAMA_BASE_URL_CUSTOM || "http://192.168.86.101:11434",
253
+ getAuthToken: () => "ollama",
254
+ getDescription: () => {
255
+ const url = process.env.OLLAMA_BASE_URL_CUSTOM || "http://192.168.86.101:11434";
256
+ return `Ollama Custom (${url})`;
257
+ },
258
+ isBuiltIn: true
259
+ }
260
+ };
261
+ var _providersCache = null;
262
+ function createCustomProvider(custom) {
263
+ return {
264
+ name: custom.name,
265
+ key: custom.key,
266
+ getBaseUrl: () => custom.baseUrl,
267
+ getAuthToken: () => {
268
+ if (custom.authToken) return custom.authToken;
269
+ if (custom.authEnvVar) return process.env[custom.authEnvVar] || "";
270
+ return "";
271
+ },
272
+ getDescription: () => custom.description || `${custom.name} (${custom.baseUrl})`,
273
+ isBuiltIn: false
274
+ };
275
+ }
276
+ function getProviders() {
277
+ if (_providersCache) {
278
+ return _providersCache;
279
+ }
280
+ const config2 = loadConfig();
281
+ const providers3 = { ...builtInProviders };
282
+ if (config2.customProviders) {
283
+ for (const custom of config2.customProviders) {
284
+ if (custom.key && custom.name && custom.baseUrl) {
285
+ providers3[custom.key] = createCustomProvider(custom);
286
+ }
287
+ }
288
+ }
289
+ _providersCache = providers3;
290
+ return providers3;
291
+ }
292
+ var providers = new Proxy({}, {
293
+ get: (_, key) => getProviders()[key],
294
+ ownKeys: () => Object.keys(getProviders()),
295
+ getOwnPropertyDescriptor: (_, key) => ({
296
+ enumerable: true,
297
+ configurable: true,
298
+ value: getProviders()[key]
299
+ })
300
+ });
301
+ var models = {
302
+ openrouter: [
303
+ // Premium / Frontier Models
304
+ { id: "openai/gpt-5.2", name: "GPT-5.2", shortcut: "gpt52" },
305
+ { id: "openai/gpt-5.2-pro", name: "GPT-5.2 Pro", shortcut: "gpt52-pro" },
306
+ { id: "openai/gpt-5.2-codex", name: "GPT-5.2 Codex", shortcut: "gpt52-codex" },
307
+ { id: "anthropic/claude-opus-4.5", name: "Claude Opus 4.5", shortcut: "opus" },
308
+ { id: "x-ai/grok-4.1-fast", name: "Grok 4.1 Fast", shortcut: "grok" },
309
+ // Cost-Effective Performance
310
+ { id: "deepseek/deepseek-v3.2", name: "DeepSeek V3.2", shortcut: "deepseek" },
311
+ { id: "z-ai/glm-4.7-flash", name: "Z.AI GLM 4.7 Flash", shortcut: "zai-glm47-flash" },
312
+ // Existing Models
313
+ { id: "anthropic/claude-sonnet-4.5", name: "Claude Sonnet 4.5", shortcut: "sonnet" },
314
+ { id: "anthropic/claude-haiku-4.5", name: "Claude Haiku 4.5", shortcut: "haiku" },
315
+ { id: "@preset/gpt-oss-120b-cerebras", name: "GPT-OSS 120B (Cerebras)", shortcut: "gpt120" },
316
+ { id: "@preset/cerebras-glm-4-7-cerebras", name: "GLM 4.7 (Cerebras)", shortcut: "glm47" },
317
+ { id: "z-ai/glm-4.7", name: "Z.AI GLM 4.7", shortcut: "zai-glm47" },
318
+ { id: "google/gemini-3-pro-preview", name: "Gemini 3 Pro Preview", shortcut: "gemini-pro" },
319
+ { id: "google/gemini-3-flash-preview", name: "Gemini 3 Flash Preview", shortcut: "gemini-flash" },
320
+ { id: "openrouter/auto", name: "OpenRouter Auto", shortcut: "auto" }
321
+ ]
322
+ // Ollama models are dynamically fetched via API
323
+ };
324
+ var providerAliases = {
325
+ or: "openrouter",
326
+ open: "openrouter",
327
+ oc: "ollama-cloud",
328
+ cloud: "ollama-cloud",
329
+ ol: "ollama-local",
330
+ local: "ollama-local",
331
+ custom: "ollama-custom",
332
+ remote: "ollama-custom"
333
+ };
334
+ function resolveProvider(shortcut) {
335
+ return providerAliases[shortcut] || shortcut;
336
+ }
337
+ function getProviderKeys() {
338
+ return Object.keys(getProviders());
339
+ }
340
+ function getProvider(key) {
341
+ return getProviders()[resolveProvider(key)];
342
+ }
343
+ async function resolveModel(providerKey, shortcut) {
344
+ const resolvedKey = resolveProvider(providerKey);
345
+ if (isOllamaProvider(resolvedKey)) {
346
+ const ollamaModels = await getOllamaModels(resolvedKey);
347
+ const model2 = ollamaModels.find(
348
+ (m) => m.shortcut === shortcut || m.id === shortcut || m.name.toLowerCase() === shortcut.toLowerCase()
349
+ );
350
+ return model2?.id || shortcut;
351
+ }
352
+ const providerModels = models[resolvedKey];
353
+ if (!providerModels) return shortcut;
354
+ const model = providerModels.find(
355
+ (m) => m.shortcut === shortcut || m.id === shortcut || m.name.toLowerCase() === shortcut.toLowerCase()
356
+ );
357
+ return model?.id || shortcut;
358
+ }
359
+ async function getModels(providerKey) {
360
+ const resolvedKey = resolveProvider(providerKey);
361
+ if (isOllamaProvider(resolvedKey)) {
362
+ return getOllamaModels(resolvedKey);
363
+ }
364
+ return models[resolvedKey] || [];
365
+ }
366
+ function isOllamaProvider(providerKey) {
367
+ return providerKey === "ollama-local" || providerKey === "ollama-custom" || providerKey === "ollama-cloud" || providerKey.startsWith("ollama-");
368
+ }
369
+ var _ollamaModelCache = /* @__PURE__ */ new Map();
370
+ async function fetchOllamaModelsFromAPI(baseUrl, timeout) {
371
+ const controller = new AbortController();
372
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
373
+ try {
374
+ const response = await fetch(`${baseUrl}/v1/models`, {
375
+ signal: controller.signal
376
+ });
377
+ clearTimeout(timeoutId);
378
+ if (!response.ok) {
379
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
380
+ }
381
+ const data = await response.json();
382
+ const models3 = [];
383
+ if (data.data && Array.isArray(data.data)) {
384
+ for (const model of data.data) {
385
+ models3.push({
386
+ id: model.id,
387
+ name: model.id,
388
+ shortcut: model.id
389
+ });
390
+ }
391
+ }
392
+ return models3;
393
+ } catch (error) {
394
+ clearTimeout(timeoutId);
395
+ if (error instanceof Error && error.name === "AbortError") {
396
+ throw new Error("Connection timed out");
397
+ }
398
+ throw error;
399
+ }
400
+ }
401
+ async function getOllamaModels(providerKey) {
402
+ const now = Date.now();
403
+ const cacheTTL = getConfig("cacheTTL") || 3e4;
404
+ const cached = _ollamaModelCache.get(providerKey);
405
+ if (cached && now - cached.timestamp < cacheTTL) {
406
+ return cached.models;
407
+ }
408
+ const allProviders = getProviders();
409
+ const provider = allProviders[providerKey];
410
+ if (!provider) return [];
411
+ const timeout = getConfig("modelDiscoveryTimeout") || 5e3;
412
+ try {
413
+ const models3 = await fetchOllamaModelsFromAPI(provider.getBaseUrl(), timeout);
414
+ _ollamaModelCache.set(providerKey, { models: models3, timestamp: now });
415
+ saveModelCache(providerKey, models3);
416
+ return models3;
417
+ } catch (error) {
418
+ const diskCached = getCachedModels(providerKey);
419
+ if (diskCached && diskCached.length > 0) {
420
+ console.warn(`Warning: Using cached models for ${providerKey} (API unreachable)`);
421
+ return diskCached;
422
+ }
423
+ const classified = classifyError(error);
424
+ if (classified.code !== "UNKNOWN" /* UNKNOWN */) {
425
+ console.error(`Failed to fetch models from ${providerKey}: ${classified.message}`);
426
+ if (classified.hint) {
427
+ console.error(`Hint: ${classified.hint}`);
428
+ }
429
+ }
430
+ return [];
431
+ }
432
+ }
433
+ async function checkProviderHealth(providerKey) {
434
+ const allProviders = getProviders();
435
+ const provider = allProviders[providerKey];
436
+ if (!provider) {
437
+ return {
438
+ provider: providerKey,
439
+ healthy: false,
440
+ error: {
441
+ code: "PROVIDER_NOT_FOUND" /* PROVIDER_NOT_FOUND */,
442
+ message: `Provider not found: ${providerKey}`
443
+ }
444
+ };
445
+ }
446
+ const timeout = getConfig("healthCheckTimeout") || 2e3;
447
+ const baseUrl = provider.getBaseUrl();
448
+ const startTime = Date.now();
449
+ const controller = new AbortController();
450
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
451
+ try {
452
+ let healthUrl = `${baseUrl}/v1/models`;
453
+ if (providerKey === "openrouter") {
454
+ healthUrl = `${baseUrl}/v1/models`;
455
+ }
456
+ const response = await fetch(healthUrl, {
457
+ signal: controller.signal,
458
+ headers: provider.getAuthToken() ? {
459
+ "Authorization": `Bearer ${provider.getAuthToken()}`
460
+ } : {}
461
+ });
462
+ clearTimeout(timeoutId);
463
+ const latencyMs = Date.now() - startTime;
464
+ if (response.ok) {
465
+ return {
466
+ provider: providerKey,
467
+ healthy: true,
468
+ latencyMs
469
+ };
470
+ }
471
+ if (response.status === 401 || response.status === 403) {
472
+ return {
473
+ provider: providerKey,
474
+ healthy: false,
475
+ latencyMs,
476
+ error: {
477
+ code: "AUTH_INVALID" /* AUTH_INVALID */,
478
+ message: `Authentication failed (${response.status})`,
479
+ hint: "Check your API key configuration."
480
+ }
481
+ };
482
+ }
483
+ return {
484
+ provider: providerKey,
485
+ healthy: false,
486
+ latencyMs,
487
+ error: {
488
+ code: "PROVIDER_UNAVAILABLE" /* PROVIDER_UNAVAILABLE */,
489
+ message: `HTTP ${response.status}: ${response.statusText}`
490
+ }
491
+ };
492
+ } catch (error) {
493
+ clearTimeout(timeoutId);
494
+ const latencyMs = Date.now() - startTime;
495
+ return {
496
+ provider: providerKey,
497
+ healthy: false,
498
+ latencyMs,
499
+ error: classifyError(error)
500
+ };
501
+ }
502
+ }
503
+ async function checkAllProvidersHealth() {
504
+ const providerKeys = getProviderKeys();
505
+ const results = await Promise.all(
506
+ providerKeys.map((key) => checkProviderHealth(key))
507
+ );
508
+ return results;
509
+ }
510
+
511
+ // src/index.ts
512
+ config();
513
+ var __filename = fileURLToPath(import.meta.url);
514
+ var __dirname = path2.dirname(__filename);
515
+ config({ path: path2.join(__dirname, "..", ".env") });
516
+ var program = new Command();
517
+ function printHeader(text) {
518
+ console.log("");
519
+ console.log(chalk2.bold.blue("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
520
+ console.log(chalk2.bold.blue("\u2551") + " " + chalk2.cyan(text));
521
+ console.log(chalk2.bold.blue("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
522
+ console.log("");
523
+ }
524
+ function printConfig(provider, modelId, mode) {
525
+ console.log(chalk2.cyan("Provider:") + " " + provider.name);
526
+ console.log(chalk2.cyan("Model:") + " " + modelId);
527
+ console.log(chalk2.cyan("Base URL:") + " " + provider.getBaseUrl());
528
+ console.log(chalk2.cyan("Mode:") + " " + mode);
529
+ console.log("");
530
+ }
531
+ function isClaudeInstalled() {
532
+ try {
533
+ execSync("which claude", { stdio: "ignore" });
534
+ return true;
535
+ } catch {
536
+ try {
537
+ execSync("where claude", { stdio: "ignore" });
538
+ return true;
539
+ } catch {
540
+ return false;
541
+ }
542
+ }
543
+ }
544
+ function validateClaudeCLI() {
545
+ if (!isClaudeInstalled()) {
546
+ printError(claudeNotFoundError());
547
+ process.exit(1);
548
+ }
549
+ }
550
+ async function runClaude(provider, modelId, headless, skipPermissions, prompt) {
551
+ validateClaudeCLI();
552
+ const baseUrl = provider.getBaseUrl();
553
+ process.env.ANTHROPIC_BASE_URL = baseUrl;
554
+ process.env.ANTHROPIC_AUTH_TOKEN = provider.getAuthToken();
555
+ process.env.ANTHROPIC_API_KEY = "";
556
+ process.env.ANTHROPIC_DEFAULT_OPUS_MODEL = modelId;
557
+ process.env.ANTHROPIC_DEFAULT_SONNET_MODEL = modelId;
558
+ process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL = modelId;
559
+ process.env.CLAUDE_CODE_SUBAGENT_MODEL = modelId;
560
+ const args = ["--model", modelId];
561
+ if (skipPermissions) {
562
+ args.push("--dangerously-skip-permissions");
563
+ }
564
+ if (headless && prompt) {
565
+ args.push("-p", prompt);
566
+ const allowedTools = getConfig("headlessAllowedTools");
567
+ if (allowedTools) {
568
+ args.push("--allowedTools", allowedTools);
569
+ }
570
+ }
571
+ const claude = spawn("claude", args, {
572
+ stdio: "inherit",
573
+ env: {
574
+ ...process.env,
575
+ ANTHROPIC_BASE_URL: baseUrl,
576
+ ANTHROPIC_AUTH_TOKEN: provider.getAuthToken(),
577
+ ANTHROPIC_API_KEY: "",
578
+ ANTHROPIC_DEFAULT_OPUS_MODEL: modelId,
579
+ ANTHROPIC_DEFAULT_SONNET_MODEL: modelId,
580
+ ANTHROPIC_DEFAULT_HAIKU_MODEL: modelId,
581
+ CLAUDE_CODE_SUBAGENT_MODEL: modelId
582
+ }
583
+ });
584
+ return new Promise((resolve, reject) => {
585
+ claude.on("close", (code) => {
586
+ if (code === 0) {
587
+ resolve();
588
+ } else {
589
+ reject(new Error(`Claude exited with code ${code}`));
590
+ }
591
+ });
592
+ claude.on("error", (err) => {
593
+ const classified = classifyError(err);
594
+ printError(classified);
595
+ reject(err);
596
+ });
597
+ });
598
+ }
599
+ function formatHealthStatus(result) {
600
+ const allProviders = getProviders();
601
+ const provider = allProviders[result.provider];
602
+ const name = provider?.name || result.provider;
603
+ if (result.healthy) {
604
+ const latency = result.latencyMs ? ` (${result.latencyMs}ms)` : "";
605
+ return `${chalk2.green("\u2713")} ${name}${chalk2.gray(latency)}`;
606
+ } else {
607
+ const error = result.error?.message || "unavailable";
608
+ return `${chalk2.red("\u2717")} ${name} ${chalk2.gray(`- ${error}`)}`;
609
+ }
610
+ }
611
+ async function displayHealthCheck() {
612
+ const skipHealthCheck = getConfig("skipHealthCheck");
613
+ if (skipHealthCheck) return;
614
+ console.log(chalk2.gray("Checking provider availability..."));
615
+ const results = await checkAllProvidersHealth();
616
+ console.log("");
617
+ for (const result of results) {
618
+ console.log(" " + formatHealthStatus(result));
619
+ }
620
+ console.log("");
621
+ }
622
+ async function interactiveMode(skipPermissions) {
623
+ console.clear();
624
+ printHeader("Claude Mode");
625
+ await displayHealthCheck();
626
+ const allProviders = getProviders();
627
+ const providerKey = await select({
628
+ message: "Select Provider:",
629
+ choices: getProviderKeys().map((key) => ({
630
+ name: allProviders[key].name,
631
+ value: key,
632
+ description: allProviders[key].getDescription()
633
+ }))
634
+ });
635
+ const provider = getProvider(providerKey);
636
+ console.log(chalk2.yellow("\u2192 Selected:") + " " + provider.name);
637
+ const providerModels = await getModels(providerKey);
638
+ if (providerModels.length === 0) {
639
+ console.log(chalk2.yellow("No models available for this provider."));
640
+ console.log(chalk2.gray("You can enter a model ID manually."));
641
+ const modelId2 = await input({
642
+ message: "Enter model ID:",
643
+ validate: (value) => value.length > 0 ? true : "Model ID cannot be empty"
644
+ });
645
+ console.log(chalk2.yellow("\u2192 Model:") + " " + modelId2);
646
+ await continueInteractiveMode(provider, modelId2, skipPermissions);
647
+ return;
648
+ }
649
+ const modelId = await select({
650
+ message: "Select Model:",
651
+ choices: providerModels.map((model) => ({
652
+ name: model.name,
653
+ value: model.id,
654
+ description: model.shortcut
655
+ }))
656
+ });
657
+ const selectedModel = providerModels.find((m) => m.id === modelId);
658
+ console.log(chalk2.yellow("\u2192 Selected:") + " " + selectedModel?.name + ` (${modelId})`);
659
+ await continueInteractiveMode(provider, modelId, skipPermissions);
660
+ }
661
+ async function continueInteractiveMode(provider, modelId, skipPermissions) {
662
+ const mode = await select({
663
+ message: "Select Mode:",
664
+ choices: [
665
+ { name: "Terminal (interactive)", value: "terminal" },
666
+ { name: "Headless (single prompt)", value: "headless" }
667
+ ]
668
+ });
669
+ let prompt;
670
+ if (mode === "headless") {
671
+ prompt = await input({
672
+ message: "Enter your prompt:",
673
+ validate: (value) => value.length > 0 ? true : "Prompt cannot be empty"
674
+ });
675
+ }
676
+ if (!skipPermissions) {
677
+ skipPermissions = await select({
678
+ message: "Skip permission prompts when executing commands?",
679
+ choices: [
680
+ { name: "No (ask for permission)", value: false },
681
+ { name: "Yes (skip all prompts)", value: true }
682
+ ]
683
+ });
684
+ }
685
+ console.log(
686
+ chalk2.yellow("\u2192 Skip permissions:") + " " + (skipPermissions ? chalk2.red("Yes (\u26A0\uFE0F auto-approve)") : chalk2.green("No (ask)"))
687
+ );
688
+ const isHeadless = mode === "headless";
689
+ if (isHeadless) {
690
+ printHeader("Executing Claude Code (Headless)");
691
+ } else {
692
+ printHeader("Launching Claude Code (Interactive)");
693
+ }
694
+ printConfig(provider, modelId, isHeadless ? "Headless" : "Interactive");
695
+ try {
696
+ await runClaude(provider, modelId, isHeadless, skipPermissions, prompt);
697
+ } catch (error) {
698
+ console.error(chalk2.red("Error:"), error);
699
+ process.exit(1);
700
+ }
701
+ }
702
+ async function quickMode(providerArg, modelArg, skipPermissions, modeArg, promptArg) {
703
+ const providerKey = resolveProvider(providerArg);
704
+ const provider = getProvider(providerKey);
705
+ if (!provider) {
706
+ console.error(chalk2.red(`Unknown provider: ${providerArg}`));
707
+ console.log(chalk2.gray("Available providers: " + getProviderKeys().join(", ")));
708
+ process.exit(1);
709
+ }
710
+ const modelId = await resolveModel(providerKey, modelArg);
711
+ let isHeadless = false;
712
+ let prompt = promptArg;
713
+ if (modeArg) {
714
+ const modeAliases = {
715
+ terminal: false,
716
+ t: false,
717
+ interactive: false,
718
+ i: false,
719
+ headless: true,
720
+ h: true,
721
+ prompt: true,
722
+ p: true
723
+ };
724
+ if (modeArg in modeAliases) {
725
+ isHeadless = modeAliases[modeArg];
726
+ } else {
727
+ isHeadless = true;
728
+ prompt = modeArg;
729
+ }
730
+ }
731
+ if (isHeadless && !prompt) {
732
+ prompt = await input({
733
+ message: "Enter your prompt:",
734
+ validate: (value) => value.length > 0 ? true : "Prompt cannot be empty"
735
+ });
736
+ }
737
+ printConfig(provider, modelId, isHeadless ? "Headless" : "Interactive");
738
+ try {
739
+ await runClaude(provider, modelId, isHeadless, skipPermissions, prompt);
740
+ } catch (error) {
741
+ console.error(chalk2.red("Error:"), error);
742
+ process.exit(1);
743
+ }
744
+ }
745
+ async function listModels() {
746
+ console.log(chalk2.bold("\nAvailable Models by Provider:\n"));
747
+ const allProviders = getProviders();
748
+ for (const providerKey of getProviderKeys()) {
749
+ const provider = allProviders[providerKey];
750
+ const providerModels = await getModels(providerKey);
751
+ console.log(chalk2.cyan(`${provider.name}:`));
752
+ console.log(chalk2.gray(` ${provider.getDescription()}`));
753
+ if (providerModels.length === 0) {
754
+ console.log(` ${chalk2.gray("(no models found)")}`);
755
+ } else {
756
+ for (const model of providerModels) {
757
+ console.log(` ${chalk2.green(model.shortcut.padEnd(14))} \u2192 ${model.id}`);
758
+ }
759
+ }
760
+ console.log("");
761
+ }
762
+ }
763
+ function showConfig() {
764
+ const config2 = loadConfig();
765
+ console.log(chalk2.bold("\nCurrent Configuration:\n"));
766
+ console.log(JSON.stringify(config2, null, 2));
767
+ console.log("");
768
+ }
769
+ function initConfigCommand() {
770
+ const configPath = initConfig();
771
+ console.log(chalk2.green(`Config file created at: ${configPath}`));
772
+ console.log(chalk2.gray("Edit this file to customize your settings."));
773
+ }
774
+ function generateBashCompletion() {
775
+ const providerKeys = getProviderKeys();
776
+ const providerAliases2 = ["or", "open", "oc", "cloud", "ol", "local", "custom", "remote"];
777
+ const allProviders = [...providerKeys, ...providerAliases2].join(" ");
778
+ const modeOptions = "terminal t interactive i headless h prompt p";
779
+ return `# claude-mode bash completion
780
+ # Add to ~/.bashrc or ~/.bash_profile:
781
+ # eval "$(claude-mode completion bash)"
782
+
783
+ _claude_mode_completions() {
784
+ local cur prev
785
+ COMPREPLY=()
786
+ cur="\${COMP_WORDS[COMP_CWORD]}"
787
+ prev="\${COMP_WORDS[COMP_CWORD-1]}"
788
+
789
+ # First argument: provider or option
790
+ if [[ \${COMP_CWORD} -eq 1 ]]; then
791
+ COMPREPLY=( $(compgen -W "${allProviders} --list --help --prompt -l -p" -- "\${cur}") )
792
+ return 0
793
+ fi
794
+
795
+ # Second argument: model (depends on provider, show generic)
796
+ if [[ \${COMP_CWORD} -eq 2 ]]; then
797
+ # For now, just show common shortcuts
798
+ COMPREPLY=( $(compgen -W "sonnet haiku opus gpt120 glm47 deepseek" -- "\${cur}") )
799
+ return 0
800
+ fi
801
+
802
+ # Third argument: mode
803
+ if [[ \${COMP_CWORD} -eq 3 ]]; then
804
+ COMPREPLY=( $(compgen -W "${modeOptions}" -- "\${cur}") )
805
+ return 0
806
+ fi
807
+ }
808
+
809
+ complete -F _claude_mode_completions claude-mode
810
+ `;
811
+ }
812
+ function generateZshCompletion() {
813
+ const providerKeys = getProviderKeys();
814
+ return `#compdef claude-mode
815
+ # claude-mode zsh completion
816
+ # Add to ~/.zshrc:
817
+ # eval "$(claude-mode completion zsh)"
818
+
819
+ _claude_mode() {
820
+ local -a providers modes
821
+
822
+ providers=(
823
+ 'openrouter:OpenRouter API'
824
+ 'ollama-cloud:Ollama Cloud'
825
+ 'ollama-local:Ollama Local'
826
+ 'ollama-custom:Ollama Custom'
827
+ 'or:OpenRouter (alias)'
828
+ 'oc:Ollama Cloud (alias)'
829
+ 'ol:Ollama Local (alias)'
830
+ 'custom:Ollama Custom (alias)'
831
+ )
832
+
833
+ modes=(
834
+ 'terminal:Interactive terminal mode'
835
+ 't:Interactive terminal mode (alias)'
836
+ 'interactive:Interactive terminal mode (alias)'
837
+ 'i:Interactive terminal mode (alias)'
838
+ 'headless:Single prompt execution'
839
+ 'h:Single prompt execution (alias)'
840
+ 'prompt:Single prompt execution (alias)'
841
+ 'p:Single prompt execution (alias)'
842
+ )
843
+
844
+ case $CURRENT in
845
+ 2)
846
+ _describe 'provider' providers
847
+ _arguments '--list[List all models]' '--help[Show help]' '--prompt[Headless mode with prompt]' '-l[List all models]' '-p[Headless mode with prompt]'
848
+ ;;
849
+ 3)
850
+ _message 'model shortcut or ID'
851
+ ;;
852
+ 4)
853
+ _describe 'mode' modes
854
+ ;;
855
+ 5)
856
+ _message 'prompt (for headless mode)'
857
+ ;;
858
+ esac
859
+ }
860
+
861
+ _claude_mode "$@"
862
+ `;
863
+ }
864
+ function generateFishCompletion() {
865
+ return `# claude-mode fish completion
866
+ # Add to ~/.config/fish/completions/claude-mode.fish
867
+
868
+ # Providers
869
+ complete -c claude-mode -n "__fish_is_first_arg" -a "openrouter" -d "OpenRouter API"
870
+ complete -c claude-mode -n "__fish_is_first_arg" -a "ollama-cloud" -d "Ollama Cloud"
871
+ complete -c claude-mode -n "__fish_is_first_arg" -a "ollama-local" -d "Ollama Local"
872
+ complete -c claude-mode -n "__fish_is_first_arg" -a "ollama-custom" -d "Ollama Custom"
873
+ complete -c claude-mode -n "__fish_is_first_arg" -a "or" -d "OpenRouter (alias)"
874
+ complete -c claude-mode -n "__fish_is_first_arg" -a "oc" -d "Ollama Cloud (alias)"
875
+ complete -c claude-mode -n "__fish_is_first_arg" -a "ol" -d "Ollama Local (alias)"
876
+ complete -c claude-mode -n "__fish_is_first_arg" -a "custom" -d "Ollama Custom (alias)"
877
+
878
+ # Options
879
+ complete -c claude-mode -s l -l list -d "List all models"
880
+ complete -c claude-mode -s p -l prompt -d "Headless mode with prompt"
881
+ complete -c claude-mode -s d -l dangerously-skip-permissions -d "Skip permission prompts"
882
+
883
+ # Modes (third argument)
884
+ complete -c claude-mode -n "__fish_seen_argument" -a "terminal t interactive i" -d "Interactive mode"
885
+ complete -c claude-mode -n "__fish_seen_argument" -a "headless h prompt p" -d "Headless mode"
886
+ `;
887
+ }
888
+ function printCompletion(shell) {
889
+ switch (shell) {
890
+ case "bash":
891
+ console.log(generateBashCompletion());
892
+ break;
893
+ case "zsh":
894
+ console.log(generateZshCompletion());
895
+ break;
896
+ case "fish":
897
+ console.log(generateFishCompletion());
898
+ break;
899
+ default:
900
+ console.error(chalk2.red(`Unknown shell: ${shell}`));
901
+ console.log(chalk2.gray("Supported shells: bash, zsh, fish"));
902
+ process.exit(1);
903
+ }
904
+ }
905
+ async function healthCommand() {
906
+ console.log(chalk2.bold("\nProvider Health Check:\n"));
907
+ const results = await checkAllProvidersHealth();
908
+ for (const result of results) {
909
+ console.log(" " + formatHealthStatus(result));
910
+ }
911
+ console.log("");
912
+ const healthy = results.filter((r) => r.healthy).length;
913
+ const total = results.length;
914
+ if (healthy === total) {
915
+ console.log(chalk2.green(`All ${total} providers are healthy.`));
916
+ } else {
917
+ console.log(chalk2.yellow(`${healthy}/${total} providers are healthy.`));
918
+ }
919
+ }
920
+ program.name("claude-mode").description(
921
+ `
922
+ ${chalk2.bold("Claude Mode")} - Launch Claude Code with different providers
923
+
924
+ ${chalk2.bold("Providers:")}
925
+ openrouter OpenRouter API
926
+ ollama-cloud Ollama Cloud (OLLAMA_HOST)
927
+ ollama-local Ollama Local (OLLAMA_BASE_URL_LOCAL)
928
+ ollama-custom Ollama Custom (OLLAMA_BASE_URL_CUSTOM)
929
+
930
+ ${chalk2.bold("Model shortcuts:")}
931
+ OpenRouter (Premium): gpt52, gpt52-pro, gpt52-codex, opus, grok
932
+ OpenRouter (Value): deepseek, zai-glm47-flash, seed16, sonnet, haiku
933
+ OpenRouter (Existing): gpt120, glm47, gemini-pro, gemini-flash
934
+ Ollama: Models discovered dynamically via API
935
+
936
+ ${chalk2.bold("Mode shortcuts:")}
937
+ terminal, t, interactive, i - Interactive terminal mode
938
+ headless, h, prompt, p - Headless mode with prompt
939
+
940
+ ${chalk2.bold("Options:")}
941
+ -p, --prompt <prompt> Headless mode (uses defaults if provider/model not set)
942
+ -d, --dangerously-skip-permissions Skip permission prompts (\u26A0\uFE0F use with caution)
943
+
944
+ ${chalk2.bold("Default Provider/Model:")}
945
+ Set in ~/.claude-mode/claude-mode.json:
946
+ { "defaultProvider": "openrouter", "defaultModel": "sonnet" }
947
+
948
+ ${chalk2.bold("Examples:")}
949
+ claude-mode Interactive menu
950
+ claude-mode -p "perform a code review" Headless with default provider/model
951
+ claude-mode openrouter sonnet Interactive with Claude Sonnet
952
+ claude-mode or sonnet -p "list files" Headless with specified provider/model
953
+ claude-mode ollama-local qwen3 h "list files" Headless with local Qwen3 (legacy syntax)
954
+ claude-mode --dangerously-skip-permissions Skip all permission prompts
955
+ claude-mode --list List all available models
956
+ claude-mode health Check provider availability
957
+ claude-mode config init Create config file
958
+ claude-mode completion bash Generate shell completions
959
+ `
960
+ ).version("1.2.0");
961
+ program.command("list").description("List all available providers and models").action(async () => {
962
+ await listModels();
963
+ });
964
+ program.command("health").description("Check health/availability of all providers").action(async () => {
965
+ await healthCommand();
966
+ });
967
+ program.command("config").description("Manage configuration").argument("[action]", "Action: show, init").action((action) => {
968
+ switch (action) {
969
+ case "init":
970
+ initConfigCommand();
971
+ break;
972
+ case "show":
973
+ default:
974
+ showConfig();
975
+ break;
976
+ }
977
+ });
978
+ program.command("completion").description("Generate shell completion scripts").argument("<shell>", "Shell type: bash, zsh, fish").action((shell) => {
979
+ printCompletion(shell);
980
+ });
981
+ program.option("-d, --dangerously-skip-permissions", "Skip permission prompts when executing commands").option("-l, --list", "List all available providers and models").option("-p, --prompt <prompt>", "Run in headless mode with the given prompt (uses defaults if provider/model not specified)").argument("[provider]", "Provider (openrouter, ollama-cloud, ollama-local, ollama-custom)").argument("[model]", "Model ID or shortcut").argument("[mode]", "Mode (terminal/t, headless/h) or prompt for headless").argument("[promptArg]", "Prompt for headless mode").action(async (provider, model, mode, promptArg, options) => {
982
+ if (options.list) {
983
+ await listModels();
984
+ return;
985
+ }
986
+ const skipPermissions = options.dangerouslySkipPermissions || false;
987
+ if (options.prompt) {
988
+ const defaultProvider = getConfig("defaultProvider");
989
+ const defaultModel = getConfig("defaultModel");
990
+ const effectiveProvider = provider || defaultProvider;
991
+ const effectiveModel = model || defaultModel;
992
+ if (!effectiveProvider || !effectiveModel) {
993
+ console.error(chalk2.red("Error: Provider and model are required"));
994
+ if (!effectiveProvider && !effectiveModel) {
995
+ console.log(chalk2.gray("Set defaults in config: claude-mode config init"));
996
+ console.log(chalk2.gray("Then edit ~/.claude-mode/claude-mode.json to set defaultProvider and defaultModel"));
997
+ } else if (!effectiveProvider) {
998
+ console.log(chalk2.gray("Missing: provider (set defaultProvider in config or pass as argument)"));
999
+ } else {
1000
+ console.log(chalk2.gray("Missing: model (set defaultModel in config or pass as argument)"));
1001
+ }
1002
+ process.exit(1);
1003
+ }
1004
+ await quickMode(effectiveProvider, effectiveModel, skipPermissions, "headless", options.prompt);
1005
+ return;
1006
+ }
1007
+ if (!provider) {
1008
+ await interactiveMode(skipPermissions);
1009
+ } else if (!model) {
1010
+ const defaultModel = getConfig("defaultModel");
1011
+ if (defaultModel) {
1012
+ await quickMode(provider, defaultModel, skipPermissions, mode, promptArg);
1013
+ } else {
1014
+ console.error(chalk2.red("Error: Model is required when provider is specified"));
1015
+ console.log(chalk2.gray("Usage: claude-mode <provider> <model> [mode] [prompt]"));
1016
+ console.log(chalk2.gray("Or set defaultModel in ~/.claude-mode/claude-mode.json"));
1017
+ console.log(chalk2.gray("Run claude-mode --list to see available models"));
1018
+ process.exit(1);
1019
+ }
1020
+ } else {
1021
+ await quickMode(provider, model, skipPermissions, mode, promptArg);
1022
+ }
1023
+ });
1024
+ program.parse();