@x-all-in-one/coding-helper 0.2.0 → 0.3.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.
@@ -1,22 +1,21 @@
1
1
  import { BaseTool } from './base-tool.js';
2
+ export interface ModelProviderInfo {
3
+ name: string;
4
+ base_url?: string;
5
+ env_key?: string;
6
+ wire_api?: 'chat' | 'responses';
7
+ }
2
8
  export interface CodexConfig {
3
9
  model?: string;
4
- provider?: {
5
- name?: string;
6
- baseURL?: string;
7
- apiKey?: string;
8
- };
9
- [key: string]: any;
10
+ model_provider?: string;
11
+ model_providers?: Record<string, ModelProviderInfo>;
12
+ [key: string]: unknown;
10
13
  }
11
14
  export interface CodexModelConfig {
12
15
  apiKey: string;
13
16
  model: string;
14
17
  baseUrl?: string;
15
18
  }
16
- /**
17
- * Codex 工具实现
18
- * 实现 ITool 接口,管理 Codex 的配置和生命周期
19
- */
20
19
  export declare class CodexTool extends BaseTool {
21
20
  private static instance;
22
21
  readonly name = "codex";
@@ -26,33 +25,15 @@ export declare class CodexTool extends BaseTool {
26
25
  readonly configPath: string;
27
26
  private constructor();
28
27
  static getInstance(): CodexTool;
29
- /**
30
- * Ensure config directory exists
31
- */
32
28
  private ensureDir;
33
- /**
34
- * Read codex config
35
- */
36
29
  getConfig(): CodexConfig;
37
- /**
38
- * Save codex config
39
- */
40
- saveConfig(config: unknown): void;
41
- /**
42
- * Fetch available models from API
43
- */
30
+ saveConfig(config: CodexConfig): void;
44
31
  fetchAvailableModels(apiKey: string): Promise<string[]>;
45
- /**
46
- * Save model config to Codex
47
- */
48
32
  saveModelConfig(config: unknown): Promise<void>;
49
- /**
50
- * Get current model config
51
- */
33
+ private setApiKeyEnv;
34
+ private getShellRcPath;
35
+ private getEnvValue;
52
36
  getModelConfig(): CodexModelConfig | null;
53
- /**
54
- * Clear model config
55
- */
56
37
  clearModelConfig(): void;
57
38
  }
58
39
  export declare const codexTool: CodexTool;
@@ -1,13 +1,11 @@
1
+ import { execSync } from 'node:child_process';
1
2
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
3
  import { homedir } from 'node:os';
3
4
  import { dirname, join } from 'node:path';
5
+ import { parse, stringify } from 'smol-toml';
4
6
  import { BaseTool } from './base-tool.js';
5
- // 使用相同的 X-AIO API 端点
6
7
  const DEFAULT_BASE_URL = 'https://code-api.x-aio.com/v1';
7
- /**
8
- * Codex 工具实现
9
- * 实现 ITool 接口,管理 Codex 的配置和生命周期
10
- */
8
+ const X_AIO_CODE_API_KEY_ENV = 'X_AIO_CODE_API_KEY';
11
9
  export class CodexTool extends BaseTool {
12
10
  static instance;
13
11
  name = 'codex';
@@ -17,10 +15,7 @@ export class CodexTool extends BaseTool {
17
15
  configPath;
18
16
  constructor() {
19
17
  super();
20
- // Codex config file path (cross-platform support)
21
- // - macOS/Linux: ~/.codex/config.json
22
- // - Windows: %USERPROFILE%\.codex\config.json
23
- this.configPath = join(homedir(), '.codex', 'config.json');
18
+ this.configPath = join(homedir(), '.codex', 'config.toml');
24
19
  }
25
20
  static getInstance() {
26
21
  if (!CodexTool.instance) {
@@ -28,24 +23,17 @@ export class CodexTool extends BaseTool {
28
23
  }
29
24
  return CodexTool.instance;
30
25
  }
31
- /**
32
- * Ensure config directory exists
33
- */
34
26
  ensureDir(filePath) {
35
27
  const dir = dirname(filePath);
36
28
  if (!existsSync(dir)) {
37
29
  mkdirSync(dir, { recursive: true });
38
30
  }
39
31
  }
40
- // ==================== ITool 接口实现 ====================
41
- /**
42
- * Read codex config
43
- */
44
32
  getConfig() {
45
33
  try {
46
34
  if (existsSync(this.configPath)) {
47
35
  const content = readFileSync(this.configPath, 'utf-8');
48
- return JSON.parse(content);
36
+ return parse(content);
49
37
  }
50
38
  }
51
39
  catch (error) {
@@ -53,21 +41,15 @@ export class CodexTool extends BaseTool {
53
41
  }
54
42
  return {};
55
43
  }
56
- /**
57
- * Save codex config
58
- */
59
44
  saveConfig(config) {
60
45
  try {
61
46
  this.ensureDir(this.configPath);
62
- writeFileSync(this.configPath, JSON.stringify(config, null, 2), 'utf-8');
47
+ writeFileSync(this.configPath, stringify(config), 'utf-8');
63
48
  }
64
49
  catch (error) {
65
50
  throw new Error(`Failed to save Codex config: ${error}`);
66
51
  }
67
52
  }
68
- /**
69
- * Fetch available models from API
70
- */
71
53
  async fetchAvailableModels(apiKey) {
72
54
  try {
73
55
  const controller = new AbortController();
@@ -95,48 +77,145 @@ export class CodexTool extends BaseTool {
95
77
  return [];
96
78
  }
97
79
  }
98
- /**
99
- * Save model config to Codex
100
- */
101
80
  async saveModelConfig(config) {
102
81
  const modelConfig = config;
103
- // Read current config (preserve user's other settings)
104
82
  const currentConfig = this.getConfig();
105
- // Build new config
83
+ const providerKey = 'x-aio';
106
84
  const newConfig = {
107
85
  ...currentConfig,
108
86
  model: modelConfig.model,
109
- provider: {
110
- name: 'X-AIO',
111
- baseURL: modelConfig.baseUrl || DEFAULT_BASE_URL,
112
- apiKey: modelConfig.apiKey,
87
+ model_provider: providerKey,
88
+ model_providers: {
89
+ ...currentConfig.model_providers,
90
+ [providerKey]: {
91
+ name: 'X-AIO',
92
+ base_url: modelConfig.baseUrl || DEFAULT_BASE_URL,
93
+ env_key: X_AIO_CODE_API_KEY_ENV,
94
+ wire_api: 'responses',
95
+ },
113
96
  },
114
97
  };
115
98
  this.saveConfig(newConfig);
99
+ this.setApiKeyEnv(modelConfig.apiKey);
100
+ }
101
+ setApiKeyEnv(apiKey) {
102
+ const isWindows = process.platform === 'win32';
103
+ if (isWindows) {
104
+ const escapedKey = apiKey.replace(/'/g, '\'\'');
105
+ const psCommand = `[Environment]::SetEnvironmentVariable('${X_AIO_CODE_API_KEY_ENV}', '${escapedKey}', 'User')`;
106
+ try {
107
+ execSync(`powershell -Command "${psCommand}"`, { stdio: 'ignore' });
108
+ }
109
+ catch {
110
+ console.warn('Failed to set Windows environment variable');
111
+ }
112
+ }
113
+ else {
114
+ const shellRcPath = this.getShellRcPath();
115
+ if (shellRcPath) {
116
+ let content = existsSync(shellRcPath) ? readFileSync(shellRcPath, 'utf-8') : '';
117
+ const exportLine = `export ${X_AIO_CODE_API_KEY_ENV}="${apiKey}"`;
118
+ const regex = new RegExp(`^export ${X_AIO_CODE_API_KEY_ENV}=.*$`, 'm');
119
+ if (regex.test(content)) {
120
+ content = content.replace(regex, exportLine);
121
+ }
122
+ else {
123
+ content = content ? `${content}\n${exportLine}\n` : `${exportLine}\n`;
124
+ }
125
+ writeFileSync(shellRcPath, content, 'utf-8');
126
+ }
127
+ }
128
+ process.env[X_AIO_CODE_API_KEY_ENV] = apiKey;
129
+ }
130
+ getShellRcPath() {
131
+ const home = homedir();
132
+ const shell = process.env.SHELL || '';
133
+ if (shell.includes('zsh')) {
134
+ const zshrc = join(home, '.zshrc');
135
+ const zprofile = join(home, '.zprofile');
136
+ if (existsSync(zshrc))
137
+ return zshrc;
138
+ if (existsSync(zprofile))
139
+ return zprofile;
140
+ return zshrc;
141
+ }
142
+ if (shell.includes('bash')) {
143
+ const bashrc = join(home, '.bashrc');
144
+ const bashProfile = join(home, '.bash_profile');
145
+ if (existsSync(bashrc))
146
+ return bashrc;
147
+ if (existsSync(bashProfile))
148
+ return bashProfile;
149
+ return bashrc;
150
+ }
151
+ return join(home, '.profile');
152
+ }
153
+ getEnvValue(envKey) {
154
+ if (process.env[envKey]) {
155
+ return process.env[envKey];
156
+ }
157
+ if (process.platform === 'win32') {
158
+ try {
159
+ const result = execSync(`powershell -Command "[Environment]::GetEnvironmentVariable('${envKey}', 'User')"`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
160
+ if (result && result !== '') {
161
+ return result;
162
+ }
163
+ }
164
+ catch {
165
+ // Ignore errors
166
+ }
167
+ }
168
+ else {
169
+ try {
170
+ const shellRcPath = this.getShellRcPath();
171
+ if (existsSync(shellRcPath)) {
172
+ const content = readFileSync(shellRcPath, 'utf-8');
173
+ const regex = new RegExp(`^export ${envKey}=["']?([^"'\\n]+)["']?`, 'm');
174
+ const match = content.match(regex);
175
+ if (match && match[1]) {
176
+ return match[1];
177
+ }
178
+ }
179
+ }
180
+ catch {
181
+ // Ignore errors
182
+ }
183
+ }
184
+ return undefined;
116
185
  }
117
- /**
118
- * Get current model config
119
- */
120
186
  getModelConfig() {
121
187
  const config = this.getConfig();
122
- if (!config.provider?.apiKey) {
188
+ const providerKey = config.model_provider;
189
+ if (!providerKey || !config.model_providers?.[providerKey]) {
190
+ return null;
191
+ }
192
+ const provider = config.model_providers[providerKey];
193
+ const envKey = provider.env_key;
194
+ const apiKey = envKey ? this.getEnvValue(envKey) : undefined;
195
+ if (!apiKey) {
123
196
  return null;
124
197
  }
125
198
  return {
126
- apiKey: config.provider.apiKey,
199
+ apiKey,
127
200
  model: config.model || '',
128
- baseUrl: config.provider.baseURL || '',
201
+ baseUrl: provider.base_url || '',
129
202
  };
130
203
  }
131
- /**
132
- * Clear model config
133
- */
134
204
  clearModelConfig() {
135
205
  const currentConfig = this.getConfig();
136
- delete currentConfig.model;
137
- delete currentConfig.provider;
206
+ const providerKey = 'x-aio';
207
+ if (currentConfig.model_provider === providerKey) {
208
+ delete currentConfig.model;
209
+ delete currentConfig.model_provider;
210
+ }
211
+ if (currentConfig.model_providers?.[providerKey]) {
212
+ delete currentConfig.model_providers[providerKey];
213
+ if (Object.keys(currentConfig.model_providers).length === 0) {
214
+ delete currentConfig.model_providers;
215
+ }
216
+ }
138
217
  this.saveConfig(currentConfig);
218
+ delete process.env[X_AIO_CODE_API_KEY_ENV];
139
219
  }
140
220
  }
141
- // 单例导出
142
221
  export const codexTool = CodexTool.getInstance();
@@ -358,6 +358,9 @@ export class ToolMenu {
358
358
  }
359
359
  await new Promise(resolve => setTimeout(resolve, 800));
360
360
  spinner.succeed(chalk.green(i18n.t('wizard.config_loaded', { tool: toolRegistry.getTool(toolName)?.displayName })));
361
+ if (toolName === 'codex' && process.platform === 'win32') {
362
+ console.log(chalk.yellow(` ${i18n.t('wizard.restart_terminal_hint')}`));
363
+ }
361
364
  await new Promise(resolve => setTimeout(resolve, 2000));
362
365
  }
363
366
  catch (error) {
@@ -201,7 +201,8 @@
201
201
  "prompt_refresh_models": "Refresh model list now? (Update available models to config)",
202
202
  "refreshing_models": "Refreshing model list...",
203
203
  "models_refreshed": "Model list refreshed, {{count}} models synced",
204
- "refresh_models_failed": "Failed to refresh model list"
204
+ "refresh_models_failed": "Failed to refresh model list",
205
+ "restart_terminal_hint": "Environment variable set. Please restart your terminal before using Codex"
205
206
  },
206
207
  "doctor": {
207
208
  "checking": "Running health check...",
@@ -201,7 +201,8 @@
201
201
  "prompt_refresh_models": "是否立即刷新模型列表?(更新可用模型到配置)",
202
202
  "refreshing_models": "正在刷新模型列表...",
203
203
  "models_refreshed": "模型列表已刷新,共同步 {{count}} 个模型",
204
- "refresh_models_failed": "刷新模型列表失败"
204
+ "refresh_models_failed": "刷新模型列表失败",
205
+ "restart_terminal_hint": "环境变量已设置,请重新打开终端后再使用 Codex"
205
206
  },
206
207
  "doctor": {
207
208
  "checking": "正在进行健康检查...",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@x-all-in-one/coding-helper",
3
3
  "type": "module",
4
- "version": "0.2.0",
4
+ "version": "0.3.0",
5
5
  "description": "X All In One Coding Helper",
6
6
  "author": "X.AIO",
7
7
  "homepage": "https://docs.x-aio.com/zh/docs",
@@ -37,6 +37,7 @@
37
37
  "js-yaml": "^4.1.0",
38
38
  "open": "^11.0.0",
39
39
  "ora": "^8.0.1",
40
+ "smol-toml": "^1.6.0",
40
41
  "terminal-link": "^5.0.0"
41
42
  },
42
43
  "devDependencies": {