dsh-any-agent 0.3.7 → 0.3.9
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/CHANGELOG.md +89 -2
- package/DESIGN.md +13 -5
- package/README.md +36 -24
- package/README.zh.md +26 -22
- package/USAGE-MIGRATION.en.md +162 -0
- package/USAGE-MIGRATION.md +3 -3
- package/USAGE-NOTES.en.md +4 -4
- package/USAGE-NOTES.md +9 -9
- package/bin/restore.mjs +6 -2
- package/bin/setup.mjs +6 -0
- package/package.json +3 -1
- package/src/acp.mjs +8 -10
- package/src/clients.mjs +13 -2
- package/src/hub.mjs +99 -33
- package/src/line-decoder.mjs +41 -0
- package/src/locks.mjs +6 -1
- package/src/mcp.mjs +3 -6
- package/src/tools.mjs +5 -2
- package/src/workspace.mjs +24 -8
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# Moving to a new machine / first-time setup (English)
|
|
2
|
+
|
|
3
|
+
This is the short English version of `USAGE-MIGRATION.md`. It covers a fresh
|
|
4
|
+
install, registering the MCP server with your client, and the checks worth
|
|
5
|
+
doing before you trust it with real work.
|
|
6
|
+
|
|
7
|
+
## 1. Prerequisites
|
|
8
|
+
|
|
9
|
+
| Item | Requirement |
|
|
10
|
+
|---|---|
|
|
11
|
+
| Node.js | **≥ 22.15** (`zstdDecompressSync` is required) |
|
|
12
|
+
| DSH | DeepSeek Harness installed and able to answer a normal chat turn |
|
|
13
|
+
| Provider | A model route configured in DSH, with credentials available through `~/.dsh/.credentials.yaml` or environment variables |
|
|
14
|
+
|
|
15
|
+
> The order matters. This server does **not** ship a provider or credentials.
|
|
16
|
+
> On first start it copies the model route from an existing DSH profile. If DSH
|
|
17
|
+
> itself is not configured, the first call fails with a provider error.
|
|
18
|
+
|
|
19
|
+
## 2. Install and register
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install -g dsh-any-agent
|
|
23
|
+
|
|
24
|
+
# Preview only: reads configs, writes nothing
|
|
25
|
+
dsh-any-agent-setup
|
|
26
|
+
|
|
27
|
+
# Apply
|
|
28
|
+
dsh-any-agent-setup --yes
|
|
29
|
+
|
|
30
|
+
# Limit to some clients, or choose a permission tier
|
|
31
|
+
dsh-any-agent-setup --yes --only cursor,zed
|
|
32
|
+
dsh-any-agent-setup --yes --permission workspace-write
|
|
33
|
+
|
|
34
|
+
# Remove only this server's entry
|
|
35
|
+
dsh-any-agent-setup --remove --yes
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Supported clients: **Claude Code, Claude Desktop, Codex, Cursor, Windsurf,
|
|
39
|
+
Zed, Gemini CLI, opencode, VS Code**. Clients that are not installed are
|
|
40
|
+
reported as skipped, not failed.
|
|
41
|
+
|
|
42
|
+
The writer is deliberately conservative:
|
|
43
|
+
|
|
44
|
+
- preview mode never writes and never starts a child process;
|
|
45
|
+
- every changed file is backed up as `<file>.bak-dsh-<stamp>`;
|
|
46
|
+
- JSON/JSONC/TOML edits are text-level and only touch the
|
|
47
|
+
`dsh-any-agent` entry (indentation, key order, CRLF, BOM and comments are
|
|
48
|
+
preserved);
|
|
49
|
+
- malformed or read-only configs are refused rather than overwritten;
|
|
50
|
+
- a second `--yes` is a no-op;
|
|
51
|
+
- `setup` does **not** invoke client CLIs such as `claude mcp get` (that can
|
|
52
|
+
make Claude Code rewrite `~/.claude.json`).
|
|
53
|
+
|
|
54
|
+
## 3. Manual configuration (optional)
|
|
55
|
+
|
|
56
|
+
If your client is not covered by setup, add a stdio MCP server. For example:
|
|
57
|
+
|
|
58
|
+
Codex `~/.codex/config.toml`:
|
|
59
|
+
|
|
60
|
+
```toml
|
|
61
|
+
[mcp_servers.dsh-any-agent]
|
|
62
|
+
command = "dsh-any-agent"
|
|
63
|
+
args = []
|
|
64
|
+
startup_timeout_sec = 20.0
|
|
65
|
+
tool_timeout_sec = 120.0
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Generic JSON client:
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"mcpServers": {
|
|
73
|
+
"dsh-any-agent": {
|
|
74
|
+
"command": "dsh-any-agent",
|
|
75
|
+
"args": []
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`setup --yes` writes the absolute node path, the entry file, and
|
|
82
|
+
`DSH_MCP_PERMISSION` / `DSH_MCP_LOG`; the examples above assume
|
|
83
|
+
`dsh-any-agent` is on `PATH`.
|
|
84
|
+
|
|
85
|
+
## 4. First start and profile bootstrap
|
|
86
|
+
|
|
87
|
+
On first start the server creates `~/.dsh/profiles/dsh-mcp/` and copies the
|
|
88
|
+
model route from an existing DSH profile (`home` → `desktop` → `web` →
|
|
89
|
+
`headless` → `acp`, first usable one). It uses a built-in zero-dependency YAML
|
|
90
|
+
subset parser, so no extra npm package is needed.
|
|
91
|
+
|
|
92
|
+
What is copied: `apiKeyEnv`, `baseURL`, `api`, `models`, reasoning metadata.
|
|
93
|
+
What is stripped: literal API keys, nested `headers`, and URL userinfo. The
|
|
94
|
+
generated profile includes the three permission presets but **never**
|
|
95
|
+
`defaultPreset` (that would override the permission tier at session creation).
|
|
96
|
+
|
|
97
|
+
Credentials remain in DSH's own credential store; this project never owns them.
|
|
98
|
+
Set `DSH_MCP_BOOTSTRAP=0` to disable profile writing entirely, or
|
|
99
|
+
`DSH_MCP_ROUTE_SOURCE=<profile>` / `__home__` to pin the route source.
|
|
100
|
+
|
|
101
|
+
## 5. Permission tiers
|
|
102
|
+
|
|
103
|
+
| Tier | Meaning |
|
|
104
|
+
|---|---|
|
|
105
|
+
| `read-only` | DSH's sandbox is read-only; approvals are forced to `auto-deny` |
|
|
106
|
+
| `workspace-write` | Writes are limited to the workspace by DSH's sandbox |
|
|
107
|
+
| `danger-full-access` | No workspace sandbox |
|
|
108
|
+
|
|
109
|
+
The server's raw default is `danger-full-access` when `DSH_MCP_PERMISSION` is
|
|
110
|
+
not set; `setup` writes `workspace-write` into the client config. Out-of-
|
|
111
|
+
workspace approval requests default to `auto-allow`; pass `on_approval=ask`
|
|
112
|
+
when creating a session if you want `dsh_approval_decide` to adjudicate them.
|
|
113
|
+
|
|
114
|
+
`read-only` is a hard boundary at this layer: `read-only + auto-allow/ask` is
|
|
115
|
+
rejected, and the server reads back the session's recorded permission tier
|
|
116
|
+
after create/resume. If the projection cache has no record within the DSH
|
|
117
|
+
flush window, read-only **fails closed** instead of relying on approvals.
|
|
118
|
+
External MCP tools mounted in DSH are outside DSH's sandbox; read
|
|
119
|
+
`USAGE-NOTES.en.md` before using them with untrusted data.
|
|
120
|
+
|
|
121
|
+
## 6. Verify
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
dsh-any-agent --version
|
|
125
|
+
dsh-any-agent-setup --help
|
|
126
|
+
dsh-any-agent-setup # preview should show your clients
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
In the client, ask the agent to call `dsh_start` with a small project
|
|
130
|
+
directory, then `dsh_send` a short prompt and `dsh_get` the result. If the
|
|
131
|
+
client cannot see the server, restart the client after setup.
|
|
132
|
+
|
|
133
|
+
## 7. Common problems
|
|
134
|
+
|
|
135
|
+
- **`DSH CLI entry not found`** — set `DSH_BIN` to
|
|
136
|
+
`.../node_modules/@deepseek-ai/dsh/lib/bin.js`, or install DSH in a location
|
|
137
|
+
the server can discover.
|
|
138
|
+
- **`no adapter registered for provider` / no API key** — DSH itself is not
|
|
139
|
+
configured, or the profile route was not copied. Open DSH once, configure a
|
|
140
|
+
model, then restart the MCP server.
|
|
141
|
+
- **`session ... is already owned by an active write handle`** — the session
|
|
142
|
+
is held by another process (often the DSH GUI). Use `dsh_status` to see
|
|
143
|
+
`lock_holder`; use `dsh_release` on the other instance or `dsh_takeover`
|
|
144
|
+
when the holder is a stale dsh-any-agent process. Never kill a GUI process.
|
|
145
|
+
- **Nothing happens when waiting for completion** — use the returned
|
|
146
|
+
`sentinel_file`; a turn that is interrupted by a process exit may not land
|
|
147
|
+
one. Inspect the workspace and re-dispatch.
|
|
148
|
+
- **Old state after upgrading** — the registry moved from
|
|
149
|
+
`<repo>/.state/conversations.json` to
|
|
150
|
+
`$DSH_HOME/.dsh-any-agent/conversations.json`; the first 0.3.x start
|
|
151
|
+
migrates it automatically. Use `DSH_MCP_STATE` to override.
|
|
152
|
+
|
|
153
|
+
## 8. Upgrade / rollback
|
|
154
|
+
|
|
155
|
+
`npm install -g dsh-any-agent@latest` upgrades the server. Client configs are
|
|
156
|
+
not rewritten by an upgrade; run `dsh-any-agent-setup` again only if you want
|
|
157
|
+
to refresh the entry or permission env. `dsh-any-agent-restore` can restore a
|
|
158
|
+
client config from a `.bak-dsh-*` backup.
|
|
159
|
+
|
|
160
|
+
For the full Chinese guide, environment-variable reference and write-lock
|
|
161
|
+
measurements, see `USAGE-MIGRATION.md`, `README.md` and `USAGE-NOTES.en.md`.
|
|
162
|
+
Website build/deploy notes live in `website/README.md`.
|
package/USAGE-MIGRATION.md
CHANGED
|
@@ -294,13 +294,13 @@ codex mcp list # 应看到 dsh-any-agent ... enabled
|
|
|
294
294
|
|
|
295
295
|
### 权限档怎么选
|
|
296
296
|
|
|
297
|
-
`DSH_MCP_PERMISSION`
|
|
297
|
+
服务端未设置 `DSH_MCP_PERMISSION` 时默认 `danger-full-access`;`setup --yes` 默认写入 **`workspace-write`**。建议按用途改:
|
|
298
298
|
|
|
299
299
|
| 档位 | 适用于 |
|
|
300
300
|
|---|---|
|
|
301
301
|
| `danger-full-access` | 自己的项目,追求速度 |
|
|
302
|
-
| `workspace-write` |
|
|
303
|
-
| `read-only` |
|
|
302
|
+
| `workspace-write` | **setup 默认**;DSH 沙箱限制可写范围,越界请求默认 auto-allow,传 `on_approval=ask` 才挂起审批 |
|
|
303
|
+
| `read-only` | 分析不信任的样本(强制 `on_approval=auto-deny`) |
|
|
304
304
|
|
|
305
305
|
`DSH_MCP_APPROVAL` 三选一:`auto-allow` / `auto-deny` / `ask`。
|
|
306
306
|
`ask` 时待决审批会出现在 `dsh_status` 里,用 `dsh_approval_decide` 裁决。
|
package/USAGE-NOTES.en.md
CHANGED
|
@@ -52,7 +52,7 @@ Practical gotchas and trade-offs you *will* hit when letting Claude / Codex driv
|
|
|
52
52
|
|
|
53
53
|
**3.5 ★ The write lock: one writer per conversation at a time.** A session suddenly stops responding with a vague error (`Internal error`, or "this session is in use") and re-dispatching doesn't help. Cause: a DSH session directory carries a **cross-process write lock** whose semantics are "**the holder keeps it while it lives**", with **no API to take it from a live holder**. This server keeps a DSH process alive per conversation, so the two sides are **mutually exclusive**: while we hold a conversation your own DSH GUI cannot open it, and while your GUI has it open our `session/resume` cannot acquire it. The error is opaque because `SessionAlreadyOwnedError` is not a standard JSON-RPC error — ACP wraps it as a generic `-32603 "Internal error"` and hides the real cause in `error.data` (this server now surfaces and translates it). Fix: read `lock_holder` from `dsh_status` — `stale-mcp` (alive but wedged: its heartbeat expired; a real crash releases the lock automatically) → `dsh_takeover` takes it without `force` and kills that process; `live-mcp` (another live dsh-any-agent instance) → `dsh_release` on that side, or `dsh_takeover(force=true)`; `none` (most likely your GUI has it open) → **never killed by us** — close it in that window and retry.
|
|
54
54
|
|
|
55
|
-
**3.6 ★ "Taking the lock" means killing the holder, so we must know who that is.** The lock is a Windows named kernel semaphore, released by the
|
|
55
|
+
**3.6 ★ "Taking the lock" means killing the holder, so we must know who that is.** The lock is a Windows named kernel semaphore (or POSIX `flock`), released by the OS when the holder process dies — killing is the *only* way to take it. So this server registers a conversation **only once it actually holds it** (`<state dir>/locks/<conversation id>.json`, with pid + heartbeat). Holders it cannot identify are **never killed**. Why the GUI is untouchable: GUI conversations run inside the single **`dsh web` process** (not one process per conversation), so killing the lock holder would take down your entire UI *and every GUI conversation in that process, including the one you're reading*. Sessions on disk survive resumption, but in-flight turns are lost. **Operational rule: never open a conversation this server drives in your DSH GUI.** To view it there, `dsh_release` first and dispatch again afterwards (it resumes automatically).
|
|
56
56
|
|
|
57
57
|
**3.7 ★ Crash vs. wedge — measured, don't guess.** A **crash** (the MCP is killed or restarted): the DSH children it spawned **exit with it** — their stdio is a pipe to the parent, so closing it gives them EOF and they shut down. **The lock is therefore released automatically; no orphan keeps holding it**, so a restart always gets it back. The cost: **in-flight turns are interrupted and no sentinel lands** (the process is gone) — don't wait for that file, inspect the workspace (`git status`, files) and decide whether to re-dispatch. A **wedge** (the MCP is alive but stalled; no heartbeat for 90 s) is what `stale-mcp` really means, and `dsh_takeover` handles it **without `force`**. Judge with `dsh_status`'s `lock_holder` — and note that `none` does not mean "the lock is free"; it can also mean "a process we don't manage holds it" (usually the GUI).
|
|
58
58
|
|
|
@@ -68,9 +68,9 @@ Practical gotchas and trade-offs you *will* hit when letting Claude / Codex driv
|
|
|
68
68
|
|
|
69
69
|
## 4. Permissions and safety
|
|
70
70
|
|
|
71
|
-
**4.1 The default is full access
|
|
71
|
+
**4.1 The raw server default is full access; `setup` writes `workspace-write`.** With no `DSH_MCP_PERMISSION`, DSH can touch anything your account can. That default exists because, unattended, ACP's approval channel tends to degrade into silent failures. `dsh-any-agent-setup --yes` writes `workspace-write` into the client config unless you pass `--permission`. For untrusted material (malware, unknown dumps, unfamiliar repos) use `permission: "read-only"`.
|
|
72
72
|
|
|
73
|
-
**4.2 `approval: never`
|
|
73
|
+
**4.2 The public approval policy is `on_approval` (`auto-allow` / `auto-deny` / `ask`).** DSH's internal `approval: never` corresponds to `auto-deny`: it deterministically rejects prompts, it does not auto-allow. Under `danger-full-access` the sandbox produces no asks, so the distinction is invisible. To let the caller adjudicate escalation, use `workspace-write` + `on_approval: "ask"` + `dsh_approval_decide`.
|
|
74
74
|
|
|
75
75
|
**4.3 ★ Side effects of *external MCP tools* are NOT covered by DSH's sandbox.** DSH's permission tiers govern DSH's own file and shell tools. Tools from an MCP server mounted into DSH (e.g. IDA Pro's `patch` / `rename` / `set_comments`) execute **inside that MCP server**, where DSH's tier has no effect. For read-only work, forbid write-class tools **in the prompt** and verify afterwards that `tools_used` contains none. Do not rely on `permission` alone.
|
|
76
76
|
|
|
@@ -130,7 +130,7 @@ Practical gotchas and trade-offs you *will* hit when letting Claude / Codex driv
|
|
|
130
130
|
1. **Give goals, not commands.** DSH is an agent: say "add tests for this module and make them pass", not "run pytest". It picks tools, plans steps, and spawns subagents on its own.
|
|
131
131
|
2. **One conversation per task** — mixing unrelated work pollutes history and costs more.
|
|
132
132
|
3. **Split long work into dispatch + collect**: `wait=false` → do something else → `dsh_get(run_id)`.
|
|
133
|
-
4. **Review output via workspace changes** — `workspace_changed`
|
|
133
|
+
4. **Review output via workspace changes** — `workspace_changed` is true when the `git status` entry set changed across the turn; `diff_stat` lists the current dirty files (it is not a per-turn diff).
|
|
134
134
|
5. **Ask for a plan first** if you want to approve before execution (DSH has plan mode).
|
|
135
135
|
6. **Three cost knobs**: lower `reasoning_effort`, keep `reasoning=hide`, release and start fresh.
|
|
136
136
|
7. **Don't paste large code blocks into the prompt** — tell it which file to read instead; DSH has its own file tools.
|
package/USAGE-NOTES.md
CHANGED
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
- **对策**:用 `wait=false` 时**直接靠完成哨兵文件收活**(它自带结果、不受内存窗口限制、还扛服务重启);否则当场落盘,或用 `dsh_read` 把输出缓冲拉干(同样是内存、有界 500 条)。
|
|
71
71
|
|
|
72
72
|
### 2.6 哨兵文件会占磁盘
|
|
73
|
-
-
|
|
73
|
+
- **现象**:`$DSH_HOME/.dsh-any-agent/runs/<conversation_id>/` 下文件越攒越多,每个都含完整正文。
|
|
74
74
|
- **原因**:哨兵是**闩锁**——写一次并保留,晚到的等待方才能不漏,所以它不会自己消失。
|
|
75
75
|
- **对策**:服务**启动时会清掉超过 7 天的**(`DSH_MCP_SENTINEL_TTL_MS` 调,`0` 关闭)。等待任务消费后顺手 `rm` 更干净。哨兵**不含思考内容**(`result.thinking` 被剥掉,并始终给出布尔 `thinking_omitted`);它还会包含用量与**已按审批规则脱敏的工具输入**,仍应按敏感文件对待。
|
|
76
76
|
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
### 3.2 默认**不回收**空闲进程;显式设置 TTL 才回收
|
|
87
87
|
- **现象**:`dsh_status` 显示 `detached`(没有附着进程)。
|
|
88
88
|
- **原因**:默认 `DSH_MCP_IDLE_TTL_MS=0` = 永不回收,本服务一直握着写锁。显式设成毫秒数(如 `300000`)才会按 TTL 回收。**`detached` 不是错误** —— 会话本体在磁盘上,下次派活会自动 `session/resume` 复活,历史完整。
|
|
89
|
-
-
|
|
89
|
+
- **对策**:默认 `0` 已经就是“一直热着、永不回收”;只有你显式把 `DSH_MCP_IDLE_TTL_MS` 设成非零,才会按 TTL 回收。
|
|
90
90
|
|
|
91
91
|
### 3.3 同一个会话别在两处同时打开
|
|
92
92
|
- **现象**:你自己的 DSH GUI 打不开某个会话,或者对方报被占用。
|
|
@@ -112,7 +112,7 @@
|
|
|
112
112
|
- `none`(多半是你自己的 GUI 开着它)→ **本服务绝不杀**,请在那个窗口里关掉该会话后重试。
|
|
113
113
|
|
|
114
114
|
### 3.6 ★ "抢锁" = 杀掉持有者,所以必须先知道那是谁
|
|
115
|
-
-
|
|
115
|
+
- **原因**:Windows 上是命名内核信号量,POSIX 上是 `flock`;持有者进程一死,系统就释放 —— 这是**唯一**的夺锁手段。
|
|
116
116
|
- **所以**本服务只登记**自己的子进程在真正拿到锁之后**的持有状态(`<状态目录>/locks/<会话id>.json`,含 pid + 心跳)。判定不了的持有者**一律不杀**。
|
|
117
117
|
- **为什么对 GUI 绝不杀**:GUI 的会话跑在 **`dsh web` 那一个进程里**(不是一会话一进程)。杀掉占锁的它 = 整个界面 + **该进程里所有 GUI 会话(含你正在看的那个)一起断**。磁盘上的会话能 resume,但进行中的回合会丢。
|
|
118
118
|
- **操作铁律**:本服务驱动的会话**不要在 GUI 里打开**。要用 GUI 看,先 `dsh_release` 交还;看完再重派(会自动 resume)。
|
|
@@ -177,14 +177,14 @@
|
|
|
177
177
|
|
|
178
178
|
## 四、权限与安全(认真看)
|
|
179
179
|
|
|
180
|
-
### 4.1
|
|
181
|
-
-
|
|
182
|
-
-
|
|
180
|
+
### 4.1 服务端裸默认是"完全权限";setup 写入 `workspace-write`
|
|
181
|
+
- **现象**:未设置 `DSH_MCP_PERMISSION` 时,DSH 能碰你账户能碰的任何东西。
|
|
182
|
+
- **原因**:服务端裸默认 `danger-full-access` —— 这是为了零摩擦,因为**ACP 的审批通道在无人值守时很容易变成"静默失败"**。`dsh-any-agent-setup --yes` 默认会把 `workspace-write` 写进客户端配置(除非显式传 `--permission`)。
|
|
183
183
|
- **对策**:分析不信任的东西(恶意软件、来路不明的 dump、陌生仓库)时,用 `permission: "read-only"`(沙箱只读 + 审批一律拒绝)。
|
|
184
184
|
|
|
185
|
-
### 4.2 `approval: never`
|
|
185
|
+
### 4.2 公开审批参数是 `on_approval`;DSH 内部 `approval: never` 对应 `auto-deny`
|
|
186
186
|
- **现象**:以为关掉审批就万事大吉,结果某些操作还是失败。
|
|
187
|
-
-
|
|
187
|
+
- **原因**:DSH 内部的 `approval: never` = 每次询问都返回拒绝(确定性拒绝),不是自动放行;公开参数应写 `on_approval: "auto-deny"`。它在 `danger-full-access` 下没有副作用,是因为沙箱根本不产生询问。
|
|
188
188
|
- **对策**:需要"越界操作由调用方裁决"时,用 `permission: "workspace-write"` + `on_approval: "ask"`,再用 `dsh_approval_decide` 裁决。
|
|
189
189
|
|
|
190
190
|
### 4.3 ★ MCP 工具的副作用**不受** DSH 沙箱约束
|
|
@@ -271,7 +271,7 @@
|
|
|
271
271
|
1. **给它目标,不要给命令**。DSH 是 agent:说"把这个模块的测试补上并跑通",别说"执行 pytest"。它自己会选工具、分步骤、必要时派子代理。
|
|
272
272
|
2. **一个会话一件事**。别往一个会话里塞互不相关的任务,历史会互相污染且越来越贵。
|
|
273
273
|
3. **长任务拆成"派活 + 验收"**:`wait=false` → 干别的 → `dsh_get(run_id)`。
|
|
274
|
-
4.
|
|
274
|
+
4. **要审查产出就用工作区变更**:`workspace_changed` 表示本回合前后 `git status` 的条目集合发生了变化;`diff_stat` 列出当前仍处于改动状态的文件(不是本回合的精确 diff)。
|
|
275
275
|
5. **要它先想清楚再动手**:让它先给方案、你确认后再执行(DSH 有计划模式)。
|
|
276
276
|
6. **省钱的三个旋钮**:`reasoning_effort` 调低、`reasoning` 保持 `hide`、及时 `dsh_release` 并开新会话。
|
|
277
277
|
7. **别在提示词里重复贴大段代码**——告诉它去看哪个文件更划算(DSH 自己有文件工具)。
|
package/bin/restore.mjs
CHANGED
|
@@ -108,9 +108,13 @@ if (wantFile) {
|
|
|
108
108
|
process.exit(1);
|
|
109
109
|
}
|
|
110
110
|
} else {
|
|
111
|
-
//
|
|
111
|
+
// 每个文件只取最新的一份;但排除本工具自己写的 pre-restore 安全副本,
|
|
112
|
+
// 否则连续两次 --latest 会把上一次还原再撤回去。
|
|
112
113
|
const byTarget = new Map();
|
|
113
|
-
for (const b of all)
|
|
114
|
+
for (const b of all) {
|
|
115
|
+
if (b.stamp.startsWith('pre-restore-')) continue;
|
|
116
|
+
if (!byTarget.has(b.target)) byTarget.set(b.target, b);
|
|
117
|
+
}
|
|
114
118
|
chosen = [...byTarget.values()];
|
|
115
119
|
}
|
|
116
120
|
|
package/bin/setup.mjs
CHANGED
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
planClient,
|
|
39
39
|
resolveDirs,
|
|
40
40
|
} from '../src/clients.mjs';
|
|
41
|
+
import { PERMISSION_TIERS } from '../src/config.mjs';
|
|
41
42
|
|
|
42
43
|
/** 版本号以 package.json 为准,避免 npm version 之后这里变旧。 */
|
|
43
44
|
const VERSION = (() => {
|
|
@@ -115,6 +116,11 @@ const tick = (s) => log(' OK ' + s);
|
|
|
115
116
|
const dash = (s) => log(' --- ' + s);
|
|
116
117
|
const bang = (s) => log(' FAIL ' + s);
|
|
117
118
|
|
|
119
|
+
if (!PERMISSION_TIERS.includes(PERMISSION)) {
|
|
120
|
+
log(`--permission 必须是 ${PERMISSION_TIERS.join(' | ')} 之一;收到: ${JSON.stringify(PERMISSION)}`);
|
|
121
|
+
process.exit(2);
|
|
122
|
+
}
|
|
123
|
+
|
|
118
124
|
// ── 主流程 ─────────────────────────────────────────────────────────
|
|
119
125
|
|
|
120
126
|
const dirs = resolveDirs(HOME_OVERRIDE);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-any-agent",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.9",
|
|
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",
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"USAGE-NOTES.en.md",
|
|
37
37
|
"USAGE-NOTES.md",
|
|
38
38
|
"USAGE-MIGRATION.md",
|
|
39
|
+
"USAGE-MIGRATION.en.md",
|
|
39
40
|
"CHANGELOG.md",
|
|
40
41
|
"LICENSE",
|
|
41
42
|
"DESIGN.md",
|
|
@@ -45,6 +46,7 @@
|
|
|
45
46
|
"start": "node ./bin/dsh-any-agent.mjs",
|
|
46
47
|
"setup": "node ./bin/setup.mjs",
|
|
47
48
|
"test": "node ./test/run.mjs",
|
|
49
|
+
"test:all": "node ./test/run.mjs --all",
|
|
48
50
|
"test:smoke": "node ./test/smoke.mjs",
|
|
49
51
|
"test:guards": "node ./test/guards.mjs",
|
|
50
52
|
"test:prune": "node ./test/prune.mjs",
|
package/src/acp.mjs
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { spawn } from 'node:child_process';
|
|
13
13
|
import { ensureProfile } from './bootstrap.mjs';
|
|
14
14
|
import { DSH_PROFILE, PKG_VERSION, REQUEST_TIMEOUT_MS, resolveDshBin } from './config.mjs';
|
|
15
|
+
import { LineDecoder } from './line-decoder.mjs';
|
|
15
16
|
|
|
16
17
|
/** ACP 协议版本(@agentclientprotocol/sdk 的 v1)。 */
|
|
17
18
|
const PROTOCOL_VERSION = 1;
|
|
@@ -25,7 +26,8 @@ export class AcpProcess {
|
|
|
25
26
|
#child = null;
|
|
26
27
|
#nextId = 1;
|
|
27
28
|
#pending = new Map();
|
|
28
|
-
#
|
|
29
|
+
#lineDecoder = new LineDecoder();
|
|
30
|
+
#errDecoder = new LineDecoder();
|
|
29
31
|
#stderrTail = [];
|
|
30
32
|
#stopping = false;
|
|
31
33
|
#log = NOOP_LOG;
|
|
@@ -100,7 +102,7 @@ export class AcpProcess {
|
|
|
100
102
|
this.#child.stdout.on('error', streamError);
|
|
101
103
|
this.#child.stderr.on('error', streamError);
|
|
102
104
|
this.#child.stderr.on('data', (d) => {
|
|
103
|
-
for (const line of
|
|
105
|
+
for (const line of this.#errDecoder.push(d)) {
|
|
104
106
|
if (!line.trim()) continue;
|
|
105
107
|
this.#stderrTail.push(line.trim());
|
|
106
108
|
if (this.#stderrTail.length > STDERR_TAIL_LINES) this.#stderrTail.shift();
|
|
@@ -124,11 +126,7 @@ export class AcpProcess {
|
|
|
124
126
|
}
|
|
125
127
|
|
|
126
128
|
#onData(chunk) {
|
|
127
|
-
this.#
|
|
128
|
-
let idx;
|
|
129
|
-
while ((idx = this.#buf.indexOf('\n')) !== -1) {
|
|
130
|
-
const line = this.#buf.slice(0, idx);
|
|
131
|
-
this.#buf = this.#buf.slice(idx + 1);
|
|
129
|
+
for (const line of this.#lineDecoder.push(chunk)) {
|
|
132
130
|
if (!line.trim()) continue;
|
|
133
131
|
let msg;
|
|
134
132
|
try {
|
|
@@ -258,7 +256,7 @@ export class AcpProcess {
|
|
|
258
256
|
/** 优雅收尾:close 由调用方负责,这里只关 stdin(DSH 绑定 stdin EOF → 有界退出)。 */
|
|
259
257
|
async stop({ graceMs = 3000, killWaitMs = 5000 } = {}) {
|
|
260
258
|
const child = this.#child;
|
|
261
|
-
if (!child) return;
|
|
259
|
+
if (!child) return true;
|
|
262
260
|
this.#stopping = true;
|
|
263
261
|
try {
|
|
264
262
|
child.stdin.end();
|
|
@@ -273,7 +271,7 @@ export class AcpProcess {
|
|
|
273
271
|
});
|
|
274
272
|
});
|
|
275
273
|
// 1) 先给它体面退出的机会(stdin EOF → DSH 有界退出)
|
|
276
|
-
if (await waitExit(graceMs)) return;
|
|
274
|
+
if (await waitExit(graceMs)) return true;
|
|
277
275
|
// 2) 超时则强杀,**并且等它真的退出**。
|
|
278
276
|
// ★ 旧实现 kill() 之后立刻 resolve —— 于是调用方会在"子进程还活着、还握着写锁"
|
|
279
277
|
// 的时候继续往下走:删掉登记、再 spawn 新进程去 resume,就会撞上一个**无法归因的锁**
|
|
@@ -282,6 +280,6 @@ export class AcpProcess {
|
|
|
282
280
|
try {
|
|
283
281
|
child.kill();
|
|
284
282
|
} catch {}
|
|
285
|
-
await waitExit(killWaitMs);
|
|
283
|
+
return await waitExit(killWaitMs);
|
|
286
284
|
}
|
|
287
285
|
}
|
package/src/clients.mjs
CHANGED
|
@@ -89,11 +89,22 @@ export function pruneBackups(file) {
|
|
|
89
89
|
* 任何一步失败都必须删掉临时文件 —— 曾经有版本在 rename 失败后
|
|
90
90
|
* 把 mcp.json.tmp-dsh-<pid> 留在了客户端目录里。
|
|
91
91
|
*/
|
|
92
|
-
export function atomicWrite(file, text) {
|
|
92
|
+
export function atomicWrite(file, text, { mode } = {}) {
|
|
93
|
+
// 尽量保留原文件权限:POSIX 上 0600 的客户端配置不能因为一次 setup 变成 0644。
|
|
94
|
+
let originalMode = mode;
|
|
95
|
+
if (originalMode === undefined) {
|
|
96
|
+
try { originalMode = statSync(file).mode; } catch { originalMode = null; }
|
|
97
|
+
}
|
|
93
98
|
const tmp = file + '.tmp-dsh-' + process.pid + '-' + Math.random().toString(36).slice(2, 8);
|
|
94
99
|
try {
|
|
95
|
-
writeFileSync(tmp, text,
|
|
100
|
+
writeFileSync(tmp, text, {
|
|
101
|
+
encoding: 'utf8',
|
|
102
|
+
...(originalMode === null ? {} : { mode: originalMode & 0o7777 }),
|
|
103
|
+
});
|
|
96
104
|
renameSync(tmp, file);
|
|
105
|
+
if (originalMode !== null) {
|
|
106
|
+
try { chmodSync(file, originalMode & 0o7777); } catch { /* 尽力而为 */ }
|
|
107
|
+
}
|
|
97
108
|
} catch (e) {
|
|
98
109
|
try { if (existsSync(tmp)) unlinkSync(tmp); } catch { /* 尽力而为 */ }
|
|
99
110
|
throw e;
|