dsh-remote 0.8.15 → 0.8.17
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 +58 -3
- package/README.zh.md +40 -3
- package/cordis.patch.yml +7 -1
- package/lib/client.js +250 -9
- package/lib/http-transport.js +104 -0
- package/lib/index.js +5 -9
- package/package.json +10 -1
package/README.md
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
[](LICENSE)
|
|
9
9
|
[](https://github.com/topics/dsh-plugin)
|
|
10
10
|
|
|
11
|
+

|
|
12
|
+
|
|
11
13
|
**Remote-work assistant for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (DSH).**
|
|
12
14
|
|
|
13
15
|
Manage several SSH machines, then pick a **remote workspace** (or a **local** one) and let the agent operate right there without leaving the harness — listing files, reading code, running builds & commands over the remote host, and keeping that remote directory mirrored into a real local workspace object.
|
|
@@ -18,7 +20,7 @@ The harness Web UI intentionally binds `127.0.0.1` (the CLI rejects `--host 0.0.
|
|
|
18
20
|
|
|
19
21
|
Settings → **远程工作区** — a multi-machine SSH registry (add / edit / delete / set-current, password stored locally):
|
|
20
22
|
|
|
21
|
-
<img src="
|
|
23
|
+
<img src="docs/ui-settings-panel.png" alt="dsh-remote settings — multi-machine registry (light theme, host scrubbed)" width="720"/>
|
|
22
24
|
|
|
23
25
|
The native **"Add workspace" / "Select workspace"** flow — a centered modal, two tabs, opens on **本机 (local)**; switch to **远程 (remote)**:
|
|
24
26
|
|
|
@@ -26,7 +28,7 @@ The native **"Add workspace" / "Select workspace"** flow — a centered modal, t
|
|
|
26
28
|
|
|
27
29
|
Real capture (host scrubbed to a placeholder):
|
|
28
30
|
|
|
29
|
-
<img src="
|
|
31
|
+
<img src="docs/ui-picker-panel.png" alt="dsh-remote workspace picker — real dialog; 本机 (local) tab; 远程 machine select + prefilled root path + autocomplete" width="720"/>
|
|
30
32
|
|
|
31
33
|
---
|
|
32
34
|
|
|
@@ -58,6 +60,41 @@ Real capture (host scrubbed to a placeholder):
|
|
|
58
60
|
|
|
59
61
|
## Install
|
|
60
62
|
|
|
63
|
+
### Official Desktop compatibility (experimental, unreleased)
|
|
64
|
+
|
|
65
|
+
This branch adds a compatibility path for the **official**
|
|
66
|
+
[DeepSeek Harness Desktop](https://github.com/deepseek-ai/deepseek-harness),
|
|
67
|
+
tested against the `0.1.5-rc.2` Host transport. It does not replace the Harness
|
|
68
|
+
core or require a listening Web server:
|
|
69
|
+
|
|
70
|
+
- The SSH settings and directory picker use `/api/dsh-remote/*` over the
|
|
71
|
+
Desktop's `dsh-app:` carrier. Exact Fetch routes are registered on
|
|
72
|
+
`ctx.connection.fetch`; the carrier retains ownership of authentication.
|
|
73
|
+
- A native **Remote Files** entry uses `sidebarRightTabs` and the keyed
|
|
74
|
+
`sidebar.right.pane.tab` seat. It reuses the existing explorer/editor and
|
|
75
|
+
gives remote files their own session-scoped resource addresses, rather than
|
|
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.
|
|
95
|
+
|
|
96
|
+
### Published Web bundle
|
|
97
|
+
|
|
61
98
|
```bash
|
|
62
99
|
dsh plugin add dsh-remote # add the bundle
|
|
63
100
|
```
|
|
@@ -79,6 +116,24 @@ after** `dsh-remote` in `dsh.profile.bundles` (order-independent guard since
|
|
|
79
116
|
> `import { settingsNamespace } from "@deepseek-ai/dsh-settings"` broke once
|
|
80
117
|
> dsh-settings 0.1.2-alpha.2 made that symbol private; issue #29).
|
|
81
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
|
+
|
|
82
137
|
> **Requires the profile's pnpm linker to be `hoisted`** (the DSH profile
|
|
83
138
|
> default, `nodeLinker: hoisted` in `pnpm-workspace.yaml`). The loader resolves
|
|
84
139
|
> plugin packages from the profile root, so the sidebar must be reachable in
|
|
@@ -234,4 +289,4 @@ MIT
|
|
|
234
289
|
|
|
235
290
|
## Changelog
|
|
236
291
|
|
|
237
|
-
See [CHANGELOG.md](./CHANGELOG.md).
|
|
292
|
+
See [CHANGELOG.md](./CHANGELOG.md).
|
package/README.zh.md
CHANGED
|
@@ -4,10 +4,32 @@
|
|
|
4
4
|
|
|
5
5
|
# dsh-remote
|
|
6
6
|
|
|
7
|
+
## 官方 Desktop 兼容适配(实验性,尚未发布)
|
|
8
|
+
|
|
9
|
+
本分支增加对 [DeepSeek 官方 Desktop](https://github.com/deepseek-ai/deepseek-harness)
|
|
10
|
+
的适配,以 `0.1.5-rc.2` Host 通信协议验证,不修改 Harness 核心:
|
|
11
|
+
|
|
12
|
+
- 通过 `ctx.connection.fetch` 注册 `/api/dsh-remote/*`,由 Desktop 的
|
|
13
|
+
`dsh-app:` 通道承载请求,鉴权仍由宿主负责,不启动 Web Server。
|
|
14
|
+
- 通过 `sidebarRightTabs` 和 `sidebar.right.pane.tab` 提供原生“远程文件”入口,
|
|
15
|
+
复用原来的文件树与编辑器,不把远端路径传给本地文件预览器。
|
|
16
|
+
- 官方 Desktop 显式禁用 Web Server 时,不挂载内置 `dsh-better-sidebar`;
|
|
17
|
+
Web 版仍保留原路由、侧栏及独立安装时的去重逻辑。
|
|
18
|
+
|
|
19
|
+
已验证 Host 启动、IPC 请求、真实 SSH 的只读连接/目录列表/文本读取,以及设置页和
|
|
20
|
+
测试 SSH 配置的导入。文件标签的完整 UI 操作、编辑/同步、多机器并行会话,以及旧 Web
|
|
21
|
+
版完整 UI 回归仍需在发布前验收。尤其是现有侧栏文件接口仍使用“当前机器”的连接池,
|
|
22
|
+
不能把会话级标签地址误认为后端已实现会话级机器绑定;本分支尚不代表生产级多机器支持。
|
|
23
|
+
|
|
24
|
+
Desktop 安装器还可能要求明确配置 `ssh2` / `cpu-features` 可选构建脚本策略。
|
|
25
|
+
隔离验证中禁用了这些可选脚本;本改动不放宽应用的构建白名单,也不自动批准脚本。
|
|
26
|
+
|
|
7
27
|
[](https://www.npmjs.com/package/dsh-remote)
|
|
8
28
|
[](LICENSE)
|
|
9
29
|
[](https://github.com/topics/dsh-plugin)
|
|
10
30
|
|
|
31
|
+

|
|
32
|
+
|
|
11
33
|
**为 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(DSH)打造的远程工作助手。**
|
|
12
34
|
|
|
13
35
|
维护多台 SSH 机器,然后在「选择工作区」时选一个**远程工作区**(或**本地工作区**),Agent 就能在不离开 harness 的情况下直接操作——列文件、读代码、在远程主机上跑构建/命令,并把远程目录镜像成一个真实的本地工作区对象。
|
|
@@ -18,7 +40,7 @@ DSH 的 Web 界面刻意只监听 `127.0.0.1`(CLI 为安全拒绝 `--host 0.0.
|
|
|
18
40
|
|
|
19
41
|
设置 → **远程工作区** —— 多机 SSH 列表(增/删/改/设为当前,密码本地保存、不回显):
|
|
20
42
|
|
|
21
|
-
<img src="
|
|
43
|
+
<img src="docs/ui-settings-panel.png" alt="dsh-remote 设置页 — 多机列表(浅色主题,主机已打码)" width="720"/>
|
|
22
44
|
|
|
23
45
|
原生 **「Add workspace / 选择工作区」** 流程 —— **居中弹窗**、两个 tab,**默认落在「本机」**;切到 **「远程」**:
|
|
24
46
|
|
|
@@ -26,7 +48,7 @@ DSH 的 Web 界面刻意只监听 `127.0.0.1`(CLI 为安全拒绝 `--host 0.0.
|
|
|
26
48
|
|
|
27
49
|
真实截图(机器已打码为占位):
|
|
28
50
|
|
|
29
|
-
<img src="
|
|
51
|
+
<img src="docs/ui-picker-panel.png" alt="dsh-remote 工作区选择 — 真实弹窗;默认本机 tab;远程:机器下拉 + 预填根路径 + 自动补全" width="720"/>
|
|
30
52
|
|
|
31
53
|
---
|
|
32
54
|
|
|
@@ -65,6 +87,21 @@ dsh plugin add dsh-remote # 添加 bundle
|
|
|
65
87
|
> 该导出自 dsh-settings 0.1.2-alpha.2 起被移除,静态导入失败会让整个插件树加载失败
|
|
66
88
|
> (issue #29)。
|
|
67
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
|
+
|
|
68
105
|
> **要求 profile 的 pnpm linker 为 `hoisted`**(DSH profile 默认,
|
|
69
106
|
> `pnpm-workspace.yaml` 里 `nodeLinker: hoisted`)。loader 从 profile 根解析
|
|
70
107
|
> 插件包,侧边栏必须能在顶层 `node_modules` 被解析到。如果你的
|
|
@@ -193,4 +230,4 @@ MIT
|
|
|
193
230
|
|
|
194
231
|
## 变更记录
|
|
195
232
|
|
|
196
|
-
见 [CHANGELOG.md](./CHANGELOG.md)。
|
|
233
|
+
见 [CHANGELOG.md](./CHANGELOG.md)。
|
package/cordis.patch.yml
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
#
|
|
5
5
|
# Since v0.7.2 dsh-remote ships dsh-better-sidebar as a hard dependency and
|
|
6
6
|
# mounts it here, so `dsh plugin add dsh-remote` brings up the sidebar with
|
|
7
|
-
# zero extra steps.
|
|
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`)
|
|
8
10
|
# and a guard so it backs off when another row mounts `dsh-better-sidebar`
|
|
9
11
|
# (e.g. the user installed the sidebar bundle on its own, or an aggregate
|
|
10
12
|
# bundle owns it) — two enabled mounts would both register /sidebar/api and
|
|
@@ -37,6 +39,10 @@
|
|
|
37
39
|
name: 'dsh-better-sidebar'
|
|
38
40
|
disabled: !!js >-
|
|
39
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
|
|
40
46
|
try {
|
|
41
47
|
const inc = ctx.loader && ctx.loader.resolve && ctx.loader.resolve('include')
|
|
42
48
|
const patches = inc && inc.subtree && inc.subtree.config && inc.subtree.config.patches
|
package/lib/client.js
CHANGED
|
@@ -517,10 +517,18 @@ window.__ModuleLoader__.load({
|
|
|
517
517
|
return msg
|
|
518
518
|
}
|
|
519
519
|
|
|
520
|
+
// The official Electron shell serves the shared API over its private
|
|
521
|
+
// dsh-app carrier. Legacy browser hosts keep their existing route prefix.
|
|
522
|
+
// Choose before sending: never retry a mutating request on another route.
|
|
523
|
+
function apiPath(path) {
|
|
524
|
+
return window.location && window.location.protocol === 'dsh-app:'
|
|
525
|
+
? '/api' + path : path
|
|
526
|
+
}
|
|
527
|
+
|
|
520
528
|
async function api(method, path, body) {
|
|
521
529
|
const opts = { method, headers: {} }
|
|
522
530
|
if (body) { opts.headers['Content-Type'] = 'application/json'; opts.body = JSON.stringify(body) }
|
|
523
|
-
const res = await fetch(path, opts)
|
|
531
|
+
const res = await fetch(apiPath(path), opts)
|
|
524
532
|
const data = await res.json().catch(() => ({}))
|
|
525
533
|
if (!res.ok) throw new Error(trHostError((data && (data.error || data.message))) || 'HTTP ' + res.status)
|
|
526
534
|
return data
|
|
@@ -530,7 +538,7 @@ window.__ModuleLoader__.load({
|
|
|
530
538
|
async function apiRaw(method, path, body) {
|
|
531
539
|
const opts = { method, headers: {} }
|
|
532
540
|
if (body) { opts.headers['Content-Type'] = 'application/json'; opts.body = JSON.stringify(body) }
|
|
533
|
-
const res = await fetch(path, opts)
|
|
541
|
+
const res = await fetch(apiPath(path), opts)
|
|
534
542
|
const data = await res.json().catch(() => ({}))
|
|
535
543
|
if (data && typeof data.error === 'string') data.error = trHostError(data.error)
|
|
536
544
|
if (data && typeof data.message === 'string') data.message = trHostError(data.message)
|
|
@@ -1483,11 +1491,177 @@ window.__ModuleLoader__.load({
|
|
|
1483
1491
|
React.createElement('path', { d: 'M3.5 1.5a1 1 0 0 0-1 1v11a1 1 0 0 0 1 1h9a1 1 0 0 0 1-1V5.2a1 1 0 0 0-.29-.7L10.5 1.79a1 1 0 0 0-.7-.29H3.5Zm1.5 3h2.25a.75.75 0 0 1 0 1.5H5a.75.75 0 0 1 0-1.5ZM5 9.5h6a.75.75 0 0 1 0 1.5H5a.75.75 0 0 1 0-1.5Zm0 2.25h4a.75.75 0 0 1 0 1.5H5a.75.75 0 0 1 0-1.5Z' }),
|
|
1484
1492
|
)
|
|
1485
1493
|
|
|
1486
|
-
//
|
|
1487
|
-
//
|
|
1488
|
-
//
|
|
1489
|
-
|
|
1490
|
-
|
|
1494
|
+
// Issue #32: the remote tree's expansion list travels on the TAB (`meta`),
|
|
1495
|
+
// never on better-sidebar's session-shared `expanded` array. Best-effort:
|
|
1496
|
+
// an older sidebar without tab meta simply forgets the expansion on reload.
|
|
1497
|
+
const TAB_EXPANDED_META = 'remoteExpanded'
|
|
1498
|
+
/** The tab's persisted expansion list, or null when the tab records none
|
|
1499
|
+
* (so a caller can tell "never expanded" from "expanded then collapsed
|
|
1500
|
+
* everything"). */
|
|
1501
|
+
function readTabExpanded(tab) {
|
|
1502
|
+
try {
|
|
1503
|
+
const meta = tab && tab.meta
|
|
1504
|
+
if (!meta || typeof meta !== 'object' || !(TAB_EXPANDED_META in meta)) return null
|
|
1505
|
+
const list = meta[TAB_EXPANDED_META]
|
|
1506
|
+
return Array.isArray(list) ? list.filter((p) => typeof p === 'string') : []
|
|
1507
|
+
} catch { return null }
|
|
1508
|
+
}
|
|
1509
|
+
/** Fallback store when the sidebar predates tab meta (no persistence then,
|
|
1510
|
+
* but the tree still opens and closes within the page's lifetime). */
|
|
1511
|
+
const tabExpandedFallback = new Map()
|
|
1512
|
+
function tabExpandedOf(tab) {
|
|
1513
|
+
const fromMeta = readTabExpanded(tab)
|
|
1514
|
+
if (fromMeta !== null) return fromMeta
|
|
1515
|
+
const id = tab && tab.id
|
|
1516
|
+
return (id && tabExpandedFallback.get(id)) || []
|
|
1517
|
+
}
|
|
1518
|
+
function toggleTabExpanded(ctx, tab, path) {
|
|
1519
|
+
const cur = tabExpandedOf(tab)
|
|
1520
|
+
const next = cur.includes(path) ? cur.filter((p) => p !== path) : [...cur, path]
|
|
1521
|
+
const id = tab && tab.id
|
|
1522
|
+
if (id) tabExpandedFallback.set(id, next)
|
|
1523
|
+
try {
|
|
1524
|
+
if (!ctx || !tab || !id || typeof ctx.get !== 'function') return next
|
|
1525
|
+
const bs = ctx.get('betterSidebar')
|
|
1526
|
+
if (!bs || typeof bs.updateTab !== 'function') return next
|
|
1527
|
+
const meta = tab.meta && typeof tab.meta === 'object' ? tab.meta : {}
|
|
1528
|
+
bs.updateTab(id, { meta: { ...meta, [TAB_EXPANDED_META]: next } })
|
|
1529
|
+
} catch { /* expansion memory is best-effort */ }
|
|
1530
|
+
return next
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
// Issue #32 healing, kept pure so it is directly testable: an older
|
|
1534
|
+
// dsh-remote pushed REMOTE paths into better-sidebar's session-shared
|
|
1535
|
+
// `expanded` set through onToggleDir. That set is PERSISTED (localStorage
|
|
1536
|
+
// `dsh-sidebar:v1:<sessionId>`) and the built-in local Files tree loads
|
|
1537
|
+
// every entry through a LOCAL fs.realpath, so those remote paths kept
|
|
1538
|
+
// failing with ENOENT until the set was cleaned.
|
|
1539
|
+
//
|
|
1540
|
+
// Return exactly the entries this tree owns: the current remote root and
|
|
1541
|
+
// anything under it. That range is remote-only by construction (the root
|
|
1542
|
+
// comes from resolve-mirror), so a legitimate local directory cannot
|
|
1543
|
+
// match. A LOCAL MIRROR path is never evicted — on a host whose own home is
|
|
1544
|
+
// an ancestor of the remote root (a macOS host driving a macOS remote) the
|
|
1545
|
+
// mirror under $DSH_HOME/remote-workspaces would otherwise fall inside it.
|
|
1546
|
+
//
|
|
1547
|
+
// On a WINDOWS host the sweep is broader and complete: a POSIX-absolute
|
|
1548
|
+
// entry (`/home/...`) can never be a valid local path there (Windows paths
|
|
1549
|
+
// are drive- or UNC-rooted), which is exactly why the bug only bit Windows.
|
|
1550
|
+
// That also cleans pollution left by an earlier remote root, which no
|
|
1551
|
+
// per-root rule could find.
|
|
1552
|
+
function isPosixAbsolutePath(p) {
|
|
1553
|
+
return typeof p === 'string' && p.startsWith('/') && !p.startsWith('//')
|
|
1554
|
+
}
|
|
1555
|
+
function windowsHost() {
|
|
1556
|
+
try {
|
|
1557
|
+
const nav = typeof navigator !== 'undefined' ? navigator : null
|
|
1558
|
+
if (!nav) return false
|
|
1559
|
+
if (nav.userAgentData && typeof nav.userAgentData.platform === 'string') {
|
|
1560
|
+
return /win/i.test(nav.userAgentData.platform)
|
|
1561
|
+
}
|
|
1562
|
+
if (typeof nav.platform === 'string' && nav.platform) return /win/i.test(nav.platform)
|
|
1563
|
+
return typeof nav.userAgent === 'string' && /windows/i.test(nav.userAgent)
|
|
1564
|
+
} catch { return false }
|
|
1565
|
+
}
|
|
1566
|
+
function strayRemoteSharedPaths(root, shared, hostIsWindows = windowsHost()) {
|
|
1567
|
+
if (!Array.isArray(shared) || !shared.length) return []
|
|
1568
|
+
let prefix = ''
|
|
1569
|
+
if (typeof root === 'string' && root !== '' && root !== '/') prefix = root.replace(/[\\/]+$/, '')
|
|
1570
|
+
const isMirror = (p) => p.includes('/remote-workspaces/') || p.includes('\\remote-workspaces\\')
|
|
1571
|
+
return shared.filter((p) => {
|
|
1572
|
+
if (typeof p !== 'string' || p === '' || isMirror(p)) return false
|
|
1573
|
+
if (prefix !== '' && (p === prefix || p === root ||
|
|
1574
|
+
p.startsWith(prefix + '/') || p.startsWith(prefix + '\\'))) return true
|
|
1575
|
+
return hostIsWindows && isPosixAbsolutePath(p)
|
|
1576
|
+
})
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
// Issue #32: the remote tree must NOT use better-sidebar's `expanded` /
|
|
1580
|
+
// `onToggleDir` props. That pair is the SESSION-shared expansion set, which
|
|
1581
|
+
// the built-in local Files tree also reads and loads as local directories
|
|
1582
|
+
// through /sidebar/api fs.tree. Feeding it remote paths (/home/...) made the
|
|
1583
|
+
// core tree run a LOCAL fs.realpath on them; on win32 a POSIX path counts as
|
|
1584
|
+
// absolute (drive-rooted), so it resolved to `D:\home\...` and failed with
|
|
1585
|
+
// ENOENT. Own the expansion here and persist it on the tab instead.
|
|
1586
|
+
//
|
|
1587
|
+
// Because that shared set is PERSISTED, an already-affected user stays
|
|
1588
|
+
// broken across reloads, so the stray entries are also migrated eagerly (see
|
|
1589
|
+
// migratePollutedSidebarState) and healed again from the tab (below).
|
|
1590
|
+
const SIDEBAR_STATE_PREFIX = 'dsh-sidebar:v1:'
|
|
1591
|
+
/**
|
|
1592
|
+
* Strip remote paths a pre-fix dsh-remote left in PERSISTED sidebar state,
|
|
1593
|
+
* before the built-in Files tree loads them. Only entries that cannot be a
|
|
1594
|
+
* valid local path on this host are removed (see strayRemoteSharedPaths).
|
|
1595
|
+
* Best-effort: any storage failure just leaves the tab-level healing to do
|
|
1596
|
+
* the job on the next load.
|
|
1597
|
+
*
|
|
1598
|
+
* @param storage - a Storage-like object (window.localStorage).
|
|
1599
|
+
* @param hostIsWindows - overrides host detection (tests).
|
|
1600
|
+
* @returns how many stray entries were removed.
|
|
1601
|
+
*/
|
|
1602
|
+
function migratePollutedSidebarState(storage, hostIsWindows = windowsHost()) {
|
|
1603
|
+
if (!storage || typeof storage.length !== 'number') return 0
|
|
1604
|
+
let cleaned = 0
|
|
1605
|
+
try {
|
|
1606
|
+
const keys = []
|
|
1607
|
+
for (let i = 0; i < storage.length; i++) {
|
|
1608
|
+
const key = storage.key(i)
|
|
1609
|
+
if (key && key.startsWith(SIDEBAR_STATE_PREFIX)) keys.push(key)
|
|
1610
|
+
}
|
|
1611
|
+
for (const key of keys) {
|
|
1612
|
+
const raw = storage.getItem(key)
|
|
1613
|
+
if (typeof raw !== 'string' || raw === '') continue
|
|
1614
|
+
let parsed
|
|
1615
|
+
try { parsed = JSON.parse(raw) } catch { continue }
|
|
1616
|
+
if (parsed === null || typeof parsed !== 'object' || !Array.isArray(parsed.expanded)) continue
|
|
1617
|
+
const stray = strayRemoteSharedPaths('', parsed.expanded, hostIsWindows)
|
|
1618
|
+
if (!stray.length) continue
|
|
1619
|
+
const drop = new Set(stray)
|
|
1620
|
+
parsed.expanded = parsed.expanded.filter((p) => !drop.has(p))
|
|
1621
|
+
storage.setItem(key, JSON.stringify(parsed))
|
|
1622
|
+
cleaned += stray.length
|
|
1623
|
+
}
|
|
1624
|
+
} catch { /* storage unavailable: the tab-level healing still runs */ }
|
|
1625
|
+
return cleaned
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
// `evictStray` heals state an older dsh-remote already polluted: the tree
|
|
1629
|
+
// calls it once it knows its remote root, and it drops exactly those paths
|
|
1630
|
+
// from the shared set. It is a plain function (not an effect) so the rule is
|
|
1631
|
+
// directly testable.
|
|
1632
|
+
function RemoteExplorerTabOwned(props) {
|
|
1633
|
+
const [expanded, setExpanded] = React.useState(tabExpandedOf(props.tab))
|
|
1634
|
+
const sharedExpanded = Array.isArray(props.expanded) ? props.expanded : []
|
|
1635
|
+
const evictShared = typeof props.onToggleDir === 'function' ? props.onToggleDir : null
|
|
1636
|
+
// Heal without needing the remote root: on a Windows host a POSIX-absolute
|
|
1637
|
+
// entry can never be a valid local path, so the sweep is complete there.
|
|
1638
|
+
// (The root-scoped sweep for the session's own remote tree runs inside the
|
|
1639
|
+
// tab, which is where the resolved root becomes known.)
|
|
1640
|
+
const sharedKey = sharedExpanded.join('\u0000')
|
|
1641
|
+
React.useEffect(() => {
|
|
1642
|
+
if (!evictShared) return
|
|
1643
|
+
const stray = strayRemoteSharedPaths('', sharedKey ? sharedKey.split('\u0000') : [])
|
|
1644
|
+
for (const p of stray) evictShared(p)
|
|
1645
|
+
}, [sharedKey, evictShared])
|
|
1646
|
+
// Explicitly drop the framework's session-shared pair from the child.
|
|
1647
|
+
const owned = { ...props }
|
|
1648
|
+
delete owned.expanded
|
|
1649
|
+
delete owned.onToggleDir
|
|
1650
|
+
return React.createElement(RemoteExplorerTab, {
|
|
1651
|
+
...owned,
|
|
1652
|
+
evictStray: (root) => {
|
|
1653
|
+
if (!evictShared || !sharedExpanded.length) return []
|
|
1654
|
+
const stray = strayRemoteSharedPaths(root, sharedExpanded)
|
|
1655
|
+
for (const p of stray) evictShared(p)
|
|
1656
|
+
return stray
|
|
1657
|
+
},
|
|
1658
|
+
expanded,
|
|
1659
|
+
onToggleDir: (path) => setExpanded(toggleTabExpanded(props.ctx, props.tab, path)),
|
|
1660
|
+
})
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
// Tree-style remote file explorer. Data is listed live from the remote host
|
|
1664
|
+
// over /dsh-remote/ls, rooted at this session's remote workspace dir.
|
|
1491
1665
|
function RemoteExplorerTab(props) {
|
|
1492
1666
|
const [status, setStatus] = React.useState(null)
|
|
1493
1667
|
const [root, setRoot] = React.useState('') // remote root path for this session
|
|
@@ -1619,6 +1793,13 @@ window.__ModuleLoader__.load({
|
|
|
1619
1793
|
React.useEffect(() => { if (props.visible) refreshStatus({ visible: true }) }, [props.visible])
|
|
1620
1794
|
// Load dirs as they get expanded (keep already-loaded content).
|
|
1621
1795
|
React.useEffect(() => { if (root) for (const dir of expanded) loadDir(dir, { keep: true }) }, [root, expanded, loadDir])
|
|
1796
|
+
// Issue #32 healing: once the remote root is known, drop the paths an
|
|
1797
|
+
// older dsh-remote left in better-sidebar's PERSISTED session-shared
|
|
1798
|
+
// expansion set (the built-in local tree was loading them through a local
|
|
1799
|
+
// fs.realpath and failing). See strayRemoteSharedPaths().
|
|
1800
|
+
React.useEffect(() => {
|
|
1801
|
+
if (root && typeof props.evictStray === 'function') props.evictStray(root)
|
|
1802
|
+
}, [root, props.evictStray])
|
|
1622
1803
|
// ── Context-menu portal: append to document.body so the sidebar's
|
|
1623
1804
|
// overflow/transform cannot clip it. Pure DOM, no React portal needed.
|
|
1624
1805
|
React.useEffect(() => {
|
|
@@ -1652,7 +1833,8 @@ window.__ModuleLoader__.load({
|
|
|
1652
1833
|
|
|
1653
1834
|
const openFile = (p) => {
|
|
1654
1835
|
const name = String(p).split(/[\\/]/).pop() || p
|
|
1655
|
-
props.
|
|
1836
|
+
if (props.onOpenFile) props.onOpenFile(p)
|
|
1837
|
+
else props.ctx.betterSidebar.openTab({ type: SIDEBAR_FILE_ID, title: name, path: p }, props.scope)
|
|
1656
1838
|
}
|
|
1657
1839
|
|
|
1658
1840
|
const doFs = (op, payload, refreshParent) => {
|
|
@@ -1908,6 +2090,13 @@ window.__ModuleLoader__.load({
|
|
|
1908
2090
|
function registerSidebarIntegration(ctx) {
|
|
1909
2091
|
const bs = (ctx && ctx.get && ctx.get('betterSidebar')) || null
|
|
1910
2092
|
if (!bs || typeof bs.registerTab !== 'function') return
|
|
2093
|
+
// Issue #32 migration: drop the remote paths an older dsh-remote left in
|
|
2094
|
+
// PERSISTED sidebar state, BEFORE the built-in Files tree loads them
|
|
2095
|
+
// through its local fs guard. Runs once per activation; a failure is
|
|
2096
|
+
// harmless because the tab-level healing repeats it.
|
|
2097
|
+
try {
|
|
2098
|
+
migratePollutedSidebarState(typeof window !== 'undefined' ? window.localStorage : null)
|
|
2099
|
+
} catch { /* best-effort migration */ }
|
|
1911
2100
|
const disposers = []
|
|
1912
2101
|
try {
|
|
1913
2102
|
disposers.push(bs.registerTab({
|
|
@@ -1916,7 +2105,8 @@ window.__ModuleLoader__.load({
|
|
|
1916
2105
|
icon: (size) => React.createElement('span', { style: { fontSize: (size || 14), lineHeight: 1 } }, '🌐'),
|
|
1917
2106
|
order: 55,
|
|
1918
2107
|
single: true,
|
|
1919
|
-
|
|
2108
|
+
// Issue #32: own the expansion state (see RemoteExplorerTabOwned).
|
|
2109
|
+
component: RemoteExplorerTabOwned,
|
|
1920
2110
|
}))
|
|
1921
2111
|
disposers.push(bs.registerTab({
|
|
1922
2112
|
id: SIDEBAR_FILE_ID,
|
|
@@ -1960,6 +2150,56 @@ window.__ModuleLoader__.load({
|
|
|
1960
2150
|
return () => disposers.forEach((d) => { try { d() } catch {} })
|
|
1961
2151
|
}
|
|
1962
2152
|
|
|
2153
|
+
// Official Desktop's right-sidebar extension contract. Reuse the SSH
|
|
2154
|
+
// explorer/editor rather than replacing core Files or pretending remote
|
|
2155
|
+
// paths are local filesystem resources. Each address is session-scoped.
|
|
2156
|
+
const NATIVE_EXPLORER = 'dsh-remote/explorer'
|
|
2157
|
+
const NATIVE_FILE = 'dsh-remote/file'
|
|
2158
|
+
function remoteFileAddress(sessionId, path) {
|
|
2159
|
+
return 'dsh-resource://dsh-remote/' + encodeURIComponent(sessionId) + '/' + encodeURIComponent(path)
|
|
2160
|
+
}
|
|
2161
|
+
function remoteFilePath(address) {
|
|
2162
|
+
const url = new URL(address)
|
|
2163
|
+
if (url.protocol !== 'dsh-resource:' || url.hostname !== 'dsh-remote') throw new Error('Invalid remote resource')
|
|
2164
|
+
return decodeURIComponent(url.pathname.split('/').slice(2).join('/'))
|
|
2165
|
+
}
|
|
2166
|
+
function NativeExplorer(props) {
|
|
2167
|
+
const info = props.useTabInfo()
|
|
2168
|
+
const [expanded, setExpanded] = React.useState([])
|
|
2169
|
+
return React.createElement(RemoteExplorerTab, {
|
|
2170
|
+
scope: { sessionId: props.sessionId }, visible: info.tab.visible,
|
|
2171
|
+
expanded,
|
|
2172
|
+
onToggleDir: (path) => setExpanded((old) => old.includes(path) ? old.filter((p) => p !== path) : [...old, path]),
|
|
2173
|
+
onOpenFile: (path) => info.tab.actions.openResource(remoteFileAddress(props.sessionId, path)),
|
|
2174
|
+
})
|
|
2175
|
+
}
|
|
2176
|
+
function NativeFile(props) {
|
|
2177
|
+
const info = props.useTabInfo()
|
|
2178
|
+
const path = remoteFilePath(info.tab.navigation.address)
|
|
2179
|
+
return React.createElement(RemoteFileTab, { key: path, tab: { path }, scope: { sessionId: props.sessionId } })
|
|
2180
|
+
}
|
|
2181
|
+
function registerNativeSidebar(inner) {
|
|
2182
|
+
const tabs = inner.get('sidebarRightTabs')
|
|
2183
|
+
const slots = inner.get('slots')
|
|
2184
|
+
inner.effect(() => tabs.register({
|
|
2185
|
+
id: NATIVE_EXPLORER, kind: NATIVE_EXPLORER,
|
|
2186
|
+
title: () => tr('explorer.tabTitle'),
|
|
2187
|
+
guide: [{ order: 55, title: () => tr('explorer.tabTitle') }],
|
|
2188
|
+
}), 'dsh-remote.native-explorer-type')
|
|
2189
|
+
inner.effect(() => tabs.register({
|
|
2190
|
+
id: NATIVE_FILE, kind: NATIVE_FILE,
|
|
2191
|
+
patterns: ['dsh-resource://dsh-remote/**'],
|
|
2192
|
+
title: (address) => remoteFilePath(address).split(/[\\/]/).pop() || tr('explorer.tabTitle'),
|
|
2193
|
+
}), 'dsh-remote.native-file-type')
|
|
2194
|
+
inner.effect(() => slots.inject('sidebar.right.pane.tab', () => {
|
|
2195
|
+
const disposers = [
|
|
2196
|
+
slots.register({ name: 'sidebar.right.pane.tab', key: NATIVE_EXPLORER }, NativeExplorer),
|
|
2197
|
+
slots.register({ name: 'sidebar.right.pane.tab', key: NATIVE_FILE }, NativeFile),
|
|
2198
|
+
]
|
|
2199
|
+
return () => disposers.forEach((dispose) => dispose())
|
|
2200
|
+
}), 'dsh-remote.native-bodies')
|
|
2201
|
+
}
|
|
2202
|
+
|
|
1963
2203
|
function apply(ctx) {
|
|
1964
2204
|
const slots = ctx.get('slots')
|
|
1965
2205
|
wireLocale(ctx)
|
|
@@ -1983,6 +2223,7 @@ window.__ModuleLoader__.load({
|
|
|
1983
2223
|
ctx.inject(['betterSidebar'], (inner) => {
|
|
1984
2224
|
inner.effect(() => registerSidebarIntegration(inner), 'dsh-remote.betterSidebar')
|
|
1985
2225
|
})
|
|
2226
|
+
ctx.inject(['sidebarRightTabs'], registerNativeSidebar)
|
|
1986
2227
|
}
|
|
1987
2228
|
|
|
1988
2229
|
exports.name = name
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// The routes in index.js are bounded JSON handlers, not arbitrary HTTP proxy
|
|
2
|
+
// handlers. Keep their implementation shared with legacy Web hosts while using
|
|
3
|
+
// Connection's authenticated, carrier-neutral channel on portless Desktop.
|
|
4
|
+
import { Readable } from 'node:stream'
|
|
5
|
+
|
|
6
|
+
const MAX_BODY_BYTES = 1024 * 1024
|
|
7
|
+
|
|
8
|
+
export function connectionRoute(route) {
|
|
9
|
+
// Only the prefix is a real invariant here. `kind`/`exact` belongs to
|
|
10
|
+
// dsh-host-webserver's registry (WebRouteKind); Connection's
|
|
11
|
+
// ConnectionFetchRoute has no `kind` field and assertFetchRoute never reads
|
|
12
|
+
// one, so requiring it would let a future route that omits it silently lose
|
|
13
|
+
// itself and every route registered after it.
|
|
14
|
+
if (!route.path.startsWith('/dsh-remote/')) {
|
|
15
|
+
throw new Error('dsh-remote: expected a plugin route under /dsh-remote/')
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
path: '/api' + route.path,
|
|
19
|
+
methods: ['GET', 'POST'],
|
|
20
|
+
requestBody: 'buffered',
|
|
21
|
+
async fetch(request) {
|
|
22
|
+
const chunks = []
|
|
23
|
+
let size = 0
|
|
24
|
+
// Desktop's IPC carrier need not impose the Web carrier's JSON cap.
|
|
25
|
+
// Enforce our own cap before dispatch: oversized input must never become
|
|
26
|
+
// an empty object that could accidentally trigger a default operation.
|
|
27
|
+
const reader = request.body?.getReader()
|
|
28
|
+
try {
|
|
29
|
+
if (reader) {
|
|
30
|
+
while (true) {
|
|
31
|
+
request.signal.throwIfAborted()
|
|
32
|
+
const { done, value } = await reader.read()
|
|
33
|
+
if (done) break
|
|
34
|
+
size += value.byteLength
|
|
35
|
+
if (size > MAX_BODY_BYTES) {
|
|
36
|
+
await reader.cancel()
|
|
37
|
+
return Response.json({ ok: false, error: 'request body too large' }, { status: 413 })
|
|
38
|
+
}
|
|
39
|
+
chunks.push(Buffer.from(value))
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
} finally {
|
|
43
|
+
reader?.releaseLock()
|
|
44
|
+
}
|
|
45
|
+
request.signal.throwIfAborted()
|
|
46
|
+
const url = new URL(request.url)
|
|
47
|
+
// The legacy JSON reader joins chunks as strings. Supply one bounded
|
|
48
|
+
// buffer so UTF-8 characters split by the IPC stream remain intact.
|
|
49
|
+
const req = Readable.from(size ? [Buffer.concat(chunks, size)] : [])
|
|
50
|
+
req.method = request.method
|
|
51
|
+
req.url = route.path + url.search
|
|
52
|
+
const headers = new Headers()
|
|
53
|
+
let response
|
|
54
|
+
const res = {
|
|
55
|
+
statusCode: 200,
|
|
56
|
+
setHeader(key, value) { headers.set(key, value) },
|
|
57
|
+
end(body) { response = new Response(body, { status: this.statusCode, headers }) },
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
await route.handler(req, res)
|
|
61
|
+
if (!response) throw new Error('JSON handler did not finish its response')
|
|
62
|
+
return response
|
|
63
|
+
} finally {
|
|
64
|
+
req.destroy()
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function registerHttpTransports(ctx, routes) {
|
|
71
|
+
// Reactive injection matters: either service can arrive after the plugin.
|
|
72
|
+
// Neither optional transport is a hard prerequisite for the SSH tools.
|
|
73
|
+
ctx.inject(['webServer'], (inner) => {
|
|
74
|
+
const disposers = routes.map((route) => inner.get('webServer').register(route))
|
|
75
|
+
inner.effect(() => () => disposers.forEach((dispose) => dispose()), 'dsh-remote.web-routes')
|
|
76
|
+
})
|
|
77
|
+
ctx.inject(['connection'], (inner) => {
|
|
78
|
+
const connection = inner.get('connection')
|
|
79
|
+
// Older Web compositions may have Connection without exact Fetch routes.
|
|
80
|
+
// dsh < 0.1.2-rc.1 is such a composition: the Desktop transport is simply
|
|
81
|
+
// absent there, and the legacy /dsh-remote/* routes above still serve.
|
|
82
|
+
if (typeof connection.fetch?.register !== 'function') return
|
|
83
|
+
// Register one route at a time. A single failing route must not abort the
|
|
84
|
+
// rest of the list (which would silently drop every later route) nor leak
|
|
85
|
+
// the routes already registered, because this callback is a ctx.inject
|
|
86
|
+
// child fiber: its throw is caught and logged by the loader, so the parent
|
|
87
|
+
// plugin stays ACTIVE and the failure is easy to miss.
|
|
88
|
+
const disposers = []
|
|
89
|
+
const failed = []
|
|
90
|
+
for (const route of routes) {
|
|
91
|
+
try {
|
|
92
|
+
disposers.push(connection.fetch.register(connectionRoute(route)))
|
|
93
|
+
} catch (err) {
|
|
94
|
+
failed.push(route.path + ': ' + String((err && err.message) || err))
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
inner.effect(() => () => Promise.all(disposers.map((dispose) => dispose())), 'dsh-remote.fetch-routes')
|
|
98
|
+
if (failed.length) {
|
|
99
|
+
// Surface a partial registration instead of swallowing it; the Web
|
|
100
|
+
// transport is unaffected, so this is a warning rather than a failure.
|
|
101
|
+
console.warn('[dsh-remote] some Connection Fetch routes did not register:', failed.join('; '))
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -44,15 +44,15 @@ import { TaskManager } from './tasks.js'
|
|
|
44
44
|
import { ForwardManager } from './forwards.js'
|
|
45
45
|
import { selfDir, readVersion, gtVersion, fetchLatestVersion, applyUpdate, persistUpdateMode, readUpdateMode } from './update.js'
|
|
46
46
|
import { loadMachines as _loadMachines, saveMachines as _saveMachines, sanitizeMachine as _sanitizeMachine, applyMachine as _applyMachine, machineId as _machineId } from './registry.js'
|
|
47
|
+
import { registerHttpTransports } from './http-transport.js'
|
|
47
48
|
|
|
48
49
|
const { Client } = ssh2
|
|
49
50
|
|
|
50
51
|
export const name = 'dsh-remote'
|
|
51
52
|
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
export const inject = ['tools', 'systemPrompt', 'webServer']
|
|
53
|
+
// Web and Desktop share the SSH tools; each optional UI transport is attached
|
|
54
|
+
// reactively below, so service startup order does not drop the JSON routes.
|
|
55
|
+
export const inject = ['tools', 'systemPrompt']
|
|
56
56
|
|
|
57
57
|
export const Config = z.object({
|
|
58
58
|
/** Remote SSH host (empty → the plugin starts disconnected). */
|
|
@@ -2300,9 +2300,6 @@ export async function apply(ctx, config) {
|
|
|
2300
2300
|
}
|
|
2301
2301
|
|
|
2302
2302
|
// ── JSON endpoints for settings UI ─────────────────────────────────────────
|
|
2303
|
-
const webServer = ctx.get('webServer')
|
|
2304
|
-
if (webServer === undefined) return
|
|
2305
|
-
|
|
2306
2303
|
const sendJson = (res, status, body) => {
|
|
2307
2304
|
res.statusCode = status
|
|
2308
2305
|
res.setHeader('Content-Type', 'application/json; charset=utf-8')
|
|
@@ -3264,8 +3261,7 @@ export async function apply(ctx, config) {
|
|
|
3264
3261
|
},
|
|
3265
3262
|
]
|
|
3266
3263
|
|
|
3267
|
-
|
|
3268
|
-
ctx.effect(() => () => disposers.forEach((d) => d && d()), 'dsh-remote.routes')
|
|
3264
|
+
registerHttpTransports(ctx, routes)
|
|
3269
3265
|
|
|
3270
3266
|
// ── auto-update: check on load + on an interval; apply silently ───────────
|
|
3271
3267
|
// Failures are swallowed (never break the plugin). The persisted override
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-remote",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.17",
|
|
4
4
|
"description": "Remote-work assistant for DeepSeek Harness: connect SSH (password/key/agent/keyboard-interactive/proxy jump), pick a remote workspace, operate on it with 21 rw_* tools (incl. rw_edit/rw_stat/rw_mkdir/rw_remove/rw_move/rw_forward), conflict-aware mirror sync, port forwarding, side-bar remote file editing, audit log, keychain passwords.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "flymysql <flyphp@outlook.com>",
|
|
@@ -52,8 +52,17 @@
|
|
|
52
52
|
"@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.6",
|
|
53
53
|
"@deepseek-ai/dsh-client-ui-renderer": "^0.1.2-rc.1",
|
|
54
54
|
"@deepseek-ai/dsh-client-locale": "^0.1.2-rc.1",
|
|
55
|
+
"@deepseek-ai/dsh-client-connection": "^0.1.2-rc.1",
|
|
55
56
|
"@deepseek-ai/cordis": "^4.0.1"
|
|
56
57
|
},
|
|
58
|
+
"peerDependenciesMeta": {
|
|
59
|
+
"@deepseek-ai/dsh-host-webserver": {
|
|
60
|
+
"optional": true
|
|
61
|
+
},
|
|
62
|
+
"@deepseek-ai/dsh-client-connection": {
|
|
63
|
+
"optional": true
|
|
64
|
+
}
|
|
65
|
+
},
|
|
57
66
|
"dependencies": {
|
|
58
67
|
"ssh2": "^1.16.0",
|
|
59
68
|
"@deepseek-ai/schemastery": "^3.18.1",
|