@hzxwonder/dsh-mcp 0.3.0 → 0.4.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/.agents/skills/deepseek-worker/SKILL.md +35 -6
- package/README.md +118 -160
- package/config/routing.md +37 -4
- package/config/worker.patch.yml +1 -0
- package/package.json +5 -1
- package/server.json +2 -2
- package/src/server.js +1 -0
- package/src/session-output.js +72 -0
- package/src/setup.js +41 -12
- package/src/worker.js +33 -5
|
@@ -38,6 +38,17 @@ Project and user restrictions take precedence over this skill.
|
|
|
38
38
|
13. Choose `read-only` by default.
|
|
39
39
|
14. Choose `workspace-write` only when the task requires edits.
|
|
40
40
|
|
|
41
|
+
Select `thinking_effort` explicitly for every call, including session continuation.
|
|
42
|
+
Use only `off`, `low`, `high`, or `max`.
|
|
43
|
+
Choose `low` for mechanical tasks.
|
|
44
|
+
Choose `high` for bounded tasks that require reasoning.
|
|
45
|
+
Choose `off` only after a deliberate decision to disable thinking.
|
|
46
|
+
Choose `max` only when the task justifies its additional cost.
|
|
47
|
+
Do not silently supply a missing value.
|
|
48
|
+
Do not use aliases such as `medium` or `xhigh`.
|
|
49
|
+
This parameter sets DSH `agent-default-model.config.reasoningEffort`, not prompt text.
|
|
50
|
+
Do not rely on the previous session's effort.
|
|
51
|
+
|
|
41
52
|
The client may prefix the tool name with its server name.
|
|
42
53
|
Do not change the Codex provider or silently substitute a direct API call.
|
|
43
54
|
DSH cannot see the main conversation.
|
|
@@ -45,6 +56,9 @@ Do not include keys, tokens, or login details in the task.
|
|
|
45
56
|
|
|
46
57
|
The user skill and MCP initialization instructions apply across projects.
|
|
47
58
|
Global setup does not require a per-project installation or `AGENTS.md` section.
|
|
59
|
+
The installer can upgrade an owned, unchanged previous skill template.
|
|
60
|
+
It preserves customized skills and files without a previous-template reference.
|
|
61
|
+
Back up those files and resolve conflicts manually before replacement.
|
|
48
62
|
Existing project, user, or global rules can prevent automatic delegation.
|
|
49
63
|
Do not infer `project_root` from the server's working directory, HOME, installation path, or session history.
|
|
50
64
|
Do not use HOME or `/` as a default grant.
|
|
@@ -72,6 +86,7 @@ Example tool arguments:
|
|
|
72
86
|
```json
|
|
73
87
|
{
|
|
74
88
|
"project_root": "/absolute/authorized/project",
|
|
89
|
+
"thinking_effort": "high",
|
|
75
90
|
"task": "Add boundary tests for parsePort. Change only tests/port.test.ts. Use the existing test framework. Preserve production code and user changes. Cover zero, 65535, negative values, 65536, and non-integers. Run npm test -- tests/port.test.ts. Do not commit or delegate. Stop if a decision remains unresolved. Report questions in the final answer. Report changes, exact check commands, and results. Identify checks that you did not run.",
|
|
76
91
|
"cwd": ".",
|
|
77
92
|
"permission_mode": "workspace-write",
|
|
@@ -81,18 +96,30 @@ Example tool arguments:
|
|
|
81
96
|
|
|
82
97
|
## Review the result
|
|
83
98
|
|
|
84
|
-
1.
|
|
85
|
-
2.
|
|
86
|
-
3.
|
|
87
|
-
4.
|
|
88
|
-
5.
|
|
89
|
-
6.
|
|
99
|
+
1. Read `answer` and check that `output_source` is `session`.
|
|
100
|
+
2. Read `session_id`, `session_file`, `assistant_seq`, and `turn`.
|
|
101
|
+
3. Inspect `status`, `exit_code`, `reason`, `terminal_reason`, `session_output_error`, `diagnostics`, and `thinking_effort`.
|
|
102
|
+
4. Inspect `changed_files` and `change_warning`.
|
|
103
|
+
5. Review actual edits, new files, and existing user changes.
|
|
104
|
+
6. Check the result against each acceptance criterion.
|
|
105
|
+
7. Run applicable acceptance checks when the assignment permits them.
|
|
106
|
+
8. Report worker changes separately from your own review and checks.
|
|
90
107
|
|
|
91
108
|
`completed` means that the Harness turn completed, not that the task passed acceptance.
|
|
92
109
|
A successful tool call does not prove that internal tests passed.
|
|
93
110
|
Do not claim that you ran a check merely because the worker reports it.
|
|
94
111
|
Respect existing check evidence unless changes or new findings invalidate it.
|
|
95
112
|
|
|
113
|
+
After the worker stops, MCP reads its persisted session and checks the session ID and saved physical working directory.
|
|
114
|
+
The answer is the last committed assistant text in the latest durable turn.
|
|
115
|
+
Hidden reasoning, tool calls, and tool results are not the final answer.
|
|
116
|
+
If that turn has no assistant text, `answer` is empty and `assistant_seq` is `null`.
|
|
117
|
+
Never substitute an earlier turn's successful answer.
|
|
118
|
+
Missing, corrupt, unreadable, or mismatched session data causes an explicit failure.
|
|
119
|
+
Do not substitute stdout when the durable read fails.
|
|
120
|
+
Do not accept work from `completed` or stdout alone.
|
|
121
|
+
Do not expose hidden reasoning in your report.
|
|
122
|
+
|
|
96
123
|
Git reports omit ignored files and complete internal submodule diffs.
|
|
97
124
|
They cannot attribute concurrent writes.
|
|
98
125
|
A non-Git workspace can return `null` instead of a file list.
|
|
@@ -111,11 +138,13 @@ The `never` approval policy denies approval requests instead of approving them.
|
|
|
111
138
|
4. Send a new call with the returned `session_id`.
|
|
112
139
|
5. Supply the same authorized `project_root` and preserve exactly the same actual `cwd`.
|
|
113
140
|
6. Supply explicit continuation instructions and the required permission mode.
|
|
141
|
+
7. Select and supply `thinking_effort` again, even when the session already has an effort setting.
|
|
114
142
|
|
|
115
143
|
Wait for the previous call to return before continuation.
|
|
116
144
|
The server waits for process termination before it returns.
|
|
117
145
|
This sequence resumes a session but does not provide real-time dialogue.
|
|
118
146
|
Do not assume a session can resume in a different project.
|
|
147
|
+
There is no separate tool for reading arbitrary sessions.
|
|
119
148
|
|
|
120
149
|
On failure, inspect partial changes before another call.
|
|
121
150
|
Preserve user changes.
|
package/README.md
CHANGED
|
@@ -1,48 +1,18 @@
|
|
|
1
1
|
# dsh-mcp
|
|
2
2
|
|
|
3
|
-
让 Codex 保持当前 GPT 模型、Provider 和账号登录,通过 MCP 把边界明确的任务交给 **DeepSeek Harness + DeepSeek Flash**。Codex
|
|
3
|
+
让 Codex 保持当前 GPT 模型、Provider 和账号登录,通过 MCP 把边界明确的任务交给 **DeepSeek Harness + DeepSeek Flash**。Codex 主代理负责选择思考强度、读取工作器结果、审查修改和最终验收。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## 快速开始:npx 安装一次
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
需要 macOS 或 Linux、Node.js **24.9.0 或更高版本**、npm,以及支持 MCP 的 Codex 客户端。准备好 [DeepSeek API Key](https://platform.deepseek.com/api_keys),或兼容 Messages API 的服务凭据。
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
用户电脑上的 Codex
|
|
11
|
-
↓ MCP stdio
|
|
12
|
-
用户电脑上的 dsh-mcp
|
|
13
|
-
↓ 启动本地子进程
|
|
14
|
-
用户电脑上的 DeepSeek Harness
|
|
15
|
-
├─ 读写本次授权的项目
|
|
16
|
-
└─ 请求 DeepSeek 官方 API 或自定义 Base URL
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
安装 MCP 时,npm 会自动下载完整的 Harness 依赖。不需要用户另行全局安装 `dsh`,也不会安装桌面 App 或下载模型权重。
|
|
20
|
-
|
|
21
|
-
你使用自己的 DeepSeek API Key,模型费用由对应 API 服务收取。本项目只提供软件,不提供远程任务执行服务。若使用自定义 Base URL,该服务会收到你的密钥和任务数据。
|
|
22
|
-
|
|
23
|
-
依赖的磁盘占用较大。开发环境的 `node_modules` 曾约为 922 MB,具体占用因平台、依赖布局和缓存而异。
|
|
24
|
-
|
|
25
|
-
## 环境要求
|
|
26
|
-
|
|
27
|
-
- macOS 或 Linux。
|
|
28
|
-
- Node.js **24.9.0 或更高版本**。
|
|
29
|
-
- npm。
|
|
30
|
-
- 支持 MCP 的 Codex 客户端。
|
|
31
|
-
- [DeepSeek API Key](https://platform.deepseek.com/api_keys),或兼容 Messages API 的服务凭据。
|
|
32
|
-
|
|
33
|
-
ChatGPT 账号登录、订阅额度和 DSH GUI 登录不能替代 API Key。
|
|
34
|
-
|
|
35
|
-
## 快速安装
|
|
36
|
-
|
|
37
|
-
### npm 安装
|
|
38
|
-
|
|
39
|
-
包页面:[npm · @hzxwonder/dsh-mcp](https://www.npmjs.com/package/@hzxwonder/dsh-mcp)。以下命令固定使用 `0.3.0`,避免自动升级改变执行行为。
|
|
9
|
+
安装命令固定使用 `0.4.0`。此版本要求每次显式选择思考强度,并从持久化 session 返回最终结果。
|
|
40
10
|
|
|
41
11
|
```sh
|
|
42
|
-
npx --yes --package @hzxwonder/dsh-mcp@0.
|
|
12
|
+
npx --yes --package @hzxwonder/dsh-mcp@0.4.0 dsh-mcp setup
|
|
43
13
|
```
|
|
44
14
|
|
|
45
|
-
|
|
15
|
+
安装器打开本机浏览器表单,只填写两个配置项:
|
|
46
16
|
|
|
47
17
|
| 配置项 | 说明 |
|
|
48
18
|
| --- | --- |
|
|
@@ -51,200 +21,188 @@ npx --yes --package @hzxwonder/dsh-mcp@0.3.0 dsh-mcp setup
|
|
|
51
21
|
|
|
52
22
|
保存后,在 Codex 中新建会话,确认工具列表出现 `delegate_to_deepseek`。客户端可能为工具名添加服务器前缀。
|
|
53
23
|
|
|
54
|
-
|
|
24
|
+
**全局安装一次,不选择或绑定项目,也不需要逐项目配置。** 每次委派时,Codex 必须传入当前授权项目的绝对路径 `project_root`,并主动选择 `thinking_effort`。
|
|
55
25
|
|
|
56
26
|
没有图形浏览器时,使用终端输入:
|
|
57
27
|
|
|
58
28
|
```sh
|
|
59
|
-
npx --yes --package @hzxwonder/dsh-mcp@0.
|
|
29
|
+
npx --yes --package @hzxwonder/dsh-mcp@0.4.0 dsh-mcp setup --terminal
|
|
60
30
|
```
|
|
61
31
|
|
|
62
|
-
终端不会回显 API Key。不要把真实密钥写进命令参数、URL、任务文本、skill 或项目配置。
|
|
32
|
+
终端不会回显 API Key。不要把真实密钥写进命令参数、URL、任务文本、skill 或项目配置。ChatGPT 登录、订阅额度和 DSH GUI 登录不能替代 API Key。
|
|
63
33
|
|
|
64
|
-
###
|
|
34
|
+
### Base URL 必须支持 Messages
|
|
65
35
|
|
|
66
|
-
|
|
67
|
-
git clone https://github.com/hzxwonder/dsh-mcp.git
|
|
68
|
-
cd dsh-mcp
|
|
69
|
-
npm ci
|
|
70
|
-
npm run setup
|
|
71
|
-
```
|
|
36
|
+
DSH 使用 **Messages API**,请求路径为 `/v1/messages`。默认根地址是 `https://api.deepseek.com/anthropic`。只支持 OpenAI Chat Completions 的地址不能直接使用。
|
|
72
37
|
|
|
73
|
-
|
|
38
|
+
自定义地址不能包含用户名、密码、查询参数或片段。请只使用可信服务:该服务会收到你的密钥和任务数据。模型费用由对应 API 服务收取。
|
|
74
39
|
|
|
75
|
-
|
|
76
|
-
npm run setup -- --terminal
|
|
77
|
-
```
|
|
40
|
+
## 让 Codex 委派任务
|
|
78
41
|
|
|
79
|
-
|
|
42
|
+
安装器安装用户级 `deepseek-worker` skill,MCP 初始化时也提供委派说明,不修改项目的 `AGENTS.md`。可以直接描述需求:
|
|
80
43
|
|
|
81
|
-
|
|
82
|
-
npm run setup -- --home /absolute/path/to/private/dsh-home
|
|
83
|
-
```
|
|
44
|
+
> 为端口解析模块补充边界测试。只修改现有测试文件,不改变生产代码。请按任务选择 DeepSeek 思考强度,读取会话结果后审查 diff 并运行相关测试。
|
|
84
45
|
|
|
85
|
-
|
|
46
|
+
需要明确指定时:
|
|
86
47
|
|
|
87
|
-
|
|
48
|
+
> $deepseek-worker 把这个独立测试任务交给 DeepSeek。你选择合适的思考强度,并负责读取结果、审查实际修改和最终验收。
|
|
88
49
|
|
|
89
|
-
|
|
50
|
+
适合委派边界清晰的简单实现、独立测试、文档和机械修改。极小任务应直接完成。不委派架构、安全、鉴权、支付、数据库迁移、根因不明的问题或涉及秘密的任务。
|
|
90
51
|
|
|
91
|
-
|
|
92
|
-
https://api.deepseek.com/anthropic
|
|
93
|
-
```
|
|
52
|
+
规则指导模型选择,**不保证每次调用**,也不取消 Codex 的批准提示。项目和用户规则仍然优先。
|
|
94
53
|
|
|
95
|
-
|
|
54
|
+
### Codex 必须主动选择思考强度
|
|
96
55
|
|
|
97
|
-
|
|
56
|
+
每次调用(包括用 `session_id` 续接)都必须显式传入 `thinking_effort`,没有默认值:
|
|
98
57
|
|
|
99
|
-
|
|
|
58
|
+
| 值 | 选择依据 |
|
|
100
59
|
| --- | --- |
|
|
101
|
-
|
|
|
102
|
-
|
|
|
103
|
-
|
|
|
104
|
-
|
|
|
105
|
-
|
|
106
|
-
如果设置了 `CODEX_HOME`,Codex 配置写入 `$CODEX_HOME/config.toml`。`--home` 只改变 DSH 的私有状态目录。
|
|
107
|
-
|
|
108
|
-
私有目录权限为 `0700`,密钥和设置文件权限为 `0600`。MCP 配置中不包含密钥或固定项目路径。
|
|
109
|
-
|
|
110
|
-
DSH 不允许在 `.env` 中设置网络路由变量。**不要把 `DEEPSEEK_BASE_URL` 写进私有 `.env`。** 桥接器会读取 `settings.json`,再通过进程环境把地址传给 DSH。
|
|
111
|
-
|
|
112
|
-
安装器保留其他模型、Provider、MCP 和登录配置,不修改任何 `AGENTS.md`,也不向项目中写配置或 skill。
|
|
113
|
-
|
|
114
|
-
已有同名但不归安装器管理的 `dsh` 配置,或已有不同内容的同名 skill 时,安装器会报告冲突,不会覆盖。
|
|
115
|
-
|
|
116
|
-
npx 安装可能把服务路径记录为 npm 缓存中的绝对路径。清理缓存、移动安装目录或更换 Node 可执行文件后,需要重新运行 `setup`。
|
|
117
|
-
|
|
118
|
-
## 如何让 Codex 自动委派
|
|
119
|
-
|
|
120
|
-
安装器会安装用户级 `deepseek-worker` skill。MCP 初始化时也会提供委派说明,不需要为每个项目追加 `AGENTS.md`。
|
|
121
|
-
|
|
122
|
-
普通任务可以直接描述需求。例如:
|
|
123
|
-
|
|
124
|
-
> 为端口解析模块补充边界测试。只修改现有测试文件,不改变生产代码。完成后审查 diff 并运行相关测试。
|
|
125
|
-
|
|
126
|
-
Codex 会根据规则判断是否值得委派:
|
|
60
|
+
| `low` | 机械修改、简单文档或明确的样板任务。 |
|
|
61
|
+
| `high` | 边界明确,但需要推理的实现或测试任务。 |
|
|
62
|
+
| `off` | 明确决定不使用思考时才选择。 |
|
|
63
|
+
| `max` | 额外推理成本有明确理由时才选择。 |
|
|
127
64
|
|
|
128
|
-
-
|
|
129
|
-
- 不适合:架构、安全、鉴权、支付、数据库迁移、根因不明的问题,以及涉及秘密的任务。
|
|
130
|
-
- 极小任务直接完成,避免委派成本超过实现成本。
|
|
65
|
+
Codex 不得静默补齐缺失值。只接受 `off`、`low`、`high`、`max`,不接受 `medium` 或 `xhigh` 等别名。这是真实的 DSH `agent-default-model.config.reasoningEffort` 配置,不是任务中的提示词。续接时也必须重新选择,不依赖旧会话的设置。
|
|
131
66
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
需要明确指定时,可以使用:
|
|
135
|
-
|
|
136
|
-
> $deepseek-worker 把这个独立测试任务交给 DeepSeek。你负责审查实际修改和最终验收。
|
|
137
|
-
|
|
138
|
-
## 项目路径由谁提供
|
|
139
|
-
|
|
140
|
-
安装和启动 MCP 时不配置项目根目录。每次调用必须由 Codex 提供绝对路径:
|
|
67
|
+
### 最小调用示例
|
|
141
68
|
|
|
142
69
|
```json
|
|
143
70
|
{
|
|
144
71
|
"project_root": "/absolute/path/to/current-project",
|
|
145
|
-
"
|
|
72
|
+
"thinking_effort": "high",
|
|
73
|
+
"task": "补充指定模块的边界测试。只修改对应测试文件,复用现有测试框架,不提交 Git,不再委派。遇到未决问题停止并报告。报告精确验证命令和结果。",
|
|
146
74
|
"cwd": ".",
|
|
147
75
|
"permission_mode": "workspace-write",
|
|
148
76
|
"timeout_seconds": 300
|
|
149
77
|
}
|
|
150
78
|
```
|
|
151
79
|
|
|
152
|
-
Codex
|
|
80
|
+
Codex 从当前项目上下文或用户明确授权中取得路径。服务不会从自身工作目录、HOME、安装目录或上次调用猜测项目。缺少路径、相对路径、目录不存在或不是目录时,调用失败,不启动工作器。
|
|
153
81
|
|
|
154
|
-
|
|
82
|
+
`project_root` 是调用方对本次目录的授权,**不是服务端独立核实过的 Codex 工作区 ACL**。不要默认授权 HOME 或 `/`。
|
|
155
83
|
|
|
156
|
-
|
|
84
|
+
## 工作器结束后,读取什么结果
|
|
157
85
|
|
|
158
|
-
|
|
86
|
+
`delegate_to_deepseek` 同步执行一个任务。DSH 进程停止后,MCP 读取对应的持久化会话,核对会话 ID 和保存的实际工作目录。`answer` 来自**当前最新持久化回合中最后一条已提交的 assistant 文本**,不是 stdout 的替代摘要。
|
|
159
87
|
|
|
160
|
-
`
|
|
88
|
+
结果放在 `structuredContent` 和文本 JSON 中。Codex 必须先读取以下字段,再审查实际修改:
|
|
161
89
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
| `session_id` | 无 | 续接已有 DSH 会话。 |
|
|
169
|
-
| `timeout_seconds` | `300` | 整数,范围 1–900 秒。 |
|
|
90
|
+
- `answer`、`output_source: "session"`:会话中的最终文本及来源。
|
|
91
|
+
- `session_id`、`session_file`、`assistant_seq`、`turn`:对应会话和文本的位置元数据。
|
|
92
|
+
- `status`、`exit_code`、`signal`、`reason`、`diagnostics`:进程状态、终止原因和诊断。
|
|
93
|
+
- `terminal_reason`、`session_output_error`:持久化回合的结束原因和读取失败说明。
|
|
94
|
+
- `project_root`、`cwd`、`model`、`thinking_effort`、`permission_mode`:实际执行设置。
|
|
95
|
+
- `changed_files`、`git_root`、`change_warning`:Git 变化报告及限制。
|
|
170
96
|
|
|
171
|
-
|
|
97
|
+
隐藏思考、工具调用和工具结果都不是最终答案。若最新回合没有已提交的 assistant 文本,`answer` 为空且 `assistant_seq` 为 `null`,不能取上一回合的成功答案。读取的回合必须与本次进程的 `turn_start` 一致:续接若在新回合开始前失败,不会返回旧文本。会话缺失、校验失败、不可读或身份/目录不匹配会明确报错,不回退到 stdout。
|
|
172
98
|
|
|
173
|
-
|
|
99
|
+
读取使用 DSH 官方的只读存储接口,支持压缩会话。官方接口可能忽略未完成的物理尾帧,只返回已验证的持久化前缀。读取需要重放会话历史,大会话的读取成本会增加。
|
|
174
100
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
101
|
+
只有进程退出码为 0、DSH 最终原因为 `completed`,且成功读取的对应回合也已持久化完成,服务才报告完成。**这只表示代理回合结束,不代表任务通过验收。** Codex 必须检查 `answer`、`reason`、`terminal_reason`、`session_output_error` 和 `diagnostics`,再审查实际 diff 并执行验收。工作器声称运行了检查,不等于主代理亲自运行了检查。
|
|
102
|
+
|
|
103
|
+
### 未决问题与续接
|
|
104
|
+
|
|
105
|
+
当前调用不实时转发中间事件,不支持途中的双向问答、批准响应或单独浏览任意会话的工具。
|
|
106
|
+
|
|
107
|
+
1. DeepSeek 停止任务,在最终文本中报告未决问题。
|
|
108
|
+
2. Codex 读取结果,在已有授权内决策,必要时询问用户。
|
|
109
|
+
3. 上次调用返回后,Codex 用返回的 `session_id`、相同 `project_root` 和相同实际 `cwd` 续接。
|
|
110
|
+
4. 新调用仍须显式选择 `thinking_effort`,并提供完整续接指令和权限。
|
|
111
|
+
|
|
112
|
+
这是多轮会话续接,不是实时对话。`--approval-policy never` 拒绝需要人工批准的操作,不是自动批准。
|
|
113
|
+
|
|
114
|
+
## 本地依赖与配置
|
|
115
|
+
|
|
116
|
+
npm 自动下载**真实、完整的本地 Harness 依赖**。不需要另行全局安装 `dsh`,不会安装桌面 App 或下载模型权重。npm 主流程不需要克隆源码,也不要求 Git 作为安装步骤。任务本身需要的工具仍须在本机可用。
|
|
117
|
+
|
|
118
|
+
```text
|
|
119
|
+
本机 Codex → MCP stdio → 本机 dsh-mcp → 本机 DSH 子进程 → 配置的 API 服务
|
|
120
|
+
```
|
|
180
121
|
|
|
181
|
-
|
|
122
|
+
本项目不提供远程执行服务。依赖占用较大,具体磁盘占用取决于平台、依赖布局和缓存。
|
|
182
123
|
|
|
183
|
-
|
|
124
|
+
固定使用 `@deepseek-ai/dsh@0.2.2-rc.1` 和 `deepseek-flash`。DSH 仍处于 developer preview,升级时需要检查兼容性。使用官方 `headless --json` 和持久化/查询库,不依赖 DSH Web 内部接口。
|
|
184
125
|
|
|
185
|
-
|
|
126
|
+
| 文件 | 用途 |
|
|
127
|
+
| --- | --- |
|
|
128
|
+
| `~/.codex/config.toml` | 注册用户级 MCP 服务器 `dsh`。 |
|
|
129
|
+
| `~/.agents/skills/deepseek-worker/SKILL.md` | 安装用户级委派 skill。 |
|
|
130
|
+
| `~/.dsh-mcp/.env` | 保存 API Key。 |
|
|
131
|
+
| `~/.dsh-mcp/settings.json` | 保存 Base URL。 |
|
|
186
132
|
|
|
187
|
-
|
|
133
|
+
设置 `CODEX_HOME` 时,Codex 配置写入 `$CODEX_HOME/config.toml`。`--home` 只改变 DSH 私有状态目录。私有目录权限为 `0700`,密钥和设置文件为 `0600`。MCP 配置不包含密钥或固定项目路径。
|
|
188
134
|
|
|
189
|
-
|
|
190
|
-
2. Codex 在已有授权内决策,必要时向用户询问。
|
|
191
|
-
3. Codex 用返回的 `session_id`、相同 `project_root` 和相同实际 `cwd` 发起续接。
|
|
135
|
+
**不要把 `DEEPSEEK_BASE_URL` 写进私有 `.env`。** DSH 不允许在那里设置网络路由变量。桥接器从 `settings.json` 读取地址,再通过进程环境传入。
|
|
192
136
|
|
|
193
|
-
|
|
137
|
+
安装器复用已保存的密钥,不覆盖它。更换密钥时,编辑私有 `.env` 后启动新的 MCP 会话。安装器保留其他模型、Provider、MCP 和登录配置,不向项目写入配置或 skill。
|
|
194
138
|
|
|
195
|
-
|
|
139
|
+
### 更新与冲突保护
|
|
196
140
|
|
|
197
|
-
|
|
198
|
-
- `workspace-write` 可以写入 `cwd`、必要的 DSH 状态和平台定义的临时目录。敏感任务应使用容器或独立账户。
|
|
199
|
-
- 任务中的文件名单是行为约束,不是强制文件级 ACL。服务不会自动撤销越界编辑。
|
|
200
|
-
- 同一工作区不能有并发写入者,包括主代理、编辑器、其他代理和其他 MCP 实例。需要并发时,先准备独立 worktree。
|
|
201
|
-
- Git 变化报告比较任务前后的状态和内容指纹,不包含忽略文件,也不提供完整的子模块内部 diff。它不能归因外部并发修改。
|
|
202
|
-
- `changed_files: null` 表示报告不可用,不表示没有修改。始终检查 `change_warning` 和实际 diff。
|
|
203
|
-
- 超时、取消、客户端断开或进程终止时,服务清理 POSIX 进程组。自行脱离该组的后代进程不在保证内。
|
|
204
|
-
- 服务会等待工作器退出,但退出不是回滚。失败或中断后可能留下部分修改。
|
|
205
|
-
- NDJSON 标准输出超过 8 MiB 时,服务终止任务并报告 `output_limit`,不会截断后冒报成功。
|
|
206
|
-
- DSH 会持久化会话和任务内容。请按敏感数据保护私有目录与诊断输出,不要在任务中放入秘密。
|
|
207
|
-
- 服务不自动提交、发布、合并、回滚或创建 worktree,也不提供 full-access 参数。
|
|
141
|
+
更新已安装配置仍使用同一条全局 setup 命令,不需要逐项目操作:
|
|
208
142
|
|
|
209
|
-
|
|
143
|
+
```sh
|
|
144
|
+
npx --yes --package @hzxwonder/dsh-mcp@0.4.0 dsh-mcp setup
|
|
145
|
+
```
|
|
210
146
|
|
|
211
|
-
|
|
147
|
+
安装器可以更新归它管理且未被修改的旧 skill 模板。若 skill 已定制,或缺少可核对的旧模板引用,安装器保留现有文件并报告冲突。先备份,再手动合并或处理冲突,不要直接覆盖定制内容。同名但不归安装器管理的 `dsh` 配置也不会被覆盖。
|
|
212
148
|
|
|
213
|
-
|
|
149
|
+
npx 安装可能把服务路径记录为 npm 缓存中的绝对路径。清理缓存、移动安装目录或更换 Node 后,应重新运行 setup。
|
|
214
150
|
|
|
215
|
-
|
|
151
|
+
旧项目的 `.codex/config.toml` 可能覆盖新全局配置。只移除旧的受管理 `dsh` 区块,保留其他内容。对旧项目 skill 或 `AGENTS.md` 中的固定路径规则也只移除过时受管理部分。安装器不扫描或批量修改所有仓库。
|
|
216
152
|
|
|
217
|
-
##
|
|
153
|
+
## 参数参考
|
|
218
154
|
|
|
219
|
-
|
|
155
|
+
| 参数 | 默认值 | 说明 |
|
|
156
|
+
| --- | --- | --- |
|
|
157
|
+
| `project_root` | 必填 | 本次授权的项目绝对路径。 |
|
|
158
|
+
| `thinking_effort` | 必填 | `off`、`low`、`high` 或 `max`。续接也必填。 |
|
|
159
|
+
| `task` | 必填 | 完整目标、上下文、允许修改的文件、限制和验收标准,最多 65536 字符。 |
|
|
160
|
+
| `cwd` | `.` | 根目录内的相对目录或绝对目录。 |
|
|
161
|
+
| `permission_mode` | `read-only` | 确需编辑时显式选择 `workspace-write`。 |
|
|
162
|
+
| `session_id` | 无 | 续接对应 DSH 会话。 |
|
|
163
|
+
| `timeout_seconds` | `300` | 整数,范围 1–900 秒。 |
|
|
220
164
|
|
|
221
|
-
|
|
165
|
+
服务解析真实路径,拒绝 `cwd` 通过 `..` 或符号链接逃出本次根目录。一个服务器实例最多运行一个任务,包括跨项目任务。重叠调用立即报错,不后台排队。客户端超时应长于工作器超时和清理时间,安装器设为 960 秒。
|
|
222
166
|
|
|
223
|
-
|
|
167
|
+
## 安全边界
|
|
224
168
|
|
|
225
|
-
|
|
169
|
+
- 工作区限制不是读取隔离。DSH 可以读取其他可访问文件,shell 工具不是恶意代码容器。
|
|
170
|
+
- `workspace-write` 可以写入 `cwd`、必要 DSH 状态和平台定义的临时目录。敏感任务应使用容器或独立账户。
|
|
171
|
+
- 任务文件名单是行为约束,不是强制文件级 ACL。服务不自动撤销越界编辑。
|
|
172
|
+
- 同一工作区不能有并发写入者,包括主代理、编辑器、其他代理和 MCP 实例。并行任务应先准备独立 worktree,不并发修改共享 Git 元数据。
|
|
173
|
+
- Git 报告比较前后状态和内容指纹,不包含忽略文件或完整子模块内部 diff,也不能归因外部并发修改。
|
|
174
|
+
- `changed_files: null` 表示报告不可用,不表示没有修改。检查 `change_warning` 和实际 diff。
|
|
175
|
+
- 超时、取消、客户端断开或终止时,服务清理 POSIX 进程组。自行脱离该组的后代不在保证内。
|
|
176
|
+
- 服务等待工作器退出,但不回滚。失败或中断后先检查部分修改,不自动 reset、clean 或重试覆盖用户修改。
|
|
177
|
+
- NDJSON stdout 超过 8 MiB 时,服务终止任务并报告 `output_limit`,不会截断后冒报成功。
|
|
178
|
+
- DSH 持久化会话和任务内容。保护私有目录与诊断输出,不在任务中放秘密,也不公开隐藏思考。
|
|
179
|
+
- 工作器不得递归委派。服务不自动提交、发布、合并、回滚或创建 worktree,不提供 full-access 参数。
|
|
226
180
|
|
|
227
|
-
|
|
181
|
+
## 高级:源码安装与开发
|
|
228
182
|
|
|
229
|
-
|
|
183
|
+
普通 npm 用户不需要此流程。需要修改桥接器源码时:
|
|
230
184
|
|
|
231
185
|
```sh
|
|
232
|
-
|
|
186
|
+
git clone https://github.com/hzxwonder/dsh-mcp.git
|
|
187
|
+
cd dsh-mcp
|
|
188
|
+
npm ci
|
|
189
|
+
npm run setup
|
|
233
190
|
```
|
|
234
191
|
|
|
235
|
-
|
|
192
|
+
终端安装使用 `npm run setup -- --terminal`。自定义私有目录使用 `npm run setup -- --home /absolute/path/to/private/dsh-home`。
|
|
193
|
+
|
|
194
|
+
开发检查命令为 `npm test`,当前 44 项检查全部通过。另用真实 DSH 和本地 Messages 协议桩核对了四档 effort 的实际请求、同会话续接和压缩 session 最终文本读取;从已发布的 `0.3.0` 配置升级也已验证。真实 DeepSeek 在线推理、Codex App 加载和模型自动选择工具仍需在实际环境中检查。
|
|
236
195
|
|
|
237
|
-
|
|
196
|
+
## 分发与参考
|
|
238
197
|
|
|
239
|
-
|
|
198
|
+
包页面:[npm · @hzxwonder/dsh-mcp](https://www.npmjs.com/package/@hzxwonder/dsh-mcp)。普通安装用户不需要登录 npm。
|
|
240
199
|
|
|
241
|
-
|
|
200
|
+
官方 MCP Registry 条目尚未登记。npm 发布不自动进入客户端商店。`server.json` 提供两个 Provider 输入项,但不保证 Codex 原生商店显示相同表单。`isSecret` 只是客户端处理提示,不保证加密存储。
|
|
242
201
|
|
|
243
|
-
|
|
202
|
+
手动配置见 [examples/codex.toml](examples/codex.toml)。优先使用安装器私有文件或客户端受保护凭据存储,不把真实密钥提交到 TOML。
|
|
244
203
|
|
|
245
204
|
- [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)
|
|
246
205
|
- [DSH Headless 凭据和 NDJSON 接口](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/bundle/headless/README.md)
|
|
247
206
|
- [Codex MCP 配置](https://developers.openai.com/codex/mcp/)
|
|
248
207
|
- [Codex skills](https://developers.openai.com/codex/skills/)
|
|
249
208
|
- [MCP Registry 发布说明](https://modelcontextprotocol.io/registry/quickstart)
|
|
250
|
-
- [npm 发布说明](https://docs.npmjs.com/creating-and-publishing-scoped-public-packages)
|
package/config/routing.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
## DeepSeek task delegation
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
Keep the current GPT model, provider, and account.
|
|
4
|
+
Every delegate_to_deepseek call needs the current authorized absolute project_root and explicit thinking_effort: off, low, high, or max.
|
|
5
|
+
Include both on resume. Never infer the root from the server or installation. Never default effort.
|
|
6
|
+
Use read-only unless the task needs edits. Read the session answer, reason, and diagnostics before review. Follow user and project restrictions.
|
|
7
7
|
|
|
8
8
|
- Use the installed `deepseek-worker` skill for eligible tasks.
|
|
9
9
|
- Delegate worthwhile, bounded, low-risk tasks by default when the MCP tool is available.
|
|
@@ -13,6 +13,12 @@ Never infer this path from the server or installation. Use read-only unless the
|
|
|
13
13
|
- Use the active project root unless the user explicitly authorizes a different project directory.
|
|
14
14
|
- Do not infer the root from the server directory, home directory, installation, or previous calls.
|
|
15
15
|
- Do not delegate when the current project root is unknown.
|
|
16
|
+
- Select `thinking_effort` consciously for every call, including continuation.
|
|
17
|
+
- Use `low` for mechanical tasks and `high` for bounded reasoning.
|
|
18
|
+
- Use `off` only to disable thinking deliberately.
|
|
19
|
+
- Use `max` only when the task justifies its additional cost.
|
|
20
|
+
- Never silently supply missing effort or use aliases such as `medium` or `xhigh`.
|
|
21
|
+
- This parameter sets DSH `agent-default-model.config.reasoningEffort`, not prompt text.
|
|
16
22
|
- Each `project_root` grants directory access for that call only.
|
|
17
23
|
- The server does not independently check this grant against a Codex workspace ACL.
|
|
18
24
|
- This grant is not a read isolation boundary.
|
|
@@ -27,8 +33,35 @@ Never infer this path from the server or installation. Use read-only unless the
|
|
|
27
33
|
- Require the worker to stop and report unresolved questions in its final answer.
|
|
28
34
|
- Do not promise live dialogue or automatic approval responses during a call.
|
|
29
35
|
- Resolve questions before a new call with the returned `session_id`, the same `project_root`, and the same `cwd`.
|
|
36
|
+
- Select `thinking_effort` again when you resume. Do not depend on the old session setting.
|
|
37
|
+
- After the worker stops, MCP reads the persisted session and checks its ID and saved physical working directory.
|
|
38
|
+
- Read `answer`, `output_source: "session"`, `session_id`, `session_file`, `assistant_seq`, and `turn`.
|
|
39
|
+
- Read `reason`, `terminal_reason`, `session_output_error`, and `diagnostics` before reviewing actual changes.
|
|
40
|
+
- The answer is the last committed assistant text in the latest durable turn.
|
|
41
|
+
- Hidden reasoning, tool calls, and tool results are not final answers.
|
|
42
|
+
- An empty latest turn returns empty text and a null assistant sequence, never an earlier successful answer.
|
|
43
|
+
- Session read failures are explicit. Do not substitute stdout or old answers.
|
|
44
|
+
- The persisted turn must match this invocation's turn_start event. Never return a previous turn after a failed resume.
|
|
45
|
+
- Do not promise a separate arbitrary-session reader.
|
|
30
46
|
- Review actual changes and check evidence before acceptance.
|
|
31
47
|
- Treat `completed` as Harness turn completion, not task acceptance.
|
|
32
48
|
- Report unverified results and partial changes explicitly.
|
|
33
49
|
|
|
34
50
|
These instructions guide delegation decisions but cannot guarantee automatic tool invocation.
|
|
51
|
+
|
|
52
|
+
Example arguments:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"project_root": "/absolute/authorized/project",
|
|
57
|
+
"thinking_effort": "low",
|
|
58
|
+
"task": "Correct the specified documentation typo. Preserve user changes. Do not commit or delegate. Report the changed file and any checks.",
|
|
59
|
+
"cwd": ".",
|
|
60
|
+
"permission_mode": "workspace-write"
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Global setup needs no project configuration.
|
|
65
|
+
The installer can upgrade an owned, unchanged previous skill template.
|
|
66
|
+
It preserves customized skills and skills without a previous-template reference.
|
|
67
|
+
Back up those files and resolve conflicts manually before replacement.
|
package/config/worker.patch.yml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hzxwonder/dsh-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"mcpName": "io.github.hzxwonder/dsh-mcp",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Delegate bounded Codex tasks to DeepSeek Harness over MCP stdio.",
|
|
@@ -35,7 +35,11 @@
|
|
|
35
35
|
"dsh": "dsh"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
+
"@deepseek-ai/cordis": "4.1.0",
|
|
38
39
|
"@deepseek-ai/dsh": "0.2.2-rc.1",
|
|
40
|
+
"@deepseek-ai/dsh-session": "0.2.2-rc.1",
|
|
41
|
+
"@deepseek-ai/dsh-session-format-catalog": "0.2.2-rc.1",
|
|
42
|
+
"@deepseek-ai/dsh-session-persistence-jsonl": "0.2.2-rc.1",
|
|
39
43
|
"@modelcontextprotocol/sdk": "1.32.2",
|
|
40
44
|
"smol-toml": "1.9.1",
|
|
41
45
|
"zod": "^4.0.0"
|
package/server.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "io.github.hzxwonder/dsh-mcp",
|
|
4
4
|
"title": "DeepSeek Harness for Codex",
|
|
5
5
|
"description": "Delegate bounded tasks to local DeepSeek Harness with an explicit project_root in every tool call.",
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.4.0",
|
|
7
7
|
"repository": {
|
|
8
8
|
"url": "https://github.com/hzxwonder/dsh-mcp",
|
|
9
9
|
"source": "github"
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"registryType": "npm",
|
|
15
15
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
16
16
|
"identifier": "@hzxwonder/dsh-mcp",
|
|
17
|
-
"version": "0.
|
|
17
|
+
"version": "0.4.0",
|
|
18
18
|
"runtimeHint": "npx",
|
|
19
19
|
"transport": {
|
|
20
20
|
"type": "stdio"
|
package/src/server.js
CHANGED
|
@@ -38,6 +38,7 @@ server.registerTool('delegate_to_deepseek', {
|
|
|
38
38
|
inputSchema: {
|
|
39
39
|
project_root: z.string().min(1).describe('Required absolute project directory from Codex context or explicit user authorization. This is the caller directory grant, not an independently verified workspace ACL.'),
|
|
40
40
|
task: z.string().trim().min(1).max(65536).describe('Self-contained task, allowed files, constraints, acceptance criteria and requested verification.'),
|
|
41
|
+
thinking_effort: z.enum(['off', 'low', 'high', 'max']).describe('Required DSH reasoning effort. Choose explicitly for every call and resume. Use low for mechanical tasks and high for bounded reasoning.'),
|
|
41
42
|
cwd: z.string().min(1).default('.').describe('Directory relative to project_root, or an absolute directory within project_root.'),
|
|
42
43
|
permission_mode: z.enum(['read-only', 'workspace-write']).default('read-only'),
|
|
43
44
|
session_id: z.string().min(1).max(256).optional().describe('Previously returned DSH session ID for scoped revision in the same cwd.'),
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { realpath } from 'node:fs/promises';
|
|
2
|
+
import { isAbsolute, join } from 'node:path';
|
|
3
|
+
import { Context } from '@deepseek-ai/cordis';
|
|
4
|
+
import { Session } from '@deepseek-ai/dsh-session';
|
|
5
|
+
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl';
|
|
6
|
+
import { currentSessionMessageProjections } from '@deepseek-ai/dsh-session-format-catalog/message-projections';
|
|
7
|
+
|
|
8
|
+
export async function readSessionOutput({ home, sessionId, cwd }) {
|
|
9
|
+
if (typeof sessionId !== 'string' || !sessionId) throw new Error('A persisted DSH session ID is required');
|
|
10
|
+
if (typeof cwd !== 'string' || !isAbsolute(cwd)) throw new Error('The session cwd must be an absolute path');
|
|
11
|
+
const physicalCwd = await realpath(cwd);
|
|
12
|
+
const ctx = new Context();
|
|
13
|
+
try {
|
|
14
|
+
await ctx.plugin(JsonlSessionPersistence, { root: join(home, 'sessions') }).await();
|
|
15
|
+
const persistence = ctx.sessionPersistence;
|
|
16
|
+
const snapshot = await persistence.stat(sessionId);
|
|
17
|
+
if (!snapshot?.location?.path) throw new Error('Persisted session log not found');
|
|
18
|
+
const handle = await persistence.open(sessionId, 'read');
|
|
19
|
+
let read;
|
|
20
|
+
try {
|
|
21
|
+
if (handle.header.id !== sessionId || snapshot.header.id !== sessionId) {
|
|
22
|
+
throw new Error('Persisted session header ID does not match the requested session');
|
|
23
|
+
}
|
|
24
|
+
if (!handle.header.cwd || await realpath(handle.header.cwd) !== physicalCwd
|
|
25
|
+
|| snapshot.header.cwd !== handle.header.cwd) {
|
|
26
|
+
throw new Error('Persisted session cwd does not match the requested physical cwd');
|
|
27
|
+
}
|
|
28
|
+
read = await handle.read();
|
|
29
|
+
} finally {
|
|
30
|
+
await handle.close();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Restore only in memory: the storage backend owns decoding and migrations.
|
|
34
|
+
const session = Session.fromRestore(sessionId, read.events, handle.header,
|
|
35
|
+
handle.inheritedEventCount, read.eventState, currentSessionMessageProjections);
|
|
36
|
+
const events = read.events;
|
|
37
|
+
let start = events.length - 1;
|
|
38
|
+
while (start >= 0 && events[start].type !== 'turn/start') start--;
|
|
39
|
+
if (start < 0) throw new Error('Persisted session has no durable turn');
|
|
40
|
+
const turn = events[start].data.turn;
|
|
41
|
+
const current = new Set(session.surface.nodes);
|
|
42
|
+
let text = '';
|
|
43
|
+
let assistantSeq = null;
|
|
44
|
+
let terminalReason = null;
|
|
45
|
+
for (let index = events.length - 1; index > start; index--) {
|
|
46
|
+
const event = events[index];
|
|
47
|
+
if (event.type === 'turn/end' && event.data.turn === turn && terminalReason === null) {
|
|
48
|
+
terminalReason = event.data.reason;
|
|
49
|
+
}
|
|
50
|
+
if (event.type !== 'assistant/message' || event.data.turn !== turn
|
|
51
|
+
|| assistantSeq !== null || !current.has(event.seq)) continue;
|
|
52
|
+
const message = session.deriveEventMessage(event);
|
|
53
|
+
const visibleText = message?.content.filter((block) => block.type === 'text').map((block) => block.text).join('') ?? '';
|
|
54
|
+
if (visibleText) {
|
|
55
|
+
text = visibleText;
|
|
56
|
+
assistantSeq = event.seq;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
session_id: sessionId,
|
|
61
|
+
text,
|
|
62
|
+
session_file: snapshot.location.path,
|
|
63
|
+
assistant_seq: assistantSeq,
|
|
64
|
+
turn,
|
|
65
|
+
terminal_reason: terminalReason,
|
|
66
|
+
};
|
|
67
|
+
} catch (error) {
|
|
68
|
+
throw new Error(`Cannot read persisted DSH session "${sessionId}": ${error.message}`, { cause: error });
|
|
69
|
+
} finally {
|
|
70
|
+
await ctx.fiber.dispose();
|
|
71
|
+
}
|
|
72
|
+
}
|
package/src/setup.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { constants } from 'node:fs';
|
|
2
2
|
import { chmod, lstat, mkdir, open, readFile, rename, rm } from 'node:fs/promises';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
|
-
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
6
|
-
import { parseArgs, parseEnv } from 'node:util';
|
|
6
|
+
import { isDeepStrictEqual, parseArgs, parseEnv } from 'node:util';
|
|
7
7
|
import { createInterface } from 'node:readline';
|
|
8
8
|
import { Writable } from 'node:stream';
|
|
9
9
|
import { randomUUID } from 'node:crypto';
|
|
@@ -39,10 +39,13 @@ function managedBlock(text, begin, end, body) {
|
|
|
39
39
|
throw new Error(`Repair the incomplete or duplicate managed block: ${begin}`);
|
|
40
40
|
}
|
|
41
41
|
const block = `${begin}\n${body.trimEnd()}\n${end}\n`;
|
|
42
|
-
if (!starts.length)
|
|
42
|
+
if (!starts.length) {
|
|
43
|
+
return { text: text + (text && !text.endsWith('\n') ? '\n' : '') + (text ? '\n' : '') + block, previous: null };
|
|
44
|
+
}
|
|
45
|
+
const previous = text.slice(starts[0].index + starts[0][0].length, ends[0].index);
|
|
43
46
|
let after = ends[0].index + ends[0][0].length;
|
|
44
47
|
if (text[after] === '\n') after++;
|
|
45
|
-
return text.slice(0, starts[0].index) + block + text.slice(after);
|
|
48
|
+
return { text: text.slice(0, starts[0].index) + block + text.slice(after), previous };
|
|
46
49
|
}
|
|
47
50
|
|
|
48
51
|
function parseConfig(text) {
|
|
@@ -50,6 +53,28 @@ function parseConfig(text) {
|
|
|
50
53
|
catch { throw new Error('Codex config.toml is not valid TOML. Repair this file before setup.'); }
|
|
51
54
|
}
|
|
52
55
|
|
|
56
|
+
async function matchesPreviousBundledSkill(server, skillText) {
|
|
57
|
+
const script = server?.args?.[0];
|
|
58
|
+
if (typeof script !== 'string' || !isAbsolute(script)) return false;
|
|
59
|
+
const previousRoot = dirname(dirname(script));
|
|
60
|
+
if (script !== join(previousRoot, 'src', 'server.js')) return false;
|
|
61
|
+
try {
|
|
62
|
+
const [currentPackage, previousPackage, previousSkill, scriptInfo] = await Promise.all([
|
|
63
|
+
readExisting(join(packageRoot, 'package.json')),
|
|
64
|
+
readExisting(join(previousRoot, 'package.json')),
|
|
65
|
+
readExisting(join(previousRoot, '.agents', 'skills', 'deepseek-worker', 'SKILL.md')),
|
|
66
|
+
inspect(script),
|
|
67
|
+
]);
|
|
68
|
+
const name = JSON.parse(currentPackage.text).name;
|
|
69
|
+
return typeof name === 'string' && name.length > 0
|
|
70
|
+
&& JSON.parse(previousPackage.text).name === name
|
|
71
|
+
&& scriptInfo?.isFile() === true
|
|
72
|
+
&& previousSkill.mode !== null && previousSkill.text === skillText;
|
|
73
|
+
} catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
53
78
|
async function atomicWrite(path, text, mode) {
|
|
54
79
|
const temp = join(dirname(path), `.dsh-mcp-${randomUUID()}.tmp`);
|
|
55
80
|
try {
|
|
@@ -82,18 +107,22 @@ export async function installCodex({
|
|
|
82
107
|
readFile(join(packageRoot, '.agents', 'skills', 'deepseek-worker', 'SKILL.md'), 'utf8'),
|
|
83
108
|
]);
|
|
84
109
|
const parsed = parseConfig(config.text);
|
|
85
|
-
if (parsed.mcp_servers?.dsh && !/^# BEGIN dsh-mcp\r?$/m.test(config.text)) {
|
|
86
|
-
throw new Error('Codex already defines mcp_servers.dsh. Preserve or rename that server before setup.');
|
|
87
|
-
}
|
|
88
|
-
if (skill.mode !== null && skill.text !== bundledSkill) {
|
|
89
|
-
throw new Error('A different deepseek-worker skill already exists. Preserve or rename that skill before setup.');
|
|
90
|
-
}
|
|
91
110
|
const args = [join(packageRoot, 'src', 'server.js'), '--dsh-home', home];
|
|
92
111
|
const entry = [
|
|
93
112
|
'[mcp_servers.dsh]', `command = ${JSON.stringify(process.execPath)}`, `args = ${JSON.stringify(args)}`,
|
|
94
113
|
'startup_timeout_sec = 20', 'tool_timeout_sec = 960',
|
|
95
114
|
].join('\n');
|
|
96
|
-
const nextConfig = managedBlock(config.text, '# BEGIN dsh-mcp', '# END dsh-mcp', entry);
|
|
115
|
+
const { text: nextConfig, previous } = managedBlock(config.text, '# BEGIN dsh-mcp', '# END dsh-mcp', entry);
|
|
116
|
+
const previousServer = previous === null ? undefined : parseConfig(previous).mcp_servers?.dsh;
|
|
117
|
+
const ownsServer = parsed.mcp_servers?.dsh && previousServer
|
|
118
|
+
&& isDeepStrictEqual(parsed.mcp_servers.dsh, previousServer);
|
|
119
|
+
if (parsed.mcp_servers?.dsh && !ownsServer) {
|
|
120
|
+
throw new Error('Codex already defines mcp_servers.dsh outside this installer’s managed block. Preserve or rename that server before setup.');
|
|
121
|
+
}
|
|
122
|
+
if (skill.mode !== null && skill.text !== bundledSkill
|
|
123
|
+
&& !(ownsServer && await matchesPreviousBundledSkill(previousServer, skill.text))) {
|
|
124
|
+
throw new Error('A different deepseek-worker skill already exists and cannot be safely upgraded. Back up or rename that skill before setup; existing files were not changed.');
|
|
125
|
+
}
|
|
97
126
|
parseConfig(nextConfig);
|
|
98
127
|
const existingKey = parseEnv(credentials.text).DEEPSEEK_API_KEY;
|
|
99
128
|
const providerSettings = parseProviderSettings(provider.text);
|
|
@@ -115,7 +144,7 @@ export async function installCodex({
|
|
|
115
144
|
if (nextProvider !== provider.text) await atomicWrite(providerPath, nextProvider, 0o600);
|
|
116
145
|
else await chmod(providerPath, 0o600);
|
|
117
146
|
if (nextConfig !== config.text) await atomicWrite(configPath, nextConfig, config.mode ?? 0o644);
|
|
118
|
-
if (skill.mode === null) await atomicWrite(skillPath, bundledSkill, 0o644);
|
|
147
|
+
if (skill.text !== bundledSkill || skill.mode === null) await atomicWrite(skillPath, bundledSkill, skill.mode ?? 0o644);
|
|
119
148
|
return { home, configPath, skillPath };
|
|
120
149
|
}
|
|
121
150
|
|
package/src/worker.js
CHANGED
|
@@ -5,11 +5,13 @@ import { createReadStream } from 'node:fs';
|
|
|
5
5
|
import { lstat, readlink, realpath, stat } from 'node:fs/promises';
|
|
6
6
|
import { isAbsolute, relative, resolve, sep } from 'node:path';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { readSessionOutput } from './session-output.js';
|
|
8
9
|
|
|
9
10
|
const exec = promisify(execFile);
|
|
10
11
|
const dsh = fileURLToPath(new URL('./lib/bin.js', import.meta.resolve('@deepseek-ai/dsh/package.json')));
|
|
11
12
|
const patch = fileURLToPath(new URL('../config/worker.patch.yml', import.meta.url));
|
|
12
13
|
const MAX_OUTPUT = 8 * 1024 * 1024;
|
|
14
|
+
const THINKING_EFFORTS = new Set(['off', 'low', 'high', 'max']);
|
|
13
15
|
|
|
14
16
|
// Wait for termination before releasing the worker; kill the process group, not just dsh.
|
|
15
17
|
export function runProcess(command, { cwd, env, input, timeoutMs, signal }) {
|
|
@@ -69,6 +71,7 @@ export function parseRun(stdout, exitCode) {
|
|
|
69
71
|
let sessionId = null;
|
|
70
72
|
let final = null;
|
|
71
73
|
let error = null;
|
|
74
|
+
let turn = null;
|
|
72
75
|
for (const line of stdout.split('\n')) {
|
|
73
76
|
if (!line.trim()) continue;
|
|
74
77
|
let event;
|
|
@@ -76,12 +79,13 @@ export function parseRun(stdout, exitCode) {
|
|
|
76
79
|
catch { throw new Error('Invalid DSH NDJSON output; check the pinned Harness version'); }
|
|
77
80
|
if (event?.type === 'session') sessionId = event.sessionId;
|
|
78
81
|
if (event?.type === 'error') error = event;
|
|
82
|
+
if (event?.type === 'status' && event.phase === 'turn_start') turn = event.turn;
|
|
79
83
|
if (event?.type === 'final') final = event;
|
|
80
84
|
}
|
|
81
85
|
return {
|
|
82
86
|
status: exitCode === 0 && final?.reason?.kind === 'completed' ? 'completed' : 'failed',
|
|
83
87
|
session_id: final?.sessionId ?? sessionId,
|
|
84
|
-
|
|
88
|
+
turn,
|
|
85
89
|
reason: final?.reason ?? error ?? null,
|
|
86
90
|
};
|
|
87
91
|
}
|
|
@@ -119,10 +123,10 @@ async function gitSnapshot(cwd) {
|
|
|
119
123
|
return { root, files };
|
|
120
124
|
}
|
|
121
125
|
|
|
122
|
-
export function createWorker({ home, command = [process.execPath, dsh] }) {
|
|
126
|
+
export function createWorker({ home, command = [process.execPath, dsh], readSession = readSessionOutput }) {
|
|
123
127
|
let busy = false;
|
|
124
128
|
return {
|
|
125
|
-
async delegate({ project_root, task, cwd = '.', permission_mode = 'read-only', session_id, timeout_seconds = 300 }, { signal } = {}) {
|
|
129
|
+
async delegate({ project_root, task, thinking_effort, cwd = '.', permission_mode = 'read-only', session_id, timeout_seconds = 300 }, { signal } = {}) {
|
|
126
130
|
if (busy) throw new Error('DeepSeek worker is busy; serialize tasks across all projects');
|
|
127
131
|
busy = true;
|
|
128
132
|
let before;
|
|
@@ -138,6 +142,9 @@ export function createWorker({ home, command = [process.execPath, dsh] }) {
|
|
|
138
142
|
throw new Error('cwd is outside project_root');
|
|
139
143
|
}
|
|
140
144
|
if (!(await stat(directory)).isDirectory()) throw new Error('cwd must be a directory');
|
|
145
|
+
if (!THINKING_EFFORTS.has(thinking_effort)) {
|
|
146
|
+
throw new Error('thinking_effort is required and must be off, low, high, or max');
|
|
147
|
+
}
|
|
141
148
|
before = await gitSnapshot(directory);
|
|
142
149
|
const args = ['headless', '--patch', patch, '--json', '--permission-mode', permission_mode, '--approval-policy', 'never'];
|
|
143
150
|
if (session_id) args.push('--session-id', session_id);
|
|
@@ -154,12 +161,26 @@ export function createWorker({ home, command = [process.execPath, dsh] }) {
|
|
|
154
161
|
].join('\n');
|
|
155
162
|
const result = await runProcess([...command, ...args], {
|
|
156
163
|
cwd: directory,
|
|
157
|
-
env: { ...process.env, DSH_HOME: home, DSH_PERMISSION_MODE: permission_mode },
|
|
164
|
+
env: { ...process.env, DSH_HOME: home, DSH_PERMISSION_MODE: permission_mode, DSH_MCP_THINKING_EFFORT: thinking_effort },
|
|
158
165
|
input: prompt, timeoutMs: timeout_seconds * 1000, signal,
|
|
159
166
|
});
|
|
160
167
|
let run;
|
|
161
168
|
try { run = parseRun(result.stdout, result.exit_code); }
|
|
162
|
-
catch (error) { run = { status: 'failed', session_id: null,
|
|
169
|
+
catch (error) { run = { status: 'failed', session_id: null, turn: null, reason: error.message }; }
|
|
170
|
+
let output = null;
|
|
171
|
+
let sessionOutputError = null;
|
|
172
|
+
try {
|
|
173
|
+
if (!run.session_id) throw new Error('The run returned no session ID for its persisted output');
|
|
174
|
+
const persisted = await readSession({ home, sessionId: run.session_id, cwd: directory });
|
|
175
|
+
if (run.turn === null || persisted.turn !== run.turn) {
|
|
176
|
+
throw new Error('The persisted latest turn does not match this run; no current answer is available');
|
|
177
|
+
}
|
|
178
|
+
output = persisted;
|
|
179
|
+
if (run.status === 'completed' && output.terminal_reason?.kind !== 'completed') {
|
|
180
|
+
sessionOutputError = 'The session turn did not complete durably';
|
|
181
|
+
}
|
|
182
|
+
} catch (error) { sessionOutputError = error.message; }
|
|
183
|
+
if (sessionOutputError && run.status === 'completed') run.status = 'failed';
|
|
163
184
|
let changedFiles = null;
|
|
164
185
|
let changeWarning = before ? null : 'Not a Git repository; changed_files is unavailable.';
|
|
165
186
|
try {
|
|
@@ -172,6 +193,13 @@ export function createWorker({ home, command = [process.execPath, dsh] }) {
|
|
|
172
193
|
return {
|
|
173
194
|
...run,
|
|
174
195
|
status: result.failure ?? run.status,
|
|
196
|
+
answer: output?.text ?? '',
|
|
197
|
+
output_source: output ? 'session' : null,
|
|
198
|
+
session_file: output?.session_file ?? null,
|
|
199
|
+
assistant_seq: output?.assistant_seq ?? null,
|
|
200
|
+
terminal_reason: output?.terminal_reason ?? null,
|
|
201
|
+
session_output_error: sessionOutputError,
|
|
202
|
+
thinking_effort,
|
|
175
203
|
project_root: root, cwd: directory, model: 'deepseek-flash', permission_mode,
|
|
176
204
|
exit_code: result.exit_code, signal: result.signal,
|
|
177
205
|
changed_files: changedFiles, git_root: before?.root ?? null,
|