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 CHANGED
@@ -1,129 +1,127 @@
1
1
  # dev-flow-deepseek
2
2
 
3
- `dev-flow-deepseek` is a prebuilt DeepSeek Harness bundle that connects
4
- the official DSH MCP client to an independently installed Dev Flow Orchestrator
5
- runtime. Its local STDIO projection proxy exposes exactly six tools and renders
6
- each complete Dev Flow result envelope as deterministic canonical JSON while
7
- preserving valid upstream success and domain-error semantics. The external
8
- Python Controller remains the only workflow and task-state authority.
9
-
10
- ## Exact compatibility
11
-
12
- | Component | Supported baseline |
13
- |---|---|
14
- | This package | `0.1.0` |
15
- | DeepSeek Harness | `@deepseek-ai/dsh@0.1.0-rc.6`, npm SHA-1 `de9fbf39056c7f4e658a3e284cb1d66ebc86d040`, public source baseline `47f943859bef60e4160492346772ded9b24f765a` |
16
- | Dev Flow Orchestrator | release `0.6.12`, commit `7edd4fa12fe6787aac57f16246cbab00b8d79272` |
17
- | Dev Flow MCP interface | `dev-flow-mcp/1.0.0` |
18
- | Dev Flow result schema | `dev-flow-mcp-result/1.0.0` |
19
- | Dev Flow model namespace | `0.4.0` |
20
- | Node.js | `^22.19.0 || >=24.0.0` |
21
- | MCP SDK | `1.12.0` |
22
-
23
- Compatibility is exact. Later DSH or Dev Flow releases are not inferred to be
24
- compatible.
25
-
26
- ## Prerequisites and installation
27
-
28
- Install DeepSeek Harness `0.1.0-rc.6` and the separate `dev-flow-mcp` executable
29
- from Dev Flow Orchestrator `0.6.12` before using this bundle. The executable may
30
- be on `PATH` or selected with an absolute `DEV_FLOW_MCP_COMMAND` value. This npm
31
- package does not install, update, repair, migrate, or remove Python, `uv`, or the
32
- Dev Flow runtime.
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
- Alternatively, build and pack locally with the pinned package manager:
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
- ```sh
43
- corepack pnpm@11.7.0 install --frozen-lockfile
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
- Add the resulting tarball to an isolated DSH profile, for example the `web`
49
- profile, then remove it by package name when no longer needed:
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
- ```sh
52
- dsh plugin --profile web add /absolute/path/dev-flow-deepseek-0.1.0.tgz
53
- dsh plugin --profile web remove dev-flow-deepseek
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
- Removal deletes only the profile dependency and bundle layer. It does not
57
- delete Dev Flow task data.
109
+ 移除后按照 DSH profile lifecycle 重启,再通过有效配置确认 bundle contribution 已消失。重新安装
110
+ 时重新执行 npm `@latest` pack 和 DSH add 命令。
58
111
 
59
- ## Data and use
112
+ ## Package 内容
60
113
 
61
- By default the bundle sets `DEV_FLOW_DATA_DIR` to
62
- `$DSH_HOME/dev-flow-orchestrator/data`; the compatible backend appends its
63
- `0.4.0` namespace. This keeps DSH tasks separate from Codex Dev Flow data. An
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
- Inside one existing Git worktree, invoke the Skill explicitly:
118
+ ## 维护者入口
67
119
 
68
- ```text
69
- /dev-flow Implement the requested change and verify it.
120
+ Package-local 验证:
121
+
122
+ ```bash
123
+ pnpm --dir packages/deepseek test
70
124
  ```
71
125
 
72
- The Skill validates the exact runtime identity, discovers a compatible active
73
- task or starts one `lite` task, follows only the Controller's fresh action and
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-mcp
3
- name: '@deepseek-ai/dsh-mcp-client'
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
+ }