dsh-remote 0.8.72 → 0.8.74

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/LICENSE +21 -21
  2. package/README.en.md +470 -389
  3. package/README.md +529 -462
  4. package/cordis.patch.yml +9 -9
  5. package/lib/binding.js +172 -172
  6. package/lib/client.js +4039 -4022
  7. package/lib/credential.js +263 -263
  8. package/lib/errors.js +141 -141
  9. package/lib/exec-job.js +235 -235
  10. package/lib/file-reference.js +457 -457
  11. package/lib/forwards.js +258 -258
  12. package/lib/host-api.js +808 -0
  13. package/lib/hostkey.js +108 -108
  14. package/lib/http-transport.js +196 -196
  15. package/lib/ignore.js +202 -202
  16. package/lib/index.js +4923 -4541
  17. package/lib/instructions-dynamic.js +415 -415
  18. package/lib/openssh.js +879 -879
  19. package/lib/paths.js +252 -252
  20. package/lib/pool.js +963 -893
  21. package/lib/registry.js +296 -293
  22. package/lib/remote-fs.js +173 -173
  23. package/lib/remote-sandbox.js +183 -183
  24. package/lib/remote-shell-child.cjs +344 -305
  25. package/lib/remote-shell.js +542 -518
  26. package/lib/routes-fs.js +245 -245
  27. package/lib/routes-term.js +169 -169
  28. package/lib/search.js +235 -235
  29. package/lib/sftp-surface.js +119 -119
  30. package/lib/sshconfig.js +779 -705
  31. package/lib/sync.js +275 -275
  32. package/lib/tasks.js +95 -95
  33. package/lib/telemetry.js +213 -213
  34. package/lib/terminal.js +205 -205
  35. package/lib/tui/flow.js +130 -0
  36. package/lib/tui/format.js +332 -0
  37. package/lib/tui/i18n.js +344 -0
  38. package/lib/tui/index.js +322 -0
  39. package/lib/tui/provider.js +327 -0
  40. package/lib/tui/scene.js +996 -0
  41. package/lib/update.js +472 -472
  42. package/lib/web-attach.js +850 -850
  43. package/lib/workspace-cleanup.js +337 -0
  44. package/package.json +100 -99
@@ -0,0 +1,337 @@
1
+ // Local-mirror workspace cleanup — the filesystem half of the TUI's
2
+ // "彻底删除工作区 / 只删除对话" actions.
3
+ //
4
+ // Two rules drive everything here:
5
+ // 1. NEVER delete outside `$DSH_HOME/remote-workspaces/<host>-<user>-<port>/<name>`.
6
+ // The mirror root is the only tree this module accepts, the target must be
7
+ // exactly two levels below it, and its `.dsh-remote-meta.json` must parse —
8
+ // a symlinked or half-typed path is refused instead of guessed at.
9
+ // 2. Session logs live at `$DSH_HOME/sessions/<project-key(cwd)>/<encoded id>`
10
+ // (the layout of `@deepseek-ai/dsh-session-persistence-jsonl`). The key and
11
+ // segment encoders below mirror that package's `projectKey()` /
12
+ // `encodeSegment()` so this module can find the directories DSH itself
13
+ // writes. They are pure and covered by tests; if DSH ever changes the
14
+ // layout, those tests name the drift instead of silently deleting nothing.
15
+ //
16
+ // Deletion is intentionally synchronous: the caller runs it from a maintenance
17
+ // action (not a hot path), and it must finish before the UI reports success.
18
+
19
+ import { existsSync, readdirSync, readFileSync, realpathSync, rmdirSync, rmSync, statSync } from 'node:fs'
20
+ import path from 'node:path'
21
+
22
+ /** Name of the per-mirror binding file (`ensureMirror()` writes it). */
23
+ export const MIRROR_META_FILE = '.dsh-remote-meta.json'
24
+
25
+ /** Encode an arbitrary string as one path segment (mirror of DSH's encoder).
26
+ *
27
+ * Safe code units stay literal; everything else (including `~`) becomes
28
+ * `~XXXX` (UTF-16 code unit, uppercase hex, at least four digits). `.`/`..`
29
+ * are special-cased so a segment can never traverse.
30
+ *
31
+ * @param {string} raw - non-empty string (session id).
32
+ * @returns {string} a single filesystem-safe segment.
33
+ */
34
+ export function encodeSegment(raw) {
35
+ const value = String(raw === undefined || raw === null ? '' : raw)
36
+ if (value.length === 0) throw new Error('cannot encode an empty path segment')
37
+ if (value === '.') return '~002E'
38
+ if (value === '..') return '~002E~002E'
39
+ let out = ''
40
+ for (let i = 0; i < value.length; i += 1) {
41
+ const code = value.charCodeAt(i)
42
+ const ch = String.fromCharCode(code)
43
+ if (ch !== '~' && /^[A-Za-z0-9._-]$/.test(ch)) out += ch
44
+ else out += '~' + code.toString(16).toUpperCase().padStart(4, '0')
45
+ }
46
+ return out
47
+ }
48
+
49
+ /** Decode one `encodeSegment()` result back to the original string.
50
+ *
51
+ * @param {string} segment - encoded path segment.
52
+ * @returns {string} the decoded value (unknown escapes are left verbatim).
53
+ */
54
+ export function decodeSegment(segment) {
55
+ const value = String(segment === undefined || segment === null ? '' : segment)
56
+ let out = ''
57
+ for (let i = 0; i < value.length;) {
58
+ if (value[i] === '~' && /^[0-9A-Fa-f]{4}$/.test(value.slice(i + 1, i + 5))) {
59
+ out += String.fromCharCode(parseInt(value.slice(i + 1, i + 5), 16))
60
+ i += 5
61
+ continue
62
+ }
63
+ out += value[i]
64
+ i += 1
65
+ }
66
+ return out
67
+ }
68
+
69
+ /** The project directory key DSH gives a session cwd (mirror of its encoder).
70
+ *
71
+ * Separators (`/`, `\`, `:`) collapse into one `-`; unsafe units escape as
72
+ * `~XXXX`; the whole key is wrapped in `--` and capped at 251 characters.
73
+ *
74
+ * @param {string} cwd - absolute project directory.
75
+ * @returns {string} the directory name under the sessions root.
76
+ */
77
+ export function projectKey(cwd) {
78
+ const value = String(cwd === undefined || cwd === null ? '' : cwd)
79
+ if (value.length === 0) throw new Error('cannot encode an empty project path')
80
+ let readable = ''
81
+ let separatorRun = false
82
+ for (let i = 0; i < value.length; i += 1) {
83
+ const code = value.charCodeAt(i)
84
+ const ch = String.fromCharCode(code)
85
+ if (ch === '/' || ch === '\\' || ch === ':') {
86
+ if (!separatorRun) readable += '-'
87
+ separatorRun = true
88
+ } else if (ch !== '~' && /^[A-Za-z0-9._-]$/.test(ch)) {
89
+ readable += ch
90
+ separatorRun = false
91
+ } else {
92
+ readable += '~' + code.toString(16).toUpperCase().padStart(4, '0')
93
+ separatorRun = false
94
+ }
95
+ }
96
+ return `--${(readable.replace(/^-+/, '') || 'root').slice(0, 251)}--`
97
+ }
98
+
99
+ /** True when `target` is a strict descendant of `root` (separator-aware). */
100
+ export function isUnder(root, target) {
101
+ if (!root || !target) return false
102
+ const rel = path.relative(path.resolve(String(root)), path.resolve(String(target)))
103
+ return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel)
104
+ }
105
+
106
+ /** Parse a mirror's `.dsh-remote-meta.json`; undefined when absent/unusable.
107
+ *
108
+ * @param {string} localMirror - candidate mirror directory.
109
+ * @returns {{host: string, port: number, username: string, remotePath: string, alias: string}|undefined}
110
+ */
111
+ export function readMirrorMeta(localMirror) {
112
+ const file = path.join(String(localMirror || ''), MIRROR_META_FILE)
113
+ try {
114
+ const parsed = JSON.parse(readFileSync(file, 'utf8'))
115
+ if (!parsed || typeof parsed !== 'object') return undefined
116
+ if (!parsed.host) return undefined
117
+ return {
118
+ host: String(parsed.host),
119
+ port: Number(parsed.port) || 22,
120
+ username: String(parsed.username || ''),
121
+ remotePath: String(parsed.remotePath || ''),
122
+ alias: String(parsed.alias || ''),
123
+ }
124
+ } catch {
125
+ return undefined
126
+ }
127
+ }
128
+
129
+ /** Validate that `localMirror` is a real mirror exactly two levels under the root.
130
+ *
131
+ * @param {object} input
132
+ * @param {string} input.mirrorRoot - `$DSH_HOME/remote-workspaces`.
133
+ * @param {string} input.localMirror - candidate directory.
134
+ * @returns {{real: string, meta: object}} resolved path + parsed meta.
135
+ * @throws {Error} `code: 'VALIDATION'` when the path is not a usable mirror.
136
+ */
137
+ export function validateMirror({ mirrorRoot, localMirror } = {}) {
138
+ const invalid = (message) => {
139
+ const error = new Error(message)
140
+ error.code = 'VALIDATION'
141
+ return error
142
+ }
143
+ if (!mirrorRoot || !existsSync(mirrorRoot)) throw invalid('远程工作区根目录不存在')
144
+ if (!localMirror || !path.isAbsolute(String(localMirror))) throw invalid('必须是绝对路径')
145
+ let realRoot
146
+ let real
147
+ try {
148
+ realRoot = realpathSync(String(mirrorRoot))
149
+ } catch {
150
+ throw invalid('远程工作区根目录不可读')
151
+ }
152
+ try {
153
+ real = realpathSync(String(localMirror))
154
+ } catch {
155
+ throw invalid('目录不存在或已删除')
156
+ }
157
+ if (!isUnder(realRoot, real)) throw invalid('不是远程工作区镜像(不在 remote-workspaces 下)')
158
+ const parts = path.relative(realRoot, real).split(path.sep).filter(Boolean)
159
+ if (parts.length !== 2) throw invalid('不是远程工作区镜像(层级不符)')
160
+ const meta = readMirrorMeta(real)
161
+ if (!meta) throw invalid('缺少 .dsh-remote-meta.json,拒绝删除')
162
+ return { real, meta }
163
+ }
164
+
165
+ /** Every mirror found under the registry root (read-only scan, no SSH). */
166
+ export function listMirrorEntries(mirrorRoot) {
167
+ const entries = []
168
+ if (!mirrorRoot || !existsSync(mirrorRoot)) return entries
169
+ let hostDirs = []
170
+ try {
171
+ hostDirs = readdirSync(mirrorRoot, { withFileTypes: true })
172
+ } catch {
173
+ return entries
174
+ }
175
+ for (const hostDir of hostDirs) {
176
+ if (!hostDir.isDirectory()) continue
177
+ const hostPath = path.join(mirrorRoot, hostDir.name)
178
+ let children = []
179
+ try {
180
+ children = readdirSync(hostPath, { withFileTypes: true })
181
+ } catch {
182
+ continue
183
+ }
184
+ for (const child of children) {
185
+ if (!child.isDirectory()) continue
186
+ const localMirror = path.join(hostPath, child.name)
187
+ const meta = readMirrorMeta(localMirror)
188
+ if (!meta) continue
189
+ entries.push({ localMirror, mirrorName: child.name, hostDir: hostDir.name, ...meta })
190
+ }
191
+ }
192
+ return entries
193
+ }
194
+
195
+ /** Sum a tree's bytes, capped so a huge mirror cannot stall the UI. */
196
+ export function dirSizeBytes(dir, limit = 5000) {
197
+ let bytes = 0
198
+ let files = 0
199
+ let truncated = false
200
+ const walk = (current, depth) => {
201
+ if (truncated || depth > 24) return
202
+ let list
203
+ try {
204
+ list = readdirSync(current, { withFileTypes: true })
205
+ } catch {
206
+ return
207
+ }
208
+ for (const entry of list) {
209
+ if (entry.name === '.git') continue
210
+ const full = path.join(current, entry.name)
211
+ if (entry.isDirectory()) {
212
+ walk(full, depth + 1)
213
+ if (truncated) return
214
+ continue
215
+ }
216
+ files += 1
217
+ if (files > limit) {
218
+ truncated = true
219
+ return
220
+ }
221
+ try {
222
+ bytes += statSync(full).size
223
+ } catch { /* unreadable file: skip */ }
224
+ }
225
+ }
226
+ walk(String(dir || ''), 0)
227
+ return { bytes, truncated }
228
+ }
229
+
230
+ /** The project directory DSH groups one cwd's sessions under. */
231
+ export function sessionsDirFor(sessionsRoot, cwd) {
232
+ return path.join(String(sessionsRoot), projectKey(String(cwd)))
233
+ }
234
+
235
+ /** Session directories DSH keeps for one project cwd.
236
+ *
237
+ * @param {object} input
238
+ * @param {string} input.sessionsRoot - `$DSH_HOME/sessions`.
239
+ * @param {string} input.cwd - the mirror directory recorded as the session cwd.
240
+ * @returns {Array<{id: string, name: string, dir: string, modifiedAt: number}>}
241
+ */
242
+ export function listSessionDirs({ sessionsRoot, cwd, cwds = [] } = {}) {
243
+ const out = []
244
+ if (!sessionsRoot || !cwd || !existsSync(sessionsRoot)) return out
245
+ // DSH stores the cwd as the session header saw it. When `$DSH_HOME` itself
246
+ // is behind a symlink, realpath() and the header path differ, so accept both
247
+ // spellings (the caller passes the alias it was given) and dedupe by dir.
248
+ const candidates = []
249
+ for (const candidate of [cwd, ...(Array.isArray(cwds) ? cwds : [])]) {
250
+ const value = String(candidate || '')
251
+ if (!value) continue
252
+ try {
253
+ const dir = sessionsDirFor(sessionsRoot, value)
254
+ if (!candidates.includes(dir)) candidates.push(dir)
255
+ } catch { /* unencodable candidate */ }
256
+ }
257
+ const seen = new Set()
258
+ for (const projectDir of candidates) {
259
+ if (!existsSync(projectDir)) continue
260
+ let children = []
261
+ try {
262
+ children = readdirSync(projectDir, { withFileTypes: true })
263
+ } catch {
264
+ continue
265
+ }
266
+ for (const child of children) {
267
+ if (!child.isDirectory()) continue
268
+ const dir = path.join(projectDir, child.name)
269
+ if (seen.has(dir)) continue
270
+ seen.add(dir)
271
+ let modifiedAt = 0
272
+ try {
273
+ modifiedAt = statSync(dir).mtimeMs || 0
274
+ } catch { /* keep 0 */ }
275
+ out.push({ id: decodeSegment(child.name), name: child.name, dir, modifiedAt })
276
+ }
277
+ }
278
+ return out
279
+ }
280
+
281
+ /** Delete one mirror tree, refusing anything that is not a validated mirror.
282
+ *
283
+ * @param {object} input
284
+ * @param {string} input.mirrorRoot - registry root.
285
+ * @param {string} input.localMirror - mirror to remove.
286
+ * @returns {boolean} true when removed.
287
+ */
288
+ export function deleteMirrorTree({ mirrorRoot, localMirror } = {}) {
289
+ const { real } = validateMirror({ mirrorRoot, localMirror })
290
+ rmSync(real, { recursive: true, force: true })
291
+ return true
292
+ }
293
+
294
+ /** Delete every session directory of one project cwd.
295
+ *
296
+ * `isLive(sessionId, dir)` is consulted per session; live ones are skipped and
297
+ * reported so the caller can tell the person to stop them first. With
298
+ * `force: true` they are deleted anyway (the confirmation text says so).
299
+ *
300
+ * @param {object} input
301
+ * @param {string} input.sessionsRoot - `$DSH_HOME/sessions`.
302
+ * @param {string} input.cwd - project cwd (the local mirror).
303
+ * @param {(id: string, dir: string) => boolean|Promise<boolean>} [input.isLive]
304
+ * @param {boolean} [input.force]
305
+ * @returns {Promise<{removed: string[], skippedLive: string[]}>}
306
+ */
307
+ export async function deleteSessionsFor({ sessionsRoot, cwd, cwds = [], isLive, force = false } = {}) {
308
+ const removed = []
309
+ const skippedLive = []
310
+ for (const session of listSessionDirs({ sessionsRoot, cwd, cwds })) {
311
+ let live = false
312
+ if (!force && typeof isLive === 'function') {
313
+ try {
314
+ live = await isLive(session.id, session.dir) === true
315
+ } catch {
316
+ live = false
317
+ }
318
+ }
319
+ if (live) {
320
+ skippedLive.push(session.id)
321
+ continue
322
+ }
323
+ try {
324
+ rmSync(session.dir, { recursive: true, force: true })
325
+ removed.push(session.id)
326
+ } catch { /* unreadable dir: leave it and let the caller report fewer removals */ }
327
+ }
328
+ // Leave no empty history group behind when the last session is gone; a
329
+ // non-empty dir (live session, unreadable sibling) makes rmdir a no-op.
330
+ for (const candidate of [cwd, ...(Array.isArray(cwds) ? cwds : [])]) {
331
+ if (!candidate) continue
332
+ try {
333
+ rmdirSync(sessionsDirFor(sessionsRoot, candidate))
334
+ } catch { /* still holds something */ }
335
+ }
336
+ return { removed, skippedLive }
337
+ }
package/package.json CHANGED
@@ -1,99 +1,100 @@
1
- {
2
- "name": "dsh-remote",
3
- "version": "0.8.72",
4
- "description": "Remote-work assistant for DeepSeek Harness: connect SSH (password/key/agent/keyboard-interactive, multi-hop ProxyJump), list hosts from ~/.ssh/config, pick a remote workspace, operate on it with 21 rw_* tools, conflict-aware mirror sync, an embedded PTY terminal, remote DSH web UI attach, port forwarding, host grouping, remote sandbox fencing, audit log, keychain passwords.",
5
- "license": "MIT",
6
- "author": "flymysql <flyphp@outlook.com>",
7
- "readme": "README.md",
8
- "readmeFilename": "README.md",
9
- "homepage": "https://github.com/flymysql/dsh-remote",
10
- "repository": {
11
- "type": "git",
12
- "url": "git+https://github.com/flymysql/dsh-remote.git"
13
- },
14
- "bugs": {
15
- "url": "https://github.com/flymysql/dsh-remote/issues"
16
- },
17
- "keywords": [
18
- "deepseek-harness",
19
- "dsh",
20
- "sandbox",
21
- "terminal",
22
- "dsh-plugin",
23
- "remote",
24
- "ssh",
25
- "tunnel",
26
- "plugin"
27
- ],
28
- "type": "module",
29
- "main": "lib/index.js",
30
- "exports": {
31
- ".": "./lib/index.js",
32
- "./client": "./lib/client.js",
33
- "./package.json": "./package.json",
34
- "./cordis.patch.yml": "./cordis.patch.yml"
35
- },
36
- "files": [
37
- "lib",
38
- "cordis.patch.yml"
39
- ],
40
- "dsh": {
41
- "engines": {
42
- "dsh": ">=0.1.0-rc.6 <0.3.0"
43
- },
44
- "bundle": {
45
- "patch": "./cordis.patch.yml"
46
- },
47
- "client": {
48
- "inject": [
49
- "@deepseek-ai/dsh-client-ui-renderer",
50
- "@deepseek-ai/dsh-client-locale"
51
- ],
52
- "platform": "web"
53
- }
54
- },
55
- "peerDependencies": {
56
- "@deepseek-ai/dsh-commands": ">=0.1.0-rc.6 <0.3.0",
57
- "@deepseek-ai/dsh-host-webserver": ">=0.1.0-rc.6 <0.3.0",
58
- "@deepseek-ai/dsh-tools": ">=0.1.0-rc.6 <0.3.0",
59
- "@deepseek-ai/dsh-system-prompt": ">=0.1.0-rc.6 <0.3.0",
60
- "@deepseek-ai/dsh-client-ui-renderer": ">=0.1.2-rc.1 <0.3.0",
61
- "@deepseek-ai/dsh-client-locale": ">=0.1.2-rc.1 <0.3.0",
62
- "@deepseek-ai/dsh-client-connection": ">=0.1.2-rc.1 <0.3.0",
63
- "@deepseek-ai/cordis": "^4.0.1"
64
- },
65
- "peerDependenciesMeta": {
66
- "@deepseek-ai/dsh-host-webserver": {
67
- "optional": true
68
- },
69
- "@deepseek-ai/dsh-client-connection": {
70
- "optional": true
71
- }
72
- },
73
- "dependencies": {
74
- "ssh2": "^1.16.0",
75
- "@deepseek-ai/schemastery": "^3.18.1",
76
- "iconv-lite": "^0.6.3"
77
- },
78
- "bundleDependencies": [
79
- "ssh2",
80
- "asn1",
81
- "bcrypt-pbkdf",
82
- "tweetnacl"
83
- ],
84
- "devDependencies": {
85
- "@deepseek-ai/cordis": "^4.0.1",
86
- "@deepseek-ai/dsh-llm": "^0.1.0-rc.8",
87
- "@deepseek-ai/dsh-scope": "^0.1.0-rc.8",
88
- "@deepseek-ai/dsh-session": "^0.1.0-rc.8",
89
- "@deepseek-ai/dsh-timeout": "^0.1.0-rc.8",
90
- "@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
91
- "react": "^18.3.1",
92
- "semver": "^7.8.5"
93
- },
94
- "scripts": {
95
- "test": "node --import ./test/setup-telemetry-off.mjs --test --test-timeout=40000 --test-force-exit --test-concurrency=8",
96
- "test:coverage": "node scripts/coverage-union.mjs",
97
- "test:coverage:gate": "node scripts/coverage-union.mjs --gate 70 --file-gate 60"
98
- }
99
- }
1
+ {
2
+ "name": "dsh-remote",
3
+ "version": "0.8.74",
4
+ "description": "Remote-work assistant for DeepSeek Harness: connect SSH (password/key/agent/keyboard-interactive, multi-hop ProxyJump), list hosts from ~/.ssh/config, pick a remote workspace, operate on it with 21 rw_* tools, conflict-aware mirror sync, an embedded PTY terminal, remote DSH web UI attach, port forwarding, host grouping, remote sandbox fencing, audit log, keychain passwords.",
5
+ "license": "MIT",
6
+ "author": "flymysql <flyphp@outlook.com>",
7
+ "readme": "README.md",
8
+ "readmeFilename": "README.md",
9
+ "homepage": "https://github.com/flymysql/dsh-remote",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/flymysql/dsh-remote.git"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/flymysql/dsh-remote/issues"
16
+ },
17
+ "keywords": [
18
+ "deepseek-harness",
19
+ "dsh",
20
+ "sandbox",
21
+ "terminal",
22
+ "dsh-plugin",
23
+ "remote",
24
+ "ssh",
25
+ "tunnel",
26
+ "plugin"
27
+ ],
28
+ "type": "module",
29
+ "main": "lib/index.js",
30
+ "exports": {
31
+ ".": "./lib/index.js",
32
+ "./client": "./lib/client.js",
33
+ "./package.json": "./package.json",
34
+ "./cordis.patch.yml": "./cordis.patch.yml"
35
+ },
36
+ "files": [
37
+ "lib",
38
+ "cordis.patch.yml"
39
+ ],
40
+ "dsh": {
41
+ "engines": {
42
+ "dsh": ">=0.1.0-rc.6 <0.3.0"
43
+ },
44
+ "bundle": {
45
+ "patch": "./cordis.patch.yml"
46
+ },
47
+ "client": {
48
+ "inject": [
49
+ "@deepseek-ai/dsh-client-ui-renderer",
50
+ "@deepseek-ai/dsh-client-locale"
51
+ ],
52
+ "platform": "web"
53
+ }
54
+ },
55
+ "peerDependencies": {
56
+ "@deepseek-ai/cordis": "^4.0.5",
57
+ "@deepseek-ai/dsh-client-connection": ">=0.1.2-rc.1 <0.3.0",
58
+ "@deepseek-ai/dsh-client-locale": ">=0.1.2-rc.1 <0.3.0",
59
+ "@deepseek-ai/dsh-client-ui-renderer": ">=0.1.2-rc.1 <0.3.0",
60
+ "@deepseek-ai/dsh-commands": ">=0.1.0-rc.6 <0.3.0",
61
+ "@deepseek-ai/dsh-host-webserver": ">=0.1.0-rc.6 <0.3.0",
62
+ "@deepseek-ai/dsh-system-prompt": ">=0.1.0-rc.6 <0.3.0",
63
+ "@deepseek-ai/dsh-tools": ">=0.1.0-rc.6 <0.3.0"
64
+ },
65
+ "peerDependenciesMeta": {
66
+ "@deepseek-ai/dsh-host-webserver": {
67
+ "optional": true
68
+ },
69
+ "@deepseek-ai/dsh-client-connection": {
70
+ "optional": true
71
+ }
72
+ },
73
+ "dependencies": {
74
+ "@deepseek-ai/schemastery": "^3.18.5",
75
+ "iconv-lite": "^0.7.3",
76
+ "ssh2": "^1.16.0"
77
+ },
78
+ "bundleDependencies": [
79
+ "ssh2",
80
+ "asn1",
81
+ "bcrypt-pbkdf",
82
+ "tweetnacl"
83
+ ],
84
+ "devDependencies": {
85
+ "@deepseek-ai/cordis": "^4.0.5",
86
+ "@deepseek-ai/dsh-llm": "^0.1.0-rc.8",
87
+ "@deepseek-ai/dsh-sandbox": "^0.2.0-rc.2",
88
+ "@deepseek-ai/dsh-scope": "^0.2.0-rc.2",
89
+ "@deepseek-ai/dsh-session": "^0.1.0-rc.8",
90
+ "@deepseek-ai/dsh-timeout": "^0.2.0-rc.2",
91
+ "@deepseek-ai/dsh-tools": "^0.2.0-rc.2",
92
+ "react": "^18.3.1",
93
+ "semver": "^7.8.5"
94
+ },
95
+ "scripts": {
96
+ "test": "node --import ./test/setup-telemetry-off.mjs --test --test-timeout=40000 --test-force-exit --test-concurrency=8",
97
+ "test:coverage": "node scripts/coverage-union.mjs",
98
+ "test:coverage:gate": "node scripts/coverage-union.mjs --gate 70 --file-gate 60"
99
+ }
100
+ }