@linyanzhi/dsh-mesh 0.1.0 → 0.2.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.
package/lib/index.js CHANGED
@@ -1,3 +1,422 @@
1
- // dsh-mesh meta bundle: the actual plugins are injected by cordis.patch.yml.
2
- export const name = 'dsh-mesh'
3
- export function apply() {}
1
+ // dsh-mesh-ui — 服务端半(host 进程)
2
+ //
3
+ // 注册只读路由 GET /api/dsh-mesh/status(设置页「AI 网络」用)+ 后台刷新循环。
4
+ // 数据由 lib/collect.js 采集(mesh-health / host status / peer host_self / 台账 / 告警历史)。
5
+ //
6
+ // 2026-11-06 修正(三台实测死锁):v1 在 request handler 里 spawnSync 跑 mesh-health,
7
+ // 同步阻塞 DSH host 事件循环,而 mesh-health 要探测同进程的 host bridge(9100/9101)→ 自死锁 20s。
8
+ // 现在:后台 execFile 异步刷新 + 内存缓存,handler 只读缓存(0ms,不阻塞)。
9
+ //
10
+ // 安全:仅 loopback;不含 token/凭据;纯只读(只读文件 + 只调只读 CLI)。
11
+ //
12
+ // 归宿:reusable(dsh-mesh/plugin/dsh-mesh-ui/lib/)
13
+
14
+ import { collect, resolveRepo, meshHome, run, nodeExec } from './collect.js'
15
+ import { listDelegations, sessionHistory, taskStatus } from './delegations.js'
16
+ import { networkInterfaces } from 'node:os'
17
+ import net from 'node:net'
18
+ import tls from 'node:tls'
19
+ import { join } from 'node:path'
20
+ import { readFileSync } from 'node:fs'
21
+
22
+ /** Cordis inject 列表:只需要 webServer。 */
23
+ export const inject = ['webServer']
24
+
25
+ export const name = 'dsh-mesh-ui'
26
+
27
+ // 2026-11-09:默认 10s(可用 DSH_MESH_UI_REFRESH_MS 覆盖);隧道/节点变化更快可见
28
+ const REFRESH_MS = Math.max(3_000, Number(process.env.DSH_MESH_UI_REFRESH_MS || 5_000))
29
+ const LATENCY_HISTORY_MAX = 30
30
+ const REQUEST_REFRESH_COOLDOWN_MS = 8_000
31
+
32
+ function isLoopback(req) {
33
+ const raw = req?.socket?.remoteAddress || ''
34
+ const addr = String(raw).replace(/^::ffff:/, '')
35
+ return addr === '127.0.0.1' || addr === '::1' || addr === 'localhost' || addr.startsWith('127.')
36
+ }
37
+
38
+ function writeJson(res, status, body) {
39
+ res.statusCode = status
40
+ res.setHeader('content-type', 'application/json; charset=utf-8')
41
+ res.setHeader('cache-control', 'no-store')
42
+ res.end(JSON.stringify(body, null, 2))
43
+ }
44
+
45
+ function readBody(req, limit = 16 * 1024) {
46
+ return new Promise((resolve) => {
47
+ let size = 0
48
+ const chunks = []
49
+ req.on('data', (chunk) => {
50
+ size += chunk.length
51
+ if (size > limit) { resolve(null); req.destroy(); return }
52
+ chunks.push(chunk)
53
+ })
54
+ req.on('end', () => {
55
+ try { resolve(JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}')) } catch { resolve(null) }
56
+ })
57
+ req.on('error', () => resolve(null))
58
+ })
59
+ }
60
+
61
+ function lanIPv4() {
62
+ const ifaces = networkInterfaces()
63
+ for (const list of Object.values(ifaces)) {
64
+ for (const item of list || []) {
65
+ if (item.family === 'IPv4' && !item.internal && !/^169\.254\./.test(item.address)) return item.address
66
+ }
67
+ }
68
+ return '127.0.0.1'
69
+ }
70
+
71
+ /**
72
+ * Cordis apply:注册只读状态路由 + 后台刷新循环。
73
+ * @param {import('@deepseek-ai/cordis').Context} ctx host 插件上下文(已注入 webServer)
74
+ */
75
+ export function apply(ctx) {
76
+ const repo = resolveRepo()
77
+ const state = {
78
+ repo,
79
+ snapshot: {
80
+ ok: false,
81
+ warming: Boolean(repo),
82
+ at: null,
83
+ node: process.env.COMPUTERNAME || process.env.HOSTNAME || '',
84
+ meshHome: meshHome(),
85
+ nodeVersion: process.version,
86
+ repo,
87
+ error: repo ? 'warming up (first background refresh pending)' : 'dsh-mesh repo not found (set DSH_MESH_REPO or clone to ~/Code/dsh-mesh)'
88
+ },
89
+ refreshing: false,
90
+ lastStartedAt: 0,
91
+ latencyHistory: new Map()
92
+ }
93
+
94
+ /** 记录每个 peer 的延迟小趋势(在内存里滚动,不落盘)。 */
95
+ function rememberLatency(peers) {
96
+ for (const peer of peers || []) {
97
+ const history = state.latencyHistory.get(peer.name) || []
98
+ history.push(peer.up ? peer.latencyMs : null)
99
+ while (history.length > LATENCY_HISTORY_MAX) history.shift()
100
+ state.latencyHistory.set(peer.name, history)
101
+ }
102
+ return (peers || []).map((peer) => ({ ...peer, latencyHistory: state.latencyHistory.get(peer.name) || [] }))
103
+ }
104
+
105
+ async function refresh() {
106
+ if (state.refreshing || !state.repo) return
107
+ state.refreshing = true
108
+ state.lastStartedAt = Date.now()
109
+ try {
110
+ const data = await collect(state.repo)
111
+ state.snapshot = {
112
+ ...data,
113
+ warming: false,
114
+ at: data.generatedAt || new Date().toISOString(),
115
+ latencyMs: Date.now() - state.lastStartedAt,
116
+ node: process.env.COMPUTERNAME || process.env.HOSTNAME || '',
117
+ meshHome: meshHome(),
118
+ nodeVersion: process.version,
119
+ peers: data.ok ? rememberLatency(data.peers) : data.peers || []
120
+ }
121
+ } catch (error) {
122
+ state.snapshot = { ...state.snapshot, ok: false, warming: false, at: new Date().toISOString(), error: String(error?.message || error) }
123
+ } finally {
124
+ state.refreshing = false
125
+ }
126
+ }
127
+
128
+ const timer = setInterval(() => {
129
+ refresh().catch(() => {})
130
+ }, REFRESH_MS)
131
+ if (typeof timer.unref === 'function') timer.unref()
132
+ refresh().catch(() => {})
133
+
134
+ // ---- 委托面板只读路由(2026-11-07 方案 A)----
135
+ const delegationCache = { at: 0, data: null }
136
+ const taskCache = new Map()
137
+ const sessionCache = new Map()
138
+ function cached(taskIdKey, map, ttlMs, producer) {
139
+ const hit = map.get(taskIdKey)
140
+ if (hit && Date.now() - hit.at < ttlMs) return hit.data
141
+ const data = producer()
142
+ const settled = data instanceof Promise ? data.then((value) => { map.set(taskIdKey, { at: Date.now(), data: value }); return value }) : (map.set(taskIdKey, { at: Date.now(), data }), data)
143
+ return settled
144
+ }
145
+
146
+ const route = {
147
+ kind: 'exact',
148
+ path: '/api/dsh-mesh/status',
149
+ handler: async (req, res) => {
150
+ if (!isLoopback(req)) {
151
+ writeJson(res, 403, { ok: false, error: 'forbidden: loopback-only' })
152
+ return
153
+ }
154
+ if ((req.method || 'GET').toUpperCase() !== 'GET') {
155
+ writeJson(res, 405, { ok: false, error: 'method not allowed' })
156
+ return
157
+ }
158
+ try {
159
+ const url = new URL(req.url || '/', 'http://localhost')
160
+ if (url.searchParams.get('force') === '1' && Date.now() - state.lastStartedAt > REQUEST_REFRESH_COOLDOWN_MS) {
161
+ refresh().catch(() => {})
162
+ }
163
+ writeJson(res, 200, {
164
+ ...state.snapshot,
165
+ refreshing: state.refreshing,
166
+ snapshotAgeSeconds: state.snapshot.at ? Math.round((Date.now() - Date.parse(state.snapshot.at)) / 1000) : null
167
+ })
168
+ } catch (error) {
169
+ writeJson(res, 500, { ok: false, error: String(error?.message || error) })
170
+ }
171
+ }
172
+ }
173
+
174
+ /** 统一 GET 只读处理器。 */
175
+ function readonlyRoute(path, handler) {
176
+ return {
177
+ kind: 'exact',
178
+ path,
179
+ handler: async (req, res) => {
180
+ if (!isLoopback(req)) return writeJson(res, 403, { ok: false, error: 'forbidden: loopback-only' })
181
+ if ((req.method || 'GET').toUpperCase() !== 'GET') return writeJson(res, 405, { ok: false, error: 'method not allowed' })
182
+ try {
183
+ const url = new URL(req.url || '/', 'http://localhost')
184
+ writeJson(res, 200, await handler(url))
185
+ } catch (error) {
186
+ writeJson(res, 500, { ok: false, error: String(error?.message || error) })
187
+ }
188
+ }
189
+ }
190
+ }
191
+
192
+ function meshPort() {
193
+ try { return Number(JSON.parse(readFileSync(join(meshHome(), 'config.json'), 'utf8')).node?.port) || 8124 } catch { return 8124 }
194
+ }
195
+
196
+ async function runCli(args, timeout = 30_000) {
197
+ if (!state.repo) return { error: 'dsh-mesh-node is not installed', stdout: '', stderr: '' }
198
+ return run(nodeExec(), [join(state.repo, 'src', 'cli.js'), ...args], { cwd: state.repo, timeout })
199
+ }
200
+
201
+ const lanScanRoute = readonlyRoute('/api/dsh-mesh/lan/scan', async () => {
202
+ const r = await runCli(['discover', '--json', '--timeout-ms', '1500'], 20_000)
203
+ if (r.error) return { ok: false, error: r.error, stderr: r.stderr }
204
+ try {
205
+ const data = JSON.parse(r.stdout)
206
+ return { ok: true, localAddress: lanIPv4(), nodes: data.nodes || [] }
207
+ } catch {
208
+ return { ok: false, error: 'invalid discover output', stdout: r.stdout, stderr: r.stderr }
209
+ }
210
+ })
211
+
212
+ const lanCodeRoute = readonlyRoute('/api/dsh-mesh/lan/code', async () => {
213
+ const host = `${lanIPv4()}:${meshPort()}`
214
+ const r = await runCli(['pair', '--code', '--ttl', '600', '--role', 'node', '--host', host])
215
+ if (r.error) return { ok: false, error: r.error, stderr: r.stderr }
216
+ try {
217
+ return { ok: true, ...JSON.parse(r.stdout) }
218
+ } catch {
219
+ return { ok: false, error: 'invalid pair output', stdout: r.stdout, stderr: r.stderr }
220
+ }
221
+ })
222
+
223
+
224
+ const lanCheckRoute = {
225
+ kind: 'exact',
226
+ path: '/api/dsh-mesh/lan/check',
227
+ handler: async (req, res) => {
228
+ if (!isLoopback(req)) return writeJson(res, 403, { ok: false, error: 'forbidden: loopback-only' })
229
+ if ((req.method || 'GET').toUpperCase() !== 'POST') return writeJson(res, 405, { ok: false, error: 'method not allowed' })
230
+ const body = await readBody(req)
231
+ if (!body || typeof body.url !== 'string') return writeJson(res, 400, { ok: false, error: 'url required' })
232
+ let target
233
+ try { target = new URL(String(body.url)) } catch { return writeJson(res, 400, { ok: false, error: 'invalid url' }) }
234
+ const host = target.hostname.replace(/^\[|\]$/g, '')
235
+ const port = Number(target.port || (target.protocol === 'https:' ? 443 : 80))
236
+ const started = Date.now()
237
+ function tcp() {
238
+ return new Promise((resolve) => {
239
+ const socket = net.connect({ host, port, timeout: 2500 })
240
+ socket.once('connect', () => { socket.destroy(); resolve({ ok: true }) })
241
+ socket.once('timeout', () => { socket.destroy(); resolve({ ok: false, error: 'tcp timeout' }) })
242
+ socket.once('error', (error) => { socket.destroy(); resolve({ ok: false, error: String(error.message || error) }) })
243
+ })
244
+ }
245
+ const tcpResult = await tcp()
246
+ let tlsResult = null
247
+ if (tcpResult.ok && target.protocol === 'https:') {
248
+ tlsResult = await new Promise((resolve) => {
249
+ const socket = tls.connect({ host, port, rejectUnauthorized: false, servername: host, timeout: 2500 })
250
+ socket.once('secureConnect', () => { socket.destroy(); resolve({ ok: true, fingerprint: socket.getPeerCertificate()?.fingerprint256 || null }) })
251
+ socket.once('timeout', () => { socket.destroy(); resolve({ ok: false, error: 'tls timeout' }) })
252
+ socket.once('error', (error) => { socket.destroy(); resolve({ ok: false, error: String(error.message || error) }) })
253
+ })
254
+ }
255
+ writeJson(res, 200, { ok: tcpResult.ok, host, port, tcp: tcpResult.ok, tls: tlsResult, latencyMs: Date.now() - started })
256
+ }
257
+ }
258
+
259
+ const lanPairRoute = {
260
+ kind: 'exact',
261
+ path: '/api/dsh-mesh/lan/pair',
262
+ handler: async (req, res) => {
263
+ if (!isLoopback(req)) return writeJson(res, 403, { ok: false, error: 'forbidden: loopback-only' })
264
+ if ((req.method || 'GET').toUpperCase() !== 'POST') return writeJson(res, 405, { ok: false, error: 'method not allowed' })
265
+ const body = await readBody(req)
266
+ if (!body || typeof body.url !== 'string' || typeof body.code !== 'string') {
267
+ return writeJson(res, 400, { ok: false, error: 'url and code required' })
268
+ }
269
+ const pairingCode = String(body.code).trim()
270
+ if (pairingCode.replace(/-/g, '').length < 6) {
271
+ return writeJson(res, 400, { ok: false, error: 'pairing code is required (one-time code from the target node)' })
272
+ }
273
+ const args = ['peer', 'add', '--url', String(body.url), '--code', pairingCode, '--name', String(body.name || 'lan-peer')]
274
+ if (body.fingerprint) args.push('--fingerprint', String(body.fingerprint))
275
+ try {
276
+ const r = await runCli(args)
277
+ writeJson(res, 200, { ok: !r.error, error: r.error, stdout: r.stdout, stderr: r.stderr })
278
+ } catch (error) {
279
+ writeJson(res, 500, { ok: false, error: String(error?.message || error) })
280
+ }
281
+ }
282
+ }
283
+
284
+ // 只读联邦拓扑:向每个直连 peer 查询它的 cluster_status,得到 1 跳间接节点。
285
+ const federatedCache = { at: 0, data: null }
286
+ const federatedRoute = readonlyRoute('/api/dsh-mesh/federated', async () => {
287
+ if (federatedCache.data && Date.now() - federatedCache.at < 45_000) return federatedCache.data
288
+ const listResult = await runCli(['peer', 'list'])
289
+ let direct = []
290
+ try { direct = JSON.parse(listResult.stdout).peers || [] } catch { /* ignore */ }
291
+ const directNames = new Set(direct.map((p) => String(p.name || '')).filter(Boolean))
292
+ const nodes = []
293
+ const errors = []
294
+ const results = await Promise.all(direct.slice(0, 6).map(async (peer) => {
295
+ const name = String(peer.name || '')
296
+ if (!name) return null
297
+ // 快速路径:新节点暴露 mesh_peers(只读、不探测、毫秒级)。
298
+ const fast = await runCli(['peer', 'call', 'mesh_peers', '--to', name, '--timeout-ms', '4000'], 10_000)
299
+ if (!fast.error) {
300
+ try {
301
+ const fw = JSON.parse(fast.stdout)
302
+ const fi = JSON.parse(fw?.result?.content?.[0]?.text || '{}')
303
+ if (Array.isArray(fi.peers)) return { via: name, peers: fi.peers, instances: [], fast: true }
304
+ } catch { /* fall through */ }
305
+ }
306
+ // 兼容旧节点:回退到 cluster_status。
307
+ const r = await runCli(['peer', 'call', 'cluster_status', '--to', name, '--args', JSON.stringify({ noPeers: true, probeSelf: false }), '--timeout-ms', '10000'], 16_000)
308
+ if (r.error) { errors.push({ via: name, error: r.error }); return null }
309
+ try {
310
+ const wrapper = JSON.parse(r.stdout)
311
+ const inner = JSON.parse(wrapper?.result?.content?.[0]?.text || '{}')
312
+ return { via: name, peers: inner.peers || [], instances: inner.remoteInstances || [], fast: false }
313
+ } catch (error) { errors.push({ via: name, error: 'invalid cluster_status' }); return null }
314
+ }))
315
+ for (const result of results.filter(Boolean)) {
316
+ for (const peer of result.peers) {
317
+ const name = String(peer.name || '')
318
+ if (!name || directNames.has(name)) continue
319
+ nodes.push({ name, via: result.via, up: peer.up == null ? null : Boolean(peer.up), latencyMs: peer.latencyMs ?? null, version: peer.version || null, node: peer.node || name })
320
+ }
321
+ }
322
+ const data = { ok: true, nodes: nodes.slice(0, 40), errors }
323
+ federatedCache.at = Date.now(); federatedCache.data = data
324
+ return data
325
+ })
326
+
327
+ const transfersRoute = readonlyRoute('/api/dsh-mesh/transfers', async () => {
328
+ try {
329
+ const list = JSON.parse(readFileSync(join(meshHome(), 'transfers.json'), 'utf8'))
330
+ const items = (Array.isArray(list) ? list : []).slice(-60).reverse().map((x) => ({ at: x.at || null, direction: x.direction || '', path: x.path || '', bytes: Number(x.bytes) || 0, totalBytes: Number(x.totalBytes) || 0, peer: x.peer || '' }))
331
+ return { ok: true, items }
332
+ } catch { return { ok: true, items: [] } }
333
+ })
334
+
335
+ const peersListRoute = readonlyRoute('/api/dsh-mesh/peers', async () => {
336
+ const r = await runCli(['peer', 'list'])
337
+ try { const data = JSON.parse(r.stdout); return { ok: true, peers: (data.peers || []).map((p) => ({ id: p.id, name: p.name, url: p.url, addedAt: p.addedAt || null })) } } catch { return { ok: false, error: 'invalid peer list', stderr: r.stderr } }
338
+ })
339
+ const peersRemoveRoute = {
340
+ kind: 'exact', path: '/api/dsh-mesh/peers/remove',
341
+ handler: async (req, res) => {
342
+ if (!isLoopback(req)) return writeJson(res, 403, { ok: false, error: 'forbidden: loopback-only' })
343
+ if ((req.method || 'GET').toUpperCase() !== 'POST') return writeJson(res, 405, { ok: false, error: 'method not allowed' })
344
+ const body = await readBody(req)
345
+ const id = String(body?.id || '').trim()
346
+ if (!id) return writeJson(res, 400, { ok: false, error: 'id required' })
347
+ const r = await runCli(['peer', 'remove', id])
348
+ writeJson(res, 200, { ok: !r.error, error: r.error, stdout: r.stdout, stderr: r.stderr })
349
+ }
350
+ }
351
+ const peersToggleRoute = {
352
+ kind: 'exact', path: '/api/dsh-mesh/peers/toggle',
353
+ handler: async (req, res) => {
354
+ if (!isLoopback(req)) return writeJson(res, 403, { ok: false, error: 'forbidden: loopback-only' })
355
+ if ((req.method || 'GET').toUpperCase() !== 'POST') return writeJson(res, 405, { ok: false, error: 'method not allowed' })
356
+ const body = await readBody(req)
357
+ const id = String(body?.id || '').trim()
358
+ if (!id) return writeJson(res, 400, { ok: false, error: 'id required' })
359
+ const r = await runCli(['peer', body?.disabled ? 'disable' : 'enable', id])
360
+ writeJson(res, 200, { ok: !r.error, error: r.error, stdout: r.stdout, stderr: r.stderr })
361
+ }
362
+ }
363
+
364
+ const tokensListRoute = readonlyRoute('/api/dsh-mesh/tokens', async () => {
365
+ const r = await runCli(['token', 'list'])
366
+ try { const data = JSON.parse(r.stdout); return { ok: true, tokens: (data || []).map((x) => ({ id: x.id, note: x.note || '', role: x.role || null, roleHint: x.roleHint || '' })) } } catch { return { ok: false, error: 'invalid token list', stderr: r.stderr } }
367
+ })
368
+ const tokensRevokeRoute = {
369
+ kind: 'exact', path: '/api/dsh-mesh/tokens/revoke',
370
+ handler: async (req, res) => {
371
+ if (!isLoopback(req)) return writeJson(res, 403, { ok: false, error: 'forbidden: loopback-only' })
372
+ if ((req.method || 'GET').toUpperCase() !== 'POST') return writeJson(res, 405, { ok: false, error: 'method not allowed' })
373
+ const body = await readBody(req)
374
+ const id = String(body?.id || '').trim()
375
+ if (!id) return writeJson(res, 400, { ok: false, error: 'id required' })
376
+ const r = await runCli(['revoke', id])
377
+ writeJson(res, 200, { ok: !r.error, error: r.error, stdout: r.stdout, stderr: r.stderr })
378
+ }
379
+ }
380
+
381
+ const delegateRoutes = [
382
+ readonlyRoute('/api/dsh-mesh/delegations', async (url) => {
383
+ // 缓存整份列表(与请求的 limit 无关),避免不同 limit 的请求互相污染缓存
384
+ if (Date.now() - delegationCache.at > 15_000) {
385
+ delegationCache.at = Date.now()
386
+ delegationCache.data = listDelegations(200)
387
+ }
388
+ const data = delegationCache.data
389
+ const limit = Math.max(1, Math.min(Number(url.searchParams.get('limit')) || 100, 200))
390
+ return { ok: true, total: data.total, byState: data.byState, localNode: data.localNode, tasks: data.tasks.slice(0, limit) }
391
+ }),
392
+ readonlyRoute('/api/dsh-mesh/task', async (url) => {
393
+ const taskId = String(url.searchParams.get('taskId') || '')
394
+ if (!taskId) return { ok: false, error: 'taskId required' }
395
+ return cached('task:' + taskId, taskCache, 60_000, () => taskStatus(taskId))
396
+ }),
397
+ readonlyRoute('/api/dsh-mesh/session', async (url) => {
398
+ const node = String(url.searchParams.get('node') || '')
399
+ // 2026-11-10:本机任务(node=local)没有对端会话,直接给出明确错误,避免 peer call --to local 报 unknown peer
400
+ if (/^(local|self|localhost)$/i.test(node)) return { ok: false, code: "E_LOCAL_NODE", error: "本机任务(local)没有对端会话;请在 DSH 侧栏查看本机会话" }
401
+ const sessionId = String(url.searchParams.get('sessionId') || '')
402
+ const limit = Number(url.searchParams.get('limit')) || 40
403
+ if (!node || !sessionId) return { ok: false, error: 'node and sessionId required' }
404
+ return cached('sess:' + node + ':' + sessionId + ':' + limit, sessionCache, 120_000, () => sessionHistory(node, sessionId, limit))
405
+ })
406
+ ]
407
+
408
+ ctx.effect(() => {
409
+ const disposeRoute = ctx.webServer.register(route)
410
+ const disposeLanRoutes = [lanScanRoute, lanCodeRoute, lanCheckRoute, lanPairRoute, federatedRoute, peersListRoute, peersRemoveRoute, peersToggleRoute, tokensListRoute, tokensRevokeRoute, transfersRoute].map((item) => ctx.webServer.register(item))
411
+ const disposeRoutes = delegateRoutes.map((item) => ctx.webServer.register(item))
412
+ return () => {
413
+ try {
414
+ clearInterval(timer)
415
+ } catch {
416
+ /* ignore */
417
+ }
418
+ for (const dispose of disposeLanRoutes.concat(disposeRoutes)) { try { dispose() } catch { /* ignore */ } }
419
+ if (typeof disposeRoute === 'function') disposeRoute()
420
+ }
421
+ }, 'dsh-mesh-ui: GET /api/dsh-mesh/status + background refresh')
422
+ }
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@linyanzhi/dsh-mesh",
3
- "version": "0.1.0",
4
- "description": "One-command install for dsh-mesh: AI network node, DSH host bridge, sidebar UI and autostart supervisor in a single DSH plugin bundle.",
3
+ "version": "0.2.0",
4
+ "description": "dsh-mesh main plugin: multi-device AI network topology in the DSH sidebar, with host bridge and server autostart installed as dependencies.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {
8
8
  ".": "./lib/index.js",
9
+ "./client": "./lib/client.js",
9
10
  "./package.json": "./package.json"
10
11
  },
11
12
  "engines": {
@@ -16,21 +17,30 @@
16
17
  "manifestVersion": 1,
17
18
  "bundle": {
18
19
  "patch": "./cordis.patch.yml"
20
+ },
21
+ "client": {
22
+ "inject": [
23
+ "@deepseek-ai/dsh-client-ui-settings",
24
+ "@deepseek-ai/dsh-client-ui-layout"
25
+ ],
26
+ "platform": "web"
19
27
  }
20
28
  },
21
29
  "dependencies": {
22
30
  "dsh-mesh-node": "0.0.20",
23
31
  "dsh-mesh-host": "0.0.5",
24
- "dsh-mesh-ui": "0.0.45",
25
32
  "dsh-mesh-autostart": "0.0.7"
26
33
  },
27
34
  "peerDependencies": {
28
- "@deepseek-ai/dsh": ">=0.2.0-rc.2 <0.3.0"
35
+ "@deepseek-ai/dsh": ">=0.2.0-rc.2 <0.3.0",
36
+ "react": "^18.2.0",
37
+ "react-dom": "^18.2.0"
29
38
  },
30
39
  "files": [
31
- "lib",
40
+ "lib/**/*.js",
32
41
  "cordis.patch.yml",
33
42
  "README.md",
43
+ "icon.svg",
34
44
  "LICENSE"
35
45
  ],
36
46
  "license": "MIT",