claude-code-provider-switch 1.1.4 → 1.1.5

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.
@@ -187,6 +187,12 @@ async function main(forceMenu = false) {
187
187
  return;
188
188
  }
189
189
 
190
+ if (args[0] === "api-keys") {
191
+ const { showApiKeyMenu } = require("../lib/config");
192
+ await showApiKeyMenu();
193
+ return;
194
+ }
195
+
190
196
  // Handle --version flag
191
197
  if (args[0] === "--version" || args[0] === "-v") {
192
198
  try {
@@ -249,6 +255,13 @@ async function main(forceMenu = false) {
249
255
  case "show-defaults":
250
256
  showDefaults();
251
257
  return;
258
+ case "api-keys":
259
+ const { showApiKeyMenu } = require("../lib/config");
260
+ await showApiKeyMenu();
261
+ // Show menu again after API key management
262
+ const newSelectedProvider = await showProviderMenu();
263
+ // Handle the new selection recursively
264
+ return main(true);
252
265
  }
253
266
 
254
267
  // For provider selection, show model selection
package/index.js CHANGED
@@ -1,24 +1,30 @@
1
1
  /**
2
2
  * Claude Code Provider Switcher - Main Entry Point
3
- *
3
+ *
4
4
  * This is the main entry point for importing the package as a module.
5
5
  * For CLI usage, use the bin/claude-switch.js file.
6
6
  */
7
7
 
8
8
  module.exports = {
9
9
  // Configuration utilities
10
- ...require('./lib/config'),
11
-
10
+ ...require("./lib/config"),
11
+
12
12
  // Provider launchers
13
- launchOpenRouter: require('./lib/openrouter').launchOpenRouter,
14
- launchAnthropic: require('./lib/anthropic').launchAnthropic,
15
- launchOllama: require('./lib/ollama').launchOllama,
16
- launchDefault: require('./lib/default').launchDefault,
17
-
13
+ launchOpenRouter: require("./lib/openrouter").launchOpenRouter,
14
+ launchAnthropic: require("./lib/anthropic").launchAnthropic,
15
+ launchOllama: require("./lib/ollama").launchOllama,
16
+ launchDefault: require("./lib/default").launchDefault,
17
+
18
18
  // Menu functions
19
- showProviderMenu: require('./lib/menu').showProviderMenu,
20
- showUsage: require('./lib/menu').showUsage,
21
- showDefaults: require('./lib/menu').showDefaults,
22
- setupDefaults: require('./lib/menu').setupDefaults,
23
- showModelSelectionForProvider: require('./lib/menu').showModelSelectionForProvider,
19
+ showProviderMenu: require("./lib/menu").showProviderMenu,
20
+ showUsage: require("./lib/menu").showUsage,
21
+ showDefaults: require("./lib/menu").showDefaults,
22
+ setupDefaults: require("./lib/menu").setupDefaults,
23
+ showModelSelectionForProvider:
24
+ require("./lib/menu").showModelSelectionForProvider,
25
+
26
+ // API key management
27
+ showApiKeyMenu: require("./lib/config").showApiKeyMenu,
28
+ updateApiKey: require("./lib/config").updateApiKey,
29
+ maskApiKey: require("./lib/config").maskApiKey,
24
30
  };
package/lib/config.js CHANGED
@@ -261,6 +261,223 @@ function getProviderDefaultModel(provider, envVars = null) {
261
261
  return modelMap[provider] || "";
262
262
  }
263
263
 
264
+ /**
265
+ * Show API key management menu
266
+ */
267
+ async function showApiKeyMenu() {
268
+ const readline = require("readline");
269
+ const rl = readline.createInterface({
270
+ input: process.stdin,
271
+ output: process.stdout,
272
+ });
273
+
274
+ const providers = [
275
+ { id: "openrouter", name: "OpenRouter", envVar: "OPENROUTER_AUTH_TOKEN" },
276
+ { id: "anthropic", name: "Anthropic", envVar: "ANTHROPIC_API_KEY" },
277
+ { id: "ollama", name: "Ollama", envVar: "OLLAMA_AUTH_TOKEN" },
278
+ ];
279
+
280
+ let selectedIndex = 0;
281
+
282
+ function displayMenu() {
283
+ console.clear();
284
+ log("API Key Management", "cyan");
285
+ log("", "reset");
286
+ log("Select a provider to update its API key:", "yellow");
287
+ log("", "reset");
288
+
289
+ providers.forEach((provider, index) => {
290
+ const marker = index === selectedIndex ? "❯" : " ";
291
+ const envVars = loadEnvFile();
292
+ const hasKey = envVars[provider.envVar] ? "✅" : "❌";
293
+ log(
294
+ `${marker} ${index + 1}) ${provider.name} - ${hasKey}`,
295
+ index === selectedIndex ? "green" : "reset",
296
+ );
297
+ });
298
+
299
+ log("", "reset");
300
+ log("Controls:", "yellow");
301
+ log("↑/↓ - Navigate", "reset");
302
+ log("Enter - Update API key", "reset");
303
+ log("1-3 - Quick select", "reset");
304
+ log("ESC - Return to main menu", "reset");
305
+ log("", "reset");
306
+ log("💡 Tips:", "cyan");
307
+ log("• ✅ = API key is set", "reset");
308
+ log("• ❌ = API key is missing", "reset");
309
+ log("• Press Enter to set or update the API key", "reset");
310
+ }
311
+
312
+ displayMenu();
313
+
314
+ return new Promise((resolve) => {
315
+ const handleKeyPress = (str, key) => {
316
+ if (key.name === "up") {
317
+ selectedIndex =
318
+ (selectedIndex - 1 + providers.length) % providers.length;
319
+ displayMenu();
320
+ } else if (key.name === "down") {
321
+ selectedIndex = (selectedIndex + 1) % providers.length;
322
+ displayMenu();
323
+ } else if (key.name === "escape") {
324
+ process.stdin.removeAllListeners("keypress");
325
+ rl.close();
326
+ resolve();
327
+ } else if (key.name === "return") {
328
+ process.stdin.removeAllListeners("keypress");
329
+ rl.close();
330
+ updateApiKey(providers[selectedIndex]).then(resolve);
331
+ } else if (key.name >= "1" && key.name <= "3") {
332
+ const index = parseInt(key.name) - 1;
333
+ if (index < providers.length) {
334
+ selectedIndex = index;
335
+ process.stdin.removeAllListeners("keypress");
336
+ rl.close();
337
+ updateApiKey(providers[selectedIndex]).then(resolve);
338
+ }
339
+ }
340
+ };
341
+
342
+ process.stdin.on("keypress", handleKeyPress);
343
+ });
344
+ }
345
+
346
+ /**
347
+ * Update API key for a specific provider
348
+ */
349
+ async function updateApiKey(provider) {
350
+ const envVars = loadEnvFile();
351
+ const currentKey = envVars[provider.envVar] || "";
352
+
353
+ log(`\n=== ${provider.name} API Key Management ===`, "cyan");
354
+
355
+ if (currentKey) {
356
+ log(`Current API key: ${maskApiKey(currentKey)}`, "yellow");
357
+ log("", "reset");
358
+
359
+ const options = [
360
+ { id: "update", name: "Update API key" },
361
+ { id: "remove", name: "Remove API key" },
362
+ { id: "cancel", name: "Cancel" },
363
+ ];
364
+
365
+ let selectedIndex = 0;
366
+
367
+ function displayOptions() {
368
+ log("Options:", "yellow");
369
+ options.forEach((option, index) => {
370
+ const marker = index === selectedIndex ? "❯" : " ";
371
+ log(
372
+ `${marker} ${option.name}`,
373
+ index === selectedIndex ? "green" : "reset",
374
+ );
375
+ });
376
+ log("", "reset");
377
+ log("Controls:", "yellow");
378
+ log("↑/↓ - Navigate", "reset");
379
+ log("Enter - Select option", "reset");
380
+ log("ESC - Cancel", "reset");
381
+ }
382
+
383
+ displayOptions();
384
+
385
+ const readline = require("readline");
386
+ const rl = readline.createInterface({
387
+ input: process.stdin,
388
+ output: process.stdout,
389
+ });
390
+
391
+ const choice = await new Promise((resolve) => {
392
+ const handleKeyPress = (str, key) => {
393
+ if (key.name === "up") {
394
+ selectedIndex = (selectedIndex - 1 + options.length) % options.length;
395
+ // Clear and redraw
396
+ console.clear();
397
+ log(`=== ${provider.name} API Key Management ===`, "cyan");
398
+ log(`Current API key: ${maskApiKey(currentKey)}`, "yellow");
399
+ log("", "reset");
400
+ displayOptions();
401
+ } else if (key.name === "down") {
402
+ selectedIndex = (selectedIndex + 1) % options.length;
403
+ // Clear and redraw
404
+ console.clear();
405
+ log(`=== ${provider.name} API Key Management ===`, "cyan");
406
+ log(`Current API key: ${maskApiKey(currentKey)}`, "yellow");
407
+ log("", "reset");
408
+ displayOptions();
409
+ } else if (key.name === "escape") {
410
+ process.stdin.removeAllListeners("keypress");
411
+ rl.close();
412
+ resolve("cancel");
413
+ } else if (key.name === "return") {
414
+ process.stdin.removeAllListeners("keypress");
415
+ rl.close();
416
+ resolve(options[selectedIndex].id);
417
+ }
418
+ };
419
+
420
+ process.stdin.on("keypress", handleKeyPress);
421
+ readline.emitKeypressEvents(process.stdin);
422
+ if (process.stdin.setRawMode) {
423
+ process.stdin.setRawMode(true);
424
+ }
425
+ rl.question("", () => {});
426
+ });
427
+
428
+ if (choice === "update") {
429
+ const newKey = await promptForApiKey(provider.name, provider.envVar);
430
+ if (newKey) {
431
+ updateEnvFile(provider.envVar, newKey);
432
+ log(`${provider.name} API key updated successfully!`, "green");
433
+ }
434
+ } else if (choice === "remove") {
435
+ updateEnvFile(provider.envVar, "");
436
+ log(`${provider.name} API key removed!`, "yellow");
437
+ } else {
438
+ log("Operation cancelled.", "yellow");
439
+ }
440
+ } else {
441
+ log(`No ${provider.name} API key is currently set.`, "yellow");
442
+ log("", "reset");
443
+ const newKey = await promptForApiKey(provider.name, provider.envVar);
444
+ if (newKey) {
445
+ updateEnvFile(provider.envVar, newKey);
446
+ log(`${provider.name} API key set successfully!`, "green");
447
+ } else {
448
+ log("Operation cancelled.", "yellow");
449
+ }
450
+ }
451
+
452
+ log("", "reset");
453
+ log("Press Enter to continue...", "cyan");
454
+
455
+ const rl = require("readline").createInterface({
456
+ input: process.stdin,
457
+ output: process.stdout,
458
+ });
459
+
460
+ await new Promise((resolve) => {
461
+ rl.question("", () => {
462
+ rl.close();
463
+ resolve();
464
+ });
465
+ });
466
+ }
467
+
468
+ /**
469
+ * Mask API key for display
470
+ */
471
+ function maskApiKey(apiKey) {
472
+ if (!apiKey) return "Not set";
473
+ if (apiKey.length <= 8) return "*".repeat(apiKey.length);
474
+ return (
475
+ apiKey.substring(0, 4) +
476
+ "*".repeat(apiKey.length - 8) +
477
+ apiKey.substring(apiKey.length - 4)
478
+ );
479
+ }
480
+
264
481
  module.exports = {
265
482
  colors,
266
483
  log,
@@ -275,4 +492,7 @@ module.exports = {
275
492
  setDefaultProvider,
276
493
  setDefaultModel,
277
494
  getProviderDefaultModel,
495
+ showApiKeyMenu,
496
+ updateApiKey,
497
+ maskApiKey,
278
498
  };
package/lib/menu.js CHANGED
@@ -9,6 +9,7 @@ const {
9
9
  getProviderDefaultModel,
10
10
  setDefaultProvider,
11
11
  setDefaultModel,
12
+ showApiKeyMenu,
12
13
  } = require("./config");
13
14
  const {
14
15
  showModelSelection: showOpenRouterModelSelection,
@@ -77,6 +78,7 @@ function showProviderMenu() {
77
78
  aliases: ["original", "orig", "def", "d"],
78
79
  },
79
80
  { id: "set-default", name: "Set as Default", aliases: ["set-default"] },
81
+ { id: "api-keys", name: "Manage API Keys", aliases: ["api-keys", "keys"] },
80
82
  { id: "help", name: "Help", aliases: ["help", "-h", "--help"] },
81
83
  ];
82
84
 
@@ -111,18 +113,21 @@ function showProviderMenu() {
111
113
  const isDefault = provider.id === defaultProvider;
112
114
  const defaultIndicator = isDefault ? " [DEFAULT]" : "";
113
115
  const isSetDefaultOption = provider.id === "set-default";
116
+ const isApiKeysOption = provider.id === "api-keys";
114
117
  const aliases =
115
118
  provider.aliases.length > 0
116
119
  ? ` Aliases: (${provider.aliases.join(", ")})`
117
120
  : "";
118
121
 
119
- // Highlight "Set as Default" option in orange to make it stand out
122
+ // Highlight special options with different colors
120
123
  const color =
121
124
  index === selectedIndex
122
125
  ? "green"
123
126
  : isSetDefaultOption
124
127
  ? "orange"
125
- : "reset";
128
+ : isApiKeysOption
129
+ ? "cyan"
130
+ : "reset";
126
131
 
127
132
  log(
128
133
  `${marker} ${index + 1}) ${provider.name}${defaultIndicator}${aliases}`,
@@ -134,7 +139,7 @@ function showProviderMenu() {
134
139
  log("Controls:", "yellow");
135
140
  log("↑/↓ - Navigate", "reset");
136
141
  log("Enter - Select provider", "reset");
137
- log("1-6 - Quick select", "reset");
142
+ log("1-7 - Quick select", "reset");
138
143
  log("ESC - Exit", "reset");
139
144
  log("", "reset");
140
145
 
@@ -197,7 +202,7 @@ function showProviderMenu() {
197
202
  process.stdin.removeAllListeners("keypress");
198
203
  rl.close();
199
204
  resolve(providers[selectedIndex]);
200
- } else if (str && /^[1-6]$/.test(str)) {
205
+ } else if (str && /^[1-7]$/.test(str)) {
201
206
  const index = parseInt(str) - 1;
202
207
  if (index >= 0 && index < providers.length) {
203
208
  selectedIndex = index;
@@ -483,6 +488,7 @@ function showUsage() {
483
488
  );
484
489
  log(" show-defaults - Display current default settings", "reset");
485
490
  log(" clear-defaults - Reset all default settings", "reset");
491
+ log(" api-keys - Manage API keys for providers", "reset");
486
492
  log("", "reset");
487
493
  log("Options:", "reset");
488
494
  log(" --model - Show interactive model selection menu", "reset");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-provider-switch",
3
- "version": "1.1.4",
3
+ "version": "1.1.5",
4
4
  "description": "Cross-platform Claude Code provider switcher (OpenRouter, Ollama, Anthropic)",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -24,13 +24,13 @@
24
24
  "keywords": [
25
25
  "claude",
26
26
  "claude-code",
27
- "openrouter",
28
- "llm switcher",
29
- "llm provider switcher",
30
- "ai provider switcher",
31
- "ai model switcher",
32
- "ai model provider switcher",
33
27
  "claude code provider switcher",
28
+ "claude code llm switcher",
29
+ "claude code llm provider switcher",
30
+ "ai provider switcher",
31
+ "claude code ai model switcher",
32
+ "claude code ai model provider switcher",
33
+ "claude code OpenRouter Ollama Anthropic",
34
34
  "cli"
35
35
  ],
36
36
  "author": "Adrian R",
@@ -41,7 +41,9 @@
41
41
  "files": [
42
42
  "bin/",
43
43
  "lib/",
44
- "README.md"
44
+ "test/",
45
+ "README.md",
46
+ "index.js"
45
47
  ],
46
48
  "preferGlobal": true
47
49
  }
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { spawn } = require("child_process");
4
+ const path = require("path");
5
+
6
+ console.log("🧪 Running complete claude-switch test suite...\n");
7
+
8
+ // Test files in order
9
+ const testFiles = [
10
+ { name: "Validation & Error Tests", file: "test-validation-errors.js" },
11
+ { name: "Provider Integration Tests", file: "test-provider-integration.js" },
12
+ { name: "Comprehensive Integration Tests", file: "test-comprehensive.js" },
13
+ ];
14
+
15
+ let currentTest = 0;
16
+
17
+ function runNextTest() {
18
+ if (currentTest >= testFiles.length) {
19
+ console.log("\n🎉 All test suites passed!");
20
+ console.log("\n📊 Test Coverage Summary:");
21
+ console.log(" ✅ Constants and configuration");
22
+ console.log(" ✅ Error handling and validation");
23
+ console.log(" ✅ Provider-specific functionality");
24
+ console.log(" ✅ Integration and end-to-end tests");
25
+ console.log(" ✅ Cache and performance tests");
26
+ console.log(" ✅ Environment and configuration tests");
27
+ process.exit(0);
28
+ return;
29
+ }
30
+
31
+ const test = testFiles[currentTest];
32
+ console.log(`📋 Running ${test.name}...`);
33
+
34
+ const testProcess = spawn("node", [path.join(__dirname, test.file)], {
35
+ stdio: "inherit",
36
+ });
37
+
38
+ testProcess.on("close", (code) => {
39
+ if (code === 0) {
40
+ console.log(`✅ ${test.name} passed!\n`);
41
+ currentTest++;
42
+ runNextTest();
43
+ } else {
44
+ console.log(`\n❌ ${test.name} failed!`);
45
+ process.exit(1);
46
+ }
47
+ });
48
+
49
+ testProcess.on("error", (error) => {
50
+ console.log(`\n💥 Failed to run ${test.name}: ${error.message}`);
51
+ process.exit(1);
52
+ });
53
+ }
54
+
55
+ // Start running tests
56
+ runNextTest();