dsh-remote 0.8.17 → 0.8.20

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
@@ -8,7 +8,7 @@
8
8
  [![license](https://img.shields.io/github/license/flymysql/dsh-remote)](LICENSE)
9
9
  [![dsh-plugin](https://img.shields.io/badge/topic-dsh--plugin-7a3ef3)](https://github.com/topics/dsh-plugin)
10
10
 
11
- ![dsh-remote workspace picker](docs/cover.png)
11
+ ![dsh-remote — make any SSH machine a real DSH workspace](docs/cover.png)
12
12
 
13
13
  **Remote-work assistant for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (DSH).**
14
14
 
@@ -41,10 +41,10 @@ Real capture (host scrubbed to a placeholder):
41
41
  - **远程 / Remote** — the picker is a **centered modal**. Pick a **machine** → on Windows hosts the root shows a **"This PC" drive view** (`C:\`, `D:\`, `E:\`… instead of the Git Bash MSYS root) and the path field live **autocompletes** directories (accepts `C:\Users\…` or `/c/Users/…` — Windows paths are rewritten to the Git Bash form underneath); selecting a directory immediately lists its next level. A **浏览…** floating browser (Windows-aware breadcrumb `此电脑 / C:\ / Users / dev`, drive rows, size + mtime, dirs first, follows symlinks) fills the field without committing; the **回上一级** button works at any depth (even when the browser was opened at the path bar's value). **最近 workspaces** quick-pick, **`~` 主目录** shortcut and **新建目录** are one click away. On confirm it creates a **real local mirror** under `$DSH_HOME/remote-workspaces/<host>-<user>-<port>/<base>` that passes `fs.realpath` → the harness adopts it as a real workspace while dsh-remote keeps it synced over SFTP.
42
42
  - **Git Bash default terminal (Windows remotes)** — the remote platform is auto-detected (`cmd /c ver`, plus an `uname -s` MINGW/MSYS probe as fallback); on Windows the plugin locates Git Bash (`config.shell` can pin a path or `native` disables wrapping) and pipes every command to `bash -s` over the exec channel, so quoting/backslash escaping is never an issue regardless of the SSH default shell. `rw_exec` runs with a Git Bash cwd (`/c/Users/…` form). `/dsh-remote/status`, `rw_info` and the 测试连接 button report the detected platform + shell.
43
43
  - **Windows path auto-conversion** — typing `C:\Users\dev\project` (or `C:/…`, `/c/…`, `/C:/…`) is normalized underneath to the Git Bash form `/c/Users/dev/project` for shell commands, while workspaces are stored and shown Windows-style (`C:\Users\dev\project`). All model tools accept and report both forms; SFTP access uses the Win32-OpenSSH `/D:/…` form (see `toSftpPath`).
44
- - **Bidirectional SFTP sync, conflict-aware** — `rw_sync` (remote → mirror) and `rw_push` (mirror → remote) are **three-way** (remote vs local vs last-synced snapshot): files changed on both sides are **reported as conflicts and never silently overwritten** (`force=true` overrides). Both support **dry-run**, **background tasks**, and honor **gitignore-style ignore rules** (`.dsh-remote-ignore` under `remote-workspaces`, defaults cover `.git/node_modules/target/dist/build/…`).
44
+ - **Bidirectional SFTP sync, conflict-aware** — `rw_sync` (remote → mirror) and `rw_push` (mirror → remote) are **three-way** (remote vs local vs last-synced snapshot): files changed on both sides are **reported as conflicts and never silently overwritten** (`force=true` overrides). Defaults are **depth 8 / 2000 files**; hitting a cap is reported as **`TRUNCATED`**. Both support **dry-run**, **background tasks**, and honor **gitignore-style ignore rules**.
45
45
  - **Model tools** — 20 tools, all Windows/POSIX portable via SFTP: `rw_info`, `rw_connect` (with `save`), `rw_pick_workspace`, `rw_list_dir` (size+mtime), `rw_stat`, `rw_read_file` (encoding-aware: utf-8/gbk), `rw_write_file`, **`rw_edit`** (literal replace + mtime optimistic lock), `rw_append`, `rw_mkdir`, `rw_remove` (recursive, bounded), `rw_move`, `rw_exec` (pty/env), **`rw_search`** (SFTP tree walk — works on Windows too, honors ignore rules, context lines), `rw_download`/`rw_upload` (streaming fastGet/fastPut + size caps), **`rw_forward`** (SSH tunnels), `rw_sync`, `rw_push`, `rw_disconnect`.
46
46
  - **Port forwarding panel** — create/start/stop/remove **local** (`127.0.0.1:port → remote`) and **reverse** (`remote → local`) tunnels in the Settings page or via `rw_forward`; definitions persist, auto-restart on reconnect when enabled, all tunnels stop on disconnect.
47
- - **Sidebar remote editing** — the better-sidebar remote file tab is now **editable**: click **编辑** → edit → **保存到远程** with an mtime optimistic lock (409 + "重新读取" on concurrent change). The explorer rows show file sizes and have a **right-click menu** (下载到本地镜像 / 重命名 / 删除 / 新建目录).
47
+ - **Sidebar remote editing** — the remote file tab is **editable**: click **编辑** → edit → **保存到远程** with an mtime optimistic lock (409 + "重新读取" on concurrent change). File ops are **session-bound** (v0.8.19): the explorer sends `sessionId` so two conversations on different hosts do not share the active-machine pool. The explorer rows show file sizes and have a **right-click menu** (下载到本地镜像 / 重命名 / 删除 / 新建目录).
48
48
  - **Command audit log** — every `rw_exec`/write/remove/move/forward is appended to `$DSH_HOME/remote-workspaces/audit.log` (time · user@host · op · exit code · command); the Settings page shows the last 30.
49
49
  - **Async long tasks** — `rw_sync`/`rw_push` with `async: true` return a `taskId`; progress/result/cancel via `/dsh-remote/task` (single-flight queue).
50
50
  - **Connection health** — a **「测试连接」** button validates host/user/key/password (with per-category error hints: auth / network / host key / timeout) before you save a machine; latency is cached on the machine record.
@@ -74,24 +74,20 @@ core or require a listening Web server:
74
74
  `sidebar.right.pane.tab` seat. It reuses the existing explorer/editor and
75
75
  gives remote files their own session-scoped resource addresses, rather than
76
76
  sending remote paths to the local Files viewer.
77
- - When the core Web-server row is explicitly disabled (the official Desktop
78
- composition), the bundled `dsh-better-sidebar` row stays disabled. Web hosts
79
- retain the existing `/dsh-remote/*` routes and sidebar composition, including
80
- the standalone-sidebar deduplication guard.
81
-
82
- Validation so far covers Host startup, Desktop IPC JSON requests, read-only
83
- SSH connection/list/read, and opening the settings/import UI. Native file-tab
84
- navigation, editing/sync, concurrent sessions on different machines, and the
85
- full legacy Web UI still need end-to-end acceptance before release. In
86
- particular, existing sidebar file endpoints use the active-machine pool;
87
- session-scoped tab addresses alone do **not** make those endpoints
88
- session-bound. This is not a claim of production-ready multi-machine Desktop
89
- support.
90
-
91
- Desktop's package installer may also require an explicit policy for the
92
- optional `ssh2` / `cpu-features` build scripts. The isolated transport test
93
- disabled those optional scripts; this change does not loosen an application's
94
- build allowlist or automatically approve dependency scripts.
77
+ - `dsh-better-sidebar` is not bundled. Web hosts may install it separately;
78
+ official Desktop uses the native right-sidebar integration instead.
79
+
80
+ Since **v0.8.19**, sidebar `/ls` `/read` `/write` `/fs` resolve the session's
81
+ mirror binding (same path as `rw_*`) when the client sends `sessionId`. Two
82
+ sessions on different hosts no longer share the active-machine pool for file
83
+ ops. Host-side tests cover that routing plus the editor 409/re-read/save path.
84
+
85
+ Official Desktop's native file-tab GUI, failed/cancelled dialogs, non-macOS
86
+ hosts, and a full legacy Web UI pass are still experimental. Desktop's package
87
+ installer may also require an explicit policy for the optional `ssh2` /
88
+ `cpu-features` build scripts. The isolated transport test disabled those
89
+ optional scripts; this change does not loosen an application's build allowlist
90
+ or automatically approve dependency scripts.
95
91
 
96
92
  ### Published Web bundle
97
93
 
@@ -99,48 +95,29 @@ build allowlist or automatically approve dependency scripts.
99
95
  dsh plugin add dsh-remote # add the bundle
100
96
  ```
101
97
 
102
- One command installs everything: since **v0.7.2** the sidebar
103
- ([dsh-better-sidebar](https://www.npmjs.com/package/dsh-better-sidebar)) is a
104
- hard dependency and is mounted automatically — the 🌐 remote-file explorer and
105
- remote file viewer show up in the sidebar with no extra step. If you already
106
- have the sidebar installed on its own, the embedded copy backs off (no double
107
- mount) — regardless of whether the standalone bundle is listed **before or
108
- after** `dsh-remote` in `dsh.profile.bundles` (order-independent guard since
109
- 0.8.7; earlier versions crashed boot with `duplicate prefix route
110
- "/sidebar/api"` when the standalone bundle came after `dsh-remote`).
111
-
112
- > **Upgrading from ≤0.8.6 with a standalone sidebar?** You may keep the
113
- > standalone `dsh-better-sidebar` bundle (any order) — 0.8.7+ no longer
114
- > crashes. Or remove it from `bundles` and let dsh-remote mount the embedded
115
- > copy (version ^0.18.1 since 0.8.15 — earlier releases pinned 0.14.x, whose
116
- > `import { settingsNamespace } from "@deepseek-ai/dsh-settings"` broke once
117
- > dsh-settings 0.1.2-alpha.2 made that symbol private; issue #29).
118
-
119
- > **Harness requirement of the embedded sidebar (0.8.15+): `dsh ≥ 0.1.2-rc.1`.**
120
- > `dsh-better-sidebar` 0.18.x imports `SessionLogOffset` from
121
- > `@deepseek-ai/dsh-session`, which only exists from 0.1.2-rc.1 on. On an older
122
- > harness (0.1.0-rc.x) that import fails and the loader aborts the whole plugin
123
- > tree, so dsh does not start at all. Verified: 0.8.14 + sidebar 0.14.0 boots on
124
- > 0.1.0-rc.8, while 0.8.15 + sidebar 0.18.1 does not; both boot on 0.1.2-rc.1.
125
- > On an older harness either stay on **0.8.14**, or keep the host half by
126
- > disabling the embedded sidebar row in your profile's `cordis.patch.yml`
127
- > (verified working on 0.1.0-rc.8 — the `rw_*` tools keep working, the sidebar
128
- > UI is what you give up):
129
- >
130
- > ```yaml
131
- > - id: dsh-remote-sidebar
132
- > disabled: true
133
- > ```
134
- >
135
- > (and do not list a standalone `dsh-better-sidebar` bundle either).
136
-
137
- > **Requires the profile's pnpm linker to be `hoisted`** (the DSH profile
138
- > default, `nodeLinker: hoisted` in `pnpm-workspace.yaml`). The loader resolves
139
- > plugin packages from the profile root, so the sidebar must be reachable in
140
- > the top-level `node_modules`. If your `pnpm-workspace.yaml` was rewritten
141
- > without `nodeLinker: hoisted`, add it back (`nodeLinker: hoisted`) and run
142
- > `pnpm install` once — otherwise the embedded sidebar row fails with
143
- > `Cannot find package 'dsh-better-sidebar'`.
98
+ Since **v0.8.18**, `dsh-remote` installs and mounts only itself. The Web sidebar
99
+ ([dsh-better-sidebar](https://www.npmjs.com/package/dsh-better-sidebar)) is
100
+ optional and is no longer a dependency or an automatically mounted row. This
101
+ keeps the SSH tools and settings UI independent from a particular sidebar
102
+ implementation.
103
+
104
+ To add the optional Web remote-file explorer/editor, install both bundles:
105
+
106
+ ```bash
107
+ dsh plugin add dsh-remote
108
+ dsh plugin add dsh-better-sidebar
109
+ ```
110
+
111
+ When the standalone sidebar service is present, `dsh-remote` discovers it
112
+ dynamically and registers its remote explorer/editor tabs. Without it, all
113
+ `rw_*` tools, the settings UI, sync, audit log, and port forwarding continue to
114
+ work. Official Desktop uses its native right-sidebar seats and does not need
115
+ `dsh-better-sidebar`.
116
+
117
+ > **Upgrading from 0.7.2–0.8.17:** upgrading to 0.8.18 removes the embedded
118
+ > sidebar dependency and mount. Install `dsh-better-sidebar` separately only if
119
+ > you still want that Web UI. Any old profile override for
120
+ > `id: dsh-remote-sidebar` can be removed because that row no longer exists.
144
121
 
145
122
  (or `npm install dsh-remote` + add `- id: dsh-remote / name: dsh-remote` in `cordis.patch.yml`).
146
123
 
package/README.zh.md CHANGED
@@ -13,13 +13,14 @@
13
13
  `dsh-app:` 通道承载请求,鉴权仍由宿主负责,不启动 Web Server。
14
14
  - 通过 `sidebarRightTabs` 和 `sidebar.right.pane.tab` 提供原生“远程文件”入口,
15
15
  复用原来的文件树与编辑器,不把远端路径传给本地文件预览器。
16
- - 官方 Desktop 显式禁用 Web Server 时,不挂载内置 `dsh-better-sidebar`;
17
- Web 版仍保留原路由、侧栏及独立安装时的去重逻辑。
16
+ - `dsh-better-sidebar` 不再内置;Web 版可以单独安装,官方 Desktop 则使用
17
+ 原生右侧栏集成。
18
18
 
19
19
  已验证 Host 启动、IPC 请求、真实 SSH 的只读连接/目录列表/文本读取,以及设置页和
20
- 测试 SSH 配置的导入。文件标签的完整 UI 操作、编辑/同步、多机器并行会话,以及旧 Web
21
- 版完整 UI 回归仍需在发布前验收。尤其是现有侧栏文件接口仍使用“当前机器”的连接池,
22
- 不能把会话级标签地址误认为后端已实现会话级机器绑定;本分支尚不代表生产级多机器支持。
20
+ 测试 SSH 配置的导入。**v0.8.19** 起侧栏 `/ls` `/read` `/write` `/fs` 在请求带
21
+ `sessionId` 时按该会话的镜像绑定选机(与 `rw_*` 同一套),不再落到「当前机器」
22
+ 连接池;宿主侧测试覆盖双机会话路由与编辑 409/重读/保存。原生文件标签的完整 GUI、
23
+ 失败/取消交互、非 macOS 宿主以及旧 Web 版完整 UI 回归仍属实验性。
23
24
 
24
25
  Desktop 安装器还可能要求明确配置 `ssh2` / `cpu-features` 可选构建脚本策略。
25
26
  隔离验证中禁用了这些可选脚本;本改动不放宽应用的构建白名单,也不自动批准脚本。
@@ -28,7 +29,7 @@ Desktop 安装器还可能要求明确配置 `ssh2` / `cpu-features` 可选构
28
29
  [![license](https://img.shields.io/github/license/flymysql/dsh-remote)](LICENSE)
29
30
  [![dsh-plugin](https://img.shields.io/badge/topic-dsh--plugin-7a)](https://github.com/topics/dsh-plugin)
30
31
 
31
- ![dsh-remote 工作区选择](docs/cover.png)
32
+ ![dsh-remote —— 把任意 SSH 机器变成真正的 DSH 工作区](docs/cover.png)
32
33
 
33
34
  **为 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(DSH)打造的远程工作助手。**
34
35
 
@@ -54,18 +55,21 @@ DSH 的 Web 界面刻意只监听 `127.0.0.1`(CLI 为安全拒绝 `--host 0.0.
54
55
 
55
56
  ## 功能
56
57
 
57
- - **多机 SSH** —— 可存任意多台主机(host/port/user + **私钥**或**密码**)。密码只存在本地,界面不回显;在设置里一键切当前机。
58
+ - **多机 SSH** —— 可存任意多台主机(host/port/user + **私钥**或**密码**)。密码只存在本地,界面不回显;在设置里一键切当前机。每机可配 passphrase / 主机指纹策略 / SSH agent / keyboard-interactive(OTP)/ 跳板机,以及可选的 **系统钥匙串加密密码**。
59
+ - **`~/.ssh/config` 导入** —— 设置页列出 Host 别名,一点填入表单(只引用路径,不读密钥材料)。
58
60
  - **双 tab 工作区选择器**(填充原生「Add workspace」流程):
59
61
  - **本机** —— 走 **host 端原生系统文件夹对话框**选本地目录(或直接输入本地路径)→ 直接成为普通 DSH 本地工作区(与本地工作区共存)。优先用 DSH 的 `directoryPicker` 服务,服务缺失时**回退到插件自持的原生选择器**(macOS `osascript` / Linux `zenity`→`kdialog`)——桌面启动路径上框架服务不注册也能用。
60
62
  - **远程** —— 选择器是**居中弹窗**(窄侧边栏也不会被挤压)。先**选机器** → Windows 主机根级显示 **「此电脑」多盘视图**(`C:\`、`D:\`、`E:\`…,而不是 Git Bash 的 MSYS 根),路径框**实时补全**目录(支持 `C:\Users\…` 或 `/c/Users/…` 任意写法,Windows 路径在底层自动改写为 Git Bash 形式);**选中一个目录立即列出它下一级**(OS/VSCode 式级联)。另有 **「浏览…」文件选择式浮层**(Windows 面包屑 `此电脑 / C:\ / Users / dev` 可点击跳级、驱动器行、大小/时间、跟随软链),选中**回填输入框不提交**,你复核/修改后再确定;「回上一级」任意深度可用(包括浮层直接打开在路径栏当前路径时)。**最近工作区**快捷入口、**`~` 主目录**、**新建目录**一键可达。确定会创建**真实本地镜像**(`$DSH_HOME/remote-workspaces/<host>-<user>-<port>/<basename>`;仅当同主机上**别的远端路径**已占用同名 basename 时才追加短路径 hash)→ harness 把它当真实工作区收养,同时 dsh-remote 通过 SFTP 保持同步。所选工作区会**持久化到该机器**,重启不丢。
61
63
  - **Git Bash 默认终端(Windows 主机)** —— 自动探测远程平台(`cmd /c ver`,附 `uname -s` 的 MINGW/MSYS 探测兜底);Windows 机器自动定位 Git Bash(`config.shell` 可显式指定或 `native` 关闭),所有命令经 `bash -s` 从 SSH 通道 stdin 管道执行,不依赖 cmd/PowerShell,也不受引号/反斜杠转义困扰;`rw_exec` 默认在 Git Bash 形式的 cwd(`/c/Users/…`)下执行。`/dsh-remote/status`、`rw_info`、设置页「测试连接」都会报告检测到的平台与 shell。
62
64
  - **Windows 路径自动改写** —— 用户输入 `C:\Users\dev\project`(或 `C:/…`、`/c/…`、`/C:/…`)时底层自动规范为 Git Bash 形式 `/c/Users/dev/project` 执行;工作区存储与展示为 Windows 形式 `C:\Users\dev\project`。模型工具全部接受并展示两种写法;SFTP 访问使用 Win32-OpenSSH 的 `/D:/…` 形式(见 `toSftpPath`)。
63
- - **双向 SFTP 同步(增量)** —— `rw_sync`(远程→镜像)、`rw_push`(镜像→远程),本地镜像改动可回传机器。两者都会**跳过 size+mtime 未变化的文件**,并有**单文件大小上限**防误拉大二进制;目录遍历带**有界并发**。
64
- - **模型工具** —— `rw_info`、`rw_connect`、`rw_pick_workspace`、`rw_list_dir`、`rw_read_file`、`rw_write_file`、`rw_exec`(默认在工作区目录执行,可传 `cwd=`)、`rw_search`(可移植递归 grep)、`rw_download`、`rw_upload`、`rw_sync`、`rw_push`、`rw_disconnect`。
65
- - **直接写远程文件** —— `rw_write_file` 直接创建/覆盖远程文件(自动建父目录),单个文件改动不必绕本地镜像来回同步;`rw_download` / `rw_upload` 可单文件双向取真字节。
66
- - **连接体检** —— 设置页提供「测试连接」按钮,在保存机器之前先验证 host/user/密码/私钥是否可用。
65
+ - **双向 SFTP 同步(三路冲突检测)** —— `rw_sync`(远程→镜像)、`rw_push`(镜像→远程)。两边都改过的文件会列出冲突、绝不静默覆盖(`force=true` 覆盖)。默认 **深度 8 / 2000 文件**,触顶会标明 **`TRUNCATED`**。支持 dry-run、后台任务、gitignore 风格 ignore 规则。
66
+ - **模型工具(20 个)** —— `rw_info`、`rw_connect`、`rw_pick_workspace`、`rw_list_dir`、`rw_stat`、`rw_read_file`(utf-8/gbk)、`rw_write_file`、`rw_edit`(字面替换 + mtime 乐观锁)、`rw_append`、`rw_mkdir`、`rw_remove`、`rw_move`、`rw_exec`、`rw_search`(POSIX 优先 rg/grep,否则 SFTP 遍历)、`rw_download`/`rw_upload`、`rw_forward`、`rw_sync`、`rw_push`、`rw_disconnect`。
67
+ - **端口转发面板** —— 设置页或 `rw_forward` 创建/启停本地与反向隧道。
68
+ - **侧栏远程编辑** —— 远程文件 tab 可编辑并保存到远端(mtime 乐观锁)。**v0.8.19** 起文件操作按会话绑定机器。
69
+ - **命令审计** —— `rw_exec`/写/删/移动/转发写入 `audit.log`;设置页显示最近 30 条。
70
+ - **连接体检** —— 设置页「测试连接」按类别提示(认证 / 网络 / 主机指纹 / 超时)。
67
71
  - 当前 `user@host:/path` 会注入每次系统提示,让 Agent 明确自己的工作根。
68
- - **远端跨平台** —— 命令全部用可移植 POSIX 写法(`ls -la` / `sed -n` / `find … -exec grep`),macOS/BSD 与 GNU/Linux 远端都能用。
72
+ - **远端跨平台** —— 文件访问走 SFTP 协议(不依赖 POSIX shell),Linux/macOS/Windows 远端都能列/读/写/搜索/同步。
69
73
  - **主机指纹校验(TOFU)** —— 每次 SSH 连接都校验主机密钥(`hostKeyMode: accept-new`):首次连接记录,之后**密钥一旦变化立即拒绝**(防中间人)。`verify` 模式还会拒绝从未见过的机器;`off` 关闭校验。指纹存于 `$DSH_HOME/remote-workspaces/known_hosts.json`;误判可用 `/remote forget-key` 重置。
70
74
  - **数据跟随 Harness 根目录** —— 机器清单与镜像放在 `$DSH_HOME/remote-workspaces`(桌面版即 `userData/harness` 下);0.6 之前落在 `~/.dsh/remote-workspaces` 的数据**首次启动自动迁移**,不丢失。
71
75
  - **不改任何 `dsh-workspace` 官方代码** —— 全部作为普通插件实现(client 半以 `priority -100` 填充 directory-flow holes)。
@@ -76,38 +80,25 @@ DSH 的 Web 界面刻意只监听 `127.0.0.1`(CLI 为安全拒绝 `--host 0.0.
76
80
  dsh plugin add dsh-remote # 添加 bundle
77
81
  ```
78
82
 
79
- 一条命令装齐:从 **v0.7.2** 起,侧边栏
80
- ([dsh-better-sidebar](https://www.npmjs.com/package/dsh-better-sidebar))是
81
- **硬依赖并自动挂载** —— 装完 dsh-remote 后,侧边栏里的「🌐 远程文件」目录树和
82
- 远程文件查看器即可直接用,无需额外步骤。如果你已单独安装过该侧边栏,内嵌副本
83
- 会自动退避(不会重复挂载)。
84
-
85
- > **内嵌侧边栏版本**:0.8.15 起依赖范围为 `^0.18.1`。0.8.14 及更早锁在 `^0.14.0`,
86
- > 而 0.14.0~0.17.1 仍然 `import { settingsNamespace } from "@deepseek-ai/dsh-settings"`,
87
- > 该导出自 dsh-settings 0.1.2-alpha.2 起被移除,静态导入失败会让整个插件树加载失败
88
- > (issue #29)。
89
-
90
- > **⚠ 内嵌侧边栏对 harness 的要求(0.8.15+):`dsh ≥ 0.1.2-rc.1`**。
91
- > `dsh-better-sidebar` 0.18.x 会 `import { SessionLogOffset } from "@deepseek-ai/dsh-session"`,
92
- > 该导出从 0.1.2-rc.1 才有。在更老的 harness(0.1.0-rc.x)上这个静态导入会失败,
93
- > loader 因此判定整个插件树加载失败 —— **dsh 直接起不来**。实测:`0.8.14 + 侧边栏 0.14.0`
94
- > 在 0.1.0-rc.8 上可以正常启动,`0.8.15 + 侧边栏 0.18.1` 不行;两者在 0.1.2-rc.1 上都正常。
95
- > 老 harness 上要么**留在 0.8.14**,要么在 profile 的 `cordis.patch.yml` 里关掉内嵌侧边栏行
96
- > 以保住 host 半(已在 0.1.0-rc.8 实测可启动,`rw_*` 工具照常可用,放弃的只是侧边栏 UI):
97
- >
98
- > ```yaml
99
- > - id: dsh-remote-sidebar
100
- > disabled: true
101
- > ```
102
- >
103
- > (同时不要再单独列出 `dsh-better-sidebar` bundle。)
104
-
105
- > **要求 profile 的 pnpm linker 为 `hoisted`**(DSH profile 默认,
106
- > `pnpm-workspace.yaml` 里 `nodeLinker: hoisted`)。loader 从 profile 根解析
107
- > 插件包,侧边栏必须能在顶层 `node_modules` 被解析到。如果你的
108
- > `pnpm-workspace.yaml` 被重写丢掉了 `nodeLinker: hoisted`,请补回并执行一次
109
- > `pnpm install` —— 否则内嵌侧边栏行会报
110
- > `Cannot find package 'dsh-better-sidebar'`。
83
+ 从 **v0.8.18** 起,`dsh-remote` 只安装并挂载自身。Web 侧边栏
84
+ ([dsh-better-sidebar](https://www.npmjs.com/package/dsh-better-sidebar))
85
+ 改为可选,不再是依赖,也不会被自动挂载。这样 SSH 工具和设置页不再被某个侧边栏
86
+ 实现的版本/API 变化拖垮。
87
+
88
+ 如需 Web 版远程文件浏览/编辑,请显式安装两个 bundle:
89
+
90
+ ```bash
91
+ dsh plugin add dsh-remote
92
+ dsh plugin add dsh-better-sidebar
93
+ ```
94
+
95
+ 独立侧边栏 service 存在时,`dsh-remote` 会动态发现它并注册远程文件 tab;
96
+ 不安装时,`rw_*` 工具、设置页、同步、审计日志和端口转发均照常工作。
97
+ 官方 Desktop 使用原生右侧栏,不需要安装 `dsh-better-sidebar`。
98
+
99
+ > **从 0.7.2–0.8.17 升级:** 升到 0.8.18 后,内嵌侧边栏依赖和挂载会消失。
100
+ > 只有仍需要 Web 侧边栏 UI 时才单独安装 `dsh-better-sidebar`。旧 profile 里针对
101
+ > `id: dsh-remote-sidebar` 的覆盖可以删除,因为这行已不存在。
111
102
 
112
103
  (或 `npm install dsh-remote`,再在 `cordis.patch.yml` 加 `- id: dsh-remote / name: dsh-remote`。)
113
104
 
@@ -121,11 +112,13 @@ dsh plugin add dsh-remote # 添加 bundle
121
112
  - **本机** → 系统文件夹选择(或输入本地路径)→ 本地工作区。
122
113
  - **远程** → 选机器 → 浏览到远程目录(或输入 `/path`)→ 「设为远程工作区」⇒ 创建并收养一个本地镜像工作区。
123
114
  3. **让 Agent 工作** —— 把它当普通工作区用:
124
- - `rw_list_dir(path?)` / `rw_read_file` —— 查看远程文件
125
- - `rw_write_file(path, content)` —— 直接创建或覆盖远程文件
115
+ - `rw_list_dir(path?)` / `rw_read_file` / `rw_stat` —— 查看远程文件
116
+ - `rw_write_file` / `rw_edit` / `rw_append` —— 创建、补丁、追加远程文件
117
+ - `rw_mkdir` / `rw_remove` / `rw_move` —— 管理远程路径
126
118
  - `rw_search(pattern, path?)` —— 远程 grep
127
119
  - `rw_exec(command, cwd?)` —— 在远程执行命令(默认在工作区目录)
128
- - `rw_sync` / `rw_push` —— 拉取/推送本地镜像 <-> 远程
120
+ - `rw_forward` —— SSH 隧道
121
+ - `rw_sync` / `rw_push` —— 冲突感知的镜像拉取/推送
129
122
 
130
123
  > **Remote context 是 session 级的(v0.8.8+)**:system prompt 只会在**当前 session 的
131
124
  > cwd 位于某个远程 mirror 内**(即你把远程目录选成了这个 session 的工作区)时注入
package/cordis.patch.yml CHANGED
@@ -1,59 +1,9 @@
1
- # dsh-remote bundle patch: inserts this plugin into a profile's layer stack.
2
- # Applied when a profile lists this bundle; rows reference the package by name so
3
- # Node resolution finds the installed code.
1
+ # dsh-remote bundle patch: inserts only this plugin into a profile's layer stack.
4
2
  #
5
- # Since v0.7.2 dsh-remote ships dsh-better-sidebar as a hard dependency and
6
- # mounts it here, so `dsh plugin add dsh-remote` brings up the sidebar with
7
- # zero extra steps on Web. Official Desktop explicitly disables the Web server
8
- # row and uses the native right-sidebar seats instead. The sidebar
9
- # row uses a DEDICATED id (`dsh-remote-sidebar`)
10
- # and a guard so it backs off when another row mounts `dsh-better-sidebar`
11
- # (e.g. the user installed the sidebar bundle on its own, or an aggregate
12
- # bundle owns it) — two enabled mounts would both register /sidebar/api and
13
- # fail the whole plugin tree at boot.
14
- #
15
- # Order-independence (issue #12): the guard must NOT depend on
16
- # `ctx.loader.entries()` creation order. The loader creates entries in bundle
17
- # order and each row's disabled expression is evaluated at creation time, when
18
- # only rows created BEFORE it are visible — so with
19
- # `bundles: [..., "dsh-remote", "dsh-better-sidebar", ...]` (standalone sidebar
20
- # installed after dsh-remote, the normal `dsh plugin add` append order) both
21
- # rows saw no other sidebar mount and BOTH started, crashing boot with
22
- # `duplicate prefix route "/sidebar/api"`.
23
- #
24
- # Instead we scan the COMPOSED PATCH STACK — the include entry's
25
- # `config.patches` (every bundle layer in `dsh.profile.bundles` order, then the
26
- # profile/home/overlay layers) — for any OTHER row that mounts
27
- # `dsh-better-sidebar`. That data is fully materialized before any row guard
28
- # evaluates, so the outcome is identical no matter where the standalone bundle
29
- # sits in the bundle list. It is also pure data: it never reads another row's
30
- # `disabled` (reading another row's disabled expression would recursively
31
- # re-enter the loader — dsh-better-sidebar's own row carries one too, so the
32
- # two would ping-pong), so no recursion is possible. The old entries()-based
33
- # check is kept as a belt-and-suspenders fallback for rows not visible in the
34
- # patch stack (e.g. an entry injected at runtime by HMR or another plugin).
3
+ # dsh-better-sidebar is deliberately not mounted here and is not a dependency.
4
+ # Users who want that optional Web UI can install its bundle separately;
5
+ # lib/client.js discovers the betterSidebar service dynamically and adds the
6
+ # remote explorer/editor tabs when it is present.
35
7
  - insert:
36
8
  - id: dsh-remote
37
9
  name: 'dsh-remote'
38
- - id: dsh-remote-sidebar
39
- name: 'dsh-better-sidebar'
40
- disabled: !!js >-
41
- (function () {
42
- // Inspect the core row's explicit disabled flag, not service presence:
43
- // a Web server that simply has not started yet must still be awaited.
44
- // Do not evaluate another sidebar's disabled guard (issue #12).
45
- if ([...ctx.loader.entries()].some((e) => e.options.name === '@deepseek-ai/dsh-host-webserver' && e.options.disabled === true)) return true
46
- try {
47
- const inc = ctx.loader && ctx.loader.resolve && ctx.loader.resolve('include')
48
- const patches = inc && inc.subtree && inc.subtree.config && inc.subtree.config.patches
49
- if (Array.isArray(patches)) {
50
- for (const p of patches) {
51
- if (!p || !Array.isArray(p.insert)) continue
52
- for (const row of p.insert) {
53
- if (row && row.name === 'dsh-better-sidebar' && row.id !== 'dsh-remote-sidebar') return true
54
- }
55
- }
56
- }
57
- } catch (e) {}
58
- return [...ctx.loader.entries()].some((e) => e.options.name === 'dsh-better-sidebar' && e.options.id !== 'dsh-remote-sidebar')
59
- })()
package/lib/binding.js CHANGED
@@ -13,6 +13,7 @@
13
13
 
14
14
  import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
15
15
  import path from 'node:path'
16
+ import zlib from 'node:zlib'
16
17
 
17
18
  /** Strip trailing separators so a mirror root and a path inside it compare cleanly. */
18
19
  const norm = (p) => path.resolve(String(p || '')).replace(/[\\/]+$/, '') || ''
@@ -95,3 +96,71 @@ export function resolveMirror(local, root) {
95
96
  export function poolKey(m) {
96
97
  return `${(m && m.username) || ''}@${(m && m.host) || ''}:${Number(m && m.port) || 22}`
97
98
  }
99
+
100
+ /** Safe path segment for a session id (mirrors DSH's encodeSegment). */
101
+ export function encodeSegmentSafe(id) {
102
+ const s = String(id || '').replace(/[^A-Za-z0-9._-]/g, '_')
103
+ return s || 'session'
104
+ }
105
+
106
+ /** Read the cwd from a session log header (JSONL / .gz / zstd first frame). */
107
+ export function readSessionHeaderCwd(file) {
108
+ try {
109
+ let buf = readFileSync(file)
110
+ if (/\.zstd$/.test(file)) buf = zlib.zstdDecompressSync(buf)
111
+ else if (/\.gz$/.test(file)) buf = zlib.gunzipSync(buf)
112
+ const nl = buf.indexOf(10)
113
+ if (nl < 0) return ''
114
+ const head = JSON.parse(buf.subarray(0, nl).toString('utf8'))
115
+ return typeof head.cwd === 'string' ? head.cwd : ''
116
+ } catch {
117
+ return ''
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Resolve a session's workspace cwd from the live sessions service, then from
123
+ * durable session logs under `$DSH_HOME/sessions`.
124
+ */
125
+ export function lookupSessionCwd(sessionId, { sessions, dshHome } = {}) {
126
+ if (!sessionId) return ''
127
+ try {
128
+ if (sessions && typeof sessions.get === 'function') {
129
+ const s = sessions.get(sessionId)
130
+ const cwd = s && s.header && s.header.cwd
131
+ if (cwd) return String(cwd)
132
+ }
133
+ } catch { /* sessions service unavailable */ }
134
+ if (!dshHome) return ''
135
+ try {
136
+ const sessionsRoot = path.join(dshHome, 'sessions')
137
+ if (!existsSync(sessionsRoot)) return ''
138
+ const targetDir = encodeSegmentSafe(sessionId)
139
+ for (const projDir of readdirSync(sessionsRoot)) {
140
+ const projPath = path.join(sessionsRoot, projDir)
141
+ if (!statSync(projPath, { throwIfNoEntry: false })?.isDirectory?.()) continue
142
+ const sessDir = path.join(projPath, targetDir)
143
+ if (!statSync(sessDir, { throwIfNoEntry: false })?.isDirectory?.()) continue
144
+ const logFile = ['session.jsonl.zstd', 'session.jsonl', 'session.jsonl.gz']
145
+ .map((n) => path.join(sessDir, n)).find((p) => existsSync(p))
146
+ if (!logFile) continue
147
+ const cwd = readSessionHeaderCwd(logFile)
148
+ if (cwd) return cwd
149
+ }
150
+ } catch { /* session log scan failed */ }
151
+ return ''
152
+ }
153
+
154
+ /** Pull sessionId / local from a JSON HTTP request (query string + body). */
155
+ export function requestSessionHint(req, body = {}) {
156
+ let sessionId = ''
157
+ let local = ''
158
+ try {
159
+ const q = new URL(req.url || '', 'http://localhost').searchParams
160
+ sessionId = q.get('sessionId') ? decodeURIComponent(q.get('sessionId')) : ''
161
+ local = q.get('local') ? decodeURIComponent(q.get('local')) : ''
162
+ } catch { /* malformed url */ }
163
+ if (!sessionId && body && body.sessionId) sessionId = String(body.sessionId)
164
+ if (!local && body && body.local) local = String(body.local)
165
+ return { sessionId: String(sessionId || '').trim(), local: String(local || '').trim() }
166
+ }