dsh-any-agent 0.3.1 → 0.3.3
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 +128 -65
- package/README.zh.md +119 -60
- package/USAGE-NOTES.en.md +5 -3
- package/USAGE-NOTES.md +6 -4
- package/bin/dsh-any-agent.mjs +4 -1
- package/package.json +5 -3
- package/scripts/commit.mjs +122 -0
- package/src/config.mjs +1 -1
- package/src/hub.mjs +308 -30
- package/src/mcp.mjs +91 -3
- package/src/tools.mjs +74 -18
- package/src/workspace.mjs +8 -0
package/README.zh.md
CHANGED
|
@@ -1,50 +1,51 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<img src="https://raw.githubusercontent.com/xmtlzzz/dsh-any-agent/main/docs/brand/header.jpg" width="100%" alt="dsh-any-agent:蓝发鲸鱼娘捧着白米饭,坐在深蓝色编程桌前">
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
[](https://nodejs.org)
|
|
7
|
-
[](LICENSE)
|
|
8
|
-
[](package.json)
|
|
5
|
+
**给任何 MCP 客户端配一个能长期共事的编码搭子。**
|
|
9
6
|
|
|
10
|
-
|
|
7
|
+
**[官网](https://dsh-any-agent.xmtlz.dev/)** · [English](README.md) · **简体中文**
|
|
11
8
|
|
|
12
|
-
|
|
9
|
+
[](https://www.npmjs.com/package/dsh-any-agent)
|
|
10
|
+
[](https://nodejs.org)
|
|
11
|
+
[](LICENSE)
|
|
12
|
+
[](package.json)
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
[官网](https://dsh-any-agent.xmtlz.dev/) · [快速开始](#快速开始) · [主要优势](#-主要优势) · [工具](#-工具) · [使用注意事项](USAGE-NOTES.md) · [更新日志](CHANGELOG.md)
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
</div>
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
🧪 0.3.3 本机离线 673 项检查全绿(九种客户端配置齐全时 691 项;其中 517 项属于客户端注册器套件,齐全时 535 项) · 🔌 MCP over stdio · 📦 零依赖 · 📜 MIT
|
|
19
|
+
|
|
20
|
+
一个 MCP 服务端,把 DeepSeek Harness 当作**长驻的编码 agent** 来驱动。开个会话、扔个目标,它自己写代码、跑脚本、派子代理;你随时能看、能插话、能回头验收。
|
|
21
|
+
|
|
22
|
+
**Claude Code · Claude Desktop · Codex · Cursor · Windsurf · Zed · Gemini CLI · opencode · VS Code**,以及任何支持 MCP over stdio 的客户端。
|
|
23
|
+
|
|
24
|
+
## 快速开始
|
|
25
|
+
|
|
26
|
+
需要 **Node.js ≥ 22.15**,并且**已安装 DSH、配置好可用的模型路由与凭据**。详见[前置要求](#-前置要求)。
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install -g dsh-any-agent
|
|
30
|
+
dsh-any-agent-setup
|
|
31
|
+
dsh-any-agent-setup --yes
|
|
32
|
+
```
|
|
21
33
|
|
|
22
|
-
|
|
34
|
+
第二条命令只预览,加 `--yes` 才会写入配置。重新连接客户端的 MCP 服务,然后让你的 agent 在当前项目中开启 DSH 会话、派活、收结果。[先派活,回头验收](#-用法先派活回头再验收)展示了完整的工具调用顺序。
|
|
23
35
|
|
|
36
|
+
> **第一次派活前:** 先读[使用注意事项](USAGE-NOTES.md),尤其是模型配置与外部 MCP 的权限边界。**外部 MCP 工具的副作用不受 DSH 沙箱约束。** 新机器部署可按[迁移指引](USAGE-MIGRATION.md)逐步操作。
|
|
37
|
+
|
|
38
|
+
<details>
|
|
39
|
+
<summary>展开完整目录</summary>
|
|
40
|
+
|
|
41
|
+
- **快速开始** —— [安装并连接](#快速开始) · [更新日志](CHANGELOG.md)
|
|
24
42
|
- **先看这里** —— [解决什么问题](#-解决什么问题) · [主要优势](#-主要优势) · [为什么不是包一层](#-为什么不是直接把-dsh-包一层) · [前置要求](#-前置要求) · [安装配置](#-安装配置)
|
|
25
43
|
- **怎么用** —— [工具](#-工具) · [先派活回头验收](#-用法先派活回头再验收) · [跑完自动通知](#-用法跑完自动通知不轮询) · [多会话并发](#-用法同时控制多个会话)
|
|
26
44
|
- **行为** —— [权限](#-权限) · [思考内容](#-思考内容默认隐藏) · [日志与隐私](#-日志与隐私) · [工作区与 GUI](#-会话在-dsh-gui-里的可见性)
|
|
27
45
|
- **难点** —— [写锁](#-写锁一个会话同一时刻只有一个写者)
|
|
28
46
|
- **参考** —— [环境变量](#-环境变量) · [测试](#-测试) · [已知限制](#-已知限制) · [为什么还叫旧 profile 名?](#-为什么-profile-还叫-dsh-mcp)
|
|
29
47
|
|
|
30
|
-
|
|
31
|
-
```
|
|
32
|
-
任意 MCP 客户端 ← Claude Code、Codex、Cursor、Zed、Cline、你自己写的…
|
|
33
|
-
(Claude/Codex/Cursor/…) │
|
|
34
|
-
│ MCP over stdio │ 换行分隔 JSON-RPC(ACP)
|
|
35
|
-
▼ ▼
|
|
36
|
-
┌─────────────┐ ┌──────────────────────────┐
|
|
37
|
-
│dsh-any-agent│─────────▶│ dsh --profile dsh-mcp │ 每个会话一个长驻进程
|
|
38
|
-
└─────────────┘ │ │
|
|
39
|
-
└────────────┬─────────────┘
|
|
40
|
-
│
|
|
41
|
-
┌────────────┴─────────────┐
|
|
42
|
-
│ DSH 自带工具 │ 文件/shell/搜索/
|
|
43
|
-
│ │ 技能/子代理/workflow
|
|
44
|
-
├──────────────────────────┤
|
|
45
|
-
│ dsh-mcp-client │ → 你自己的其他 MCP 服务器
|
|
46
|
-
└──────────────────────────┘
|
|
47
|
-
```
|
|
48
|
+
</details>
|
|
48
49
|
|
|
49
50
|
## 🎯 解决什么问题
|
|
50
51
|
|
|
@@ -63,13 +64,13 @@ DSH(DeepSeek Harness)本身**全都能做** —— 它有文件/shell 工具
|
|
|
63
64
|
|
|
64
65
|
## ✨ 主要优势
|
|
65
66
|
|
|
66
|
-
|
|
|
67
|
+
| 能力 | 你能得到什么 |
|
|
67
68
|
|---|---|
|
|
68
69
|
| 🔄 **进程死了对话也不丢** | 对话身份在磁盘上。进程崩溃、被空闲回收、甚至整个服务重启,都不影响继续——下次调用自动 `resume`,记忆完整(跨进程实测)。 |
|
|
69
70
|
| 🗣️ **不用傻等** | 回合跑偏了能**插话改口**(约 100ms 收敛到 idle,四次打断实测),或**排队**等这轮跑完再说;打断后会话不损坏。 |
|
|
70
71
|
| 📡 **跑完自动通知** | 长任务 `wait=false` 立刻返回 `run_id`;回合结束时**原子写出哨兵文件**,用你宿主的后台任务等它即可。不轮询、不占轮次。 |
|
|
71
72
|
| 🪟 **多会话并发** | `dsh_list(only_running)` 看谁在跑,`dsh_read` 游标式增量读——像切窗口一样轮流盯。 |
|
|
72
|
-
| 🧠 **灰色思考默认隐藏** | 只给"想了 N 字 / M 秒"
|
|
73
|
+
| 🧠 **灰色思考默认隐藏** | 只给"想了 N 字 / M 秒"的统计,不烧上下文;**本服务不写日志、不落盘**(DSH 自己的会话日志仍含思考,见“日志与隐私”)。 |
|
|
73
74
|
| 🤫 **默认完全静默** | 一个字节都不输出,不污染宿主日志;stdout 只走协议。 |
|
|
74
75
|
| 🧩 **能力可叠加** | DSH 自己还能挂别的 MCP(IDA Pro、浏览器、内部工具)——这是一座通往整个工具箱的桥。 |
|
|
75
76
|
| 🛡️ **只读档真能只读** | `read-only` 沙箱 + 审批一律拒绝,适合分析不信任样本。 |
|
|
@@ -89,6 +90,31 @@ DSH 自带两个可编程入口。`sdk` profile 看着更简单,但它的 JSON
|
|
|
89
90
|
|
|
90
91
|
**核心思想:进程只是缓存,不是身份。** 对话的身份是 DSH 的 `sessionId`,落盘在 `~/.dsh/sessions`。进程被回收、崩溃、甚至整个 MCP 服务重启,下一次调用都会自动 `session/resume` 把它接回来,历史完整保留。
|
|
91
92
|
|
|
93
|
+
<details>
|
|
94
|
+
<summary>客户端、服务端与 DSH 是怎样连接的</summary>
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
任意 MCP 客户端 ← Claude Code、Codex、Cursor、Zed、Cline、你自己写的…
|
|
98
|
+
(Claude/Codex/Cursor/…) │
|
|
99
|
+
│ MCP over stdio │ 换行分隔 JSON-RPC(ACP)
|
|
100
|
+
▼ ▼
|
|
101
|
+
┌─────────────┐ ┌──────────────────────────┐
|
|
102
|
+
│dsh-any-agent│─────────▶│ dsh --profile dsh-mcp │ 每个会话一个长驻进程
|
|
103
|
+
└─────────────┘ │ │
|
|
104
|
+
└────────────┬─────────────┘
|
|
105
|
+
│
|
|
106
|
+
┌────────────┴─────────────┐
|
|
107
|
+
│ DSH 自带工具 │ 文件/shell/搜索/
|
|
108
|
+
│ │ 技能/子代理/workflow
|
|
109
|
+
├──────────────────────────┤
|
|
110
|
+
│ dsh-mcp-client │ → 你自己的其他 MCP 服务器
|
|
111
|
+
└──────────────────────────┘
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**MCP 兼容性。** 服务端只使用 `initialize`、`ping`、`tools/list`、`tools/call` 四个标准方法。已用一个协商旧版协议、从不发送 `notifications/initialized` 的原始客户端验证。
|
|
115
|
+
|
|
116
|
+
</details>
|
|
117
|
+
|
|
92
118
|
## 📦 前置要求
|
|
93
119
|
|
|
94
120
|
- **Node.js ≥ 22.15** —— 服务用 `node:zlib` 的 `zstdDecompressSync` 读 DSH 会话头,这个 API 在 22.15 之前不存在
|
|
@@ -112,6 +138,9 @@ DSH 自带两个可编程入口。`sdk` profile 看着更简单,但它的 JSON
|
|
|
112
138
|
|
|
113
139
|
想要的来源不是自动挑的那个,可设 `DSH_MCP_ROUTE_SOURCE=desktop`(或 `__home__`);彻底关掉自动同步则设 `DSH_MCP_BOOTSTRAP=0`。
|
|
114
140
|
|
|
141
|
+
<details>
|
|
142
|
+
<summary>手工维护 profile 与 ACP 配置细节(可选)</summary>
|
|
143
|
+
|
|
115
144
|
### 1b. 手工维护(可选)
|
|
116
145
|
|
|
117
146
|
```bash
|
|
@@ -125,15 +154,11 @@ dsh --profile dsh-mcp --from-default-profile acp
|
|
|
125
154
|
> 1. **ACP 的模型不走 `agent-default-model`**,而是读 `dsh-acp` 插件自己的 `config.provider` / `config.model` —— 而 `acp` bundle 把它硬编码成了 `deepseek-official`。不覆盖 `acp` 这一行,会话会静默跑在错误的 provider 上,报 `no API key for provider route "deepseek-official"`。
|
|
126
155
|
> 2. **ACP 的 `reasoning_effort` 默认是空串**(= "Provider default"),不是最大档。本服务在每次 `session/new` 以及每次 `session/resume` 之后都会显式设成 `max` —— 因为新进程不会记住上一次的选择。
|
|
127
156
|
|
|
128
|
-
|
|
157
|
+
</details>
|
|
129
158
|
|
|
130
|
-
|
|
159
|
+
### 2. 注册 MCP 服务
|
|
131
160
|
|
|
132
|
-
|
|
133
|
-
npm install -g dsh-any-agent
|
|
134
|
-
dsh-any-agent-setup # 检测你装了哪些客户端,不写任何文件
|
|
135
|
-
dsh-any-agent-setup --yes # 真正写入
|
|
136
|
-
```
|
|
161
|
+
执行[快速开始](#快速开始)中的三条命令,再重新连接客户端。需要手工配置时,可展开下方的客户端示例。
|
|
137
162
|
|
|
138
163
|
`setup` 会找出你机器上的 MCP 客户端 —— **Claude Code、Claude Desktop、Codex、Cursor、Windsurf、Zed、Gemini CLI、opencode、VS Code** —— 逐个写进去,没装的跳过。它的安全性是结构性保证的:
|
|
139
164
|
|
|
@@ -143,12 +168,17 @@ dsh-any-agent-setup --yes # 真正写入
|
|
|
143
168
|
- **文本级写入**:只替换自己那个条目的字节,缩进(2 空格 / 4 空格 / Tab)、键顺序、CRLF 行尾、UTF-8 BOM、有无末尾换行都原样保留
|
|
144
169
|
- **幂等**,重复运行只改变有差异的部分;第二次 `--yes` 不写盘、不新增 `.bak`
|
|
145
170
|
|
|
171
|
+
**实测影响范围(2026-11-02,本机 plan 模式):** 9 个客户端中检测到 5 个(Claude Code、Codex、Cursor、Zed、VS Code),其余 4 个跳过;`--json` 报告 `mode=plan, changed=5, skipped=4, failed=0`。所有已存在客户端配置的 SHA-256、大小与 mtime **全部未变**。真正执行 `--yes` 也是增量写入:只写 `dsh-any-agent` 这一个条目,其余字节原样保留,写前先落一份 `.bak-dsh-<时间戳>`;`--remove` 也只删这一个条目。
|
|
172
|
+
|
|
146
173
|
```bash
|
|
147
174
|
dsh-any-agent-setup --yes --only cursor,zed # 只装这几个
|
|
148
175
|
dsh-any-agent-setup --yes --permission read-only
|
|
149
176
|
dsh-any-agent-setup --remove --yes # 全部移除,只摘自己那条
|
|
150
177
|
```
|
|
151
178
|
|
|
179
|
+
<details>
|
|
180
|
+
<summary>高级开关与各客户端的配置路径</summary>
|
|
181
|
+
|
|
152
182
|
**新增的三个开关**
|
|
153
183
|
|
|
154
184
|
| 开关 | 作用 |
|
|
@@ -177,6 +207,11 @@ dsh-any-agent-setup --remove --yes # 全部移除,只摘自己
|
|
|
177
207
|
|
|
178
208
|
有三种形状值得单独点出来,写错了是**静默失效**。**VS Code** 的 MCP 配置放在 `servers` 下,不是 `mcpServers`;按常见写法写进去,编辑器会直接忽略。**opencode** 用的是**一个** `command` 数组(`{ type: "local", command: [exe, ...args], enabled: true }`),不是分开的 `command`/`args`。**Claude Code** 则是本次发布**完全不用 JSON 库**的原因:`~/.claude.json` 的 `projects` 映射里确实存在**只差大小写**的重复键(`.../Rust` 和 `.../rust` 并存),朴素的 `JSON.parse` 在那里会失败,而任何整份重写都会毁掉用户的项目列表。所以改用按字符区间扫描、**从不构造值对象**的扫描器;早先那版「调 `claude mcp add`」也出于同样理由被放弃 —— 那条命令会用它自己的格式重写整个文件,且不产生我们可以回滚的备份。
|
|
179
209
|
|
|
210
|
+
</details>
|
|
211
|
+
|
|
212
|
+
<details>
|
|
213
|
+
<summary>注册器检查与注册、撤销的往返验证</summary>
|
|
214
|
+
|
|
180
215
|
**setup 会动什么、不会动什么**
|
|
181
216
|
|
|
182
217
|
它只做新增,而且只动自己那一条。在真正装着别的 MCP 服务器的机器上验证过(Cursor 4 个、Codex 2 个段、Claude Code 5 个、Zed 33 个配置键):所有既有条目**逐字节不变**,键顺序不变,唯一的差异是每个客户端多了一个条目。
|
|
@@ -224,6 +259,15 @@ Codex 的 `config.toml` 同理,是按行处理的:其它 `[表]` 及其注
|
|
|
224
259
|
|
|
225
260
|
撤销后所有 JSON 仍可解析、`codex mcp list` 仍能正常读取配置,客户端不需要人工修复。
|
|
226
261
|
|
|
262
|
+
</details>
|
|
263
|
+
|
|
264
|
+
**`cwd` 可以省略:默认用客户端当前打开的项目**
|
|
265
|
+
|
|
266
|
+
客户端本来就知道用户在哪个目录干活(MCP roots)。现在 `dsh_start` 不传 `cwd` 时,会向客户端发一次 `roots/list`,把返回的 `file://` URI 转成路径直接用。于是「客户端里看到哪个项目,DSH 就在哪个项目干活」成了默认行为,也顺带堵掉了「手拼路径拼到家目录」这条最常见的翻车路径。客户端不声明 roots、或者超时没回,就退回一条能照着改的报错,而不是猜一个目录。
|
|
267
|
+
|
|
268
|
+
**cwd 不要选家目录本身**
|
|
269
|
+
|
|
270
|
+
DSH 自己的 ACL 沙箱要求临时目录在工作区**之外**。Windows 上 `%TEMP%` 是 `C:/Users/<你>/AppData/Local/Temp`,就在家目录里面,所以 `cwd` 取家目录时,第一条 shell 命令会失败并报 `Windows ACL: temp root must be outside the workspace`。现在 `dsh_start` 会直接拒绝这个组合并给出两条出路:换一个具体项目目录,或改用 `permission=read-only` / `danger-full-access`。
|
|
227
271
|
|
|
228
272
|
**备份与回滚**
|
|
229
273
|
|
|
@@ -256,6 +300,9 @@ Codex 的 `config.toml` 同理,是按行处理的:其它 `[表]` 及其注
|
|
|
256
300
|
| 只从某几个移除 | `dsh-any-agent-setup --remove --yes --only codex` |
|
|
257
301
|
| 撤销某次误写 | 把 `.bak-dsh-<时间戳>` 复制回原文件 |
|
|
258
302
|
|
|
303
|
+
<details>
|
|
304
|
+
<summary>移除操作的实测记录</summary>
|
|
305
|
+
|
|
259
306
|
**`--remove` 到底动了什么 —— 实测**
|
|
260
307
|
|
|
261
308
|
注销只删掉每个客户端里的**那一个键**,其余一概不碰。在真机上测得的前后对比:
|
|
@@ -277,12 +324,17 @@ Codex 我们的两个 TOML 段 -> 已移除
|
|
|
277
324
|
|
|
278
325
|
事后可自行核验:`claude mcp list`、`codex mcp list`,以及看 `~/.cursor/mcp.json` 和 Zed 设置里的 `context_servers` 键。
|
|
279
326
|
|
|
327
|
+
</details>
|
|
328
|
+
|
|
280
329
|
**记录的是绝对路径**
|
|
281
330
|
|
|
282
331
|
`setup` 写进去的是它运行时所处安装目录的**绝对路径**。**如果你移动或删除了那个目录,所有客户端都会报 `CONNECTION_CLOSED`**,直到你在新位置重新跑一遍 `setup`,或者用 `--remove` 把失效条目清掉。
|
|
283
332
|
|
|
284
333
|
最容易踩到的情况:装到临时目录里试用,然后清理掉 —— 配置还指着那个已经不存在的路径。
|
|
285
334
|
|
|
335
|
+
<details>
|
|
336
|
+
<summary>手工配置客户端:Claude Code、Codex 与通用 MCP</summary>
|
|
337
|
+
|
|
286
338
|
**从本地目录安装**
|
|
287
339
|
|
|
288
340
|
解压到任意位置,把 `command` 指向 `node`、`args` 指向 `<解压目录>/bin/dsh-any-agent.mjs`(见下)。
|
|
@@ -326,6 +378,8 @@ tool_timeout_sec = 120.0
|
|
|
326
378
|
|
|
327
379
|
> Cursor、Windsurf、Zed、Cline、Continue 等大部分客户端都吃同一套 `mcpServers` 结构——只要能拉起 stdio MCP 服务,就能用。
|
|
328
380
|
|
|
381
|
+
</details>
|
|
382
|
+
|
|
329
383
|
### 3. 自检
|
|
330
384
|
|
|
331
385
|
```bash
|
|
@@ -341,12 +395,12 @@ node test/smoke.mjs # 不调用 LLM,验证整条管道
|
|
|
341
395
|
| `dsh_start` | 建会话:工作区、权限档、审批策略、**思考深度(默认 `max`)**、模型 |
|
|
342
396
|
| `dsh_send` | 派任务。**默认 `wait=false`:立刻返回 `run_id` + `sentinel_file`**,回合在后台跑,跑完由哨兵文件通知你(推荐)。`wait=true` 才原地阻塞到回合结束;到期(`timeout_ms`,默认 0 = 一直等)**只会转后台,不取消回合、不丢结果** |
|
|
343
397
|
| `dsh_list` | 所有会话及运行状态:`running` / `idle` / `detached`,已跑时长、当前工具、本轮输出。`only_running=true` 只看活动会话 |
|
|
344
|
-
| `dsh_read` |
|
|
345
|
-
| `dsh_get` | 会话详情;带 `run_id` 时返回**那一次派活的完整结果**("回头验收"
|
|
346
|
-
| `dsh_interject` | 会话在忙时插话:`interject` 打断并改口,`queue` 等本轮跑完接着说。**这一轮同样会写哨兵**,返回里给 `sentinel_file` |
|
|
398
|
+
| `dsh_read` | **游标式增量读**——看正在跑的会话此刻吐出了什么。cursor 是**修订号**:文本被追加过的条目会再次返回,按 `seq` 替换全文(不要重复累加) |
|
|
399
|
+
| `dsh_get` | 会话详情;带 `run_id` 时返回**那一次派活的完整结果**("回头验收"的入口)。run 记录只在内存(最近 20 条),服务重启后请读哨兵文件 |
|
|
400
|
+
| `dsh_interject` | 会话在忙时插话:`interject` 打断并改口,`queue` 等本轮跑完接着说。**这一轮同样会写哨兵**,返回里给 `sentinel_file`。会话空闲时等价于 `wait=true`,**会阻塞到回合结束** |
|
|
347
401
|
| `dsh_takeover` | **抢占写锁**。锁被别的 DSH 进程占着时的唯一出路(DSH 没有夺锁 API,抢 = 杀掉持有者)。先判定持有者:本服务残留的孤儿 → 自动抢;另一个活着的实例 → 需 `force`;**GUI/未知 → 绝不杀**,只报告 |
|
|
348
402
|
| `dsh_interrupt` | 打断当前回合(会话不会损坏) |
|
|
349
|
-
| `dsh_release` | 交还会话(释放写锁,之后可在你自己的 DSH GUI 里打开)。仍可 resume |
|
|
403
|
+
| `dsh_release` | 交还会话(释放写锁,之后可在你自己的 DSH GUI 里打开)。仍可 resume;`forget=true` 会写墓碑,此后按 id 查询一律拒绝,直到删除 `$DSH_HOME/.dsh-any-agent-forgotten/<id>.json` |
|
|
350
404
|
| `dsh_approval_decide` | 裁决待决审批(仅当 `on_approval=ask`) |
|
|
351
405
|
| `dsh_status` | 轻量状态查询 |
|
|
352
406
|
|
|
@@ -395,7 +449,7 @@ echo "DONE" # 后台任务退出 → 宿主唤醒你
|
|
|
395
449
|
- **闩锁语义**:文件写一次并保留。即便 run 在你启动等待任务**之前**就已完成,`[ -f ]` 也立刻为真,不会漏。
|
|
396
450
|
- **原子落地**:`tmp` + `rename`,文件一旦存在内容必然完整;每个 run 用独立 tmp,并发写互不干扰。
|
|
397
451
|
- **无主动推送**:本服务在 MCP 协议上只应答请求、不发通知;"跑完通知"完全由这个哨兵文件 + 你的后台等待任务实现。
|
|
398
|
-
- **★
|
|
452
|
+
- **★ 哨兵里没有思考内容**:载荷含正文、思考**统计**、用量与(已脱敏的)工具输入,但没有思考正文;`result.thinking` 会被剥掉,并始终给出布尔 `thinking_omitted`(剥离了为 `true`,本来没有为 `false`)——本服务自己的落盘物里不写思考。`dsh_get(run_id)` 在内存里仍能看到它(记录还在时)。设 `DSH_MCP_SENTINEL_INCLUDE_REASONING=1` 可以故意打破这条保证。注意:**DSH 自己的会话日志仍含思考**,见“日志与隐私”。
|
|
399
453
|
|
|
400
454
|
> 清理:哨兵内含完整正文,所以服务**启动时会清掉超过 7 天的哨兵**(用 `DSH_MCP_SENTINEL_TTL_MS` 调;`0` 关闭)。等待任务消费后仍建议自行 `rm`。
|
|
401
455
|
|
|
@@ -407,7 +461,7 @@ echo "DONE" # 后台任务退出 → 宿主唤醒你
|
|
|
407
461
|
- … | id=33427096… | 🔵进行中(已跑8s, 本轮输出=34字) | cwd=…
|
|
408
462
|
2. dsh_read(id, cursor=0) # 读第一个,记下 cursor
|
|
409
463
|
3. dsh_read(id2, cursor=0) # 切到第二个
|
|
410
|
-
4. dsh_read(id, cursor=<上次>) #
|
|
464
|
+
4. dsh_read(id, cursor=<上次>) # 切回来;上次之后被追加过的条目会再次出现(按 seq 替换全文)
|
|
411
465
|
```
|
|
412
466
|
|
|
413
467
|
## 🤫 思考内容:默认隐藏
|
|
@@ -428,6 +482,8 @@ DSH 在模型层就把思考与正文分开,ACP 又把它们做成**两条独
|
|
|
428
482
|
|
|
429
483
|
`on_approval` 可选 `auto-allow` / `auto-deny` / `ask`(`ask` 会把审批挂起,交给 `dsh_approval_decide` 裁决)。
|
|
430
484
|
|
|
485
|
+
**`read-only` 是强制边界,不只是默认值**:它固定使用 `auto-deny`;给只读会话传 `auto-allow` / `ask` 会在 `dsh_start` 被直接拒绝,审批层也会拒绝来自只读会话的任何权限请求。否则 DSH 可以请求 `sandbox_permissions` 提权,而宽松的审批策略会把写操作放行(对抗审计实测过)。
|
|
486
|
+
|
|
431
487
|
## 🪟 会话在 DSH GUI 里的可见性
|
|
432
488
|
|
|
433
489
|
两件事,别混淆:
|
|
@@ -466,13 +522,13 @@ node bin/dsh-any-agent-workspaces.mjs --purge-test-sessions # 删测试会话
|
|
|
466
522
|
| `debug` | 更细的本服务事件;仍不打印 DSH 输出 |
|
|
467
523
|
| `DSH_MCP_LOG_STDERR=1` | 额外转发 DSH 原始 stderr(**可能含思考内容**) |
|
|
468
524
|
|
|
469
|
-
|
|
525
|
+
**本服务不把思考内容写进自己的落盘物**,只有一个**刻意留下**的例外:
|
|
470
526
|
|
|
471
|
-
-
|
|
472
|
-
-
|
|
527
|
+
- 不写日志(默认静默);状态文件与 `.state/meta/` 下的会话元数据侧车都只有元数据(id、cwd、权限、计数、标题提示),冒烟测试里有一条断言专门守注册表。
|
|
528
|
+
- **异步完成哨兵需要把正文写进文件**(否则调用方无法离线收活),载荷还包含用量与(已按审批规则脱敏的)工具输入,但 `result.thinking` 会被**剥掉**,并始终给出布尔 `thinking_omitted`。测试里专门验证了:即使请求 `reasoning=full`,哨兵里也不出现任何思考文本——而同一 run 在内存(`dsh_get`)里仍能看到。设 `DSH_MCP_SENTINEL_INCLUDE_REASONING=1` 可刻意打破这条。
|
|
473
529
|
- 哨兵默认 7 天后由启动清理删除(`DSH_MCP_SENTINEL_TTL_MS`,`0` 表示永久保留)。
|
|
474
530
|
|
|
475
|
-
|
|
531
|
+
> ⚠️ **DSH 自己会在 `~/.dsh/sessions` 写会话日志,而且其中包含完整 reasoning 文本**(即使 `reasoning=hide` 也照存)。那是 resume 的依据,不在本服务控制范围内;“思考不落盘”的绝对表述只适用于**本服务自己的**日志/哨兵/注册表。
|
|
476
532
|
|
|
477
533
|
## ⚙️ 环境变量
|
|
478
534
|
|
|
@@ -515,18 +571,21 @@ node test/cleanup.mjs # 单独跑收尾清理(只针对临时目录;--d
|
|
|
515
571
|
| 套件 | 检查数 | 覆盖 |
|
|
516
572
|
|---|---|---|
|
|
517
573
|
| `smoke` | 29 | 握手、工具表、建会话、配置、注册表纯净性 |
|
|
518
|
-
| `boundary` |
|
|
574
|
+
| `boundary` | 46 | 协议边界(重复 initialize、脏行、未知方法)、参数校验(含 read-only + 宽松审批必须拒绝、未知 provider/model、失败创建不留下写锁登记)、未知 id、游标边界、生命周期幂等、unicode |
|
|
519
575
|
| `integration` | 30 | 跨进程 resume 与记忆、中断、两种插话 |
|
|
520
576
|
| `async` | 16 | fire-and-forget + 事后验收 |
|
|
521
577
|
| `sentinel` | 36 | 完成哨兵:原子性、闩锁语义、多会话并发不串台、被取消也落地、**思考不进文件** |
|
|
522
578
|
| `prune` | 11 | **哨兵留存**:清超期与崩溃残留、留新、清空壳目录、`TTL=0` 关闭清理、且绝不碰同目录的会话注册表(不花 token,每次必跑) |
|
|
523
579
|
| `guards` | 31 | **环境变量守卫**:奇怪的数值一律压到安全侧 —— `IDLE_TTL_MS` 的 `-1`/非法值都当"永不回收"(绝不主动让出写锁)、回收间隔过小压回默认(防空转紧循环)、**失联阈值下限 30 秒**(防别的实例"合法地"抢占并杀掉我们的回合)(不花 token,每次必跑) |
|
|
524
580
|
| `sandbox-guard` | 18 | **测试沙箱自身**:「临时 HOME」如果落到真实用户目录、它的子目录、盘符根目录,或 OS 临时目录之外,一律报错拒绝,绝不被写入和删除。其余套件都通过 `test/sandbox-home.mjs` 拿目录、只销毁自己建的那个(不花 token,每次必跑) |
|
|
525
|
-
| `approval-details` |
|
|
581
|
+
| `approval-details` | 27 | **审批审查上下文**:待决审批的快照带命令、理由、cwd 与原始审批 ID —— 不只是一个裸 ID,调用方才能真正裁决;缺失的 `rawInput` 会按 ID 从先前的 `tool_call` 更新关联回来;凭据形状的字段与常见内联字面量在结构化上下文和展示标题里都会脱敏;关闭会话会取消待决审批。经真实 stdio MCP 端到端验证 |
|
|
582
|
+
| `read-cursor` | 10 | **`dsh_read` 游标语义**:追加到已读条目里的文本必须再次返回(修订号游标,按 `seq` 替换全文);`limit` 钳到 1..500,`0`/负数不会空转或丢尾(不花 token) |
|
|
583
|
+
| `sentinel-redaction` | 11 | **哨兵落盘卫生**:`result.thinking` 被剥掉、`thinking_omitted` 始终是布尔值、`tools_used[].input` 走与审批相同的凭据脱敏(不花 token) |
|
|
584
|
+
| `forget-tombstone` | 7 | **`forget` 是终态**:`release(forget=true)` 写墓碑,按 id 查询明确失败并给出恢复路径,列表不再展示;删除墓碑后回到常规“未知会话”路径(不花 token) |
|
|
526
585
|
| `setup` | 41 | **注册 CLI**:预览不写盘、`--remove` 只摘自己那条、没装客户端算跳过不算失败、Codex TOML 段往返、以及把写进配置里的命令真的拉起一次并要求应答 `initialize`(不花 token) |
|
|
527
586
|
| `removal-scope` | 24 | **注销只动自己那条**:给一份装着别的 MCP 服务器、顶层 Codex 设置和 Zed 兄弟设置的配置,注册再注销,断言其余每个值逐字节不变;删一个从没写过的东西 = 无写入、无报错(不花 token) |
|
|
528
|
-
| `config-safety` |
|
|
529
|
-
| `real-configs` | 66 | **拿真家伙做往返**:把本机**真实**的客户端配置复制进临时目录(18 万字节、含几百个 `projects` 条目与大小写重复键的 `~/.claude.json`,238 行的 Codex TOML,Zed 的完整 `settings.json
|
|
587
|
+
| `config-safety` | 404 | **0.3 这条线的核心性质**,逐客户端验证:预览后文件逐字节不变且不产生 `.bak`;注册再注销后其余条目逐字节还原、键顺序不变、顶层键不丢;4 空格 / 2 空格 / Tab 缩进、单行 JSON、无末尾换行、CRLF、UTF-8 BOM 都能在写入后保留;坏 JSON 与只读文件被拒绝且原文件无损;第二次 `--yes` 空跑不新增备份;每个 `.bak-dsh-<时间戳>` 只出现一次且内容等于写入前字节(不花 token) |
|
|
588
|
+
| `real-configs` | 66(本机 5/9 配置时 48) | **拿真家伙做往返**:把本机**真实**的客户端配置复制进临时目录(18 万字节、含几百个 `projects` 条目与大小写重复键的 `~/.claude.json`,238 行的 Codex TOML,Zed 的完整 `settings.json`),注册再撤销,要求每个文件**逐字节**还原;条目是否存在按**键**判断而不是裸子串;缺少 fixture 时明确打印 `skipped`,绝不假绿(不花 token) |
|
|
530
589
|
| `timeout` | 38 | **超时语义**:`wait=false` 不受 `timeout_ms` 影响、`wait=true` 到期只转后台(不取消/不丢结果、`busy` 不说谎)、`timeout_ms<=0` 一直等、resume 失败会作废进程而不永久卡死、空 prompt 给清晰错误 |
|
|
531
590
|
| `lock` | 79 | **写锁与抢占**:四类持有者判定、`writeMarker` 不覆盖活着的持有者、登记文件损坏/目录缺失等边界;**端到端**两实例抢锁 → 清晰报错 → 拒绝 → `force` 夺锁;**崩溃自动释放锁**、**卡死持有者无 force 自动抢占**;以及"一个回合 = 一个哨兵"的不变量(含内联回合与空闲插话) |
|
|
532
591
|
| `cycle` | 36 | **异步派活的生命周期**:异步派活 → 收哨兵 → 空闲回收 → 再派活,连做 3 轮必须全程无锁错误;贴着回收窗口立刻重派(放大竞态);`dsh_release` 后立刻重派;并断言回收后**不留下无法归因的锁**(这条实测抓到过"刚拉起的进程被 reaper 当空闲收掉"的竞态) |
|
|
@@ -534,17 +593,17 @@ node test/cleanup.mjs # 单独跑收尾清理(只针对临时目录;--d
|
|
|
534
593
|
| `capability` | 22 | 写代码、跑脚本、**自行派子代理**(用磁盘上的子会话头验证) |
|
|
535
594
|
| `workspace-effect` | 24 | 不给路径时工作区是否真的生效 |
|
|
536
595
|
| `acceptance` | 36 | 两个文件夹 × 两个会话做只读 IDA Pro 分析 |
|
|
537
|
-
| `multi` | 19 | **多实例共存 + 非 ASCII 路径**:复现"两实例共写注册表 → 后写的把先建的会话抹掉",并验证**仅凭 id
|
|
596
|
+
| `multi` | 19 | **多实例共存 + 非 ASCII 路径**:复现"两实例共写注册表 → 后写的把先建的会话抹掉",并验证**仅凭 id 就能从会话存储找回并真的派活**(指注册表丢失,不是显式 `forget` —— forget 是终态墓碑);**兜底取回不会悄悄提权**(read-only 会话取回后仍是 read-only);不留 `.tmp` 残渣;**中文 / emoji 工作区路径**能建会话、能干活、文件落在正确目录 |
|
|
538
597
|
| `permission` | 11 | ★ **权限档是否真的生效**(安全属性):不看我们的返回值(那正是会说谎的地方),而是读**会话自己记录的事实**(投影缓存里的 `permissions.preset` / `sandboxMode`),三档逐一核对且三档记录值互不相同。**这条测试抓到过无声提权**:profile 里 `defaultPreset` 会在会话创建时盖掉 `DSH_PERMISSION_MODE` |
|
|
539
598
|
| `list` | 13 | **`dsh_list` 的开销**(实测驱动):含磁盘探测的默认调用约 1 秒(要 spawn 一个 DSH 进程),**连续调用命中缓存降到个位数毫秒**;`only_running=true` 与 `include_closed=false` **跳过探测**且语义等价(不把磁盘上的未打开会话算进来) |
|
|
540
599
|
|
|
541
|
-
|
|
600
|
+
**离线套件在九种客户端配置齐全时合计 691 项(本机 5/9 配置时 673 项),0.3.3 上全绿** —— `guards` 31、`prune` 11、`approval-details` 27、`sandbox-guard` 18、`setup` 41、`removal-scope` 24、`config-safety` 404、`real-configs` 66(本机 48)、`package-check` 14、`roots` 12、`workspace-titles` 15、`read-cursor` 10、`sentinel-redaction` 11、`forget-tombstone` 7。全部退出码 0,都不需要模型、不需要装 DSH、也不需要 provider。
|
|
542
601
|
|
|
543
|
-
其中 **
|
|
602
|
+
其中 **535 项属于客户端注册器套件**(`setup`、`removal-scope`、`config-safety`、`real-configs`;本机 5/9 配置时 517 项)—— 0.3 的活其实都在这里。
|
|
544
603
|
|
|
545
|
-
需要模型的套件(`smoke` 29、`boundary`
|
|
604
|
+
需要模型的套件(`smoke` 29、`boundary` 46、`integration` 30、`async` 16、`sentinel` 36、`timeout` 38、`lock` 79、`cycle` 36、`concurrency` 31、`capability` 22、`workspace-effect` 24、`acceptance` 36、`multi` 19、`permission` 11、`list` 13)合计 466 项,**完整实测停在 0.1.2** —— 它们要可用 provider,发版前请自己重跑一遍:`node test/run.mjs --all`。`acceptance` 在没有 IDA MCP(`mcp__ida__*`)时会明确 **skip**。
|
|
546
605
|
|
|
547
|
-
> 数字取各套件自己打印的汇总,不在这里手抄。`smoke`/`prune`/`guards`/`sandbox-guard`/`approval-details`/`boundary`/`setup`/`removal-scope`/`config-safety`/`package-check` 不调用模型;其余需要可用的 provider。
|
|
606
|
+
> 数字取各套件自己打印的汇总,不在这里手抄。`smoke`/`prune`/`guards`/`sandbox-guard`/`approval-details`/`boundary`/`setup`/`removal-scope`/`config-safety`/`package-check`/`read-cursor`/`sentinel-redaction`/`forget-tombstone` 不调用模型;其余需要可用的 provider。
|
|
548
607
|
|
|
549
608
|
### 🧯 为什么会有 `sandbox-guard`
|
|
550
609
|
|
package/USAGE-NOTES.en.md
CHANGED
|
@@ -38,7 +38,7 @@ Practical gotchas and trade-offs you *will* hit when letting Claude / Codex driv
|
|
|
38
38
|
|
|
39
39
|
**2.5 Run records are memory-only, last 20.** An old `run_id` eventually returns `unknown run_id`. Persist important results yourself, drain `dsh_read` (also memory, bounded to 500 entries), or — best with `wait=false` — **collect from the completion sentinel file**, which carries the result, is immune to the in-memory window, and survives restarts.
|
|
40
40
|
|
|
41
|
-
**2.6 Sentinel files consume disk.** They are a *latch*: written once and kept so a late waiter never misses one, so they don't vanish on their own. The service **prunes sentinels older than 7 days at startup** (`DSH_MCP_SENTINEL_TTL_MS`, `0` disables); have your waiter `rm` after consuming. Sentinels **contain no reasoning** — `result.thinking` is stripped and
|
|
41
|
+
**2.6 Sentinel files consume disk.** They are a *latch*: written once and kept so a late waiter never misses one, so they don't vanish on their own. The service **prunes sentinels older than 7 days at startup** (`DSH_MCP_SENTINEL_TTL_MS`, `0` disables); have your waiter `rm` after consuming. Sentinels **contain no reasoning** — `result.thinking` is stripped and a boolean `thinking_omitted` is always present. They also carry usage and **credential-redacted tool inputs**, so treat them as sensitive files.
|
|
42
42
|
|
|
43
43
|
## 3. Concurrency and lifecycle
|
|
44
44
|
|
|
@@ -98,9 +98,11 @@ Practical gotchas and trade-offs you *will* hit when letting Claude / Codex driv
|
|
|
98
98
|
|
|
99
99
|
**7.1 Silent by default** — not one byte. MCP's stdout is the protocol channel, and stderr lands in Claude/Codex's logs.
|
|
100
100
|
|
|
101
|
-
**7.2
|
|
101
|
+
**7.2 This server never writes reasoning to its own disk artifacts — with one deliberate exception.** It is not logged, and the state file holds metadata only. Only `dsh_read(include_reasoning=true)` returns live reasoning of the currently running turn, discarded when it ends.
|
|
102
102
|
|
|
103
|
-
**The exception: the async completion sentinel.** It must persist the *answer* (so a caller can collect offline), but `result.thinking` is **stripped** and replaced by `thinking_omitted
|
|
103
|
+
**The exception: the async completion sentinel.** It must persist the *answer* (so a caller can collect offline), and it also carries usage and credential-redacted tool inputs, but `result.thinking` is **stripped** and replaced by a boolean `thinking_omitted` (`true` when removed, `false` when there was none) — even when `reasoning=full` was requested, no reasoning text appears in the file (a test guards this), while `dsh_get(run_id)` still returns it from memory. `DSH_MCP_SENTINEL_INCLUDE_REASONING=1` breaks this on purpose.
|
|
104
|
+
|
|
105
|
+
> ⚠️ **DSH's own session log (`~/.dsh/sessions/**/session.v4.jsonl.zstd`) contains the full reasoning text**, even with `reasoning=hide`. That is DSH's storage (what makes resume possible) and is outside this server's control; the guarantee above covers *this server's* logs, sentinels and registry only.
|
|
104
106
|
|
|
105
107
|
**7.3 Be careful with `DSH_MCP_LOG_STDERR=1`** — it forwards DSH's raw stderr, which may contain reasoning. Local debugging only.
|
|
106
108
|
|
package/USAGE-NOTES.md
CHANGED
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
### 2.6 哨兵文件会占磁盘
|
|
73
73
|
- **现象**:`.state/runs/<会话id>/` 下文件越攒越多,每个都含完整正文。
|
|
74
74
|
- **原因**:哨兵是**闩锁**——写一次并保留,晚到的等待方才能不漏,所以它不会自己消失。
|
|
75
|
-
- **对策**:服务**启动时会清掉超过 7 天的**(`DSH_MCP_SENTINEL_TTL_MS` 调,`0` 关闭)。等待任务消费后顺手 `rm` 更干净。哨兵**不含思考内容**(`result.thinking`
|
|
75
|
+
- **对策**:服务**启动时会清掉超过 7 天的**(`DSH_MCP_SENTINEL_TTL_MS` 调,`0` 关闭)。等待任务消费后顺手 `rm` 更干净。哨兵**不含思考内容**(`result.thinking` 被剥掉,并始终给出布尔 `thinking_omitted`);它还会包含用量与**已按审批规则脱敏的工具输入**,仍应按敏感文件对待。
|
|
76
76
|
|
|
77
77
|
---
|
|
78
78
|
|
|
@@ -233,10 +233,12 @@
|
|
|
233
233
|
### 7.1 默认完全静默
|
|
234
234
|
本服务默认**一个字节都不输出**。这不是洁癖:MCP 的 stdout 是协议通道(混东西进去会破坏报文),而 stderr 会进 Claude/Codex 的日志。
|
|
235
235
|
|
|
236
|
-
### 7.2
|
|
237
|
-
|
|
236
|
+
### 7.2 本服务不把思考写进自己的落盘物(有一个**刻意留下**的例外)
|
|
237
|
+
不写日志;状态文件与侧车只有元数据(id / cwd / 权限 / 计数)。只有 `dsh_read(include_reasoning=true)` 才会返回**当前正在跑那一轮**的实时思考,回合结束即丢弃。
|
|
238
238
|
|
|
239
|
-
**例外:异步完成哨兵。**
|
|
239
|
+
**例外:异步完成哨兵。** 它必须把**正文**写进文件(否则调用方无法离线收活),载荷还包含用量与(已脱敏的)工具输入,但 `result.thinking` 会被**剥掉**并给出布尔 `thinking_omitted`(剥离了为 true,本来没有为 false)—— 即使你请求了 `reasoning=full`,哨兵里也不会出现思考文本(有测试专门守着这一点),同一 run 在内存里仍能看到它。要刻意打破这条保证,设 `DSH_MCP_SENTINEL_INCLUDE_REASONING=1`。
|
|
240
|
+
|
|
241
|
+
> ⚠️ **DSH 自己的会话日志 `~/.dsh/sessions/**/session.v4.jsonl.zstd` 含完整 reasoning**(即使 `reasoning=hide` 也照存)。那是 DSH 的存储、resume 的依据,不在本服务控制范围内;上面的“不落盘”只适用于本服务自己的日志/哨兵/注册表。
|
|
240
242
|
|
|
241
243
|
### 7.3 `DSH_MCP_LOG_STDERR=1` 要谨慎
|
|
242
244
|
它会转发 DSH 子进程的原始 stderr,**其中可能包含思考内容**。只在本地排障时临时开。
|
package/bin/dsh-any-agent.mjs
CHANGED
|
@@ -53,6 +53,7 @@ const { createLogger } = await import('../src/config.mjs');
|
|
|
53
53
|
const { Hub } = await import('../src/hub.mjs');
|
|
54
54
|
const { createMcpServer } = await import('../src/mcp.mjs');
|
|
55
55
|
const { listOrphans, pruneDeadMarkers } = await import('../src/locks.mjs');
|
|
56
|
+
const { createHandlers } = await import('../src/tools.mjs');
|
|
56
57
|
|
|
57
58
|
const log = createLogger();
|
|
58
59
|
|
|
@@ -78,7 +79,9 @@ if (deadMarkers.removed || orphans.length) {
|
|
|
78
79
|
}
|
|
79
80
|
if (orphans.length) log.info(`[dsh-any-agent] 孤儿持有者会话: ${orphans.map((o) => o.conversation_id).join(', ')}`);
|
|
80
81
|
|
|
81
|
-
|
|
82
|
+
// dsh_start 不传 cwd 时,用客户端当前打开的项目目录(MCP roots)。
|
|
83
|
+
// 这样「客户端里看到哪个项目,DSH 就在哪个项目干活」是默认行为。
|
|
84
|
+
const server = createMcpServer({ hub, log, handlers: createHandlers(hub, { projectDir: () => server.projectDir() }) });
|
|
82
85
|
server.start();
|
|
83
86
|
|
|
84
87
|
log.info(`[dsh-any-agent] 已启动(注册表载入 ${hub.conversations.size} 个会话,日志级别 ${log.level})`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-any-agent",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "Drive DeepSeek Harness (DSH) as a long-lived coding agent from any MCP client — Claude Code, Claude Desktop, Codex, Cursor, Windsurf, Zed, Gemini CLI, opencode, VS Code. Resume-aware, interruptible, approval-capable, and it only ever touches the one config entry it owns.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
"USAGE-NOTES.md",
|
|
38
38
|
"USAGE-MIGRATION.md",
|
|
39
39
|
"LICENSE",
|
|
40
|
-
"DESIGN.md"
|
|
40
|
+
"DESIGN.md",
|
|
41
|
+
"scripts/"
|
|
41
42
|
],
|
|
42
43
|
"scripts": {
|
|
43
44
|
"start": "node ./bin/dsh-any-agent.mjs",
|
|
@@ -45,7 +46,8 @@
|
|
|
45
46
|
"test": "node ./test/run.mjs",
|
|
46
47
|
"test:smoke": "node ./test/smoke.mjs",
|
|
47
48
|
"test:guards": "node ./test/guards.mjs",
|
|
48
|
-
"test:prune": "node ./test/prune.mjs"
|
|
49
|
+
"test:prune": "node ./test/prune.mjs",
|
|
50
|
+
"commit": "node ./scripts/commit.mjs"
|
|
49
51
|
},
|
|
50
52
|
"engines": {
|
|
51
53
|
"node": ">=22.15"
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 提交/打标签,绝不写临时文件。
|
|
4
|
+
*
|
|
5
|
+
* 为什么要有这个脚本:过去几次发布都把 _cm.txt、_msg.cjs 这类临时文件
|
|
6
|
+
* 一起提交进了仓库 —— 因为生成多行提交信息的老办法是「先写文件,再
|
|
7
|
+
* git commit -F 那个文件」。文件一旦落盘,只要有一条 git add -A 就会顺手
|
|
8
|
+
* 把它加进索引,事后还得补一个清理提交。
|
|
9
|
+
*
|
|
10
|
+
* 正确做法是根本不让它落盘:提交信息走 stdin。
|
|
11
|
+
*
|
|
12
|
+
* node scripts/commit.mjs -m "标题" -m "正文第一段" ...
|
|
13
|
+
* node scripts/commit.mjs --file - 从 stdin 读
|
|
14
|
+
* node scripts/commit.mjs --tag v1.2.3 -m "..." 打附注标签
|
|
15
|
+
*
|
|
16
|
+
* 安全阀:提交前检查索引里有没有下划线开头的临时文件或 *.tmp-dsh-* 残留,
|
|
17
|
+
* 有就直接失败并列出它们 —— 宁可中断,也不要再制造一个「发布提交里混进
|
|
18
|
+
* 垃圾文件」的历史。
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { spawnSync } from 'node:child_process';
|
|
22
|
+
import { readFileSync } from 'node:fs';
|
|
23
|
+
|
|
24
|
+
const argv = process.argv.slice(2);
|
|
25
|
+
const has = (f) => argv.includes(f);
|
|
26
|
+
const val = (f, d) => {
|
|
27
|
+
const i = argv.indexOf(f);
|
|
28
|
+
return i >= 0 && argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[i + 1] : d;
|
|
29
|
+
};
|
|
30
|
+
const vals = (f) => {
|
|
31
|
+
const out = [];
|
|
32
|
+
for (let i = 0; i < argv.length; i++) {
|
|
33
|
+
if (argv[i] === f && argv[i + 1] && !argv[i + 1].startsWith('--')) out.push(argv[i + 1]);
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
if (has('--help') || has('-h') || argv.length === 0) {
|
|
39
|
+
process.stdout.write(
|
|
40
|
+
'commit.mjs -- commit or tag without ever writing a message file\n\n' +
|
|
41
|
+
' node scripts/commit.mjs -m "subject" -m "body paragraph"\n' +
|
|
42
|
+
' node scripts/commit.mjs --tag v1.2.3 -m "release notes"\n' +
|
|
43
|
+
' node scripts/commit.mjs --file - < msg.txt\n\n' +
|
|
44
|
+
'Messages go to git over stdin (git commit -F -), so no temp file exists\n' +
|
|
45
|
+
'and no git add -A can sweep one into the tree. Refuses to run if the\n' +
|
|
46
|
+
'index already contains scratch files (leading underscore) or *.tmp-dsh-*.\n',
|
|
47
|
+
);
|
|
48
|
+
process.exit(has('--help') || has('-h') ? 0 : 1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const DRY = has('--dry-run');
|
|
52
|
+
const TAG = val('--tag');
|
|
53
|
+
const ALLOW_TEMP = has('--allow-staged-temp');
|
|
54
|
+
|
|
55
|
+
/** 组装提交信息:每段之间一个空行。 */
|
|
56
|
+
function buildMessage() {
|
|
57
|
+
const fileArg = val('--file');
|
|
58
|
+
if (fileArg) {
|
|
59
|
+
const text = fileArg === '-' ? readFileSync(0, 'utf8') : readFileSync(fileArg, 'utf8');
|
|
60
|
+
// 统一成 LF,并且去掉结尾的空白行 —— git 会把它们当正文保留。
|
|
61
|
+
return text.replace(/\r\n/g, '\n').replace(/[ \t]+$/gm, '').replace(/\n+$/, '');
|
|
62
|
+
}
|
|
63
|
+
const paras = vals('-m');
|
|
64
|
+
if (!paras.length) {
|
|
65
|
+
process.stderr.write('需要至少一个 -m,或 --file <path|->。用 --help 看用法。\n');
|
|
66
|
+
process.exit(2);
|
|
67
|
+
}
|
|
68
|
+
// 每段各自去掉首尾空白:残留的空行会被 git 原样保留进提交信息。
|
|
69
|
+
return paras.map((s) => s.trim()).filter(Boolean).join('\n\n');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function git(args, input) {
|
|
73
|
+
const r = spawnSync('git', args, {
|
|
74
|
+
// 三种 stdio 都继承,git 自己的报错才看得见;只有 stdin 接管道。
|
|
75
|
+
stdio: input === undefined ? 'inherit' : ['pipe', 'inherit', 'inherit'],
|
|
76
|
+
input,
|
|
77
|
+
encoding: 'utf8',
|
|
78
|
+
});
|
|
79
|
+
return r.status ?? 1;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** 索引里不该有的东西。宁可在这一步失败。 */
|
|
83
|
+
function scratchInIndex() {
|
|
84
|
+
const r = spawnSync('git', ['diff', '--cached', '--name-only'], { encoding: 'utf8' });
|
|
85
|
+
if (r.status !== 0) return [];
|
|
86
|
+
return r.stdout
|
|
87
|
+
.split('\n')
|
|
88
|
+
.map((s) => s.trim())
|
|
89
|
+
.filter(Boolean)
|
|
90
|
+
.filter((p) => {
|
|
91
|
+
const base = p.split('/').pop();
|
|
92
|
+
return base.startsWith('_') || /\.tmp-dsh-/.test(base) || /\.bak-dsh-/.test(base);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const message = buildMessage();
|
|
97
|
+
if (!message.trim()) {
|
|
98
|
+
process.stderr.write('提交信息为空,拒绝执行。\n');
|
|
99
|
+
process.exit(2);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!ALLOW_TEMP) {
|
|
103
|
+
const scratch = scratchInIndex();
|
|
104
|
+
if (scratch.length) {
|
|
105
|
+
process.stderr.write('索引里有临时/备份文件,拒绝提交:\n');
|
|
106
|
+
for (const p of scratch) process.stderr.write(' ' + p + '\n');
|
|
107
|
+
process.stderr.write('先 git restore --staged 它们,或确认无误后加 --allow-staged-temp。\n');
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const args = TAG ? ['tag', '-a', TAG, '-F', '-'] : ['commit', '-F', '-'];
|
|
113
|
+
|
|
114
|
+
if (DRY) {
|
|
115
|
+
process.stdout.write('[dry-run] git ' + args.join(' ') + ' <<< 信息走 stdin,不落盘\n');
|
|
116
|
+
process.stdout.write('----\n' + message + '\n----\n');
|
|
117
|
+
process.exit(0);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 关键一行:信息从 stdin 进 git,磁盘上不存在任何信息文件。
|
|
121
|
+
const code = git(args, message);
|
|
122
|
+
process.exit(code);
|
package/src/config.mjs
CHANGED
|
@@ -228,7 +228,7 @@ export const MAX_LIVE = Number.isFinite(rawMaxLive) && rawMaxLive > 0 ? Math.flo
|
|
|
228
228
|
*
|
|
229
229
|
* 为什么要缓存:列会话时要 spawn 一个 DSH 进程做 `session/list`(**实测约 1 秒**),
|
|
230
230
|
* 连着调几次就反复拉进程。缓存 10 秒既省掉这些开销,也不至于让"别的实例刚建的会话"久等。
|
|
231
|
-
*
|
|
231
|
+
* 想排除磁盘上未打开的会话可传 `include_closed=false`(只列本服务已打开的,毫秒级)。
|
|
232
232
|
*/
|
|
233
233
|
export const LIST_PROBE_TTL_MS = numEnv('DSH_MCP_LIST_PROBE_TTL_MS', 10_000);
|
|
234
234
|
|