@wemind/cli 1.0.0 → 1.1.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/README.md +121 -2
- package/dist/cli.js +188 -33
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,7 +16,14 @@ wemind version # → wemind 1.0.0 (@wemind/cli, node 24.x, win32-x64)
|
|
|
16
16
|
npx -y @wemind/cli list
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
要求:**Node.js >= 20**;想让它真的会「思考」,需要 PATH 上有一个支持 **ACP** 的 agent CLI(`kiro-cli` / `opencode` / 桌面端 `dsh` 等;未配置时自动探测 `kiro → opencode → gemini → deepseek`)。
|
|
19
|
+
要求:**Node.js >= 20**;想让它真的会「思考」,需要 PATH 上有一个支持 **ACP** 的 agent CLI(`kiro-cli` / `opencode` / 桌面端 `dsh` / `qodercli` 等;未配置时自动探测 `kiro → opencode → gemini → deepseek → qoder`)。
|
|
20
|
+
|
|
21
|
+
## 升级
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm i -g @wemind/cli@latest
|
|
25
|
+
wemind stop <账号> && wemind start <账号> # 已在运行的 daemon 不会自动加载新代码
|
|
26
|
+
```
|
|
20
27
|
|
|
21
28
|
## 三步上手
|
|
22
29
|
|
|
@@ -53,7 +60,7 @@ wemind brain check # ③ 验证大脑能说 ACP(initialize +
|
|
|
53
60
|
## 数据与安全
|
|
54
61
|
|
|
55
62
|
- 所有状态在 `~/.wemind/<账号>/`(可用 `WEMIND_HOME` 改根目录):`account.json` 登录凭证、`state.json` 游标、`allowFrom.json` 授权清单、`brain-home/` 大脑沙箱、`daemon.log` 日志。
|
|
56
|
-
- ⚠️ **daemon
|
|
63
|
+
- ⚠️ **daemon 会让大脑免确认执行工具**(kiro 预设 `--trust-all-tools`;qoder 预设 `--yolo`):能给这个 bot 发消息的人,即可让大脑在本机执行任意工具(shell / 写文件)。唯一安全闸是**发件人授权(allowFrom)**:扫码登录者自动授权,**勿清空**。
|
|
57
64
|
- `account.json` 里 bot token 是明文(POSIX 上 chmod 600;Windows 请用目录权限或磁盘加密)。
|
|
58
65
|
- 同一个 bot token **只能有一个消费者**:`wemind` daemon 与 `@wemind/wechat-mcp` 二选一,否则互相抢消息。
|
|
59
66
|
|
|
@@ -78,10 +85,122 @@ wemind brain check # ③ 验证大脑能说 ACP(initialize +
|
|
|
78
85
|
## 文档
|
|
79
86
|
|
|
80
87
|
- 完整文档:<https://github.com/imwangpan/WeMind>
|
|
88
|
+
- 新手一步步教程:[docs/新手教程.md](https://github.com/imwangpan/WeMind/blob/main/docs/新手教程.md)
|
|
89
|
+
- 发布与版本规则:[docs/发布-wemind-npm.md](https://github.com/imwangpan/WeMind/blob/main/docs/发布-wemind-npm.md)
|
|
81
90
|
- AI 协作者入口:[AGENTS.md](https://github.com/imwangpan/WeMind/blob/main/AGENTS.md)
|
|
82
91
|
- 接入其它 ACP agent:[docs/接入其它-ACP-Agent.md](https://github.com/imwangpan/WeMind/blob/main/docs/接入其它-ACP-Agent.md)
|
|
92
|
+
- Qoder 接入:[docs/接入-Qoder.md](https://github.com/imwangpan/WeMind/blob/main/docs/接入-Qoder.md)
|
|
83
93
|
- Daemon 常驻原理:[docs/repowiki/10-部署与运维.md](https://github.com/imwangpan/WeMind/blob/main/docs/repowiki/10-部署与运维.md)
|
|
84
94
|
|
|
95
|
+
## 维护者:发布新版本
|
|
96
|
+
|
|
97
|
+
本包(包名 `@wemind/cli`,命令 `wemind`)通过 GitHub Actions([`.github/workflows/wemind-publish.yml`](../../.github/workflows/wemind-publish.yml))发布到 npm。**下面的命令都在 WeMind 仓库根目录运行**,不是在 `packages/wechat-daemon` 子目录。
|
|
98
|
+
|
|
99
|
+
> ⚠️ **本包比 `@wemind/wechat-mcp` 多一处要改**:版本号除了 `package.json` 和 `package-lock.json`,还有源码常量 `src/version.ts` 的 `CLI_VERSION`(`wemind version` 的输出就来自它)。漏改会让单测与 CI 直接失败——仓库自带 `npm run check:version` 专门挡这个。
|
|
100
|
+
|
|
101
|
+
### 第一步:判断要不要发版
|
|
102
|
+
|
|
103
|
+
| 你改了什么 | 需要发版吗 |
|
|
104
|
+
| --- | --- |
|
|
105
|
+
| `packages/wechat-daemon/src/**` 或 `packages/wechat-ilink-core/src/**`(内核会内联进来) | ✅ 要 |
|
|
106
|
+
| 只改了 `*.md` 文档 | ❌ 不用(CI 仍跑检查,但版本没变会跳过发布) |
|
|
107
|
+
| 只改了 `packages/wechat-mcp/**` | ❌ 与本包无关,发的是另一个包 |
|
|
108
|
+
|
|
109
|
+
> 同一个版本号**不能重复发布**。所以「有没有改代码」不是关键,「有没有升版本号」才是——没升就只会跑检查、不发布。
|
|
110
|
+
|
|
111
|
+
### 第二步:选版本号
|
|
112
|
+
|
|
113
|
+
| 参数 | 示例 | 什么时候用 |
|
|
114
|
+
| --- | --- | --- |
|
|
115
|
+
| `patch` | `1.0.0` → `1.0.1` | Bug 修复、兼容性改动(**最常用**) |
|
|
116
|
+
| `minor` | `1.0.0` → `1.1.0` | 新增向后兼容的能力(如新增内置大脑预设、新增子命令) |
|
|
117
|
+
| `major` | `1.0.0` → `2.0.0` | 不兼容改动(如删子命令、改数据目录布局、改 CLI 参数) |
|
|
118
|
+
|
|
119
|
+
### 第三步:升版本号(三处必须同步)
|
|
120
|
+
|
|
121
|
+
**方式 A:用 npm 改 `package.json` + lock,再手改 `version.ts`(推荐)**
|
|
122
|
+
|
|
123
|
+
```powershell
|
|
124
|
+
# ① 改 package.json 与 package-lock.json
|
|
125
|
+
npm version patch --workspace @wemind/cli --no-git-tag-version
|
|
126
|
+
|
|
127
|
+
# ② 手工同步源码常量(npm 不会帮你改这个)
|
|
128
|
+
# 编辑 packages/wechat-daemon/src/version.ts:
|
|
129
|
+
# export const CLI_VERSION = "1.0.1"; ← 与上一步的版本一致
|
|
130
|
+
|
|
131
|
+
# ③ 自检三处是否一致(不一致会 exit 1)
|
|
132
|
+
npm run check:version
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
- `--workspace @wemind/cli`:只动本包,不改根项目、mcp、core 的版本;
|
|
136
|
+
- `--no-git-tag-version`:不自动创建 commit 或 tag,改完由你自己检查提交。
|
|
137
|
+
|
|
138
|
+
**方式 B:全部手动**
|
|
139
|
+
|
|
140
|
+
1. 编辑 `packages/wechat-daemon/package.json` 的 `"version"`;
|
|
141
|
+
2. 编辑 `packages/wechat-daemon/src/version.ts` 的 `CLI_VERSION`;
|
|
142
|
+
3. 回到仓库根目录刷新锁文件:
|
|
143
|
+
|
|
144
|
+
```powershell
|
|
145
|
+
npm install --package-lock-only --ignore-scripts
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
> 不要手动编辑 `package-lock.json`。上面这条只重算 lockfile 元数据,不重装 `node_modules`、不跑依赖的安装脚本。
|
|
149
|
+
|
|
150
|
+
### 第四步:本地校验
|
|
151
|
+
|
|
152
|
+
```powershell
|
|
153
|
+
npm run check:version # 包名 + 三处版本一致(package.json / version.ts / lock)
|
|
154
|
+
npm run lint && npm run typecheck && npm run build && npm test
|
|
155
|
+
npm pack --dry-run --workspace @wemind/cli # 应看到 4 个文件:cli.js / cli.js.map / README.md / package.json
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
> ⚠️ **`npm publish` 不会自动 build**。CI 会 build,但本地 `npm pack` 前必须先 `npm run build`,否则打出来是旧产物(`dist/` 在 `.gitignore` 里)。
|
|
159
|
+
>
|
|
160
|
+
> `prepack` 守卫(`scripts/prepack-guard.mjs`)会兜底拒绝:产物目录为空、`bin` 缺失、或声明了 sourcemap 却没把 `.map` 带进包。
|
|
161
|
+
|
|
162
|
+
### 第五步:提交并推送
|
|
163
|
+
|
|
164
|
+
```powershell
|
|
165
|
+
git add packages/wechat-daemon/package.json packages/wechat-daemon/src/version.ts package-lock.json
|
|
166
|
+
git add packages/wechat-daemon/src packages/wechat-ilink-core/src # 有源码改动时
|
|
167
|
+
git commit -m "chore: release @wemind/cli 1.0.1"
|
|
168
|
+
git push origin main
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
推送到 `main` 后,工作流依次执行:
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
check-version(三处一致)→ npm ci → lint → format:check → typecheck → build → test
|
|
175
|
+
→ npm pack --dry-run(prepack 守卫确认包内容)
|
|
176
|
+
→ 把 tarball 真装到临时目录跑 wemind version / help(装包冒烟)
|
|
177
|
+
→ 检查 npm 上是否已存在该版本
|
|
178
|
+
→ 不存在则 npm publish(带 provenance 来源证明);已存在则跳过并输出 notice
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**看到 `Publish to npm` 显示 skipped 是正常的**:说明这个版本已发过(比如你只改了文档、没升版本)。想真正发版,回到第二步升版本号。
|
|
182
|
+
|
|
183
|
+
### 第六步:发布后验收
|
|
184
|
+
|
|
185
|
+
```powershell
|
|
186
|
+
npm view @wemind/cli version # 应显示新版本号
|
|
187
|
+
npm i -g @wemind/cli@latest && wemind version # → wemind <新版本> (@wemind/cli, node …, <platform>-<arch>)
|
|
188
|
+
wemind list # 应能正确识别在跑的账号与进程
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
已装用户升级:`npm i -g @wemind/cli@latest`,然后 `wemind stop <账号> && wemind start <账号>` ——**正在运行的 daemon 不会自动加载新代码**。
|
|
192
|
+
|
|
193
|
+
### 常见坑
|
|
194
|
+
|
|
195
|
+
| 现象 | 原因 | 处理 |
|
|
196
|
+
| --- | --- | --- |
|
|
197
|
+
| CI 在 `check-version` 就红了 | 只改了 `package.json`,没同步 `version.ts` | 改 `CLI_VERSION` 后重推 |
|
|
198
|
+
| CI 绿了但 npm 上没新版本 | 版本号没升,被跳过 | 升版本号后再推 |
|
|
199
|
+
| CI 报 lock 版本不一致 | 没跑 `npm install --package-lock-only` | 补跑第三步 |
|
|
200
|
+
| `npm pack` 出来是空壳 | 没先 `npm run build` | 先 build 再 pack |
|
|
201
|
+
| `prepack` 守卫拒绝发布 | 产物为空 / `bin` 缺失 / 声明了 sourcemap 但 `.map` 不在包里 | 按守卫提示补 build 或检查 `files` |
|
|
202
|
+
| `npm publish` 报 403 / EOTP | 本地直发需要交互式 2FA | **不要本地发**,走 CI(Trusted Publisher 已配好) |
|
|
203
|
+
|
|
85
204
|
## License
|
|
86
205
|
|
|
87
206
|
MIT
|
package/dist/cli.js
CHANGED
|
@@ -940,7 +940,7 @@ const ACP_AGENT_PRESETS = {
|
|
|
940
940
|
realHome: path.join(os.homedir(), ".dsh"),
|
|
941
941
|
inheritDirs: ["skills"],
|
|
942
942
|
inheritFiles: ["AGENTS.md"],
|
|
943
|
-
|
|
943
|
+
credentialFiles: [".credentials.yaml"],
|
|
944
944
|
restore: "resume",
|
|
945
945
|
firstFrameMs: 6e4,
|
|
946
946
|
heartbeatMs: 6e4,
|
|
@@ -948,6 +948,29 @@ const ACP_AGENT_PRESETS = {
|
|
|
948
948
|
model: "[\"deepseek-account\",\"deepseek-flash\"]",
|
|
949
949
|
mcpViaAcp: true
|
|
950
950
|
},
|
|
951
|
+
qoder: {
|
|
952
|
+
name: "qoder",
|
|
953
|
+
bin: "qodercli",
|
|
954
|
+
binWin: "qodercli.exe",
|
|
955
|
+
fallbackBinPaths: process.platform === "win32" ? [path.join(os.homedir(), ".qoder", "bin", "qodercli", "qodercli.exe")] : [],
|
|
956
|
+
args: ["--acp", "--yolo"],
|
|
957
|
+
env: { QODER_CONFIG_DIR: `%HOME%${path.sep}qoder` },
|
|
958
|
+
realHome: path.join(os.homedir(), ".qoder"),
|
|
959
|
+
inheritDirs: [
|
|
960
|
+
"memory",
|
|
961
|
+
"skills",
|
|
962
|
+
"agents",
|
|
963
|
+
"commands"
|
|
964
|
+
],
|
|
965
|
+
inheritFiles: ["AGENTS.md"],
|
|
966
|
+
configSubdir: "qoder",
|
|
967
|
+
mcpFormat: "kiro",
|
|
968
|
+
mcpConfigFile: "settings.json",
|
|
969
|
+
catalogFile: "settings.json",
|
|
970
|
+
settingsBase: "settings.json",
|
|
971
|
+
credentialFiles: [".auth/user"],
|
|
972
|
+
identityFiles: [".auth/machine_id"]
|
|
973
|
+
},
|
|
951
974
|
"claude-code": {
|
|
952
975
|
name: "claude-code",
|
|
953
976
|
bin: "npx",
|
|
@@ -975,7 +998,8 @@ const AUTO_PROBE_ORDER = [
|
|
|
975
998
|
"kiro",
|
|
976
999
|
"opencode",
|
|
977
1000
|
"gemini",
|
|
978
|
-
"deepseek"
|
|
1001
|
+
"deepseek",
|
|
1002
|
+
"qoder"
|
|
979
1003
|
];
|
|
980
1004
|
/** 预设在该平台的「可执行文件名」(用于 PATH 探测)。 */
|
|
981
1005
|
function presetExe(preset) {
|
|
@@ -1015,16 +1039,17 @@ function resolveAgent(cfg) {
|
|
|
1015
1039
|
key = "kiro";
|
|
1016
1040
|
}
|
|
1017
1041
|
const preset = ACP_AGENT_PRESETS[key];
|
|
1042
|
+
const overrideCfg = envKey && cfg?.agent?.trim() && cfg.agent.trim().toLowerCase() !== requestedKey ? null : cfg;
|
|
1018
1043
|
const shellEnv = process.env.WEMIND_BRAIN_SHELL?.trim();
|
|
1019
|
-
const shell = shellEnv !== void 0 && shellEnv !== "" ? shellEnv === "1" || shellEnv.toLowerCase() === "true" :
|
|
1020
|
-
const explicitBin = process.env.WEMIND_BRAIN_BIN?.trim() || (preset.name === "kiro" ? process.env.KIRO_CLI_BIN?.trim() : void 0) ||
|
|
1044
|
+
const shell = shellEnv !== void 0 && shellEnv !== "" ? shellEnv === "1" || shellEnv.toLowerCase() === "true" : overrideCfg?.shell ?? preset.shell ?? false;
|
|
1045
|
+
const explicitBin = process.env.WEMIND_BRAIN_BIN?.trim() || (preset.name === "kiro" ? process.env.KIRO_CLI_BIN?.trim() : void 0) || overrideCfg?.bin?.trim() || "";
|
|
1021
1046
|
const exe = presetExe(preset);
|
|
1022
|
-
const binPath = explicitBin ? locateBin(explicitBin) : onPath(exe);
|
|
1047
|
+
const binPath = explicitBin ? locateBin(explicitBin) : onPath(exe) ?? (preset.fallbackBinPaths ?? []).find((p) => fs.existsSync(p)) ?? null;
|
|
1023
1048
|
const bin = explicitBin || (shell ? process.platform === "win32" ? exe : preset.bin : binPath || preset.bin);
|
|
1024
1049
|
const available = known && binPath !== null;
|
|
1025
1050
|
const argsOverride = process.env.WEMIND_BRAIN_ARGS?.trim();
|
|
1026
|
-
const args = argsOverride ? argsOverride.split(/\s+/) :
|
|
1027
|
-
const homeEnvVar = process.env.WEMIND_BRAIN_HOME_ENV?.trim() ||
|
|
1051
|
+
const args = argsOverride ? argsOverride.split(/\s+/) : overrideCfg?.args && overrideCfg.args.length ? overrideCfg.args : preset.args;
|
|
1052
|
+
const homeEnvVar = process.env.WEMIND_BRAIN_HOME_ENV?.trim() || overrideCfg?.homeEnvVar?.trim() || preset.homeEnvVar;
|
|
1028
1053
|
const inherit = (process.env.WEMIND_BRAIN_INHERIT ?? process.env.WECHAT_BRAIN_INHERIT ?? "1") !== "0";
|
|
1029
1054
|
const realHome = process.env.WEMIND_BRAIN_REAL_HOME?.trim() || process.env.WECHAT_REAL_KIRO_HOME?.trim() || preset.realHome;
|
|
1030
1055
|
const dirsEnv = process.env.WEMIND_BRAIN_INHERIT_DIRS?.trim();
|
|
@@ -1048,7 +1073,9 @@ function resolveAgent(cfg) {
|
|
|
1048
1073
|
firstFrameMs: preset.firstFrameMs,
|
|
1049
1074
|
heartbeatMs: preset.heartbeatMs,
|
|
1050
1075
|
images: preset.images ?? true,
|
|
1051
|
-
|
|
1076
|
+
credentialFiles: preset.credentialFiles ?? [],
|
|
1077
|
+
identityFiles: preset.identityFiles ?? [],
|
|
1078
|
+
settingsBase: preset.settingsBase,
|
|
1052
1079
|
model: preset.model,
|
|
1053
1080
|
mcpViaAcp: preset.mcpViaAcp ?? false,
|
|
1054
1081
|
requestedKey,
|
|
@@ -1065,7 +1092,7 @@ function resolveAgent(cfg) {
|
|
|
1065
1092
|
function agentProblem(ag) {
|
|
1066
1093
|
if (!ag.known) return `未知大脑预设「${ag.requestedKey}」。可选:${Object.keys(ACP_AGENT_PRESETS).join("、")}(自定义二进制可用 WEMIND_BRAIN_BIN 或 brain set --bin 指定)`;
|
|
1067
1094
|
if (ag.requestedKey === "auto" && !ag.available) return `自动探测未找到可用的大脑(已试:${AUTO_PROBE_ORDER.join("、")})。请安装其中之一,或运行 \`wemind brain set <预设>\``;
|
|
1068
|
-
if (!ag.available) return ag.name === "deepseek" ? `找不到 DeepSeek 的「${ag.bin}」。桌面端通常有 dsh.cmd;把它的绝对路径传给 \`wemind brain set deepseek --bin "<...>\\dsh.cmd"\`` : `找不到大脑「${ag.name}」的二进制「${ag.bin}」。安装它,或运行 \`wemind brain set <预设>\` 切换`;
|
|
1095
|
+
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 <预设>\` 切换`;
|
|
1069
1096
|
return null;
|
|
1070
1097
|
}
|
|
1071
1098
|
//#endregion
|
|
@@ -1306,22 +1333,32 @@ function readJson$1(file) {
|
|
|
1306
1333
|
}
|
|
1307
1334
|
}
|
|
1308
1335
|
/**
|
|
1309
|
-
* 把 agent 的凭据文件同步进沙箱(如 DSH 的 `.credentials.yaml`)。
|
|
1310
|
-
*
|
|
1336
|
+
* 把 agent 的凭据文件同步进沙箱(如 DSH 的 `.credentials.yaml`、Qoder 的 `.auth/user`)。
|
|
1337
|
+
* - `newer`:首次复制;真实 home 的 token 刷新后覆盖沙箱;沙箱自己刷新过则保留沙箱版本。
|
|
1338
|
+
* - `always`:内容不同即覆盖(标识类文件,如 Qoder 的 `.auth/machine_id`——agent 在缺凭据时会自造一个,
|
|
1339
|
+
* 它比真实文件新,「新者胜」会把错的留下,导致永远认证失败)。
|
|
1311
1340
|
* 这是敏感文件,仅放在账号沙箱内;Windows 无 POSIX 600,建议专用账号 + 目录 ACL。
|
|
1312
1341
|
*/
|
|
1313
|
-
function syncCredentialFile$1() {
|
|
1314
|
-
if (!AG$1.
|
|
1315
|
-
const src = path.join(AG$1.realHome,
|
|
1316
|
-
const dst = path.join(BRAIN_CONFIG_DIR,
|
|
1342
|
+
function syncCredentialFile$1(file, mode) {
|
|
1343
|
+
if (!AG$1.realHome) return;
|
|
1344
|
+
const src = path.join(AG$1.realHome, file);
|
|
1345
|
+
const dst = path.join(BRAIN_CONFIG_DIR, file);
|
|
1317
1346
|
try {
|
|
1318
1347
|
if (!fs.existsSync(src)) return;
|
|
1319
1348
|
fs.mkdirSync(path.dirname(dst), { recursive: true });
|
|
1349
|
+
if (mode === "always") {
|
|
1350
|
+
if (!(fs.existsSync(dst) && fs.readFileSync(src).equals(fs.readFileSync(dst)))) fs.copyFileSync(src, dst);
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1320
1353
|
const s = fs.statSync(src);
|
|
1321
1354
|
const d = fs.existsSync(dst) ? fs.statSync(dst) : null;
|
|
1322
1355
|
if (!d || s.mtimeMs > d.mtimeMs + 1e3) fs.copyFileSync(src, dst);
|
|
1323
1356
|
} catch {}
|
|
1324
1357
|
}
|
|
1358
|
+
function syncCredentials$1() {
|
|
1359
|
+
for (const f of AG$1.credentialFiles) syncCredentialFile$1(f, "newer");
|
|
1360
|
+
for (const f of AG$1.identityFiles) syncCredentialFile$1(f, "always");
|
|
1361
|
+
}
|
|
1325
1362
|
function loadCatalog$1() {
|
|
1326
1363
|
return readMcpServers(readJson$1(CATALOG$1), AG$1.mcpFormat);
|
|
1327
1364
|
}
|
|
@@ -1398,10 +1435,41 @@ function syncOpencodeBase$1() {
|
|
|
1398
1435
|
fs.mkdirSync(BRAIN_CONFIG_DIR, { recursive: true });
|
|
1399
1436
|
fs.writeFileSync(BRAIN_MCP_JSON, JSON.stringify(parsed, null, 2), "utf-8");
|
|
1400
1437
|
}
|
|
1438
|
+
/** Qoder 这类「设置与 MCP 同文件」的 agent:以真实 settings.json 为基底生成沙箱设置。
|
|
1439
|
+
* 剥离必须由沙箱权威决定的键:
|
|
1440
|
+
* - `mcpServers`:MCP 只能来自沙箱 curated(防 wechat 回环);
|
|
1441
|
+
* - `enabledPlugins`:插件可自带 MCP,且沙箱不继承 `plugins/`;
|
|
1442
|
+
* - `providers` / `model`:自定义供应商(BYOK)含明文密钥,`model` 又可能指向已剥离的供应商。
|
|
1443
|
+
* 并强制 `general.enableAutoUpdate=false`:否则每个沙箱都会各下一份约 210 MB 的 CLI。 */
|
|
1444
|
+
function settingsBase$1() {
|
|
1445
|
+
let parsed = null;
|
|
1446
|
+
try {
|
|
1447
|
+
parsed = JSON.parse(fs.readFileSync(path.join(AG$1.realHome, AG$1.settingsBase), "utf-8"));
|
|
1448
|
+
} catch {}
|
|
1449
|
+
const base = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? { ...parsed } : {};
|
|
1450
|
+
delete base.mcpServers;
|
|
1451
|
+
delete base.enabledPlugins;
|
|
1452
|
+
delete base.providers;
|
|
1453
|
+
delete base.model;
|
|
1454
|
+
const general = base.general;
|
|
1455
|
+
base.general = {
|
|
1456
|
+
...general && typeof general === "object" && !Array.isArray(general) ? general : {},
|
|
1457
|
+
enableAutoUpdate: false
|
|
1458
|
+
};
|
|
1459
|
+
return base;
|
|
1460
|
+
}
|
|
1461
|
+
function syncSettingsBase$1() {
|
|
1462
|
+
fs.mkdirSync(BRAIN_CONFIG_DIR, { recursive: true });
|
|
1463
|
+
fs.writeFileSync(BRAIN_MCP_JSON, JSON.stringify(settingsBase$1(), null, 2), "utf-8");
|
|
1464
|
+
}
|
|
1401
1465
|
/** 继承真实配置目录的「非 MCP」设置(模型/免确认等),让大脑与日常使用同款配置;mcp 段保持沙箱权威。
|
|
1402
1466
|
* `mcpViaAcp` 预设没有文件式 MCP/settings 可继承,跳过(模型走 preset.model + set_config_option)。 */
|
|
1403
1467
|
function syncSettings$1() {
|
|
1404
1468
|
if (AG$1.mcpViaAcp) return;
|
|
1469
|
+
if (AG$1.settingsBase && AG$1.realHome) {
|
|
1470
|
+
syncSettingsBase$1();
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1405
1473
|
if (!AG$1.inherit || !AG$1.realHome) return;
|
|
1406
1474
|
if (AG$1.mcpFormat === "opencode") syncOpencodeBase$1();
|
|
1407
1475
|
else syncKiroSettings$1();
|
|
@@ -1411,7 +1479,7 @@ function ensureBrainHome() {
|
|
|
1411
1479
|
syncSettings$1();
|
|
1412
1480
|
applySandbox();
|
|
1413
1481
|
mirrorRealKiro$1();
|
|
1414
|
-
|
|
1482
|
+
syncCredentials$1();
|
|
1415
1483
|
}
|
|
1416
1484
|
function addMcp(name) {
|
|
1417
1485
|
if (FORBIDDEN.has(name)) return `不允许加载「${name}」(防回环)。`;
|
|
@@ -1480,6 +1548,43 @@ function spawnAgent(agent, opts = {}) {
|
|
|
1480
1548
|
});
|
|
1481
1549
|
}
|
|
1482
1550
|
//#endregion
|
|
1551
|
+
//#region src/session-map.ts
|
|
1552
|
+
/**
|
|
1553
|
+
* brainstorm/worker 共用的 session 映射键规则:`<agent>:<key>`。
|
|
1554
|
+
* 换大脑后各 agent 的会话互不覆盖,切回原大脑仍能 session/load 回原上下文。
|
|
1555
|
+
*/
|
|
1556
|
+
function sessionKey(agent, key) {
|
|
1557
|
+
return `${agent}:${key}`;
|
|
1558
|
+
}
|
|
1559
|
+
/**
|
|
1560
|
+
* 把磁盘上的映射归一成 `<agent>:<key>` 形式。
|
|
1561
|
+
* - 已带前缀的条目原样保留(其它 agent 的会话不动);
|
|
1562
|
+
* - 旧格式(裸 key)由「当时那个 agent」写入、已无从得知是哪个 → 归给当前 agent;
|
|
1563
|
+
* 若同名的新格式条目已存在,则保留新格式(它的归属确定),只丢掉裸条目。
|
|
1564
|
+
* migrated=true 时调用方应立即回写,避免下次换 agent 时旧条目再被继承一次。
|
|
1565
|
+
*/
|
|
1566
|
+
function normalizeSessionMap(raw, agent) {
|
|
1567
|
+
const map = {};
|
|
1568
|
+
const legacy = [];
|
|
1569
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {
|
|
1570
|
+
map,
|
|
1571
|
+
migrated: false
|
|
1572
|
+
};
|
|
1573
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
1574
|
+
if (typeof v !== "string") continue;
|
|
1575
|
+
if (k.includes(":")) map[k] = v;
|
|
1576
|
+
else legacy.push([k, v]);
|
|
1577
|
+
}
|
|
1578
|
+
for (const [k, v] of legacy) {
|
|
1579
|
+
const nk = sessionKey(agent, k);
|
|
1580
|
+
if (!map[nk]) map[nk] = v;
|
|
1581
|
+
}
|
|
1582
|
+
return {
|
|
1583
|
+
map,
|
|
1584
|
+
migrated: legacy.length > 0
|
|
1585
|
+
};
|
|
1586
|
+
}
|
|
1587
|
+
//#endregion
|
|
1483
1588
|
//#region src/acp-brain.ts
|
|
1484
1589
|
/**
|
|
1485
1590
|
* ACP 大脑:维护一个常驻的 ACP agent 进程(JSON-RPC over stdio,换行分隔)。
|
|
@@ -1546,13 +1651,17 @@ function reset$1() {
|
|
|
1546
1651
|
assembling$1.clear();
|
|
1547
1652
|
stdoutBuf$1 = "";
|
|
1548
1653
|
}
|
|
1654
|
+
const keyOf$1 = (userId) => sessionKey(AGENT$1.name, userId);
|
|
1549
1655
|
function loadSessionMap$1() {
|
|
1656
|
+
let raw;
|
|
1550
1657
|
try {
|
|
1551
|
-
|
|
1552
|
-
return j && typeof j === "object" ? j : {};
|
|
1658
|
+
raw = JSON.parse(fs.readFileSync(SESSION_MAP_FILE$1, "utf-8"));
|
|
1553
1659
|
} catch {
|
|
1554
1660
|
return {};
|
|
1555
1661
|
}
|
|
1662
|
+
const { map, migrated } = normalizeSessionMap(raw, AGENT$1.name);
|
|
1663
|
+
if (migrated) saveSessionMap$1(map);
|
|
1664
|
+
return map;
|
|
1556
1665
|
}
|
|
1557
1666
|
function saveSessionMap$1(m) {
|
|
1558
1667
|
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
@@ -1776,7 +1885,7 @@ async function ensureSession$1(userId) {
|
|
|
1776
1885
|
if (existing) return existing;
|
|
1777
1886
|
const mcpServers = AGENT$1.mcpViaAcp ? activeMcpServers() : [];
|
|
1778
1887
|
const map = loadSessionMap$1();
|
|
1779
|
-
const saved = map[userId];
|
|
1888
|
+
const saved = map[keyOf$1(userId)];
|
|
1780
1889
|
if (saved) {
|
|
1781
1890
|
const methods = AGENT$1.restore === "resume" ? ["resume"] : ["load", "resume"];
|
|
1782
1891
|
for (const method of methods) try {
|
|
@@ -1800,7 +1909,7 @@ async function ensureSession$1(userId) {
|
|
|
1800
1909
|
if (!res?.sessionId) throw new Error("session/new returned no sessionId");
|
|
1801
1910
|
await applyModel$1(res.sessionId);
|
|
1802
1911
|
userSessions.set(userId, res.sessionId);
|
|
1803
|
-
map[userId] = res.sessionId;
|
|
1912
|
+
map[keyOf$1(userId)] = res.sessionId;
|
|
1804
1913
|
saveSessionMap$1(map);
|
|
1805
1914
|
dlog$1(`session/new ok ${res.sessionId} mcp=[${mcpServers.map((s) => s.name).join(",")}]`);
|
|
1806
1915
|
return res.sessionId;
|
|
@@ -1859,7 +1968,7 @@ async function askBrain(userId, message, onProgress, images) {
|
|
|
1859
1968
|
/** 用户主动开新会话:丢弃旧 session(含磁盘映射),重建一个全新 session。 */
|
|
1860
1969
|
async function newSession(userId) {
|
|
1861
1970
|
const map = loadSessionMap$1();
|
|
1862
|
-
delete map[userId];
|
|
1971
|
+
delete map[keyOf$1(userId)];
|
|
1863
1972
|
saveSessionMap$1(map);
|
|
1864
1973
|
userSessions.delete(userId);
|
|
1865
1974
|
await ensureProcess$1();
|
|
@@ -2007,19 +2116,28 @@ function readJson(file) {
|
|
|
2007
2116
|
return null;
|
|
2008
2117
|
}
|
|
2009
2118
|
}
|
|
2010
|
-
/** 同 brain:把凭据文件(如 DSH 的
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
const
|
|
2119
|
+
/** 同 brain:把凭据文件(如 DSH 的 `.credentials.yaml`、Qoder 的 `.auth/user`)同步进 worker 沙箱。
|
|
2120
|
+
* `newer`=新者胜;`always`=内容不同即覆盖(标识类文件必须与真实目录一致,见 mcp-sandbox.ts)。 */
|
|
2121
|
+
function syncCredentialFile(file, mode) {
|
|
2122
|
+
if (!AG.realHome) return;
|
|
2123
|
+
const src = path.join(AG.realHome, file);
|
|
2124
|
+
const dst = path.join(WORKER_CONFIG_DIR, file);
|
|
2015
2125
|
try {
|
|
2016
2126
|
if (!fs.existsSync(src)) return;
|
|
2017
2127
|
fs.mkdirSync(path.dirname(dst), { recursive: true });
|
|
2128
|
+
if (mode === "always") {
|
|
2129
|
+
if (!(fs.existsSync(dst) && fs.readFileSync(src).equals(fs.readFileSync(dst)))) fs.copyFileSync(src, dst);
|
|
2130
|
+
return;
|
|
2131
|
+
}
|
|
2018
2132
|
const s = fs.statSync(src);
|
|
2019
2133
|
const d = fs.existsSync(dst) ? fs.statSync(dst) : null;
|
|
2020
2134
|
if (!d || s.mtimeMs > d.mtimeMs + 1e3) fs.copyFileSync(src, dst);
|
|
2021
2135
|
} catch {}
|
|
2022
2136
|
}
|
|
2137
|
+
function syncCredentials() {
|
|
2138
|
+
for (const f of AG.credentialFiles) syncCredentialFile(f, "newer");
|
|
2139
|
+
for (const f of AG.identityFiles) syncCredentialFile(f, "always");
|
|
2140
|
+
}
|
|
2023
2141
|
function loadCatalog() {
|
|
2024
2142
|
return readMcpServers(readJson(CATALOG), AG.mcpFormat);
|
|
2025
2143
|
}
|
|
@@ -2091,8 +2209,34 @@ function syncOpencodeBase() {
|
|
|
2091
2209
|
fs.mkdirSync(WORKER_CONFIG_DIR, { recursive: true });
|
|
2092
2210
|
fs.writeFileSync(WORKER_MCP_JSON, JSON.stringify(parsed, null, 2), "utf-8");
|
|
2093
2211
|
}
|
|
2212
|
+
/** Qoder 这类「设置与 MCP 同文件」的 agent:与 brain 相同的基底疗法(剥离 MCP/插件/自定义供应商 + 关自动更新)。 */
|
|
2213
|
+
function settingsBase() {
|
|
2214
|
+
let parsed = null;
|
|
2215
|
+
try {
|
|
2216
|
+
parsed = JSON.parse(fs.readFileSync(path.join(AG.realHome, AG.settingsBase), "utf-8"));
|
|
2217
|
+
} catch {}
|
|
2218
|
+
const base = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? { ...parsed } : {};
|
|
2219
|
+
delete base.mcpServers;
|
|
2220
|
+
delete base.enabledPlugins;
|
|
2221
|
+
delete base.providers;
|
|
2222
|
+
delete base.model;
|
|
2223
|
+
const general = base.general;
|
|
2224
|
+
base.general = {
|
|
2225
|
+
...general && typeof general === "object" && !Array.isArray(general) ? general : {},
|
|
2226
|
+
enableAutoUpdate: false
|
|
2227
|
+
};
|
|
2228
|
+
return base;
|
|
2229
|
+
}
|
|
2230
|
+
function syncSettingsBase() {
|
|
2231
|
+
fs.mkdirSync(WORKER_CONFIG_DIR, { recursive: true });
|
|
2232
|
+
fs.writeFileSync(WORKER_MCP_JSON, JSON.stringify(settingsBase(), null, 2), "utf-8");
|
|
2233
|
+
}
|
|
2094
2234
|
function syncSettings() {
|
|
2095
2235
|
if (AG.mcpViaAcp) return;
|
|
2236
|
+
if (AG.settingsBase && AG.realHome) {
|
|
2237
|
+
syncSettingsBase();
|
|
2238
|
+
return;
|
|
2239
|
+
}
|
|
2096
2240
|
if (!AG.inherit || !AG.realHome) return;
|
|
2097
2241
|
if (AG.mcpFormat === "opencode") syncOpencodeBase();
|
|
2098
2242
|
else syncKiroSettings();
|
|
@@ -2148,7 +2292,7 @@ function ensureWorkerHome() {
|
|
|
2148
2292
|
syncSettings();
|
|
2149
2293
|
applyWorkerSandbox();
|
|
2150
2294
|
mirrorRealKiro();
|
|
2151
|
-
|
|
2295
|
+
syncCredentials();
|
|
2152
2296
|
}
|
|
2153
2297
|
//#endregion
|
|
2154
2298
|
//#region src/worker-acp.ts
|
|
@@ -2229,13 +2373,17 @@ function reset() {
|
|
|
2229
2373
|
assembling.clear();
|
|
2230
2374
|
stdoutBuf = "";
|
|
2231
2375
|
}
|
|
2376
|
+
const keyOf = (key) => sessionKey(AGENT.name, key);
|
|
2232
2377
|
function loadSessionMap() {
|
|
2378
|
+
let raw;
|
|
2233
2379
|
try {
|
|
2234
|
-
|
|
2235
|
-
return j && typeof j === "object" ? j : {};
|
|
2380
|
+
raw = JSON.parse(fs.readFileSync(SESSION_MAP_FILE, "utf-8"));
|
|
2236
2381
|
} catch {
|
|
2237
2382
|
return {};
|
|
2238
2383
|
}
|
|
2384
|
+
const { map, migrated } = normalizeSessionMap(raw, AGENT.name);
|
|
2385
|
+
if (migrated) saveSessionMap(map);
|
|
2386
|
+
return map;
|
|
2239
2387
|
}
|
|
2240
2388
|
function saveSessionMap(m) {
|
|
2241
2389
|
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
@@ -2456,7 +2604,7 @@ async function ensureSession(key) {
|
|
|
2456
2604
|
if (existing) return existing;
|
|
2457
2605
|
const mcpServers = AGENT.mcpViaAcp ? workerMcpServers() : [];
|
|
2458
2606
|
const map = loadSessionMap();
|
|
2459
|
-
const saved = map[key];
|
|
2607
|
+
const saved = map[keyOf(key)];
|
|
2460
2608
|
if (saved) {
|
|
2461
2609
|
const methods = AGENT.restore === "resume" ? ["resume"] : ["load", "resume"];
|
|
2462
2610
|
for (const method of methods) try {
|
|
@@ -2480,7 +2628,7 @@ async function ensureSession(key) {
|
|
|
2480
2628
|
if (!res?.sessionId) throw new Error("session/new returned no sessionId");
|
|
2481
2629
|
await applyModel(res.sessionId);
|
|
2482
2630
|
taskSessions.set(key, res.sessionId);
|
|
2483
|
-
map[key] = res.sessionId;
|
|
2631
|
+
map[keyOf(key)] = res.sessionId;
|
|
2484
2632
|
saveSessionMap(map);
|
|
2485
2633
|
dlog(`session/new ok ${res.sessionId} mcp=[${mcpServers.map((s) => s.name).join(",")}]`);
|
|
2486
2634
|
return res.sessionId;
|
|
@@ -2525,7 +2673,7 @@ async function askWorker(instruction, onProgress) {
|
|
|
2525
2673
|
/** 开新任务会话:丢弃旧 session(含磁盘映射),下次 askWorker 全新开始。 */
|
|
2526
2674
|
function newWorkerSession() {
|
|
2527
2675
|
const map = loadSessionMap();
|
|
2528
|
-
delete map[TASK_KEY];
|
|
2676
|
+
delete map[keyOf(TASK_KEY)];
|
|
2529
2677
|
saveSessionMap(map);
|
|
2530
2678
|
taskSessions.delete(TASK_KEY);
|
|
2531
2679
|
}
|
|
@@ -3585,6 +3733,13 @@ async function checkAgent(ag, timeoutMs = 6e4) {
|
|
|
3585
3733
|
const env = { ...process.env };
|
|
3586
3734
|
if (ag.homeEnvVar) env[ag.homeEnvVar] = home;
|
|
3587
3735
|
for (const [k, v] of Object.entries(ag.env ?? {})) env[k] = v.replaceAll("%HOME%", home);
|
|
3736
|
+
const cfgDir = path.join(home, ag.configSubdir);
|
|
3737
|
+
for (const f of [...ag.credentialFiles, ...ag.identityFiles]) try {
|
|
3738
|
+
const src = path.join(ag.realHome ?? "", f);
|
|
3739
|
+
if (!ag.realHome || !fs.existsSync(src)) continue;
|
|
3740
|
+
fs.mkdirSync(path.dirname(path.join(cfgDir, f)), { recursive: true });
|
|
3741
|
+
fs.copyFileSync(src, path.join(cfgDir, f));
|
|
3742
|
+
} catch {}
|
|
3588
3743
|
const proc = spawnAgent(ag, {
|
|
3589
3744
|
cwd: home,
|
|
3590
3745
|
env
|
|
@@ -3694,7 +3849,7 @@ async function checkAgent(ag, timeoutMs = 6e4) {
|
|
|
3694
3849
|
* 与 package.json 的 version 必须一致——单测 version.test.ts 会读 package.json 校验,
|
|
3695
3850
|
* 避免「发了新版本、CLI 还报旧版本号」这种排查成本极高的小漂移。
|
|
3696
3851
|
*/
|
|
3697
|
-
const CLI_VERSION = "1.
|
|
3852
|
+
const CLI_VERSION = "1.1.0";
|
|
3698
3853
|
/**
|
|
3699
3854
|
* npm 包名(与 package.json 的 name 一致,单测校验)。
|
|
3700
3855
|
* 命令名是 `wemind`,但**包名**是 `@wemind/cli`——两者不同,所以 `npx` 不能靠「唯一 bin」自动解析,
|