ai-account-switch 1.7.0 → 1.8.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-account-switch",
3
- "version": "1.7.0",
3
+ "version": "1.8.1",
4
4
  "description": "A cross-platform CLI tool to manage and switch Claude/Codex/Droids account configurations",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -1,6 +1,7 @@
1
1
  const chalk = require("chalk");
2
2
  const inquirer = require("inquirer");
3
3
  const ConfigManager = require("../config");
4
+ const { WIRE_API_MODES, DEFAULT_WIRE_API, ACCOUNT_TYPES } = require("../config");
4
5
  const { maskApiKey } = require("./helpers");
5
6
  const { promptForModelGroup } = require("./model");
6
7
 
@@ -53,6 +54,9 @@ async function addAccount(name, options) {
53
54
  },
54
55
  ]);
55
56
 
57
+ // Store wire_api selection for Codex accounts
58
+ let wireApiSelection = null;
59
+
56
60
  // Show configuration tips based on account type
57
61
  if (typeAnswer.type === "Codex") {
58
62
  console.log(
@@ -60,12 +64,22 @@ async function addAccount(name, options) {
60
64
  );
61
65
  console.log(
62
66
  chalk.gray(
63
- " • API URL should include the full path (e.g., https://api.example.com/v1) (API URL 应包含完整路径,例如: https://api.example.com/v1)"
67
+ " • For domain-only URLs (e.g., https://api.example.com), /v1 will be added automatically"
64
68
  )
65
69
  );
66
70
  console.log(
67
71
  chalk.gray(
68
- " AIS will automatically add /v1 if missing (AIS 会自动添加 /v1 如果缺失)"
72
+ " 对于仅域名的 URL (例如 https://api.example.com), 将自动添加 /v1"
73
+ )
74
+ );
75
+ console.log(
76
+ chalk.gray(
77
+ " • URLs with existing paths (e.g., https://api.example.com/v2) will remain unchanged"
78
+ )
79
+ );
80
+ console.log(
81
+ chalk.gray(
82
+ " 已有路径的 URL (例如 https://api.example.com/v2) 将保持不变"
69
83
  )
70
84
  );
71
85
  console.log(
@@ -73,6 +87,44 @@ async function addAccount(name, options) {
73
87
  " • Codex uses OpenAI-compatible API format (Codex 使用 OpenAI 兼容的 API 格式)\n"
74
88
  )
75
89
  );
90
+
91
+ // Prompt for wire_api mode selection
92
+ const wireApiAnswer = await inquirer.prompt([
93
+ {
94
+ type: "list",
95
+ name: "wireApi",
96
+ message: "Select wire_api mode (请选择 wire_api 模式):",
97
+ choices: [
98
+ {
99
+ name: "chat - Use API key in HTTP headers (OpenAI-compatible) (在 HTTP 头中使用 API key,OpenAI 兼容)",
100
+ value: WIRE_API_MODES.CHAT
101
+ },
102
+ {
103
+ name: "responses - Use API key in auth.json (requires_openai_auth) (在 auth.json 中使用 API key)",
104
+ value: WIRE_API_MODES.RESPONSES
105
+ }
106
+ ],
107
+ default: DEFAULT_WIRE_API
108
+ }
109
+ ]);
110
+
111
+ wireApiSelection = wireApiAnswer.wireApi;
112
+
113
+ // Validate input
114
+ if (!Object.values(WIRE_API_MODES).includes(wireApiSelection)) {
115
+ console.log(
116
+ chalk.yellow(
117
+ `⚠ Invalid wire_api mode, using default: ${DEFAULT_WIRE_API} (无效的模式,使用默认值)`
118
+ )
119
+ );
120
+ wireApiSelection = DEFAULT_WIRE_API;
121
+ }
122
+
123
+ console.log(
124
+ chalk.cyan(
125
+ `\n✓ Selected wire_api mode (已选择模式): ${wireApiSelection}\n`
126
+ )
127
+ );
76
128
  } else if (typeAnswer.type === "Droids") {
77
129
  console.log(
78
130
  chalk.cyan("\n📝 Droids Configuration Tips (Droids 配置提示):")
@@ -121,7 +173,11 @@ async function addAccount(name, options) {
121
173
  name: "apiUrl",
122
174
  message:
123
175
  typeAnswer.type === "Codex"
124
- ? "Enter API URL (请输入 API URL) (e.g., https://api.example.com or https://api.example.com/v1) :"
176
+ ? "Enter API URL (请输入 API URL)\n" +
177
+ " Examples (示例):\n" +
178
+ " https://api.example.com → will add /v1 (将添加 /v1)\n" +
179
+ " https://api.example.com/v2 → will keep as is (保持不变)\n" +
180
+ " URL:"
125
181
  : typeAnswer.type === "CCR"
126
182
  ? "Enter API URL (请输入 API URL):"
127
183
  : "Enter API URL (optional) (请输入 API URL,可选):",
@@ -151,6 +207,11 @@ async function addAccount(name, options) {
151
207
  // Merge type into accountData
152
208
  accountData.type = typeAnswer.type;
153
209
 
210
+ // Add wire_api selection for Codex accounts
211
+ if (typeAnswer.type === "Codex" && wireApiSelection) {
212
+ accountData.wireApi = wireApiSelection;
213
+ }
214
+
154
215
  // Handle custom environment variables
155
216
  if (accountData.addCustomEnv) {
156
217
  accountData.customEnv = {};
@@ -440,6 +501,27 @@ async function addAccount(name, options) {
440
501
  )
441
502
  );
442
503
  console.log(chalk.cyan(` ais use ${name}\n`));
504
+
505
+ // Show wire_api mode specific information
506
+ if (accountData.wireApi === WIRE_API_MODES.RESPONSES) {
507
+ console.log(
508
+ chalk.yellow(
509
+ " ⚠ Note: This account uses 'responses' mode (此账号使用 'responses' 模式)"
510
+ )
511
+ );
512
+ console.log(
513
+ chalk.white(
514
+ " Your API key will be stored in ~/.codex/auth.json (API key 将存储在 ~/.codex/auth.json)\n"
515
+ )
516
+ );
517
+ } else {
518
+ console.log(
519
+ chalk.cyan(
520
+ " ✓ This account uses 'chat' mode (此账号使用 'chat' 模式)\n"
521
+ )
522
+ );
523
+ }
524
+
443
525
  console.log(
444
526
  chalk.white(
445
527
  "2. Use Codex with the generated profile (使用生成的配置文件运行 Codex):"
@@ -548,6 +630,7 @@ function listAccounts() {
548
630
  console.log(`${marker}${nameDisplay}`);
549
631
  console.log(` Type: ${account.type}`);
550
632
  console.log(` API Key: ${maskApiKey(account.apiKey)}`);
633
+ if (account.apiUrl) console.log(` API URL: ${account.apiUrl}`);
551
634
  if (account.email) console.log(` Email: ${account.email}`);
552
635
  if (account.description)
553
636
  console.log(` Description: ${account.description}`);
@@ -575,6 +658,11 @@ function listAccounts() {
575
658
  ) {
576
659
  console.log(` Model: ${account.model}`);
577
660
  }
661
+ // Display wire_api configuration for Codex accounts
662
+ if (account.type === ACCOUNT_TYPES.CODEX) {
663
+ const wireApi = account.wireApi || `${DEFAULT_WIRE_API} (default)`;
664
+ console.log(` Wire API: ${wireApi}`);
665
+ }
578
666
  console.log(
579
667
  ` Created: ${new Date(account.createdAt).toLocaleString()}`
580
668
  );
@@ -674,6 +762,27 @@ async function useAccount(name) {
674
762
  `✓ Codex profile created (Codex 配置文件已创建): ${profileName}`
675
763
  )
676
764
  );
765
+
766
+ // Display wire_api mode information
767
+ if (account.wireApi === WIRE_API_MODES.RESPONSES) {
768
+ console.log(
769
+ chalk.yellow(
770
+ `✓ Wire API mode: ${WIRE_API_MODES.RESPONSES} (使用 ${WIRE_API_MODES.RESPONSES} 模式)`
771
+ )
772
+ );
773
+ console.log(
774
+ chalk.yellow(
775
+ `✓ API key stored in ~/.codex/auth.json (API key 已存储在 ~/.codex/auth.json)`
776
+ )
777
+ );
778
+ } else {
779
+ console.log(
780
+ chalk.cyan(
781
+ `✓ Wire API mode: ${WIRE_API_MODES.CHAT} (使用 ${WIRE_API_MODES.CHAT} 模式)`
782
+ )
783
+ );
784
+ }
785
+
677
786
  console.log("");
678
787
  console.log(chalk.bold.cyan("📖 Next Steps (下一步):"));
679
788
  console.log(
@@ -864,6 +973,10 @@ function showInfo() {
864
973
  ) {
865
974
  console.log(`${chalk.cyan("Model:")} ${projectAccount.model}`);
866
975
  }
976
+ // Display wire_api configuration for Codex accounts
977
+ if (projectAccount.type === ACCOUNT_TYPES.CODEX && projectAccount.wireApi) {
978
+ console.log(`${chalk.cyan("Wire API:")} ${projectAccount.wireApi}`);
979
+ }
867
980
  console.log(
868
981
  `${chalk.cyan("Set At:")} ${new Date(
869
982
  projectAccount.setAt
package/src/config.js CHANGED
@@ -2,6 +2,22 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const os = require('os');
4
4
 
5
+ // Constants for wire API modes
6
+ const WIRE_API_MODES = {
7
+ CHAT: 'chat',
8
+ RESPONSES: 'responses'
9
+ };
10
+
11
+ const DEFAULT_WIRE_API = WIRE_API_MODES.CHAT;
12
+
13
+ // Constants for account types
14
+ const ACCOUNT_TYPES = {
15
+ CLAUDE: 'Claude',
16
+ CODEX: 'Codex',
17
+ CCR: 'CCR',
18
+ DROIDS: 'Droids'
19
+ };
20
+
5
21
  /**
6
22
  * Cross-platform configuration manager
7
23
  * Stores global accounts in user home directory
@@ -556,10 +572,23 @@ class ConfigManager {
556
572
  profileConfig += `model = "${account.model}"\n`;
557
573
  }
558
574
 
559
- // Ensure API URL has proper path
575
+ // Smart /v1 path handling
560
576
  let baseUrl = account.apiUrl || '';
561
- if (baseUrl && !baseUrl.match(/\/v1\/?$/)) {
562
- baseUrl = baseUrl.replace(/\/$/, '') + '/v1';
577
+ if (baseUrl) {
578
+ // Remove trailing slashes
579
+ baseUrl = baseUrl.replace(/\/+$/, '');
580
+
581
+ // Check if URL already has a path beyond the domain
582
+ // Pattern: protocol://domain or protocol://domain:port (no path)
583
+ const isDomainOnly = baseUrl.match(/^https?:\/\/[^\/]+$/);
584
+
585
+ // Only add /v1 if:
586
+ // 1. URL is domain-only (no path), OR
587
+ // 2. URL explicitly ends with /v1 already (ensure consistency)
588
+ if (isDomainOnly) {
589
+ baseUrl += '/v1';
590
+ }
591
+ // If URL has a path (e.g., /v2, /custom, /api), keep it as is
563
592
  }
564
593
 
565
594
  // Remove existing provider if it exists (simpler than updating)
@@ -574,11 +603,26 @@ class ConfigManager {
574
603
  profileConfig += `base_url = "${baseUrl}"\n`;
575
604
  }
576
605
 
577
- // Determine wire_api: use "chat" for most APIs (OpenAI-compatible)
578
- profileConfig += `wire_api = "chat"\n`;
579
-
580
- // Add authentication header
581
- profileConfig += `http_headers = { "Authorization" = "Bearer ${account.apiKey}" }\n`;
606
+ // Determine wire_api based on account configuration (default to chat for backward compatibility)
607
+ const wireApi = account.wireApi || DEFAULT_WIRE_API;
608
+
609
+ if (wireApi === WIRE_API_MODES.CHAT) {
610
+ // Chat mode: use HTTP headers for authentication
611
+ profileConfig += `wire_api = "${WIRE_API_MODES.CHAT}"\n`;
612
+ profileConfig += `http_headers = { "Authorization" = "Bearer ${account.apiKey}" }\n`;
613
+
614
+ // Note: We do NOT clear auth.json here because:
615
+ // 1. auth.json is a global file shared by all projects
616
+ // 2. Other projects may be using responses mode and need the API key
617
+ // 3. Chat mode doesn't use auth.json anyway, so no conflict exists
618
+ } else if (wireApi === WIRE_API_MODES.RESPONSES) {
619
+ // Responses mode: use auth.json for authentication
620
+ profileConfig += `wire_api = "${WIRE_API_MODES.RESPONSES}"\n`;
621
+ profileConfig += `requires_openai_auth = true\n`;
622
+
623
+ // Update auth.json with API key
624
+ this.updateCodexAuthJson(account.apiKey);
625
+ }
582
626
  }
583
627
 
584
628
  // Remove all old profiles with the same name (including duplicates)
@@ -607,6 +651,146 @@ class ConfigManager {
607
651
  fs.writeFileSync(helperScript, profileName, 'utf8');
608
652
  }
609
653
 
654
+ /**
655
+ * Read auth.json file
656
+ * @private
657
+ * @returns {Object} Parsed auth data or empty object
658
+ */
659
+ _readAuthJson(authJsonFile) {
660
+ if (!fs.existsSync(authJsonFile)) {
661
+ return {};
662
+ }
663
+
664
+ try {
665
+ const content = fs.readFileSync(authJsonFile, 'utf8');
666
+ return JSON.parse(content);
667
+ } catch (parseError) {
668
+ const chalk = require('chalk');
669
+ console.warn(
670
+ chalk.yellow(
671
+ `⚠ Warning: Could not parse existing auth.json, will create new file (警告: 无法解析现有 auth.json,将创建新文件)`
672
+ )
673
+ );
674
+ return {};
675
+ }
676
+ }
677
+
678
+ /**
679
+ * Write auth.json file atomically with proper permissions
680
+ * Uses atomic write (temp file + rename) to prevent corruption from concurrent access
681
+ * @private
682
+ * @param {string} authJsonFile - Path to auth.json
683
+ * @param {Object} authData - Auth data to write
684
+ */
685
+ _writeAuthJson(authJsonFile, authData) {
686
+ const chalk = require('chalk');
687
+ const tempFile = `${authJsonFile}.tmp.${process.pid}`;
688
+
689
+ try {
690
+ // Write to temporary file first (atomic operation)
691
+ fs.writeFileSync(tempFile, JSON.stringify(authData, null, 2), 'utf8');
692
+
693
+ // Set file permissions to 600 (owner read/write only) for security
694
+ if (process.platform !== 'win32') {
695
+ fs.chmodSync(tempFile, 0o600);
696
+ }
697
+
698
+ // Atomically rename temp file to actual file
699
+ // This is atomic on POSIX systems and prevents corruption
700
+ fs.renameSync(tempFile, authJsonFile);
701
+ } catch (error) {
702
+ // Clean up temp file if it exists
703
+ if (fs.existsSync(tempFile)) {
704
+ try {
705
+ fs.unlinkSync(tempFile);
706
+ } catch (cleanupError) {
707
+ // Ignore cleanup errors
708
+ }
709
+ }
710
+ throw error;
711
+ }
712
+ }
713
+
714
+ /**
715
+ * Clear OPENAI_API_KEY in ~/.codex/auth.json for chat mode
716
+ * @deprecated This method is no longer called automatically.
717
+ * Chat mode doesn't require clearing auth.json since it doesn't use it.
718
+ */
719
+ clearCodexAuthJson() {
720
+ const chalk = require('chalk');
721
+ const codexDir = path.join(os.homedir(), '.codex');
722
+ const authJsonFile = path.join(codexDir, 'auth.json');
723
+
724
+ try {
725
+ // Ensure .codex directory exists
726
+ if (!fs.existsSync(codexDir)) {
727
+ fs.mkdirSync(codexDir, { recursive: true });
728
+ }
729
+
730
+ // Read existing auth data
731
+ const authData = this._readAuthJson(authJsonFile);
732
+
733
+ // Clear OPENAI_API_KEY (set to empty string)
734
+ authData.OPENAI_API_KEY = "";
735
+
736
+ // Write atomically with proper permissions
737
+ this._writeAuthJson(authJsonFile, authData);
738
+
739
+ console.log(
740
+ chalk.cyan(
741
+ `✓ Cleared OPENAI_API_KEY in auth.json (chat mode) (已清空 auth.json 中的 OPENAI_API_KEY)`
742
+ )
743
+ );
744
+ } catch (error) {
745
+ console.error(
746
+ chalk.yellow(
747
+ `⚠ Warning: Failed to clear auth.json: ${error.message} (警告: 清空 auth.json 失败)`
748
+ )
749
+ );
750
+ // Don't throw error, just warn - this is not critical for chat mode
751
+ }
752
+ }
753
+
754
+ /**
755
+ * Update ~/.codex/auth.json with API key for responses mode
756
+ * @param {string} apiKey - API key to store in auth.json
757
+ * @throws {Error} If file operations fail
758
+ */
759
+ updateCodexAuthJson(apiKey) {
760
+ const chalk = require('chalk');
761
+ const codexDir = path.join(os.homedir(), '.codex');
762
+ const authJsonFile = path.join(codexDir, 'auth.json');
763
+
764
+ try {
765
+ // Ensure .codex directory exists
766
+ if (!fs.existsSync(codexDir)) {
767
+ fs.mkdirSync(codexDir, { recursive: true });
768
+ }
769
+
770
+ // Read existing auth data
771
+ const authData = this._readAuthJson(authJsonFile);
772
+
773
+ // Update OPENAI_API_KEY
774
+ authData.OPENAI_API_KEY = apiKey;
775
+
776
+ // Write atomically with proper permissions
777
+ this._writeAuthJson(authJsonFile, authData);
778
+
779
+ console.log(
780
+ chalk.green(
781
+ `✓ Updated auth.json at: ${authJsonFile} (已更新 auth.json)`
782
+ )
783
+ );
784
+ } catch (error) {
785
+ console.error(
786
+ chalk.red(
787
+ `✗ Failed to update auth.json: ${error.message} (更新 auth.json 失败)`
788
+ )
789
+ );
790
+ throw error;
791
+ }
792
+ }
793
+
610
794
  /**
611
795
  * Get current project's active account
612
796
  * Searches upwards from current directory to find project root
@@ -1147,3 +1331,6 @@ class ConfigManager {
1147
1331
  }
1148
1332
 
1149
1333
  module.exports = ConfigManager;
1334
+ module.exports.WIRE_API_MODES = WIRE_API_MODES;
1335
+ module.exports.DEFAULT_WIRE_API = DEFAULT_WIRE_API;
1336
+ module.exports.ACCOUNT_TYPES = ACCOUNT_TYPES;
package/src/ui-server.js CHANGED
@@ -2,6 +2,7 @@ const http = require('http');
2
2
  const path = require('path');
3
3
  const fs = require('fs');
4
4
  const ConfigManager = require('./config');
5
+ const { WIRE_API_MODES, DEFAULT_WIRE_API } = require('./config');
5
6
 
6
7
  class UIServer {
7
8
  constructor(port = null) {
@@ -1491,6 +1492,18 @@ class UIServer {
1491
1492
  <label for="apiUrl" data-i18n="apiUrl">API URL (可选)</label>
1492
1493
  <input type="text" id="apiUrl" data-i18n-placeholder="apiUrlPlaceholder" placeholder="https://api.anthropic.com">
1493
1494
  </div>
1495
+ <!-- Wire API selection for Codex accounts -->
1496
+ <div class="form-group" id="wireApiGroup" style="display: none;">
1497
+ <label for="wireApi">Wire API 模式</label>
1498
+ <select id="wireApi">
1499
+ <option value="chat">chat - HTTP Headers 认证 (OpenAI 兼容)</option>
1500
+ <option value="responses">responses - auth.json 认证 (requires_openai_auth)</option>
1501
+ </select>
1502
+ <small style="color: #666; display: block; margin-top: 5px;">
1503
+ chat: API key 存储在 HTTP headers 中<br>
1504
+ responses: API key 存储在 ~/.codex/auth.json 中
1505
+ </small>
1506
+ </div>
1494
1507
  <div class="form-group">
1495
1508
  <label for="email" data-i18n="email">邮箱 (可选)</label>
1496
1509
  <input type="email" id="email" data-i18n-placeholder="emailPlaceholder" placeholder="user@example.com">
@@ -1615,6 +1628,13 @@ class UIServer {
1615
1628
  </div>
1616
1629
 
1617
1630
  <script>
1631
+ // Constants for wire API modes (injected from backend)
1632
+ const WIRE_API_MODES = {
1633
+ CHAT: '${WIRE_API_MODES.CHAT}',
1634
+ RESPONSES: '${WIRE_API_MODES.RESPONSES}'
1635
+ };
1636
+ const DEFAULT_WIRE_API = '${DEFAULT_WIRE_API}';
1637
+
1618
1638
  // i18n translations
1619
1639
  const translations = {
1620
1640
  zh: {
@@ -2021,6 +2041,12 @@ class UIServer {
2021
2041
  <div class="info-value">\${data.model}</div>
2022
2042
  </div>
2023
2043
  \` : ''}
2044
+ \${data.type === 'Codex' ? \`
2045
+ <div class="account-info">
2046
+ <div class="info-label">Wire API</div>
2047
+ <div class="info-value">\${data.wireApi || (DEFAULT_WIRE_API + ' (default)')}</div>
2048
+ </div>
2049
+ \` : ''}
2024
2050
  \${data.type === 'CCR' && data.ccrConfig ? \`
2025
2051
  <div class="account-info">
2026
2052
  <div class="info-label">CCR Provider</div>
@@ -2051,6 +2077,12 @@ class UIServer {
2051
2077
  const simpleModelGroup = document.getElementById('simpleModelGroup');
2052
2078
  const claudeModelGroup = document.getElementById('claudeModelGroup');
2053
2079
  const ccrModelGroup = document.getElementById('ccrModelGroup');
2080
+ const wireApiGroup = document.getElementById('wireApiGroup');
2081
+
2082
+ // Show/hide wire_api field for Codex accounts
2083
+ if (wireApiGroup) {
2084
+ wireApiGroup.style.display = accountType === 'Codex' ? 'block' : 'none';
2085
+ }
2054
2086
 
2055
2087
  if (accountType === 'Codex' || accountType === 'Droids') {
2056
2088
  simpleModelGroup.style.display = 'block';
@@ -2117,6 +2149,12 @@ class UIServer {
2117
2149
  if (account.type === 'Codex' || account.type === 'Droids') {
2118
2150
  // Load simple model field
2119
2151
  document.getElementById('simpleModel').value = account.model || '';
2152
+
2153
+ // Load wire_api for Codex accounts
2154
+ if (account.type === 'Codex') {
2155
+ document.getElementById('wireApi').value = account.wireApi || DEFAULT_WIRE_API;
2156
+ }
2157
+
2120
2158
  // Clear model groups and CCR config
2121
2159
  document.getElementById('modelGroupsList').innerHTML = '';
2122
2160
  modelGroupCount = 0;
@@ -2379,6 +2417,14 @@ class UIServer {
2379
2417
  if (simpleModel) {
2380
2418
  accountData.model = simpleModel;
2381
2419
  }
2420
+
2421
+ // Add wire_api for Codex accounts
2422
+ if (accountType === 'Codex') {
2423
+ const wireApi = document.getElementById('wireApi').value;
2424
+ if (wireApi) {
2425
+ accountData.wireApi = wireApi;
2426
+ }
2427
+ }
2382
2428
  } else if (accountType === 'CCR') {
2383
2429
  // Collect CCR config
2384
2430
  const providerName = document.getElementById('ccrProviderName').value.trim();