@ljwei-stak/dsh-model-router 0.13.3 → 0.14.0-beta.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.
@@ -6,6 +6,13 @@
6
6
  * accepts a command, package name, URL, argument vector, or executable path.
7
7
  */
8
8
  import { getOfficialTool } from './official-tool-registry.mjs'
9
+ import {
10
+ parseTerminalRead,
11
+ parseTerminalResize,
12
+ parseTerminalStart,
13
+ parseTerminalStop,
14
+ parseTerminalWrite,
15
+ } from './cli-terminal-protocol.mjs'
9
16
 
10
17
  export const OFFICIAL_TOOLS_REMOTE_PACKAGE = '@ljwei-stak/dsh-model-router'
11
18
  export const OFFICIAL_TOOLS_REMOTE_NAMESPACE = 'modelRouterOfficialTools'
@@ -123,6 +130,12 @@ export function parseRunRequest(value) {
123
130
  }
124
131
  }
125
132
 
133
+ const terminalStartCodec = strictCodec(`${OFFICIAL_TOOLS_REMOTE_PACKAGE}#TerminalStart`, parseTerminalStart)
134
+ const terminalReadCodec = strictCodec(`${OFFICIAL_TOOLS_REMOTE_PACKAGE}#TerminalRead`, parseTerminalRead)
135
+ const terminalWriteCodec = strictCodec(`${OFFICIAL_TOOLS_REMOTE_PACKAGE}#TerminalWrite`, parseTerminalWrite)
136
+ const terminalResizeCodec = strictCodec(`${OFFICIAL_TOOLS_REMOTE_PACKAGE}#TerminalResize`, parseTerminalResize)
137
+ const terminalStopCodec = strictCodec(`${OFFICIAL_TOOLS_REMOTE_PACKAGE}#TerminalStop`, parseTerminalStop)
138
+
126
139
  const runRequestCodec = strictCodec(`${OFFICIAL_TOOLS_REMOTE_PACKAGE}#RunRequest`, parseRunRequest)
127
140
 
128
141
  function descriptor(method, parameters, result) {
@@ -157,6 +170,14 @@ export const OFFICIAL_TOOLS_REMOTE_DESCRIPTORS = Object.freeze([
157
170
  // Workbench "start a run": plan preview with cost estimate and confirmation reasons, then execute.
158
171
  descriptor('previewRun', [jsonParameter('request', runRequestCodec)], anyObjectCodec('RunPreview')),
159
172
  descriptor('startRun', [jsonParameter('request', runRequestCodec)], anyObjectCodec('RunStarted')),
173
+ // Workbench "官方工具终端": interactive shell / fixed official CLI sessions.
174
+ // Output streams through long-poll reads; input and resize are unary calls.
175
+ descriptor('terminalInfo', [], anyObjectCodec('TerminalInfo')),
176
+ descriptor('terminalStart', [jsonParameter('request', terminalStartCodec)], anyObjectCodec('TerminalStarted')),
177
+ descriptor('terminalRead', [jsonParameter('request', terminalReadCodec)], anyObjectCodec('TerminalOutput')),
178
+ descriptor('terminalWrite', [jsonParameter('request', terminalWriteCodec)], anyObjectCodec('TerminalWritten')),
179
+ descriptor('terminalResize', [jsonParameter('request', terminalResizeCodec)], anyObjectCodec('TerminalResized')),
180
+ descriptor('terminalStop', [jsonParameter('request', terminalStopCodec)], anyObjectCodec('TerminalStopped')),
160
181
  ])
161
182
 
162
183
  export const OFFICIAL_TOOLS_CLIENT_REMOTE = Object.freeze({
@@ -32,11 +32,13 @@ export function defaultStatePath(env = process.env) {
32
32
  }
33
33
 
34
34
  export const MAX_NOTICES = 10
35
+ /** Interactive terminal sessions kept as metadata only (no input or output). */
36
+ export const MAX_TERMINAL_SESSIONS = 50
35
37
  const LOCK_STALE_MS = 15_000
36
38
  const LOCK_TIMEOUT_MS = 10_000
37
39
 
38
40
  function emptyState() {
39
- return { version: STATE_VERSION, onboarding: { completedAt: null }, health: null, quota: {}, authFailures: {}, runs: [], notices: [] }
41
+ return { version: STATE_VERSION, onboarding: { completedAt: null }, health: null, quota: {}, authFailures: {}, runs: [], notices: [], terminalSessions: [] }
40
42
  }
41
43
 
42
44
  function sanitizeNotices(value) {
@@ -56,6 +58,25 @@ function sanitize(value) {
56
58
  .filter(([, entry]) => Number.isFinite(entry?.at))),
57
59
  runs: Array.isArray(value.runs) ? value.runs.filter(run => run && typeof run.id === 'string').slice(-MAX_RUNS) : [],
58
60
  notices: sanitizeNotices(value.notices),
61
+ terminalSessions: sanitizeTerminalSessions(value.terminalSessions),
62
+ }
63
+ }
64
+
65
+ function sanitizeTerminalSessions(value) {
66
+ return Array.isArray(value)
67
+ ? value.filter(item => item && typeof item.id === 'string' && typeof item.target === 'string' && Number.isFinite(item.startedAt)).slice(-MAX_TERMINAL_SESSIONS)
68
+ : []
69
+ }
70
+
71
+ /** Session metadata only: never input, output or environment. */
72
+ export function terminalSessionRecord(record) {
73
+ const finite = value => Number.isFinite(value) ? value : null
74
+ return {
75
+ id: String(record.id), target: String(record.target), label: String(record.label ?? record.target).slice(0, 80),
76
+ mode: record.mode === 'login' ? 'login' : 'interactive', cwd: String(record.cwd ?? '').slice(0, 4_096),
77
+ backend: record.backend === 'pipe' ? 'pipe' : 'pty', startedAt: finite(record.startedAt), finishedAt: finite(record.finishedAt),
78
+ durationMs: finite(record.durationMs), exitCode: Number.isInteger(record.exitCode) ? record.exitCode : null,
79
+ endReason: ['exit', 'stopped', 'orphan', 'lifetime', 'dispose'].includes(record.endReason) ? record.endReason : 'exit',
59
80
  }
60
81
  }
61
82
 
@@ -152,6 +173,7 @@ export function createRouterState({ file = defaultStatePath(), maxRuns = MAX_RUN
152
173
  const result = await change(state)
153
174
  state.runs = state.runs.slice(-maxRuns)
154
175
  state.notices = sanitizeNotices(state.notices)
176
+ state.terminalSessions = sanitizeTerminalSessions(state.terminalSessions)
155
177
  await persist(state)
156
178
  return result
157
179
  } finally {
@@ -194,6 +216,11 @@ export function createRouterState({ file = defaultStatePath(), maxRuns = MAX_RUN
194
216
  else delete current.authFailures[toolId]
195
217
  return current.authFailures
196
218
  }),
219
+ appendTerminalSession: record => update(current => {
220
+ const entry = terminalSessionRecord(record)
221
+ current.terminalSessions = [...(current.terminalSessions ?? []), entry]
222
+ return entry
223
+ }),
197
224
  appendRun: run => update(current => { current.runs.push(run); return run }),
198
225
  updateRun: (id, change) => update(current => {
199
226
  const run = current.runs.find(item => item.id === id)
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.14.0-beta.2 — 2026-10-03
4
+
5
+ Prerelease under the npm `next` tag only (`latest` stays 0.13.3).
6
+
7
+ **After installing or updating, quit Harness completely (including the tray icon) and start it again.** Harness serves the new workbench UI immediately, but the running Host keeps the plugin code it already imported, so new Host methods are missing until a full restart.
8
+
9
+ ### Fixed
10
+ - With 0.14.0-beta.1 installed into a running Harness, the 官方工具终端 card showed `transport failure for /api/modelRouterOfficialTools/terminalInfo: HTTP 404`. The Harness gateway only routes `/api/<namespace>/<method>` for descriptors the Host registered when it applied the plugin; the Host was still running the previous version's code. Nothing in the plugin's routes was wrong. The card now shows "插件后台版本较旧,请完全退出并重启 Harness(包括托盘图标)后再使用。" for a missing method and disables 开始.
11
+
12
+ ### Added
13
+ - `list()` reports `hostVersion`, the plugin version the Host actually loaded; the client embeds its own version at build time. When they differ (or the Host is too old to report one), the workbench shows the same restart advice at the top.
14
+
15
+ ## 0.14.0-beta.1 — 2026-10-03
16
+
17
+ Prerelease under the npm `next` tag (branch `feat/cli-terminal`).
18
+
19
+ ### Added
20
+ - **官方工具终端** workbench card: interactive sessions of the system shell (PowerShell on Windows) or an installed official CLI (codex, claude, kimi, mcode, mimo, grok, gemini), in tabs, rendered with xterm.js 6 (bundled into `client.js`). Each CLI also has a fixed **登录** mode (`codex login`, `claude auth login`, …). Sessions start only after a confirmation showing the command, the absolute working directory, and that the terminal runs outside the Harness process sandbox with the user's own CLI login and environment.
21
+ - Host session manager (`shared/cli-terminal.mjs`) on the prebuilt N-API `@lydell/node-pty@1.2.0-beta.15` (new optional dependency, per-platform binaries, no install scripts), with a pipe fallback and a clear limitation note when the PTY cannot load. Verified on Windows inside Harness Desktop (Electron 44 as Node, pnpm 11 install).
22
+ - Typert remote methods `terminalInfo`, `terminalStart`, `terminalRead` (long poll), `terminalWrite`, `terminalResize`, `terminalStop`; every request is validated on both sides (`shared/cli-terminal-protocol.mjs`).
23
+ - Session metadata (tool, mode, directory, start/end, duration, exit code, end reason) in `state.json` → `terminalSessions` and in the card's history. Input and output are never logged or stored.
24
+ - Sessions end on tab stop, panel close, plugin unload/reload, Host exit, after about 2 minutes without a reader, or after 6 hours; at most 4 at once.
25
+
3
26
  ## 0.13.3 — 2026-10-03
4
27
 
5
28
  Bug-fix release for DeepSeek Harness Desktop 0.2.0-rc.1 / 0.2.0-rc.2, published under the npm `next` and `latest` tags.
package/README.md CHANGED
@@ -402,6 +402,19 @@ Claude Code, Codex, MiMo Code, and Grok Build support read-only and approved edi
402
402
 
403
403
  The planned Harness model ID is not necessarily the vendor CLI's model name. A saved `cliModel`, or temporary `cliModelsJson` keyed by tool or work-package ID, can provide a known vendor CLI name. A package-specific temporary mapping takes priority over a tool mapping, then the saved profile. ZCode uses its configured default model. Most vendor CLIs do not provide a verifiable actual model ID in their results, so inspect vendor run records to confirm which model and price applied. The plugin's sequential CLI team runner is separate from Harness's built-in Agent Teams lifecycle.
404
404
 
405
+ ### Interactive terminal (官方工具终端, 0.14.0-beta.2, npm `next`)
406
+
407
+ The workbench has an **官方工具终端** card below the tool health check. It runs your system shell (PowerShell on Windows, your login `$SHELL` elsewhere) or one installed official CLI (`codex`, `claude`, `kimi`, `mcode`, `mimo`, `grok`, `gemini`) interactively, in tabs, with live output: multi-turn conversations, full-screen TUIs, and logins such as `codex login`, `claude auth login`, `kimi login`, `mcode login`, `mimo auth login`, `grok login` (choose **登录**).
408
+
409
+ - **Boundary.** The terminal is your own terminal: it does **not** go through the Harness process sandbox, uses your own CLI logins and your full login environment (PATH, proxies, API keys), and can read and write anything your account can. Every session starts only after a confirmation that shows the command, the absolute working directory, and this boundary. The `confirmUnsandboxedCli` setting for routed runs is unchanged.
410
+ - **Fixed launch targets.** The client can only name the shell or a registry CLI (plus its fixed login subcommand); it never supplies an executable, arguments, or environment. On Windows, native `.exe` files start directly, npm `.cmd` shims through `cmd.exe /d /s /c`, `.ps1`-only installs through `powershell -File`.
411
+ - **Backend.** A pseudo terminal from the prebuilt, N-API [`@lydell/node-pty`](https://www.npmjs.com/package/@lydell/node-pty) (an optional dependency with per-platform binaries; no build tools or install scripts). It loads in the Harness Desktop Host (Electron 44 running as Node). If it cannot load, sessions fall back to pipes: no TTY, so full-screen TUIs may not render or start, resizing does nothing, arrow keys may not work, and the Host echoes input. Line-based logins and PowerShell work. The card shows which backend is active.
412
+ - **Transport.** Output streams through long-poll reads over the plugin's existing Typert remote (`terminalRead`); keystrokes (`terminalWrite`) and resizes (`terminalResize`) are separate calls.
413
+ - **Lifetime.** Ending the tab, closing the workbench panel, unloading or reloading the plugin, or quitting Harness kills the process. A session that nobody reads for about 2 minutes is killed, and no session runs longer than 6 hours. At most 4 sessions run at once.
414
+ - **Privacy.** Input and output are never logged or saved. The card's history (and `state.json` → `terminalSessions`) stores only the tool, mode, working directory, start/end time, duration, exit code, and end reason.
415
+ - **Restart after installing.** Harness serves the new card immediately, but a running Host keeps the old plugin code; until you quit Harness completely (including the tray icon) and start it again, the card shows “插件后台版本较旧,请完全退出并重启 Harness”.
416
+ - **Keys.** With text selected, Ctrl+C copies; otherwise it interrupts. Ctrl+Shift+V pastes. Drag the terminal's lower-right corner to change its height.
417
+
405
418
  ## Optional GAL installation
406
419
 
407
420
  [DeepSeek Harness GAL](https://github.com/Alice-Marx/deepseek-harness-galgame) owns the story player, artwork, free mode, music, preferences, and saves. Install it separately with `@ljwei-stak/dsh-galgame@0.1.0`; its README contains the player tutorial. It does not require the router, and the router does not require its images or story engines.
package/README.zh.md CHANGED
@@ -391,6 +391,19 @@ Claude 与 Codex 若本机已有经核验的签名入口,仍优先走原有沙
391
391
 
392
392
  Harness 目录中的模型 ID 未必是厂商 CLI 接受的名字。逐模型设置可填写 `cliModel`;团队执行时临时的“工作包映射 > 工具映射 > 保存映射”。ZCode 3.14.3 不能逐次切换模型。多数 CLI 不回报可核验的实际模型 ID,执行后要对照厂商运行记录、权限和账单。Windows Harness 沙箱的 ACL 文件效果报告为部分隔离,涉及敏感仓库时应先用测试环境验证。
393
393
 
394
+ ### 官方工具终端(0.14.0-beta.2,npm `next`)
395
+
396
+ 工作台“官方工具 · 体检”下方新增 **官方工具终端** 卡片:以标签页形式交互运行系统终端(Windows 为 PowerShell,其他系统为登录 `$SHELL`)或已安装的官方 CLI(`codex`、`claude`、`kimi`、`mcode`、`mimo`、`grok`、`gemini`),实时输出,可多轮对话、显示全屏界面,也可选 **登录** 直接运行 `codex login`、`claude auth login`、`kimi login`、`mcode login`、`mimo auth login`、`grok login`。
397
+
398
+ - **边界**:它就是你自己的终端,**不经过 Harness 进程沙箱**,使用你自己的 CLI 登录状态和完整登录环境变量(PATH、代理、API Key),能读写你账号可访问的任何文件。每次启动前都会确认,列出命令、工作目录(绝对路径)和上述边界。路由执行的 `confirmUnsandboxedCli` 设置不变。
399
+ - **固定启动目标**:客户端只能选择系统终端或注册表中的 CLI(以及其固定登录子命令),不能指定可执行文件、参数或环境变量。Windows 上原生 `.exe` 直接启动,npm 的 `.cmd` 包装经 `cmd.exe /d /s /c` 启动,只有 `.ps1` 时经 `powershell -File` 启动。
400
+ - **终端组件**:使用预编译、N-API 的 [`@lydell/node-pty`](https://www.npmjs.com/package/@lydell/node-pty) 伪终端(可选依赖,按平台下载预编译二进制,不需要编译工具,也没有安装脚本),已验证可在 Harness Desktop 宿主(Electron 44 以 Node 模式运行)中加载。无法加载时自动改用管道模式:程序看不到真实终端,全屏界面可能无法显示或拒绝启动,窗口大小不同步,方向键可能无效,输入由宿主回显;按行交互的登录命令和 PowerShell 可用。卡片会显示当前使用的模式。
401
+ - **传输**:输出通过插件现有 Typert 远程调用的长轮询读取(`terminalRead`)实时推送;按键(`terminalWrite`)和窗口大小(`terminalResize`)分别发送。
402
+ - **生命周期**:结束标签、关闭工作台面板、卸载或重新加载插件、退出 Harness 都会结束进程;约 2 分钟无人读取输出的会话会被结束;单个会话最长 6 小时;最多同时 4 个。
403
+ - **隐私**:输入和输出不会写入日志或保存。卡片中的历史(`state.json` 的 `terminalSessions`)只记录工具、方式、工作目录、开始/结束时间、时长、退出码和结束原因。
404
+ - **安装后需重启**:Harness 会立即加载新界面,但运行中的后台仍是旧插件代码;完全退出 Harness(包括托盘图标)并重新启动前,卡片会提示“插件后台版本较旧,请完全退出并重启 Harness”。
405
+ - **按键**:选中文字时 Ctrl+C 复制,否则发送中断;Ctrl+Shift+V 粘贴;拖动终端右下角调整高度。
406
+
394
407
  ## 可选安装 GAL
395
408
 
396
409
  [DeepSeek Harness GAL](https://github.com/Alice-Marx/deepseek-harness-galgame)单独维护剧情引擎、立绘与场景、音乐、阅读设置、存档和自由模式。安装输入为 `@ljwei-stak/dsh-galgame@0.1.0`,详细操作见该仓库 README。模型路由安装包不再包含这些美术或剧情引擎,后续路由与 GAL 各自更新版本。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ljwei-stak/dsh-model-router",
3
- "version": "0.13.3",
3
+ "version": "0.14.0-beta.2",
4
4
  "type": "module",
5
5
  "description": "Model Router for official DeepSeek Harness Desktop: cost-aware task routing, dependent work packages and official vendor CLI discovery, installation and execution. GAL is available separately as @ljwei-stak/dsh-galgame.",
6
6
  "repository": {
@@ -61,6 +61,8 @@
61
61
  ".dsh-plugin/shared/routing-presets.mjs",
62
62
  ".dsh-plugin/shared/subscription-billing.mjs",
63
63
  ".dsh-plugin/shared/security-boundaries.mjs",
64
+ ".dsh-plugin/shared/cli-terminal.mjs",
65
+ ".dsh-plugin/shared/cli-terminal-protocol.mjs",
64
66
  "cordis.patch.yml",
65
67
  "README.md",
66
68
  "README.zh.md",
@@ -77,7 +79,7 @@
77
79
  "scripts": {
78
80
  "build:client": "node scripts/build-client.mjs",
79
81
  "check:client": "node scripts/build-client.mjs --check",
80
- "test": "node --test tests/router.test.mjs tests/harness-*.test.mjs tests/official-tools.test.mjs tests/official-team-runtime.test.mjs tests/model-profiles.test.mjs tests/model-profile-editor.test.mjs tests/tool-install-state.test.mjs tests/vendor-adapters.test.mjs tests/desktop-compat.test.mjs tests/plugin-split.test.mjs tests/task-executors.test.mjs tests/roadmap.test.mjs tests/insights-state.test.mjs tests/billing-team-ledger.test.mjs tests/subscription-billing.test.mjs tests/subscription-host.test.mjs tests/test-pass-regressions.test.mjs tests/roadmap-gaps.test.mjs tests/cordis-inject.test.mjs",
82
+ "test": "node --test tests/router.test.mjs tests/harness-*.test.mjs tests/official-tools.test.mjs tests/official-team-runtime.test.mjs tests/model-profiles.test.mjs tests/model-profile-editor.test.mjs tests/tool-install-state.test.mjs tests/vendor-adapters.test.mjs tests/desktop-compat.test.mjs tests/plugin-split.test.mjs tests/task-executors.test.mjs tests/roadmap.test.mjs tests/insights-state.test.mjs tests/billing-team-ledger.test.mjs tests/subscription-billing.test.mjs tests/subscription-host.test.mjs tests/test-pass-regressions.test.mjs tests/roadmap-gaps.test.mjs tests/cordis-inject.test.mjs tests/cli-terminal.test.mjs",
81
83
  "prepack": "node scripts/prepack.mjs",
82
84
  "pack:local": "npm pack --pack-destination dist"
83
85
  },
@@ -96,6 +98,9 @@
96
98
  "@deepseek-ai/dsh-typert-protocol": "0.2.0-rc.1 || 0.2.0-rc.2",
97
99
  "@deepseek-ai/schemastery": "~3.18.4"
98
100
  },
101
+ "optionalDependencies": {
102
+ "@lydell/node-pty": "1.2.0-beta.15"
103
+ },
99
104
  "devDependencies": {
100
105
  "@deepseek-ai/cordis": "4.0.4",
101
106
  "@deepseek-ai/cordis-plugin-group": "1.0.4",
@@ -154,6 +159,8 @@
154
159
  "@deepseek-ai/dsh-util-workspace-path": "0.2.0-rc.2",
155
160
  "@deepseek-ai/dsh-workspace": "0.2.0-rc.2",
156
161
  "@deepseek-ai/schemastery": "3.18.4",
162
+ "@xterm/addon-fit": "0.11.0",
163
+ "@xterm/xterm": "6.0.0",
157
164
  "esbuild": "0.28.2",
158
165
  "react": "18.3.1",
159
166
  "react-dom": "18.3.1"