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 @@ -5740,7 +5744,9 @@ const {
HTTPS_KEEP_ALIVE_AGENT,
readConfigOrVirtualDefault,
resolveBuiltinProxyProviderName,
resolveAuthTokenFromCurrentProfile
resolveAuthTokenFromCurrentProfile,
OPENAI_BRIDGE_SETTINGS_FILE,
resolveOpenaiBridgeUpstream
});

function applyBuiltinProxyProvider(params = {}) {
Expand Down Expand Up @@ -8082,15 +8088,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 @@ -9404,19 +9412,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 thread
coderabbitai[bot] marked this conversation as resolved.
}

Expand All @@ -9427,8 +9509,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 @@ -9445,19 +9527,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 @@ -9570,14 +9665,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 +9673 to +9679
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 @@ -9586,19 +9715,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 @@ -11105,6 +11236,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 @@ -11297,7 +11449,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 @@ -15894,9 +16046,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 @@ -16352,7 +16515,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