claude-ai-switcher 1.1.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.
Files changed (83) hide show
  1. package/AGENTS.md +265 -0
  2. package/ARCHITECTURE.md +162 -0
  3. package/CLAUDE.md +267 -0
  4. package/LICENSE +21 -0
  5. package/QWEN.md +429 -0
  6. package/README.md +833 -0
  7. package/dist/clients/claude-code.d.ts +92 -0
  8. package/dist/clients/claude-code.d.ts.map +1 -0
  9. package/dist/clients/claude-code.js +312 -0
  10. package/dist/clients/claude-code.js.map +1 -0
  11. package/dist/clients/opencode.d.ts +71 -0
  12. package/dist/clients/opencode.d.ts.map +1 -0
  13. package/dist/clients/opencode.js +604 -0
  14. package/dist/clients/opencode.js.map +1 -0
  15. package/dist/config.d.ts +37 -0
  16. package/dist/config.d.ts.map +1 -0
  17. package/dist/config.js +122 -0
  18. package/dist/config.js.map +1 -0
  19. package/dist/display.d.ts +51 -0
  20. package/dist/display.d.ts.map +1 -0
  21. package/dist/display.js +118 -0
  22. package/dist/display.js.map +1 -0
  23. package/dist/hooks/index.d.ts +60 -0
  24. package/dist/hooks/index.d.ts.map +1 -0
  25. package/dist/hooks/index.js +223 -0
  26. package/dist/hooks/index.js.map +1 -0
  27. package/dist/hooks/token-tracker.js +280 -0
  28. package/dist/hooks/visual-enhancements.js +364 -0
  29. package/dist/index.d.ts +9 -0
  30. package/dist/index.d.ts.map +1 -0
  31. package/dist/index.js +1091 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/models.d.ts +34 -0
  34. package/dist/models.d.ts.map +1 -0
  35. package/dist/models.js +343 -0
  36. package/dist/models.js.map +1 -0
  37. package/dist/providers/alibaba.d.ts +25 -0
  38. package/dist/providers/alibaba.d.ts.map +1 -0
  39. package/dist/providers/alibaba.js +37 -0
  40. package/dist/providers/alibaba.js.map +1 -0
  41. package/dist/providers/anthropic.d.ts +14 -0
  42. package/dist/providers/anthropic.d.ts.map +1 -0
  43. package/dist/providers/anthropic.js +19 -0
  44. package/dist/providers/anthropic.js.map +1 -0
  45. package/dist/providers/gemini.d.ts +44 -0
  46. package/dist/providers/gemini.d.ts.map +1 -0
  47. package/dist/providers/gemini.js +156 -0
  48. package/dist/providers/gemini.js.map +1 -0
  49. package/dist/providers/glm.d.ts +25 -0
  50. package/dist/providers/glm.d.ts.map +1 -0
  51. package/dist/providers/glm.js +89 -0
  52. package/dist/providers/glm.js.map +1 -0
  53. package/dist/providers/ollama.d.ts +48 -0
  54. package/dist/providers/ollama.d.ts.map +1 -0
  55. package/dist/providers/ollama.js +174 -0
  56. package/dist/providers/ollama.js.map +1 -0
  57. package/dist/providers/openrouter.d.ts +24 -0
  58. package/dist/providers/openrouter.d.ts.map +1 -0
  59. package/dist/providers/openrouter.js +36 -0
  60. package/dist/providers/openrouter.js.map +1 -0
  61. package/dist/verify.d.ts +24 -0
  62. package/dist/verify.d.ts.map +1 -0
  63. package/dist/verify.js +262 -0
  64. package/dist/verify.js.map +1 -0
  65. package/package.json +57 -0
  66. package/scripts/copy-hooks.js +15 -0
  67. package/src/clients/claude-code.ts +340 -0
  68. package/src/clients/opencode.ts +618 -0
  69. package/src/config.ts +101 -0
  70. package/src/display.ts +151 -0
  71. package/src/hooks/index.ts +208 -0
  72. package/src/hooks/token-tracker.js +280 -0
  73. package/src/hooks/visual-enhancements.js +364 -0
  74. package/src/index.ts +1263 -0
  75. package/src/models.ts +366 -0
  76. package/src/providers/alibaba.ts +43 -0
  77. package/src/providers/anthropic.ts +23 -0
  78. package/src/providers/gemini.ts +136 -0
  79. package/src/providers/glm.ts +60 -0
  80. package/src/providers/ollama.ts +146 -0
  81. package/src/providers/openrouter.ts +42 -0
  82. package/src/verify.ts +258 -0
  83. package/tsconfig.json +19 -0
package/src/index.ts ADDED
@@ -0,0 +1,1263 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Claude AI Switcher
5
+ *
6
+ * Switch between AI providers (Anthropic, GLM, Alibaba Qwen) for Claude Code.
7
+ * Also provides helper commands to add/remove Alibaba Coding Plan provider for OpenCode.
8
+ */
9
+
10
+ import { Command } from "commander";
11
+ import chalk from "chalk";
12
+ import * as readline from "readline";
13
+ import * as fs from "fs-extra";
14
+ import * as path from "path";
15
+
16
+ import {
17
+ providers,
18
+ getModels,
19
+ formatContext,
20
+ ModelTierMap,
21
+ GLM_DEFAULT_TIER_MAP,
22
+ OPENROUTER_DEFAULT_TIER_MAP,
23
+ OLLAMA_DEFAULT_TIER_MAP,
24
+ GEMINI_DEFAULT_TIER_MAP,
25
+ getAlibabaTierMap
26
+ } from "./models";
27
+ import {
28
+ configureAnthropic as configureClaudeAnthropic,
29
+ configureAlibaba as configureClaudeAlibaba,
30
+ configureGLM as configureClaudeGLM,
31
+ configureOpenRouter as configureClaudeOpenRouter,
32
+ configureOllama as configureClaudeOllama,
33
+ configureGemini as configureClaudeGemini,
34
+ getCurrentProvider as getClaudeProvider,
35
+ readClaudeSettings as readClaudeSettings,
36
+ claudeSettingsExists
37
+ } from "./clients/claude-code";
38
+ import {
39
+ configureAlibaba as configureOpenCodeAlibaba,
40
+ configureOpenRouter as configureOpenCodeOpenRouter,
41
+ configureOllama as configureOpenCodeOllama,
42
+ configureGemini as configureOpenCodeGemini,
43
+ configureGLM as configureOpenCodeGLM,
44
+ getCurrentProvider as getOpenCodeProvider,
45
+ opencodeSettingsExists
46
+ } from "./clients/opencode";
47
+ import { getApiKey, setApiKey, hasApiKey } from "./config";
48
+ import {
49
+ displayModels,
50
+ displaySuccess,
51
+ displayError,
52
+ displayWarning,
53
+ displayProviders
54
+ } from "./display";
55
+ import { reloadGLMConfig, isCodingHelperInstalled } from "./providers/glm";
56
+ import {
57
+ isLitellmInstalled as isLitellmInstalledForOllama,
58
+ isOllamaInstalled,
59
+ isOllamaRunning,
60
+ startLitellmProxy,
61
+ getOllamaConfig,
62
+ findModel as findOllamaModel
63
+ } from "./providers/ollama";
64
+ import {
65
+ isLitellmInstalled as isLitellmInstalledForGemini,
66
+ isGeminiKeyValid,
67
+ startGeminiLitellmProxy,
68
+ getGeminiConfig,
69
+ findModel as findGeminiModel
70
+ } from "./providers/gemini";
71
+ import { verifyAllKeys, maskKey } from "./verify";
72
+ import {
73
+ installAllHooks,
74
+ installTokenTracker,
75
+ installVisualEnhancements,
76
+ removeTokenTracker,
77
+ removeVisualEnhancements,
78
+ removeAllHooks,
79
+ areHooksInstalled,
80
+ showTokenStatus,
81
+ showVisualStatus,
82
+ resetTokenUsage
83
+ } from "./hooks/index";
84
+
85
+ // Read version from package.json at runtime so `claude-switch --version` never drifts.
86
+ // package.json lives outside src/rootDir, so resolve it relative to this compiled file.
87
+ const pkgVersion = (fs.readJsonSync(path.join(__dirname, "..", "package.json")) as { version: string }).version;
88
+
89
+ const program = new Command();
90
+
91
+ program
92
+ .name("claude-switch")
93
+ .description("Switch between AI providers for Claude Code. Also provides OpenCode helper commands.")
94
+ .version(pkgVersion);
95
+
96
+ // ---------------------------------------------------------------------------
97
+ // Helpers
98
+ // ---------------------------------------------------------------------------
99
+
100
+ async function promptApiKey(provider: string, helpUrl: string): Promise<string> {
101
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
102
+
103
+ console.log(chalk.yellow(`\n⚠ ${provider} API Key not found`));
104
+ console.log(chalk.dim(` Get your API key from: ${helpUrl}`));
105
+ console.log();
106
+
107
+ const answer = await new Promise<string>((resolve) => {
108
+ rl.question(`Enter your ${provider} API Key: `, resolve);
109
+ });
110
+ rl.close();
111
+
112
+ if (!answer.trim()) {
113
+ displayError("API Key is required");
114
+ process.exit(1);
115
+ }
116
+
117
+ return answer.trim();
118
+ }
119
+
120
+ function buildTierMap(
121
+ defaultMap: ModelTierMap,
122
+ opts: { opus?: string; sonnet?: string; haiku?: string }
123
+ ): ModelTierMap {
124
+ return {
125
+ opus: opts.opus || defaultMap.opus,
126
+ sonnet: opts.sonnet || defaultMap.sonnet,
127
+ haiku: opts.haiku || defaultMap.haiku
128
+ };
129
+ }
130
+
131
+ function displayTierMap(tierMap: ModelTierMap): void {
132
+ console.log(chalk.dim(" Claude model aliases:"));
133
+ console.log(chalk.dim(` ANTHROPIC_DEFAULT_OPUS_MODEL → ${tierMap.opus}`));
134
+ console.log(chalk.dim(` ANTHROPIC_DEFAULT_SONNET_MODEL → ${tierMap.sonnet}`));
135
+ console.log(chalk.dim(` ANTHROPIC_DEFAULT_HAIKU_MODEL → ${tierMap.haiku}`));
136
+ }
137
+
138
+ function addTierOptions(cmd: Command): Command {
139
+ return cmd
140
+ .option("--opus <model>", "Override opus tier model alias")
141
+ .option("--sonnet <model>", "Override sonnet tier model alias")
142
+ .option("--haiku <model>", "Override haiku tier model alias");
143
+ }
144
+
145
+ // ---------------------------------------------------------------------------
146
+ // Provider switch implementations (Claude Code)
147
+ // ---------------------------------------------------------------------------
148
+
149
+ async function switchAnthropic(): Promise<void> {
150
+ await configureClaudeAnthropic();
151
+
152
+ displaySuccess("Switched to Anthropic (default)");
153
+ console.log(chalk.dim(" Provider: Anthropic"));
154
+ console.log(chalk.dim(" Using native Claude models"));
155
+ console.log();
156
+ }
157
+
158
+ async function switchAlibaba(
159
+ model: string | undefined,
160
+ tierOpts: { opus?: string; sonnet?: string; haiku?: string }
161
+ ): Promise<void> {
162
+ const selectedModel = model || "qwen3.7-plus";
163
+
164
+ let apiKey = await getApiKey("alibaba");
165
+ if (!apiKey) {
166
+ apiKey = await promptApiKey(
167
+ "Alibaba",
168
+ "https://modelstudio.console.alibabacloud.com/"
169
+ );
170
+ await setApiKey("alibaba", apiKey);
171
+ }
172
+
173
+ const alibabaModels = getModels("alibaba");
174
+ const validModel = alibabaModels.find((m) => m.id === selectedModel);
175
+ if (!validModel) {
176
+ displayError(`Invalid model: ${selectedModel}`);
177
+ console.log(chalk.dim(" Valid models: ") + alibabaModels.map((m) => m.id).join(", "));
178
+ process.exit(1);
179
+ }
180
+
181
+ const tierMap = buildTierMap(getAlibabaTierMap(selectedModel), tierOpts);
182
+
183
+ await configureClaudeAlibaba(apiKey, selectedModel, tierMap);
184
+
185
+ console.log(chalk.green(`\n✓ Switched to: Alibaba Coding Plan`));
186
+ console.log(chalk.dim("─".repeat(60)));
187
+ console.log(` ${chalk.cyan.bold("Model:")} ${chalk.white(validModel.name)}`);
188
+ console.log(` ${chalk.cyan.bold("Context:")} ${chalk.yellow(formatContext(validModel.contextWindow))}`);
189
+ console.log(` ${chalk.cyan.bold("Endpoint:")} ${chalk.dim("https://coding-intl.dashscope.aliyuncs.com/apps/anthropic")}`);
190
+ console.log(` ${chalk.cyan.bold("Capabilities:")} ${chalk.gray(validModel.capabilities.join(", "))}`);
191
+ console.log(chalk.dim(` ${validModel.description}`));
192
+ console.log();
193
+ displayTierMap(tierMap);
194
+ console.log();
195
+ }
196
+
197
+ async function switchGLM(tierOpts: { opus?: string; sonnet?: string; haiku?: string }): Promise<void> {
198
+ const hasCodingHelper = await isCodingHelperInstalled();
199
+
200
+ if (!hasCodingHelper) {
201
+ displayWarning("coding-helper not found");
202
+ console.log(chalk.dim(" Install with: npm install -g @z_ai/coding-helper"));
203
+ console.log(chalk.dim(" Then run: coding-helper auth"));
204
+ console.log();
205
+ }
206
+
207
+ const tierMap = buildTierMap(GLM_DEFAULT_TIER_MAP, tierOpts);
208
+
209
+ await configureClaudeGLM(tierMap);
210
+ if (hasCodingHelper) {
211
+ const result = await reloadGLMConfig();
212
+ if (!result.success) {
213
+ displayWarning("coding-helper reload failed, but local config updated");
214
+ }
215
+ }
216
+
217
+ displaySuccess("Switched to GLM/Z.AI");
218
+ console.log(chalk.dim(" Provider: GLM/Z.AI"));
219
+ if (hasCodingHelper) console.log(chalk.dim(" Managed by: coding-helper"));
220
+ console.log();
221
+ displayTierMap(tierMap);
222
+ console.log();
223
+ }
224
+
225
+ async function switchOpenRouter(
226
+ model: string | undefined,
227
+ tierOpts: { opus?: string; sonnet?: string; haiku?: string }
228
+ ): Promise<void> {
229
+ const selectedModel = model || "qwen/qwen3.6-plus:free";
230
+
231
+ let apiKey = await getApiKey("openrouter");
232
+ if (!apiKey) {
233
+ apiKey = await promptApiKey(
234
+ "OpenRouter",
235
+ "https://openrouter.ai/settings/keys"
236
+ );
237
+ await setApiKey("openrouter", apiKey);
238
+ }
239
+
240
+ const openrouterModels = getModels("openrouter");
241
+ const validModel = openrouterModels.find((m) => m.id === selectedModel);
242
+ if (!validModel) {
243
+ displayError(`Invalid model: ${selectedModel}`);
244
+ console.log(chalk.dim(" Valid models: ") + openrouterModels.map((m) => m.id).join(", "));
245
+ process.exit(1);
246
+ }
247
+
248
+ const tierMap = buildTierMap(OPENROUTER_DEFAULT_TIER_MAP, tierOpts);
249
+
250
+ await configureClaudeOpenRouter(apiKey, selectedModel, tierMap);
251
+
252
+ console.log(chalk.green(`\n✓ Switched to: OpenRouter`));
253
+ console.log(chalk.dim("─".repeat(60)));
254
+ console.log(` ${chalk.cyan.bold("Model:")} ${chalk.white(validModel.name)}`);
255
+ console.log(` ${chalk.cyan.bold("Context:")} ${chalk.yellow(formatContext(validModel.contextWindow))}`);
256
+ console.log(` ${chalk.cyan.bold("Endpoint:")} ${chalk.dim("https://openrouter.ai/api/v1")}`);
257
+ console.log(` ${chalk.cyan.bold("Capabilities:")} ${chalk.gray(validModel.capabilities.join(", "))}`);
258
+ console.log(chalk.dim(` ${validModel.description}`));
259
+ console.log();
260
+ displayTierMap(tierMap);
261
+ console.log();
262
+ }
263
+
264
+ async function switchOllama(
265
+ model: string | undefined,
266
+ tierOpts: { opus?: string; sonnet?: string; haiku?: string }
267
+ ): Promise<void> {
268
+ // Pre-flight: check litellm
269
+ const hasLitellm = await isLitellmInstalledForOllama();
270
+ if (!hasLitellm) {
271
+ displayError("LiteLLM is required for Ollama support");
272
+ console.log(chalk.dim(" Install with: pip install 'litellm[proxy]'"));
273
+ process.exit(1);
274
+ }
275
+
276
+ // Pre-flight: check ollama
277
+ const hasOllama = await isOllamaInstalled();
278
+ if (!hasOllama) {
279
+ displayError("Ollama is not installed");
280
+ console.log(chalk.dim(" Install from: https://ollama.com"));
281
+ process.exit(1);
282
+ }
283
+
284
+ // Check if Ollama is running
285
+ const ollamaRunning = await isOllamaRunning();
286
+ if (!ollamaRunning) {
287
+ displayError("Ollama is not running");
288
+ console.log(chalk.dim(" Start with: ollama serve"));
289
+ process.exit(1);
290
+ }
291
+
292
+ const selectedModel = model || "deepseek-r1:latest";
293
+
294
+ const validModel = findOllamaModel(selectedModel);
295
+ if (!validModel) {
296
+ const ollamaModels = getModels("ollama");
297
+ displayError(`Invalid model: ${selectedModel}`);
298
+ console.log(chalk.dim(" Valid models: ") + ollamaModels.map((m) => m.id).join(", "));
299
+ process.exit(1);
300
+ }
301
+
302
+ // Start LiteLLM proxy
303
+ const proxyResult = await startLitellmProxy(selectedModel);
304
+ if (!proxyResult.success) {
305
+ displayError(`Failed to start LiteLLM proxy: ${proxyResult.error}`);
306
+ process.exit(1);
307
+ }
308
+
309
+ const tierMap = buildTierMap(OLLAMA_DEFAULT_TIER_MAP, tierOpts);
310
+
311
+ await configureClaudeOllama(selectedModel, tierMap);
312
+
313
+ console.log(chalk.green(`\n✓ Switched to: Ollama (Local)`));
314
+ console.log(chalk.dim("─".repeat(60)));
315
+ console.log(` ${chalk.cyan.bold("Model:")} ${chalk.white(validModel.name)}`);
316
+ console.log(` ${chalk.cyan.bold("Context:")} ${chalk.yellow(formatContext(validModel.contextWindow))}`);
317
+ console.log(` ${chalk.cyan.bold("Endpoint:")} ${chalk.dim("http://localhost:4000 (LiteLLM proxy)")}`);
318
+ console.log(` ${chalk.cyan.bold("Capabilities:")} ${chalk.gray(validModel.capabilities.join(", "))}`);
319
+ console.log(chalk.dim(` ${validModel.description}`));
320
+ console.log();
321
+ displayTierMap(tierMap);
322
+ console.log();
323
+ }
324
+
325
+ async function switchGemini(
326
+ model: string | undefined,
327
+ tierOpts: { opus?: string; sonnet?: string; haiku?: string }
328
+ ): Promise<void> {
329
+ // Pre-flight: check litellm
330
+ const hasLitellm = await isLitellmInstalledForGemini();
331
+ if (!hasLitellm) {
332
+ displayError("LiteLLM is required for Gemini support");
333
+ console.log(chalk.dim(" Install with: pip install 'litellm[proxy]'"));
334
+ process.exit(1);
335
+ }
336
+
337
+ const selectedModel = model || "gemini-2.5-pro";
338
+
339
+ const validModel = findGeminiModel(selectedModel);
340
+ if (!validModel) {
341
+ const geminiModels = getModels("gemini");
342
+ displayError(`Invalid model: ${selectedModel}`);
343
+ console.log(chalk.dim(" Valid models: ") + geminiModels.map((m) => m.id).join(", "));
344
+ process.exit(1);
345
+ }
346
+
347
+ // Get API key
348
+ let apiKey = await getApiKey("gemini");
349
+ if (!apiKey) {
350
+ apiKey = await promptApiKey(
351
+ "Gemini",
352
+ "https://aistudio.google.com/apikey"
353
+ );
354
+ await setApiKey("gemini", apiKey);
355
+ }
356
+
357
+ // Start LiteLLM proxy
358
+ const proxyResult = await startGeminiLitellmProxy(apiKey, selectedModel);
359
+ if (!proxyResult.success) {
360
+ displayError(`Failed to start LiteLLM proxy: ${proxyResult.error}`);
361
+ process.exit(1);
362
+ }
363
+
364
+ const tierMap = buildTierMap(GEMINI_DEFAULT_TIER_MAP, tierOpts);
365
+
366
+ await configureClaudeGemini(apiKey, selectedModel, tierMap);
367
+
368
+ console.log(chalk.green(`\n✓ Switched to: Gemini (Google)`));
369
+ console.log(chalk.dim("─".repeat(60)));
370
+ console.log(` ${chalk.cyan.bold("Model:")} ${chalk.white(validModel.name)}`);
371
+ console.log(` ${chalk.cyan.bold("Context:")} ${chalk.yellow(formatContext(validModel.contextWindow))}`);
372
+ console.log(` ${chalk.cyan.bold("Endpoint:")} ${chalk.dim("http://localhost:4001 (LiteLLM proxy)")}`);
373
+ console.log(` ${chalk.cyan.bold("Capabilities:")} ${chalk.gray(validModel.capabilities.join(", "))}`);
374
+ console.log(chalk.dim(` ${validModel.description}`));
375
+ console.log();
376
+ displayTierMap(tierMap);
377
+ console.log();
378
+ }
379
+
380
+ // ---------------------------------------------------------------------------
381
+ // Top-level commands — Claude Code only
382
+ // ---------------------------------------------------------------------------
383
+
384
+ addTierOptions(
385
+ program
386
+ .command("alibaba [model]")
387
+ .description("Switch Claude Code to Alibaba Coding Plan")
388
+ ).action(async (model, options) => {
389
+ try {
390
+ await switchAlibaba(model, options);
391
+ } catch (error) {
392
+ displayError(error instanceof Error ? error.message : "Failed to switch to Alibaba");
393
+ process.exit(1);
394
+ }
395
+ });
396
+
397
+ program
398
+ .command("anthropic")
399
+ .description("Switch Claude Code to Anthropic (default)")
400
+ .action(async () => {
401
+ try {
402
+ await switchAnthropic();
403
+ } catch (error) {
404
+ displayError(error instanceof Error ? error.message : "Failed to switch to Anthropic");
405
+ process.exit(1);
406
+ }
407
+ });
408
+
409
+ addTierOptions(
410
+ program
411
+ .command("glm")
412
+ .description("Switch Claude Code to GLM/Z.AI (requires @z_ai/coding-helper)")
413
+ ).action(async (options) => {
414
+ try {
415
+ await switchGLM(options);
416
+ } catch (error) {
417
+ displayError(error instanceof Error ? error.message : "Failed to switch to GLM");
418
+ process.exit(1);
419
+ }
420
+ });
421
+
422
+ addTierOptions(
423
+ program
424
+ .command("openrouter [model]")
425
+ .description("Switch Claude Code to OpenRouter")
426
+ ).action(async (model, options) => {
427
+ try {
428
+ await switchOpenRouter(model, options);
429
+ } catch (error) {
430
+ displayError(error instanceof Error ? error.message : "Failed to switch to OpenRouter");
431
+ process.exit(1);
432
+ }
433
+ });
434
+
435
+ addTierOptions(
436
+ program
437
+ .command("ollama [model]")
438
+ .description("Switch Claude Code to Ollama (local models, requires LiteLLM proxy)")
439
+ ).action(async (model, options) => {
440
+ try {
441
+ await switchOllama(model, options);
442
+ } catch (error) {
443
+ displayError(error instanceof Error ? error.message : "Failed to switch to Ollama");
444
+ process.exit(1);
445
+ }
446
+ });
447
+
448
+ addTierOptions(
449
+ program
450
+ .command("gemini [model]")
451
+ .description("Switch Claude Code to Gemini (Google, requires LiteLLM proxy)")
452
+ ).action(async (model, options) => {
453
+ try {
454
+ await switchGemini(model, options);
455
+ } catch (error) {
456
+ displayError(error instanceof Error ? error.message : "Failed to switch to Gemini");
457
+ process.exit(1);
458
+ }
459
+ });
460
+
461
+ // ---------------------------------------------------------------------------
462
+ // `claude` subcommand — explicit Claude Code targeting
463
+ // ---------------------------------------------------------------------------
464
+
465
+ const claudeCmd = program
466
+ .command("claude")
467
+ .description("Configure Claude Code (explicit targeting)");
468
+
469
+ claudeCmd
470
+ .command("anthropic")
471
+ .description("Switch Claude Code to Anthropic (default)")
472
+ .action(async () => {
473
+ try {
474
+ await switchAnthropic();
475
+ } catch (error) {
476
+ displayError(error instanceof Error ? error.message : "Failed to switch to Anthropic");
477
+ process.exit(1);
478
+ }
479
+ });
480
+
481
+ addTierOptions(
482
+ claudeCmd
483
+ .command("alibaba [model]")
484
+ .description("Switch Claude Code to Alibaba Coding Plan")
485
+ ).action(async (model, options) => {
486
+ try {
487
+ await switchAlibaba(model, options);
488
+ } catch (error) {
489
+ displayError(error instanceof Error ? error.message : "Failed to switch to Alibaba");
490
+ process.exit(1);
491
+ }
492
+ });
493
+
494
+ addTierOptions(
495
+ claudeCmd
496
+ .command("glm")
497
+ .description("Switch Claude Code to GLM/Z.AI (requires @z_ai/coding-helper)")
498
+ ).action(async (options) => {
499
+ try {
500
+ await switchGLM(options);
501
+ } catch (error) {
502
+ displayError(error instanceof Error ? error.message : "Failed to switch to GLM");
503
+ process.exit(1);
504
+ }
505
+ });
506
+
507
+ addTierOptions(
508
+ claudeCmd
509
+ .command("openrouter [model]")
510
+ .description("Switch Claude Code to OpenRouter")
511
+ ).action(async (model, options) => {
512
+ try {
513
+ await switchOpenRouter(model, options);
514
+ } catch (error) {
515
+ displayError(error instanceof Error ? error.message : "Failed to switch to OpenRouter");
516
+ process.exit(1);
517
+ }
518
+ });
519
+
520
+ addTierOptions(
521
+ claudeCmd
522
+ .command("ollama [model]")
523
+ .description("Switch Claude Code to Ollama (local models, requires LiteLLM proxy)")
524
+ ).action(async (model, options) => {
525
+ try {
526
+ await switchOllama(model, options);
527
+ } catch (error) {
528
+ displayError(error instanceof Error ? error.message : "Failed to switch to Ollama");
529
+ process.exit(1);
530
+ }
531
+ });
532
+
533
+ addTierOptions(
534
+ claudeCmd
535
+ .command("gemini [model]")
536
+ .description("Switch Claude Code to Gemini (Google, requires LiteLLM proxy)")
537
+ ).action(async (model, options) => {
538
+ try {
539
+ await switchGemini(model, options);
540
+ } catch (error) {
541
+ displayError(error instanceof Error ? error.message : "Failed to switch to Gemini");
542
+ process.exit(1);
543
+ }
544
+ });
545
+
546
+ // ---------------------------------------------------------------------------
547
+ // `opencode` subcommand — OpenCode helper commands
548
+ // ---------------------------------------------------------------------------
549
+
550
+ const opencodeCmd = program
551
+ .command("opencode")
552
+ .description("OpenCode helper commands");
553
+
554
+ const opencodeAddCmd = opencodeCmd
555
+ .command("add")
556
+ .description("Add a provider to OpenCode");
557
+
558
+ opencodeAddCmd
559
+ .command("alibaba")
560
+ .description("Add Alibaba Coding Plan provider to OpenCode")
561
+ .action(async () => {
562
+ try {
563
+ let apiKey = await getApiKey("alibaba");
564
+ if (!apiKey) {
565
+ apiKey = await promptApiKey(
566
+ "Alibaba",
567
+ "https://modelstudio.console.alibabacloud.com/"
568
+ );
569
+ await setApiKey("alibaba", apiKey);
570
+ }
571
+
572
+ await configureOpenCodeAlibaba(apiKey);
573
+
574
+ displaySuccess("Added Alibaba Coding Plan provider to OpenCode");
575
+ console.log(chalk.dim(" Config: ~/.config/opencode/opencode.json"));
576
+ console.log(chalk.dim(" Provider: bailian-coding-plan"));
577
+ console.log(chalk.dim(" Models: qwen3.7-plus, qwen3.6-plus, qwen3-max-2026-01-23, qwen3-coder-next, qwen3-coder-plus, MiniMax-M2.5, glm-5, glm-4.7, kimi-k2.5"));
578
+ console.log();
579
+ } catch (error) {
580
+ displayError(error instanceof Error ? error.message : "Failed to add Alibaba provider");
581
+ process.exit(1);
582
+ }
583
+ });
584
+
585
+ opencodeAddCmd
586
+ .command("openrouter")
587
+ .description("Add OpenRouter provider to OpenCode")
588
+ .action(async () => {
589
+ try {
590
+ let apiKey = await getApiKey("openrouter");
591
+ if (!apiKey) {
592
+ apiKey = await promptApiKey(
593
+ "OpenRouter",
594
+ "https://openrouter.ai/settings/keys"
595
+ );
596
+ await setApiKey("openrouter", apiKey);
597
+ }
598
+
599
+ await configureOpenCodeOpenRouter(apiKey);
600
+
601
+ displaySuccess("Added OpenRouter provider to OpenCode");
602
+ console.log(chalk.dim(" Config: ~/.config/opencode/opencode.json"));
603
+ console.log(chalk.dim(" Provider: openrouter"));
604
+ console.log(chalk.dim(" Models: qwen/qwen3.6-plus:free, openrouter/free"));
605
+ console.log();
606
+ } catch (error) {
607
+ displayError(error instanceof Error ? error.message : "Failed to add OpenRouter provider");
608
+ process.exit(1);
609
+ }
610
+ });
611
+
612
+ opencodeAddCmd
613
+ .command("ollama")
614
+ .description("Add Ollama provider to OpenCode (requires LiteLLM proxy)")
615
+ .action(async () => {
616
+ try {
617
+ await configureOpenCodeOllama();
618
+
619
+ displaySuccess("Added Ollama provider to OpenCode");
620
+ console.log(chalk.dim(" Config: ~/.config/opencode/opencode.json"));
621
+ console.log(chalk.dim(" Provider: ollama"));
622
+ console.log(chalk.dim(" Models: deepseek-r1:latest, qwen2.5-coder:latest, llama3.1:latest, codellama:latest"));
623
+ console.log(chalk.yellow(" Note: Requires LiteLLM proxy running on port 4000"));
624
+ console.log();
625
+ } catch (error) {
626
+ displayError(error instanceof Error ? error.message : "Failed to add Ollama provider");
627
+ process.exit(1);
628
+ }
629
+ });
630
+
631
+ opencodeAddCmd
632
+ .command("gemini")
633
+ .description("Add Gemini provider to OpenCode (requires LiteLLM proxy)")
634
+ .action(async () => {
635
+ try {
636
+ let apiKey = await getApiKey("gemini");
637
+ if (!apiKey) {
638
+ apiKey = await promptApiKey(
639
+ "Gemini",
640
+ "https://aistudio.google.com/apikey"
641
+ );
642
+ await setApiKey("gemini", apiKey);
643
+ }
644
+
645
+ await configureOpenCodeGemini(apiKey);
646
+
647
+ displaySuccess("Added Gemini provider to OpenCode");
648
+ console.log(chalk.dim(" Config: ~/.config/opencode/opencode.json"));
649
+ console.log(chalk.dim(" Provider: gemini"));
650
+ console.log(chalk.dim(" Models: gemini-2.5-pro, gemini-2.5-flash, gemini-2.5-flash-lite"));
651
+ console.log(chalk.yellow(" Note: Requires LiteLLM proxy running on port 4001"));
652
+ console.log();
653
+ } catch (error) {
654
+ displayError(error instanceof Error ? error.message : "Failed to add Gemini provider");
655
+ process.exit(1);
656
+ }
657
+ });
658
+
659
+ opencodeAddCmd
660
+ .command("glm")
661
+ .description("Add GLM/Z.AI provider to OpenCode (requires @z_ai/coding-helper)")
662
+ .action(async () => {
663
+ try {
664
+ // Check coding-helper
665
+ const hasCodingHelper = await isCodingHelperInstalled();
666
+ if (!hasCodingHelper) {
667
+ displayWarning("coding-helper not found");
668
+ console.log(chalk.dim(" Install with: npm install -g @z_ai/coding-helper"));
669
+ console.log(chalk.dim(" Then run: coding-helper auth"));
670
+ console.log();
671
+ }
672
+
673
+ // Read GLM auth from Claude settings (set by coding-helper auth reload claude)
674
+ const claudeSettings = await readClaudeSettings();
675
+ let baseURL = claudeSettings.env?.["ANTHROPIC_BASE_URL"] || "";
676
+ let apiKey = claudeSettings.env?.["ANTHROPIC_AUTH_TOKEN"] || "";
677
+
678
+ if (!baseURL || !baseURL.includes(".z.ai")) {
679
+ displayWarning("GLM not configured in Claude Code yet");
680
+ console.log(chalk.dim(" Run 'claude-switch glm' first to set up coding-helper auth"));
681
+ console.log();
682
+ return;
683
+ }
684
+
685
+ await configureOpenCodeGLM(baseURL, apiKey);
686
+
687
+ displaySuccess("Added GLM/Z.AI provider to OpenCode");
688
+ console.log(chalk.dim(" Config: ~/.config/opencode/opencode.json"));
689
+ console.log(chalk.dim(" Provider: glm"));
690
+ console.log(chalk.dim(" Models: glm-5.1, glm-5v-turbo, glm-5-turbo, glm-4.7, glm-4.7-flash"));
691
+ if (hasCodingHelper) console.log(chalk.dim(" Managed by: coding-helper"));
692
+ console.log();
693
+ } catch (error) {
694
+ displayError(error instanceof Error ? error.message : "Failed to add GLM provider");
695
+ process.exit(1);
696
+ }
697
+ });
698
+
699
+ const opencodeRemoveCmd = opencodeCmd
700
+ .command("remove")
701
+ .description("Remove a provider from OpenCode");
702
+
703
+ opencodeRemoveCmd
704
+ .command("alibaba")
705
+ .description("Remove Alibaba Coding Plan provider from OpenCode")
706
+ .action(async () => {
707
+ try {
708
+ const { removeProvider } = await import("./clients/opencode");
709
+ await removeProvider("bailian-coding-plan");
710
+
711
+ displaySuccess("Removed Alibaba Coding Plan provider from OpenCode");
712
+ console.log(chalk.dim(" Other providers remain unchanged"));
713
+ console.log();
714
+ } catch (error) {
715
+ displayError(error instanceof Error ? error.message : "Failed to remove Alibaba provider");
716
+ process.exit(1);
717
+ }
718
+ });
719
+
720
+ opencodeRemoveCmd
721
+ .command("openrouter")
722
+ .description("Remove OpenRouter provider from OpenCode")
723
+ .action(async () => {
724
+ try {
725
+ const { removeProvider } = await import("./clients/opencode");
726
+ await removeProvider("openrouter");
727
+
728
+ displaySuccess("Removed OpenRouter provider from OpenCode");
729
+ console.log(chalk.dim(" Other providers remain unchanged"));
730
+ console.log();
731
+ } catch (error) {
732
+ displayError(error instanceof Error ? error.message : "Failed to remove OpenRouter provider");
733
+ process.exit(1);
734
+ }
735
+ });
736
+
737
+ opencodeRemoveCmd
738
+ .command("ollama")
739
+ .description("Remove Ollama provider from OpenCode")
740
+ .action(async () => {
741
+ try {
742
+ const { removeProvider } = await import("./clients/opencode");
743
+ await removeProvider("ollama");
744
+
745
+ displaySuccess("Removed Ollama provider from OpenCode");
746
+ console.log(chalk.dim(" Other providers remain unchanged"));
747
+ console.log();
748
+ } catch (error) {
749
+ displayError(error instanceof Error ? error.message : "Failed to remove Ollama provider");
750
+ process.exit(1);
751
+ }
752
+ });
753
+
754
+ opencodeRemoveCmd
755
+ .command("gemini")
756
+ .description("Remove Gemini provider from OpenCode")
757
+ .action(async () => {
758
+ try {
759
+ const { removeProvider } = await import("./clients/opencode");
760
+ await removeProvider("gemini");
761
+
762
+ displaySuccess("Removed Gemini provider from OpenCode");
763
+ console.log(chalk.dim(" Other providers remain unchanged"));
764
+ console.log();
765
+ } catch (error) {
766
+ displayError(error instanceof Error ? error.message : "Failed to remove Gemini provider");
767
+ process.exit(1);
768
+ }
769
+ });
770
+
771
+ opencodeRemoveCmd
772
+ .command("glm")
773
+ .description("Remove GLM/Z.AI provider from OpenCode")
774
+ .action(async () => {
775
+ try {
776
+ const { removeProvider } = await import("./clients/opencode");
777
+ await removeProvider("glm");
778
+
779
+ displaySuccess("Removed GLM/Z.AI provider from OpenCode");
780
+ console.log(chalk.dim(" Other providers remain unchanged"));
781
+ console.log();
782
+ } catch (error) {
783
+ displayError(error instanceof Error ? error.message : "Failed to remove GLM provider");
784
+ process.exit(1);
785
+ }
786
+ });
787
+
788
+ // ---------------------------------------------------------------------------
789
+ // Info commands
790
+ // ---------------------------------------------------------------------------
791
+
792
+ program
793
+ .command("status")
794
+ .description("Show current config and verify API keys")
795
+ .action(async () => {
796
+ try {
797
+ // ── Current Configuration ──
798
+ console.log(chalk.green("\n=== Claude AI Switcher Status ===\n"));
799
+
800
+ // Claude Code
801
+ console.log(chalk.cyan.bold(" Claude Code:"));
802
+ if (claudeSettingsExists()) {
803
+ const claudeProvider = await getClaudeProvider();
804
+ if (claudeProvider) {
805
+ console.log(` Provider: ${chalk.white(claudeProvider.provider)}`);
806
+ if (claudeProvider.model) console.log(` Model: ${chalk.white(claudeProvider.model)}`);
807
+ if (claudeProvider.endpoint) console.log(` Endpoint: ${chalk.dim(claudeProvider.endpoint)}`);
808
+ if (claudeProvider.tierMap?.opus) {
809
+ console.log(chalk.dim(" Aliases:"));
810
+ console.log(chalk.dim(` opus → ${claudeProvider.tierMap.opus}`));
811
+ console.log(chalk.dim(` sonnet → ${claudeProvider.tierMap.sonnet}`));
812
+ console.log(chalk.dim(` haiku → ${claudeProvider.tierMap.haiku}`));
813
+ }
814
+ } else {
815
+ console.log(chalk.dim(" Unable to read configuration"));
816
+ }
817
+ } else {
818
+ console.log(chalk.dim(" Not configured (using defaults)"));
819
+ }
820
+
821
+ console.log();
822
+
823
+ // OpenCode
824
+ console.log(chalk.cyan.bold(" OpenCode:"));
825
+ if (opencodeSettingsExists()) {
826
+ const opencodeProvider = await getOpenCodeProvider();
827
+ if (opencodeProvider) {
828
+ console.log(` Provider: ${chalk.white(opencodeProvider.provider)}`);
829
+ if (opencodeProvider.model) console.log(` Model: ${chalk.white(opencodeProvider.model)}`);
830
+ if (opencodeProvider.endpoint) console.log(` Endpoint: ${chalk.dim(opencodeProvider.endpoint)}`);
831
+ } else {
832
+ console.log(chalk.dim(" Unable to read configuration"));
833
+ }
834
+ } else {
835
+ console.log(chalk.dim(" Not installed"));
836
+ }
837
+
838
+ // ── API Key Verification ──
839
+ console.log();
840
+ console.log(chalk.cyan.bold(" API Key Verification:"));
841
+ console.log(chalk.dim("─".repeat(50)));
842
+
843
+ const alibabaKey = await getApiKey("alibaba");
844
+ const openrouterKey = await getApiKey("openrouter");
845
+ const anthropicKey = process.env.ANTHROPIC_API_KEY;
846
+ const geminiKey = await getApiKey("gemini");
847
+
848
+ // Show spinner while verifying
849
+ const ora = (await import("ora")).default;
850
+ const spinner = ora("Verifying API keys...").start();
851
+
852
+ const results = await verifyAllKeys({
853
+ alibaba: alibabaKey,
854
+ openrouter: openrouterKey,
855
+ anthropic: anthropicKey,
856
+ checkGLM: true,
857
+ checkOllama: true,
858
+ gemini: geminiKey
859
+ });
860
+
861
+ spinner.stop();
862
+
863
+ for (const result of results) {
864
+ const label = result.provider.padEnd(12);
865
+ let icon: string;
866
+ let detail = result.message || "";
867
+
868
+ switch (result.status) {
869
+ case "ok":
870
+ icon = chalk.green("✓");
871
+ break;
872
+ case "invalid":
873
+ icon = chalk.red("✗");
874
+ break;
875
+ case "missing":
876
+ icon = chalk.dim("○");
877
+ detail = "No key configured";
878
+ break;
879
+ case "error":
880
+ icon = chalk.yellow("⚠");
881
+ break;
882
+ default:
883
+ icon = chalk.dim("–");
884
+ detail = "Skipped";
885
+ }
886
+
887
+ // Show masked key if available
888
+ let keyDisplay = "";
889
+ if (result.provider === "alibaba" && alibabaKey) {
890
+ keyDisplay = chalk.dim(` (${maskKey(alibabaKey)})`);
891
+ } else if (result.provider === "openrouter" && openrouterKey) {
892
+ keyDisplay = chalk.dim(` (${maskKey(openrouterKey)})`);
893
+ } else if (result.provider === "anthropic" && anthropicKey) {
894
+ keyDisplay = chalk.dim(` (${maskKey(anthropicKey)})`);
895
+ } else if (result.provider === "gemini" && geminiKey) {
896
+ keyDisplay = chalk.dim(` (${maskKey(geminiKey)})`);
897
+ }
898
+
899
+ console.log(` ${icon} ${chalk.white(label)} ${chalk.gray(detail)}${keyDisplay}`);
900
+ }
901
+
902
+ console.log(chalk.dim("─".repeat(50)));
903
+ console.log();
904
+ } catch (error) {
905
+ displayError(error instanceof Error ? error.message : "Failed to get status");
906
+ process.exit(1);
907
+ }
908
+ });
909
+
910
+ program
911
+ .command("current")
912
+ .description("Show current provider and model for both clients")
913
+ .action(async () => {
914
+ try {
915
+ console.log(chalk.green("\nCurrent Configuration:\n"));
916
+
917
+ console.log(chalk.cyan.bold(" Claude Code:"));
918
+ if (claudeSettingsExists()) {
919
+ const claudeProvider = await getClaudeProvider();
920
+ if (claudeProvider) {
921
+ console.log(` Provider: ${chalk.white(claudeProvider.provider)}`);
922
+ if (claudeProvider.model) console.log(` Model: ${chalk.white(claudeProvider.model)}`);
923
+ if (claudeProvider.endpoint) console.log(` Endpoint: ${chalk.dim(claudeProvider.endpoint)}`);
924
+ if (claudeProvider.tierMap?.opus) {
925
+ console.log(chalk.dim(" Model aliases:"));
926
+ console.log(chalk.dim(` opus → ${claudeProvider.tierMap.opus}`));
927
+ console.log(chalk.dim(` sonnet → ${claudeProvider.tierMap.sonnet}`));
928
+ console.log(chalk.dim(` haiku → ${claudeProvider.tierMap.haiku}`));
929
+ }
930
+ } else {
931
+ console.log(chalk.dim(" Unable to read configuration"));
932
+ }
933
+ } else {
934
+ console.log(chalk.dim(" Not configured (using defaults)"));
935
+ }
936
+
937
+ console.log();
938
+
939
+ console.log(chalk.cyan.bold(" OpenCode:"));
940
+ if (opencodeSettingsExists()) {
941
+ const opencodeProvider = await getOpenCodeProvider();
942
+ if (opencodeProvider) {
943
+ console.log(` Provider: ${chalk.white(opencodeProvider.provider)}`);
944
+ if (opencodeProvider.model) console.log(` Model: ${chalk.white(opencodeProvider.model)}`);
945
+ if (opencodeProvider.endpoint) console.log(` Endpoint: ${chalk.dim(opencodeProvider.endpoint)}`);
946
+ } else {
947
+ console.log(chalk.dim(" Unable to read configuration"));
948
+ }
949
+ } else {
950
+ console.log(chalk.dim(" Not configured (using defaults)"));
951
+ }
952
+
953
+ console.log();
954
+ } catch (error) {
955
+ displayError(error instanceof Error ? error.message : "Failed to get current provider");
956
+ process.exit(1);
957
+ }
958
+ });
959
+
960
+ program
961
+ .command("list")
962
+ .description("List all providers and their models")
963
+ .action(() => {
964
+ const providerList = Object.values(providers).map((p) => ({
965
+ id: p.id,
966
+ name: p.name,
967
+ endpoint: p.endpoint,
968
+ modelCount: p.models.length
969
+ }));
970
+
971
+ displayProviders(providerList);
972
+
973
+ for (const provider of Object.values(providers)) {
974
+ displayModels(provider.name, provider.models);
975
+ }
976
+ });
977
+
978
+ program
979
+ .command("models [provider]")
980
+ .description("Show models for a specific provider")
981
+ .action((providerName) => {
982
+ if (!providerName) {
983
+ displayError("Please specify a provider: anthropic, alibaba, openrouter, glm, ollama, or gemini");
984
+ console.log(chalk.dim(" Example: claude-switch models alibaba"));
985
+ process.exit(1);
986
+ }
987
+
988
+ const provider = providers[providerName.toLowerCase()];
989
+ if (!provider) {
990
+ displayError(`Unknown provider: ${providerName}`);
991
+ console.log(chalk.dim(" Valid providers: ") + Object.keys(providers).join(", "));
992
+ process.exit(1);
993
+ }
994
+
995
+ displayModels(provider.name, provider.models);
996
+ });
997
+
998
+ program
999
+ .command("key <provider> [apikey]")
1000
+ .description("Set or show API key for a provider")
1001
+ .action(async (provider, apikey) => {
1002
+ try {
1003
+ if (!apikey) {
1004
+ const hasKey = await hasApiKey(provider);
1005
+ if (hasKey) {
1006
+ displaySuccess(`API key is set for ${provider}`);
1007
+ } else {
1008
+ displayWarning(`No API key set for ${provider}`);
1009
+ console.log(chalk.dim(" Set with: claude-switch key " + provider + " <your-key>"));
1010
+ }
1011
+ return;
1012
+ }
1013
+
1014
+ await setApiKey(provider, apikey);
1015
+ displaySuccess(`API key set for ${provider}`);
1016
+ } catch (error) {
1017
+ displayError(error instanceof Error ? error.message : "Failed to manage API key");
1018
+ process.exit(1);
1019
+ }
1020
+ });
1021
+
1022
+ program
1023
+ .command("setup")
1024
+ .description("Interactive setup wizard")
1025
+ .action(async () => {
1026
+ try {
1027
+ console.log(chalk.green("\n=== Claude AI Switcher Setup ===\n"));
1028
+
1029
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1030
+
1031
+ const hasAlibabaKey = await hasApiKey("alibaba");
1032
+ if (!hasAlibabaKey) {
1033
+ console.log(chalk.yellow("Alibaba Coding Plan Setup"));
1034
+ console.log(chalk.dim(" Get your API key from: https://modelstudio.console.alibabacloud.com/"));
1035
+ console.log();
1036
+
1037
+ const answer = await new Promise<string>((resolve) => {
1038
+ rl.question("Enter your Alibaba API Key (or press Enter to skip): ", resolve);
1039
+ });
1040
+
1041
+ if (answer.trim()) {
1042
+ await setApiKey("alibaba", answer.trim());
1043
+ displaySuccess("Alibaba API key saved");
1044
+ }
1045
+ }
1046
+
1047
+ const hasOpenRouterKey = await hasApiKey("openrouter");
1048
+ if (!hasOpenRouterKey) {
1049
+ console.log(chalk.yellow("\nOpenRouter Setup"));
1050
+ console.log(chalk.dim(" Get your API key from: https://openrouter.ai/settings/keys"));
1051
+ console.log();
1052
+
1053
+ const answer = await new Promise<string>((resolve) => {
1054
+ rl.question("Enter your OpenRouter API Key (or press Enter to skip): ", resolve);
1055
+ });
1056
+
1057
+ if (answer.trim()) {
1058
+ await setApiKey("openrouter", answer.trim());
1059
+ displaySuccess("OpenRouter API key saved");
1060
+ }
1061
+ }
1062
+
1063
+ const hasGeminiKey = await hasApiKey("gemini");
1064
+ if (!hasGeminiKey) {
1065
+ console.log(chalk.yellow("\nGemini Setup"));
1066
+ console.log(chalk.dim(" Get your API key from: https://aistudio.google.com/apikey"));
1067
+ console.log();
1068
+
1069
+ const answer = await new Promise<string>((resolve) => {
1070
+ rl.question("Enter your Gemini API Key (or press Enter to skip): ", resolve);
1071
+ });
1072
+
1073
+ if (answer.trim()) {
1074
+ await setApiKey("gemini", answer.trim());
1075
+ displaySuccess("Gemini API key saved");
1076
+ }
1077
+ }
1078
+
1079
+ rl.close();
1080
+
1081
+ console.log(chalk.green("\n✓ Setup complete!\n"));
1082
+ console.log("Available commands:");
1083
+ console.log(chalk.dim(" claude-switch alibaba [model] - Switch Claude Code to Alibaba"));
1084
+ console.log(chalk.dim(" claude-switch anthropic - Switch Claude Code to Anthropic"));
1085
+ console.log(chalk.dim(" claude-switch glm - Switch Claude Code to GLM/Z.AI"));
1086
+ console.log(chalk.dim(" claude-switch openrouter [model] - Switch Claude Code to OpenRouter"));
1087
+ console.log(chalk.dim(" claude-switch ollama [model] - Switch Claude Code to Ollama"));
1088
+ console.log(chalk.dim(" claude-switch gemini [model] - Switch Claude Code to Gemini"));
1089
+ console.log(chalk.dim(" claude-switch claude alibaba - Explicit Claude Code targeting"));
1090
+ console.log(chalk.dim(" claude-switch opencode add alibaba - Add Alibaba provider to OpenCode"));
1091
+ console.log(chalk.dim(" claude-switch opencode add openrouter - Add OpenRouter provider to OpenCode"));
1092
+ console.log(chalk.dim(" claude-switch opencode add ollama - Add Ollama provider to OpenCode"));
1093
+ console.log(chalk.dim(" claude-switch opencode add gemini - Add Gemini provider to OpenCode"));
1094
+ console.log(chalk.dim(" claude-switch opencode add glm - Add GLM/Z.AI provider to OpenCode"));
1095
+ console.log(chalk.dim(" claude-switch opencode remove alibaba - Remove Alibaba from OpenCode"));
1096
+ console.log(chalk.dim(" claude-switch opencode remove openrouter - Remove OpenRouter from OpenCode"));
1097
+ console.log(chalk.dim(" claude-switch opencode remove ollama - Remove Ollama from OpenCode"));
1098
+ console.log(chalk.dim(" claude-switch opencode remove gemini - Remove Gemini from OpenCode"));
1099
+ console.log(chalk.dim(" claude-switch opencode remove glm - Remove GLM/Z.AI from OpenCode"));
1100
+ console.log(chalk.dim(" claude-switch openrouter --opus <model> - Custom model aliases"));
1101
+ console.log(chalk.dim(" claude-switch list - List all providers"));
1102
+ console.log(chalk.dim(" claude-switch status - Show current config + verify API keys"));
1103
+ console.log(chalk.dim(" claude-switch current - Show current config"));
1104
+ console.log(chalk.dim(" claude-switch hooks install - Install token tracking & visual enhancements"));
1105
+ console.log(chalk.dim(" claude-switch hooks status - Show token usage and visual status"));
1106
+ console.log();
1107
+ } catch (error) {
1108
+ displayError(error instanceof Error ? error.message : "Setup failed");
1109
+ process.exit(1);
1110
+ }
1111
+ });
1112
+
1113
+ // ---------------------------------------------------------------------------
1114
+ // Hooks commands - Token tracking and visual enhancements
1115
+ // ---------------------------------------------------------------------------
1116
+
1117
+ const hooksCmd = program
1118
+ .command("hooks")
1119
+ .description("Manage Claude Code hooks (token tracking, visual enhancements)");
1120
+
1121
+ hooksCmd
1122
+ .command("install")
1123
+ .description("Install all visual enhancements and token tracking")
1124
+ .action(async () => {
1125
+ try {
1126
+ const ora = await import("ora").catch(() => null);
1127
+ const spinner = ora ? ora.default("Installing hooks...").start() : null;
1128
+
1129
+ await installAllHooks();
1130
+
1131
+ spinner?.stop();
1132
+
1133
+ console.log(chalk.green("\n✓ Hooks installed successfully!\n"));
1134
+ console.log(chalk.cyan.bold(" Installed:"));
1135
+ console.log(chalk.dim(" • Token Tracker (~/.claude/token-tracker.js)"));
1136
+ console.log(chalk.dim(" • Visual Enhancements (~/.claude/visual-enhancements.js)"));
1137
+ console.log();
1138
+ console.log(chalk.yellow(" Usage:"));
1139
+ console.log(chalk.dim(" • Token usage is tracked automatically"));
1140
+ console.log(chalk.dim(" • Run 'claude-switch hooks status' to see current usage"));
1141
+ console.log(chalk.dim(" • Run 'claude-switch hooks reset' to reset counters"));
1142
+ console.log();
1143
+ } catch (error) {
1144
+ displayError(error instanceof Error ? error.message : "Failed to install hooks");
1145
+ process.exit(1);
1146
+ }
1147
+ });
1148
+
1149
+ hooksCmd
1150
+ .command("install-token")
1151
+ .description("Install only token tracker")
1152
+ .action(async () => {
1153
+ try {
1154
+ await installTokenTracker();
1155
+ displaySuccess("Token tracker installed");
1156
+ console.log(chalk.dim(" Location: ~/.claude/token-tracker.js"));
1157
+ console.log();
1158
+ } catch (error) {
1159
+ displayError(error instanceof Error ? error.message : "Failed to install token tracker");
1160
+ process.exit(1);
1161
+ }
1162
+ });
1163
+
1164
+ hooksCmd
1165
+ .command("install-visual")
1166
+ .description("Install only visual enhancements")
1167
+ .action(async () => {
1168
+ try {
1169
+ await installVisualEnhancements();
1170
+ displaySuccess("Visual enhancements installed");
1171
+ console.log(chalk.dim(" Location: ~/.claude/visual-enhancements.js"));
1172
+ console.log();
1173
+ } catch (error) {
1174
+ displayError(error instanceof Error ? error.message : "Failed to install visual enhancements");
1175
+ process.exit(1);
1176
+ }
1177
+ });
1178
+
1179
+ hooksCmd
1180
+ .command("status")
1181
+ .description("Show token usage and visual status")
1182
+ .action(async () => {
1183
+ try {
1184
+ const installed = await areHooksInstalled();
1185
+
1186
+ console.log(chalk.green("\n=== Hooks Status ===\n"));
1187
+ console.log(` Token Tracker: ${installed.tokenTracking ? chalk.green("✓ Installed") : chalk.red("Not installed")}`);
1188
+ console.log(` Visual Enhancements: ${installed.visualEnhancements ? chalk.green("✓ Installed") : chalk.red("Not installed")}`);
1189
+ console.log();
1190
+
1191
+ if (installed.tokenTracking) {
1192
+ await showTokenStatus();
1193
+ }
1194
+
1195
+ if (installed.visualEnhancements) {
1196
+ await showVisualStatus();
1197
+ }
1198
+
1199
+ if (!installed.tokenTracking && !installed.visualEnhancements) {
1200
+ console.log(chalk.yellow(" Run 'claude-switch hooks install' to install hooks"));
1201
+ console.log();
1202
+ }
1203
+ } catch (error) {
1204
+ displayError(error instanceof Error ? error.message : "Failed to get hooks status");
1205
+ process.exit(1);
1206
+ }
1207
+ });
1208
+
1209
+ hooksCmd
1210
+ .command("reset")
1211
+ .description("Reset token usage counters")
1212
+ .action(async () => {
1213
+ try {
1214
+ await resetTokenUsage();
1215
+ } catch (error) {
1216
+ displayError(error instanceof Error ? error.message : "Failed to reset token usage");
1217
+ process.exit(1);
1218
+ }
1219
+ });
1220
+
1221
+ hooksCmd
1222
+ .command("remove")
1223
+ .description("Remove all hooks")
1224
+ .action(async () => {
1225
+ try {
1226
+ await removeAllHooks();
1227
+ displaySuccess("All hooks removed");
1228
+ console.log();
1229
+ } catch (error) {
1230
+ displayError(error instanceof Error ? error.message : "Failed to remove hooks");
1231
+ process.exit(1);
1232
+ }
1233
+ });
1234
+
1235
+ hooksCmd
1236
+ .command("remove-token")
1237
+ .description("Remove token tracker")
1238
+ .action(async () => {
1239
+ try {
1240
+ await removeTokenTracker();
1241
+ displaySuccess("Token tracker removed");
1242
+ console.log();
1243
+ } catch (error) {
1244
+ displayError(error instanceof Error ? error.message : "Failed to remove token tracker");
1245
+ process.exit(1);
1246
+ }
1247
+ });
1248
+
1249
+ hooksCmd
1250
+ .command("remove-visual")
1251
+ .description("Remove visual enhancements")
1252
+ .action(async () => {
1253
+ try {
1254
+ await removeVisualEnhancements();
1255
+ displaySuccess("Visual enhancements removed");
1256
+ console.log();
1257
+ } catch (error) {
1258
+ displayError(error instanceof Error ? error.message : "Failed to remove visual enhancements");
1259
+ process.exit(1);
1260
+ }
1261
+ });
1262
+
1263
+ program.parse();