@wemind/cli 1.2.0 → 1.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.
package/dist/cli.js CHANGED
@@ -973,6 +973,8 @@ function normAgentFields(j) {
973
973
  if (typeof j.bin === "string" && j.bin.trim()) cfg.bin = j.bin.trim();
974
974
  if (Array.isArray(j.args)) cfg.args = j.args.filter((x) => typeof x === "string");
975
975
  else if (typeof j.args === "string" && j.args.trim()) cfg.args = j.args.trim().split(/\s+/);
976
+ if (Array.isArray(j.extraArgs)) cfg.extraArgs = j.extraArgs.filter((x) => typeof x === "string");
977
+ else if (typeof j.extraArgs === "string" && j.extraArgs.trim()) cfg.extraArgs = j.extraArgs.trim().split(/\s+/);
976
978
  if (typeof j.shell === "boolean") cfg.shell = j.shell;
977
979
  if (typeof j.homeEnvVar === "string" && j.homeEnvVar.trim()) cfg.homeEnvVar = j.homeEnvVar.trim();
978
980
  if (typeof j.model === "string" && j.model.trim()) cfg.model = j.model.trim();
@@ -984,6 +986,8 @@ function normPresetOverride(j) {
984
986
  if (typeof j?.bin === "string" && j.bin.trim()) ov.bin = j.bin.trim();
985
987
  if (Array.isArray(j?.args)) ov.args = j.args.filter((x) => typeof x === "string");
986
988
  else if (typeof j?.args === "string" && j.args.trim()) ov.args = j.args.trim().split(/\s+/);
989
+ if (Array.isArray(j?.extraArgs)) ov.extraArgs = j.extraArgs.filter((x) => typeof x === "string");
990
+ else if (typeof j?.extraArgs === "string" && j.extraArgs.trim()) ov.extraArgs = j.extraArgs.trim().split(/\s+/);
987
991
  if (typeof j?.shell === "boolean") ov.shell = j.shell;
988
992
  if (typeof j?.homeEnvVar === "string" && j.homeEnvVar.trim()) ov.homeEnvVar = j.homeEnvVar.trim();
989
993
  if (typeof j?.model === "string" && j.model.trim()) ov.model = j.model.trim();
@@ -1053,6 +1057,7 @@ function mergeBrainConfig(existing, patch) {
1053
1057
  const legacy = {};
1054
1058
  if (existing?.bin) legacy.bin = existing.bin;
1055
1059
  if (existing?.args?.length) legacy.args = existing.args;
1060
+ if (existing?.extraArgs?.length) legacy.extraArgs = existing.extraArgs;
1056
1061
  if (existing?.shell !== void 0) legacy.shell = existing.shell;
1057
1062
  if (existing?.homeEnvVar) legacy.homeEnvVar = existing.homeEnvVar;
1058
1063
  if (existing?.model) legacy.model = existing.model;
@@ -1064,20 +1069,23 @@ function mergeBrainConfig(existing, patch) {
1064
1069
  const next = { agent: patch.agent };
1065
1070
  if (patch.bin) next.bin = patch.bin;
1066
1071
  if (patch.args) next.args = patch.args;
1072
+ if (patch.extraArgs) next.extraArgs = patch.extraArgs;
1067
1073
  if (patch.shell !== void 0) next.shell = patch.shell;
1068
1074
  if (patch.homeEnvVar) next.homeEnvVar = patch.homeEnvVar;
1069
1075
  if (patch.model) next.model = patch.model;
1070
1076
  if (prev && prev === nextAgent) {
1071
1077
  if (!patch.bin && existing?.bin) next.bin = existing.bin;
1072
1078
  if (!patch.args && existing?.args?.length) next.args = existing.args;
1079
+ if (!patch.extraArgs && existing?.extraArgs?.length) next.extraArgs = existing.extraArgs;
1073
1080
  if (patch.shell === void 0 && existing?.shell !== void 0) next.shell = existing.shell;
1074
1081
  if (!patch.homeEnvVar && existing?.homeEnvVar) next.homeEnvVar = existing.homeEnvVar;
1075
1082
  if (!patch.model && existing?.model) next.model = existing.model;
1076
1083
  }
1077
- if (next.bin || next.args?.length || next.shell !== void 0 || next.homeEnvVar || next.model) {
1084
+ if (next.bin || next.args?.length || next.extraArgs?.length || next.shell !== void 0 || next.homeEnvVar || next.model) {
1078
1085
  const ov = { ...presets[nextAgent] };
1079
1086
  if (next.bin) ov.bin = next.bin;
1080
1087
  if (next.args?.length) ov.args = next.args;
1088
+ if (next.extraArgs?.length) ov.extraArgs = next.extraArgs;
1081
1089
  if (next.shell !== void 0) ov.shell = next.shell;
1082
1090
  if (next.homeEnvVar) ov.homeEnvVar = next.homeEnvVar;
1083
1091
  if (next.model) ov.model = next.model;
@@ -1104,6 +1112,53 @@ function clearBrainConfig(dir = DATA_DIR) {
1104
1112
  }
1105
1113
  }
1106
1114
  //#endregion
1115
+ //#region src/cline-credentials.ts
1116
+ /**
1117
+ * Cline ACP 的认证桥接。
1118
+ *
1119
+ * Cline 的 `isSessionReady()` 只自动恢复它自己的 3 种登录(`cline` / `cline-pass` / `openai-codex`),
1120
+ * 自定义/API-key provider(如 `openai-compatible`)不在 `tryRestoreAuth()` 的列表里——即使
1121
+ * `data/settings/providers.json` 里已经存在 `lastUsedProvider` 和 `apiKey`,`session/new` 也会报
1122
+ * `-32000 Authentication required`。Cline 官方给 ACP 的逃生口是 `CLINE_PROVIDER` + `CLINE_API_KEY`。
1123
+ *
1124
+ * 这里做一层**只读、按需**的桥接:父进程没有显式提供这两个变量时,把 `lastUsedProvider` 对应的
1125
+ * `settings.apiKey` 转成这两个环境变量,从而复用用户已经用 `cline auth` / 手写配置好的登录态。
1126
+ *
1127
+ * 边界(刻意收窄):
1128
+ * - **显式环境变量永远优先**:`CLINE_PROVIDER` 或 `CLINE_API_KEY` 只要有一个已存在,就完全不接管
1129
+ * (避免把父进程的 provider 和配置文件里的 key 错配);
1130
+ * - 只认 `lastUsedProvider`,且该 provider 必须有非空字符串 `apiKey`;OAuth 条目(只有 `auth`、
1131
+ * 没有 `apiKey`)不处理,继续走 Cline 自己的 `tryRestoreAuth()`;
1132
+ * - 文件不存在 / JSON 坏了 / 结构不对 → 返回空表,不抛异常(登录态问题仍由原来的认证提示暴露);
1133
+ * - 只读不写,不打印 key。
1134
+ */
1135
+ function clineCredentialEnv(realHome, parentEnv) {
1136
+ if (parentEnv.CLINE_PROVIDER?.trim() || parentEnv.CLINE_API_KEY?.trim()) return {};
1137
+ if (!realHome) return {};
1138
+ const file = path.join(realHome, "data", "settings", "providers.json");
1139
+ let parsed;
1140
+ try {
1141
+ parsed = JSON.parse(fs.readFileSync(file, "utf-8"));
1142
+ } catch {
1143
+ return {};
1144
+ }
1145
+ if (!isObj$1(parsed)) return {};
1146
+ const providerId = typeof parsed.lastUsedProvider === "string" ? parsed.lastUsedProvider.trim() : "";
1147
+ if (!providerId) return {};
1148
+ const providers = isObj$1(parsed.providers) ? parsed.providers : null;
1149
+ const entry = providers && isObj$1(providers[providerId]) ? providers[providerId] : null;
1150
+ const settings = entry && isObj$1(entry.settings) ? entry.settings : null;
1151
+ const apiKey = settings && typeof settings.apiKey === "string" ? settings.apiKey.trim() : "";
1152
+ if (!apiKey) return {};
1153
+ return {
1154
+ CLINE_PROVIDER: providerId,
1155
+ CLINE_API_KEY: apiKey
1156
+ };
1157
+ }
1158
+ function isObj$1(v) {
1159
+ return !!v && typeof v === "object" && !Array.isArray(v);
1160
+ }
1161
+ //#endregion
1107
1162
  //#region src/profile-model.ts
1108
1163
  /**
1109
1164
  * 真实 profile 用户层(`<realHome>/profiles/<profile>/cordis.patch.yml`)的三件事:
@@ -1265,7 +1320,12 @@ const ACP_AGENT_PRESETS = {
1265
1320
  name: "gemini",
1266
1321
  bin: "gemini",
1267
1322
  binWin: "gemini.cmd",
1268
- args: ["--experimental-acp"],
1323
+ args: [
1324
+ "--acp",
1325
+ "--approval-mode",
1326
+ "yolo",
1327
+ "--skip-trust"
1328
+ ],
1269
1329
  homeEnvVar: "GEMINI_CLI_HOME",
1270
1330
  shell: true
1271
1331
  },
@@ -1306,7 +1366,8 @@ const ACP_AGENT_PRESETS = {
1306
1366
  images: false,
1307
1367
  realProfile: "acp",
1308
1368
  model: "[\"deepseek-account\",\"deepseek-flash\"]",
1309
- mcpViaAcp: true
1369
+ mcpViaAcp: true,
1370
+ envDefaults: { DSH_PERMISSION_MODE: "danger-full-access" }
1310
1371
  },
1311
1372
  qoder: {
1312
1373
  name: "qoder",
@@ -1331,18 +1392,63 @@ const ACP_AGENT_PRESETS = {
1331
1392
  credentialFiles: [".auth/user"],
1332
1393
  identityFiles: [".auth/machine_id"]
1333
1394
  },
1395
+ cline: {
1396
+ name: "cline",
1397
+ bin: "cline",
1398
+ binWin: "cline.cmd",
1399
+ args: [
1400
+ "--acp",
1401
+ "--auto-approve",
1402
+ "true"
1403
+ ],
1404
+ shell: true,
1405
+ images: false,
1406
+ env: {
1407
+ CLINE_DIR: `%HOME%${path.sep}cline`,
1408
+ CLINE_DATA_DIR: `%HOME%${path.sep}cline${path.sep}data`,
1409
+ CLINE_MCP_SETTINGS_PATH: `%HOME%${path.sep}cline${path.sep}mcp.json`
1410
+ },
1411
+ realHome: path.join(os.homedir(), ".cline"),
1412
+ inheritDirs: [
1413
+ "rules",
1414
+ "skills",
1415
+ "agents"
1416
+ ],
1417
+ configSubdir: "cline",
1418
+ mcpFormat: "kiro",
1419
+ mcpConfigFile: "mcp.json",
1420
+ catalogFile: "data/settings/cline_mcp_settings.json",
1421
+ credentialFiles: ["data/settings/providers.json", "data/settings/models.json"],
1422
+ credentialEnv: clineCredentialEnv
1423
+ },
1334
1424
  "claude-code": {
1335
1425
  name: "claude-code",
1336
- bin: "npx",
1337
- binWin: "npx.cmd",
1338
- args: ["-y", "@zed-industries/claude-code-acp"],
1339
- shell: true
1426
+ bin: "claude-agent-acp",
1427
+ binWin: "claude-agent-acp.cmd",
1428
+ args: [],
1429
+ shell: true,
1430
+ env: { CLAUDE_CONFIG_DIR: `%HOME%${path.sep}claude` },
1431
+ realHome: path.join(os.homedir(), ".claude"),
1432
+ inheritDirs: [
1433
+ "skills",
1434
+ "agents",
1435
+ "commands"
1436
+ ],
1437
+ inheritFiles: ["CLAUDE.md"],
1438
+ configSubdir: "claude",
1439
+ catalogFile: path.join("..", ".claude.json"),
1440
+ credentialFiles: [".credentials.json"],
1441
+ mcpViaAcp: true,
1442
+ sessionMode: "bypassPermissions"
1340
1443
  },
1341
1444
  codex: {
1342
1445
  name: "codex",
1343
1446
  bin: "codex-acp",
1344
- binWin: "codex-acp.exe",
1345
- args: []
1447
+ binWin: "codex-acp.cmd",
1448
+ args: [],
1449
+ shell: true,
1450
+ envDefaults: { INITIAL_AGENT_MODE: "agent-full-access" },
1451
+ sessionMode: "agent-full-access"
1346
1452
  }
1347
1453
  };
1348
1454
  function onPath(exe) {
@@ -1351,15 +1457,45 @@ function onPath(exe) {
1351
1457
  }
1352
1458
  /**
1353
1459
  * `auto` 的探测顺序:按序取第一个「二进制能在 PATH 上解析到」的 agent。
1354
- * 只收纳**原生 ACP 且二进制可直接判定**的 agent——适配器类(`claude-code` 走 `npx`、`codex` 走 `codex-acp`)不参与自动探测(`npx` 几乎总在 PATH 上,会把它们误选出来)。
1460
+ * 只收纳**原生 ACP 且二进制可直接判定**的 agent——适配器类(`claude-code` 走 `claude-agent-acp`、`codex` 走 `codex-acp`)不参与自动探测:
1461
+ * 它们是独立安装的桥接进程、各有自己的认证,误选中会让「装了但没登录」的机器静默换脑;显式选用(`wemind brain set|switch <名>`)更安全。
1462
+ * 新 agent 一律追加在末尾:老机器上已装的前几个仍按原顺序被选中,不改变既有用户的默认大脑。
1355
1463
  */
1356
1464
  const AUTO_PROBE_ORDER = [
1357
1465
  "kiro",
1358
1466
  "opencode",
1359
1467
  "gemini",
1360
1468
  "deepseek",
1361
- "qoder"
1469
+ "qoder",
1470
+ "cline"
1362
1471
  ];
1472
+ /**
1473
+ * 该 agent 预设里「进入 ACP 模式」的标记 token:从默认 args 中挑出含 `acp` 的项。
1474
+ * 只用于整段替换 args 时的静态提醒;适配器(claude-code / codex)的 args 为空 → 返回空数组(不检查——适配器本体就是 ACP server,没有模式参数)。
1475
+ * 不能只查裸 `acp`:qoder 是 `--acp`、gemini 是 `--acp`、deepseek 是 `--profile acp`。
1476
+ */
1477
+ function acpModeTokens(name) {
1478
+ const preset = ACP_AGENT_PRESETS[name];
1479
+ return preset ? preset.args.filter((a) => /acp/i.test(a)) : [];
1480
+ }
1481
+ /**
1482
+ * 整段替换 args 时的静态提醒(**只提醒、不拒绝**):新 args 里找不到该 agent 的 ACP 模式标记时返回文案。
1483
+ * `binOverridden`(env / brain.json 指定了自定义 bin)时不检查——自定义入口/包装脚本可能自带 ACP 模式,静态判断不可靠。
1484
+ */
1485
+ function acpArgsWarning(name, args, binOverridden) {
1486
+ if (binOverridden) return null;
1487
+ const markers = acpModeTokens(name);
1488
+ if (!markers.length) return null;
1489
+ if (args.some((a) => /acp/i.test(a))) return null;
1490
+ return `args 里没有 ${name} 的 ACP 模式参数(预设是 ${markers.map((m) => `\`${m}\``).join(" / ")})——进程可能起得来但不会说 ACP,首条消息会握手超时。请把它一起写回去,或改用 --extra-args 只追加。`;
1491
+ }
1492
+ /** 运行时诊断:握手失败 / 进程在握手前退出时追加的提示;args 看起来正常、或 bin 是自定义入口时返回空串。 */
1493
+ function acpArgsHint(ag) {
1494
+ if (ag.binOverridden) return "";
1495
+ const markers = acpModeTokens(ag.name);
1496
+ if (!markers.length || ag.args.some((a) => /acp/i.test(a))) return "";
1497
+ return `(当前 args=[${ag.args.join(" ")}] 里没有 ${markers.map((m) => `\`${m}\``).join(" / ")}——若覆盖过 --args,确认没漏 ACP 模式参数;也可改用 --extra-args 只追加)`;
1498
+ }
1363
1499
  /** agent 在该平台的「可执行文件名」(用于 PATH 探测)。 */
1364
1500
  function presetExe(preset) {
1365
1501
  return process.platform === "win32" ? preset.binWin ?? `${preset.bin}.exe` : preset.bin;
@@ -1390,6 +1526,21 @@ function locateBin(bin) {
1390
1526
  }
1391
1527
  return null;
1392
1528
  }
1529
+ /** 组装 spawn 用的环境变量:父进程 env → `homeEnvVar` 指向沙箱 → 从凭据文件派生的 env(父进程未提供时)→ 预设强制 `env` → 预设 `envDefaults`(父进程已设同名变量时保留父进程的,`skipped` 收集这些键供调用方记日志)。 */
1530
+ function agentEnv(ag, home, skipped = []) {
1531
+ const env = { ...process.env };
1532
+ if (ag.homeEnvVar) env[ag.homeEnvVar] = home;
1533
+ for (const [k, v] of Object.entries(ag.credentialEnv?.(ag.realHome, process.env) ?? {})) if (env[k] === void 0) env[k] = v;
1534
+ for (const [k, v] of Object.entries(ag.env ?? {})) env[k] = v.replaceAll("%HOME%", home);
1535
+ for (const [k, v] of Object.entries(ag.envDefaults ?? {})) {
1536
+ if (process.env[k] !== void 0) {
1537
+ skipped.push(k);
1538
+ continue;
1539
+ }
1540
+ env[k] = v.replaceAll("%HOME%", home);
1541
+ }
1542
+ return env;
1543
+ }
1393
1544
  /** 该角色「选 agent」的环境变量:worker 先看 `WEMIND_WORKER_BRAIN`,未设则回落 `WEMIND_BRAIN`。 */
1394
1545
  function envKeyFor(role) {
1395
1546
  return (role === "worker" ? process.env.WEMIND_WORKER_BRAIN?.trim() || process.env.WEMIND_BRAIN?.trim() : process.env.WEMIND_BRAIN?.trim()) || void 0;
@@ -1430,7 +1581,10 @@ function resolveAgentCore(role, cfg, opts = {}) {
1430
1581
  const bin = explicitBin || (shell ? process.platform === "win32" ? exe : preset.bin : binPath || preset.bin);
1431
1582
  const available = known && binPath !== null;
1432
1583
  const argsOverride = opts.ignoreEnv ? void 0 : process.env.WEMIND_BRAIN_ARGS?.trim();
1433
- const args = argsOverride ? argsOverride.split(/\s+/) : overrideCfg?.args && overrideCfg.args.length ? overrideCfg.args : presetOv?.args && presetOv.args.length ? presetOv.args : preset.args;
1584
+ const baseArgs = argsOverride ? argsOverride.split(/\s+/) : overrideCfg?.args && overrideCfg.args.length ? overrideCfg.args : presetOv?.args && presetOv.args.length ? presetOv.args : preset.args;
1585
+ const extraEnv = opts.ignoreEnv ? void 0 : process.env.WEMIND_BRAIN_EXTRA_ARGS?.trim();
1586
+ const extraArgs = extraEnv ? extraEnv.split(/\s+/) : overrideCfg?.extraArgs && overrideCfg.extraArgs.length ? overrideCfg.extraArgs : presetOv?.extraArgs && presetOv.extraArgs.length ? presetOv.extraArgs : [];
1587
+ const args = [...baseArgs, ...extraArgs];
1434
1588
  const homeEnvVar = (opts.ignoreEnv ? void 0 : process.env.WEMIND_BRAIN_HOME_ENV?.trim()) || overrideCfg?.homeEnvVar?.trim() || presetOv?.homeEnvVar?.trim() || preset.homeEnvVar;
1435
1589
  const inherit = (process.env.WEMIND_BRAIN_INHERIT ?? process.env.WECHAT_BRAIN_INHERIT ?? "1") !== "0";
1436
1590
  const realHome = process.env.WEMIND_BRAIN_REAL_HOME?.trim() || process.env.WECHAT_REAL_KIRO_HOME?.trim() || preset.realHome;
@@ -1440,6 +1594,13 @@ function resolveAgentCore(role, cfg, opts = {}) {
1440
1594
  const model = explicitModel || derivedModel || (isDefaultModel(rawModel) ? void 0 : preset.model);
1441
1595
  const dirsEnv = process.env.WEMIND_BRAIN_INHERIT_DIRS?.trim();
1442
1596
  const inheritDirs = dirsEnv ? dirsEnv.split(/[,\s]+/).filter(Boolean) : preset.inheritDirs ?? [];
1597
+ const modeEnv = process.env.WEMIND_ACP_SESSION_MODE?.trim();
1598
+ const sessionMode = !!modeEnv && [
1599
+ "0",
1600
+ "false",
1601
+ "off",
1602
+ "no"
1603
+ ].includes(modeEnv.toLowerCase()) ? void 0 : modeEnv || preset.sessionMode;
1443
1604
  return {
1444
1605
  name: preset.name,
1445
1606
  bin,
@@ -1455,6 +1616,8 @@ function resolveAgentCore(role, cfg, opts = {}) {
1455
1616
  mcpConfigFile: preset.mcpConfigFile ?? "settings/mcp.json",
1456
1617
  catalogFile: preset.catalogFile ?? "settings/mcp.json",
1457
1618
  env: preset.env ?? {},
1619
+ envDefaults: preset.envDefaults ?? {},
1620
+ credentialEnv: preset.credentialEnv,
1458
1621
  restore: preset.restore,
1459
1622
  firstFrameMs: preset.firstFrameMs,
1460
1623
  heartbeatMs: preset.heartbeatMs,
@@ -1465,11 +1628,13 @@ function resolveAgentCore(role, cfg, opts = {}) {
1465
1628
  model,
1466
1629
  realProfile: preset.realProfile,
1467
1630
  mcpViaAcp: preset.mcpViaAcp ?? false,
1631
+ sessionMode,
1468
1632
  requestedKey,
1469
1633
  source,
1470
1634
  known,
1471
1635
  available,
1472
- binPath
1636
+ binPath,
1637
+ binOverridden: !!explicitBin
1473
1638
  };
1474
1639
  }
1475
1640
  /** 解析当前要用的**对话大脑**:环境变量 > 账号 brain.json 顶层 > auto。 */
@@ -1506,6 +1671,7 @@ function resolvePresetByName(name, cfg) {
1506
1671
  if (cfg?.agent?.trim().toLowerCase() === key) {
1507
1672
  if (cfg.bin) base.bin = cfg.bin;
1508
1673
  if (cfg.args?.length) base.args = cfg.args;
1674
+ if (cfg.extraArgs?.length) base.extraArgs = cfg.extraArgs;
1509
1675
  if (cfg.shell !== void 0) base.shell = cfg.shell;
1510
1676
  if (cfg.homeEnvVar) base.homeEnvVar = cfg.homeEnvVar;
1511
1677
  if (cfg.model) base.model = cfg.model;
@@ -1521,15 +1687,35 @@ function resolvePresetByName(name, cfg) {
1521
1687
  };
1522
1688
  }
1523
1689
  /**
1690
+ * 适配器类 agent 的安装指引:找不到适配器二进制时**直接给出可粘贴的命令**。
1691
+ * 口径:**适配器一律全局安装**(daemon 常驻、worker 每条任务起进程;npx 每次冷启动要联网现下、版本不可复现)。
1692
+ */
1693
+ const ADAPTER_INSTALL_HINT = { "claude-code": "`npm i -g @agentclientprotocol/claude-agent-acp`(装完得到 claude-agent-acp;Windows 上是 claude-agent-acp.cmd;**需 Node ≥22**)" };
1694
+ /**
1524
1695
  * 校验解析后的大脑是否可用:可用返回 null,否则返回一句面向用户的说明。
1525
1696
  * 供 daemon 启动校验(早失败、说人话)与 `wemind brain` / `wemind brain check` 复用。
1526
1697
  */
1527
1698
  function agentProblem(ag) {
1528
1699
  if (!ag.known) return `未知 agent「${ag.requestedKey}」。可选:${Object.keys(ACP_AGENT_PRESETS).join("、")}(自定义二进制可用 WEMIND_BRAIN_BIN 或 brain set --bin 指定)`;
1529
1700
  if (ag.requestedKey === "auto" && !ag.available) return `自动探测未找到可用的大脑(已试:${AUTO_PROBE_ORDER.join("、")})。请安装其中之一,或运行 \`wemind brain set <agent 名>\``;
1530
- if (!ag.available) return ag.name === "deepseek" ? `找不到 DeepSeek 的「${ag.bin}」。桌面端通常有 dsh.cmd;把它的绝对路径传给 \`wemind brain set deepseek --bin "<...>\\dsh.cmd"\`` : ag.name === "qoder" ? `找不到 Qoder CLI 的「${ag.bin}」。官方安装会放在 ~/.qoder/bin/qodercli/ 并加入 PATH;确认后重开终端,或 \`wemind brain set qoder --bin "<...>\\qodercli.exe"\`` : `找不到大脑「${ag.name}」的二进制「${ag.bin}」。安装它,或运行 \`wemind brain set <agent 名>\` 切换`;
1701
+ if (!ag.available) {
1702
+ const hint = ADAPTER_INSTALL_HINT[ag.name];
1703
+ if (hint) return `找不到大脑「${ag.name}」的适配器「${ag.bin}」。先装适配器:${hint};装完仍找不到就 \`wemind brain set ${ag.name} --bin "<绝对路径>"\` 指定它。`;
1704
+ return ag.name === "deepseek" ? `找不到 DeepSeek 的「${ag.bin}」。桌面端通常有 dsh.cmd;把它的绝对路径传给 \`wemind brain set deepseek --bin "<...>\\dsh.cmd"\`` : ag.name === "qoder" ? `找不到 Qoder CLI 的「${ag.bin}」。官方安装会放在 ~/.qoder/bin/qodercli/ 并加入 PATH;确认后重开终端,或 \`wemind brain set qoder --bin "<...>\\qodercli.exe"\`` : ag.name === "cline" ? `找不到 Cline CLI 的「${ag.bin}」。安装:\`npm i -g cline\`(Node 20+),装完后重开终端;或用 \`wemind brain set cline --bin "<...>\\cline.cmd"\`` : `找不到大脑「${ag.name}」的二进制「${ag.bin}」。安装它,或运行 \`wemind brain set <agent 名>\` 切换`;
1705
+ }
1531
1706
  return null;
1532
1707
  }
1708
+ /**
1709
+ * 认证失败时按 agent 补一句可执行的建议(`wemind brain check` 与 daemon 日志复用)。
1710
+ * Cline 的 ACP 只自动恢复它自己的 3 种登录;自定义 provider 必须靠环境变量——
1711
+ * 「重启 daemon」对它没用,所以不能只套用通用的「密钥/鉴权」提示。
1712
+ * 其它 agent 返回空串(不改变既有文案)。
1713
+ */
1714
+ function authHint(agentName, message) {
1715
+ if (agentName !== "cline") return "";
1716
+ if (!/authentication required|authenticate|unauthorized|not authenticated/i.test(message)) return "";
1717
+ return "Cline 的 ACP 只自动恢复 cline / cline-pass / openai-codex 三种登录;用自定义 provider(如 openai-compatible)时,请在启动 wemind 的环境里设置 CLINE_PROVIDER 与 CLINE_API_KEY(设置后重启一次 daemon)。";
1718
+ }
1533
1719
  //#endregion
1534
1720
  //#region src/agent-slot.ts
1535
1721
  /**
@@ -1814,6 +2000,7 @@ function applySandbox(ag) {
1814
2000
  for (const n of active) servers[n] = cat[n];
1815
2001
  const file = brainMcpJson(ag);
1816
2002
  const next = withMcpServers(readJson$1(file) ?? {}, servers, ag.mcpFormat);
2003
+ if (ag.mcpFormat === "opencode" && next.permission === void 0) next.permission = "allow";
1817
2004
  fs.mkdirSync(path.dirname(file), { recursive: true });
1818
2005
  fs.writeFileSync(file, JSON.stringify(next, null, 2), "utf-8");
1819
2006
  }
@@ -3043,9 +3230,9 @@ function promptRequest(ag, params) {
3043
3230
  async function ensureProcess(ag) {
3044
3231
  if (isAlive() && initialized) return;
3045
3232
  reset();
3046
- const env = { ...process.env };
3047
- if (ag.homeEnvVar) env[ag.homeEnvVar] = BRAIN_HOME;
3048
- for (const [k, v] of Object.entries(ag.env ?? {})) env[k] = v.replaceAll("%HOME%", BRAIN_HOME);
3233
+ const kept = [];
3234
+ const env = agentEnv(ag, BRAIN_HOME, kept);
3235
+ if (kept.length) dlog$1(`env defaults kept from parent: ${kept.join(",")}`);
3049
3236
  const proc = spawnAgent(ag, {
3050
3237
  cwd: BRAIN_HOME,
3051
3238
  env
@@ -3056,17 +3243,22 @@ async function ensureProcess(ag) {
3056
3243
  proc.stdout.on("data", onStdout);
3057
3244
  proc.stderr.on("data", () => {});
3058
3245
  proc.on("exit", (code) => {
3059
- dlog$1(`acp exited code=${code}`);
3246
+ dlog$1(`acp exited code=${code}${child === proc && !initialized ? acpArgsHint(ag) : ""}`);
3060
3247
  if (child === proc) reset();
3061
3248
  });
3062
3249
  proc.on("error", (e) => {
3063
3250
  dlog$1(`acp error ${String(e)}`);
3064
3251
  if (child === proc) reset();
3065
3252
  });
3066
- await request("initialize", {
3067
- protocolVersion: 1,
3068
- clientCapabilities: {}
3069
- }, REQ_TIMEOUT$1);
3253
+ try {
3254
+ await request("initialize", {
3255
+ protocolVersion: 1,
3256
+ clientCapabilities: {}
3257
+ }, REQ_TIMEOUT$1);
3258
+ } catch (e) {
3259
+ dlog$1(`initialize failed: ${String(e)}${acpArgsHint(ag)}`);
3260
+ throw e;
3261
+ }
3070
3262
  initialized = true;
3071
3263
  dlog$1("initialized");
3072
3264
  }
@@ -3085,6 +3277,32 @@ async function applyModel(ag, sid) {
3085
3277
  throw e;
3086
3278
  }
3087
3279
  }
3280
+ /** 从 `session/new|load|resume` 的回包里取可用模式 id;agent 不报 `modes` 时返回 null(=不校验,照样尝试)。 */
3281
+ function availableModeIds(res) {
3282
+ const list = res?.modes?.availableModes;
3283
+ if (!Array.isArray(list)) return null;
3284
+ const ids = list.map((m) => String(m?.id ?? m)).filter(Boolean);
3285
+ return ids.length ? ids : null;
3286
+ }
3287
+ /** 会话建立/恢复后切到预设声明的模式(免审批:claude-code 的 bypassPermissions、codex 的 agent-full-access)。
3288
+ * **失败只记日志、不抛**:模式不可用(如 claude-code 以 root 跑却没有 IS_SANDBOX)时进程仍可用,
3289
+ * 审批请求继续由应答器处理(默认拒绝,WEMIND_ACP_AUTO_ALLOW=1 放行);抛错反而会把整轮打成失败。 */
3290
+ async function applySessionMode(ag, sid, modes = null) {
3291
+ if (!ag.sessionMode) return;
3292
+ if (modes && !modes.includes(ag.sessionMode)) {
3293
+ dlog$1(`session mode ${ag.sessionMode} 不在 agent 报告的可用模式 [${modes.join(",")}] 里——跳过;审批请求仍由应答器处理`);
3294
+ return;
3295
+ }
3296
+ try {
3297
+ await request("session/set_mode", {
3298
+ sessionId: sid,
3299
+ modeId: ag.sessionMode
3300
+ }, REQ_TIMEOUT$1);
3301
+ dlog$1(`session mode set ${ag.sessionMode}`);
3302
+ } catch (e) {
3303
+ dlog$1(`session mode set failed (${String(e)})——继续;审批请求仍由应答器处理`);
3304
+ }
3305
+ }
3088
3306
  async function ensureSession(ag, userId) {
3089
3307
  const existing = userSessions.get(userId);
3090
3308
  if (existing) return existing;
@@ -3094,12 +3312,13 @@ async function ensureSession(ag, userId) {
3094
3312
  if (saved) {
3095
3313
  const methods = ag.restore === "resume" ? ["resume"] : ["load", "resume"];
3096
3314
  for (const method of methods) try {
3097
- await request(`session/${method}`, {
3315
+ const restored = await request(`session/${method}`, {
3098
3316
  sessionId: saved,
3099
3317
  cwd: BRAIN_HOME,
3100
3318
  mcpServers
3101
3319
  }, REQ_TIMEOUT$1);
3102
3320
  await applyModel(ag, saved);
3321
+ await applySessionMode(ag, saved, availableModeIds(restored));
3103
3322
  userSessions.set(userId, saved);
3104
3323
  markUsed(ag.name, userId, saved);
3105
3324
  dlog$1(`session/${method} ok ${saved} mcp=[${mcpServers.map((s) => s.name).join(",")}]`);
@@ -3114,6 +3333,7 @@ async function ensureSession(ag, userId) {
3114
3333
  }, REQ_TIMEOUT$1);
3115
3334
  if (!res?.sessionId) throw new Error("session/new returned no sessionId");
3116
3335
  await applyModel(ag, res.sessionId);
3336
+ await applySessionMode(ag, res.sessionId, availableModeIds(res));
3117
3337
  userSessions.set(userId, res.sessionId);
3118
3338
  map[keyOf(ag, userId)] = res.sessionId;
3119
3339
  saveSessionMap$1(map);
@@ -3216,12 +3436,13 @@ async function resumeSession(userId, sessionId) {
3216
3436
  const methods = ag.restore === "resume" ? ["resume"] : ["load", "resume"];
3217
3437
  let lastErr = "";
3218
3438
  for (const method of methods) try {
3219
- await request(`session/${method}`, {
3439
+ const restored = await request(`session/${method}`, {
3220
3440
  sessionId,
3221
3441
  cwd: BRAIN_HOME,
3222
3442
  mcpServers
3223
3443
  }, REQ_TIMEOUT$1);
3224
3444
  await applyModel(ag, sessionId);
3445
+ await applySessionMode(ag, sessionId, availableModeIds(restored));
3225
3446
  userSessions.set(userId, sessionId);
3226
3447
  const map = loadSessionMap$1(ag);
3227
3448
  map[keyOf(ag, userId)] = sessionId;
@@ -3263,6 +3484,55 @@ function heartbeatMs() {
3263
3484
  return getAgent("brain").heartbeatMs;
3264
3485
  }
3265
3486
  //#endregion
3487
+ //#region src/brain-state.ts
3488
+ /**
3489
+ * daemon 启动/热切换成功后,把「这一刻真正生效的对话大脑」写进 `<账号目录>/brain-runtime.json`。
3490
+ *
3491
+ * 为什么还需要这个文件:`brain.pid` 只在 **ACP 子进程真的 spawn 之后**才有内容,而大脑是懒启动的——
3492
+ * 刚启动、还没收到消息时没有 pid 记录。此前 `wemind list` 的 `agent=` 只能显示 `brain.json`(有意「不看
3493
+ * 当前 shell 的环境变量」),于是用 `WEMIND_BRAIN=cline` 启动的 daemon 会一直显示成配置里的 deepseek,
3494
+ * 让人以为切换没生效。
3495
+ *
3496
+ * 这个文件记录的是 **daemon 进程自己的解析结果**(含来源 env/file/auto),与 CLI 端环境变量无关;
3497
+ * daemon 没运行时 `manage.ts` 不会采信它(`stop` 也会清掉),所以不会把旧状态当成现在时。
3498
+ */
3499
+ const BRAIN_RUNTIME_FILE = "brain-runtime.json";
3500
+ /** 写入运行时大脑(原子替换;失败静默——显示用,不该影响收发)。 */
3501
+ function writeBrainState(dir, ag) {
3502
+ const body = {
3503
+ agent: ag.name,
3504
+ source: ag.source,
3505
+ bin: ag.bin,
3506
+ args: ag.args,
3507
+ startedAt: Date.now()
3508
+ };
3509
+ try {
3510
+ fs.mkdirSync(dir, { recursive: true });
3511
+ const file = path.join(dir, BRAIN_RUNTIME_FILE);
3512
+ const tmp = `${file}.tmp`;
3513
+ fs.writeFileSync(tmp, JSON.stringify(body, null, 2) + "\n", "utf-8");
3514
+ fs.renameSync(tmp, file);
3515
+ } catch {}
3516
+ }
3517
+ /** 读运行时大脑;没有/损坏/字段不对 → null(调用方按「没有运行时信息」处理)。 */
3518
+ function readBrainState(dir) {
3519
+ try {
3520
+ const j = JSON.parse(fs.readFileSync(path.join(dir, BRAIN_RUNTIME_FILE), "utf-8"));
3521
+ if (typeof j?.agent !== "string" || !j.agent) return null;
3522
+ const src = j.source;
3523
+ const source = src === "env" || src === "auto" ? src : src === "file" ? "file" : "file";
3524
+ return {
3525
+ agent: j.agent,
3526
+ source,
3527
+ bin: typeof j.bin === "string" ? j.bin : "",
3528
+ args: Array.isArray(j.args) ? j.args.filter((x) => typeof x === "string") : [],
3529
+ startedAt: Number(j.startedAt) || 0
3530
+ };
3531
+ } catch {
3532
+ return null;
3533
+ }
3534
+ }
3535
+ //#endregion
3266
3536
  //#region src/brain-switch.ts
3267
3537
  /**
3268
3538
  * 运行时切换对话大脑。
@@ -3375,6 +3645,7 @@ async function runSwitch(key, opts) {
3375
3645
  const note = envNote(key);
3376
3646
  if (pre.already) {
3377
3647
  if (opts.persist !== false) updateBrainConfig({ agent: key }, dir);
3648
+ writeBrainState(dir, target);
3378
3649
  return {
3379
3650
  ok: true,
3380
3651
  agent: target,
@@ -3396,6 +3667,7 @@ async function runSwitch(key, opts) {
3396
3667
  await withTimeout(warmup(target, opts.userId ?? ""), TIMEOUT_MS, warmupTimeoutMsg());
3397
3668
  setAgent("brain", target);
3398
3669
  if (opts.persist !== false) updateBrainConfig({ agent: key }, dir);
3670
+ writeBrainState(dir, target);
3399
3671
  const ms = Date.now() - t0;
3400
3672
  return {
3401
3673
  ok: true,
@@ -4231,6 +4503,7 @@ function applyWorkerSandbox(ag) {
4231
4503
  }
4232
4504
  const file = workerMcpJson(ag);
4233
4505
  const next = withMcpServers(readJson(file) ?? {}, servers, ag.mcpFormat);
4506
+ if (ag.mcpFormat === "opencode" && next.permission === void 0) next.permission = "allow";
4234
4507
  fs.mkdirSync(path.dirname(file), { recursive: true });
4235
4508
  fs.writeFileSync(file, JSON.stringify(next, null, 2), "utf-8");
4236
4509
  }
@@ -4435,7 +4708,7 @@ function saveSessionMap(m) {
4435
4708
  fs.renameSync(tmp, SESSION_MAP_FILE);
4436
4709
  }
4437
4710
  /** 一条连接:一个 ACP agent 进程 + 它自己的握手/会话/进度状态。 */
4438
- var WorkerConn = class {
4711
+ var WorkerConn = class WorkerConn {
4439
4712
  id;
4440
4713
  agent;
4441
4714
  /** 池用来标注「这条连接现在跑的是哪个任务」(空闲时为 undefined)。 */
@@ -4724,9 +4997,9 @@ var WorkerConn = class {
4724
4997
  async ensureProcess() {
4725
4998
  if (this.isAlive() && this.initialized) return;
4726
4999
  this.kill();
4727
- const env = { ...process.env };
4728
- if (this.agent.homeEnvVar) env[this.agent.homeEnvVar] = WORKER_HOME;
4729
- for (const [k, v] of Object.entries(this.agent.env ?? {})) env[k] = v.replaceAll("%HOME%", WORKER_HOME);
5000
+ const kept = [];
5001
+ const env = agentEnv(this.agent, WORKER_HOME, kept);
5002
+ if (kept.length) dlog(`${this.id} env defaults kept from parent: ${kept.join(",")}`);
4730
5003
  const cwd = this.effectiveCwd();
4731
5004
  const proc = spawnAgent(this.agent, {
4732
5005
  cwd,
@@ -4738,17 +5011,23 @@ var WorkerConn = class {
4738
5011
  proc.stdout.on("data", (d) => this.onStdout(d));
4739
5012
  proc.stderr.on("data", () => {});
4740
5013
  proc.on("exit", (code) => {
4741
- dlog(`${this.id}(${this.agent.name}) exited code=${code}`);
5014
+ const hint = this.child === proc && !this.initialized ? acpArgsHint(this.agent) : "";
5015
+ dlog(`${this.id}(${this.agent.name}) exited code=${code}${hint}`);
4742
5016
  if (this.child === proc) this.kill();
4743
5017
  });
4744
5018
  proc.on("error", (e) => {
4745
5019
  dlog(`${this.id}(${this.agent.name}) error ${String(e)}`);
4746
5020
  if (this.child === proc) this.kill();
4747
5021
  });
4748
- await this.request("initialize", {
4749
- protocolVersion: 1,
4750
- clientCapabilities: {}
4751
- }, REQ_TIMEOUT);
5022
+ try {
5023
+ await this.request("initialize", {
5024
+ protocolVersion: 1,
5025
+ clientCapabilities: {}
5026
+ }, REQ_TIMEOUT);
5027
+ } catch (e) {
5028
+ dlog(`${this.id}(${this.agent.name}) initialize failed: ${String(e)}${acpArgsHint(this.agent)}`);
5029
+ throw e;
5030
+ }
4752
5031
  this.initialized = true;
4753
5032
  dlog(`${this.id} initialized`);
4754
5033
  }
@@ -4766,6 +5045,30 @@ var WorkerConn = class {
4766
5045
  throw e;
4767
5046
  }
4768
5047
  }
5048
+ /** 从 `session/new|load|resume` 的回包里取可用模式 id;agent 不报 `modes` 时返回 null(=不校验,照样尝试)。 */
5049
+ static availableModeIds(res) {
5050
+ const list = res?.modes?.availableModes;
5051
+ if (!Array.isArray(list)) return null;
5052
+ const ids = list.map((m) => String(m?.id ?? m)).filter(Boolean);
5053
+ return ids.length ? ids : null;
5054
+ }
5055
+ /** 会话建立/恢复后切到预设声明的模式(免审批);失败只记日志、不抛——审批请求仍由应答器兜底。 */
5056
+ async applySessionMode(sid, modes = null) {
5057
+ if (!this.agent.sessionMode) return;
5058
+ if (modes && !modes.includes(this.agent.sessionMode)) {
5059
+ dlog(`${this.id} session mode ${this.agent.sessionMode} 不在可用模式 [${modes.join(",")}] 里——跳过;审批请求仍由应答器处理`);
5060
+ return;
5061
+ }
5062
+ try {
5063
+ await this.request("session/set_mode", {
5064
+ sessionId: sid,
5065
+ modeId: this.agent.sessionMode
5066
+ }, REQ_TIMEOUT);
5067
+ dlog(`${this.id} session mode set ${this.agent.sessionMode}`);
5068
+ } catch (e) {
5069
+ dlog(`${this.id} session mode set failed (${String(e)})——继续;审批请求仍由应答器处理`);
5070
+ }
5071
+ }
4769
5072
  /** 取会话:内存 → 磁盘(load/resume 恢复) → 新建。key 由调用方给(每任务一个)。
4770
5073
  * `strictResume`:有旧会话但恢复失败 → 抛「接不上」,绝不静默新建空会话。 */
4771
5074
  async ensureSession(key, strictResume = false) {
@@ -4777,12 +5080,13 @@ var WorkerConn = class {
4777
5080
  if (saved) {
4778
5081
  const methods = this.agent.restore === "resume" ? ["resume"] : ["load", "resume"];
4779
5082
  for (const method of methods) try {
4780
- await this.request(`session/${method}`, {
5083
+ const restored = await this.request(`session/${method}`, {
4781
5084
  sessionId: saved,
4782
5085
  cwd: this.effectiveCwd(),
4783
5086
  mcpServers
4784
5087
  }, REQ_TIMEOUT);
4785
5088
  await this.applyModel(saved);
5089
+ await this.applySessionMode(saved, WorkerConn.availableModeIds(restored));
4786
5090
  this.sessions.set(key, saved);
4787
5091
  dlog(`${this.id} session/${method} ok ${saved} mcp=[${mcpServers.map((s) => s.name).join(",")}]`);
4788
5092
  return saved;
@@ -4797,6 +5101,7 @@ var WorkerConn = class {
4797
5101
  }, REQ_TIMEOUT);
4798
5102
  if (!res?.sessionId) throw new Error("session/new returned no sessionId");
4799
5103
  await this.applyModel(res.sessionId);
5104
+ await this.applySessionMode(res.sessionId, WorkerConn.availableModeIds(res));
4800
5105
  this.sessions.set(key, res.sessionId);
4801
5106
  const fresh = loadSessionMap(this.agent);
4802
5107
  fresh[mapKey] = res.sessionId;
@@ -5730,10 +6035,13 @@ function brainFailurePlan(e) {
5730
6035
  category: cat
5731
6036
  };
5732
6037
  }
5733
- /** 按错误类型给用户的失败提示。 */
5734
- function brainErrorMessage(e) {
6038
+ /** 按错误类型给用户的失败提示。`agentName` 用于给特定 agent 补专属建议(如 Cline 的自定义 provider 要设环境变量)。 */
6039
+ function brainErrorMessage(e, agentName) {
5735
6040
  const prov = providerIssue(e);
5736
- if (prov) return `(这次没答上来:${prov}。这类问题重试不会好——把账号/模型处理好后把消息再发一次就行。${prov === "密钥/鉴权有问题" || prov === "模型不可用" ? "若刚在本机改过登录态或模型配置,还要重启一次 daemon(wemind stop && wemind start)才会读到。" : ""})`;
6041
+ if (prov) {
6042
+ const auth = agentName ? authHint(agentName, String(e instanceof Error ? e.message : e)) : "";
6043
+ return `(这次没答上来:${prov}。这类问题重试不会好——把账号/模型处理好后把消息再发一次就行。${auth ? ` ${auth}` : prov === "密钥/鉴权有问题" || prov === "模型不可用" ? "若刚在本机改过登录态或模型配置,还要重启一次 daemon(wemind stop && wemind start)才会读到。" : ""})`;
6044
+ }
5737
6045
  switch (classifyBrainError(e)) {
5738
6046
  case "backend": return "(模型后端临时出错了,自动重试多次仍失败,请稍后把消息再发一次)";
5739
6047
  case "timeout": return "(这次处理超时/卡住被中断了,重试也没成功——可把任务拆小一点再发)";
@@ -6221,7 +6529,7 @@ async function handleMessage(account, ctx, to, text, imageItems, createTimeMs, l
6221
6529
  answer = await runWithRetry(text, images);
6222
6530
  } catch (e2) {
6223
6531
  ok = false;
6224
- answer = brainErrorMessage(e2);
6532
+ answer = brainErrorMessage(e2, getAgent("brain").name);
6225
6533
  } finally {
6226
6534
  clearInterval(keepAlive);
6227
6535
  clearInterval(heartbeat);
@@ -6624,6 +6932,7 @@ async function runDaemon() {
6624
6932
  process.exit(78);
6625
6933
  }
6626
6934
  log(`brain=${agent.name} bin=${agent.bin} args=[${agent.args.join(" ")}] shell=${agent.shell} (source: ${agent.source})`);
6935
+ writeBrainState(DATA_DIR, agent);
6627
6936
  const wagent = getAgent("worker");
6628
6937
  if (wagent.name !== agent.name) log(`worker=${wagent.name} bin=${wagent.bin} args=[${wagent.args.join(" ")}] (source: ${wagent.source})`);
6629
6938
  const workerProblem = agentProblem(wagent);
@@ -6914,9 +7223,7 @@ function watchdogStart(key, cliPath) {
6914
7223
  async function checkAgent(ag, timeoutMs = 6e4) {
6915
7224
  const started = Date.now();
6916
7225
  const home = fs.mkdtempSync(path.join(os.tmpdir(), "wemind-brain-check-"));
6917
- const env = { ...process.env };
6918
- if (ag.homeEnvVar) env[ag.homeEnvVar] = home;
6919
- for (const [k, v] of Object.entries(ag.env ?? {})) env[k] = v.replaceAll("%HOME%", home);
7226
+ const env = agentEnv(ag, home);
6920
7227
  const cfgDir = path.join(home, ag.configSubdir);
6921
7228
  for (const f of [...ag.credentialFiles, ...ag.identityFiles]) try {
6922
7229
  const src = path.join(ag.realHome ?? "", f);
@@ -6994,22 +7301,35 @@ async function checkAgent(ag, timeoutMs = 6e4) {
6994
7301
  protocolVersion: 1,
6995
7302
  clientCapabilities: {}
6996
7303
  });
7304
+ const s = await req("session/new", {
7305
+ cwd: home,
7306
+ mcpServers: []
7307
+ });
7308
+ let modeNote = "";
7309
+ if (ag.sessionMode && s?.sessionId) try {
7310
+ await req("session/set_mode", {
7311
+ sessionId: s.sessionId,
7312
+ modeId: ag.sessionMode
7313
+ });
7314
+ modeNote = `,mode=${ag.sessionMode}`;
7315
+ } catch (e) {
7316
+ modeNote = `,但 session/set_mode ${ag.sessionMode} 失败(${e instanceof Error ? e.message : String(e)})——免审批模式可能不可用,回合仍可跑但会按 agent 自己的审批流程走`;
7317
+ }
6997
7318
  return {
6998
7319
  ok: true,
6999
- detail: `ACP 握手成功(initialize + session/new,sessionId=${(await req("session/new", {
7000
- cwd: home,
7001
- mcpServers: []
7002
- }))?.sessionId ?? "?"})`,
7320
+ detail: `ACP 握手成功(initialize + session/new,sessionId=${s?.sessionId ?? "?"}${modeNote})`,
7003
7321
  ms: Date.now() - started
7004
7322
  };
7005
7323
  } catch (e) {
7006
7324
  const all = stderrTail.join("").trim();
7007
7325
  const lines = all.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
7008
7326
  const head = lines.find((l) => /^(Error|YAMLException)\b/.test(l)) ?? lines.find((l) => /failed to parse|YAMLException/i.test(l)) ?? lines[0];
7327
+ const msg = e instanceof Error ? e.message : String(e);
7328
+ const hint = authHint(ag.name, msg);
7009
7329
  const tail = all.slice(-400);
7010
7330
  return {
7011
7331
  ok: false,
7012
- detail: `${e instanceof Error ? e.message : String(e)}${head ? `\n stderr: ${head}` : ""}${tail && !head?.includes(tail.trim()) ? `\n …${tail}` : ""}`,
7332
+ detail: `${msg}${head ? `\n stderr: ${head}` : ""}${tail && !head?.includes(tail.trim()) ? `\n …${tail}` : ""}${hint ? `\n 提示:${hint}` : ""}`,
7013
7333
  ms: Date.now() - started
7014
7334
  };
7015
7335
  } finally {
@@ -7284,7 +7604,8 @@ function stopDaemon(key) {
7284
7604
  "daemon.pid",
7285
7605
  "brain.pid",
7286
7606
  "worker.pid",
7287
- "workers.pid.json"
7607
+ "workers.pid.json",
7608
+ BRAIN_RUNTIME_FILE
7288
7609
  ]) try {
7289
7610
  fs.unlinkSync(path.join(dir, f));
7290
7611
  } catch {}
@@ -7442,6 +7763,7 @@ function cmdList() {
7442
7763
  bpid: brec.pid ?? readPid(dir, "brain.pid"),
7443
7764
  brainMarkers: brec.markers ?? brainPidMarkers(ag.bin, ag.args),
7444
7765
  brainAgent: brec.agent,
7766
+ brainRuntime: readBrainState(dir),
7445
7767
  workers: readPidRecords(path.join(dir, "workers.pid.json")).map((w) => ({
7446
7768
  pid: w.pid,
7447
7769
  agent: w.agent,
@@ -7458,11 +7780,12 @@ function cmdList() {
7458
7780
  ]));
7459
7781
  const aliveOursIn = (pid, markers) => !!pid && matchesMarkers(lines.get(pid), markers);
7460
7782
  return entries.map((e) => {
7461
- const running = aliveOursIn(e.dpid, CLI_PID_MARKERS) ? `运行中(pid ${e.dpid})` : "已停止";
7783
+ const runningNow = aliveOursIn(e.dpid, CLI_PID_MARKERS);
7784
+ const running = runningNow ? `运行中(pid ${e.dpid})` : "已停止";
7462
7785
  const brainPid = aliveOursIn(e.bpid, e.brainMarkers) ? ` brain(pid ${e.bpid}${e.brainAgent ? ` ${e.brainAgent}` : ""})` : "";
7463
7786
  const live = e.workers.filter((w) => aliveOursIn(w.pid, w.markers));
7464
7787
  const pool = live.length ? ` pool[${live.map((w) => `${w.agent}${w.taskId ? `#${w.taskId}` : ""}:${w.state}`).join(", ")}]` : "";
7465
- return `- ${e.a.key}${e.a.key === def ? " [默认]" : ""} label=${e.a.label} botId=${e.a.botId} agent=${agentLabel(e.dir)} ${running}${brainPid}${pool}`;
7788
+ return `- ${e.a.key}${e.a.key === def ? " [默认]" : ""} label=${e.a.label} botId=${e.a.botId} agent=${agentLabel(e.dir, runningNow ? e.brainRuntime : null)} ${running}${brainPid}${pool}`;
7466
7789
  }).join("\n");
7467
7790
  }
7468
7791
  const BRAIN_SRC_LABEL = {
@@ -7471,32 +7794,46 @@ const BRAIN_SRC_LABEL = {
7471
7794
  auto: "自动探测(未配置)"
7472
7795
  };
7473
7796
  /** list 用:账号「配置 / 自动探测」得到的大脑标签(不看当前 shell 的 env,避免误导)。 */
7474
- function agentLabel(dir) {
7797
+ function configAgentLabel(dir) {
7475
7798
  const cfg = loadBrainConfig(dir);
7476
7799
  if (cfg?.agent && cfg.agent.toLowerCase() !== "auto") return cfg.agent;
7477
7800
  const picked = AUTO_PROBE_ORDER.find((k) => presetAvailable(k));
7478
7801
  return picked ? `${picked}(自动)` : "(无可用)";
7479
7802
  }
7803
+ /**
7804
+ * list 用的 agent 列:daemon **在跑**时优先显示它实际生效的大脑(`brain-runtime.json`,
7805
+ * 来自 daemon 自己的解析,含 `WEMIND_BRAIN` 覆盖/热切换结果);daemon 没跑时仍只显示配置/自动探测。
7806
+ * 运行时与配置不一致时把两者都写出来,避免「看着是 deepseek、实际在跑 cline」。
7807
+ */
7808
+ function agentLabel(dir, runtime = null) {
7809
+ const cfg = configAgentLabel(dir);
7810
+ if (!runtime?.agent?.trim()) return cfg;
7811
+ const name = runtime.agent.trim();
7812
+ if (name === cfg.replace(/([^)]*)$/, "")) return cfg;
7813
+ return `${name}(运行,来源 ${runtime.source};配置 ${cfg})`;
7814
+ }
7480
7815
  /** `wemind brain`(show):打印某账号大脑的「配置」与「生效」情况。 */
7481
7816
  function describeBrain(key) {
7482
7817
  const cfg = loadBrainConfig(accountDir(key));
7483
7818
  const ag = resolveAgent(cfg);
7484
7819
  const problem = agentProblem(ag);
7485
- const cfgLine = cfg ? `brain.json → agent=${cfg.agent}${cfg.bin ? ` bin=${cfg.bin}` : ""}${cfg.args ? ` args=[${cfg.args.join(" ")}]` : ""}${cfg.shell !== void 0 ? ` shell=${cfg.shell}` : ""}${cfg.model ? ` model=${cfg.model}` : ""}` : "(未设置 brain.json)";
7820
+ const cfgLine = cfg ? `brain.json → agent=${cfg.agent}${cfg.bin ? ` bin=${cfg.bin}` : ""}${cfg.args ? ` args=[${cfg.args.join(" ")}]` : ""}${cfg.extraArgs ? ` extra_args=[${cfg.extraArgs.join(" ")}]` : ""}${cfg.shell !== void 0 ? ` shell=${cfg.shell}` : ""}${cfg.model ? ` model=${cfg.model}` : ""}` : "(未设置 brain.json)";
7486
7821
  const lines = [
7487
7822
  `账号 ${key}`,
7488
7823
  ` 配置:${cfgLine}`,
7489
7824
  ` 请求:${ag.requestedKey} 来源:${BRAIN_SRC_LABEL[ag.source]}`,
7490
- ` 生效:name=${ag.name} bin=${ag.bin} args=[${ag.args.join(" ")}] shell=${ag.shell} home_env=${ag.homeEnvVar ?? "(无)"} model=${ag.model ?? "(agent 默认)"}`,
7825
+ ` 生效:name=${ag.name} bin=${ag.bin} args=[${ag.args.join(" ")}] shell=${ag.shell} home_env=${ag.homeEnvVar ?? "(无)"} model=${ag.model ?? "(agent 默认)"} session_mode=${ag.sessionMode ?? "(无)"}`,
7491
7826
  ` 可用:${problem ? `❌ ${problem}` : `✅ ${ag.binPath ?? ag.bin}`}`,
7492
7827
  ` 可选 agent:${Object.keys(ACP_AGENT_PRESETS).join("、")}`,
7493
7828
  ` 自动探测顺序:${AUTO_PROBE_ORDER.join(" → ")}`
7494
7829
  ];
7830
+ const argsWarn = acpArgsWarning(ag.name, ag.args, ag.binOverridden);
7831
+ if (argsWarn) lines.push(` ⚠️ ${argsWarn}`);
7495
7832
  const hint = ag.realProfile && ag.realHome && ag.inherit ? acpOverrideHint(ag.realHome, ag.realProfile) : null;
7496
7833
  if (hint) lines.push("", hint);
7497
7834
  return lines.join("\n");
7498
7835
  }
7499
- /** `wemind brain set <agent 名|auto>`:把选择按账号写入磁盘(可选 --bin/--args/--shell/--model;--restart 立即重启 daemon)。 */
7836
+ /** `wemind brain set <agent 名|auto>`:把选择按账号写入磁盘(可选 --bin/--args/--extra-args/--shell/--model;--restart 立即重启 daemon)。 */
7500
7837
  async function cmdBrainSet(agentName, key, opts) {
7501
7838
  const agent = agentName.trim().toLowerCase();
7502
7839
  if (agent !== "auto" && !ACP_AGENT_PRESETS[agent]) throw new Error(`未知 agent「${agentName}」。可选:auto、${Object.keys(ACP_AGENT_PRESETS).join("、")}(自定义二进制用 --bin 搭配任一 agent)`);
@@ -7505,7 +7842,7 @@ async function cmdBrainSet(agentName, key, opts) {
7505
7842
  if (agent === "auto") throw new Error("`auto` 是「自动探测」,不绑定具体 agent,不能配 --model;请指定具体 agent(如 brain set deepseek --model …)");
7506
7843
  }
7507
7844
  const dir = accountDir(key);
7508
- const customized = opts.bin !== void 0 || opts.args !== void 0 || opts.shell !== void 0 || opts.model !== void 0;
7845
+ const customized = opts.bin !== void 0 || opts.args !== void 0 || opts.extraArgs !== void 0 || opts.shell !== void 0 || opts.model !== void 0;
7509
7846
  if (!opts.restart && !customized && bridgeReady(key)) {
7510
7847
  const r = await cmdBrainSwitch(agent, key, false);
7511
7848
  return r.startsWith("❌") ? `${r}\n(brain.json **没有被改写**:换大脑失败就不写入磁盘——这是「失败就退回原来那个」的代价。想强制写入再重启:wemind brain set ${agent} --restart)` : r;
@@ -7513,17 +7850,24 @@ async function cmdBrainSet(agentName, key, opts) {
7513
7850
  const patch = { agent };
7514
7851
  if (opts.bin) patch.bin = opts.bin;
7515
7852
  if (opts.args !== void 0) patch.args = opts.args.trim() ? opts.args.trim().split(/\s+/) : [];
7853
+ if (opts.extraArgs !== void 0) patch.extraArgs = opts.extraArgs.trim() ? opts.extraArgs.trim().split(/\s+/) : [];
7516
7854
  if (opts.shell !== void 0) patch.shell = opts.shell;
7517
7855
  if (opts.model !== void 0) patch.model = opts.model.trim();
7856
+ const before = loadBrainConfig(dir);
7518
7857
  const cfg = updateBrainConfig(patch, dir);
7519
- const lines = [`已设置 ${key} 的大脑:agent=${agent}${cfg.bin ? ` bin=${cfg.bin}` : ""}${cfg.args ? ` args=[${cfg.args.join(" ")}]` : ""}${cfg.model ? ` model=${cfg.model}` : ""}`, `配置文件:${brainConfigPath(dir)}`];
7858
+ const lines = [`已设置 ${key} 的大脑:agent=${agent}${cfg.bin ? ` bin=${cfg.bin}` : ""}${cfg.args ? ` args=[${cfg.args.join(" ")}]` : ""}${cfg.extraArgs ? ` extra_args=[${cfg.extraArgs.join(" ")}]` : ""}${cfg.model ? ` model=${cfg.model}` : ""}`, `配置文件:${brainConfigPath(dir)}`];
7859
+ if (opts.args !== void 0) {
7860
+ const binOverridden = opts.bin !== void 0 || !!before?.presets?.[agent]?.bin || before?.agent?.trim().toLowerCase() === agent && !!before?.bin;
7861
+ const warn = acpArgsWarning(agent, cfg.args ?? [], binOverridden);
7862
+ if (warn) lines.push(`⚠️ ${warn}`);
7863
+ }
7520
7864
  if (opts.restart) if (aliveOurs(readPid(dir, "daemon.pid"))) {
7521
7865
  startDaemon(key);
7522
7866
  lines.push(`已重启 daemon(账号 ${key})。`);
7523
7867
  } else lines.push(`daemon 未在运行,未重启(用 wemind start ${key} 启动)。`);
7524
7868
  else if (customized && bridgeReady(key)) {
7525
- const viaSwitch = opts.bin !== void 0 || opts.args !== void 0 || opts.model !== void 0;
7526
- lines.push(`(带 --bin/--args/--shell/--model 的改动不能由 brain set 当场应用:控制桥只认 agent 名。重启后生效——wemind stop ${key} && wemind start ${key}${viaSwitch ? `;不想重启的话,接着执行 wemind brain switch ${agent} 也能当场应用(它按 name/bin/args/model 判断是不是同一个,改过就会真切换)` : ""})`);
7869
+ const viaSwitch = opts.bin !== void 0 || opts.args !== void 0 || opts.extraArgs !== void 0 || opts.model !== void 0;
7870
+ lines.push(`(带 --bin/--args/--extra-args/--shell/--model 的改动不能由 brain set 当场应用:控制桥只认 agent 名。重启后生效——wemind stop ${key} && wemind start ${key}${viaSwitch ? `;不想重启的话,接着执行 wemind brain switch ${agent} 也能当场应用(它按 name/bin/args/model 判断是不是同一个,改过就会真切换)` : ""})`);
7527
7871
  } else if (daemonAlive(key)) lines.push(`(daemon 在跑但不是当前版本(不认控制桥):盘已写好,重启一次才生效——wemind stop ${key} && wemind start ${key})`);
7528
7872
  else lines.push(`(daemon 需重启才生效:wemind start ${key})`);
7529
7873
  return lines.join("\n");
@@ -7546,6 +7890,7 @@ async function cmdBrain(rest) {
7546
7890
  const FLAGS = /* @__PURE__ */ new Set([
7547
7891
  "--bin",
7548
7892
  "--args",
7893
+ "--extra-args",
7549
7894
  "--shell",
7550
7895
  "--no-shell",
7551
7896
  "--model",
@@ -7568,6 +7913,7 @@ async function cmdBrain(rest) {
7568
7913
  };
7569
7914
  const bin = takeFlag("--bin");
7570
7915
  const argsOverride = takeFlag("--args");
7916
+ const extraArgsOverride = takeFlag("--extra-args");
7571
7917
  const model = takeFlag("--model");
7572
7918
  const restart = argv.includes("--restart");
7573
7919
  if (restart) argv.splice(argv.indexOf("--restart"), 1);
@@ -7578,7 +7924,7 @@ async function cmdBrain(rest) {
7578
7924
  }
7579
7925
  while (argv.includes("--worker")) argv.splice(argv.indexOf("--worker"), 1);
7580
7926
  const unknown = argv.filter((x) => x.startsWith("--"));
7581
- if (unknown.length) throw new Error(`未知参数:${unknown.join(" ")}(可用:--bin --args --shell --no-shell --model --restart)`);
7927
+ if (unknown.length) throw new Error(`未知参数:${unknown.join(" ")}(可用:--bin --args --extra-args --shell --no-shell --model --restart)`);
7582
7928
  const pos = argv.filter((x) => x && !x.startsWith("--"));
7583
7929
  const first = (pos[0] || "").toLowerCase();
7584
7930
  if (first && !(/* @__PURE__ */ new Set([
@@ -7593,16 +7939,17 @@ async function cmdBrain(rest) {
7593
7939
  switch (sub) {
7594
7940
  case "show": return describeBrain(pickKey(pos[1]));
7595
7941
  case "set":
7596
- if (!pos[1]) throw new Error("用法: brain set <agent 名|auto> [--bin X] [--args \"…\"] [--shell|--no-shell] [--model X|auto] [--restart] [<key|label>]");
7942
+ if (!pos[1]) throw new Error("用法: brain set <agent 名|auto> [--bin X] [--args \"…\"] [--extra-args \"…\"] [--shell|--no-shell] [--model X|auto] [--restart] [<key|label>]");
7597
7943
  return await cmdBrainSet(pos[1], pickKey(pos[2]), {
7598
7944
  bin,
7599
7945
  args: argsOverride,
7946
+ extraArgs: extraArgsOverride,
7600
7947
  shell,
7601
7948
  model,
7602
7949
  restart
7603
7950
  });
7604
7951
  case "switch": {
7605
- if (bin !== void 0 || argsOverride !== void 0 || shell !== void 0 || model !== void 0) throw new Error("`brain switch` 只接受 agent 名(换大脑是「换一个已装好的 agent」,不能带 --bin/--args/--model)。\n需要自定义二进制用 `brain set <agent 名> --bin …`(那是配置变更,只写入磁盘——之后 `wemind brain switch <同名 agent>` 可当场应用,或重启 daemon);换模型用 `brain set <agent 名> --model …`。");
7952
+ if (bin !== void 0 || argsOverride !== void 0 || extraArgsOverride !== void 0 || shell !== void 0 || model !== void 0) throw new Error("`brain switch` 只接受 agent 名(换大脑是「换一个已装好的 agent」,不能带 --bin/--args/--extra-args/--model)。\n需要自定义二进制用 `brain set <agent 名> --bin …`(那是配置变更,只写入磁盘——之后 `wemind brain switch <同名 agent>` 可当场应用,或重启 daemon);追加参数用 `brain set <agent 名> --extra-args …`;换模型用 `brain set <agent 名> --model …`。");
7606
7953
  if (!pos[1]) throw new Error("用法: brain switch <agent 名|auto> [--worker] [<key|label>]");
7607
7954
  const worker = rest.includes("--worker");
7608
7955
  return await cmdBrainSwitch(pos[1], pickKey(pos[2]), worker);
@@ -8110,8 +8457,8 @@ function helpText() {
8110
8457
  list 列出账号、别名、运行状态(含 brain 进程与 worker 池)
8111
8458
  start [key|label] 启动某账号的 daemon(省略 = 默认账号)
8112
8459
  stop [key|label] 停止某账号的 daemon(连带 brain 与全部 worker 进程树;省略 = 默认账号)
8113
- brain set <agent 名|auto> [--bin X] [--args "…"] [--shell|--no-shell] [--model X|auto] [--restart] [<key|label>]
8114
- 改账号大脑**配置**(写 brain.json;只给 agent 名且 daemon 认桥时当场应用;带 --bin/--args/--shell/--model 只写盘——除 --shell 外可用同名 brain switch 当场应用)
8460
+ brain set <agent 名|auto> [--bin X] [--args "…"] [--extra-args "…"] [--shell|--no-shell] [--model X|auto] [--restart] [<key|label>]
8461
+ 改账号大脑**配置**(写 brain.json;只给 agent 名且 daemon 认桥时当场应用;带 --bin/--args/--extra-args/--shell/--model 只写盘——除 --shell 外可用同名 brain switch 当场应用;--extra-args 追加在预设参数后,不会漏掉 ACP 子命令)
8115
8462
  brain unset|check|show [<key|label>] 清除配置 / 真机 ACP 自检 / 查看配置与生效情况
8116
8463
  logout <key|label> 停 daemon + 通知下线 + 清凭证(保留目录,可重登)
8117
8464
  delete <key|label> 停 daemon + 删除目录 + 移除注册