dsh-code 1.0.5 → 1.0.7

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.
Files changed (47) hide show
  1. package/README.en.md +338 -286
  2. package/README.md +68 -16
  3. package/bin/deepseek.mjs +204 -4
  4. package/cordis.patch.yml +105 -7
  5. package/lib/index.mjs +2148 -439
  6. package/lib/session-query.mjs +149 -0
  7. package/lib/types/app.d.ts +34 -8
  8. package/lib/types/attachments.d.ts +36 -4
  9. package/lib/types/index.d.ts +38 -2
  10. package/lib/types/kernel-panels.d.ts +23 -0
  11. package/lib/types/provider-settings.d.ts +6 -11
  12. package/lib/types/render/animations.d.ts +74 -7
  13. package/lib/types/render/editor.d.ts +4 -3
  14. package/lib/types/render/export.d.ts +0 -6
  15. package/lib/types/render/fuzzy.d.ts +21 -0
  16. package/lib/types/render/ime-cursor.d.ts +60 -0
  17. package/lib/types/render/projection.d.ts +80 -4
  18. package/lib/types/render/status.d.ts +1 -1
  19. package/lib/types/session-directory.d.ts +48 -13
  20. package/lib/types/session-query.d.ts +92 -0
  21. package/lib/types/store.d.ts +3 -0
  22. package/lib/types/terminal-title.d.ts +58 -0
  23. package/lib/types/update-panel.d.ts +49 -0
  24. package/lib/types/update.d.ts +66 -0
  25. package/package.json +307 -162
  26. package/src/app.ts +730 -266
  27. package/src/attachments.ts +110 -11
  28. package/src/commands.ts +35 -5
  29. package/src/index.ts +1986 -1779
  30. package/src/internals.ts +66 -40
  31. package/src/kernel-panels.ts +89 -3
  32. package/src/provider-settings.ts +12 -12
  33. package/src/render/animations.ts +606 -403
  34. package/src/render/editor.ts +5 -4
  35. package/src/render/export.ts +13 -3
  36. package/src/render/fuzzy.ts +83 -0
  37. package/src/render/ime-cursor.ts +147 -0
  38. package/src/render/projection.ts +1974 -1621
  39. package/src/render/status.ts +18 -4
  40. package/src/session-directory.ts +94 -16
  41. package/src/session-query.ts +235 -0
  42. package/src/skills.ts +23 -9
  43. package/src/store.ts +39 -1
  44. package/src/subagents.ts +26 -3
  45. package/src/terminal-title.ts +173 -0
  46. package/src/update-panel.ts +246 -0
  47. package/src/update.ts +110 -0
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
  <p align="center"><img alt="Typing SVG" src="https://readme-typing-svg.herokuapp.com?font=JetBrains+Mono&amp;weight=500&amp;size=22&amp;duration=4000&amp;pause=700&amp;color=4176E6&amp;center=true&amp;vCenter=true&amp;width=680&amp;lines=DeepSeek+Harness+Code;DSH+%E5%86%85%E6%A0%B8%E7%9A%84%E7%BB%88%E7%AB%AF%E7%BC%96%E7%A0%81%E7%95%8C%E9%9D%A2"></p>
8
8
  <p align="center">
9
9
  <a href="https://github.com/deepseek-ai/deepseek-harness"><img alt="DeepSeek Harness" src="https://img.shields.io/badge/DeepSeek-Harness-4176E6?style=for-the-badge&amp;logo=deepseek&amp;logoColor=white&amp;labelColor=1c1917"></a>
10
- <a href="https://www.npmjs.com/package/@deepseek-ai/dsh"><img alt="dsh version" src="https://img.shields.io/badge/dsh-0.1.1--rc.2-4176E6?style=for-the-badge&amp;logo=deepseek&amp;logoColor=white&amp;labelColor=1c1917"></a>
10
+ <a href="https://www.npmjs.com/package/@deepseek-ai/dsh"><img alt="dsh version" src="https://img.shields.io/badge/dsh-0.1.5--rc.1-4176E6?style=for-the-badge&amp;logo=deepseek&amp;logoColor=white&amp;labelColor=1c1917"></a>
11
11
  <a href="https://github.com/UNLINEARITY/dsh-code/stargazers"><img alt="GitHub stars" src="https://img.shields.io/github/stars/UNLINEARITY/dsh-code?label=Stars&amp;style=for-the-badge&amp;logo=github&amp;logoColor=white&amp;color=4176E6&amp;labelColor=1c1917"></a>
12
12
  <a href="https://www.npmjs.com/package/dsh-code"><img alt="npm version" src="https://img.shields.io/npm/v/dsh-code?label=npm&amp;style=for-the-badge&amp;logo=npm&amp;color=cb3837&amp;labelColor=1c1917"></a>
13
13
  <a href="https://github.com/UNLINEARITY/dsh-code/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/github/license/UNLINEARITY/dsh-code?label=License&amp;style=for-the-badge&amp;logo=opensourceinitiative&amp;color=4176E6&amp;labelColor=1c1917"></a>
@@ -25,23 +25,23 @@ DeepSeek Harness 将模型、工具、存储、策略和界面作为插件,通
25
25
 
26
26
  ## 二、快速开始
27
27
 
28
- 需要 Node `^22.19 || >=24` 和预览版 `dsh` CLI(当前版本线:`@deepseek-ai/dsh@0.1.2-rc.1`)。未配置模型时仍可进入 TUI、查看会话和使用非模型功能;在 `/model` 中按 Tab 进入供应商管理,配置 API key、OAuth 与设备码登录。
28
+ 需要 Node `^22.19 || >=24` 和预览版 `dsh` CLI(当前版本线:`@deepseek-ai/dsh@0.1.5-rc.2`)。未配置模型时仍可进入 TUI、查看会话和使用非模型功能;在 `/model` 中按 Tab 进入供应商管理,配置 API key、OAuth 与设备码登录。
29
29
 
30
30
  ### 1. 安装与更新
31
31
 
32
32
  初次安装和更新使用同一组指令:
33
33
 
34
34
  ```sh
35
- npm install -g @deepseek-ai/dsh@0.1.2-rc.1 dsh-code@1.0.5
35
+ npm install -g @deepseek-ai/dsh@0.1.5-rc.2 dsh-code@1.0.7
36
36
  npm install -g pnpm
37
- dsh plugin --profile cli add dsh-code@1.0.5
37
+ dsh plugin --profile cli add dsh-code@1.0.7
38
38
  ```
39
39
 
40
- > 提示:pnpm 会忽略发布不足 24 小时的包,因此发布首日请使用精确版本 `dsh-code@1.0.5`;24 小时后可省略版本号。npm 安装不受此限制。
40
+ > 提示:pnpm 会忽略发布不足 24 小时的包,因此发布首日请使用精确版本 `dsh-code@1.0.7`;24 小时后可省略版本号。npm 安装不受此限制。
41
41
  >
42
- > 版本对齐:dsh-code 1.0.5 面向 dsh `0.1.2-rc.1` 构建,全部 Harness 依赖均精确锁定为 `0.1.2-rc.1`。已安装的用户运行 `deepseek update --apply` 即可按同一条版本线一起升级全局宿主与 cli profile 中的插件,本地开发挂载(`link:`)不受影响。
42
+ > 版本对齐:dsh-code 面向 dsh `0.1.5-rc.2` 构建,全部 Harness 依赖均精确锁定为 `0.1.5-rc.2`。已安装的用户运行 `deepseek update --apply` 即可按同一条版本线一起升级全局宿主与 cli profile 中的插件,本地开发挂载(`link:`)不受影响。
43
43
  >
44
- > 升级说明:上游从本版本起将内置 `code` 预设更名为 `ptc`;旧会话与旧参数中记录的 `code` 会自动映射到 `ptc`,无需手动迁移。
44
+ > 升级说明:旧会话与旧参数中记录的 `code` 预设会自动映射到上游已改名的 `ptc`,无需手动迁移。会话日志读取端随上游升级到格式 v3:旧格式日志在读取时由内核自动迁移,磁盘上的原始文件保持不变。
45
45
 
46
46
  ### 2. 启动指令
47
47
 
@@ -68,7 +68,7 @@ DSH-Code 的重点是让 DSH 的 Agent、模型、工具和持久会话可以直
68
68
  - 使用 `/new` 新建会话,或通过 `/resume`、`--continue` 恢复已有会话
69
69
  - 使用 `/fork` 从历史节点创建新的工作分支,同时保留原会话
70
70
  - 按当前目录、更新时间和会话范围搜索历史记录
71
- - 使用 Up/Down 召回输入历史,或通过 `/history` 搜索过去的提示词
71
+ - 使用 Up/Down 召回输入历史(含 / 指令),或通过 `/history` 搜索过去的提示词与指令
72
72
  - 支持持久标题、Markdown 导出、上下文占用、token、缓存、TTFT 和耗时统计
73
73
  - 恢复会话时同步恢复该会话使用的 Agent Preset 和模型选择
74
74
 
@@ -79,7 +79,7 @@ DSH-Code 的重点是让 DSH 的 Agent、模型、工具和持久会话可以直
79
79
  ### 2. Agent、模型与扩展
80
80
 
81
81
  - 每个会话可以选择独立的 Agent Preset,用于组合工具、提示词、技能、上下文压缩、plan mode 和 subagent 能力
82
- - 使用 `/mode` 选择 `standard`、`code`、`minimal`、`cordis` 或用户自定义 Preset
82
+ - 使用 `/mode` 选择 `standard`、`ptc`、`minimal`、`cordis` 或用户自定义 Preset(旧名称 `code` 自动映射到 `ptc`)
83
83
  - 使用 `/model` 切换模型,管理 provider、API key、OAuth/设备码登录、endpoint、可用模型和上下文窗口;`Tab` 进入 provider 管理(已配置供应商置顶分组),任意阶段 `Ctrl+C` 直接退出整个 /model 流程
84
84
  - 在 provider 列表中,Enter 进入统一配置页:同页填写 API key 与 endpoint(留空即官方默认)、编辑已添加模型的上下文/输出窗口,`Tab` 进入发现页拉取端点真实可用模型并勾选添加
85
85
  - 在统一配置页的模型行上按 `e` 编辑推理档位声明(格式 `low:low high:high max:max`,`false` 禁用、留空恢复继承),按 `c` 从其他已声明模型逐字复制——例如 GLM 系列按官方三档写 `low:low high:high max:max`,新模型(如 gpt-6)可一键复制 gpt-5.6 的映射
@@ -111,7 +111,7 @@ DSH-Code 的重点是让 DSH 的 Agent、模型、工具和持久会话可以直
111
111
  ### 4. 编码工作流
112
112
 
113
113
  - 使用 `@` 引用工作区文件或已有会话;选择 PNG、JPEG、WebP、GIF 时会自动作为真实图片附件
114
- - 支持启动 prompt、多个 `--image` 参数,以及从终端拖入一张或多张图片
114
+ - 支持启动 prompt、多个 `--image` 参数,以及从终端拖入或粘贴附件:图片按图片附件发送,其他文件按原样文件附件发送(单文件 8 MiB、每条消息 8 个以内)
115
115
  - 使用 `/diff` 按文件检查改动,使用 `/review` 发起只读代码审查
116
116
  - 使用 `/copy` 复制最近一条完整回复,使用 Ctrl+O 查看完整历史和工具详情
117
117
  - 支持工具审批、结构化提问、plan review、多选和自定义答案
@@ -142,7 +142,7 @@ dsh --profile cli --session my-id # 使用指定 id 新建会话
142
142
  | `/delete [id\|前缀]` | 删除指定会话及其 subagent 会话 |
143
143
  | `/title <text>` | 修改当前会话标题 |
144
144
  | `/export [path]` | 将当前会话导出为 Markdown |
145
- | `/history` | 搜索并复用过去提交的提示词 |
145
+ | `/history` | 搜索并复用过去提交的提示词与 / 指令 |
146
146
  | `/clear` | 清空当前终端显示,不删除持久会话 |
147
147
 
148
148
  #### Agent、模型与权限
@@ -164,6 +164,7 @@ dsh --profile cli --session my-id # 使用指定 id 新建会话
164
164
  | `/todos` | 查看当前会话的完整 todo 列表 |
165
165
  | `/agents` | 查看当前会话创建的 subagent 会话 |
166
166
  | `/jobs` | 查看后台任务及其运行状态 |
167
+ | `/schedule` | 查看活动提醒(模型经 schedule 工具创建/取消,面板只读展示,逾期优先) |
167
168
  | `/copy` | 复制最近一条完整助手回复 |
168
169
 
169
170
  #### 扩展、显示与退出
@@ -171,9 +172,11 @@ dsh --profile cli --session my-id # 使用指定 id 新建会话
171
172
  | 命令 | 用途 |
172
173
  | --- | --- |
173
174
  | `/plugin [query]` | 查看已加载扩展及其状态 |
175
+ | `/update` | 一键对齐升级:按目标版本的 peers 锁定宿主 Harness 线,同步升级 dsh-code、全局宿主与 profile 内伴生插件,升级前展示完整计划与拒绝条件,完成后提示重启生效 |
174
176
  | `/statusline` | 选择状态栏显示的项目 |
175
177
  | `/vscode-keys` | 将 Ctrl+R 放行进 VS Code 系终端(幂等写入用户级 keybindings.json) |
176
178
  | `/theme` | 切换终端配色主题 |
179
+ | `/animation` | 开关计时动画(shimmer/追逐/闪烁/切换波浪),`/animation [on\|off]` |
177
180
  | `/help` | 查看快捷键、内置命令、Harness 命令和用户技能 |
178
181
  | `/quit` | 退出 DSH-Code |
179
182
 
@@ -211,21 +214,70 @@ DSH-Code 读取 Harness 的实时注册表,不在本地维护另一套副本
211
214
 
212
215
  `/plugin` 提供当前 Cordis loader 状态的只读视图。
213
216
 
214
- ### 2. 会话级 Agent Preset
217
+ ### 2. 内置扩展与可选官方插件
218
+
219
+ 以下官方插件已随 DSH-Code 一起安装并在组合中默认启用:
220
+
221
+ - **会话检索**:模型获得 `session_search` / `session_event_search` / `session_trace` / `session_event_trace` / `session_event_read` 五个只读工具,可检索历史会话内容(首次搜索时才构建索引,按工作目录精确匹配授权;Node 22 上首次搜索会当场打印一次 `node:sqlite` 实验性警告,属正常现象)。
222
+ - **定时提醒**:`schedule` 提供跨重启的持久提醒(`schedule_create` / `schedule_list` / `schedule_delete` 工具创建),`/schedule` 面板只读展示、逾期条目置顶标红;`time-context` 为模型注入时钟读数(30 秒节流)。
223
+ - **提醒的时钟读数**:`time-context` 为模型注入当前时间(30 秒节流),「下午五点提醒我」这类表述因此可用。
224
+
225
+ 以下官方插件已安装但需按需启用(在用户层 `~/.dsh/profiles/cli/cordis.patch.yml` 追加行,或按说明安装):
226
+
227
+ - **MCP 服务器**(`@deepseek-ai/dsh-mcp-client`,每个服务器一行,工具注册为 `mcp__<server>__<tool>`):
228
+
229
+ ```yaml
230
+ - insert:
231
+ - id: mcp-memory
232
+ name: '@deepseek-ai/dsh-mcp-client'
233
+ config:
234
+ transport: stdio
235
+ serverName: memory
236
+ command: mcp-server-memory
237
+ ```
238
+
239
+ (HTTP 传输改用 `transport: streamable-http` + `url`;服务器不可达时安全降级为重连循环,不影响启动。)
240
+ - **Claude Code / Codex hooks 桥**(`@deepseek-ai/dsh-hooks-claude-code` / `-codex`):在拦截缝上运行既有 hooks 配置;hooks 文件缺失时静默不生效:
241
+
242
+ ```yaml
243
+ - insert:
244
+ - id: hooks-claude
245
+ name: '@deepseek-ai/dsh-hooks-claude-code'
246
+ config:
247
+ configPath: C:/Users/you/.claude/hooks.json
248
+ ```
249
+ - **LSP 导航**:组合里预置了 `lsp` / `lsp-stdio` / `tool-lsp` 三行(禁用状态——语言服务器二进制在挂载期解析,缺失会让整个组合启动失败)。在用户层将三行 `disabled: false` 并为 `lsp-stdio` 配置 `servers`(扩展名到语言再到服务器命令),模型即获得 `lsp` 工具(goToDefinition / findReferences / goToImplementation / hover)。
250
+ - **持久终端**:PTY 服务与平台后端(Windows 走 pwsh 方言、POSIX 走 bash)已默认挂载,但六个模型工具 `terminal_open` / `terminal_send` / `terminal_read` / `terminal_signal` / `terminal_close` / `terminal_list` 出厂禁用——启用等于向所有会话放开 shell 能力,与 preset 把关原则一致,由部署显式决定。在用户层开启:
251
+ ```yaml
252
+ - id: tool-terminal
253
+ disabled: false
254
+ ```
255
+ (后台发送会出现在 /jobs 面板。)
256
+ - **tmux 面板上下文**(`@deepseek-ai/dsh-tmux-context`):在 tmux 内运行时把当前面板内容注入模型上下文(`config: { refreshIntervalMs: 60000 }`);不在 tmux 内时逐步为无害空操作:
257
+ ```yaml
258
+ - insert:
259
+ - id: tmux-context
260
+ name: '@deepseek-ai/dsh-tmux-context'
261
+ config:
262
+ refreshIntervalMs: 60000
263
+ ```
264
+ - **外部 CLI 委托**:`dsh plugin --profile cli add @deepseek-ai/dsh-subagent-claude-code`(或 `-codex`)安装休眠 provider,再按上游契约复制预设并启用 `tool-subagent-claude-code` / `tool-subagent-codex` 行,模型即可把任务委托给 claude / codex CLI。
265
+
266
+ ### 3. 会话级 Agent Preset
215
267
 
216
268
  Host 持有共享基础设施——注册表、持久化、会话查询、权限和 sandbox 策略;每个会话则获得一个隔离的 Agent scope,并由 **Agent Preset** 进行组合:
217
269
 
218
270
  - `standard`——功能完整的通用编码 Agent
219
- - `code`——面向 Code Mode / PTC 的多操作工作流
220
- - `minimal`——只保留持久 shell 和 `str_replace_editor`
271
+ - `ptc`——面向 PTC(原 Code Mode)的多操作工作流;旧名称 `code` 仍可使用
272
+ - `minimal`——仅保留持久 shell 的单工具精简组合
221
273
  - `cordis`——完整 Agent,加上运行时检查与 Preset 编写指导
222
274
  - 用户预设——自行定义工具、提示词段落、技能、上下文压缩、plan mode 与 subagent 行为
223
275
 
224
276
  在第一次 turn 之前使用 `/mode`,或通过 `--mode <preset>` 直接启动。选中的 preset 会写入会话,并在恢复时还原。
225
277
 
226
- ### 3. 会话记录与恢复
278
+ ### 4. 会话记录与恢复
227
279
 
228
- 提示词、流式 chunk、工具调用与结果、模型选择、plan 状态、权限、标题和 preset 选择都由持久 Session 事件投影得到。会话恢复、导出、历史检查、上下文统计和终端重放使用同一份记录。
280
+ 提示词、工具调用与结果、模型选择、plan 状态、权限、标题和 preset 选择都由持久 Session 事件投影得到;会话恢复、导出、历史检查、上下文统计和终端重放使用同一份记录。实时流式文本经进程内流帧呈现,落盘日志只保留装配完成的回复(内嵌计时流),两者在重放时得到同一视图。
229
281
 
230
282
  React state 只保存输入草稿、光标、当前面板、选中项和滚动位置等临时界面状态。
231
283
 
package/bin/deepseek.mjs CHANGED
@@ -8,7 +8,8 @@ import { dirname, join, resolve } from 'node:path'
8
8
  import { fileURLToPath, pathToFileURL } from 'node:url'
9
9
 
10
10
  const packageRequire = createRequire(import.meta.url)
11
- const packageVersion = packageRequire('../package.json').version
11
+ /** This launcher's release version (exported for probes and tests). */
12
+ export const packageVersion = packageRequire('../package.json').version
12
13
 
13
14
  /** Arguments required to boot DSH-Code's conventional profile. */
14
15
  export const profileArgs = (args = []) => ['--profile', 'cli', ...args]
@@ -73,6 +74,37 @@ export function profileMountedVersion(profileDir = cliProfileDir(), fileExists =
73
74
  }
74
75
  }
75
76
 
77
+ /**
78
+ * Profile-installed harness plugins that a line upgrade must carry along:
79
+ * every `@deepseek-ai/dsh-*` dependency except the dsh-code target itself
80
+ * and local checkouts (`link:`/`file:` stay untouched). Users install these
81
+ * through `dsh plugin add` once; without this pass `update --apply` would
82
+ * move the host and dsh-code forward and silently leave every other plugin
83
+ * pinned to the old line.
84
+ * @param profileDir - the cli profile root.
85
+ * @returns dependency entries as `{ name, spec }` in manifest order.
86
+ */
87
+ export function profilePluginDependencies(profileDir = cliProfileDir(), fileExists = existsSync, readFile = readFileSync) {
88
+ const manifest = join(profileDir, 'package.json')
89
+ if (!fileExists(manifest)) return []
90
+ let dependencies
91
+ try {
92
+ dependencies = JSON.parse(readFile(manifest, 'utf8'))?.dependencies
93
+ } catch {
94
+ return []
95
+ }
96
+ if (typeof dependencies !== 'object' || dependencies === null) return []
97
+ const plugins = []
98
+ for (const [name, spec] of Object.entries(dependencies)) {
99
+ if (typeof spec !== 'string') continue
100
+ if (!name.startsWith('@deepseek-ai/dsh-')) continue
101
+ if (name === 'dsh-code' || name === '@deepseek-ai/dsh-base') continue
102
+ if (/^(link|file):/iu.test(spec)) continue
103
+ plugins.push({ name, spec })
104
+ }
105
+ return plugins
106
+ }
107
+
76
108
  /** The harness release line a dsh-code release expects, read from its peers. */
77
109
  export function harnessLineFromPeers(peers) {
78
110
  if (typeof peers !== 'object' || peers === null) return undefined
@@ -84,22 +116,91 @@ export function harnessLineFromPeers(peers) {
84
116
  return undefined
85
117
  }
86
118
 
119
+ /**
120
+ * Refusal reason when a local-checkout profile would pair an older local
121
+ * build with the newer global host this upgrade installs, or undefined when
122
+ * the upgrade may proceed (no checkout mounted, or the checkout already
123
+ * matches the release). The caller prints the lines verbatim.
124
+ */
125
+ export function localCheckoutRefusal(mounted, latestCode, codeSpec) {
126
+ if (mounted === undefined || mounted === latestCode) return undefined
127
+ return [
128
+ `dsh-code: the cli profile mounts a local checkout of dsh-code ${mounted}, while this upgrade would install ${latestCode} globally`,
129
+ 'dsh-code: upgrading the host beside an older local checkout pairs incompatible code; refusing to install',
130
+ 'dsh-code: update the checkout first (git pull, pnpm install, pnpm build), or switch the profile to the published package:',
131
+ `dsh-code: dsh plugin --profile cli remove dsh-code && dsh plugin --profile cli add ${codeSpec}`,
132
+ ]
133
+ }
134
+
87
135
  /**
88
136
  * Decide what `update --apply` installs. The global DSH launcher is pinned
89
137
  * to the harness line the target dsh-code release declares in its peers,
90
138
  * so the launcher can never move ahead of the plugin it must boot. A
91
139
  * profile that mounts a local checkout (link:/file:) keeps its mount.
92
140
  */
93
- export function updatePlan({ latestCode, peers, profileSpec }) {
141
+ export function updatePlan({ latestCode, peers, profileSpec, profilePlugins = [] }) {
94
142
  const line = harnessLineFromPeers(peers)
143
+ const profileStep = !(typeof profileSpec === 'string' && /^(link|file):/iu.test(profileSpec))
95
144
  return {
96
145
  dshSpec: line === undefined ? '@deepseek-ai/dsh@latest' : `@deepseek-ai/dsh@${line}`,
146
+ line: line === undefined ? undefined : line,
97
147
  lineLocked: line !== undefined,
98
148
  codeSpec: `dsh-code@${latestCode}`,
99
- profileStep: !(typeof profileSpec === 'string' && /^(link|file):/iu.test(profileSpec)),
149
+ profileStep,
150
+ // Companion plugins the profile installed through `dsh plugin add`:
151
+ // carried to the same line so one command moves the whole profile.
152
+ pluginSpecs: line === undefined || !profileStep ? [] : profilePlugins
153
+ .filter(plugin => plugin.spec !== line)
154
+ .map(plugin => `${plugin.name}@${line}`),
100
155
  }
101
156
  }
102
157
 
158
+ /**
159
+ * Compare two @deepseek-ai/dsh release-line versions (`0.1.x`, `0.1.x-rc.N`,
160
+ * `0.1.x-alpha.N`). Positive when `left` is newer, negative when older, and 0
161
+ * on equality or an unparseable value (an unreadable version never blocks an
162
+ * update — release semantics follow semver prerelease ordering: final > rc >
163
+ * beta > alpha).
164
+ */
165
+ export function compareHarnessLines(left, right) {
166
+ const parse = (value) => {
167
+ const match = /^(\d+)\.(\d+)\.(\d+)(?:-(alpha|beta|rc)\.(\d+))?$/.exec(String(value))
168
+ if (match === null) return undefined
169
+ return {
170
+ core: [Number(match[1]), Number(match[2]), Number(match[3])],
171
+ tag: match[4] === undefined ? 3 : { alpha: 0, beta: 1, rc: 2 }[match[4]],
172
+ num: match[5] === undefined ? 0 : Number(match[5]),
173
+ }
174
+ }
175
+ const a = parse(left)
176
+ const b = parse(right)
177
+ if (a === undefined || b === undefined) return 0
178
+ for (let index = 0; index < 3; index += 1) {
179
+ if (a.core[index] !== b.core[index]) return a.core[index] - b.core[index]
180
+ }
181
+ if (a.tag !== b.tag) return a.tag - b.tag
182
+ return a.num - b.num
183
+ }
184
+
185
+ /**
186
+ * The globally installed @deepseek-ai/dsh version, when one exists across the
187
+ * npm global roots this launcher may boot from. An update whose plan targets a
188
+ * line OLDER than this would silently downgrade the booted host.
189
+ */
190
+ export function installedGlobalDshVersion(roots = globalDshRoots(), fileExists = existsSync, readFile = readFileSync) {
191
+ for (const root of roots) {
192
+ const manifest = join(root, 'node_modules', '@deepseek-ai', 'dsh', 'package.json')
193
+ if (!fileExists(manifest)) continue
194
+ try {
195
+ const version = JSON.parse(readFile(manifest, 'utf8'))?.version
196
+ if (typeof version === 'string' && version !== '') return version
197
+ } catch {
198
+ // Unreadable manifest: try the next root.
199
+ }
200
+ }
201
+ return undefined
202
+ }
203
+
103
204
  /** Run wrapper-owned child steps in order, stopping at the first failure. */
104
205
  export function runSequence(steps, spawnProcess = spawn) {
105
206
  return new Promise(resolve => {
@@ -300,6 +401,57 @@ function printUpdateStatus() {
300
401
  console.log('Run `deepseek update --apply` to upgrade the global packages and the cli profile together.')
301
402
  }
302
403
 
404
+ /**
405
+ * The structured form of the update status for machine consumers
406
+ * (`update --json`, consumed by the TUI /update panel): what is running,
407
+ * what npm serves, the exact steps `update --apply` would run, and every
408
+ * refusal that would stop it. One JSON object on stdout; `null` marks an
409
+ * unreadable value so the payload shape stays constant.
410
+ */
411
+ export function buildUpdateStatus({
412
+ view = viewJson,
413
+ installedDsh = installedDshVersion,
414
+ readSpec = profileDependencySpec,
415
+ readMounted = profileMountedVersion,
416
+ readPlugins = profilePluginDependencies,
417
+ } = {}) {
418
+ const codeLatest = view(['dsh-code', 'version'])
419
+ const installed = installedDsh()
420
+ const spec = readSpec()
421
+ const mounted = readMounted()
422
+ const peers = codeLatest === undefined ? undefined : view([`dsh-code@${codeLatest}`, 'peerDependencies'])
423
+ const plan = updatePlan({
424
+ latestCode: codeLatest ?? 'unknown',
425
+ peers,
426
+ profileSpec: spec,
427
+ profilePlugins: readPlugins(),
428
+ })
429
+ const registry = codeLatest === undefined ? 'could not read the latest dsh-code version from npm' : undefined
430
+ // The same refusals applyUpdate enforces, precomputed so a caller can
431
+ // show them BEFORE any confirmation instead of failing mid-flight.
432
+ const downgrade = plan.line !== undefined && installed !== undefined && compareHarnessLines(plan.line, installed) < 0
433
+ const localCheckout = plan.profileStep ? undefined : localCheckoutRefusal(mounted, codeLatest, plan.codeSpec)
434
+ const hostOnLine = installed !== undefined && (plan.line === undefined || compareHarnessLines(installed, plan.line) === 0)
435
+ const upToDate = registry === undefined
436
+ && codeLatest === packageVersion
437
+ && !downgrade
438
+ && localCheckout === undefined
439
+ && hostOnLine
440
+ && plan.pluginSpecs.length === 0
441
+ && (mounted === undefined || mounted === codeLatest)
442
+ return {
443
+ code: { running: packageVersion, latest: codeLatest ?? null },
444
+ host: { installed: installed ?? null, targetLine: plan.line ?? null },
445
+ profile: { spec: spec ?? null, mounted: mounted ?? null, localCheckout: !plan.profileStep },
446
+ plan: { dshSpec: plan.dshSpec, codeSpec: plan.codeSpec, pluginSpecs: plan.pluginSpecs },
447
+ // JSON.stringify drops undefined members; every unreadable value must
448
+ // serialize as an explicit null so machine consumers never see a missing
449
+ // key drift into a truthy comparison.
450
+ blockers: { registry: registry ?? null, downgrade, localCheckout: localCheckout ?? null },
451
+ upToDate,
452
+ }
453
+ }
454
+
303
455
  /**
304
456
  * Upgrade the global launcher and the cli profile plugin together. The
305
457
  * global install is pinned to the harness line the new dsh-code release
@@ -319,10 +471,40 @@ async function applyUpdate({
319
471
  return
320
472
  }
321
473
  const peers = view([`dsh-code@${latestCode}`, 'peerDependencies'], undefined, npm)
322
- const plan = updatePlan({ latestCode, peers, profileSpec: profileDependencySpec() })
474
+ const plan = updatePlan({
475
+ latestCode,
476
+ peers,
477
+ profileSpec: profileDependencySpec(),
478
+ profilePlugins: profilePluginDependencies(),
479
+ })
323
480
  if (!plan.lineLocked) {
324
481
  console.log('dsh-code: could not read the compatible harness line; installing @deepseek-ai/dsh@latest')
325
482
  }
483
+ // Downgrade guard: the latest published dsh-code may still pin an older
484
+ // harness line than the host already installed globally (a release-ordering
485
+ // window). Installing would silently downgrade every existing session's
486
+ // booted host, and a link-mounted profile would end up on a mismatched pair
487
+ // with no warning — refuse instead and name the manual command.
488
+ const installed = installedGlobalDshVersion()
489
+ if (plan.line !== undefined && installed !== undefined && compareHarnessLines(plan.line, installed) < 0) {
490
+ console.error(`dsh-code: dsh-code@${latestCode} needs @deepseek-ai/dsh@${plan.line}, but ${installed} is already installed globally`)
491
+ console.error(`dsh-code: refusing to downgrade the host; to proceed anyway run: npm install -g @deepseek-ai/dsh@${plan.line} dsh-code@${latestCode}`)
492
+ process.exitCode = 1
493
+ return
494
+ }
495
+ // Local-checkout guard: a link/file-mounted profile runs the checkout's
496
+ // own build, and this command would upgrade the global host beside it.
497
+ // When the checkout is older than the release being installed, upgrading
498
+ // pairs a new host with old local code — the terminal breaks on the next
499
+ // launch. Refuse until the checkout is updated or unmounted.
500
+ if (!plan.profileStep) {
501
+ const refusal = localCheckoutRefusal(profileMountedVersion(), latestCode, plan.codeSpec)
502
+ if (refusal !== undefined) {
503
+ for (const line of refusal) console.error(line)
504
+ process.exitCode = 1
505
+ return
506
+ }
507
+ }
326
508
  const steps = [{ command: npm.command, args: [...npm.args, 'install', '-g', plan.dshSpec, plan.codeSpec], label: 'npm install' }]
327
509
  if (plan.profileStep) {
328
510
  const command = resolveCommand(['plugin', '--profile', 'cli', 'add', plan.codeSpec])
@@ -332,6 +514,20 @@ async function applyUpdate({
332
514
  return
333
515
  }
334
516
  steps.push({ command: command.command, args: command.args, label: 'dsh plugin add' })
517
+ // Companion plugins ride the same line: without this, `dsh plugin add`-ed
518
+ // extras stay pinned to the old harness line after an upgrade.
519
+ for (const pluginSpec of plan.pluginSpecs) {
520
+ const pluginCommand = resolveCommand(['plugin', '--profile', 'cli', 'add', pluginSpec])
521
+ if (pluginCommand === undefined) {
522
+ console.error(`dsh-code: could not resolve the dsh command for ${pluginSpec}`)
523
+ process.exitCode = 1
524
+ return
525
+ }
526
+ steps.push({ command: pluginCommand.command, args: pluginCommand.args, label: `dsh plugin add ${pluginSpec}` })
527
+ }
528
+ if (plan.pluginSpecs.length > 0) {
529
+ console.log(`carrying ${plan.pluginSpecs.length} profile plugin${plan.pluginSpecs.length === 1 ? '' : 's'} to the same line: ${plan.pluginSpecs.join(', ')}`)
530
+ }
335
531
  } else {
336
532
  console.log('cli profile mounts a local checkout; leaving the profile untouched')
337
533
  }
@@ -375,6 +571,10 @@ export function launchOperation(args = process.argv.slice(2)) {
375
571
  return launchChild(command.command, command.args)
376
572
  }
377
573
  if (operation === 'update') {
574
+ if (args.includes('--json')) {
575
+ console.log(JSON.stringify(buildUpdateStatus()))
576
+ return true
577
+ }
378
578
  if (args.includes('--apply')) {
379
579
  void applyUpdate()
380
580
  return true
package/cordis.patch.yml CHANGED
@@ -7,9 +7,12 @@
7
7
  # dispatches slash commands through the shared registry, and folds submitted
8
8
  # prompts back into the same durable session until the user quits.
9
9
 
10
+ # 0.1.5 split the deployment persona into a prefix and a suffix template
11
+ # around the first-party guidance sections; this terminal keeps its identity
12
+ # line as the prefix ({{variable}} interpolation stays strict upstream).
10
13
  - id: system-prompt
11
14
  config:
12
- persona: >-
15
+ personaPrefix: >-
13
16
  You are a coding agent powered by the {{model}} model. Your working directory is {{cwd}}.
14
17
 
15
18
  # The shared module-reload HMR row stays off; the launcher's watch-only
@@ -21,6 +24,17 @@
21
24
  config:
22
25
  mode: !!js process.env.DSH_TOOLS_MODE
23
26
 
27
+ # The base row keeps full-text search off entirely (openAt: never) so nothing
28
+ # imports node:sqlite at boot; the terminal replaces it with the bundle's
29
+ # skip-tolerant subclass below. An override row cannot retarget a row's
30
+ # MODULE (name is only honored on inserts), so the base row is disabled and
31
+ # an equivalent row mounts the subclass: the derived index opens lazily on
32
+ # the first search (no startup cost, no Node 22 experimental warning), and
33
+ # one unreadable source (a pre-release artifact the frozen codecs reject)
34
+ # is skipped with a warning instead of failing every cross-session search.
35
+ - id: session-query-sqlite
36
+ disabled: true
37
+
24
38
  # Agent-plane rows are mounted by the selected preset. Keep this list aligned
25
39
  # with the upstream Web bundle so two sessions can use different compositions
26
40
  # without leaking model-facing registrations through the host layer.
@@ -34,10 +48,14 @@
34
48
  disabled: true
35
49
  - id: tool-fs-search
36
50
  disabled: true
37
- - id: tool-str-replace-editor
38
- disabled: true
51
+ # skill-filesystem stays ENABLED on the host: it only feeds the skill
52
+ # REGISTRY (local-root discovery), never the model-facing tool surface —
53
+ # tool-skill below stays disabled and presets mount it per session. The
54
+ # terminal offers skills in the slash menu before any session exists, and a
55
+ # bare launch has no preset layer yet, so the registry's global layer needs
56
+ # this provider; presets shadow it with the same discovery on their scope.
39
57
  - id: skill-filesystem
40
- disabled: true
58
+ disabled: false
41
59
  - id: tool-skill
42
60
  disabled: true
43
61
  - id: command-goal
@@ -70,9 +88,11 @@
70
88
  disabled: true
71
89
  - id: tool-todo
72
90
  disabled: true
73
- # tool-web keeps the 0.1.2-rc.1 base default (web_fetch enabled over the
91
+ # tool-web keeps the 0.1.5-rc.1 base default (web_fetch enabled over the
74
92
  # bundled web-fetch-http provider): the host row exposes web_fetch while the
75
- # per-preset agent-plane rows keep their own fetch stance.
93
+ # per-preset agent-plane rows keep their own fetch stance. The upstream base
94
+ # bundle dropped tool-str-replace-editor entirely in 0.1.5 (the minimal
95
+ # preset became a single-tool composition), so this patch no longer lists it.
76
96
 
77
97
  - insert:
78
98
  # Code Mode is a core execution capability, not a Web component.
@@ -111,7 +131,8 @@
111
131
  # tool-subagent rows set `modelSelectionSettings: true`, whose start
112
132
  # resolves this service and fails the whole preset mount when absent
113
133
  # ("failed to apply loader entry delegation (cordis:group)"). The upstream
114
- # web-app bundle adds this host row in 0.1.2-rc.1; dsh-base does not ship it.
134
+ # web-app bundle carries this host row in 0.1.5-rc.1; dsh-base still does
135
+ # not ship it.
115
136
  - id: subagent-model-selection-settings
116
137
  name: '@deepseek-ai/dsh-tool-subagent/model-selection-settings'
117
138
 
@@ -122,6 +143,83 @@
122
143
  - id: authorization
123
144
  name: '@deepseek-ai/dsh-authorization'
124
145
 
146
+ # The bundle's skip-tolerant session-query engine replacing the disabled
147
+ # base row above: same SQLite FTS5 service, observation loop patched to
148
+ # skip unreadable sources instead of failing every search.
149
+ - id: session-query-skip
150
+ name: 'dsh-code/session-query'
151
+ config:
152
+ path: ':memory:'
153
+ openAt: first-search
154
+
155
+ # Model-facing session-log search/trace/read tools over the sessionQuery
156
+ # service (read-only, workspace-scoped by exact cwd). A host-plane mount
157
+ # is deployment-global: every preset session sees the five tools. Kept
158
+ # ENABLED deliberately — read-only search leaks no capability — but note
159
+ # it does reach minimal-preset sessions too.
160
+ - id: tool-session-query
161
+ name: '@deepseek-ai/dsh-tool-session-query'
162
+
163
+ # Durable reminders: the schedule service arms per-root timers, injects
164
+ # due prompts as plugin-source user messages, and writes schedule/change
165
+ # events the projection folds into the /schedule panel. time-context
166
+ # gives the model a clock reading so 'at' times are phrased against the
167
+ # actual time; the 30s throttle bounds durable snapshot growth.
168
+ - id: schedule
169
+ name: '@deepseek-ai/dsh-schedule'
170
+
171
+ - id: time-context
172
+ name: '@deepseek-ai/dsh-time-context'
173
+ config:
174
+ refreshIntervalMs: 30000
175
+
176
+ # Persistent PTY plumbing: the owner-scoped registry and the one backend
177
+ # package in both dialects (pwsh is the Windows/ConPTY path, bash covers
178
+ # POSIX) mount as host SERVICES — no model-facing surface, so they break
179
+ # no preset boundary. The six terminal_* TOOLS ship DISABLED like LSP:
180
+ # enabling them at the host plane would hand every preset (including the
181
+ # shell-restricted minimal one) raw shell capability, the exact leak the
182
+ # disables above exist to prevent. Flip tool-terminal to disabled: false
183
+ # in the PROFILE layer to opt the deployment into PTY tools.
184
+ - id: pty
185
+ name: '@deepseek-ai/dsh-terminal'
186
+
187
+ - id: terminal-pwsh
188
+ name: '@deepseek-ai/dsh-terminal-bash'
189
+ disabled: !!js process.platform !== 'win32'
190
+ config:
191
+ shellDialect: pwsh
192
+
193
+ - id: terminal-bash
194
+ name: '@deepseek-ai/dsh-terminal-bash'
195
+ disabled: !!js process.platform === 'win32'
196
+
197
+ - id: tool-terminal
198
+ name: '@deepseek-ai/dsh-tool-terminal'
199
+ disabled: true
200
+
201
+ # LSP navigation ships DISABLED: lsp-stdio resolves every server command
202
+ # at mount and a missing binary would fail the whole composition boot.
203
+ # Enable in the PROFILE layer with real servers, e.g.:
204
+ # lsp / lsp-stdio / tool-lsp rows with disabled: false and
205
+ # lsp-stdio config.servers mapping extensions to server commands.
206
+ - id: lsp
207
+ name: '@deepseek-ai/dsh-lsp'
208
+ disabled: true
209
+
210
+ - id: lsp-stdio
211
+ name: '@deepseek-ai/dsh-lsp-stdio'
212
+ disabled: true
213
+
214
+ - id: tool-lsp
215
+ name: '@deepseek-ai/dsh-tool-lsp'
216
+ disabled: true
217
+
218
+ # MCP servers (dsh-mcp-client) and hooks bridges (dsh-hooks-claude-code /
219
+ # -codex) take per-instance config (server transports, hooks.json paths),
220
+ # so they mount as rows in the USER profile layer, not here; the packages
221
+ # ship as peers so those rows resolve. README documents the row shapes.
222
+
125
223
  - id: tui-startup
126
224
  name: 'dsh-code/startup'
127
225