claude-code-provider-switch 1.1.5 → 1.1.6

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/README.md CHANGED
@@ -72,13 +72,16 @@ claude-switch show-defaults
72
72
 
73
73
  # Clear all defaults
74
74
  claude-switch clear-defaults
75
+
76
+ # Manage API keys interactively
77
+ claude-switch api-keys
75
78
  ```
76
79
 
77
80
  ## ⚙️ Configuration
78
81
 
79
82
  ### Environment Variables
80
83
 
81
- The CLI is designed to create a `.env` file in your project directory with this syntax:
84
+ The CLI is designed to create a `~/.claude/.claude-switch-env` (in your home directory) with this syntax:
82
85
 
83
86
  ```env
84
87
  # API Keys for different providers
@@ -92,12 +95,69 @@ ANTHROPIC_MODEL=claude-3-5-sonnet-latest
92
95
  OLLAMA_MODEL=minimax-m2.5:cloud
93
96
 
94
97
  # Default provider and model settings
95
- DEFAULT_PROVIDER=original
98
+ DEFAULT_PROVIDER=default # Use 'default' to show menu on startup, or set to 'openrouter', 'anthropic', 'ollama', or 'original'
96
99
  DEFAULT_MODEL=
97
100
  ```
98
101
 
99
102
  When you run the CLI for the first time, based on your selection, it will ask for the API keys or Auth Tokens and after this will be saved in the `.env` file for future use.
100
103
 
104
+ ### Global vs Local Configuration
105
+
106
+ The CLI supports two configuration modes:
107
+
108
+ #### Global Configuration (Default)
109
+
110
+ - **Location**: `~/.claude/.claude-switch-env` (in your home directory)
111
+ - **Priority**: Used when no local `.env` file exists
112
+ - **Benefit**: Share configuration across all projects
113
+ - **Creation**: Automatically created on first run when no local `.env` exists
114
+
115
+ #### Local Configuration
116
+
117
+ - **Location**: `.env` file in your project directory
118
+ - **Priority**: Overrides global configuration when present
119
+ - **Benefit**: Project-specific settings
120
+ - **Creation**: Create manually or use "Save Configuration Locally" menu option
121
+
122
+ #### Configuration Display
123
+
124
+ The main menu shows which configuration source is active:
125
+
126
+ ```
127
+ Configuration: Global (~/.claude/.claude-switch-en)
128
+ Configuration: Local (current_folder/.env)
129
+ ```
130
+
131
+ #### Menu Behavior
132
+
133
+ - **No defaults set**: Shows interactive menu for provider selection
134
+ - **Defaults set**: Auto-launches with configured provider
135
+ - **Fresh install**: Always shows menu until you set defaults
136
+
137
+ #### Switching Between Modes
138
+
139
+ - **To Local**: Use "Save Configuration Locally" option from main menu
140
+ - **To Global**: Delete local `.env` file to fall back to global config
141
+ - **Priority**: Local always takes precedence over global when both exist
142
+
143
+ ### API Key Management
144
+
145
+ For easier API key management, use the interactive menu:
146
+
147
+ ```bash
148
+ # Launch interactive API key management
149
+ claude-switch api-keys
150
+ ```
151
+
152
+ Or access it through the main menu (option 6) when you run `claude-switch` without arguments.
153
+
154
+ **Features:**
155
+
156
+ - 🔐 **Secure**: API keys are masked for display (shows only first 4 and last 4 characters)
157
+ - 🎯 **Visual**: Clear status indicators (✅/❌) show which providers have keys configured
158
+ - ⚡ **Interactive**: Arrow key navigation with visual selection indicators
159
+ - 🔄 **Flexible**: Update, remove, or set new API keys interactively
160
+
101
161
  ### Provider Setup
102
162
 
103
163
  #### OpenRouter
@@ -137,13 +197,14 @@ When you run the CLI for the first time, based on your selection, it will ask fo
137
197
 
138
198
  ### Configuration Commands
139
199
 
140
- | Command | Description | Example |
141
- | ---------------- | ------------------------- | ------------------------------ |
142
- | `set-default` | Interactive default setup | `claude-switch set-default` |
143
- | `show-defaults` | View current defaults | `claude-switch show-defaults` |
144
- | `clear-defaults` | Reset all defaults | `claude-switch clear-defaults` |
145
- | `help` | Show help information | `claude-switch --help` |
146
- | `version` | Show Version information | `claude-switch --version` |
200
+ | Command | Description | Example |
201
+ | ---------------- | ----------------------------- | ------------------------------ |
202
+ | `set-default` | Interactive default setup | `claude-switch set-default` |
203
+ | `show-defaults` | View current defaults | `claude-switch show-defaults` |
204
+ | `clear-defaults` | Reset all defaults | `claude-switch clear-defaults` |
205
+ | `api-keys` | Manage API keys interactively | `claude-switch api-keys` |
206
+ | `help` | Show help information | `claude-switch --help` |
207
+ | `version` | Show Version information | `claude-switch --version` |
147
208
 
148
209
  ### Aliases
149
210
 
@@ -154,7 +215,8 @@ When you run the CLI for the first time, based on your selection, it will ask fo
154
215
  | `ollama` | `oll` |
155
216
  | `original` | `original`, `orig`, `def`, `d` |
156
217
 
157
- ## ⚠️ Important: Clearing Default Settings
218
+
219
+ ### ⚠️ Important:Clearing Defaults
158
220
 
159
221
  **If the interactive menu doesn't show and Claude Code opens directly with a predefined provider**, you need to clear the default settings:
160
222
 
@@ -164,7 +226,7 @@ claude-switch clear-defaults
164
226
 
165
227
  ### Why This Happens
166
228
 
167
- If you go through the option `set-default`, you are setting a default provider and model and the application remembers your last provider choice to provide a faster experience. However, if you want to change providers or access the full menu again, you must clear these defaults first.
229
+ When you use the "Set as Default" option, the application saves your provider and model choices to provide a faster experience. However, if you want to change providers or access the full menu again, you must clear these defaults first.
168
230
 
169
231
  ### What Clearing Defaults Does
170
232
 
@@ -197,12 +259,13 @@ Available providers:
197
259
  3) Anthropic Aliases: (anthropic, ant)
198
260
  4) Original Claude Code Aliases: (original, orig, def, d)
199
261
  5) Set as Default Aliases: (set-default)
200
- 6) Help Aliases: (help, -h, --help)
262
+ 6) Manage API Keys Aliases: (api-keys, keys)
263
+ 7) Help Aliases: (help, -h, --help)
201
264
 
202
265
  Controls:
203
266
  ↑/↓ - Navigate
204
267
  Enter - Select provider
205
- 1-6 - Quick select
268
+ 1-7 - Quick select
206
269
  ESC - Exit
207
270
  ```
208
271
 
@@ -218,6 +281,9 @@ const {
218
281
  getDefaultProvider,
219
282
  launchOpenRouter,
220
283
  launchAnthropic,
284
+ showApiKeyMenu,
285
+ updateApiKey,
286
+ maskApiKey,
221
287
  } = require("claude-code-provider-switch");
222
288
 
223
289
  // Set default provider programmatically
@@ -229,6 +295,15 @@ console.log(`Current provider: ${current}`);
229
295
 
230
296
  // Launch specific provider
231
297
  await launchOpenRouter(false, [], "gpt-4");
298
+
299
+ // Manage API keys programmatically
300
+ await showApiKeyMenu(); // Show interactive API key menu
301
+ await updateApiKey({
302
+ id: "openrouter",
303
+ name: "OpenRouter",
304
+ envVar: "OPENROUTER_AUTH_TOKEN",
305
+ }); // Update specific API key
306
+ console.log(maskApiKey("sk-1234567890abcdef")); // "sk-1234...cdef"
232
307
  ```
233
308
 
234
309
  ### Development
@@ -187,6 +187,12 @@ async function main(forceMenu = false) {
187
187
  return;
188
188
  }
189
189
 
190
+ if (args[0] === "save-local" || args[0] === "save-locally") {
191
+ const { saveConfigurationLocally } = require("../lib/config");
192
+ await saveConfigurationLocally();
193
+ return;
194
+ }
195
+
190
196
  if (args[0] === "api-keys") {
191
197
  const { showApiKeyMenu } = require("../lib/config");
192
198
  await showApiKeyMenu();
@@ -212,7 +218,11 @@ async function main(forceMenu = false) {
212
218
  const defaultProvider = getDefaultProvider();
213
219
  const defaultModel = getDefaultModel();
214
220
 
215
- if (defaultProvider && defaultProvider !== "default") {
221
+ if (
222
+ defaultProvider &&
223
+ defaultProvider !== null &&
224
+ defaultProvider !== "default"
225
+ ) {
216
226
  const { log } = require("../lib/config");
217
227
  log(
218
228
  `Using default: ${defaultProvider}${defaultModel ? ` (${defaultModel})` : ""}`,
@@ -240,52 +250,70 @@ async function main(forceMenu = false) {
240
250
  return;
241
251
  }
242
252
 
243
- // No defaults set, show interactive menu
244
- const selectedProvider = await showProviderMenu();
245
-
246
- // Handle special menu options
247
- switch (selectedProvider.id) {
248
- case "help":
249
- showUsage();
250
- return;
251
- case "set-default":
252
- await setupDefaults();
253
- handlePostConfiguration("restart", true);
254
- return;
255
- case "show-defaults":
256
- showDefaults();
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);
265
- }
253
+ // No defaults set, show interactive menu - use loop to prevent recursion issues
254
+ mainLoop: while (true) {
255
+ const selectedProvider = await showProviderMenu();
256
+
257
+ // Handle special menu options
258
+ switch (selectedProvider.id) {
259
+ case "help":
260
+ showUsage();
261
+ return;
262
+ case "set-default":
263
+ await setupDefaults();
264
+ handlePostConfiguration("restart", true);
265
+ return;
266
+ case "show-defaults":
267
+ showDefaults();
268
+ return;
269
+ case "api-keys":
270
+ const { showApiKeyMenu } = require("../lib/config");
271
+ await showApiKeyMenu();
272
+ // Continue the loop to show menu again
273
+ continue mainLoop;
274
+ case "save-local":
275
+ const { saveConfigurationLocally, log } = require("../lib/config");
276
+ await saveConfigurationLocally();
277
+ // Show menu again after saving locally
278
+ log("Press Enter to continue...", "cyan");
279
+ const continueRl = require("readline").createInterface({
280
+ input: process.stdin,
281
+ output: process.stdout,
282
+ });
283
+ await new Promise((resolve) => {
284
+ continueRl.question("", () => {
285
+ continueRl.close();
286
+ resolve();
287
+ });
288
+ });
289
+ // Continue the loop to show menu again
290
+ continue mainLoop;
291
+ }
266
292
 
267
- // For provider selection, show model selection
268
- let selectedModel = null;
269
- if (selectedProvider.id !== "original") {
270
- selectedModel = await showModelSelectionForProvider(selectedProvider);
271
- }
293
+ // For provider selection, show model selection
294
+ let selectedModel = null;
295
+ if (selectedProvider.id !== "original") {
296
+ selectedModel = await showModelSelectionForProvider(selectedProvider);
297
+ }
272
298
 
273
- // Launch the selected provider with the selected model
274
- switch (selectedProvider.id) {
275
- case "openrouter":
276
- await launchOpenRouter(false, [], selectedModel);
277
- break;
278
- case "anthropic":
279
- await launchAnthropic(false, [], selectedModel);
280
- break;
281
- case "ollama":
282
- await launchOllama(false, [], selectedModel);
283
- break;
284
- case "original":
285
- await launchDefault([]);
286
- break;
299
+ // Launch the selected provider with the selected model
300
+ switch (selectedProvider.id) {
301
+ case "openrouter":
302
+ await launchOpenRouter(false, [], selectedModel);
303
+ break;
304
+ case "anthropic":
305
+ await launchAnthropic(false, [], selectedModel);
306
+ break;
307
+ case "ollama":
308
+ const modelToUse = selectedModel || getProviderDefaultModel("ollama");
309
+ await launchOllama(false, [], modelToUse);
310
+ break;
311
+ case "original":
312
+ await launchDefault([]);
313
+ break;
314
+ }
315
+ return;
287
316
  }
288
- return;
289
317
  }
290
318
 
291
319
  const command = args[0].toLowerCase();
package/lib/anthropic.js CHANGED
@@ -8,7 +8,7 @@ const {
8
8
  log,
9
9
  loadEnvFile,
10
10
  promptForApiKey,
11
- updateEnvFile,
11
+ updateConfigFile,
12
12
  findBestMatchingModel,
13
13
  } = require("./config");
14
14
  const { modelCache } = require("./cache");
@@ -151,7 +151,7 @@ async function showModelSelection() {
151
151
  log("Anthropic API key is required for model selection", "red");
152
152
  process.exit(1);
153
153
  }
154
- updateEnvFile("ANTHROPIC_API_KEY", newApiKey);
154
+ updateConfigFile("ANTHROPIC_API_KEY", newApiKey, null);
155
155
  apiKey = newApiKey;
156
156
  }
157
157
 
@@ -436,8 +436,6 @@ async function launchAnthropic(
436
436
  extraArgs = [],
437
437
  directModel = null,
438
438
  ) {
439
- log("Launching Claude Code with Anthropic settings...", "green");
440
-
441
439
  const envVars = loadEnvFile();
442
440
  log(`Loading environment from: ${envVars.envFile}`, "yellow");
443
441
 
@@ -448,7 +446,7 @@ async function launchAnthropic(
448
446
  log("Error: Anthropic API key is required", "red");
449
447
  process.exit(1);
450
448
  }
451
- updateEnvFile("ANTHROPIC_API_KEY", apiKey);
449
+ updateConfigFile("ANTHROPIC_API_KEY", apiKey, null);
452
450
  envVars.ANTHROPIC_API_KEY = apiKey;
453
451
  }
454
452
 
package/lib/config.js CHANGED
@@ -23,14 +23,236 @@ function log(message, color = "reset") {
23
23
  console.log(`${colorCode}${message}${colors.reset}`);
24
24
  }
25
25
 
26
+ // Get user home directory for global configuration
27
+ const os = require("os");
28
+ const homeDir = os.homedir();
29
+ const claudeDir = path.join(homeDir, ".claude");
30
+ const globalEnvFile = path.join(claudeDir, ".claude-switch-env");
31
+
26
32
  // Get script directory (use current working directory for global installs)
27
33
  const scriptPath = process.cwd();
28
- const envFile = path.join(scriptPath, ".env");
34
+ const localEnvFile = path.join(scriptPath, ".env");
35
+
36
+ // Use global env file by default, fallback to local
37
+ const envFile = globalEnvFile;
38
+
39
+ /**
40
+ * Get configuration source (Local or Global)
41
+ */
42
+ function getConfigurationSource() {
43
+ if (fs.existsSync(localEnvFile)) {
44
+ return "Local";
45
+ }
46
+ return "Global";
47
+ }
48
+
49
+ /**
50
+ * Get configuration file path for display
51
+ */
52
+ function getConfigurationPath() {
53
+ if (fs.existsSync(localEnvFile)) {
54
+ return localEnvFile;
55
+ }
56
+ return globalEnvFile;
57
+ }
58
+
59
+ /**
60
+ * Check if global configuration exists and has meaningful content
61
+ */
62
+ function hasGlobalConfiguration() {
63
+ try {
64
+ if (!fs.existsSync(globalEnvFile)) {
65
+ return false;
66
+ }
67
+
68
+ const globalConfig = loadSingleEnvFile(globalEnvFile);
69
+
70
+ // Check for at least one API key or default setting
71
+ const hasApiKey =
72
+ globalConfig.OPENROUTER_AUTH_TOKEN ||
73
+ globalConfig.ANTHROPIC_API_KEY ||
74
+ globalConfig.OLLAMA_AUTH_TOKEN;
75
+
76
+ const hasDefaultSetting =
77
+ globalConfig.DEFAULT_PROVIDER || globalConfig.DEFAULT_MODEL;
78
+
79
+ return !!(hasApiKey || hasDefaultSetting);
80
+ } catch (error) {
81
+ return false;
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Save configuration locally by copying global config to local .env
87
+ */
88
+ async function saveConfigurationLocally() {
89
+ // Ensure log is available in this context
90
+ const localLog = log;
91
+
92
+ try {
93
+ // Check if global config exists
94
+ if (!fs.existsSync(globalEnvFile)) {
95
+ localLog("No global configuration found to copy.", "red");
96
+ return false;
97
+ }
98
+
99
+ // Check if local file already exists
100
+ if (fs.existsSync(localEnvFile)) {
101
+ localLog("Local .env file already exists.", "yellow");
102
+ localLog("This will overwrite your local configuration.", "yellow");
103
+
104
+ // Ask for confirmation
105
+ const readline = require("readline");
106
+ const rl = readline.createInterface({
107
+ input: process.stdin,
108
+ output: process.stdout,
109
+ });
110
+
111
+ const answer = await new Promise((resolve) => {
112
+ rl.question("Do you want to continue? (y/N): ", resolve);
113
+ });
114
+ rl.close();
115
+
116
+ if (answer.toLowerCase() !== "y" && answer.toLowerCase() !== "yes") {
117
+ localLog("Operation cancelled.", "yellow");
118
+ return false;
119
+ }
120
+ }
121
+
122
+ // Copy global config to local
123
+ const globalContent = fs.readFileSync(globalEnvFile, "utf8");
124
+ fs.writeFileSync(localEnvFile, globalContent, "utf8");
125
+
126
+ // Clear cache to reflect changes
127
+ envCache = null;
128
+ envCacheTime = null;
129
+
130
+ localLog("Configuration saved locally!", "green");
131
+ localLog(`Local file created: ${localEnvFile}`, "cyan");
132
+ localLog("All future updates will use the local configuration.", "yellow");
133
+
134
+ return true;
135
+ } catch (error) {
136
+ localLog(`Error saving configuration locally: ${error.message}`, "red");
137
+ return false;
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Load a single environment file without recursion
143
+ */
144
+ function loadSingleEnvFile(filePath) {
145
+ try {
146
+ // Handle file existence race condition
147
+ if (!fs.existsSync(filePath)) {
148
+ if (filePath === globalEnvFile) {
149
+ log("Global environment file not found. Creating one...", "yellow");
150
+ createEnvFile(globalEnvFile);
151
+ }
152
+ // Return empty object for missing files (don't try to read)
153
+ return {};
154
+ }
155
+
156
+ // Read file with error handling
157
+ const envContent = fs.readFileSync(filePath, "utf8");
158
+ const envVars = {};
159
+
160
+ envContent.split("\n").forEach((line) => {
161
+ const trimmed = line.trim();
162
+ if (trimmed && !trimmed.startsWith("#")) {
163
+ const [key, ...valueParts] = trimmed.split("=");
164
+ if (key && valueParts.length > 0) {
165
+ envVars[key.trim()] = valueParts.join("=").trim();
166
+ }
167
+ }
168
+ });
169
+
170
+ return envVars;
171
+ } catch (error) {
172
+ log(`Error loading environment file ${filePath}: ${error.message}`, "red");
173
+ // Return empty object on error to prevent crashes
174
+ return {};
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Get configuration from both global and local files
180
+ * Local settings override global settings
181
+ */
182
+ function loadConfigFiles() {
183
+ const globalConfig = loadSingleEnvFile(globalEnvFile);
184
+ const localConfig = loadSingleEnvFile(localEnvFile);
185
+
186
+ // Merge configs, local takes precedence
187
+ return { ...globalConfig, ...localConfig };
188
+ }
189
+
190
+ /**
191
+ * Update configuration with smart targeting (local if exists, global if not)
192
+ */
193
+ function updateConfigFile(key, value, useGlobal = null) {
194
+ // Smart targeting: if useGlobal is null, determine based on file existence
195
+ let targetFile;
196
+ if (useGlobal === null) {
197
+ targetFile = fs.existsSync(localEnvFile) ? localEnvFile : globalEnvFile;
198
+ } else {
199
+ targetFile = useGlobal ? globalEnvFile : localEnvFile;
200
+ }
201
+
202
+ try {
203
+ // Ensure the directory exists
204
+ const targetDir = path.dirname(targetFile);
205
+ if (!fs.existsSync(targetDir)) {
206
+ fs.mkdirSync(targetDir, { recursive: true });
207
+ }
208
+
209
+ let content = "";
210
+
211
+ // Read existing file or create new one
212
+ if (fs.existsSync(targetFile)) {
213
+ content = fs.readFileSync(targetFile, "utf8");
214
+ } else {
215
+ createEnvFile(targetFile);
216
+ content = fs.readFileSync(targetFile, "utf8");
217
+ }
218
+
219
+ const lines = content.split("\n");
220
+ let keyFound = false;
221
+
222
+ // Update or add the key
223
+ const updatedLines = lines.map((line) => {
224
+ if (line.startsWith(`${key}=`)) {
225
+ keyFound = true;
226
+ return `${key}=${value}`;
227
+ }
228
+ return line;
229
+ });
230
+
231
+ if (!keyFound) {
232
+ updatedLines.push(`${key}=${value}`);
233
+ }
234
+
235
+ // Write back to file
236
+ fs.writeFileSync(targetFile, updatedLines.join("\n"), "utf8");
237
+
238
+ // Atomically invalidate cache
239
+ envCache = null;
240
+ envCacheTime = null;
241
+
242
+ log(
243
+ `Updated ${useGlobal ? "global" : "local"} configuration: ${key}`,
244
+ "green",
245
+ );
246
+ } catch (error) {
247
+ log(`Error updating configuration file: ${error.message}`, "red");
248
+ throw error;
249
+ }
250
+ }
29
251
 
30
252
  /**
31
253
  * Create default .env file if it doesn't exist
32
254
  */
33
- function createEnvFile() {
255
+ function createEnvFile(targetPath = globalEnvFile) {
34
256
  const defaultEnvContent = `# API Keys for different providers
35
257
  OPENROUTER_AUTH_TOKEN=
36
258
  ANTHROPIC_API_KEY=
@@ -42,14 +264,20 @@ ANTHROPIC_MODEL=claude-3-5-sonnet-latest
42
264
  OLLAMA_MODEL=minimax-m2.5:cloud
43
265
 
44
266
  # Default provider and model settings
45
- DEFAULT_PROVIDER=original
267
+ DEFAULT_PROVIDER=default
46
268
  DEFAULT_MODEL=
47
269
  `;
48
270
 
49
271
  try {
50
- fs.writeFileSync(envFile, defaultEnvContent, "utf8");
51
- log(`Created environment file: ${envFile}`, "green");
52
- log("Please add your API keys to .env file", "yellow");
272
+ // Ensure the directory exists
273
+ const targetDir = path.dirname(targetPath);
274
+ if (!fs.existsSync(targetDir)) {
275
+ fs.mkdirSync(targetDir, { recursive: true });
276
+ log(`Created directory: ${targetDir}`, "green");
277
+ }
278
+
279
+ fs.writeFileSync(targetPath, defaultEnvContent, "utf8");
280
+ log(`Created environment file: ${targetPath}`, "green");
53
281
  return true;
54
282
  } catch (error) {
55
283
  log(`Error creating .env file: ${error.message}`, "red");
@@ -127,6 +355,7 @@ const CACHE_TTL = 5 * 60 * 1000; // 5 minutes in milliseconds
127
355
 
128
356
  /**
129
357
  * Load environment variables from .env file with caching
358
+ * Uses merged configuration from global and local files
130
359
  */
131
360
  function loadEnvFile() {
132
361
  const now = Date.now();
@@ -137,38 +366,25 @@ function loadEnvFile() {
137
366
  }
138
367
 
139
368
  try {
140
- // Handle file existence race condition
141
- if (!fs.existsSync(envFile)) {
142
- log("Environment file not found. Creating one...", "yellow");
143
- createEnvFile();
144
- }
145
-
146
- // Read file with error handling
147
- const envContent = fs.readFileSync(envFile, "utf8");
148
- const envVars = {};
149
-
150
- envContent.split("\n").forEach((line) => {
151
- const trimmed = line.trim();
152
- if (trimmed && !trimmed.startsWith("#")) {
153
- const [key, ...valueParts] = trimmed.split("=");
154
- if (key && valueParts.length > 0) {
155
- envVars[key.trim()] = valueParts.join("=").trim();
156
- }
157
- }
158
- });
369
+ // Load and merge global and local configurations
370
+ const mergedConfig = loadConfigFiles();
159
371
 
160
372
  // Atomically update cache
161
- envCache = envVars;
373
+ envCache = mergedConfig;
162
374
  envCacheTime = now;
163
375
 
164
376
  // Include envFile path in the returned object for debugging
165
- envVars.envFile = envFile;
377
+ // Point envFile to the actual active configuration source
378
+ mergedConfig.envFile = fs.existsSync(localEnvFile)
379
+ ? localEnvFile
380
+ : globalEnvFile;
381
+ mergedConfig.localEnvFile = localEnvFile;
166
382
 
167
- return envVars;
383
+ return mergedConfig;
168
384
  } catch (error) {
169
- log(`Error loading environment file: ${error.message}`, "red");
385
+ log(`Error loading environment files: ${error.message}`, "red");
170
386
  // Return empty object on error to prevent crashes
171
- return { envFile };
387
+ return { envFile: globalEnvFile, localEnvFile };
172
388
  }
173
389
  }
174
390
 
@@ -210,7 +426,7 @@ function findBestMatchingModel(searchTerm, models) {
210
426
  */
211
427
  function getDefaultProvider(envVars = null) {
212
428
  const vars = envVars || loadEnvFile();
213
- return vars.DEFAULT_PROVIDER || "original";
429
+ return vars.DEFAULT_PROVIDER || null;
214
430
  }
215
431
 
216
432
  /**
@@ -222,7 +438,7 @@ function getDefaultModel(envVars = null) {
222
438
  }
223
439
 
224
440
  /**
225
- * Set default provider in .env file
441
+ * Set default provider with smart targeting (local if exists, global if not)
226
442
  */
227
443
  function setDefaultProvider(provider) {
228
444
  // Validate input
@@ -230,12 +446,12 @@ function setDefaultProvider(provider) {
230
446
  throw new Error("Provider must be a non-empty string");
231
447
  }
232
448
 
233
- updateEnvFile("DEFAULT_PROVIDER", provider.trim());
449
+ updateConfigFile("DEFAULT_PROVIDER", provider.trim(), null);
234
450
  log(`Default provider set to: ${provider.trim()}`, "green");
235
451
  }
236
452
 
237
453
  /**
238
- * Set default model in .env file
454
+ * Set default model with smart targeting (local if exists, global if not)
239
455
  */
240
456
  function setDefaultModel(model) {
241
457
  // Validate input (allow empty string for clearing)
@@ -243,7 +459,7 @@ function setDefaultModel(model) {
243
459
  throw new Error("Model must be a string");
244
460
  }
245
461
 
246
- updateEnvFile("DEFAULT_MODEL", model.trim());
462
+ updateConfigFile("DEFAULT_MODEL", model.trim(), null);
247
463
  log(`Default model set to: ${model.trim()}`, "green");
248
464
  }
249
465
 
@@ -328,9 +544,9 @@ async function showApiKeyMenu() {
328
544
  process.stdin.removeAllListeners("keypress");
329
545
  rl.close();
330
546
  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) {
547
+ } else if (str && /^[1-3]$/.test(str)) {
548
+ const index = parseInt(str) - 1;
549
+ if (index >= 0 && index < providers.length) {
334
550
  selectedIndex = index;
335
551
  process.stdin.removeAllListeners("keypress");
336
552
  rl.close();
@@ -339,7 +555,16 @@ async function showApiKeyMenu() {
339
555
  }
340
556
  };
341
557
 
558
+ // Set up raw mode for arrow key detection
559
+ if (process.stdin.setRawMode) {
560
+ process.stdin.setRawMode(true);
561
+ }
562
+
563
+ // Enable keypress events
564
+ readline.emitKeypressEvents(process.stdin);
565
+
342
566
  process.stdin.on("keypress", handleKeyPress);
567
+ rl.question("", () => {});
343
568
  });
344
569
  }
345
570
 
@@ -428,11 +653,11 @@ async function updateApiKey(provider) {
428
653
  if (choice === "update") {
429
654
  const newKey = await promptForApiKey(provider.name, provider.envVar);
430
655
  if (newKey) {
431
- updateEnvFile(provider.envVar, newKey);
656
+ updateConfigFile(provider.envVar, newKey, null);
432
657
  log(`${provider.name} API key updated successfully!`, "green");
433
658
  }
434
659
  } else if (choice === "remove") {
435
- updateEnvFile(provider.envVar, "");
660
+ updateConfigFile(provider.envVar, "", null);
436
661
  log(`${provider.name} API key removed!`, "yellow");
437
662
  } else {
438
663
  log("Operation cancelled.", "yellow");
@@ -442,7 +667,7 @@ async function updateApiKey(provider) {
442
667
  log("", "reset");
443
668
  const newKey = await promptForApiKey(provider.name, provider.envVar);
444
669
  if (newKey) {
445
- updateEnvFile(provider.envVar, newKey);
670
+ updateConfigFile(provider.envVar, newKey, null);
446
671
  log(`${provider.name} API key set successfully!`, "green");
447
672
  } else {
448
673
  log("Operation cancelled.", "yellow");
@@ -481,11 +706,17 @@ function maskApiKey(apiKey) {
481
706
  module.exports = {
482
707
  colors,
483
708
  log,
484
- envFile,
709
+ envFile: globalEnvFile,
710
+ localEnvFile,
711
+ globalEnvFile,
712
+ claudeDir,
485
713
  createEnvFile,
486
714
  promptForApiKey,
487
715
  updateEnvFile,
716
+ updateConfigFile,
488
717
  loadEnvFile,
718
+ loadSingleEnvFile,
719
+ loadConfigFiles,
489
720
  findBestMatchingModel,
490
721
  getDefaultProvider,
491
722
  getDefaultModel,
@@ -495,4 +726,8 @@ module.exports = {
495
726
  showApiKeyMenu,
496
727
  updateApiKey,
497
728
  maskApiKey,
729
+ getConfigurationSource,
730
+ getConfigurationPath,
731
+ hasGlobalConfiguration,
732
+ saveConfigurationLocally,
498
733
  };
package/lib/menu.js CHANGED
@@ -10,6 +10,9 @@ const {
10
10
  setDefaultProvider,
11
11
  setDefaultModel,
12
12
  showApiKeyMenu,
13
+ getConfigurationSource,
14
+ getConfigurationPath,
15
+ hasGlobalConfiguration,
13
16
  } = require("./config");
14
17
  const {
15
18
  showModelSelection: showOpenRouterModelSelection,
@@ -28,12 +31,6 @@ async function showModelSelectionForProvider(provider) {
28
31
 
29
32
  switch (provider.id) {
30
33
  case "openrouter":
31
- if (!envVars.OPENROUTER_AUTH_TOKEN) {
32
- log("OpenRouter auth token required for model selection", "red");
33
- log("Please set OPENROUTER_AUTH_TOKEN in .env file", "yellow");
34
- log("Using default model...", "yellow");
35
- return getProviderDefaultModel("openrouter");
36
- }
37
34
  return await showOpenRouterModelSelection();
38
35
 
39
36
  case "ollama":
@@ -60,10 +57,12 @@ function showProviderMenu() {
60
57
  output: process.stdout,
61
58
  });
62
59
 
63
- // Get current defaults
60
+ // Get current defaults and configuration source
64
61
  const defaultProvider = getDefaultProvider();
65
62
  const defaultModel = getDefaultModel();
63
+ const configSource = getConfigurationSource();
66
64
 
65
+ // Build providers array dynamically
67
66
  const providers = [
68
67
  {
69
68
  id: "openrouter",
@@ -79,9 +78,24 @@ function showProviderMenu() {
79
78
  },
80
79
  { id: "set-default", name: "Set as Default", aliases: ["set-default"] },
81
80
  { id: "api-keys", name: "Manage API Keys", aliases: ["api-keys", "keys"] },
82
- { id: "help", name: "Help", aliases: ["help", "-h", "--help"] },
83
81
  ];
84
82
 
83
+ // Add "Save Configuration Locally" option only if global configuration exists
84
+ if (hasGlobalConfiguration()) {
85
+ providers.push({
86
+ id: "save-local",
87
+ name: "Save Configuration Locally",
88
+ aliases: ["save-local", "local", "save-locally"],
89
+ });
90
+ }
91
+
92
+ // Always add Help at the end
93
+ providers.push({
94
+ id: "help",
95
+ name: "Help",
96
+ aliases: ["help", "-h", "--help"],
97
+ });
98
+
85
99
  let selectedIndex = 0;
86
100
 
87
101
  function displayMenu() {
@@ -89,8 +103,18 @@ function showProviderMenu() {
89
103
  log("Claude Code Provider Switcher", "green");
90
104
  log("", "reset");
91
105
 
106
+ // Show configuration source with file path
107
+ const configSource = getConfigurationSource();
108
+ const configPath = getConfigurationPath();
109
+ log(`Configuration: ${configSource} (${configPath})`, "cyan");
110
+ log("", "reset");
111
+
92
112
  // Show current defaults
93
- if (defaultProvider && defaultProvider !== "default") {
113
+ if (
114
+ defaultProvider &&
115
+ defaultProvider !== null &&
116
+ defaultProvider !== "default"
117
+ ) {
94
118
  const providerName =
95
119
  providers.find((p) => p.id === defaultProvider)?.name ||
96
120
  defaultProvider;
@@ -100,10 +124,8 @@ function showProviderMenu() {
100
124
  `Current default: ${providerName}${currentModel ? ` (${currentModel})` : ""}`,
101
125
  "yellow",
102
126
  );
103
- } else {
104
- log("No default provider set", "yellow");
127
+ log("", "reset");
105
128
  }
106
- log("", "reset");
107
129
 
108
130
  log("Available providers:", "yellow");
109
131
  log("", "reset");
@@ -135,51 +157,47 @@ function showProviderMenu() {
135
157
  );
136
158
  });
137
159
 
138
- log("", "reset");
139
- log("Controls:", "yellow");
140
- log("↑/↓ - Navigate", "reset");
141
- log("Enter - Select provider", "reset");
142
- log("1-7 - Quick select", "reset");
143
- log("ESC - Exit", "reset");
144
160
  log("", "reset");
145
161
 
146
162
  // Add helpful usage text
147
163
  log("💡 Quick Start:", "cyan");
148
164
  log("• Use ↑/↓ arrows to navigate providers", "reset");
149
165
  log("• Press Enter to launch selected provider", "reset");
166
+ log(`• Press 1-${providers.length} for quick select`, "reset");
150
167
  log("• Use 'Set as Default' to save your preferred provider", "reset");
151
168
  log("", "reset");
152
- log("Commands:", "yellow");
153
- log(
154
- " claude-switch - Show menu or use default",
155
- "reset",
156
- );
157
- log(
158
- " claude-switch openrouter - Use OpenRouter provider",
159
- "reset",
160
- );
161
- log(
162
- " claude-switch anthropic - Use Anthropic provider",
163
- "reset",
164
- );
165
- log(" claude-switch ollama - Use Ollama provider", "reset");
166
- log(
167
- " claude-switch set-default - Setup default provider",
168
- "reset",
169
- );
170
- log("", "reset");
171
- log("Model Selection:", "yellow");
172
- log(
173
- " claude-switch openrouter --model - Select OpenRouter model",
174
- "reset",
175
- );
176
- log(
177
- " claude-switch anthropic --model - Select Anthropic model",
178
- "reset",
179
- );
180
- log("", "reset");
181
- log("Type 'claude-switch --help' for complete documentation", "cyan");
182
- log("", "reset");
169
+ // Commented out Commands section for cleaner menu
170
+ // log("Commands:", "yellow");
171
+ // log(
172
+ // " claude-switch - Show menu or use default",
173
+ // "reset",
174
+ // );
175
+ // log(
176
+ // " claude-switch openrouter - Use OpenRouter provider",
177
+ // "reset",
178
+ // );
179
+ // log(
180
+ // " claude-switch anthropic - Use Anthropic provider",
181
+ // "reset",
182
+ // );
183
+ // log(" claude-switch ollama - Use Ollama provider", "reset");
184
+ // log(
185
+ // " claude-switch set-default - Setup default provider",
186
+ // "reset",
187
+ // );
188
+ // log("", "reset");
189
+ // log("Model Selection:", "yellow");
190
+ // log(
191
+ // " claude-switch openrouter --model - Select OpenRouter model",
192
+ // "reset",
193
+ // );
194
+ // log(
195
+ // " claude-switch anthropic --model - Select Anthropic model",
196
+ // "reset",
197
+ // );
198
+ // log("", "reset");
199
+ // log("Type 'claude-switch --help' for complete documentation", "cyan");
200
+ // log("", "reset");
183
201
  }
184
202
 
185
203
  displayMenu();
@@ -489,6 +507,10 @@ function showUsage() {
489
507
  log(" show-defaults - Display current default settings", "reset");
490
508
  log(" clear-defaults - Reset all default settings", "reset");
491
509
  log(" api-keys - Manage API keys for providers", "reset");
510
+ log(
511
+ " save-local - Save global configuration to local .env file",
512
+ "reset",
513
+ );
492
514
  log("", "reset");
493
515
  log("Options:", "reset");
494
516
  log(" --model - Show interactive model selection menu", "reset");
package/lib/ollama.js CHANGED
@@ -10,7 +10,7 @@ const {
10
10
  loadEnvFile,
11
11
  findBestMatchingModel,
12
12
  promptForApiKey,
13
- updateEnvFile,
13
+ updateConfigFile,
14
14
  } = require("./config");
15
15
  const { modelCache } = require("./cache");
16
16
  const { OLLAMA, CACHE, HTTP_STATUS, DEFAULT_MODELS } = require("./constants");
@@ -97,7 +97,7 @@ async function showModelSelection() {
97
97
  log("Ollama auth token is required for model selection", "red");
98
98
  process.exit(1);
99
99
  }
100
- updateEnvFile("OLLAMA_AUTH_TOKEN", newToken);
100
+ updateConfigFile("OLLAMA_AUTH_TOKEN", newToken, null);
101
101
  authToken = newToken;
102
102
  }
103
103
 
@@ -378,8 +378,6 @@ async function launchOllama(
378
378
  extraArgs = [],
379
379
  directModel = null,
380
380
  ) {
381
- log("Launching Claude Code with Ollama settings...", "green");
382
-
383
381
  const envVars = loadEnvFile();
384
382
  log(`Loading environment from: ${envVars.envFile}`, "yellow");
385
383
 
@@ -391,7 +389,7 @@ async function launchOllama(
391
389
  log("Press Enter to skip, or provide an auth token:", "reset");
392
390
  authToken = await promptForApiKey("Ollama (optional)", "OLLAMA_AUTH_TOKEN");
393
391
  if (authToken) {
394
- updateEnvFile("OLLAMA_AUTH_TOKEN", authToken);
392
+ updateConfigFile("OLLAMA_AUTH_TOKEN", authToken, null);
395
393
  envVars.OLLAMA_AUTH_TOKEN = authToken;
396
394
  }
397
395
  }
package/lib/openrouter.js CHANGED
@@ -9,7 +9,7 @@ const {
9
9
  loadEnvFile,
10
10
  findBestMatchingModel,
11
11
  promptForApiKey,
12
- updateEnvFile,
12
+ updateConfigFile,
13
13
  } = require("./config");
14
14
  const { modelCache } = require("./cache");
15
15
  const {
@@ -435,8 +435,6 @@ async function launchOpenRouter(
435
435
  extraArgs = [],
436
436
  directModel = null,
437
437
  ) {
438
- log("Launching Claude Code with OpenRouter settings...", "green");
439
-
440
438
  const envVars = loadEnvFile();
441
439
  log(`Loading environment from: ${envVars.envFile}`, "yellow");
442
440
 
@@ -450,7 +448,7 @@ async function launchOpenRouter(
450
448
  log("Error: OpenRouter auth token is required", "red");
451
449
  process.exit(1);
452
450
  }
453
- updateEnvFile("OPENROUTER_AUTH_TOKEN", authToken);
451
+ updateConfigFile("OPENROUTER_AUTH_TOKEN", authToken, null);
454
452
  envVars.OPENROUTER_AUTH_TOKEN = authToken;
455
453
  }
456
454
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-provider-switch",
3
- "version": "1.1.5",
3
+ "version": "1.1.6",
4
4
  "description": "Cross-platform Claude Code provider switcher (OpenRouter, Ollama, Anthropic)",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -9,7 +9,6 @@
9
9
  "scripts": {
10
10
  "start": "node bin/claude-switch.js",
11
11
  "dev": "node bin/claude-switch.js",
12
- "version": "npm version",
13
12
  "link": "npm link",
14
13
  "unlink": "npm unlink -g claude-code-provider-switch",
15
14
  "pub": "npm version patch && npm publish && git push origin main --tags",
@@ -22,15 +21,16 @@
22
21
  "test:all": "node test/run-tests.js"
23
22
  },
24
23
  "keywords": [
25
- "claude",
26
24
  "claude-code",
27
25
  "claude code provider switcher",
26
+ "claude code model switcher",
28
27
  "claude code llm switcher",
29
28
  "claude code llm provider switcher",
30
29
  "ai provider switcher",
31
30
  "claude code ai model switcher",
32
31
  "claude code ai model provider switcher",
33
- "claude code OpenRouter Ollama Anthropic",
32
+ "claude code OpenRouter",
33
+ "claude code Ollama",
34
34
  "cli"
35
35
  ],
36
36
  "author": "Adrian R",