Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
205 changes: 184 additions & 21 deletions cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ const {
deleteCodexSkills
} = require('./cli/skills');
const { cmdImportSkills: cmdImportSkillsFromUrl } = require('./cli/import-skills-url');
const { cmdToolUpdate } = require('./cli/update');
const { cmdToolUpdate, fetchLatestVersion } = require('./cli/update');
const {
getFileStatSafe,
isBootstrapLikeText,
Expand Down Expand Up @@ -291,7 +291,11 @@ const DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS = Object.freeze({
host: '127.0.0.1',
port: 8328,
provider: '',
upstreamProviderName: '',
upstreamBaseUrl: '',
upstreamApiKey: '',
authSource: 'provider',
targetApi: 'responses',
timeoutMs: 30000
});
const CLI_INSTALL_TARGETS = Object.freeze([
Expand Down Expand Up @@ -5655,7 +5659,9 @@ const {
HTTPS_KEEP_ALIVE_AGENT,
readConfigOrVirtualDefault,
resolveBuiltinProxyProviderName,
resolveAuthTokenFromCurrentProfile
resolveAuthTokenFromCurrentProfile,
OPENAI_BRIDGE_SETTINGS_FILE,
resolveOpenaiBridgeUpstream
});

function applyBuiltinProxyProvider(params = {}) {
Expand Down Expand Up @@ -7997,15 +8003,17 @@ function buildClaudeSharePayload(config = {}) {
const apiKey = typeof config.apiKey === 'string' ? config.apiKey : '';
const baseUrl = typeof config.baseUrl === 'string' ? config.baseUrl : '';
const model = typeof config.model === 'string' ? config.model : '';
const targetApi = normalizeClaudeTargetApi(config.targetApi);

if (!baseUrl) return { error: 'Claude Base URL 未设置' };
if (!apiKey) return { error: 'Claude API 密钥未设置' };
if (!apiKey && targetApi !== 'ollama') return { error: 'Claude API 密钥未设置' };

return {
payload: {
baseUrl: baseUrl.trim(),
apiKey: apiKey.trim(),
model: (model && model.trim()) || DEFAULT_CLAUDE_MODEL
model: (model && model.trim()) || DEFAULT_CLAUDE_MODEL,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
targetApi
}
};
}
Expand Down Expand Up @@ -9319,19 +9327,93 @@ function maskKey(key) {
return key.substring(0, 4) + '...' + key.substring(key.length - 4);
}

function normalizeClaudeTargetApi(value) {
const raw = typeof value === 'string' ? value.trim().toLowerCase() : '';
if (raw === 'chat_completions' || raw === 'chat-completions' || raw === 'chat/completions') {
return 'chat_completions';
}
if (raw === 'ollama') {
return 'ollama';
}
return 'responses';
}

function resetBuiltinClaudeProxySavedSettingsToResponses() {
const proxySettingsResult = readJsonObjectFromFile(BUILTIN_CLAUDE_PROXY_SETTINGS_FILE, DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS);
const proxySettings = proxySettingsResult.ok && proxySettingsResult.data && typeof proxySettingsResult.data === 'object' && !Array.isArray(proxySettingsResult.data)
? proxySettingsResult.data
: DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS;
writeJsonAtomic(BUILTIN_CLAUDE_PROXY_SETTINGS_FILE, {
...DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS,
...proxySettings,
enabled: false,
targetApi: 'responses'
});
}

// 应用到 Claude Code settings.json(跨平台)
function applyToClaudeSettings(config = {}) {
assertToolConfigWriteAllowed('claude');
async function applyToClaudeSettings(config = {}) {
let proxyStarted = false;
try {
assertToolConfigWriteAllowed('claude');
const apiKey = (config.apiKey || '').trim();
if (!apiKey) {
const targetApi = normalizeClaudeTargetApi(config.targetApi);
if (!apiKey && targetApi !== 'ollama') {
return { success: false, mode: 'settings-file', error: '请先输入 API Key' };
}

const baseUrl = (config.baseUrl || 'https://open.bigmodel.cn/api/anthropic').trim();
const configuredBaseUrl = typeof config.baseUrl === 'string' ? config.baseUrl.trim() : '';
const baseUrl = (configuredBaseUrl || (targetApi === 'ollama' ? 'http://127.0.0.1:11434' : 'https://open.bigmodel.cn/api/anthropic')).trim();
const model = (config.model || DEFAULT_CLAUDE_MODEL).trim();
let settingsBaseUrl = baseUrl;
let settingsApiKey = apiKey;
let proxyResult = null;

if (targetApi === 'chat_completions' || targetApi === 'ollama') {
const upstreamProviderName = typeof config.name === 'string' ? config.name.trim() : '';
if (targetApi === 'chat_completions' && !configuredBaseUrl && !upstreamProviderName) {
return {
success: false,
mode: 'claude-proxy',
error: 'chat_completions 模式需要显式的上游 Base URL 或可解析的 provider 名称'
};
}
await stopBuiltinClaudeProxyRuntime();
const proxyToken = crypto.randomBytes(24).toString('hex');
proxyResult = await startBuiltinClaudeProxyRuntime({
enabled: true,
host: DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS.host,
provider: upstreamProviderName,
authSource: 'provider',
targetApi,
timeoutMs: DEFAULT_BUILTIN_CLAUDE_PROXY_SETTINGS.timeoutMs,
upstreamProviderName,
...(configuredBaseUrl ? { upstreamBaseUrl: configuredBaseUrl } : {}),
upstreamApiKey: apiKey
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if (!proxyResult || proxyResult.error || proxyResult.success === false || !proxyResult.listenUrl) {
await stopBuiltinClaudeProxyRuntime();
resetBuiltinClaudeProxySavedSettingsToResponses();
return {
success: false,
mode: 'claude-proxy',
error: (proxyResult && proxyResult.error) || '启动 Claude 兼容代理失败'
};
}
proxyStarted = true;
settingsBaseUrl = proxyResult.listenUrl;
settingsApiKey = proxyToken;
} else {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
await stopBuiltinClaudeProxyRuntime();
resetBuiltinClaudeProxySavedSettingsToResponses();
}

const readResult = readJsonObjectFromFile(CLAUDE_SETTINGS_FILE, {});
if (!readResult.ok) {
if (proxyStarted) {
await stopBuiltinClaudeProxyRuntime();
resetBuiltinClaudeProxySavedSettingsToResponses();
}
return { success: false, mode: 'settings-file', error: readResult.error };
Comment on lines +9372 to 9417
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Preserve the current Claude runtime until settings I/O succeeds.

This path tears down the active builtin Claude proxy before settings.json is even read. If readJsonObjectFromFile(...), backupFileIfNeededOnce(...), or writeJsonAtomic(...) fails, the rollback only stops the new proxy and resets saved mode; it never restores the previously running proxy or the old Claude settings. That can leave Claude still pointing at a proxy URL that has already been stopped, so a failed apply breaks an otherwise working setup.

Also applies to: 9462-9465

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli.js` around lines 9367 - 9412, You tear down the existing builtin Claude
proxy (stopBuiltinClaudeProxyRuntime /
resetBuiltinClaudeProxySavedSettingsToResponses) before performing settings I/O
(readJsonObjectFromFile, backupFileIfNeededOnce, writeJsonAtomic), so failures
leave the old runtime un-restored; instead capture the current runtime/settings
state before touching anything (e.g. save whether a runtime was running and its
baseUrl/apiKey), delay calling stopBuiltinClaudeProxyRuntime until after
settings I/O succeeds, or if you must stop it first then on any subsequent
failure restart the previous runtime by calling startBuiltinClaudeProxyRuntime
with the saved parameters and restore saved settings via
resetBuiltinClaudeProxySavedSettingsToResponses; update the branches around
startBuiltinClaudeProxyRuntime, proxyStarted, settingsBaseUrl and settingsApiKey
and the error-return paths (including the later block noted at 9462-9465) so
failures perform restoration and do not leave Claude pointing at a stopped
proxy.

}

Expand All @@ -9342,8 +9424,8 @@ function applyToClaudeSettings(config = {}) {

const nextEnv = {
...currentEnv,
ANTHROPIC_API_KEY: apiKey,
ANTHROPIC_BASE_URL: baseUrl,
ANTHROPIC_API_KEY: settingsApiKey,
ANTHROPIC_BASE_URL: settingsBaseUrl,
ANTHROPIC_MODEL: model
};
delete nextEnv.ANTHROPIC_AUTH_TOKEN;
Expand All @@ -9360,19 +9442,32 @@ function applyToClaudeSettings(config = {}) {

const result = {
success: true,
mode: 'settings-file',
mode: targetApi === 'responses' ? 'settings-file' : 'claude-proxy',
targetApi,
targetPath: CLAUDE_SETTINGS_FILE,
updatedKeys: [
'env.ANTHROPIC_API_KEY',
'env.ANTHROPIC_BASE_URL',
'env.ANTHROPIC_MODEL'
]
};
if (proxyResult) {
result.proxy = {
running: true,
listenUrl: proxyResult.listenUrl,
upstreamProvider: proxyResult.upstreamProvider || '',
mode: proxyResult.mode || (targetApi === 'ollama' ? 'anthropic-to-ollama' : 'anthropic-to-chat-completions')
};
}
if (backupPath) {
result.backupPath = backupPath;
}
return result;
} catch (e) {
if (proxyStarted) {
try { await stopBuiltinClaudeProxyRuntime(); } catch (_) {}
try { resetBuiltinClaudeProxySavedSettingsToResponses(); } catch (_) {}
}
return {
success: false,
mode: 'settings-file',
Expand Down Expand Up @@ -9485,14 +9580,48 @@ async function restoreCodexDir(payload) {
}

// CLI: 一行写入 Claude Code 配置
function parseClaudeCommandArgs(argv = []) {
const positionals = [];
let targetApi = 'responses';
for (let i = 0; i < argv.length; i += 1) {
const token = String(argv[i] ?? '');
if (token === '--target-api' || token === '--targetApi') {
const nextValue = String(argv[i + 1] ?? '');
if (!nextValue || nextValue.startsWith('--')) {
throw new Error('错误: --target-api 需要一个值(responses、chat_completions 或 ollama)');
}
targetApi = normalizeClaudeTargetApi(nextValue);
i += 1;
Comment on lines +9588 to +9594
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject unknown --target-api values instead of silently falling back to responses.

Line 9590 maps any unrecognized value to responses. Here that is not a safe default: a typo skips the proxy-backed path and Line 9640-Line 9645 will apply direct Claude settings instead of the local proxy/token flow.

Proposed fix
         if (token === '--target-api' || token === '--targetApi') {
             const nextValue = String(argv[i + 1] ?? '');
             if (!nextValue || nextValue.startsWith('--')) {
                 throw new Error('错误: --target-api 需要一个值(responses、chat_completions 或 ollama)');
             }
-            targetApi = normalizeClaudeTargetApi(nextValue);
+            const rawTargetApi = nextValue.trim().toLowerCase();
+            const allowedTargetApis = new Set([
+                'responses',
+                'chat_completions',
+                'chat-completions',
+                'chat/completions',
+                'ollama'
+            ]);
+            if (!allowedTargetApis.has(rawTargetApi)) {
+                throw new Error(`错误: 不支持的 --target-api 值: ${nextValue}`);
+            }
+            targetApi = normalizeClaudeTargetApi(rawTargetApi);
             i += 1;
             continue;
         }

Also applies to: 9640-9645

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli.js` around lines 9585 - 9591, The CLI currently accepts
--target-api/--targetApi and uses normalizeClaudeTargetApi(nextValue) but
silently maps unknown values to "responses"; change this to validate the
normalized value and throw an informative Error when normalizeClaudeTargetApi
returns an unrecognized value so typos don't fall back to responses and bypass
proxy/token flow; update the same validation where targetApi is later handled
(the branch that applies direct Claude settings) to reject unknown targetApi
values as well, referencing the normalizeClaudeTargetApi function and the
targetApi variable to locate the changes.

continue;
}
positionals.push(token);
}

const baseUrl = positionals[0];
if (targetApi === 'ollama' && positionals.length === 2) {
return {
baseUrl,
apiKey: '',
model: positionals[1],
targetApi
};
}
return {
baseUrl,
apiKey: positionals[1],
model: positionals[2],
targetApi
};
}

async function cmdClaude(args = []) {
const argv = Array.isArray(args) ? args : [];
// 无参数 → 代理启动
if (argv.length === 0 || (argv.length === 1 && argv[0] === undefined)) {
return runProxyCommand('Claude', 'claude', [], '', { autoFlag: '--dangerously-skip-permissions' });
}
// 有参数 → 配置写入
const [baseUrl, apiKey, model] = argv;
const { baseUrl, apiKey, model, targetApi } = parseClaudeCommandArgs(argv);
const normalizedBaseUrl = typeof baseUrl === 'string' ? baseUrl.trim() : '';
const normalizedKey = typeof apiKey === 'string' ? apiKey.trim() : '';
const normalizedModel = typeof model === 'string' && model.trim()
Expand All @@ -9501,19 +9630,21 @@ async function cmdClaude(args = []) {

const silent = false;

if (!normalizedBaseUrl || !normalizedKey) {
if (!normalizedBaseUrl || (!normalizedKey && targetApi !== 'ollama')) {
if (!silent) {
console.error('用法: codexmate claude <BaseURL> <API密钥> [模型]');
console.error('用法: codexmate claude <BaseURL> <API密钥> [模型] [--target-api responses|chat_completions|ollama]');
console.log('\n示例:');
console.log(' codexmate claude https://open.bigmodel.cn/api/anthropic sk-ant-xxx glm-4.7');
console.log(" codexmate claude http://127.0.0.1:11434 '' llama3.1:8b --target-api ollama");
}
throw new Error('BaseURL 和 API 密钥必填');
throw new Error(targetApi === 'ollama' ? 'BaseURL 必填' : 'BaseURL 和 API 密钥必填');
}

const result = applyToClaudeSettings({
const result = await applyToClaudeSettings({
baseUrl: normalizedBaseUrl,
apiKey: normalizedKey,
model: normalizedModel
model: normalizedModel,
targetApi
});

if (!result || result.success === false) {
Expand Down Expand Up @@ -10983,6 +11114,27 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
case 'install-status':
result = buildInstallStatusReport();
break;
case 'version-status': {
const currentVersion = (() => {
try {
const pkg = require('./package.json');
return pkg && pkg.version ? pkg.version : '';
} catch (_) {
return '';
}
})();
try {
const latestVersion = await fetchLatestVersion({ timeoutMs: 2000 });
result = { currentVersion, latestVersion };
} catch (e) {
result = {
currentVersion,
latestVersion: '',
error: e && e.message ? e.message : '获取最新版本失败'
};
}
break;
}
case 'list':
result = buildMcpProviderListPayload();
break;
Expand Down Expand Up @@ -11175,7 +11327,7 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
result = applyClaudeSettingsRaw(params || {});
break;
case 'apply-claude-config':
result = applyToClaudeSettings(params.config);
result = await applyToClaudeSettings(params.config);
if (result && !result.error) {
const cfgName = (params && params.config && typeof params.config.name === 'string') ? params.config.name : '';
const cfgFrom = (params && typeof params.previousName === 'string') ? params.previousName : '';
Expand Down Expand Up @@ -15740,9 +15892,20 @@ function createMcpTools(options = {}) {
properties: {
apiKey: { type: 'string' },
baseUrl: { type: 'string' },
model: { type: 'string' }
model: { type: 'string' },
name: { type: 'string' },
targetApi: { type: 'string' }
},
required: ['apiKey'],
allOf: [{
if: {
not: {
type: 'object',
properties: { targetApi: { type: 'string', pattern: '^[\\s]*[oO][lL][lL][aA][mM][aA][\\s]*$' } },
required: ['targetApi']
}
},
then: { required: ['apiKey'] }
}],
Comment thread
coderabbitai[bot] marked this conversation as resolved.
additionalProperties: false
},
handler: async (args = {}) => applyToClaudeSettings(args || {})
Expand Down Expand Up @@ -16198,7 +16361,7 @@ function printMainHelp() {
console.log(' codexmate add <名称> <URL> [密钥] [--bridge <openai>]');
console.log(' codexmate delete <名称> 删除提供商');
console.log(' codexmate claude 等同于 claude --dangerously-skip-permissions');
console.log(' codexmate claude <BaseURL> <API密钥> [模型] 写入 Claude Code 配置');
console.log(' codexmate claude <BaseURL> <API密钥> [模型] [--target-api responses|chat_completions|ollama] 写入 Claude Code 配置');
Comment thread
coderabbitai[bot] marked this conversation as resolved.
console.log(' codexmate auth <list|import|switch|delete|status> 认证管理');
console.log(' codexmate add-model <模型> 添加模型');
console.log(' codexmate delete-model <模型> 删除模型');
Expand Down
Loading
Loading