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.
package/src/commands.js DELETED
@@ -1,1182 +0,0 @@
1
- const chalk = require('chalk');
2
- const inquirer = require('inquirer');
3
- const ConfigManager = require('./config');
4
-
5
- const config = new ConfigManager();
6
-
7
- /**
8
- * Add a new account
9
- */
10
- async function addAccount(name, options) {
11
- // If name not provided, prompt for it
12
- if (!name) {
13
- const answers = await inquirer.prompt([
14
- {
15
- type: 'input',
16
- name: 'accountName',
17
- message: 'Enter account name:',
18
- validate: (input) => input.trim() !== '' || 'Account name is required'
19
- }
20
- ]);
21
- name = answers.accountName;
22
- }
23
-
24
- // Check if account already exists
25
- if (config.accountExists(name)) {
26
- const { overwrite } = await inquirer.prompt([
27
- {
28
- type: 'confirm',
29
- name: 'overwrite',
30
- message: `Account '${name}' already exists. Overwrite?`,
31
- default: false
32
- }
33
- ]);
34
-
35
- if (!overwrite) {
36
- console.log(chalk.yellow('Operation cancelled.'));
37
- return;
38
- }
39
- }
40
-
41
- // Prompt for account type first
42
- const typeAnswer = await inquirer.prompt([
43
- {
44
- type: 'list',
45
- name: 'type',
46
- message: 'Select account type:',
47
- choices: ['Claude', 'Codex', 'Other'],
48
- default: 'Claude'
49
- }
50
- ]);
51
-
52
- // Show configuration tips based on account type
53
- if (typeAnswer.type === 'Codex') {
54
- console.log(chalk.cyan('\nšŸ“ Codex Configuration Tips:'));
55
- console.log(chalk.gray(' • API URL should include the full path (e.g., https://api.example.com/v1)'));
56
- console.log(chalk.gray(' • AIS will automatically add /v1 if missing'));
57
- console.log(chalk.gray(' • Codex uses OpenAI-compatible API format\n'));
58
- }
59
-
60
- // Prompt for remaining account details
61
- const accountData = await inquirer.prompt([
62
- {
63
- type: 'input',
64
- name: 'apiKey',
65
- message: 'Enter API Key:',
66
- validate: (input) => input.trim() !== '' || 'API Key is required'
67
- },
68
- {
69
- type: 'input',
70
- name: 'apiUrl',
71
- message: typeAnswer.type === 'Codex'
72
- ? 'Enter API URL (e.g., https://api.example.com or https://api.example.com/v1):'
73
- : 'Enter API URL (optional):',
74
- default: ''
75
- },
76
- {
77
- type: 'input',
78
- name: 'email',
79
- message: 'Enter associated email (optional):',
80
- default: ''
81
- },
82
- {
83
- type: 'input',
84
- name: 'description',
85
- message: 'Enter description (optional):',
86
- default: ''
87
- },
88
- {
89
- type: 'confirm',
90
- name: 'addCustomEnv',
91
- message: 'Add custom environment variables? (e.g., CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC)',
92
- default: false
93
- }
94
- ]);
95
-
96
- // Merge type into accountData
97
- accountData.type = typeAnswer.type;
98
-
99
- // Handle custom environment variables
100
- if (accountData.addCustomEnv) {
101
- accountData.customEnv = {};
102
- let addMore = true;
103
-
104
- console.log(chalk.cyan('\nšŸ’” Tip: Enter in format KEY=VALUE (e.g., CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1)'));
105
- console.log(chalk.gray(' Or leave empty to finish\n'));
106
-
107
- while (addMore) {
108
- const envInput = await inquirer.prompt([
109
- {
110
- type: 'input',
111
- name: 'envVar',
112
- message: 'Environment variable (KEY=VALUE format):',
113
- validate: (input) => {
114
- // Allow empty input to skip
115
- if (!input.trim()) return true;
116
-
117
- // Check if input contains '='
118
- if (!input.includes('=')) {
119
- return 'Invalid format. Use KEY=VALUE format (e.g., MY_VAR=value)';
120
- }
121
-
122
- const [key, ...valueParts] = input.split('=');
123
- const value = valueParts.join('='); // In case value contains '='
124
-
125
- if (!key.trim()) {
126
- return 'Variable name cannot be empty';
127
- }
128
-
129
- if (!/^[A-Z_][A-Z0-9_]*$/.test(key.trim())) {
130
- return 'Invalid variable name. Use uppercase letters, numbers, and underscores (e.g., MY_VAR)';
131
- }
132
-
133
- if (!value.trim()) {
134
- return 'Variable value cannot be empty';
135
- }
136
-
137
- return true;
138
- }
139
- }
140
- ]);
141
-
142
- // If user left input empty, skip adding more
143
- if (!envInput.envVar.trim()) {
144
- break;
145
- }
146
-
147
- // Parse KEY=VALUE
148
- const [key, ...valueParts] = envInput.envVar.split('=');
149
- const value = valueParts.join('='); // In case value contains '='
150
-
151
- accountData.customEnv[key.trim()] = value.trim();
152
-
153
- // Display currently added variables
154
- console.log(chalk.green('\nāœ“ Added:'), chalk.cyan(`${key.trim()}=${value.trim()}`));
155
-
156
- if (Object.keys(accountData.customEnv).length > 0) {
157
- console.log(chalk.bold('\nšŸ“‹ Current environment variables:'));
158
- Object.entries(accountData.customEnv).forEach(([k, v]) => {
159
- console.log(chalk.gray(' •'), chalk.cyan(`${k}=${v}`));
160
- });
161
- console.log('');
162
- }
163
-
164
- const { continueAdding } = await inquirer.prompt([
165
- {
166
- type: 'confirm',
167
- name: 'continueAdding',
168
- message: 'Add another environment variable?',
169
- default: false
170
- }
171
- ]);
172
-
173
- addMore = continueAdding;
174
- }
175
-
176
- if (Object.keys(accountData.customEnv).length > 0) {
177
- console.log(chalk.green(`\nāœ“ Total: ${Object.keys(accountData.customEnv).length} custom environment variable(s) added\n`));
178
- } else {
179
- console.log(chalk.yellow('\n⚠ No custom environment variables added\n'));
180
- }
181
- }
182
-
183
- // Remove the addCustomEnv flag before saving
184
- delete accountData.addCustomEnv;
185
-
186
- // Initialize model groups structure
187
- accountData.modelGroups = {};
188
- accountData.activeModelGroup = null;
189
-
190
- // Prompt for model group configuration
191
- const { createModelGroup } = await inquirer.prompt([
192
- {
193
- type: 'confirm',
194
- name: 'createModelGroup',
195
- message: 'Do you want to create a model group? (Recommended)',
196
- default: true
197
- }
198
- ]);
199
-
200
- if (createModelGroup) {
201
- const groupName = 'default';
202
- const modelGroupConfig = await promptForModelGroup();
203
-
204
- if (Object.keys(modelGroupConfig).length > 0) {
205
- accountData.modelGroups[groupName] = modelGroupConfig;
206
- accountData.activeModelGroup = groupName;
207
- console.log(chalk.green(`\nāœ“ Created model group '${groupName}'`));
208
- }
209
- }
210
-
211
- // Save account
212
- config.addAccount(name, accountData);
213
- console.log(chalk.green(`āœ“ Account '${name}' added successfully!`));
214
-
215
- if (accountData.activeModelGroup) {
216
- console.log(chalk.cyan(`āœ“ Active model group: ${accountData.activeModelGroup}\n`));
217
- console.log(chalk.gray('šŸ’” Tip: Use "ais model add" to create more model groups'));
218
- console.log(chalk.gray('šŸ’” Tip: Use "ais model list" to view all model groups\n'));
219
- }
220
-
221
- // Show usage instructions based on account type
222
- if (accountData.type === 'Codex') {
223
- console.log(chalk.bold.cyan('\nšŸ“– Codex Usage Instructions:\n'));
224
- console.log(chalk.gray('1. Switch to this account in your project:'));
225
- console.log(chalk.cyan(` ais use ${name}\n`));
226
- console.log(chalk.gray('2. Use Codex with the generated profile:'));
227
- console.log(chalk.cyan(` codex --profile ais_<project-name> "your prompt"\n`));
228
- console.log(chalk.gray('3. The profile name will be shown when you run "ais use"\n'));
229
- } else if (accountData.type === 'Claude') {
230
- console.log(chalk.bold.cyan('\nšŸ“– Claude Usage Instructions:\n'));
231
- console.log(chalk.gray('1. Switch to this account in your project:'));
232
- console.log(chalk.cyan(` ais use ${name}\n`));
233
- console.log(chalk.gray('2. Start Claude Code in your project directory'));
234
- console.log(chalk.gray('3. Claude Code will automatically use the project configuration\n'));
235
- }
236
- }
237
-
238
- /**
239
- * Helper function to prompt for model group configuration
240
- */
241
- async function promptForModelGroup() {
242
- console.log(chalk.cyan('\nšŸ¤– Model Group Configuration'));
243
- console.log(chalk.gray('Configure which models to use. Leave empty to skip.\n'));
244
-
245
- const modelQuestions = await inquirer.prompt([
246
- {
247
- type: 'input',
248
- name: 'defaultModel',
249
- message: 'DEFAULT_MODEL (base model, used if others are not set):',
250
- default: ''
251
- },
252
- {
253
- type: 'input',
254
- name: 'anthropicDefaultOpusModel',
255
- message: 'ANTHROPIC_DEFAULT_OPUS_MODEL (leave empty to use DEFAULT_MODEL):',
256
- default: ''
257
- },
258
- {
259
- type: 'input',
260
- name: 'anthropicDefaultSonnetModel',
261
- message: 'ANTHROPIC_DEFAULT_SONNET_MODEL (leave empty to use DEFAULT_MODEL):',
262
- default: ''
263
- },
264
- {
265
- type: 'input',
266
- name: 'anthropicDefaultHaikuModel',
267
- message: 'ANTHROPIC_DEFAULT_HAIKU_MODEL (leave empty to use DEFAULT_MODEL):',
268
- default: ''
269
- },
270
- {
271
- type: 'input',
272
- name: 'claudeCodeSubagentModel',
273
- message: 'CLAUDE_CODE_SUBAGENT_MODEL (leave empty to use DEFAULT_MODEL):',
274
- default: ''
275
- },
276
- {
277
- type: 'input',
278
- name: 'anthropicModel',
279
- message: 'ANTHROPIC_MODEL (leave empty to use DEFAULT_MODEL):',
280
- default: ''
281
- }
282
- ]);
283
-
284
- const modelConfig = {};
285
-
286
- // Only add non-empty model configurations
287
- if (modelQuestions.defaultModel.trim()) {
288
- modelConfig.DEFAULT_MODEL = modelQuestions.defaultModel.trim();
289
- }
290
- if (modelQuestions.anthropicDefaultOpusModel.trim()) {
291
- modelConfig.ANTHROPIC_DEFAULT_OPUS_MODEL = modelQuestions.anthropicDefaultOpusModel.trim();
292
- }
293
- if (modelQuestions.anthropicDefaultSonnetModel.trim()) {
294
- modelConfig.ANTHROPIC_DEFAULT_SONNET_MODEL = modelQuestions.anthropicDefaultSonnetModel.trim();
295
- }
296
- if (modelQuestions.anthropicDefaultHaikuModel.trim()) {
297
- modelConfig.ANTHROPIC_DEFAULT_HAIKU_MODEL = modelQuestions.anthropicDefaultHaikuModel.trim();
298
- }
299
- if (modelQuestions.claudeCodeSubagentModel.trim()) {
300
- modelConfig.CLAUDE_CODE_SUBAGENT_MODEL = modelQuestions.claudeCodeSubagentModel.trim();
301
- }
302
- if (modelQuestions.anthropicModel.trim()) {
303
- modelConfig.ANTHROPIC_MODEL = modelQuestions.anthropicModel.trim();
304
- }
305
-
306
- if (Object.keys(modelConfig).length > 0) {
307
- console.log(chalk.green('\nāœ“ Model configuration:'));
308
- Object.entries(modelConfig).forEach(([key, value]) => {
309
- console.log(chalk.gray(' •'), chalk.cyan(`${key}=${value}`));
310
- });
311
- console.log('');
312
- }
313
-
314
- return modelConfig;
315
- }
316
-
317
- /**
318
- * List all accounts
319
- */
320
- function listAccounts() {
321
- const accounts = config.getAllAccounts();
322
- const accountNames = Object.keys(accounts);
323
-
324
- if (accountNames.length === 0) {
325
- console.log(chalk.yellow('No accounts found. Use "ais add" to add an account.'));
326
- return;
327
- }
328
-
329
- const currentProject = config.getProjectAccount();
330
-
331
- console.log(chalk.bold('\nšŸ“‹ Available Accounts:\n'));
332
-
333
- accountNames.forEach(name => {
334
- const account = accounts[name];
335
- const isActive = currentProject && currentProject.name === name;
336
- const marker = isActive ? chalk.green('ā— ') : ' ';
337
- const nameDisplay = isActive ? chalk.green.bold(name) : chalk.cyan(name);
338
-
339
- console.log(`${marker}${nameDisplay}`);
340
- console.log(` Type: ${account.type}`);
341
- console.log(` API Key: ${maskApiKey(account.apiKey)}`);
342
- if (account.email) console.log(` Email: ${account.email}`);
343
- if (account.description) console.log(` Description: ${account.description}`);
344
- if (account.customEnv && Object.keys(account.customEnv).length > 0) {
345
- console.log(` Custom Env: ${Object.keys(account.customEnv).join(', ')}`);
346
- }
347
- // Display model groups
348
- if (account.modelGroups && Object.keys(account.modelGroups).length > 0) {
349
- const groupNames = Object.keys(account.modelGroups);
350
- const activeMarker = account.activeModelGroup ? ` (active: ${account.activeModelGroup})` : '';
351
- console.log(` Model Groups: ${groupNames.join(', ')}${activeMarker}`);
352
- }
353
- console.log(` Created: ${new Date(account.createdAt).toLocaleString()}`);
354
- console.log('');
355
- });
356
-
357
- if (currentProject) {
358
- console.log(chalk.green(`āœ“ Current project is using: ${currentProject.name}\n`));
359
- } else {
360
- console.log(chalk.yellow('⚠ No account set for current project. Use "ais use <account>" to set one.\n'));
361
- }
362
- }
363
-
364
- /**
365
- * Switch to a specific account for current project
366
- */
367
- async function useAccount(name) {
368
- if (!name) {
369
- // If no name provided, show interactive selection
370
- const accounts = config.getAllAccounts();
371
- const accountNames = Object.keys(accounts);
372
-
373
- if (accountNames.length === 0) {
374
- console.log(chalk.yellow('No accounts found. Use "ais add" to add an account first.'));
375
- return;
376
- }
377
-
378
- const answers = await inquirer.prompt([
379
- {
380
- type: 'list',
381
- name: 'accountName',
382
- message: 'Select an account to use:',
383
- choices: accountNames
384
- }
385
- ]);
386
-
387
- name = answers.accountName;
388
- }
389
-
390
- if (!config.accountExists(name)) {
391
- console.log(chalk.red(`āœ— Account '${name}' not found.`));
392
- console.log(chalk.yellow('Use "ais list" to see available accounts.'));
393
- return;
394
- }
395
-
396
- const success = config.setProjectAccount(name);
397
- if (success) {
398
- const fs = require('fs');
399
- const path = require('path');
400
- const account = config.getAccount(name);
401
-
402
- console.log(chalk.green(`āœ“ Switched to account '${name}' for current project.`));
403
- console.log(chalk.yellow(`Project: ${process.cwd()}`));
404
-
405
- // Show different messages based on account type
406
- if (account && account.type === 'Codex') {
407
- const profileFile = path.join(process.cwd(), '.codex-profile');
408
- if (fs.existsSync(profileFile)) {
409
- const profileName = fs.readFileSync(profileFile, 'utf8').trim();
410
- console.log(chalk.cyan(`āœ“ Codex profile created: ${profileName}`));
411
- console.log(chalk.yellow(` Use: codex --profile ${profileName} [prompt]`));
412
- }
413
- } else {
414
- console.log(chalk.cyan(`āœ“ Claude configuration generated at: .claude/settings.local.json`));
415
- }
416
-
417
- // Check if .gitignore was updated
418
- const gitignorePath = path.join(process.cwd(), '.gitignore');
419
- const gitDir = path.join(process.cwd(), '.git');
420
- if (fs.existsSync(gitDir) && fs.existsSync(gitignorePath)) {
421
- console.log(chalk.cyan(`āœ“ Updated .gitignore to exclude AIS configuration files`));
422
- }
423
- } else {
424
- console.log(chalk.red('āœ— Failed to set account.'));
425
- }
426
- }
427
-
428
- /**
429
- * Show current project's account info
430
- */
431
- function showInfo() {
432
- const projectAccount = config.getProjectAccount();
433
-
434
- if (!projectAccount) {
435
- console.log(chalk.yellow('⚠ No account set for current project.'));
436
- console.log(chalk.yellow(`Project: ${process.cwd()}`));
437
- console.log(chalk.cyan('\nUse "ais use <account>" to set an account for this project.'));
438
- return;
439
- }
440
-
441
- console.log(chalk.bold('\nšŸ“Œ Current Project Account Info:\n'));
442
- console.log(`${chalk.cyan('Account Name:')} ${chalk.green.bold(projectAccount.name)}`);
443
- console.log(`${chalk.cyan('Type:')} ${projectAccount.type}`);
444
- console.log(`${chalk.cyan('API Key:')} ${maskApiKey(projectAccount.apiKey)}`);
445
- if (projectAccount.apiUrl) console.log(`${chalk.cyan('API URL:')} ${projectAccount.apiUrl}`);
446
- if (projectAccount.email) console.log(`${chalk.cyan('Email:')} ${projectAccount.email}`);
447
- if (projectAccount.description) console.log(`${chalk.cyan('Description:')} ${projectAccount.description}`);
448
- if (projectAccount.customEnv && Object.keys(projectAccount.customEnv).length > 0) {
449
- console.log(`${chalk.cyan('Custom Environment Variables:')}`);
450
- Object.entries(projectAccount.customEnv).forEach(([key, value]) => {
451
- console.log(` ${chalk.gray('•')} ${key}: ${value}`);
452
- });
453
- }
454
- // Display model groups
455
- if (projectAccount.modelGroups && Object.keys(projectAccount.modelGroups).length > 0) {
456
- console.log(`${chalk.cyan('Model Groups:')}`);
457
- Object.keys(projectAccount.modelGroups).forEach(groupName => {
458
- const isActive = projectAccount.activeModelGroup === groupName;
459
- const marker = isActive ? chalk.green('ā— ') : ' ';
460
- console.log(`${marker}${isActive ? chalk.green.bold(groupName) : groupName}`);
461
- });
462
- if (projectAccount.activeModelGroup) {
463
- console.log(`${chalk.cyan('Active Model Group:')} ${chalk.green.bold(projectAccount.activeModelGroup)}`);
464
- }
465
- }
466
- console.log(`${chalk.cyan('Set At:')} ${new Date(projectAccount.setAt).toLocaleString()}`);
467
- console.log(`${chalk.cyan('Project Root:')} ${projectAccount.projectRoot}`);
468
- console.log(`${chalk.cyan('Current Directory:')} ${process.cwd()}\n`);
469
- }
470
-
471
- /**
472
- * Remove an account
473
- */
474
- async function removeAccount(name) {
475
- if (!name) {
476
- const accounts = config.getAllAccounts();
477
- const accountNames = Object.keys(accounts);
478
-
479
- if (accountNames.length === 0) {
480
- console.log(chalk.yellow('No accounts found.'));
481
- return;
482
- }
483
-
484
- const answers = await inquirer.prompt([
485
- {
486
- type: 'list',
487
- name: 'accountName',
488
- message: 'Select an account to remove:',
489
- choices: accountNames
490
- }
491
- ]);
492
-
493
- name = answers.accountName;
494
- }
495
-
496
- if (!config.accountExists(name)) {
497
- console.log(chalk.red(`āœ— Account '${name}' not found.`));
498
- return;
499
- }
500
-
501
- const { confirm } = await inquirer.prompt([
502
- {
503
- type: 'confirm',
504
- name: 'confirm',
505
- message: `Are you sure you want to remove account '${name}'?`,
506
- default: false
507
- }
508
- ]);
509
-
510
- if (!confirm) {
511
- console.log(chalk.yellow('Operation cancelled.'));
512
- return;
513
- }
514
-
515
- const success = config.removeAccount(name);
516
- if (success) {
517
- console.log(chalk.green(`āœ“ Account '${name}' removed successfully.`));
518
- } else {
519
- console.log(chalk.red('āœ— Failed to remove account.'));
520
- }
521
- }
522
-
523
- /**
524
- * Show current account for current project
525
- */
526
- function showCurrent() {
527
- const projectAccount = config.getProjectAccount();
528
-
529
- if (!projectAccount) {
530
- console.log(chalk.yellow('⚠ No account set for current project.'));
531
- return;
532
- }
533
-
534
- console.log(chalk.green(`Current account: ${chalk.bold(projectAccount.name)}`));
535
- }
536
-
537
- /**
538
- * Show configuration paths
539
- */
540
- function showPaths() {
541
- const paths = config.getConfigPaths();
542
- const projectRoot = config.findProjectRoot();
543
-
544
- console.log(chalk.bold('\nšŸ“‚ Configuration Paths:\n'));
545
- console.log(`${chalk.cyan('Global config file:')} ${paths.global}`);
546
- console.log(`${chalk.cyan('Global config directory:')} ${paths.globalDir}`);
547
- console.log(`${chalk.cyan('Project config file:')} ${paths.project}`);
548
-
549
- if (projectRoot) {
550
- const claudeConfigPath = require('path').join(projectRoot, '.claude', 'settings.local.json');
551
- const fs = require('fs');
552
- console.log(`${chalk.cyan('Claude config file:')} ${claudeConfigPath}`);
553
- console.log(`${chalk.cyan('Claude config exists:')} ${fs.existsSync(claudeConfigPath) ? chalk.green('āœ“ Yes') : chalk.red('āœ— No')}`);
554
- console.log(`${chalk.cyan('Project root:')} ${projectRoot}`);
555
- console.log(`${chalk.cyan('Current directory:')} ${process.cwd()}`);
556
- } else {
557
- console.log(chalk.yellow('\n⚠ Not in a configured project directory'));
558
- }
559
- console.log('');
560
- }
561
-
562
- /**
563
- * Export account configuration
564
- */
565
- function exportAccount(name) {
566
- if (!name) {
567
- console.log(chalk.red('Please specify an account name.'));
568
- console.log(chalk.cyan('Usage: ais export <account-name>'));
569
- return;
570
- }
571
-
572
- const account = config.getAccount(name);
573
- if (!account) {
574
- console.log(chalk.red(`āœ— Account '${name}' not found.`));
575
- return;
576
- }
577
-
578
- console.log(chalk.bold(`\nšŸ“¤ Export for account '${name}':\n`));
579
- console.log(JSON.stringify({ [name]: account }, null, 2));
580
- console.log('');
581
- }
582
-
583
- /**
584
- * Utility function to mask API key
585
- */
586
- function maskApiKey(apiKey) {
587
- if (!apiKey || apiKey.length < 8) return '****';
588
- return apiKey.substring(0, 4) + '****' + apiKey.substring(apiKey.length - 4);
589
- }
590
-
591
- /**
592
- * Validate account API availability
593
- */
594
- async function validateAccount(apiKey, apiUrl) {
595
- const https = require('https');
596
- const http = require('http');
597
-
598
- return new Promise((resolve) => {
599
- const url = apiUrl || 'https://api.anthropic.com';
600
- const urlObj = new URL(url);
601
- const client = urlObj.protocol === 'https:' ? https : http;
602
-
603
- const options = {
604
- hostname: urlObj.hostname,
605
- port: urlObj.port,
606
- path: '/v1/messages',
607
- method: 'POST',
608
- headers: {
609
- 'Content-Type': 'application/json',
610
- 'x-api-key': apiKey,
611
- 'anthropic-version': '2023-06-01'
612
- },
613
- timeout: 5000
614
- };
615
-
616
- const req = client.request(options, (res) => {
617
- resolve({ valid: res.statusCode !== 401 && res.statusCode !== 403, statusCode: res.statusCode });
618
- });
619
-
620
- req.on('error', () => resolve({ valid: false, error: 'Network error' }));
621
- req.on('timeout', () => {
622
- req.destroy();
623
- resolve({ valid: false, error: 'Timeout' });
624
- });
625
-
626
- req.write(JSON.stringify({ model: 'claude-3-haiku-20240307', max_tokens: 1, messages: [{ role: 'user', content: 'test' }] }));
627
- req.end();
628
- });
629
- }
630
-
631
- /**
632
- * Diagnose Claude Code configuration issues
633
- */
634
- async function doctor() {
635
- const path = require('path');
636
- const fs = require('fs');
637
- const os = require('os');
638
-
639
- console.log(chalk.bold.cyan('\nšŸ” Claude Code Configuration Diagnostics\n'));
640
-
641
- // Check current directory
642
- console.log(chalk.bold('1. Current Directory:'));
643
- console.log(` ${process.cwd()}\n`);
644
-
645
- // Check project root
646
- const projectRoot = config.findProjectRoot();
647
- console.log(chalk.bold('2. Project Root Detection:'));
648
- if (projectRoot) {
649
- console.log(chalk.green(` āœ“ Found project root: ${projectRoot}`));
650
- } else {
651
- console.log(chalk.yellow(' ⚠ No project root found (not in a configured project)'));
652
- console.log(chalk.gray(' Run "ais use <account>" in your project root first\n'));
653
- return;
654
- }
655
-
656
- // Check ais project config
657
- const aisConfigPath = path.join(projectRoot, '.ais-project-config');
658
- console.log(chalk.bold('\n3. AIS Project Configuration:'));
659
- if (fs.existsSync(aisConfigPath)) {
660
- console.log(chalk.green(` āœ“ Config exists: ${aisConfigPath}`));
661
- try {
662
- const aisConfig = JSON.parse(fs.readFileSync(aisConfigPath, 'utf8'));
663
- console.log(` Account: ${chalk.cyan(aisConfig.activeAccount)}`);
664
- } catch (e) {
665
- console.log(chalk.red(` āœ— Error reading config: ${e.message}`));
666
- }
667
- } else {
668
- console.log(chalk.red(` āœ— Config not found: ${aisConfigPath}`));
669
- }
670
-
671
- // Check Claude config
672
- const claudeDir = path.join(projectRoot, '.claude');
673
- const claudeConfigPath = path.join(claudeDir, 'settings.local.json');
674
-
675
- console.log(chalk.bold('\n4. Claude Code Configuration:'));
676
- console.log(` Expected location: ${claudeConfigPath}`);
677
-
678
- if (fs.existsSync(claudeConfigPath)) {
679
- console.log(chalk.green(' āœ“ Claude config exists'));
680
- try {
681
- const claudeConfig = JSON.parse(fs.readFileSync(claudeConfigPath, 'utf8'));
682
-
683
- if (claudeConfig.env && claudeConfig.env.ANTHROPIC_AUTH_TOKEN) {
684
- const token = claudeConfig.env.ANTHROPIC_AUTH_TOKEN;
685
- const masked = token.substring(0, 6) + '****' + token.substring(token.length - 4);
686
- console.log(` API Token: ${masked}`);
687
- }
688
-
689
- if (claudeConfig.env && claudeConfig.env.ANTHROPIC_BASE_URL) {
690
- console.log(` API URL: ${claudeConfig.env.ANTHROPIC_BASE_URL}`);
691
- }
692
- } catch (e) {
693
- console.log(chalk.red(` āœ— Error reading Claude config: ${e.message}`));
694
- }
695
- } else {
696
- console.log(chalk.red(' āœ— Claude config not found'));
697
- console.log(chalk.yellow(' Run "ais use <account>" to generate it'));
698
- }
699
-
700
- // Check Codex profile
701
- const codexProfilePath = path.join(projectRoot, '.codex-profile');
702
- const globalCodexConfig = path.join(os.homedir(), '.codex', 'config.toml');
703
-
704
- console.log(chalk.bold('\n5. Codex Configuration:'));
705
- console.log(` Profile file: ${codexProfilePath}`);
706
-
707
- if (fs.existsSync(codexProfilePath)) {
708
- const profileName = fs.readFileSync(codexProfilePath, 'utf8').trim();
709
- console.log(chalk.green(` āœ“ Codex profile exists: ${profileName}`));
710
- console.log(chalk.cyan(` Usage: codex --profile ${profileName} [prompt]`));
711
-
712
- // Check if profile exists in global config
713
- if (fs.existsSync(globalCodexConfig)) {
714
- try {
715
- const globalConfig = fs.readFileSync(globalCodexConfig, 'utf8');
716
- const profilePattern = new RegExp(`\\[profiles\\.${profileName}\\]`);
717
-
718
- if (profilePattern.test(globalConfig)) {
719
- console.log(chalk.green(` āœ“ Profile configured in ~/.codex/config.toml`));
720
-
721
- // Parse profile info
722
- const providerMatch = globalConfig.match(new RegExp(`\\[profiles\\.${profileName}\\][\\s\\S]*?model_provider\\s*=\\s*"([^"]+)"`));
723
- const modelMatch = globalConfig.match(new RegExp(`\\[profiles\\.${profileName}\\][\\s\\S]*?model\\s*=\\s*"([^"]+)"`));
724
-
725
- if (providerMatch) {
726
- console.log(` Model Provider: ${providerMatch[1]}`);
727
-
728
- // Find provider details
729
- const providerName = providerMatch[1];
730
- const baseUrlMatch = globalConfig.match(new RegExp(`\\[model_providers\\.${providerName}\\][\\s\\S]*?base_url\\s*=\\s*"([^"]+)"`));
731
- if (baseUrlMatch) {
732
- console.log(` API URL: ${baseUrlMatch[1]}`);
733
- }
734
- }
735
- if (modelMatch) {
736
- console.log(` Model: ${modelMatch[1]}`);
737
- }
738
- } else {
739
- console.log(chalk.yellow(` ⚠ Profile not found in global config`));
740
- console.log(chalk.yellow(` Run "ais use <account>" to regenerate it`));
741
- }
742
- } catch (e) {
743
- console.log(chalk.red(` āœ— Error reading global Codex config: ${e.message}`));
744
- }
745
- }
746
- } else {
747
- console.log(chalk.yellow(' ⚠ No Codex profile configured'));
748
- console.log(chalk.yellow(' Run "ais use <codex-account>" to create one'));
749
- }
750
-
751
- // Check global Claude config
752
- const globalClaudeConfig = path.join(os.homedir(), '.claude', 'settings.json');
753
- console.log(chalk.bold('\n6. Global Claude Configuration:'));
754
- console.log(` Location: ${globalClaudeConfig}`);
755
-
756
- if (fs.existsSync(globalClaudeConfig)) {
757
- console.log(chalk.yellow(' ⚠ Global config exists (may override project config in some cases)'));
758
- try {
759
- const globalConfig = JSON.parse(fs.readFileSync(globalClaudeConfig, 'utf8'));
760
- if (globalConfig.env && globalConfig.env.ANTHROPIC_AUTH_TOKEN) {
761
- const token = globalConfig.env.ANTHROPIC_AUTH_TOKEN;
762
- const masked = token.substring(0, 6) + '****' + token.substring(token.length - 4);
763
- console.log(` Global API Token: ${masked}`);
764
- }
765
- if (globalConfig.env && globalConfig.env.ANTHROPIC_BASE_URL) {
766
- console.log(` Global API URL: ${globalConfig.env.ANTHROPIC_BASE_URL}`);
767
- }
768
- } catch (e) {
769
- console.log(chalk.red(` āœ— Error reading global config: ${e.message}`));
770
- }
771
- } else {
772
- console.log(chalk.green(' āœ“ No global config (good - project config will be used)'));
773
- }
774
-
775
- // Check current account availability
776
- console.log(chalk.bold('\n7. Current Account Availability:'));
777
- const projectAccount = config.getProjectAccount();
778
-
779
- if (projectAccount && projectAccount.apiKey) {
780
- console.log(` Testing account: ${chalk.cyan(projectAccount.name)}`);
781
- console.log(` Account type: ${chalk.cyan(projectAccount.type)}`);
782
-
783
- if (projectAccount.type === 'Claude') {
784
- console.log(' Testing with Claude CLI...');
785
- const { execSync } = require('child_process');
786
- try {
787
- execSync('claude --version', { stdio: 'pipe', timeout: 5000 });
788
- console.log(chalk.green(' āœ“ Claude CLI is available'));
789
-
790
- // Interactive CLI test
791
- console.log(' Running interactive test...');
792
- try {
793
- const testResult = execSync('echo "test" | claude', {
794
- encoding: 'utf8',
795
- timeout: 10000,
796
- env: { ...process.env, ANTHROPIC_API_KEY: projectAccount.apiKey }
797
- });
798
- console.log(chalk.green(' āœ“ Claude CLI interactive test passed'));
799
- } catch (e) {
800
- console.log(chalk.yellow(' ⚠ Claude CLI interactive test failed'));
801
- console.log(chalk.gray(` Error: ${e.message}`));
802
- }
803
- } catch (e) {
804
- console.log(chalk.yellow(' ⚠ Claude CLI not found, using API validation'));
805
- }
806
- } else if (projectAccount.type === 'Codex') {
807
- console.log(' Testing with Codex CLI...');
808
- const { execSync } = require('child_process');
809
- try {
810
- execSync('codex --version', { stdio: 'pipe', timeout: 5000 });
811
- console.log(chalk.green(' āœ“ Codex CLI is available'));
812
- } catch (e) {
813
- console.log(chalk.yellow(' ⚠ Codex CLI not found'));
814
- }
815
- }
816
-
817
- console.log(` API URL: ${projectAccount.apiUrl || 'https://api.anthropic.com'}`);
818
- console.log(' Validating API key...');
819
-
820
- const result = await validateAccount(projectAccount.apiKey, projectAccount.apiUrl);
821
-
822
- if (result.valid) {
823
- console.log(chalk.green(' āœ“ Account is valid and accessible'));
824
- if (result.statusCode) {
825
- console.log(chalk.gray(` Response status: ${result.statusCode}`));
826
- }
827
- } else {
828
- console.log(chalk.red(' āœ— Account validation failed'));
829
- if (result.error) {
830
- console.log(chalk.red(` Error: ${result.error}`));
831
- } else if (result.statusCode) {
832
- console.log(chalk.red(` Status code: ${result.statusCode}`));
833
- if (result.statusCode === 401 || result.statusCode === 403) {
834
- console.log(chalk.yellow(' ⚠ API key appears to be invalid or expired'));
835
- }
836
- }
837
- }
838
- } else {
839
- console.log(chalk.yellow(' ⚠ No account configured or API key missing'));
840
- }
841
-
842
- // Recommendations
843
- console.log(chalk.bold('\n8. Recommendations:'));
844
-
845
- if (projectRoot && process.cwd() !== projectRoot) {
846
- console.log(chalk.yellow(` ⚠ You are in a subdirectory (${path.relative(projectRoot, process.cwd())})`));
847
- console.log(chalk.cyan(' • Claude Code should still find the project config'));
848
- console.log(chalk.cyan(' • Make sure to start Claude Code from this directory or parent directories'));
849
- }
850
-
851
- if (fs.existsSync(globalClaudeConfig)) {
852
- console.log(chalk.yellow(' ⚠ Global Claude config exists:'));
853
- console.log(chalk.cyan(' • Project config should take precedence'));
854
- console.log(chalk.cyan(' • If issues persist, consider removing global env settings'));
855
- console.log(chalk.gray(` • File: ${globalClaudeConfig}`));
856
- }
857
-
858
- console.log(chalk.bold('\n9. Next Steps:'));
859
- console.log(chalk.cyan(' • Start Claude Code from your project directory or subdirectory'));
860
- console.log(chalk.cyan(' • Check which account Claude Code is using'));
861
- console.log(chalk.cyan(' • If wrong account is used, run: ais use <correct-account>'));
862
- console.log('');
863
- }
864
-
865
- /**
866
- * Start Web UI server
867
- */
868
- function startUI() {
869
- const UIServer = require('./ui-server');
870
- const server = new UIServer();
871
-
872
- console.log(chalk.cyan('\n🌐 Starting AIS Web UI...\n'));
873
- server.start();
874
- }
875
-
876
- /**
877
- * List all model groups for current account
878
- */
879
- function listModelGroups() {
880
- const projectAccount = config.getProjectAccount();
881
-
882
- if (!projectAccount) {
883
- console.log(chalk.yellow('⚠ No account set for current project.'));
884
- console.log(chalk.cyan('Use "ais use <account>" to set an account first.\n'));
885
- return;
886
- }
887
-
888
- if (!projectAccount.modelGroups || Object.keys(projectAccount.modelGroups).length === 0) {
889
- console.log(chalk.yellow(`⚠ No model groups configured for account '${projectAccount.name}'.`));
890
- console.log(chalk.cyan('Use "ais model add" to create a model group.\n'));
891
- return;
892
- }
893
-
894
- console.log(chalk.bold(`\nšŸ“‹ Model Groups for '${projectAccount.name}':\n`));
895
-
896
- Object.entries(projectAccount.modelGroups).forEach(([groupName, groupConfig]) => {
897
- const isActive = projectAccount.activeModelGroup === groupName;
898
- const marker = isActive ? chalk.green('ā— ') : ' ';
899
- const nameDisplay = isActive ? chalk.green.bold(groupName) : chalk.cyan(groupName);
900
-
901
- console.log(`${marker}${nameDisplay}`);
902
- if (Object.keys(groupConfig).length > 0) {
903
- Object.entries(groupConfig).forEach(([key, value]) => {
904
- console.log(` ${chalk.cyan(key + ':')} ${value}`);
905
- });
906
- } else {
907
- console.log(` ${chalk.gray('(empty configuration)')}`);
908
- }
909
- console.log('');
910
- });
911
-
912
- if (projectAccount.activeModelGroup) {
913
- console.log(chalk.green(`āœ“ Active model group: ${projectAccount.activeModelGroup}\n`));
914
- }
915
- }
916
-
917
- /**
918
- * Add a new model group
919
- */
920
- async function addModelGroup(name) {
921
- const projectAccount = config.getProjectAccount();
922
-
923
- if (!projectAccount) {
924
- console.log(chalk.yellow('⚠ No account set for current project.'));
925
- console.log(chalk.cyan('Use "ais use <account>" to set an account first.\n'));
926
- return;
927
- }
928
-
929
- // Prompt for group name if not provided
930
- if (!name) {
931
- const answers = await inquirer.prompt([
932
- {
933
- type: 'input',
934
- name: 'groupName',
935
- message: 'Enter model group name:',
936
- validate: (input) => input.trim() !== '' || 'Group name is required'
937
- }
938
- ]);
939
- name = answers.groupName;
940
- }
941
-
942
- // Check if group already exists
943
- if (projectAccount.modelGroups && projectAccount.modelGroups[name]) {
944
- const { overwrite } = await inquirer.prompt([
945
- {
946
- type: 'confirm',
947
- name: 'overwrite',
948
- message: `Model group '${name}' already exists. Overwrite?`,
949
- default: false
950
- }
951
- ]);
952
-
953
- if (!overwrite) {
954
- console.log(chalk.yellow('Operation cancelled.'));
955
- return;
956
- }
957
- }
958
-
959
- // Prompt for model configuration
960
- const modelGroupConfig = await promptForModelGroup();
961
-
962
- if (Object.keys(modelGroupConfig).length === 0) {
963
- console.log(chalk.yellow('⚠ No configuration provided. Model group not created.'));
964
- return;
965
- }
966
-
967
- // Add the model group to the account
968
- const account = config.getAccount(projectAccount.name);
969
- if (!account.modelGroups) {
970
- account.modelGroups = {};
971
- }
972
- account.modelGroups[name] = modelGroupConfig;
973
-
974
- // Set as active if it's the first group
975
- if (!account.activeModelGroup) {
976
- account.activeModelGroup = name;
977
- }
978
-
979
- config.addAccount(projectAccount.name, account);
980
- console.log(chalk.green(`āœ“ Model group '${name}' added successfully!`));
981
-
982
- // Ask if user wants to activate this group
983
- if (account.activeModelGroup !== name) {
984
- const { activate } = await inquirer.prompt([
985
- {
986
- type: 'confirm',
987
- name: 'activate',
988
- message: `Set '${name}' as active model group?`,
989
- default: false
990
- }
991
- ]);
992
-
993
- if (activate) {
994
- account.activeModelGroup = name;
995
- config.addAccount(projectAccount.name, account);
996
-
997
- // Regenerate Claude config with new active group
998
- config.setProjectAccount(projectAccount.name);
999
- console.log(chalk.green(`āœ“ Switched to model group '${name}' and updated Claude configuration.`));
1000
- }
1001
- } else {
1002
- // Regenerate Claude config
1003
- config.setProjectAccount(projectAccount.name);
1004
- console.log(chalk.green(`āœ“ Updated Claude configuration with active group '${name}'.`));
1005
- }
1006
- }
1007
-
1008
- /**
1009
- * Switch to a different model group
1010
- */
1011
- async function useModelGroup(name) {
1012
- const projectAccount = config.getProjectAccount();
1013
-
1014
- if (!projectAccount) {
1015
- console.log(chalk.yellow('⚠ No account set for current project.'));
1016
- console.log(chalk.cyan('Use "ais use <account>" to set an account first.\n'));
1017
- return;
1018
- }
1019
-
1020
- if (!projectAccount.modelGroups || Object.keys(projectAccount.modelGroups).length === 0) {
1021
- console.log(chalk.yellow(`⚠ No model groups configured for account '${projectAccount.name}'.`));
1022
- console.log(chalk.cyan('Use "ais model add" to create a model group first.\n'));
1023
- return;
1024
- }
1025
-
1026
- if (!projectAccount.modelGroups[name]) {
1027
- console.log(chalk.red(`āœ— Model group '${name}' not found.`));
1028
- console.log(chalk.yellow('Available groups:'), Object.keys(projectAccount.modelGroups).join(', '));
1029
- return;
1030
- }
1031
-
1032
- // Update active model group
1033
- const account = config.getAccount(projectAccount.name);
1034
- account.activeModelGroup = name;
1035
- config.addAccount(projectAccount.name, account);
1036
-
1037
- // Regenerate Claude config with new active group
1038
- config.setProjectAccount(projectAccount.name);
1039
-
1040
- console.log(chalk.green(`āœ“ Switched to model group '${name}'.`));
1041
- console.log(chalk.cyan('āœ“ Claude configuration updated.\n'));
1042
- }
1043
-
1044
- /**
1045
- * Remove a model group
1046
- */
1047
- async function removeModelGroup(name) {
1048
- const projectAccount = config.getProjectAccount();
1049
-
1050
- if (!projectAccount) {
1051
- console.log(chalk.yellow('⚠ No account set for current project.'));
1052
- console.log(chalk.cyan('Use "ais use <account>" to set an account first.\n'));
1053
- return;
1054
- }
1055
-
1056
- if (!projectAccount.modelGroups || Object.keys(projectAccount.modelGroups).length === 0) {
1057
- console.log(chalk.yellow(`⚠ No model groups configured for account '${projectAccount.name}'.`));
1058
- return;
1059
- }
1060
-
1061
- // Prompt for group name if not provided
1062
- if (!name) {
1063
- const groupNames = Object.keys(projectAccount.modelGroups);
1064
- const answers = await inquirer.prompt([
1065
- {
1066
- type: 'list',
1067
- name: 'groupName',
1068
- message: 'Select a model group to remove:',
1069
- choices: groupNames
1070
- }
1071
- ]);
1072
- name = answers.groupName;
1073
- }
1074
-
1075
- if (!projectAccount.modelGroups[name]) {
1076
- console.log(chalk.red(`āœ— Model group '${name}' not found.`));
1077
- return;
1078
- }
1079
-
1080
- const { confirm } = await inquirer.prompt([
1081
- {
1082
- type: 'confirm',
1083
- name: 'confirm',
1084
- message: `Are you sure you want to remove model group '${name}'?`,
1085
- default: false
1086
- }
1087
- ]);
1088
-
1089
- if (!confirm) {
1090
- console.log(chalk.yellow('Operation cancelled.'));
1091
- return;
1092
- }
1093
-
1094
- // Remove the model group
1095
- const account = config.getAccount(projectAccount.name);
1096
- delete account.modelGroups[name];
1097
-
1098
- // Update active group if needed
1099
- if (account.activeModelGroup === name) {
1100
- const remainingGroups = Object.keys(account.modelGroups);
1101
- account.activeModelGroup = remainingGroups.length > 0 ? remainingGroups[0] : null;
1102
-
1103
- if (account.activeModelGroup) {
1104
- console.log(chalk.cyan(`āœ“ Switched active group to '${account.activeModelGroup}'.`));
1105
- } else {
1106
- console.log(chalk.yellow('⚠ No model groups remaining.'));
1107
- }
1108
- }
1109
-
1110
- config.addAccount(projectAccount.name, account);
1111
-
1112
- // Regenerate Claude config
1113
- config.setProjectAccount(projectAccount.name);
1114
-
1115
- console.log(chalk.green(`āœ“ Model group '${name}' removed successfully.`));
1116
- }
1117
-
1118
- /**
1119
- * Show model group configuration
1120
- */
1121
- function showModelGroup(name) {
1122
- const projectAccount = config.getProjectAccount();
1123
-
1124
- if (!projectAccount) {
1125
- console.log(chalk.yellow('⚠ No account set for current project.'));
1126
- console.log(chalk.cyan('Use "ais use <account>" to set an account first.\n'));
1127
- return;
1128
- }
1129
-
1130
- if (!projectAccount.modelGroups || Object.keys(projectAccount.modelGroups).length === 0) {
1131
- console.log(chalk.yellow(`⚠ No model groups configured for account '${projectAccount.name}'.`));
1132
- return;
1133
- }
1134
-
1135
- // Show active group if no name provided
1136
- if (!name) {
1137
- if (!projectAccount.activeModelGroup) {
1138
- console.log(chalk.yellow('⚠ No active model group.'));
1139
- return;
1140
- }
1141
- name = projectAccount.activeModelGroup;
1142
- }
1143
-
1144
- if (!projectAccount.modelGroups[name]) {
1145
- console.log(chalk.red(`āœ— Model group '${name}' not found.`));
1146
- console.log(chalk.yellow('Available groups:'), Object.keys(projectAccount.modelGroups).join(', '));
1147
- return;
1148
- }
1149
-
1150
- const isActive = projectAccount.activeModelGroup === name;
1151
- const activeMarker = isActive ? chalk.green(' (active)') : '';
1152
-
1153
- console.log(chalk.bold(`\nšŸ“‹ Model Group: ${chalk.cyan(name)}${activeMarker}\n`));
1154
-
1155
- const groupConfig = projectAccount.modelGroups[name];
1156
- if (Object.keys(groupConfig).length === 0) {
1157
- console.log(chalk.gray(' (empty configuration)\n'));
1158
- } else {
1159
- Object.entries(groupConfig).forEach(([key, value]) => {
1160
- console.log(` ${chalk.cyan(key)}: ${value}`);
1161
- });
1162
- console.log('');
1163
- }
1164
- }
1165
-
1166
- module.exports = {
1167
- addAccount,
1168
- listAccounts,
1169
- useAccount,
1170
- showInfo,
1171
- removeAccount,
1172
- showCurrent,
1173
- showPaths,
1174
- exportAccount,
1175
- doctor,
1176
- startUI,
1177
- listModelGroups,
1178
- addModelGroup,
1179
- useModelGroup,
1180
- removeModelGroup,
1181
- showModelGroup
1182
- };