ai-account-switch 1.5.6 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,326 @@
1
+ const chalk = require('chalk');
2
+ const ConfigManager = require('../config');
3
+ const { validateAccount } = require('./helpers');
4
+
5
+ const config = new ConfigManager();
6
+
7
+ /**
8
+ * Show configuration paths
9
+ */
10
+ function showPaths() {
11
+ const paths = config.getConfigPaths();
12
+ const projectRoot = config.findProjectRoot();
13
+
14
+ console.log(chalk.bold('\n📂 Configuration Paths (配置路径):\n'));
15
+ console.log(`${chalk.cyan('Global config file (全局配置文件):')} ${paths.global}`);
16
+ console.log(`${chalk.cyan('Global config directory (全局配置目录):')} ${paths.globalDir}`);
17
+ console.log(`${chalk.cyan('Project config file (项目配置文件):')} ${paths.project}`);
18
+
19
+ if (projectRoot) {
20
+ const claudeConfigPath = require('path').join(projectRoot, '.claude', 'settings.local.json');
21
+ const fs = require('fs');
22
+ console.log(`${chalk.cyan('Claude config file (Claude 配置文件):')} ${claudeConfigPath}`);
23
+ console.log(`${chalk.cyan('Claude config exists (Claude 配置是否存在):')} ${fs.existsSync(claudeConfigPath) ? chalk.green('✓ Yes (是)') : chalk.red('✗ No (否)')}`);
24
+ console.log(`${chalk.cyan('Project root (项目根目录):')} ${projectRoot}`);
25
+ console.log(`${chalk.cyan('Current directory (当前目录):')} ${process.cwd()}`);
26
+ } else {
27
+ console.log(chalk.yellow('\n⚠ Not in a configured project directory (不在已配置的项目目录中)'));
28
+ }
29
+ console.log('');
30
+ }
31
+
32
+ /**
33
+ * Diagnose Claude Code configuration issues
34
+ */
35
+ async function doctor() {
36
+ const path = require('path');
37
+ const fs = require('fs');
38
+ const os = require('os');
39
+
40
+ console.log(chalk.bold.cyan('\n🔍 Claude Code Configuration Diagnostics\n'));
41
+
42
+ // Check current directory
43
+ console.log(chalk.bold('1. Current Directory:'));
44
+ console.log(` ${process.cwd()}\n`);
45
+
46
+ // Check project root
47
+ const projectRoot = config.findProjectRoot();
48
+ console.log(chalk.bold('2. Project Root Detection:'));
49
+ if (projectRoot) {
50
+ console.log(chalk.green(` ✓ Found project root: ${projectRoot}`));
51
+ } else {
52
+ console.log(chalk.yellow(' ⚠ No project root found (not in a configured project)'));
53
+ console.log(chalk.yellow(' Run "ais use <account>" in your project root first (请先在项目根目录运行 "ais use <账号名>")\n'));
54
+ return;
55
+ }
56
+
57
+ // Check ais project config
58
+ const aisConfigPath = path.join(projectRoot, '.ais-project-config');
59
+ console.log(chalk.bold('\n3. AIS Project Configuration:'));
60
+ if (fs.existsSync(aisConfigPath)) {
61
+ console.log(chalk.green(` ✓ Config exists: ${aisConfigPath}`));
62
+ try {
63
+ const aisConfig = JSON.parse(fs.readFileSync(aisConfigPath, 'utf8'));
64
+ console.log(` Account: ${chalk.cyan(aisConfig.activeAccount)}`);
65
+ } catch (e) {
66
+ console.log(chalk.red(` ✗ Error reading config: ${e.message}`));
67
+ }
68
+ } else {
69
+ console.log(chalk.red(` ✗ Config not found: ${aisConfigPath}`));
70
+ }
71
+
72
+ // Check Claude config
73
+ const claudeDir = path.join(projectRoot, '.claude');
74
+ const claudeConfigPath = path.join(claudeDir, 'settings.local.json');
75
+
76
+ console.log(chalk.bold('\n4. Claude Code Configuration:'));
77
+ console.log(` Expected location: ${claudeConfigPath}`);
78
+
79
+ if (fs.existsSync(claudeConfigPath)) {
80
+ console.log(chalk.green(' ✓ Claude config exists'));
81
+ try {
82
+ const claudeConfig = JSON.parse(fs.readFileSync(claudeConfigPath, 'utf8'));
83
+
84
+ if (claudeConfig.env && claudeConfig.env.ANTHROPIC_AUTH_TOKEN) {
85
+ const token = claudeConfig.env.ANTHROPIC_AUTH_TOKEN;
86
+ const masked = token.substring(0, 6) + '****' + token.substring(token.length - 4);
87
+ console.log(` API Token: ${masked}`);
88
+ }
89
+
90
+ if (claudeConfig.env && claudeConfig.env.ANTHROPIC_BASE_URL) {
91
+ console.log(` API URL: ${claudeConfig.env.ANTHROPIC_BASE_URL}`);
92
+ }
93
+ } catch (e) {
94
+ console.log(chalk.red(` ✗ Error reading Claude config: ${e.message}`));
95
+ }
96
+ } else {
97
+ console.log(chalk.red(' ✗ Claude config not found'));
98
+ console.log(chalk.yellow(' Run "ais use <account>" to generate it'));
99
+ }
100
+
101
+ // Check Droids config
102
+ const droidsDir = path.join(projectRoot, '.droids');
103
+ const droidsConfigPath = path.join(droidsDir, 'config.json');
104
+
105
+ console.log(chalk.bold('\n5. Droids Configuration:'));
106
+ console.log(` Expected location: ${droidsConfigPath}`);
107
+
108
+ if (fs.existsSync(droidsConfigPath)) {
109
+ console.log(chalk.green(' ✓ Droids config exists'));
110
+ try {
111
+ const droidsConfig = JSON.parse(fs.readFileSync(droidsConfigPath, 'utf8'));
112
+
113
+ if (droidsConfig.apiKey) {
114
+ const masked = droidsConfig.apiKey.substring(0, 6) + '****' + droidsConfig.apiKey.substring(droidsConfig.apiKey.length - 4);
115
+ console.log(` API Key: ${masked}`);
116
+ }
117
+
118
+ if (droidsConfig.baseUrl) {
119
+ console.log(` Base URL: ${droidsConfig.baseUrl}`);
120
+ }
121
+
122
+ if (droidsConfig.model) {
123
+ console.log(` Model: ${droidsConfig.model}`);
124
+ }
125
+
126
+ if (droidsConfig.customSettings) {
127
+ console.log(` Custom Settings: ${Object.keys(droidsConfig.customSettings).join(', ')}`);
128
+ }
129
+ } catch (e) {
130
+ console.log(chalk.red(` ✗ Error reading Droids config: ${e.message}`));
131
+ }
132
+ } else {
133
+ console.log(chalk.yellow(' ⚠ Droids config not found'));
134
+ console.log(chalk.yellow(' Run "ais use <droids-account>" to generate it'));
135
+ }
136
+
137
+ // Check Codex profile
138
+ const codexProfilePath = path.join(projectRoot, '.codex-profile');
139
+ const globalCodexConfig = path.join(os.homedir(), '.codex', 'config.toml');
140
+
141
+ console.log(chalk.bold('\n6. Codex Configuration:'));
142
+ console.log(` Profile file: ${codexProfilePath}`);
143
+
144
+ if (fs.existsSync(codexProfilePath)) {
145
+ const profileName = fs.readFileSync(codexProfilePath, 'utf8').trim();
146
+ console.log(chalk.green(` ✓ Codex profile exists: ${profileName}`));
147
+ console.log(chalk.cyan(` Usage: codex --profile ${profileName} [prompt]`));
148
+
149
+ // Check if profile exists in global config
150
+ if (fs.existsSync(globalCodexConfig)) {
151
+ try {
152
+ const globalConfig = fs.readFileSync(globalCodexConfig, 'utf8');
153
+ const profilePattern = new RegExp(`\\[profiles\\.${profileName}\\]`);
154
+
155
+ if (profilePattern.test(globalConfig)) {
156
+ console.log(chalk.green(` ✓ Profile configured in ~/.codex/config.toml`));
157
+
158
+ // Parse profile info
159
+ const providerMatch = globalConfig.match(new RegExp(`\\[profiles\\.${profileName}\\][\\s\\S]*?model_provider\\s*=\\s*"([^"]+)"`));
160
+ const modelMatch = globalConfig.match(new RegExp(`\\[profiles\\.${profileName}\\][\\s\\S]*?model\\s*=\\s*"([^"]+)"`));
161
+
162
+ if (providerMatch) {
163
+ console.log(` Model Provider: ${providerMatch[1]}`);
164
+
165
+ // Find provider details
166
+ const providerName = providerMatch[1];
167
+ const baseUrlMatch = globalConfig.match(new RegExp(`\\[model_providers\\.${providerName}\\][\\s\\S]*?base_url\\s*=\\s*"([^"]+)"`));
168
+ if (baseUrlMatch) {
169
+ console.log(` API URL: ${baseUrlMatch[1]}`);
170
+ }
171
+ }
172
+ if (modelMatch) {
173
+ console.log(` Model: ${modelMatch[1]}`);
174
+ }
175
+ } else {
176
+ console.log(chalk.yellow(` ⚠ Profile not found in global config`));
177
+ console.log(chalk.yellow(` Run "ais use <account>" to regenerate it`));
178
+ }
179
+ } catch (e) {
180
+ console.log(chalk.red(` ✗ Error reading global Codex config: ${e.message}`));
181
+ }
182
+ }
183
+ } else {
184
+ console.log(chalk.yellow(' ⚠ No Codex profile configured'));
185
+ console.log(chalk.yellow(' Run "ais use <codex-account>" to create one'));
186
+ }
187
+
188
+ // Check global Claude config
189
+ const globalClaudeConfig = path.join(os.homedir(), '.claude', 'settings.json');
190
+ console.log(chalk.bold('\n7. Global Claude Configuration:'));
191
+ console.log(` Location: ${globalClaudeConfig}`);
192
+
193
+ if (fs.existsSync(globalClaudeConfig)) {
194
+ console.log(chalk.yellow(' ⚠ Global config exists (may override project config in some cases)'));
195
+ try {
196
+ const globalConfig = JSON.parse(fs.readFileSync(globalClaudeConfig, 'utf8'));
197
+ if (globalConfig.env && globalConfig.env.ANTHROPIC_AUTH_TOKEN) {
198
+ const token = globalConfig.env.ANTHROPIC_AUTH_TOKEN;
199
+ const masked = token.substring(0, 6) + '****' + token.substring(token.length - 4);
200
+ console.log(` Global API Token: ${masked}`);
201
+ }
202
+ if (globalConfig.env && globalConfig.env.ANTHROPIC_BASE_URL) {
203
+ console.log(` Global API URL: ${globalConfig.env.ANTHROPIC_BASE_URL}`);
204
+ }
205
+ } catch (e) {
206
+ console.log(chalk.red(` ✗ Error reading global config: ${e.message}`));
207
+ }
208
+ } else {
209
+ console.log(chalk.green(' ✓ No global config (good - project config will be used)'));
210
+ }
211
+
212
+ // Check current account availability
213
+ console.log(chalk.bold('\n8. Current Account Availability:'));
214
+ const projectAccount = config.getProjectAccount();
215
+
216
+ if (projectAccount && projectAccount.apiKey) {
217
+ console.log(` Testing account: ${chalk.cyan(projectAccount.name)}`);
218
+ console.log(` Account type: ${chalk.cyan(projectAccount.type)}`);
219
+
220
+ if (projectAccount.type === 'Claude') {
221
+ console.log(' Testing with Claude CLI...');
222
+ const { execSync } = require('child_process');
223
+ try {
224
+ execSync('claude --version', { stdio: 'pipe', timeout: 5000 });
225
+ console.log(chalk.green(' ✓ Claude CLI is available'));
226
+
227
+ // Interactive CLI test
228
+ console.log(' Running interactive test...');
229
+ try {
230
+ const testResult = execSync('echo "test" | claude', {
231
+ encoding: 'utf8',
232
+ timeout: 10000,
233
+ env: { ...process.env, ANTHROPIC_API_KEY: projectAccount.apiKey }
234
+ });
235
+ console.log(chalk.green(' ✓ Claude CLI interactive test passed'));
236
+ } catch (e) {
237
+ console.log(chalk.yellow(' ⚠ Claude CLI interactive test failed'));
238
+ console.log(chalk.gray(` Error: ${e.message}`));
239
+ }
240
+ } catch (e) {
241
+ console.log(chalk.yellow(' ⚠ Claude CLI not found, using API validation'));
242
+ }
243
+ } else if (projectAccount.type === 'Codex') {
244
+ console.log(' Testing with Codex CLI...');
245
+ const { execSync } = require('child_process');
246
+ try {
247
+ execSync('codex --version', { stdio: 'pipe', timeout: 5000 });
248
+ console.log(chalk.green(' ✓ Codex CLI is available'));
249
+ } catch (e) {
250
+ console.log(chalk.yellow(' ⚠ Codex CLI not found'));
251
+ }
252
+ } else if (projectAccount.type === 'Droids') {
253
+ console.log(' Testing with Droids CLI...');
254
+ const { execSync } = require('child_process');
255
+ try {
256
+ execSync('droid --version', { stdio: 'pipe', timeout: 5000 });
257
+ console.log(chalk.green(' ✓ Droids CLI is available'));
258
+ } catch (e) {
259
+ console.log(chalk.yellow(' ⚠ Droids CLI not found'));
260
+ }
261
+ }
262
+
263
+ console.log(` API URL: ${projectAccount.apiUrl || 'https://api.anthropic.com'}`);
264
+ console.log(' Validating API key...');
265
+
266
+ const result = await validateAccount(projectAccount.apiKey, projectAccount.apiUrl);
267
+
268
+ if (result.valid) {
269
+ console.log(chalk.green(' ✓ Account is valid and accessible'));
270
+ if (result.statusCode) {
271
+ console.log(chalk.gray(` Response status: ${result.statusCode}`));
272
+ }
273
+ } else {
274
+ console.log(chalk.red(' ✗ Account validation failed'));
275
+ if (result.error) {
276
+ console.log(chalk.red(` Error: ${result.error}`));
277
+ } else if (result.statusCode) {
278
+ console.log(chalk.red(` Status code: ${result.statusCode}`));
279
+ if (result.statusCode === 401 || result.statusCode === 403) {
280
+ console.log(chalk.yellow(' ⚠ API key appears to be invalid or expired'));
281
+ }
282
+ }
283
+ }
284
+ } else {
285
+ console.log(chalk.yellow(' ⚠ No account configured or API key missing'));
286
+ }
287
+
288
+ // Recommendations
289
+ console.log(chalk.bold('\n9. Recommendations:'));
290
+
291
+ if (projectRoot && process.cwd() !== projectRoot) {
292
+ console.log(chalk.yellow(` ⚠ You are in a subdirectory (${path.relative(projectRoot, process.cwd())})`));
293
+ console.log(chalk.cyan(' • Claude Code should still find the project config'));
294
+ console.log(chalk.cyan(' • Make sure to start Claude Code from this directory or parent directories'));
295
+ }
296
+
297
+ if (fs.existsSync(globalClaudeConfig)) {
298
+ console.log(chalk.yellow(' ⚠ Global Claude config exists:'));
299
+ console.log(chalk.cyan(' • Project config should take precedence'));
300
+ console.log(chalk.cyan(' • If issues persist, consider removing global env settings'));
301
+ console.log(chalk.gray(` • File: ${globalClaudeConfig}`));
302
+ }
303
+
304
+ console.log(chalk.bold('\n10. Next Steps:'));
305
+ console.log(chalk.cyan(' • Start Claude Code/Codex/Droids from your project directory or subdirectory'));
306
+ console.log(chalk.cyan(' • Check which account is being used'));
307
+ console.log(chalk.cyan(' • If wrong account is used, run: ais use <correct-account>'));
308
+ console.log('');
309
+ }
310
+
311
+ /**
312
+ * Start Web UI server
313
+ */
314
+ function startUI() {
315
+ const UIServer = require('../ui-server');
316
+ const server = new UIServer();
317
+
318
+ console.log(chalk.cyan('\n🌐 Starting AIS Web UI... (正在启动 AIS Web 界面...)\n'));
319
+ server.start();
320
+ }
321
+
322
+ module.exports = {
323
+ showPaths,
324
+ doctor,
325
+ startUI
326
+ };
package/src/config.js CHANGED
@@ -146,6 +146,13 @@ class ConfigManager {
146
146
  if (account.type === 'Codex') {
147
147
  // Codex type accounts only need Codex configuration
148
148
  this.generateCodexConfig(account, projectRoot);
149
+ } else if (account.type === 'Droids') {
150
+ // Droids type accounts only need Droids configuration
151
+ this.generateDroidsConfig(account, projectRoot);
152
+ } else if (account.type === 'CCR') {
153
+ // CCR type accounts need both CCR and Claude configuration
154
+ this.generateCCRConfig(account, projectRoot);
155
+ this.generateClaudeConfigForCCR(account, projectRoot);
149
156
  } else {
150
157
  // Claude and other types need Claude Code configuration
151
158
  this.generateClaudeConfig(account, projectRoot);
@@ -173,7 +180,8 @@ class ConfigManager {
173
180
  const filesToIgnore = [
174
181
  this.projectConfigFilename,
175
182
  '.claude/settings.local.json',
176
- '.codex-profile'
183
+ '.codex-profile',
184
+ '.droids/config.json'
177
185
  ];
178
186
 
179
187
  let gitignoreContent = '';
@@ -346,6 +354,152 @@ class ConfigManager {
346
354
  fs.writeFileSync(claudeConfigFile, JSON.stringify(claudeConfig, null, 2), 'utf8');
347
355
  }
348
356
 
357
+ /**
358
+ * Generate Droids configuration in .droids/config.json
359
+ */
360
+ generateDroidsConfig(account, projectRoot = process.cwd()) {
361
+ const droidsDir = path.join(projectRoot, '.droids');
362
+ const droidsConfigFile = path.join(droidsDir, 'config.json');
363
+
364
+ // Create .droids directory if it doesn't exist
365
+ if (!fs.existsSync(droidsDir)) {
366
+ fs.mkdirSync(droidsDir, { recursive: true });
367
+ }
368
+
369
+ // Build Droids configuration
370
+ const droidsConfig = {
371
+ apiKey: account.apiKey
372
+ };
373
+
374
+ // Add API URL if specified
375
+ if (account.apiUrl) {
376
+ droidsConfig.baseUrl = account.apiUrl;
377
+ }
378
+
379
+ // Add model configuration - Droids uses simple model field
380
+ if (account.model) {
381
+ droidsConfig.model = account.model;
382
+ }
383
+
384
+ // Add custom environment variables as customSettings
385
+ if (account.customEnv && typeof account.customEnv === 'object') {
386
+ droidsConfig.customSettings = account.customEnv;
387
+ }
388
+
389
+ // Write Droids configuration
390
+ fs.writeFileSync(droidsConfigFile, JSON.stringify(droidsConfig, null, 2), 'utf8');
391
+ }
392
+
393
+ /**
394
+ * Generate CCR configuration in ~/.claude-code-router/config.json
395
+ */
396
+ generateCCRConfig(account, projectRoot = process.cwd()) {
397
+ const ccrConfigDir = path.join(os.homedir(), '.claude-code-router');
398
+ const ccrConfigFile = path.join(ccrConfigDir, 'config.json');
399
+
400
+ // Read existing config
401
+ let config = {};
402
+ if (fs.existsSync(ccrConfigFile)) {
403
+ const data = fs.readFileSync(ccrConfigFile, 'utf8');
404
+ config = JSON.parse(data);
405
+ }
406
+
407
+ if (!account.ccrConfig) return;
408
+
409
+ const { providerName, models, defaultModel, backgroundModel, thinkModel } = account.ccrConfig;
410
+
411
+ // Check if provider exists
412
+ const providerIndex = config.Providers?.findIndex(p => p.name === providerName);
413
+
414
+ const provider = {
415
+ api_base_url: account.apiUrl || '',
416
+ api_key: account.apiKey,
417
+ models: models,
418
+ name: providerName
419
+ };
420
+
421
+ if (providerIndex >= 0) {
422
+ config.Providers[providerIndex] = provider;
423
+ } else {
424
+ if (!config.Providers) config.Providers = [];
425
+ config.Providers.push(provider);
426
+ }
427
+
428
+ // Update Router configuration
429
+ if (!config.Router) config.Router = {};
430
+ config.Router.default = `${providerName},${defaultModel}`;
431
+ config.Router.background = `${providerName},${backgroundModel}`;
432
+ config.Router.think = `${providerName},${thinkModel}`;
433
+
434
+ fs.writeFileSync(ccrConfigFile, JSON.stringify(config, null, 2), 'utf8');
435
+ }
436
+
437
+ /**
438
+ * Generate Claude configuration for CCR type accounts
439
+ */
440
+ generateClaudeConfigForCCR(account, projectRoot = process.cwd()) {
441
+ const claudeDir = path.join(projectRoot, '.claude');
442
+ const claudeConfigFile = path.join(claudeDir, 'settings.local.json');
443
+ const ccrConfigFile = path.join(os.homedir(), '.claude-code-router', 'config.json');
444
+
445
+ // Create .claude directory if it doesn't exist
446
+ if (!fs.existsSync(claudeDir)) {
447
+ fs.mkdirSync(claudeDir, { recursive: true });
448
+ }
449
+
450
+ // Read CCR config to get PORT
451
+ let port = 3456; // default port
452
+ if (fs.existsSync(ccrConfigFile)) {
453
+ try {
454
+ const ccrConfig = JSON.parse(fs.readFileSync(ccrConfigFile, 'utf8'));
455
+ if (ccrConfig.PORT) {
456
+ port = ccrConfig.PORT;
457
+ }
458
+ } catch (e) {
459
+ // Use default port if reading fails
460
+ }
461
+ }
462
+
463
+ // Read existing config if it exists
464
+ let existingConfig = {};
465
+ if (fs.existsSync(claudeConfigFile)) {
466
+ try {
467
+ const data = fs.readFileSync(claudeConfigFile, 'utf8');
468
+ existingConfig = JSON.parse(data);
469
+ } catch (error) {
470
+ existingConfig = {};
471
+ }
472
+ }
473
+
474
+ const claudeConfig = {
475
+ ...existingConfig,
476
+ env: {
477
+ ...(existingConfig.env || {}),
478
+ ANTHROPIC_AUTH_TOKEN: account.apiKey,
479
+ ANTHROPIC_BASE_URL: `http://127.0.0.1:${port}`
480
+ }
481
+ };
482
+
483
+ // Add custom environment variables if specified
484
+ if (account.customEnv && typeof account.customEnv === 'object') {
485
+ Object.keys(account.customEnv).forEach(key => {
486
+ claudeConfig.env[key] = account.customEnv[key];
487
+ });
488
+ }
489
+
490
+ // Preserve existing permissions if any
491
+ if (!claudeConfig.permissions) {
492
+ claudeConfig.permissions = existingConfig.permissions || {
493
+ allow: [],
494
+ deny: [],
495
+ ask: []
496
+ };
497
+ }
498
+
499
+ // Write Claude configuration
500
+ fs.writeFileSync(claudeConfigFile, JSON.stringify(claudeConfig, null, 2), 'utf8');
501
+ }
502
+
349
503
  /**
350
504
  * Generate Codex profile in global ~/.codex/config.toml
351
505
  */
@@ -385,12 +539,9 @@ class ConfigManager {
385
539
 
386
540
  profileConfig += `model_provider = "${providerName}"\n`;
387
541
 
388
- // Add model configuration from active model group if available
389
- if (account.modelGroups && account.activeModelGroup) {
390
- const activeGroup = account.modelGroups[account.activeModelGroup];
391
- if (activeGroup && activeGroup.DEFAULT_MODEL) {
392
- profileConfig += `model = "${activeGroup.DEFAULT_MODEL}"\n`;
393
- }
542
+ // Add model configuration - Codex uses simple model field
543
+ if (account.model) {
544
+ profileConfig += `model = "${account.model}"\n`;
394
545
  }
395
546
 
396
547
  // Ensure API URL has proper path