dsh-remote-agent 0.0.0-stage → 0.4.0

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 (70) hide show
  1. package/CHANGELOG.md +700 -0
  2. package/CONTRIBUTING.md +358 -0
  3. package/KNOWN-ISSUES.md +601 -0
  4. package/LICENSE +21 -0
  5. package/README.en.md +353 -0
  6. package/README.md +317 -2
  7. package/SECURITY.md +348 -0
  8. package/bin/dsh-remote-relay.js +257 -0
  9. package/bin/dsh-remote-setup.js +440 -0
  10. package/bin//345/217/214/345/207/273/350/277/220/350/241/214-/351/205/215/347/275/256/345/220/221/345/257/274.cmd +28 -0
  11. package/cordis.patch.yml +87 -0
  12. package/deploy/relay-vps/Caddyfile +24 -0
  13. package/deploy/relay-vps/README.md +132 -0
  14. package/deploy/relay-vps/dsh-remote-relay.service +47 -0
  15. package/deploy/relay-vps/nginx.conf +49 -0
  16. package/deploy/relay-vps/relay.env.example +20 -0
  17. package/docs/dispatch-automation-eval.md +140 -0
  18. package/docs/screenshots/panel-main.png +0 -0
  19. package/docs/screenshots/panel-task-detail.png +0 -0
  20. package/docs/verification-agent-e2e.md +425 -0
  21. package/docs/verification-repos-routing-real.md +521 -0
  22. package/docs/verification-resume-real.md +254 -0
  23. package/docs/worker-setup-pitfalls.md +266 -0
  24. package/lib/artifacts.js +451 -0
  25. package/lib/client.js +1752 -0
  26. package/lib/e2e.js +304 -0
  27. package/lib/glob.js +225 -0
  28. package/lib/hub/audit-log.js +161 -0
  29. package/lib/hub/hub-util.js +114 -0
  30. package/lib/hub/index.js +500 -0
  31. package/lib/hub/node-registry.js +943 -0
  32. package/lib/hub/pull-manager.js +781 -0
  33. package/lib/hub/registry-handshake.js +403 -0
  34. package/lib/hub/registry-ingest.js +346 -0
  35. package/lib/hub/relay-link.js +228 -0
  36. package/lib/hub/subagent-provider.js +424 -0
  37. package/lib/hub/templates.js +693 -0
  38. package/lib/hub/tools.js +1059 -0
  39. package/lib/hub/ui-routes.js +856 -0
  40. package/lib/hub/views.js +351 -0
  41. package/lib/identity.js +271 -0
  42. package/lib/index.js +272 -0
  43. package/lib/node/artifact-service.js +231 -0
  44. package/lib/node/diff.js +124 -0
  45. package/lib/node/exec.js +536 -0
  46. package/lib/node/hub-pin.js +71 -0
  47. package/lib/node/index.js +1022 -0
  48. package/lib/node/repos.js +134 -0
  49. package/lib/node/resource.js +63 -0
  50. package/lib/node/runner.js +792 -0
  51. package/lib/node/task-engine.js +712 -0
  52. package/lib/node/task-store.js +273 -0
  53. package/lib/node/transport.js +763 -0
  54. package/lib/pairing.js +186 -0
  55. package/lib/protocol.js +414 -0
  56. package/lib/relay-config.js +317 -0
  57. package/lib/relay.js +732 -0
  58. package/lib/setup-routes.js +313 -0
  59. package/lib/setup.js +522 -0
  60. package/lib/ws.js +466 -0
  61. package/package.json +103 -4
  62. package/profiles/README.md +573 -0
  63. package/profiles/hub.cordis.yml +84 -0
  64. package/profiles/worker.cordis.yml +60 -0
  65. package/scripts/check-doc-drift.mjs +785 -0
  66. package/scripts/check-packaging.mjs +130 -0
  67. package/scripts/diag-inject.mjs +243 -0
  68. package/scripts/e2e-live-check.mjs +181 -0
  69. package/scripts/relay-init.ps1 +286 -0
  70. package/scripts/relay-init.sh +199 -0
package/lib/ws.js ADDED
@@ -0,0 +1,466 @@
1
+ /**
2
+ * 最小 RFC6455 WebSocket 实现(零外部依赖)。
3
+ *
4
+ * 只实现 dsh-remote-agent 需要的那一片:
5
+ * - 文本帧 / 二进制帧 / ping / pong / close
6
+ * - 分片(continuation)重组
7
+ * - 客户端强制掩码,服务端强制不掩码
8
+ * - 单帧长度上限,防止畸形帧吃内存
9
+ * 不做:扩展协商(permessage-deflate)、子协议、UTF-8 增量校验。
10
+ * 这是刻意的取舍 —— 少一层可变行为,跨网络长连接更好预测。
11
+ */
12
+ import { createHash, randomBytes } from 'node:crypto'
13
+ import { EventEmitter } from 'node:events'
14
+ import http from 'node:http'
15
+ import https from 'node:https'
16
+ import { MAX_ARTIFACT_FRAME_BYTES, MAX_CONTROL_FRAME_BYTES } from './protocol.js'
17
+
18
+ const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
19
+
20
+ const OP = { CONT: 0x0, TEXT: 0x1, BINARY: 0x2, CLOSE: 0x8, PING: 0x9, PONG: 0xa }
21
+
22
+ export function acceptKey(key) {
23
+ return createHash('sha1').update(String(key) + GUID).digest('base64')
24
+ }
25
+
26
+ function encodeFrame(opcode, payload, mask) {
27
+ const len = payload.length
28
+ let header
29
+ if (len < 126) {
30
+ header = Buffer.allocUnsafe(2)
31
+ header[1] = len
32
+ } else if (len < 65536) {
33
+ header = Buffer.allocUnsafe(4)
34
+ header[1] = 126
35
+ header.writeUInt16BE(len, 2)
36
+ } else {
37
+ header = Buffer.allocUnsafe(10)
38
+ header[1] = 127
39
+ header.writeBigUInt64BE(BigInt(len), 2)
40
+ }
41
+ header[0] = 0x80 | opcode
42
+ if (!mask) return Buffer.concat([header, payload], header.length + len)
43
+
44
+ header[1] |= 0x80
45
+ const key = randomBytes(4)
46
+ const out = Buffer.allocUnsafe(len)
47
+ for (let i = 0; i < len; i += 1) out[i] = payload[i] ^ key[i & 3]
48
+ return Buffer.concat([header, key, out], header.length + 4 + len)
49
+ }
50
+
51
+ /** 增量帧解析器:喂 Buffer,吐 {opcode, payload}。 */
52
+ class FrameParser {
53
+ constructor(onFrame, onProtocolError) {
54
+ this.buf = Buffer.alloc(0)
55
+ this.onFrame = onFrame
56
+ this.onProtocolError = onProtocolError
57
+ this.fragOp = 0
58
+ this.frags = []
59
+ this.fragBytes = 0
60
+ this.dead = false
61
+ }
62
+
63
+ push(chunk) {
64
+ if (this.dead) return
65
+ this.buf = this.buf.length === 0 ? chunk : Buffer.concat([this.buf, chunk])
66
+ while (!this.dead) {
67
+ const consumed = this.step()
68
+ if (consumed === 0) break
69
+ }
70
+ }
71
+
72
+ /** @returns {number} 消费的字节数;0 表示数据不足。 */
73
+ step() {
74
+ const b = this.buf
75
+ if (b.length < 2) return 0
76
+
77
+ const fin = (b[0] & 0x80) !== 0
78
+ const rsv = b[0] & 0x70
79
+ const opcode = b[0] & 0x0f
80
+ const masked = (b[1] & 0x80) !== 0
81
+ let len = b[1] & 0x7f
82
+ let offset = 2
83
+
84
+ if (rsv !== 0) {
85
+ this.fail('rsv-set')
86
+ return 0
87
+ }
88
+ if (len === 126) {
89
+ if (b.length < offset + 2) return 0
90
+ len = b.readUInt16BE(offset)
91
+ offset += 2
92
+ } else if (len === 127) {
93
+ if (b.length < offset + 8) return 0
94
+ const big = b.readBigUInt64BE(offset)
95
+ if (big > BigInt(Number.MAX_SAFE_INTEGER)) {
96
+ this.fail('frame-too-large')
97
+ return 0
98
+ }
99
+ len = Number(big)
100
+ offset += 8
101
+ }
102
+
103
+ const limit = opcode === OP.BINARY ? MAX_ARTIFACT_FRAME_BYTES : MAX_CONTROL_FRAME_BYTES
104
+ if (len > limit) {
105
+ this.fail('frame-too-large')
106
+ return 0
107
+ }
108
+
109
+ let maskKey
110
+ if (masked) {
111
+ if (b.length < offset + 4) return 0
112
+ maskKey = b.subarray(offset, offset + 4)
113
+ offset += 4
114
+ }
115
+
116
+ if (b.length < offset + len) return 0
117
+
118
+ const payload = Buffer.allocUnsafe(len)
119
+ b.copy(payload, 0, offset, offset + len)
120
+ if (masked) {
121
+ for (let i = 0; i < len; i += 1) payload[i] ^= maskKey[i & 3]
122
+ }
123
+
124
+ const total = offset + len
125
+ this.buf = b.length === total ? Buffer.alloc(0) : b.subarray(total)
126
+ this.dispatch(fin, opcode, payload)
127
+ return total
128
+ }
129
+
130
+ dispatch(fin, opcode, payload) {
131
+ if (opcode === OP.CONT) {
132
+ if (this.fragOp === 0) {
133
+ this.fail('unexpected-continuation')
134
+ return
135
+ }
136
+ this.frags.push(payload)
137
+ this.fragBytes += payload.length
138
+ // ⚠ 分片也要有**总量**上限,不能只限单帧。
139
+ //
140
+ // 上面 `len > limit` 只挡住单个分片;RFC 6455 允许把一个消息拆成任意多片,
141
+ // 而 CONT 帧数量不限 —— 于是一条连接可以发 N 个 4MiB 分片,把
142
+ // `Buffer.concat(frags, fragBytes)` 之前的 frags 数组撑到任意大小。
143
+ // 直连端点在 `nodeToken` 留空时**不做鉴权**(文档明示只适用于可信私网),
144
+ // 所以这是一条"一条连接打爆 hub 内存"的路径。
145
+ //
146
+ // 阈值取单帧上限的 4 倍:产物分块本身不需要跨多片(单片上限已是 4MiB),
147
+ // 留 4 倍余量足够容纳合法的大消息,又不给无限增长留空间。
148
+ if (this.fragBytes > MAX_ARTIFACT_FRAME_BYTES * 4) {
149
+ this.fail('fragment-too-large')
150
+ return
151
+ }
152
+ if (fin) {
153
+ const full = Buffer.concat(this.frags, this.fragBytes)
154
+ const op = this.fragOp
155
+ this.fragOp = 0
156
+ this.frags = []
157
+ this.fragBytes = 0
158
+ this.onFrame(op, full)
159
+ }
160
+ return
161
+ }
162
+
163
+ if (opcode === OP.PING || opcode === OP.PONG || opcode === OP.CLOSE) {
164
+ this.onFrame(opcode, payload)
165
+ return
166
+ }
167
+
168
+ if (opcode !== OP.TEXT && opcode !== OP.BINARY) {
169
+ this.fail('bad-opcode')
170
+ return
171
+ }
172
+
173
+ if (this.fragOp !== 0) {
174
+ this.fail('interleaved-fragments')
175
+ return
176
+ }
177
+
178
+ if (fin) {
179
+ this.onFrame(opcode, payload)
180
+ return
181
+ }
182
+
183
+ this.fragOp = opcode
184
+ this.frags = [payload]
185
+ this.fragBytes = payload.length
186
+ }
187
+
188
+ fail(reason) {
189
+ this.dead = true
190
+ this.onProtocolError(reason)
191
+ }
192
+ }
193
+
194
+ /**
195
+ * 一条已建立握手的 WebSocket 连接。
196
+ * 事件:'message'(data:Buffer, isBinary:boolean) / 'close'(code, reason) / 'error'(err)
197
+ */
198
+ export class WSConnection extends EventEmitter {
199
+ constructor(socket, { mask, maxQueuedBytes = 64 * 1024 * 1024, name = 'ws' } = {}) {
200
+ super()
201
+ this.socket = socket
202
+ this.mask = mask
203
+ this.name = name
204
+ this.closed = false
205
+ this.queuedBytes = 0
206
+ this.maxQueuedBytes = maxQueuedBytes
207
+ this.parser = new FrameParser(
208
+ (op, payload) => this.handleFrame(op, payload),
209
+ (reason) => this.destroy(new Error(`ws protocol error: ${reason}`)),
210
+ )
211
+ socket.on('data', (chunk) => {
212
+ try {
213
+ this.parser.push(chunk)
214
+ } catch (err) {
215
+ this.destroy(err)
216
+ }
217
+ })
218
+ socket.on('error', (err) => this.destroy(err))
219
+
220
+ // 对端硬断(kill -9 / 拔网线 / NAT 超时)时**不发** WebSocket close 帧,只发 FIN。
221
+ // upgrade 过的 socket 是 allowHalfOpen=true,所以 FIN 只 emit 'end'、不 emit 'close',
222
+ // 而 finish() 原本只挂在 'close' 上 —— 结果 conn.on('close') 永不触发,
223
+ // 上层(relay 的 node 清理、hub 的节点下线)会永久泄漏这条连接:
224
+ // 死节点占着 maxNodes 配额,节点列表与在线广播全部失真。
225
+ // 实测:客户端 destroy() 后 2 秒,服务端只见 'socket end','close' 从未来过。
226
+ socket.on('end', () => {
227
+ this.finish(1006, 'peer closed without close frame')
228
+ // allowHalfOpen 下 'end' 之后 socket 仍可写,必须主动销毁才能释放 fd
229
+ try {
230
+ socket.destroy()
231
+ } catch {
232
+ /* 已销毁 */
233
+ }
234
+ })
235
+
236
+ socket.on('close', () => this.finish(1006, 'socket closed'))
237
+ socket.setNoDelay?.(true)
238
+ // 长连接不被中间设备静默掐断:交给上层 heartbeat,但底层也关掉空闲超时。
239
+ socket.setTimeout?.(0)
240
+ }
241
+
242
+ handleFrame(opcode, payload) {
243
+ if (opcode === OP.PING) {
244
+ this.rawSend(OP.PONG, payload)
245
+ this.emit('ping', payload)
246
+ return
247
+ }
248
+ if (opcode === OP.PONG) {
249
+ this.emit('pong', payload)
250
+ return
251
+ }
252
+ if (opcode === OP.CLOSE) {
253
+ const code = payload.length >= 2 ? payload.readUInt16BE(0) : 1005
254
+ const reason = payload.length > 2 ? payload.subarray(2).toString('utf8') : ''
255
+ this.rawSend(OP.CLOSE, payload.length >= 2 ? payload.subarray(0, 2) : Buffer.alloc(0))
256
+ this.finish(code, reason)
257
+ return
258
+ }
259
+ this.emit('message', payload, opcode === OP.BINARY)
260
+ }
261
+
262
+ /**
263
+ * 写缓冲是否已超预算。
264
+ *
265
+ * `maxQueuedBytes` 此前只是声明、从未被读(死代码)。中继用它做下行背压:
266
+ * 一个停滞的节点(不读 socket)会让 `socket.write` 的缓冲无限增长 ——
267
+ * 实测推 2000×256KiB 后 writableLength 达 339.8 MiB、relay RSS 419 MiB,
268
+ * 单个停滞节点就能把中继 OOM 打掉。
269
+ *
270
+ * 注意:只对**数据帧**生效(send/sendBinary)。PING/PONG/CLOSE 走 rawSend,
271
+ * 必须始终能发出,否则一条卡住的连接连优雅关闭都做不到。
272
+ */
273
+ get overBudget() {
274
+ return this.bufferedBytes > this.maxQueuedBytes
275
+ }
276
+
277
+ rawSend(opcode, payload) {
278
+ if (this.closed) return false
279
+ try {
280
+ this.socket.write(encodeFrame(opcode, payload, this.mask))
281
+ return true
282
+ } catch (err) {
283
+ this.destroy(err)
284
+ return false
285
+ }
286
+ }
287
+
288
+ /** 发送文本帧。 */
289
+ send(text) {
290
+ return this.rawSend(OP.TEXT, Buffer.from(String(text), 'utf8'))
291
+ }
292
+
293
+ /** 发送二进制帧(产物分块走这里,避免 base64 的 33% 膨胀)。 */
294
+ sendBinary(buf) {
295
+ return this.rawSend(OP.BINARY, Buffer.isBuffer(buf) ? buf : Buffer.from(buf))
296
+ }
297
+
298
+ /**
299
+ * 尚未写出的字节数。
300
+ * 产物分块必须靠它做背压 —— 一个 200MB 的产物若一次灌进 socket,
301
+ * 内存会被撑爆,而且中继侧的转发队列会跟着一起涨。
302
+ */
303
+ get bufferedBytes() {
304
+ return this.socket?.writableLength ?? 0
305
+ }
306
+
307
+ /** 等待写缓冲降到阈值以下,或超时。 */
308
+ drain(threshold = 1 * 1024 * 1024, timeoutMs = 30_000) {
309
+ if (this.bufferedBytes <= threshold) return Promise.resolve(true)
310
+ return new Promise((resolve) => {
311
+ const started = Date.now()
312
+ const tick = () => {
313
+ if (this.closed) {
314
+ resolve(false)
315
+ return
316
+ }
317
+ if (this.bufferedBytes <= threshold) {
318
+ resolve(true)
319
+ return
320
+ }
321
+ if (Date.now() - started > timeoutMs) {
322
+ resolve(false)
323
+ return
324
+ }
325
+ setTimeout(tick, 5)
326
+ }
327
+ tick()
328
+ })
329
+ }
330
+
331
+ ping() {
332
+ return this.rawSend(OP.PING, Buffer.alloc(0))
333
+ }
334
+
335
+ close(code = 1000, reason = '') {
336
+ if (this.closed) return
337
+ const r = Buffer.from(reason, 'utf8')
338
+ const payload = Buffer.allocUnsafe(2 + r.length)
339
+ payload.writeUInt16BE(code, 0)
340
+ r.copy(payload, 2)
341
+ this.rawSend(OP.CLOSE, payload)
342
+ this.finish(code, reason)
343
+ try {
344
+ this.socket.end()
345
+ } catch {
346
+ /* socket 可能已断,忽略 */
347
+ }
348
+ }
349
+
350
+ destroy(err) {
351
+ if (err) this.emit('error', err)
352
+ this.finish(1006, err ? err.message : 'destroyed')
353
+ try {
354
+ this.socket.destroy()
355
+ } catch {
356
+ /* 已销毁 */
357
+ }
358
+ }
359
+
360
+ finish(code, reason) {
361
+ if (this.closed) return
362
+ this.closed = true
363
+ this.parser.dead = true
364
+ this.emit('close', code, reason)
365
+ }
366
+ }
367
+
368
+ /**
369
+ * 服务端:接管一个已经 upgrade 的 socket。
370
+ * 直接对接 ctx.webServer.registerUpgrade({ path, handler }) 的 handler 签名。
371
+ */
372
+ export function acceptUpgrade(req, socket, head, opts = {}) {
373
+ const key = req.headers['sec-websocket-key']
374
+ if (!key || req.headers.upgrade?.toLowerCase() !== 'websocket') {
375
+ socket.destroy()
376
+ return undefined
377
+ }
378
+ const headers = [
379
+ 'HTTP/1.1 101 Switching Protocols',
380
+ 'Upgrade: websocket',
381
+ 'Connection: Upgrade',
382
+ `Sec-WebSocket-Accept: ${acceptKey(key)}`,
383
+ '\r\n',
384
+ ]
385
+ socket.write(headers.join('\r\n'))
386
+ const conn = new WSConnection(socket, { mask: false, ...opts })
387
+ if (head && head.length > 0) conn.parser.push(head)
388
+ return conn
389
+ }
390
+
391
+ /**
392
+ * 客户端:连到 ws:// 或 wss://。
393
+ * @returns {Promise<WSConnection>}
394
+ */
395
+ export function connect(url, { headers = {}, timeoutMs = 15_000, name = 'ws-client' } = {}) {
396
+ return new Promise((resolve, reject) => {
397
+ let target
398
+ try {
399
+ target = new URL(url)
400
+ } catch {
401
+ reject(new Error(`invalid ws url: ${url}`))
402
+ return
403
+ }
404
+ const isSecure = target.protocol === 'wss:'
405
+ const mod = isSecure ? https : http
406
+ const key = randomBytes(16).toString('base64')
407
+ const req = mod.request({
408
+ protocol: target.protocol === 'wss:' ? 'https:' : 'http:',
409
+ hostname: target.hostname,
410
+ port: target.port || (isSecure ? 443 : 80),
411
+ path: `${target.pathname}${target.search}`,
412
+ method: 'GET',
413
+ headers: {
414
+ Connection: 'Upgrade',
415
+ Upgrade: 'websocket',
416
+ 'Sec-WebSocket-Version': '13',
417
+ 'Sec-WebSocket-Key': key,
418
+ ...headers,
419
+ },
420
+ })
421
+
422
+ let settled = false
423
+ const timer = setTimeout(() => {
424
+ if (settled) return
425
+ settled = true
426
+ req.destroy()
427
+ reject(new Error(`ws connect timeout after ${timeoutMs}ms`))
428
+ }, timeoutMs)
429
+ if (timer.unref) timer.unref()
430
+
431
+ req.on('upgrade', (res, socket, head) => {
432
+ if (settled) {
433
+ socket.destroy()
434
+ return
435
+ }
436
+ settled = true
437
+ clearTimeout(timer)
438
+ const expect = acceptKey(key)
439
+ if (res.headers['sec-websocket-accept'] !== expect) {
440
+ socket.destroy()
441
+ reject(new Error('ws handshake: bad Sec-WebSocket-Accept'))
442
+ return
443
+ }
444
+ const conn = new WSConnection(socket, { mask: true, name })
445
+ if (head && head.length > 0) conn.parser.push(head)
446
+ resolve(conn)
447
+ })
448
+
449
+ req.on('response', (res) => {
450
+ if (settled) return
451
+ settled = true
452
+ clearTimeout(timer)
453
+ reject(new Error(`ws handshake rejected: HTTP ${res.statusCode}`))
454
+ res.resume()
455
+ })
456
+
457
+ req.on('error', (err) => {
458
+ if (settled) return
459
+ settled = true
460
+ clearTimeout(timer)
461
+ reject(err)
462
+ })
463
+
464
+ req.end()
465
+ })
466
+ }
package/package.json CHANGED
@@ -1,6 +1,105 @@
1
1
  {
2
2
  "name": "dsh-remote-agent",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.4.0",
4
+ "type": "module",
5
+ "description": "把另一台电脑变成你 DSH 的远程 Agent 同事:副机本地持久跑任务、独立 Agent 循环,只回传 handoff 摘要与产物清单,成品按需拉取。",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/zbzbhs/dsh-remote-agent.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/zbzbhs/dsh-remote-agent/issues"
13
+ },
14
+ "homepage": "https://github.com/zbzbhs/dsh-remote-agent#readme",
15
+ "keywords": [
16
+ "dsh",
17
+ "deepseek-harness",
18
+ "dsh-plugin",
19
+ "ai-agent",
20
+ "agent",
21
+ "multi-agent",
22
+ "remote-agent",
23
+ "remote-execution",
24
+ "task-automation",
25
+ "self-hosted",
26
+ "zero-dependency",
27
+ "handoff",
28
+ "artifact",
29
+ "subagent-provider"
30
+ ],
31
+ "exports": {
32
+ ".": "./lib/index.js",
33
+ "./client": "./lib/client.js",
34
+ "./hub": "./lib/hub/index.js",
35
+ "./worker": "./lib/node/index.js",
36
+ "./protocol": "./lib/protocol.js",
37
+ "./pairing": "./lib/pairing.js",
38
+ "./relay": "./lib/relay.js",
39
+ "./setup": "./lib/setup.js",
40
+ "./package.json": "./package.json",
41
+ "./cordis.patch.yml": "./cordis.patch.yml",
42
+ "./profiles/hub.cordis.yml": "./profiles/hub.cordis.yml",
43
+ "./profiles/worker.cordis.yml": "./profiles/worker.cordis.yml"
44
+ },
45
+ "main": "./lib/index.js",
46
+ "bin": {
47
+ "dsh-remote-relay": "./bin/dsh-remote-relay.js",
48
+ "dsh-remote-setup": "./bin/dsh-remote-setup.js"
49
+ },
50
+ "files": [
51
+ "lib",
52
+ "bin",
53
+ "deploy",
54
+ "docs",
55
+ "profiles",
56
+ "scripts",
57
+ "cordis.patch.yml",
58
+ "README.md",
59
+ "CHANGELOG.md",
60
+ "CONTRIBUTING.md",
61
+ "SECURITY.md",
62
+ "KNOWN-ISSUES.md",
63
+ "LICENSE"
64
+ ],
65
+ "dsh": {
66
+ "bundle": {
67
+ "patch": "./cordis.patch.yml"
68
+ },
69
+ "client": {
70
+ "platform": "web",
71
+ "immediately": true,
72
+ "inject": []
73
+ }
74
+ },
75
+ "peerDependencies": {
76
+ "@deepseek-ai/schemastery": "*"
77
+ },
78
+ "peerDependenciesMeta": {
79
+ "@deepseek-ai/schemastery": {
80
+ "optional": true
81
+ }
82
+ },
83
+ "dependencies": {
84
+ "@deepseek-ai/schemastery": "^3.18.4"
85
+ },
86
+ "engines": {
87
+ "node": "^22.19.0 || >=24.0.0"
88
+ },
89
+ "scripts": {
90
+ "test": "node --test test/*.test.js",
91
+ "check:docs": "node scripts/check-doc-drift.mjs",
92
+ "check:packaging": "node scripts/check-packaging.mjs",
93
+ "lint": "eslint .",
94
+ "check": "npm run check:packaging && npm run check:docs && npm run diag:inject && npm test",
95
+ "diag:inject": "node scripts/diag-inject.mjs"
96
+ },
97
+ "devDependencies": {
98
+ "@eslint/js": "10.0.1",
99
+ "eslint": "10.12.0",
100
+ "globals": "17.13.0"
101
+ },
102
+ "publishConfig": {
103
+ "access": "public"
104
+ }
105
+ }