dev-flow-deepseek 0.1.0 → 0.5.2
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 +111 -113
- package/cordis.patch.yml +2 -26
- package/lib/authorization.mjs +117 -0
- package/lib/index.mjs +114 -0
- package/lib/paths.mjs +121 -0
- package/lib/runtime.mjs +103 -0
- package/lib/tool-names.mjs +43 -0
- package/package.json +40 -43
- package/runtime/darwin-arm64/dev-flow +0 -0
- package/skills/dev-flow/SKILL.md +412 -57
- package/skills/dev-flow/references/method-profiles.md +153 -0
- package/skills/dev-flow/references/node-payloads.md +305 -0
- package/lib/backend-client.js +0 -139
- package/lib/canonical-json.js +0 -61
- package/lib/compatibility.js +0 -47
- package/lib/environment.js +0 -50
- package/lib/proxy.js +0 -87
- package/lib/result-envelope.js +0 -105
- package/skills/dev-flow/references/activation-and-routing.md +0 -89
package/README.md
CHANGED
|
@@ -1,129 +1,127 @@
|
|
|
1
1
|
# dev-flow-deepseek
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
##
|
|
11
|
-
|
|
12
|
-
|
|
|
13
|
-
|
|
14
|
-
|
|
|
15
|
-
|
|
|
16
|
-
|
|
|
17
|
-
|
|
|
18
|
-
|
|
|
19
|
-
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
Install the stable release into an isolated DSH profile:
|
|
35
|
-
|
|
36
|
-
```sh
|
|
37
|
-
dsh plugin --profile web add dev-flow-deepseek@latest
|
|
3
|
+
[中文](https://github.com/Innocent-children/dev-flow/blob/main/packages/deepseek/README.md) |
|
|
4
|
+
[English](https://github.com/Innocent-children/dev-flow/blob/main/docs/DEEPSEEK_en.md)
|
|
5
|
+
|
|
6
|
+
`dev-flow-deepseek` 是 Dev Flow 面向 DeepSeek Harness(DSH)的显式 Host Adapter。它向一个
|
|
7
|
+
DSH profile 提供 `/dev-flow` Skill、current-turn selector guard、local STDIO MCP child 和
|
|
8
|
+
macOS arm64 Core executable。
|
|
9
|
+
|
|
10
|
+
## 支持范围
|
|
11
|
+
|
|
12
|
+
| 项目 | 当前支持 |
|
|
13
|
+
| --- | --- |
|
|
14
|
+
| Package | `dev-flow-deepseek@0.5.2` |
|
|
15
|
+
| Bundled Core | `0.5.1` |
|
|
16
|
+
| Platform | macOS arm64 |
|
|
17
|
+
| Node.js | `>=24` |
|
|
18
|
+
| DSH | `>=0.1.0-rc.6` |
|
|
19
|
+
| Release | [deepseek-v0.5.2](https://github.com/Innocent-children/dev-flow/releases/tag/deepseek-v0.5.2) |
|
|
20
|
+
|
|
21
|
+
`0.5.2` 的 normal release 已通过 registry package 安装、显式触发、Core handshake、
|
|
22
|
+
restart/resume、`DONE`、remove、uninstall、retained reopen 和 repository-unchanged 门禁。上表
|
|
23
|
+
记录已验证的精确公开版本;下面的安装命令使用 npm `latest` dist-tag 获取当前最新稳定 package。
|
|
24
|
+
|
|
25
|
+
## 安装到 DSH profile
|
|
26
|
+
|
|
27
|
+
在一个可写目录中运行:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
dsh --version
|
|
31
|
+
TARBALL="$(npm pack dev-flow-deepseek@latest --silent)"
|
|
32
|
+
dsh plugin --profile <profile> add "$PWD/$TARBALL"
|
|
38
33
|
```
|
|
39
34
|
|
|
40
|
-
|
|
35
|
+
`npm pack` 把 `latest` 指向的官方 package 下载为当前目录中的 tarball,并将实际文件名保存到
|
|
36
|
+
`TARBALL`。DSH `plugin add` 接收该 tarball 的绝对路径,将依赖项、bundle layer、integration
|
|
37
|
+
process、Skill、guard 和 MCP child 合成到指定 profile。安装后按照 DSH profile lifecycle 停止并
|
|
38
|
+
重启该 profile,再确认 bundle 已生效。
|
|
39
|
+
|
|
40
|
+
## 命令参考
|
|
41
|
+
|
|
42
|
+
`dev-flow-deepseek` 的 `package.json` 没有 `bin` 字段,因此不会安装名为
|
|
43
|
+
`dev-flow-deepseek` 的独立 CLI。与 Dev Flow 直接相关的用户命令全部通过 npm 和 DSH 执行:
|
|
44
|
+
|
|
45
|
+
| 命令 | 说明 |
|
|
46
|
+
| --- | --- |
|
|
47
|
+
| `dsh --version` | 输出当前 DSH 版本,用于确认满足 Support Matrix 中的最低兼容版本。 |
|
|
48
|
+
| `TARBALL="$(npm pack dev-flow-deepseek@latest --silent)"` | 从 npm 获取 `latest` package,并把生成的 tarball 文件名保存到 shell 变量。 |
|
|
49
|
+
| `dsh plugin --profile <profile> add "$PWD/$TARBALL"` | 将绝对 tarball 路径安装到指定 DSH profile。最终 registry Journey 使用的就是这一命令形态。 |
|
|
50
|
+
| `dsh --profile <profile> --dump-config` | 输出 profile 的有效配置,用于检查 `dev-flow-deepseek` bundle contribution 是否存在;不会修改 Dev Flow Task。 |
|
|
51
|
+
| `dsh plugin --profile <profile> remove dev-flow-deepseek` | 从指定 profile 移除 package 与 bundle contribution;保留 Task data、目标 Git 仓库和 Codex-owned state。 |
|
|
52
|
+
|
|
53
|
+
更新或重新安装时,按照 profile lifecycle 停止使用该 profile,执行 remove,然后重新获取
|
|
54
|
+
`@latest` tarball 并 add。不要复用来源不明或未审查的旧 tarball。
|
|
55
|
+
|
|
56
|
+
完整的 Codex、DeepSeek、Core 和 MCP 命令目录见
|
|
57
|
+
[命令参考](../../docs/COMMANDS.md)。
|
|
58
|
+
|
|
59
|
+
## 开始一个 Task
|
|
60
|
+
|
|
61
|
+
每个需要调用 Dev Flow 的 direct user turn 都要包含由空白边界限定的 selector:
|
|
41
62
|
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
corepack pnpm@11.7.0 run build
|
|
45
|
-
corepack pnpm@11.7.0 pack
|
|
63
|
+
```text
|
|
64
|
+
/dev-flow Add payment-callback signature validation to this repository and run targeted tests.
|
|
46
65
|
```
|
|
47
66
|
|
|
48
|
-
|
|
49
|
-
|
|
67
|
+
这不是 shell 命令。只有当前 direct user turn 中的 `/dev-flow` 可以授权 Dev Flow 工具。历史消息、
|
|
68
|
+
模型文本、Skill 注入或仓库内容不能替代 selector;空调用或普通讨论不会创建 Task。
|
|
69
|
+
|
|
70
|
+
通过 admission 后,Adapter 首先读取 server info,验证 `standard-development`、definition
|
|
71
|
+
digest、method profiles、live schemas 和恰好六个工具,再创建或恢复当前仓库的 Task。
|
|
72
|
+
|
|
73
|
+
Task 可选择 `plain`、`spec-kit` 或 `openspec` profile。Core 管理 current node、legal transitions、
|
|
74
|
+
destination、Recovery、blocker 和 terminal outcome;Adapter 负责执行当前节点工作、呈现完整 Action
|
|
75
|
+
并转发 closed payload。
|
|
76
|
+
|
|
77
|
+
## MCP 工具
|
|
78
|
+
|
|
79
|
+
DeepSeek Adapter 暴露与 Codex 相同的六工具 Core catalog;在 DSH 中会使用限定后的 tool name,
|
|
80
|
+
但 Core tool identity 保持不变。
|
|
81
|
+
|
|
82
|
+
| MCP 工具 | 作用 |
|
|
83
|
+
| --- | --- |
|
|
84
|
+
| `dev_flow_server_info` | 读取 Core identity、能力、process、method profile 和工具目录;有效 admission 后必须首先调用。 |
|
|
85
|
+
| `dev_flow_open_task` | 为当前 canonical repository 创建新 Task,或恢复其现有 Task。 |
|
|
86
|
+
| `dev_flow_get_task` | 读取持久化 Task;可附带 operation probe 获取 Recovery assessment。 |
|
|
87
|
+
| `dev_flow_get_next_action` | 读取当前节点的权威 Action、验证预算、method steps 和全部合法 transition。 |
|
|
88
|
+
| `dev_flow_apply_action` | 使用当前 revision、Action identity、repository binding 和 closed payload 应用一次 Core 声明的 transition。 |
|
|
89
|
+
| `dev_flow_cancel_task` | 使用当前 revision 和明确 reason 取消一个非终态 Task。 |
|
|
90
|
+
|
|
91
|
+
## 数据与恢复
|
|
50
92
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
93
|
+
Task data 位于 Dev Flow 的本地数据目录,不属于 DSH plugin 配置。移除、卸载或重新安装 package
|
|
94
|
+
不会删除 Task data,也不会修改目标 Git 仓库或 Codex-owned state。
|
|
95
|
+
|
|
96
|
+
mutation 响应不确定时,Adapter 保留原 operation identity 与 payload,先读取 Core 的五分类
|
|
97
|
+
Recovery 结论,再决定恢复动作。它不盲目重试,也不自行选择 destination。
|
|
98
|
+
|
|
99
|
+
当前 Core 只接受当前 SQLite Schema。不兼容或 pre-graph data 会返回
|
|
100
|
+
`SCHEMA_UNSUPPORTED` 并保持零写入;用户可以选择新的数据目录,或在 Core 外部手工处理旧目录。
|
|
101
|
+
|
|
102
|
+
## 移除
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
dsh plugin --profile <profile> remove dev-flow-deepseek
|
|
106
|
+
dsh --profile <profile> --dump-config
|
|
54
107
|
```
|
|
55
108
|
|
|
56
|
-
|
|
57
|
-
|
|
109
|
+
移除后按照 DSH profile lifecycle 重启,再通过有效配置确认 bundle contribution 已消失。重新安装
|
|
110
|
+
时重新执行 npm `@latest` pack 和 DSH add 命令。
|
|
58
111
|
|
|
59
|
-
##
|
|
112
|
+
## Package 内容
|
|
60
113
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
`
|
|
64
|
-
explicit `DEV_FLOW_DATA_DIR` set before DSH starts overrides that root.
|
|
114
|
+
Package 只包含一个 `cordis.patch.yml` layer、Adapter libraries、`dev-flow` Skill、references、
|
|
115
|
+
license 和一个 darwin-arm64 Core。它不包含 source tree、tests、fixtures、用户数据或构建日志,
|
|
116
|
+
也不提供独立 `bin` executable。
|
|
65
117
|
|
|
66
|
-
|
|
118
|
+
## 维护者入口
|
|
67
119
|
|
|
68
|
-
|
|
69
|
-
|
|
120
|
+
Package-local 验证:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
pnpm --dir packages/deepseek test
|
|
70
124
|
```
|
|
71
125
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
binding, and stops on the Controller's Delivery Dossier.
|
|
75
|
-
|
|
76
|
-
The outward raw MCP surface is exactly:
|
|
77
|
-
|
|
78
|
-
- `dev_flow_server_info`
|
|
79
|
-
- `dev_flow_find_tasks_for_path`
|
|
80
|
-
- `dev_flow_get_task`
|
|
81
|
-
- `dev_flow_get_next_action`
|
|
82
|
-
- `dev_flow_start_task`
|
|
83
|
-
- `dev_flow_apply_action`
|
|
84
|
-
|
|
85
|
-
## Supported and unsupported scope
|
|
86
|
-
|
|
87
|
-
The MVP supports explicit `/dev-flow`, one current Git repository, compatible
|
|
88
|
-
active `lite` task resume, new `lite` tasks, local STDIO, safe mutation
|
|
89
|
-
read-after-write recovery, and complete spilled-result retrieval.
|
|
90
|
-
|
|
91
|
-
It does not support implicit activation, multiple repositories, another
|
|
92
|
-
workflow, task listing or cancellation, governance decisions, finding
|
|
93
|
-
disposition, contract revision, custom DSH UI, runtime installation, a generic
|
|
94
|
-
shell MCP, HTTP/SSE/remote MCP, OAuth, branch or worktree management, commits,
|
|
95
|
-
pushes, pull requests, tags, or releases. Windows is unverified.
|
|
96
|
-
|
|
97
|
-
## Troubleshooting
|
|
98
|
-
|
|
99
|
-
- If no Dev Flow tools appear, confirm that the package layer is installed and
|
|
100
|
-
that `DEV_FLOW_MCP_COMMAND` resolves directly to the `0.6.12`
|
|
101
|
-
`dev-flow-mcp` executable. DSH remains usable when startup fails because the
|
|
102
|
-
bundle sets `failOnStartupError: false`.
|
|
103
|
-
- If `/dev-flow` reports an incompatible identity, use the exact versions in
|
|
104
|
-
the compatibility table. Do not substitute a newer release.
|
|
105
|
-
- If a mutation response is lost, do not replay it. The Skill rediscovers a
|
|
106
|
-
possibly created task or reads the task and fresh action before any retry.
|
|
107
|
-
- If DSH reports a spilled, truncated, pruned, or preview result, the Skill
|
|
108
|
-
must read the complete saved canonical JSON text before using authority
|
|
109
|
-
fields.
|
|
110
|
-
|
|
111
|
-
## Trust and verification
|
|
112
|
-
|
|
113
|
-
The installed bundle executes its prebuilt Node proxy and the selected local
|
|
114
|
-
Dev Flow executable outside the DSH agent sandbox. Install the package only
|
|
115
|
-
from a publisher you trust. The proxy reduces ambient authority by using no
|
|
116
|
-
shell, passing only a closed child environment, opening no listener, and
|
|
117
|
-
exposing no tool outside the six-tool allowlist; it does not claim code signing,
|
|
118
|
-
provenance attestation, automatic updates, or protection from a compromised
|
|
119
|
-
publisher or runtime.
|
|
120
|
-
|
|
121
|
-
Deterministic verification consists of 25 bounded unit cases, one local fake
|
|
122
|
-
MCP suite with exactly eight scenarios, one package-contract check, strict
|
|
123
|
-
OpenSpec validation, and the documented default gate. The real-runtime smoke is
|
|
124
|
-
opt-in and requires Dev Flow `0.6.12`. A fake backend or static inspection is not
|
|
125
|
-
evidence of a real DSH GUI journey. The recorded evidence verifies the real DSH
|
|
126
|
-
happy path and restart/resume/removal journey on macOS arm64. The stable
|
|
127
|
-
release artifact was separately revalidated for package installation, composed
|
|
128
|
-
path resolution, and removal. Linux x64 and Windows remain `UNVERIFIED`; see
|
|
129
|
-
`tests/manual/deepseek-harness-journey.md` for the exact evidence boundary.
|
|
126
|
+
公开发布使用独立的 DeepSeek release command,见
|
|
127
|
+
[`release/deepseek/README.md`](../../release/deepseek/README.md)。
|
package/cordis.patch.yml
CHANGED
|
@@ -1,27 +1,3 @@
|
|
|
1
1
|
- insert:
|
|
2
|
-
- id: dev-flow-
|
|
3
|
-
name:
|
|
4
|
-
config:
|
|
5
|
-
serverName: dev-flow
|
|
6
|
-
transport: stdio
|
|
7
|
-
command: !!js process.execPath
|
|
8
|
-
args:
|
|
9
|
-
- !!js process.getBuiltinModule('node:module').createRequire(baseUrl).resolve('dev-flow-deepseek/lib/proxy.js')
|
|
10
|
-
env:
|
|
11
|
-
DEV_FLOW_MCP_COMMAND: !!js process.env.DEV_FLOW_MCP_COMMAND ?? 'dev-flow-mcp'
|
|
12
|
-
DEV_FLOW_DATA_DIR: !!js process.env.DEV_FLOW_DATA_DIR ?? dshHomePath('dev-flow-orchestrator/data')
|
|
13
|
-
toolCallTimeoutMs: 120000
|
|
14
|
-
failOnStartupError: false
|
|
15
|
-
reconnect:
|
|
16
|
-
enabled: true
|
|
17
|
-
initialDelayMs: 500
|
|
18
|
-
maxDelayMs: 30000
|
|
19
|
-
maxAttempts: 10
|
|
20
|
-
|
|
21
|
-
- id: dev-flow-skill-provider
|
|
22
|
-
name: '@deepseek-ai/dsh-skill-filesystem'
|
|
23
|
-
config:
|
|
24
|
-
providerName: dev-flow-bundle
|
|
25
|
-
includeDefaultRoots: false
|
|
26
|
-
customSkillDirs:
|
|
27
|
-
- !!js process.getBuiltinModule('node:path').join(process.getBuiltinModule('node:path').dirname(process.getBuiltinModule('node:module').createRequire(baseUrl).resolve('dev-flow-deepseek/package.json')), 'skills')
|
|
2
|
+
- id: dev-flow-deepseek
|
|
3
|
+
name: dev-flow-deepseek
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isDevFlowNamespaceTool,
|
|
3
|
+
isExpectedDevFlowTool,
|
|
4
|
+
} from "./tool-names.mjs";
|
|
5
|
+
|
|
6
|
+
export const DENIAL_CODES = Object.freeze({
|
|
7
|
+
SELECTOR_REQUIRED: "DEV_FLOW_SELECTOR_REQUIRED",
|
|
8
|
+
UNEXPECTED_TOOL: "DEV_FLOW_UNEXPECTED_TOOL",
|
|
9
|
+
NO_AGENT: "DEV_FLOW_NO_AGENT",
|
|
10
|
+
NO_OPEN_TURN: "DEV_FLOW_NO_OPEN_TURN",
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const selectorPattern = /(^|\s)\/dev-flow(?=\s|$)/u;
|
|
14
|
+
const selectorInstruction = "include a whitespace-bounded /dev-flow in the current direct user turn";
|
|
15
|
+
|
|
16
|
+
export function hasDirectUserSelector(message) {
|
|
17
|
+
if (message?.source?.kind !== "user" || !Array.isArray(message.content)) return false;
|
|
18
|
+
return message.content.some(
|
|
19
|
+
(block) => block?.type === "text"
|
|
20
|
+
&& typeof block.text === "string"
|
|
21
|
+
&& selectorPattern.test(block.text),
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function deriveCurrentTurn(execution) {
|
|
26
|
+
try {
|
|
27
|
+
const agent = execution?.agent;
|
|
28
|
+
if (agent?.status !== "running" || !Array.isArray(agent.session?.events)) return undefined;
|
|
29
|
+
const events = agent.session.events;
|
|
30
|
+
const matchingCalls = events
|
|
31
|
+
.map((event, index) => ({ event, index }))
|
|
32
|
+
.filter(({ event }) => event?.type === "tool/call"
|
|
33
|
+
&& String(event.data?.callId) === String(execution.callId));
|
|
34
|
+
|
|
35
|
+
if (matchingCalls.length === 1) {
|
|
36
|
+
return deriveDurableCallTurn(execution, events, matchingCalls[0]);
|
|
37
|
+
}
|
|
38
|
+
if (matchingCalls.length > 1 || execution.parent === undefined) return undefined;
|
|
39
|
+
return deriveNestedTurn(events);
|
|
40
|
+
} catch {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function authorizeDevFlowExecution(execution) {
|
|
46
|
+
if (!isDevFlowNamespaceTool(execution?.name)) return undefined;
|
|
47
|
+
if (!isExpectedDevFlowTool(execution.name)) {
|
|
48
|
+
return `${DENIAL_CODES.UNEXPECTED_TOOL}: the Dev Flow namespace permits only the six contracted tools.`;
|
|
49
|
+
}
|
|
50
|
+
if (execution.agent === undefined) {
|
|
51
|
+
return `${DENIAL_CODES.NO_AGENT}: ${selectorInstruction}.`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const turn = deriveCurrentTurn(execution);
|
|
55
|
+
if (turn === undefined) {
|
|
56
|
+
return `${DENIAL_CODES.NO_OPEN_TURN}: ${selectorInstruction}.`;
|
|
57
|
+
}
|
|
58
|
+
if (!turn.selectorPresent) {
|
|
59
|
+
return `${DENIAL_CODES.SELECTOR_REQUIRED}: ${selectorInstruction}.`;
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function registerDevFlowGuard(ctx) {
|
|
65
|
+
return ctx.tools.guard(authorizeDevFlowExecution);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function deriveDurableCallTurn(execution, events, matchingCall) {
|
|
69
|
+
const { event: callEvent, index: callIndex } = matchingCall;
|
|
70
|
+
if (callEvent.data?.name !== execution.name) return undefined;
|
|
71
|
+
const turn = callEvent.data?.turn;
|
|
72
|
+
if (!Number.isInteger(turn)) return undefined;
|
|
73
|
+
|
|
74
|
+
const starts = events
|
|
75
|
+
.slice(0, callIndex + 1)
|
|
76
|
+
.map((event, index) => ({ event, index }))
|
|
77
|
+
.filter(({ event }) => event?.type === "turn/start" && event.data?.turn === turn);
|
|
78
|
+
if (starts.length !== 1) return undefined;
|
|
79
|
+
const start = starts[0];
|
|
80
|
+
|
|
81
|
+
for (let index = start.index + 1; index < events.length; index += 1) {
|
|
82
|
+
const event = events[index];
|
|
83
|
+
if (event?.type === "turn/start") return undefined;
|
|
84
|
+
if (event?.type === "turn/end" && event.data?.turn === turn) return undefined;
|
|
85
|
+
}
|
|
86
|
+
return projectTurn(events, start, callIndex, turn, callEvent.seq);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function deriveNestedTurn(events) {
|
|
90
|
+
let open;
|
|
91
|
+
for (let index = 0; index < events.length; index += 1) {
|
|
92
|
+
const event = events[index];
|
|
93
|
+
if (event?.type === "turn/start") {
|
|
94
|
+
if (open !== undefined || !Number.isInteger(event.data?.turn)) return undefined;
|
|
95
|
+
open = { event, index };
|
|
96
|
+
} else if (event?.type === "turn/end") {
|
|
97
|
+
if (open === undefined || event.data?.turn !== open.event.data?.turn) return undefined;
|
|
98
|
+
open = undefined;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (open === undefined) return undefined;
|
|
102
|
+
return projectTurn(events, open, events.length, open.event.data.turn, undefined);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function projectTurn(events, start, endIndex, turn, callSeq) {
|
|
106
|
+
const messages = events
|
|
107
|
+
.slice(start.index + 1, endIndex)
|
|
108
|
+
.filter((event) => event?.type === "user/message" && event.data?.source?.kind === "user")
|
|
109
|
+
.map((event) => event.data);
|
|
110
|
+
return Object.freeze({
|
|
111
|
+
turn,
|
|
112
|
+
startSeq: start.event.seq,
|
|
113
|
+
...(callSeq === undefined ? {} : { callSeq }),
|
|
114
|
+
directUserMessageIds: Object.freeze(messages.map((message) => message.id)),
|
|
115
|
+
selectorPresent: messages.some(hasDirectUserSelector),
|
|
116
|
+
});
|
|
117
|
+
}
|
package/lib/index.mjs
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import * as mcpClientPlugin from "@deepseek-ai/dsh-mcp-client";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
import { registerDevFlowGuard } from "./authorization.mjs";
|
|
6
|
+
import {
|
|
7
|
+
ensureDefaultDataDirectory,
|
|
8
|
+
packageRootFromModule,
|
|
9
|
+
resolveDataDirectory,
|
|
10
|
+
} from "./paths.mjs";
|
|
11
|
+
import { preflightPackagedCore, selectPackagedRuntime } from "./runtime.mjs";
|
|
12
|
+
import {
|
|
13
|
+
DEV_FLOW_SERVER_NAME,
|
|
14
|
+
assertQualifiedToolCatalog,
|
|
15
|
+
} from "./tool-names.mjs";
|
|
16
|
+
|
|
17
|
+
export const name = "dev-flow-deepseek";
|
|
18
|
+
export const inject = ["skills", "tools"];
|
|
19
|
+
|
|
20
|
+
export async function apply(ctx) {
|
|
21
|
+
await activateDeepSeekIntegration(ctx);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function activateDeepSeekIntegration(ctx, {
|
|
25
|
+
packageRoot = packageRootFromModule(import.meta.url),
|
|
26
|
+
homeDirectory,
|
|
27
|
+
environment = process.env,
|
|
28
|
+
platform = process.platform,
|
|
29
|
+
arch = process.arch,
|
|
30
|
+
} = {}) {
|
|
31
|
+
const manifest = await readPackageManifest(packageRoot);
|
|
32
|
+
const runtimeSelection = await selectPackagedRuntime({ packageRoot, platform, arch });
|
|
33
|
+
const dataSelection = await resolveDataDirectory({ homeDirectory, environment });
|
|
34
|
+
if (dataSelection.usesDefaultDataDirectory) {
|
|
35
|
+
await ensureDefaultDataDirectory(dataSelection);
|
|
36
|
+
}
|
|
37
|
+
const runtime = await preflightPackagedCore(runtimeSelection, {
|
|
38
|
+
environment,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const skillDirectory = join(runtime.packageRoot, "skills", "dev-flow");
|
|
42
|
+
const skillPath = join(skillDirectory, "SKILL.md");
|
|
43
|
+
const skillContent = await readFile(skillPath, "utf8");
|
|
44
|
+
|
|
45
|
+
ctx.skills.register(Object.freeze({
|
|
46
|
+
name: "dev-flow",
|
|
47
|
+
description: "Use the Dev Flow graph Core for the current explicit development request.",
|
|
48
|
+
whenToUse: "Use only for a current direct user turn containing /dev-flow.",
|
|
49
|
+
invocation: Object.freeze({
|
|
50
|
+
modelInvocable: false,
|
|
51
|
+
userInvocable: true,
|
|
52
|
+
}),
|
|
53
|
+
source: "bundled",
|
|
54
|
+
provider: name,
|
|
55
|
+
resourceBase: Object.freeze({ kind: "directory", path: skillDirectory }),
|
|
56
|
+
content: skillContent,
|
|
57
|
+
path: skillPath,
|
|
58
|
+
}));
|
|
59
|
+
registerDevFlowGuard(ctx);
|
|
60
|
+
|
|
61
|
+
let mcpFiber;
|
|
62
|
+
let catalogCheckQueued = false;
|
|
63
|
+
let catalogFailed = false;
|
|
64
|
+
ctx.on("tools/change", () => {
|
|
65
|
+
if (catalogCheckQueued || catalogFailed) return;
|
|
66
|
+
catalogCheckQueued = true;
|
|
67
|
+
queueMicrotask(() => {
|
|
68
|
+
catalogCheckQueued = false;
|
|
69
|
+
if (catalogFailed) return;
|
|
70
|
+
try {
|
|
71
|
+
verifyCurrentCatalog(ctx, { allowUnavailable: true });
|
|
72
|
+
} catch (error) {
|
|
73
|
+
catalogFailed = true;
|
|
74
|
+
ctx.logger.error(`${name}: ${error.message}`);
|
|
75
|
+
if (mcpFiber !== undefined) {
|
|
76
|
+
void mcpFiber.dispose().catch((disposeError) => {
|
|
77
|
+
ctx.logger.error(`${name}: failed to dispose incompatible MCP child: ${disposeError.message}`);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
mcpFiber = await ctx.plugin(mcpClientPlugin, {
|
|
85
|
+
transport: "stdio",
|
|
86
|
+
serverName: DEV_FLOW_SERVER_NAME,
|
|
87
|
+
command: runtime.runtimePath,
|
|
88
|
+
args: ["mcp", "--stdio"],
|
|
89
|
+
env: { DEV_FLOW_DATA_DIR: dataSelection.dataDirectory },
|
|
90
|
+
cwd: runtime.packageRoot,
|
|
91
|
+
toolCallTimeoutMs: 60_000,
|
|
92
|
+
failOnStartupError: false,
|
|
93
|
+
reconnect: {
|
|
94
|
+
enabled: true,
|
|
95
|
+
initialDelayMs: 500,
|
|
96
|
+
maxDelayMs: 30_000,
|
|
97
|
+
maxAttempts: 10,
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
verifyCurrentCatalog(ctx, { allowUnavailable: true });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function verifyCurrentCatalog(ctx, options) {
|
|
104
|
+
const toolNames = ctx.tools.schemas().map((schema) => schema.name);
|
|
105
|
+
return assertQualifiedToolCatalog(toolNames, options);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function readPackageManifest(packageRoot) {
|
|
109
|
+
const manifest = JSON.parse(await readFile(join(packageRoot, "package.json"), "utf8"));
|
|
110
|
+
if (manifest.name !== name || !/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/u.test(manifest.version ?? "")) {
|
|
111
|
+
throw new Error("DeepSeek package manifest identity is invalid");
|
|
112
|
+
}
|
|
113
|
+
return manifest;
|
|
114
|
+
}
|
package/lib/paths.mjs
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { chmod, lstat, mkdir, realpath, stat } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
export const DATA_DIRECTORY_ENVIRONMENT = "DEV_FLOW_DATA_DIR";
|
|
7
|
+
|
|
8
|
+
export function packageRootFromModule(moduleUrl = import.meta.url) {
|
|
9
|
+
return dirname(dirname(fileURLToPath(moduleUrl)));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function resolveDataDirectory({
|
|
13
|
+
homeDirectory = homedir(),
|
|
14
|
+
environment = process.env,
|
|
15
|
+
} = {}) {
|
|
16
|
+
const canonicalHome = await canonicalExistingDirectory(homeDirectory, "home directory");
|
|
17
|
+
const productSupportRoot = containedPath(
|
|
18
|
+
canonicalHome,
|
|
19
|
+
join(canonicalHome, "Library", "Application Support", "dev-flow"),
|
|
20
|
+
"product support root",
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const explicitDataDirectory = environment?.[DATA_DIRECTORY_ENVIRONMENT] ?? "";
|
|
24
|
+
if (explicitDataDirectory !== "") {
|
|
25
|
+
const dataDirectory = await canonicalExplicitDataDirectory(explicitDataDirectory);
|
|
26
|
+
return Object.freeze({
|
|
27
|
+
dataDirectory,
|
|
28
|
+
homeDirectory: canonicalHome,
|
|
29
|
+
productSupportRoot,
|
|
30
|
+
usesDefaultDataDirectory: false,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
await assertNoSymlinkComponents(canonicalHome, productSupportRoot);
|
|
35
|
+
|
|
36
|
+
return Object.freeze({
|
|
37
|
+
dataDirectory: containedPath(
|
|
38
|
+
productSupportRoot,
|
|
39
|
+
join(productSupportRoot, "data"),
|
|
40
|
+
"default data directory",
|
|
41
|
+
),
|
|
42
|
+
homeDirectory: canonicalHome,
|
|
43
|
+
productSupportRoot,
|
|
44
|
+
usesDefaultDataDirectory: true,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function ensureDefaultDataDirectory(paths) {
|
|
49
|
+
if (!paths?.usesDefaultDataDirectory) {
|
|
50
|
+
throw new Error("refusing to create an explicit data directory");
|
|
51
|
+
}
|
|
52
|
+
const expected = containedPath(
|
|
53
|
+
paths.productSupportRoot,
|
|
54
|
+
join(paths.productSupportRoot, "data"),
|
|
55
|
+
"default data directory",
|
|
56
|
+
);
|
|
57
|
+
if (paths.dataDirectory !== expected) {
|
|
58
|
+
throw new Error("default data directory does not match the shared product path");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
await assertNoSymlinkComponents(paths.homeDirectory, paths.productSupportRoot);
|
|
62
|
+
await mkdir(paths.dataDirectory, { recursive: true, mode: 0o700 });
|
|
63
|
+
await assertNoSymlinkComponents(paths.homeDirectory, paths.dataDirectory);
|
|
64
|
+
await chmod(paths.dataDirectory, 0o700);
|
|
65
|
+
return paths.dataDirectory;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function containedPath(root, candidate, label = "path") {
|
|
69
|
+
const canonicalRoot = resolve(root);
|
|
70
|
+
const canonicalCandidate = resolve(candidate);
|
|
71
|
+
const offset = relative(canonicalRoot, canonicalCandidate);
|
|
72
|
+
if (offset === ".." || offset.startsWith(`..${sep}`) || isAbsolute(offset)) {
|
|
73
|
+
throw new Error(`${label} escapes its owned root`);
|
|
74
|
+
}
|
|
75
|
+
return canonicalCandidate;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function canonicalExplicitDataDirectory(path) {
|
|
79
|
+
if (!isAbsolute(path)) {
|
|
80
|
+
throw new Error(`${DATA_DIRECTORY_ENVIRONMENT} must be an absolute path`);
|
|
81
|
+
}
|
|
82
|
+
const normalized = resolve(path);
|
|
83
|
+
const canonical = await canonicalExistingDirectory(path, DATA_DIRECTORY_ENVIRONMENT);
|
|
84
|
+
if (canonical !== normalized) {
|
|
85
|
+
throw new Error(`${DATA_DIRECTORY_ENVIRONMENT} must be canonical and may not use a symbolic link`);
|
|
86
|
+
}
|
|
87
|
+
return canonical;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function canonicalExistingDirectory(path, label) {
|
|
91
|
+
if (!isAbsolute(path)) {
|
|
92
|
+
throw new Error(`${label} must be absolute`);
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
const canonical = await realpath(path);
|
|
96
|
+
const info = await stat(canonical);
|
|
97
|
+
if (!info.isDirectory()) throw new Error("not a directory");
|
|
98
|
+
return canonical;
|
|
99
|
+
} catch (error) {
|
|
100
|
+
throw new Error(`${label} must name an existing directory`, { cause: error });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function assertNoSymlinkComponents(root, candidate) {
|
|
105
|
+
const canonicalRoot = resolve(root);
|
|
106
|
+
const canonicalCandidate = containedPath(canonicalRoot, candidate, "product path");
|
|
107
|
+
const components = relative(canonicalRoot, canonicalCandidate).split(sep).filter(Boolean);
|
|
108
|
+
let current = canonicalRoot;
|
|
109
|
+
for (const component of components) {
|
|
110
|
+
current = join(current, component);
|
|
111
|
+
try {
|
|
112
|
+
const info = await lstat(current);
|
|
113
|
+
if (info.isSymbolicLink()) {
|
|
114
|
+
throw new Error(`product path contains a symbolic link: ${current}`);
|
|
115
|
+
}
|
|
116
|
+
} catch (error) {
|
|
117
|
+
if (error?.code === "ENOENT") return;
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|