@kkmila/cpc 1.0.1 → 1.0.5

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.
@@ -4,7 +4,10 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
4
4
  import { resolve, join } from "node:path";
5
5
  import { homedir } from "node:os";
6
6
  import yaml from "js-yaml";
7
- import { loadRegistry, getAgents, resolveAgentPaths } from "../lib/agents.mjs";
7
+ import { loadRegistry, getAgents, resolveAgentPaths, detectCurrentAgent } from "../lib/agents.mjs";
8
+ import { enrichModelWithBaseline } from "../lib/baseline-models.mjs";
9
+ import { persistEnvVar, envKeyNameForProvider } from "../lib/env.mjs";
10
+ import { writeCodexProviderConfig, readCodexProviderInfo } from "../lib/codex-toml.mjs";
8
11
 
9
12
  /**
10
13
  * Get the config directory path (~/.config/cpc)
@@ -120,10 +123,10 @@ const MODEL_DEFAULTS = {
120
123
  "gpt-4o": { contextWindow: 128000, maxTokens: 16384, reasoning: false },
121
124
  "gpt-4o-mini": { contextWindow: 128000, maxTokens: 16384, reasoning: false },
122
125
  "gpt-3.5-turbo": { contextWindow: 16385, maxTokens: 4096, reasoning: false },
123
- o1: { contextWindow: 200000, maxTokens: 100000, reasoning: true },
124
- "o1-mini": { contextWindow: 128000, maxTokens: 65536, reasoning: true },
125
- "o1-preview": { contextWindow: 128000, maxTokens: 32768, reasoning: true },
126
- "o3-mini": { contextWindow: 200000, maxTokens: 100000, reasoning: true },
126
+ o1: { contextWindow: 200000, maxTokens: 100000, reasoning: false },
127
+ "o1-mini": { contextWindow: 128000, maxTokens: 65536, reasoning: false },
128
+ "o1-preview": { contextWindow: 128000, maxTokens: 32768, reasoning: false },
129
+ "o3-mini": { contextWindow: 200000, maxTokens: 100000, reasoning: false },
127
130
 
128
131
  // Anthropic
129
132
  "claude-3-opus-20240229": {
@@ -158,7 +161,7 @@ const MODEL_DEFAULTS = {
158
161
  "deepseek-reasoner": {
159
162
  contextWindow: 65536,
160
163
  maxTokens: 8192,
161
- reasoning: true,
164
+ reasoning: false,
162
165
  },
163
166
 
164
167
  // Qwen
@@ -166,6 +169,10 @@ const MODEL_DEFAULTS = {
166
169
  "qwen-plus": { contextWindow: 32768, maxTokens: 8192, reasoning: false },
167
170
  "qwen-max": { contextWindow: 32768, maxTokens: 8192, reasoning: false },
168
171
 
172
+ // 智谱 GLM-5.2
173
+ "glm-5.2": { contextWindow: 1000000, maxTokens: 131072, reasoning: false },
174
+ "glm-5.2-anthropic": { contextWindow: 1000000, maxTokens: 131072, reasoning: false },
175
+
169
176
  // 通用默认值
170
177
  default: { contextWindow: 32768, maxTokens: 4096, reasoning: false },
171
178
  };
@@ -187,16 +194,9 @@ function getModelDefaults(modelId) {
187
194
  }
188
195
  }
189
196
 
190
- // 检测是否是推理模型
191
- const isReasoning =
192
- lowerId.includes("reason") ||
193
- lowerId.includes("o1") ||
194
- lowerId.includes("o3") ||
195
- lowerId.includes("think");
196
-
197
197
  return {
198
198
  ...MODEL_DEFAULTS["default"],
199
- reasoning: isReasoning,
199
+ reasoning: false,
200
200
  };
201
201
  }
202
202
 
@@ -322,24 +322,26 @@ function getAgentProviderConfig(agentId, provider) {
322
322
  },
323
323
  };
324
324
 
325
- case "codex":
325
+ case "codex": {
326
+ // codex 一次只能用一个模型(顶层 model + model_provider)。
327
+ // provider 段用 env_key 引用环境变量,key 本身由 cpc 跨平台落地。
328
+ const envKey = envKeyNameForProvider(provider.name);
326
329
  return {
327
- configToml: {
328
- model_provider: provider.name,
330
+ codex: {
331
+ providerName: provider.name,
332
+ baseUrl: provider.baseUrl,
333
+ wireApi:
334
+ provider.api === "anthropic-messages"
335
+ ? "chat_completions"
336
+ : provider.api === "openai-completions"
337
+ ? "chat_completions"
338
+ : "responses",
339
+ envKey,
329
340
  model: provider.models[0]?.id || "default",
330
- model_providers: {
331
- [provider.name]: {
332
- name: provider.name,
333
- base_url: provider.baseUrl,
334
- wire_api:
335
- provider.api === "anthropic-messages"
336
- ? "chat_completions"
337
- : "responses",
338
- requires_openai_auth: false,
339
- },
340
- },
341
+ apiKey: provider.apiKey,
341
342
  },
342
343
  };
344
+ }
343
345
 
344
346
  case "openclaw":
345
347
  return {
@@ -387,6 +389,27 @@ function getAgentProviderConfig(agentId, provider) {
387
389
  },
388
390
  };
389
391
 
392
+ case "opencode":
393
+ return {
394
+ opencodeJson: {
395
+ provider: {
396
+ [provider.name]: {
397
+ models: provider.models.reduce((acc, m) => {
398
+ acc[m.id] = {
399
+ name: m.name || m.id,
400
+ };
401
+ return acc;
402
+ }, {}),
403
+ npm: "@ai-sdk/openai-compatible",
404
+ options: {
405
+ apiKey: provider.apiKey,
406
+ baseURL: provider.baseUrl,
407
+ },
408
+ },
409
+ },
410
+ },
411
+ };
412
+
390
413
  default:
391
414
  return null;
392
415
  }
@@ -395,7 +418,7 @@ function getAgentProviderConfig(agentId, provider) {
395
418
  /**
396
419
  * Install provider configuration to agent
397
420
  */
398
- function installProviderToAgent(agentId, provider, agentHome) {
421
+ function installProviderToAgent(agentId, provider, agentHome, opts = {}) {
399
422
  const config = getAgentProviderConfig(agentId, provider);
400
423
  if (!config) {
401
424
  console.log(chalk.yellow(` ⚠ 未知 agent: ${agentId}`));
@@ -420,7 +443,13 @@ function installProviderToAgent(agentId, provider, agentHome) {
420
443
  const modelsPath = join(agentHome, "models.json");
421
444
  let models = { providers: {} };
422
445
  if (existsSync(modelsPath)) {
423
- models = JSON.parse(readFileSync(modelsPath, "utf8"));
446
+ try {
447
+ const content = readFileSync(modelsPath, "utf8").trim();
448
+ if (content) models = JSON.parse(content);
449
+ } catch {
450
+ // 空文件或损坏的 JSON,重置为默认值
451
+ models = { providers: {} };
452
+ }
424
453
  }
425
454
  models.providers = {
426
455
  ...models.providers,
@@ -445,38 +474,55 @@ function installProviderToAgent(agentId, provider, agentHome) {
445
474
 
446
475
  case "codex": {
447
476
  const configPath = join(agentHome, "config.toml");
448
- let configContent = "";
449
- if (existsSync(configPath)) {
450
- configContent = readFileSync(configPath, "utf8");
451
- }
452
-
453
- const providerSection = `\n[model_providers.${provider.name}]\nname = "${provider.name}"\nbase_url = "${provider.baseUrl}"\nwire_api = "${provider.api === "anthropic-messages" ? "chat_completions" : "responses"}"\nrequires_openai_auth = false\n`;
454
-
455
- const modelLine = `model_provider = "${provider.name}"\n`;
456
- const modelIdLine = `model = "${provider.models[0]?.id || "default"}"\n`;
477
+ const c = config.codex;
457
478
 
458
- if (configContent.includes("model_provider =")) {
459
- configContent = configContent.replace(
460
- /model_provider = .*\n/,
461
- modelLine,
479
+ // 1) 跨平台落地 API key 到环境变量
480
+ let envResult;
481
+ try {
482
+ envResult = persistEnvVar(c.envKey, c.apiKey);
483
+ } catch (err) {
484
+ console.error(
485
+ chalk.red(` ✗ 落地环境变量 ${c.envKey} 失败: ${err.message}`),
462
486
  );
463
- } else {
464
- configContent = modelLine + configContent;
465
- }
466
-
467
- if (configContent.includes("model =")) {
468
- configContent = configContent.replace(/model = .*\n/, modelIdLine);
469
- } else {
470
- configContent = modelIdLine + configContent;
487
+ return false;
471
488
  }
472
489
 
473
- if (!configContent.includes(`[model_providers.${provider.name}]`)) {
474
- configContent += providerSection;
490
+ // 2) 稳健写入 config.toml(顶层 + provider 段,备份原文件)
491
+ try {
492
+ const { backed } = writeCodexProviderConfig(configPath, {
493
+ providerName: c.providerName,
494
+ baseUrl: c.baseUrl,
495
+ wireApi: opts.wireApi || c.wireApi,
496
+ envKey: c.envKey,
497
+ model: c.model,
498
+ modelReasoningEffort: opts.modelReasoningEffort,
499
+ });
500
+ console.log(chalk.green(` ✓ 更新 Codex config.toml`));
501
+ if (backed) {
502
+ console.log(chalk.dim(` • 备份: ${backed}`));
503
+ }
504
+ console.log(
505
+ chalk.dim(
506
+ ` • env_key=$${c.envKey} (${envResult.method}${envResult.target ? ": " + envResult.target : ""}${envResult.created ? ", 新增" : ", 已存在"})`,
507
+ ),
508
+ );
509
+ if (process.platform !== "win32") {
510
+ console.log(
511
+ chalk.dim(
512
+ ` • 新终端自动加载; 当前终端需执行: source ${envResult.target}`,
513
+ ),
514
+ );
515
+ }
516
+ console.log(
517
+ chalk.dim(` • 启动: codex (全局默认即 ${c.providerName}/${c.model})`),
518
+ );
519
+ return true;
520
+ } catch (err) {
521
+ console.error(
522
+ chalk.red(` ✗ 更新 ${agentId} 失败: ${err.message}`),
523
+ );
524
+ return false;
475
525
  }
476
-
477
- writeFileSync(configPath, configContent);
478
- console.log(chalk.green(` ✓ 更新 Codex config.toml`));
479
- return true;
480
526
  }
481
527
 
482
528
  case "openclaw": {
@@ -511,6 +557,22 @@ function installProviderToAgent(agentId, provider, agentHome) {
511
557
  return true;
512
558
  }
513
559
 
560
+ case "opencode": {
561
+ const configPath = join(agentHome, "opencode.json");
562
+ let configData = {};
563
+ if (existsSync(configPath)) {
564
+ configData = JSON.parse(readFileSync(configPath, "utf8"));
565
+ }
566
+ if (!configData.provider) configData.provider = {};
567
+ configData.provider = {
568
+ ...configData.provider,
569
+ ...config.opencodeJson.provider,
570
+ };
571
+ writeFileSync(configPath, JSON.stringify(configData, null, 2));
572
+ console.log(chalk.green(` ✓ 更新 OpenCode opencode.json`));
573
+ return true;
574
+ }
575
+
514
576
  default:
515
577
  console.log(chalk.yellow(` ⚠ 未知 agent: ${agentId}`));
516
578
  return false;
@@ -521,6 +583,15 @@ function installProviderToAgent(agentId, provider, agentHome) {
521
583
  }
522
584
  }
523
585
 
586
+ /**
587
+ * Check if an agent is installed by looking at its home directory
588
+ */
589
+ function isAgentInstalled(agentId, agents) {
590
+ const agent = resolveAgentPaths(agents[agentId]);
591
+ if (!agent || !agent.home) return false;
592
+ return existsSync(agent.home);
593
+ }
594
+
524
595
  export function registerProvider(program, repoRoot) {
525
596
  // ── cpc add ──────────────────────────────────────────────────
526
597
  program
@@ -647,14 +718,23 @@ export function registerProvider(program, repoRoot) {
647
718
  for (const modelId of selected) {
648
719
  const model = result.models.find((m) => m.id === modelId);
649
720
  const defaults = getModelDefaults(modelId);
650
- const configured = {
721
+ // 先用 baseline 字典补全 contextWindow / input / description,
722
+ // 未命中时回退到 API 响应与 MODEL_DEFAULTS
723
+ const configured = enrichModelWithBaseline({
651
724
  id: modelId,
652
725
  name: model?.name || modelId,
653
726
  contextWindow: model?.contextWindow || defaults.contextWindow,
654
727
  maxTokens: model?.maxTokens || defaults.maxTokens,
655
728
  reasoning: model?.reasoning || defaults.reasoning,
656
729
  input: model?.input || ["text"],
657
- };
730
+ });
731
+ if (configured.description) {
732
+ console.log(
733
+ chalk.dim(
734
+ ` ↪ ${modelId}: 命中 baseline, 已注入 contextWindow=${configured.contextWindow}, input=[${configured.input.join(",")}]`,
735
+ ),
736
+ );
737
+ }
658
738
  models.push(configured);
659
739
  }
660
740
  } else {
@@ -976,14 +1056,23 @@ export function registerProvider(program, repoRoot) {
976
1056
  for (const modelId of selected) {
977
1057
  const model = result.models.find((m) => m.id === modelId);
978
1058
  const defaults = getModelDefaults(modelId);
979
- const configured = {
1059
+ // 先用 baseline 字典补全 contextWindow / input / description,
1060
+ // 未命中时回退到 API 响应与 MODEL_DEFAULTS
1061
+ const configured = enrichModelWithBaseline({
980
1062
  id: modelId,
981
1063
  name: model?.name || modelId,
982
1064
  contextWindow: model?.contextWindow || defaults.contextWindow,
983
1065
  maxTokens: model?.maxTokens || defaults.maxTokens,
984
1066
  reasoning: model?.reasoning || defaults.reasoning,
985
1067
  input: model?.input || ["text"],
986
- };
1068
+ });
1069
+ if (configured.description) {
1070
+ console.log(
1071
+ chalk.dim(
1072
+ ` ↪ ${modelId}: 命中 baseline, 已注入 contextWindow=${configured.contextWindow}, input=[${configured.input.join(",")}]`,
1073
+ ),
1074
+ );
1075
+ }
987
1076
 
988
1077
  if (opts.merge) {
989
1078
  config.models = config.models || [];
@@ -1005,6 +1094,192 @@ export function registerProvider(program, repoRoot) {
1005
1094
  );
1006
1095
  });
1007
1096
 
1097
+ // ── cpc update ───────────────────────────────────────────────
1098
+ program
1099
+ .command("update [providerName]")
1100
+ .description("重新从 API 拉取模型并更新 providers.json + 已安装的 agent")
1101
+ .option("--all", "选择全部模型")
1102
+ .option("--agent <agent>", "指定目标 agent")
1103
+ .action(async (providerName, opts) => {
1104
+ const providersData = loadProviders();
1105
+ const providers = Object.entries(providersData.providers);
1106
+
1107
+ if (providers.length === 0) {
1108
+ console.log(chalk.yellow("No providers configured."));
1109
+ console.log(chalk.dim("Use `cpc add` to add a new provider first."));
1110
+ return;
1111
+ }
1112
+
1113
+ // Step 1: 选择 provider
1114
+ if (!providerName) {
1115
+ providerName = await select({
1116
+ message: "Select provider to update:",
1117
+ choices: providers.map(([name, config]) => ({
1118
+ name: `${name.padEnd(20)} (${config.models?.length || 0} models)`,
1119
+ value: name,
1120
+ })),
1121
+ });
1122
+ }
1123
+
1124
+ const provider = providersData.providers[providerName];
1125
+ if (!provider) {
1126
+ console.error(chalk.red(`Provider "${providerName}" not found.`));
1127
+ process.exit(1);
1128
+ }
1129
+
1130
+ // Step 2: 从 API 拉取模型
1131
+ console.log(chalk.dim(`\nFetching models from ${provider.baseUrl}...`));
1132
+ const result = await fetchModelsFromAPI(provider.baseUrl, provider.apiKey, provider.api);
1133
+
1134
+ if (!result.success) {
1135
+ console.log(chalk.red(`✗ Failed to fetch models: ${result.error}`));
1136
+ return;
1137
+ }
1138
+
1139
+ console.log(chalk.green(`✓ Found ${result.models.length} models from API`));
1140
+
1141
+ // Step 3: 选择模型
1142
+ const oldIds = (provider.models || []).map((m) => m.id);
1143
+ const newIds = result.models.map((m) => m.id);
1144
+
1145
+ // 报告新增和移除的模型
1146
+ const addedIds = newIds.filter((id) => !oldIds.includes(id));
1147
+ const removedIds = oldIds.filter((id) => !newIds.includes(id));
1148
+ const keptIds = oldIds.filter((id) => newIds.includes(id));
1149
+
1150
+ if (addedIds.length > 0) {
1151
+ console.log(chalk.cyan(` + ${addedIds.length} new model(s): ${addedIds.join(", ")}`));
1152
+ }
1153
+ if (removedIds.length > 0) {
1154
+ console.log(chalk.yellow(` - ${removedIds.length} removed model(s): ${removedIds.join(", ")}`));
1155
+ }
1156
+ if (keptIds.length > 0) {
1157
+ console.log(chalk.dim(` = ${keptIds.length} kept model(s): ${keptIds.join(", ")}`));
1158
+ }
1159
+
1160
+ // 让用户选择要安装的模型
1161
+ let selectedIds;
1162
+ if (opts.all) {
1163
+ selectedIds = result.models.map((m) => m.id);
1164
+ console.log(chalk.dim(`Auto-selected all ${selectedIds.length} models`));
1165
+ } else {
1166
+ selectedIds = await checkbox({
1167
+ message: "Select models to install:",
1168
+ choices: result.models.map((m) => {
1169
+ const isNew = !oldIds.includes(m.id);
1170
+ return {
1171
+ name: `${isNew ? chalk.cyan("+ ") : chalk.dim(" ")}${m.id.padEnd(40)} ctx:${m.contextWindow || "?"}`,
1172
+ value: m.id,
1173
+ checked: true,
1174
+ };
1175
+ }),
1176
+ instructions: {
1177
+ navigator: "↑↓ navigate",
1178
+ select: "space select",
1179
+ all: "a toggle all",
1180
+ },
1181
+ });
1182
+ }
1183
+
1184
+ if (selectedIds.length === 0) {
1185
+ console.log(chalk.yellow("No models selected. Cancelled."));
1186
+ return;
1187
+ }
1188
+
1189
+ // Step 4: 用 baseline + MODEL_DEFAULTS 补全模型属性
1190
+ const updatedModels = [];
1191
+ for (const modelId of selectedIds) {
1192
+ const apiModel = result.models.find((m) => m.id === modelId);
1193
+ const defaults = getModelDefaults(modelId);
1194
+ const configured = enrichModelWithBaseline({
1195
+ id: modelId,
1196
+ name: apiModel?.name || modelId,
1197
+ contextWindow: apiModel?.contextWindow || defaults.contextWindow,
1198
+ maxTokens: apiModel?.maxTokens || defaults.maxTokens,
1199
+ reasoning: false,
1200
+ input: apiModel?.input || defaults.input || ["text"],
1201
+ });
1202
+ if (configured.description) {
1203
+ console.log(chalk.dim(` ↪ ${modelId}: baseline 命中, contextWindow=${configured.contextWindow}, input=[${configured.input.join(",")}]`));
1204
+ }
1205
+ updatedModels.push(configured);
1206
+ }
1207
+
1208
+ // Step 5: 更新 providers.json
1209
+ provider.models = updatedModels;
1210
+ provider.updatedAt = new Date().toISOString();
1211
+ saveProviders(providersData);
1212
+ console.log(chalk.green(`\n✓ Provider "${providerName}" updated: ${updatedModels.length} model(s)`));
1213
+
1214
+ // Step 6: 更新已安装的 agent
1215
+ const registry = loadRegistry(repoRoot);
1216
+ const agents = getAgents(registry);
1217
+ const resolvedAgents = Object.fromEntries(
1218
+ Object.entries(agents).map(([id, a]) => [id, resolveAgentPaths(a)]),
1219
+ );
1220
+ const installProvider = { ...provider, models: updatedModels };
1221
+
1222
+ // 找出哪些 agent 之前安装过这个 provider
1223
+ let agentIds;
1224
+ if (opts.agent) {
1225
+ agentIds = opts.agent === "all" ? Object.keys(agents) : [opts.agent];
1226
+ } else {
1227
+ // 只显示 pi 和 opencode,默认不勾选
1228
+ const visibleAgentIds = Object.keys(resolvedAgents)
1229
+ .filter((id) => id === "pi" || id === "opencode");
1230
+
1231
+ if (visibleAgentIds.length === 0) {
1232
+ console.log(chalk.dim("\nNo pi or opencode agents found. Skipping agent update."));
1233
+ return;
1234
+ }
1235
+
1236
+ agentIds = await checkbox({
1237
+ message: "Select target agents to update:",
1238
+ choices: visibleAgentIds.map((id) => {
1239
+ const a = resolvedAgents[id];
1240
+ const installed = isAgentInstalled(id, resolvedAgents);
1241
+ const mark = installed ? " (installed)" : "";
1242
+ return {
1243
+ name: `${id.padEnd(14)} ${a.description || ""}${mark}`,
1244
+ value: id,
1245
+ checked: false,
1246
+ };
1247
+ }),
1248
+ instructions: {
1249
+ navigator: "↑↓ navigate",
1250
+ select: "space select",
1251
+ all: "a toggle all",
1252
+ },
1253
+ });
1254
+ }
1255
+
1256
+ if (agentIds.length === 0) {
1257
+ console.log(chalk.yellow("No agents selected. Skipping agent update."));
1258
+ return;
1259
+ }
1260
+
1261
+ console.log();
1262
+ let successCount = 0;
1263
+ for (const aid of agentIds) {
1264
+ const agent = resolvedAgents[aid];
1265
+ if (!agent) {
1266
+ console.log(chalk.red(`Unknown agent: ${aid}`));
1267
+ continue;
1268
+ }
1269
+ console.log(chalk.bold(`→ ${aid}`));
1270
+ console.log(chalk.dim(` ${updatedModels.length} model(s)`));
1271
+ const success = installProviderToAgent(aid, installProvider, agent.home);
1272
+ if (success) successCount++;
1273
+ console.log();
1274
+ }
1275
+
1276
+ if (successCount > 0) {
1277
+ console.log(chalk.green.bold(`✓ Provider updated in ${successCount} agent(s)!`));
1278
+ } else {
1279
+ console.log(chalk.yellow("No agents were updated."));
1280
+ }
1281
+ });
1282
+
1008
1283
  // ── cpc remove ───────────────────────────────────────────────
1009
1284
  program
1010
1285
  .command("remove [name]")
@@ -1057,6 +1332,8 @@ export function registerProvider(program, repoRoot) {
1057
1332
  .option("--model <model>", "Install only specific model")
1058
1333
  .option("--all", "Install all models")
1059
1334
  .option("--first", "Install only first model")
1335
+ .option("--wire-api <api>", "Codex wire_api (responses | chat_completions)")
1336
+ .option("--reasoning-effort <level>", "Codex model_reasoning_effort (minimal|low|medium|high)")
1060
1337
  .action(async (providerName, opts) => {
1061
1338
  const registry = loadRegistry(repoRoot);
1062
1339
  const agents = getAgents(registry);
@@ -1134,44 +1411,132 @@ export function registerProvider(program, repoRoot) {
1134
1411
  }
1135
1412
 
1136
1413
  // Step 3: Select agent(s)
1137
- let agentId;
1414
+ let agentIds;
1138
1415
  if (opts.agent) {
1139
- agentId = opts.agent;
1416
+ agentIds = opts.agent === "all" ? Object.keys(agents) : [opts.agent];
1140
1417
  } else {
1141
- agentId = await select({
1142
- message: "Select target agent:",
1418
+ // Auto-detect: 只显示 pi 和 opencode,默认不勾选
1419
+ const visibleAgentIds = Object.keys(agents)
1420
+ .filter((id) => id === "pi" || id === "opencode");
1421
+ const currentAgent = detectCurrentAgent(agents);
1422
+ const selected = await checkbox({
1423
+ message: "Select target agents:",
1143
1424
  choices: [
1144
- ...Object.entries(agents).map(([id, a]) => ({
1145
- name: `${id.padEnd(14)} ${a.description || ""}`,
1146
- value: id,
1147
- })),
1148
- { name: "─────────", value: "__separator__", disabled: true },
1149
- { name: "all agents", value: "all" },
1425
+ ...visibleAgentIds.map((id) => {
1426
+ const a = agents[id];
1427
+ const mark = id === currentAgent ? " ← current" : "";
1428
+ return {
1429
+ name: `${id.padEnd(14)} ${a.description || ""}${mark}`,
1430
+ value: id,
1431
+ checked: false,
1432
+ };
1433
+ }),
1150
1434
  ],
1435
+ instructions: {
1436
+ navigator: "↑↓ navigate",
1437
+ select: "space select",
1438
+ all: "a toggle all",
1439
+ },
1151
1440
  });
1152
- }
1153
1441
 
1154
- const agentIds = agentId === "all" ? Object.keys(agents) : [agentId];
1442
+ if (selected.length === 0) {
1443
+ console.log(chalk.yellow("No agents selected. Cancelled."));
1444
+ return;
1445
+ }
1446
+
1447
+ agentIds = selected;
1448
+ }
1155
1449
 
1156
1450
  // Step 4: Install provider to agents
1157
1451
  console.log();
1158
1452
  let successCount = 0;
1159
1453
 
1454
+ // codex 特殊处理:全局只能用一个 model,需确定 wire_api(协议)。
1455
+ // 如果目标含 codex 且选了多个模型,先让用户挑一个作为默认。
1456
+ const hasCodex = agentIds.includes("codex");
1457
+ let codexDefaultModel = null;
1458
+ let codexWireApi = opts.wireApi || null;
1459
+ let codexReasoningEffort = opts.reasoningEffort || null;
1460
+ if (hasCodex) {
1461
+ let codexModels = installProvider.models;
1462
+ if (codexModels.length > 1) {
1463
+ console.log(
1464
+ chalk.yellow(
1465
+ `⚠ codex 一次只能用一个模型(顶层 model),多模型时仅生效默认这一个`,
1466
+ ),
1467
+ );
1468
+ const picked = await select({
1469
+ message: "选择 codex 的默认 model:",
1470
+ choices: codexModels.map((m) => ({
1471
+ name: `${m.id}${m.description ? " — " + m.description : ""}`,
1472
+ value: m.id,
1473
+ })),
1474
+ });
1475
+ codexDefaultModel = picked;
1476
+ } else {
1477
+ codexDefaultModel = codexModels[0]?.id;
1478
+ }
1479
+ // wire_api 选择(未传 --wire-api 时交互确认)
1480
+ if (!codexWireApi) {
1481
+ const inferred =
1482
+ installProvider.api === "responses"
1483
+ ? "responses"
1484
+ : installProvider.api === "anthropic-messages"
1485
+ ? "chat_completions"
1486
+ : "chat_completions";
1487
+ codexWireApi = await select({
1488
+ message: `codex wire_api(与 ${installProvider.name} 通信协议):`,
1489
+ choices: [
1490
+ {
1491
+ name: `responses — Codex Responses 原生协议(CCX 网关用这个)${inferred === "responses" ? " [推断]" : ""}`,
1492
+ value: "responses",
1493
+ },
1494
+ {
1495
+ name: `chat_completions — 标准 OpenAI 兼容协议(多数网关用这个)${inferred === "chat_completions" ? " [推断]" : ""}`,
1496
+ value: "chat_completions",
1497
+ },
1498
+ ],
1499
+ });
1500
+ }
1501
+ }
1502
+
1160
1503
  for (const aid of agentIds) {
1161
1504
  const agentDef = agents[aid];
1162
1505
  if (!agentDef) {
1163
1506
  console.log(chalk.red(`Unknown agent: ${aid}`));
1164
1507
  continue;
1165
- }
1508
+ }
1166
1509
 
1167
1510
  const agent = resolveAgentPaths(agentDef);
1168
1511
  console.log(chalk.bold(`→ ${aid}`));
1169
- console.log(chalk.dim(` ${installProvider.models.length} model(s)`));
1512
+
1513
+ let targetProvider = installProvider;
1514
+ let targetOpts = {};
1515
+ if (aid === "codex") {
1516
+ const picked =
1517
+ installProvider.models.find(
1518
+ (m) => m.id === codexDefaultModel,
1519
+ ) || installProvider.models[0];
1520
+ targetProvider = { ...installProvider, models: [picked] };
1521
+ targetOpts = {
1522
+ wireApi: codexWireApi,
1523
+ modelReasoningEffort: codexReasoningEffort,
1524
+ };
1525
+ console.log(
1526
+ chalk.dim(` model: ${picked.id} (codex 全局默认, 单模型)`),
1527
+ );
1528
+ console.log(chalk.dim(` wire_api: ${codexWireApi}`));
1529
+ } else {
1530
+ console.log(
1531
+ chalk.dim(` ${targetProvider.models.length} model(s)`),
1532
+ );
1533
+ }
1170
1534
 
1171
1535
  const success = installProviderToAgent(
1172
1536
  aid,
1173
- installProvider,
1537
+ targetProvider,
1174
1538
  agent.home,
1539
+ targetOpts,
1175
1540
  );
1176
1541
  if (success) successCount++;
1177
1542
  console.log();
@@ -1445,7 +1810,13 @@ export function registerProvider(program, repoRoot) {
1445
1810
  case "pi": {
1446
1811
  const modelsPath = join(agent.home, "models.json");
1447
1812
  if (existsSync(modelsPath)) {
1448
- const models = JSON.parse(readFileSync(modelsPath, "utf8"));
1813
+ let models = { providers: {} };
1814
+ try {
1815
+ const content = readFileSync(modelsPath, "utf8").trim();
1816
+ if (content) models = JSON.parse(content);
1817
+ } catch {
1818
+ models = { providers: {} };
1819
+ }
1449
1820
  if (
1450
1821
  models.providers &&
1451
1822
  Object.keys(models.providers).length > 0
@@ -1494,20 +1865,15 @@ export function registerProvider(program, repoRoot) {
1494
1865
 
1495
1866
  case "codex": {
1496
1867
  const configPath = join(agent.home, "config.toml");
1497
- if (existsSync(configPath)) {
1498
- const content = readFileSync(configPath, "utf8");
1499
- const providerMatch = content.match(/model_provider = "(.*?)"/);
1500
- const modelMatch = content.match(/model = "(.*?)"/);
1501
- if (providerMatch) {
1502
- console.log(` Provider: ${chalk.dim(providerMatch[1])}`);
1503
- console.log(
1504
- ` Model: ${chalk.dim(modelMatch ? modelMatch[1] : "not set")}`,
1505
- );
1506
- } else {
1507
- console.log(chalk.dim(" No provider configured"));
1508
- }
1868
+ const info = readCodexProviderInfo(configPath);
1869
+ if (info.provider) {
1870
+ console.log(` Provider: ${chalk.dim(info.provider)}`);
1871
+ console.log(` Model: ${chalk.dim(info.model || "not set")}`);
1872
+ console.log(` Base URL: ${chalk.dim(info.baseUrl || "not set")}`);
1873
+ console.log(` Wire API: ${chalk.dim(info.wireApi || "not set")}`);
1874
+ console.log(` Env Key: ${chalk.dim(info.envKey || "(none)")}`);
1509
1875
  } else {
1510
- console.log(chalk.dim(" Config file not found"));
1876
+ console.log(chalk.dim(" No provider configured"));
1511
1877
  }
1512
1878
  break;
1513
1879
  }
@@ -1563,6 +1929,29 @@ export function registerProvider(program, repoRoot) {
1563
1929
  break;
1564
1930
  }
1565
1931
 
1932
+ case "opencode": {
1933
+ const configPath = join(agent.home, "opencode.json");
1934
+ if (existsSync(configPath)) {
1935
+ const configData = JSON.parse(readFileSync(configPath, "utf8"));
1936
+ if (configData.provider && Object.keys(configData.provider).length > 0) {
1937
+ for (const [name, config] of Object.entries(
1938
+ configData.provider,
1939
+ )) {
1940
+ console.log(` ${chalk.blue(name)}`);
1941
+ console.log(` URL: ${chalk.dim(config.options?.baseURL || "not set")}`);
1942
+ console.log(
1943
+ ` Models: ${chalk.dim(Object.keys(config.models || {}).length)}`,
1944
+ );
1945
+ }
1946
+ } else {
1947
+ console.log(chalk.dim(" No providers configured"));
1948
+ }
1949
+ } else {
1950
+ console.log(chalk.dim(" Config file not found"));
1951
+ }
1952
+ break;
1953
+ }
1954
+
1566
1955
  default:
1567
1956
  console.log(chalk.dim(" Unsupported agent"));
1568
1957
  }
@@ -1570,7 +1959,88 @@ export function registerProvider(program, repoRoot) {
1570
1959
  console.log(chalk.red(` Error reading config: ${err.message}`));
1571
1960
  }
1572
1961
 
1573
- console.log();
1962
+ console.log();
1963
+ }
1964
+ });
1965
+
1966
+ // ── cpc doctor ───────────────────────────────────────────────
1967
+ program
1968
+ .command("doctor")
1969
+ .description("Check if cpc is properly configured and working")
1970
+ .action(() => {
1971
+ console.log(chalk.bold("\n🔍 CPC Doctor\n"));
1972
+
1973
+ let issues = 0;
1974
+
1975
+ // Check Node.js version
1976
+ const nodeVersion = process.version;
1977
+ const requiredVersion = "18.0.0";
1978
+ const nodeOk = compareVersions(nodeVersion.slice(1), requiredVersion) >= 0;
1979
+ console.log(`${nodeOk ? "✅" : "❌"} Node.js version: ${nodeVersion} (required: >=${requiredVersion})`);
1980
+ if (!nodeOk) issues++;
1981
+
1982
+ // Check registry.yaml
1983
+ try {
1984
+ const registry = loadRegistry(repoRoot);
1985
+ const agents = getAgents(registry);
1986
+ const agentCount = Object.keys(agents).length;
1987
+ console.log(`✅ registry.yaml: ${agentCount} agents defined`);
1988
+ } catch (err) {
1989
+ console.log(`❌ registry.yaml: ${err.message}`);
1990
+ issues++;
1991
+ }
1992
+
1993
+ // Check config directory
1994
+ const configDir = getConfigDir();
1995
+ if (existsSync(configDir)) {
1996
+ console.log(`✅ Config directory: ${configDir}`);
1997
+
1998
+ // Check providers.json
1999
+ try {
2000
+ const providers = loadProviders();
2001
+ const providerCount = Object.keys(providers.providers).length;
2002
+ console.log(`✅ providers.json: ${providerCount} provider(s) configured`);
2003
+ } catch (err) {
2004
+ console.log(`❌ providers.json: ${err.message}`);
2005
+ issues++;
2006
+ }
2007
+ } else {
2008
+ console.log(`⚠️ Config directory: ${configDir} (will be created on first use)`);
2009
+ }
2010
+
2011
+ // Check installed agents
2012
+ console.log(chalk.bold("\nInstalled Agents:"));
2013
+ try {
2014
+ const registry = loadRegistry(repoRoot);
2015
+ const agents = getAgents(registry);
2016
+ for (const [id, agent] of Object.entries(agents)) {
2017
+ const installed = isAgentInstalled(id, agents);
2018
+ console.log(`${installed ? "✅" : "⬜"} ${id.padEnd(14)} ${agent.description || ""}`);
2019
+ }
2020
+ } catch (err) {
2021
+ console.log(chalk.red(` Error: ${err.message}`));
1574
2022
  }
2023
+
2024
+ // Summary
2025
+ console.log(chalk.bold(`\n${"═".repeat(40)}`));
2026
+ if (issues === 0) {
2027
+ console.log(chalk.green.bold("✅ All checks passed! CPC is ready to use."));
2028
+ } else {
2029
+ console.log(chalk.red.bold(`❌ Found ${issues} issue(s). Please fix them before using CPC.`));
2030
+ }
2031
+ console.log();
1575
2032
  });
1576
2033
  }
2034
+
2035
+ /**
2036
+ * Compare semver versions
2037
+ */
2038
+ function compareVersions(a, b) {
2039
+ const pa = a.split(".").map(Number);
2040
+ const pb = b.split(".").map(Number);
2041
+ for (let i = 0; i < 3; i++) {
2042
+ if (pa[i] > pb[i]) return 1;
2043
+ if (pa[i] < pb[i]) return -1;
2044
+ }
2045
+ return 0;
2046
+ }