@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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
83
|
+
const providerKey = 'x-aio';
|
|
106
84
|
const newConfig = {
|
|
107
85
|
...currentConfig,
|
|
108
86
|
model: modelConfig.model,
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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
|
|
199
|
+
apiKey,
|
|
127
200
|
model: config.model || '',
|
|
128
|
-
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
|
-
|
|
137
|
-
|
|
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) {
|
package/dist/locales/en_US.json
CHANGED
|
@@ -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...",
|
package/dist/locales/zh_CN.json
CHANGED
|
@@ -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.
|
|
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": {
|