dsh-remote 0.8.15 → 0.8.16

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
@@ -58,6 +58,41 @@ Real capture (host scrubbed to a placeholder):
58
58
 
59
59
  ## Install
60
60
 
61
+ ### Official Desktop compatibility (experimental, unreleased)
62
+
63
+ This branch adds a compatibility path for the **official**
64
+ [DeepSeek Harness Desktop](https://github.com/deepseek-ai/deepseek-harness),
65
+ tested against the `0.1.5-rc.2` Host transport. It does not replace the Harness
66
+ core or require a listening Web server:
67
+
68
+ - The SSH settings and directory picker use `/api/dsh-remote/*` over the
69
+ Desktop's `dsh-app:` carrier. Exact Fetch routes are registered on
70
+ `ctx.connection.fetch`; the carrier retains ownership of authentication.
71
+ - A native **Remote Files** entry uses `sidebarRightTabs` and the keyed
72
+ `sidebar.right.pane.tab` seat. It reuses the existing explorer/editor and
73
+ gives remote files their own session-scoped resource addresses, rather than
74
+ sending remote paths to the local Files viewer.
75
+ - When the core Web-server row is explicitly disabled (the official Desktop
76
+ composition), the bundled `dsh-better-sidebar` row stays disabled. Web hosts
77
+ retain the existing `/dsh-remote/*` routes and sidebar composition, including
78
+ the standalone-sidebar deduplication guard.
79
+
80
+ Validation so far covers Host startup, Desktop IPC JSON requests, read-only
81
+ SSH connection/list/read, and opening the settings/import UI. Native file-tab
82
+ navigation, editing/sync, concurrent sessions on different machines, and the
83
+ full legacy Web UI still need end-to-end acceptance before release. In
84
+ particular, existing sidebar file endpoints use the active-machine pool;
85
+ session-scoped tab addresses alone do **not** make those endpoints
86
+ session-bound. This is not a claim of production-ready multi-machine Desktop
87
+ support.
88
+
89
+ Desktop's package installer may also require an explicit policy for the
90
+ optional `ssh2` / `cpu-features` build scripts. The isolated transport test
91
+ disabled those optional scripts; this change does not loosen an application's
92
+ build allowlist or automatically approve dependency scripts.
93
+
94
+ ### Published Web bundle
95
+
61
96
  ```bash
62
97
  dsh plugin add dsh-remote # add the bundle
63
98
  ```
@@ -79,6 +114,24 @@ after** `dsh-remote` in `dsh.profile.bundles` (order-independent guard since
79
114
  > `import { settingsNamespace } from "@deepseek-ai/dsh-settings"` broke once
80
115
  > dsh-settings 0.1.2-alpha.2 made that symbol private; issue #29).
81
116
 
117
+ > **Harness requirement of the embedded sidebar (0.8.15+): `dsh ≥ 0.1.2-rc.1`.**
118
+ > `dsh-better-sidebar` 0.18.x imports `SessionLogOffset` from
119
+ > `@deepseek-ai/dsh-session`, which only exists from 0.1.2-rc.1 on. On an older
120
+ > harness (0.1.0-rc.x) that import fails and the loader aborts the whole plugin
121
+ > tree, so dsh does not start at all. Verified: 0.8.14 + sidebar 0.14.0 boots on
122
+ > 0.1.0-rc.8, while 0.8.15 + sidebar 0.18.1 does not; both boot on 0.1.2-rc.1.
123
+ > On an older harness either stay on **0.8.14**, or keep the host half by
124
+ > disabling the embedded sidebar row in your profile's `cordis.patch.yml`
125
+ > (verified working on 0.1.0-rc.8 — the `rw_*` tools keep working, the sidebar
126
+ > UI is what you give up):
127
+ >
128
+ > ```yaml
129
+ > - id: dsh-remote-sidebar
130
+ > disabled: true
131
+ > ```
132
+ >
133
+ > (and do not list a standalone `dsh-better-sidebar` bundle either).
134
+
82
135
  > **Requires the profile's pnpm linker to be `hoisted`** (the DSH profile
83
136
  > default, `nodeLinker: hoisted` in `pnpm-workspace.yaml`). The loader resolves
84
137
  > plugin packages from the profile root, so the sidebar must be reachable in
@@ -234,4 +287,4 @@ MIT
234
287
 
235
288
  ## Changelog
236
289
 
237
- See [CHANGELOG.md](./CHANGELOG.md).
290
+ See [CHANGELOG.md](./CHANGELOG.md).
package/README.zh.md CHANGED
@@ -4,6 +4,26 @@
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
  [![npm version](https://img.shields.io/npm/v/dsh-remote)](https://www.npmjs.com/package/dsh-remote)
8
28
  [![license](https://img.shields.io/github/license/flymysql/dsh-remote)](LICENSE)
9
29
  [![dsh-plugin](https://img.shields.io/badge/topic-dsh--plugin-7a)](https://github.com/topics/dsh-plugin)
@@ -65,6 +85,21 @@ dsh plugin add dsh-remote # 添加 bundle
65
85
  > 该导出自 dsh-settings 0.1.2-alpha.2 起被移除,静态导入失败会让整个插件树加载失败
66
86
  > (issue #29)。
67
87
 
88
+ > **⚠ 内嵌侧边栏对 harness 的要求(0.8.15+):`dsh ≥ 0.1.2-rc.1`**。
89
+ > `dsh-better-sidebar` 0.18.x 会 `import { SessionLogOffset } from "@deepseek-ai/dsh-session"`,
90
+ > 该导出从 0.1.2-rc.1 才有。在更老的 harness(0.1.0-rc.x)上这个静态导入会失败,
91
+ > loader 因此判定整个插件树加载失败 —— **dsh 直接起不来**。实测:`0.8.14 + 侧边栏 0.14.0`
92
+ > 在 0.1.0-rc.8 上可以正常启动,`0.8.15 + 侧边栏 0.18.1` 不行;两者在 0.1.2-rc.1 上都正常。
93
+ > 老 harness 上要么**留在 0.8.14**,要么在 profile 的 `cordis.patch.yml` 里关掉内嵌侧边栏行
94
+ > 以保住 host 半(已在 0.1.0-rc.8 实测可启动,`rw_*` 工具照常可用,放弃的只是侧边栏 UI):
95
+ >
96
+ > ```yaml
97
+ > - id: dsh-remote-sidebar
98
+ > disabled: true
99
+ > ```
100
+ >
101
+ > (同时不要再单独列出 `dsh-better-sidebar` bundle。)
102
+
68
103
  > **要求 profile 的 pnpm linker 为 `hoisted`**(DSH profile 默认,
69
104
  > `pnpm-workspace.yaml` 里 `nodeLinker: hoisted`)。loader 从 profile 根解析
70
105
  > 插件包,侧边栏必须能在顶层 `node_modules` 被解析到。如果你的
@@ -193,4 +228,4 @@ MIT
193
228
 
194
229
  ## 变更记录
195
230
 
196
- 见 [CHANGELOG.md](./CHANGELOG.md)。
231
+ 见 [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. The sidebar row uses a DEDICATED id (`dsh-remote-sidebar`)
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)
@@ -1652,7 +1660,8 @@ window.__ModuleLoader__.load({
1652
1660
 
1653
1661
  const openFile = (p) => {
1654
1662
  const name = String(p).split(/[\\/]/).pop() || p
1655
- props.ctx.betterSidebar.openTab({ type: SIDEBAR_FILE_ID, title: name, path: p }, props.scope)
1663
+ if (props.onOpenFile) props.onOpenFile(p)
1664
+ else props.ctx.betterSidebar.openTab({ type: SIDEBAR_FILE_ID, title: name, path: p }, props.scope)
1656
1665
  }
1657
1666
 
1658
1667
  const doFs = (op, payload, refreshParent) => {
@@ -1960,6 +1969,56 @@ window.__ModuleLoader__.load({
1960
1969
  return () => disposers.forEach((d) => { try { d() } catch {} })
1961
1970
  }
1962
1971
 
1972
+ // Official Desktop's right-sidebar extension contract. Reuse the SSH
1973
+ // explorer/editor rather than replacing core Files or pretending remote
1974
+ // paths are local filesystem resources. Each address is session-scoped.
1975
+ const NATIVE_EXPLORER = 'dsh-remote/explorer'
1976
+ const NATIVE_FILE = 'dsh-remote/file'
1977
+ function remoteFileAddress(sessionId, path) {
1978
+ return 'dsh-resource://dsh-remote/' + encodeURIComponent(sessionId) + '/' + encodeURIComponent(path)
1979
+ }
1980
+ function remoteFilePath(address) {
1981
+ const url = new URL(address)
1982
+ if (url.protocol !== 'dsh-resource:' || url.hostname !== 'dsh-remote') throw new Error('Invalid remote resource')
1983
+ return decodeURIComponent(url.pathname.split('/').slice(2).join('/'))
1984
+ }
1985
+ function NativeExplorer(props) {
1986
+ const info = props.useTabInfo()
1987
+ const [expanded, setExpanded] = React.useState([])
1988
+ return React.createElement(RemoteExplorerTab, {
1989
+ scope: { sessionId: props.sessionId }, visible: info.tab.visible,
1990
+ expanded,
1991
+ onToggleDir: (path) => setExpanded((old) => old.includes(path) ? old.filter((p) => p !== path) : [...old, path]),
1992
+ onOpenFile: (path) => info.tab.actions.openResource(remoteFileAddress(props.sessionId, path)),
1993
+ })
1994
+ }
1995
+ function NativeFile(props) {
1996
+ const info = props.useTabInfo()
1997
+ const path = remoteFilePath(info.tab.navigation.address)
1998
+ return React.createElement(RemoteFileTab, { key: path, tab: { path }, scope: { sessionId: props.sessionId } })
1999
+ }
2000
+ function registerNativeSidebar(inner) {
2001
+ const tabs = inner.get('sidebarRightTabs')
2002
+ const slots = inner.get('slots')
2003
+ inner.effect(() => tabs.register({
2004
+ id: NATIVE_EXPLORER, kind: NATIVE_EXPLORER,
2005
+ title: () => tr('explorer.tabTitle'),
2006
+ guide: [{ order: 55, title: () => tr('explorer.tabTitle') }],
2007
+ }), 'dsh-remote.native-explorer-type')
2008
+ inner.effect(() => tabs.register({
2009
+ id: NATIVE_FILE, kind: NATIVE_FILE,
2010
+ patterns: ['dsh-resource://dsh-remote/**'],
2011
+ title: (address) => remoteFilePath(address).split(/[\\/]/).pop() || tr('explorer.tabTitle'),
2012
+ }), 'dsh-remote.native-file-type')
2013
+ inner.effect(() => slots.inject('sidebar.right.pane.tab', () => {
2014
+ const disposers = [
2015
+ slots.register({ name: 'sidebar.right.pane.tab', key: NATIVE_EXPLORER }, NativeExplorer),
2016
+ slots.register({ name: 'sidebar.right.pane.tab', key: NATIVE_FILE }, NativeFile),
2017
+ ]
2018
+ return () => disposers.forEach((dispose) => dispose())
2019
+ }), 'dsh-remote.native-bodies')
2020
+ }
2021
+
1963
2022
  function apply(ctx) {
1964
2023
  const slots = ctx.get('slots')
1965
2024
  wireLocale(ctx)
@@ -1983,6 +2042,7 @@ window.__ModuleLoader__.load({
1983
2042
  ctx.inject(['betterSidebar'], (inner) => {
1984
2043
  inner.effect(() => registerSidebarIntegration(inner), 'dsh-remote.betterSidebar')
1985
2044
  })
2045
+ ctx.inject(['sidebarRightTabs'], registerNativeSidebar)
1986
2046
  }
1987
2047
 
1988
2048
  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
- // tools + systemPrompt + webServer are required. webServer is INJECTED (not just
53
- // lazily read) so this plugin activates only after the web server is up — otherwise
54
- // apply() runs ahead of webServer and the /dsh-remote/* JSON routes never register.
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
- const disposers = routes.map((r) => webServer.register(r))
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.15",
3
+ "version": "0.8.16",
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",