dsh-device-fleet 0.1.0-dev.10 → 0.1.0-dev.12

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
@@ -9,7 +9,8 @@ DeepSeek Harness 的设备联邦插件:把登录同一账号、且都安装本
9
9
  - ✅ **Host 插件 + 直调 MVP(代码完成、单测通过,待真实 DSH 安装联调)**:`fleet_list` / `fleet_tools` / `fleet_call` + `fleet_read` / `fleet_fetch` / `fleet_run`、Tool Policy、Execution Slot、审批 fail-closed
10
10
  - ✅ **审批收件箱逻辑层(dev.7)**:`fleet_approvals` / `fleet_approve` / `fleet_deny`,ask 档排队、超时 fail-closed、once/ttl 授权、`fleet_simulate_request` 本机演练;Client 审批面板待做
11
11
  - ✅ **Room 逻辑层(dev.8,单机模拟)**:`room_create` / `room_add_local` / `room_send` / `room_messages` / `room_status` / `room_task_*` / `room_close`;本地真实 agent 参与者、寻址唤醒、Room Board 串行交接;单机仿真验证通过
12
- - ⏳ Client 审批面板 / 房间视图、远程参与者传输、插件重载 rehydrate(下一步)
12
+ - ✅ **审批面板 UI(dev.10)**:Host `/device-fleet/approvals` HTTP 数据面 + Client `shell.overlay` 浮动审批按钮/面板(允许一次/15 分钟/拒绝);单测 14/14;刷新页面后可见
13
+ - ⏳ 远程参与者传输、房间视图、插件重载 rehydrate、SSE 推送(下一步)
13
14
 
14
15
  ## 安装(开发版)
15
16
 
package/client.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // Fleet approval panel (Client half).
2
- // A floating button under shell.overlay polls the Host route registered by
3
- // src/host/approval-web.js. Decisions go through ApprovalInbox on the Host.
2
+ // The trigger lives in the native sidebar footer action row (no overlap with
3
+ // content panels); the inbox opens as an intentional centered dialog.
4
+ // Data comes from the Host routes registered by src/host/approval-web.js.
4
5
  window.__ModuleLoader__.load({
5
6
  id: 'dsh-device-fleet',
6
7
  factory(require) {
@@ -9,148 +10,205 @@ window.__ModuleLoader__.load({
9
10
  const { useEffect, useState } = React;
10
11
 
11
12
  const POLL_MS = 2500;
12
- const panelStyle = {
13
+ const store = {
14
+ items: [],
15
+ open: false,
16
+ listeners: new Set(),
17
+ };
18
+ let pollTimer = null;
19
+ let polling = false;
20
+
21
+ const notify = () => {
22
+ for (const listener of store.listeners) listener();
23
+ };
24
+
25
+ const setItems = (items) => {
26
+ const next = Array.isArray(items) ? items : [];
27
+ if (next.length === store.items.length && next.every((item, index) => item.id === store.items[index].id)) return;
28
+ store.items = next;
29
+ notify();
30
+ };
31
+
32
+ const startPolling = () => {
33
+ if (polling) return () => {};
34
+ polling = true;
35
+ const tick = async () => {
36
+ try {
37
+ const response = await fetch('/device-fleet/approvals', { headers: { accept: 'application/json' } });
38
+ if (!response.ok) throw new Error('HTTP ' + response.status);
39
+ const data = await response.json();
40
+ setItems(data.approvals);
41
+ } catch {
42
+ setItems([]);
43
+ }
44
+ if (polling) pollTimer = setTimeout(tick, POLL_MS);
45
+ };
46
+ tick();
47
+ return () => {
48
+ polling = false;
49
+ if (pollTimer) clearTimeout(pollTimer);
50
+ pollTimer = null;
51
+ };
52
+ };
53
+
54
+ const useStore = () => {
55
+ const [, setVersion] = useState(0);
56
+ useEffect(() => {
57
+ const listener = () => setVersion((value) => value + 1);
58
+ store.listeners.add(listener);
59
+ return () => store.listeners.delete(listener);
60
+ }, []);
61
+ return store;
62
+ };
63
+
64
+ const decide = async (id, action, scope) => {
65
+ try {
66
+ await fetch('/device-fleet/approvals/decide', {
67
+ method: 'POST',
68
+ headers: { 'content-type': 'application/json' },
69
+ body: JSON.stringify({ id, action, ...(scope ? { scope } : {}) }),
70
+ });
71
+ } finally {
72
+ store.items = store.items.filter((item) => item.id !== id);
73
+ notify();
74
+ }
75
+ };
76
+
77
+ const dialogStyle = {
13
78
  position: 'fixed',
14
- right: 16,
15
- bottom: 72,
16
- width: 360,
17
- maxHeight: '65vh',
79
+ left: '50%',
80
+ top: '12vh',
81
+ transform: 'translateX(-50%)',
82
+ width: 'min(440px, calc(100vw - 32px))',
83
+ maxHeight: '70vh',
18
84
  overflow: 'auto',
19
- zIndex: 900,
85
+ zIndex: 950,
20
86
  background: 'var(--dsw-alias-surface-raised, #1f1f1f)',
21
87
  color: 'var(--dsw-alias-text, #e8e8e8)',
22
88
  border: '1px solid var(--dsw-alias-border, #3a3a3a)',
23
- borderRadius: 10,
24
- boxShadow: '0 12px 32px rgba(0,0,0,.35)',
25
- padding: 12,
89
+ borderRadius: 12,
90
+ boxShadow: '0 18px 48px rgba(0,0,0,.45)',
91
+ padding: 14,
26
92
  font: '13px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif',
27
93
  };
28
- const buttonStyle = {
29
- position: 'fixed',
30
- right: 16,
31
- bottom: 16,
32
- zIndex: 900,
33
- display: 'flex',
34
- alignItems: 'center',
35
- gap: 8,
36
- padding: '8px 12px',
37
- borderRadius: 999,
38
- border: '1px solid var(--dsw-alias-border, #3a3a3a)',
39
- background: 'var(--dsw-alias-surface-raised, #1f1f1f)',
40
- color: 'var(--dsw-alias-text, #e8e8e8)',
41
- cursor: 'pointer',
42
- boxShadow: '0 6px 18px rgba(0,0,0,.3)',
43
- font: '13px system-ui, -apple-system, "Segoe UI", sans-serif',
44
- };
45
94
 
46
- function ActionButton({ children, onClick, tone = 'normal', disabled = false }) {
47
- const colors = {
48
- normal: { bg: 'var(--dsw-alias-surface, transparent)', border: 'var(--dsw-alias-border, #3a3a3a)' },
49
- allow: { bg: 'var(--dsw-alias-accent, #2f6fed)', border: 'var(--dsw-alias-accent, #2f6fed)' },
50
- deny: { bg: 'transparent', border: 'var(--dsw-alias-border, #3a3a3a)' },
51
- };
52
- const theme = colors[tone] ?? colors.normal;
53
- return h(
95
+ const smallButton = (label, onClick, tone, disabled) =>
96
+ h(
54
97
  'button',
55
98
  {
56
99
  type: 'button',
57
- disabled,
100
+ disabled: disabled === true,
58
101
  onClick,
59
102
  style: {
60
103
  padding: '3px 9px',
61
104
  borderRadius: 6,
62
- border: '1px solid ' + theme.border,
63
- background: theme.bg,
105
+ border: '1px solid ' + (tone === 'allow' ? 'var(--dsw-alias-accent, #2f6fed)' : 'var(--dsw-alias-border, #3a3a3a)'),
106
+ background: tone === 'allow' ? 'var(--dsw-alias-accent, #2f6fed)' : 'transparent',
64
107
  color: 'var(--dsw-alias-text, #e8e8e8)',
65
- cursor: disabled ? 'default' : 'pointer',
66
- opacity: disabled ? 0.55 : 1,
108
+ cursor: disabled === true ? 'default' : 'pointer',
109
+ opacity: disabled === true ? 0.55 : 1,
67
110
  fontSize: 12,
68
111
  },
69
112
  },
70
- children,
113
+ label,
71
114
  );
72
- }
73
115
 
74
- function ApprovalPanel() {
75
- const [items, setItems] = useState([]);
76
- const [open, setOpen] = useState(false);
77
- const [busy, setBusy] = useState({});
116
+ function Trigger({ wide }) {
117
+ const current = useStore();
118
+ const count = current.items.length;
119
+ const expanded = wide !== false;
120
+ return h(
121
+ 'button',
122
+ {
123
+ type: 'button',
124
+ title: count > 0 ? 'Fleet 审批:' + count + ' 条待处理' : 'Fleet 审批',
125
+ 'aria-label': 'Fleet 审批' + (count > 0 ? '(' + count + ' 条待处理)' : ''),
126
+ onClick: () => {
127
+ store.open = !store.open;
128
+ notify();
129
+ },
130
+ style: {
131
+ display: 'flex',
132
+ alignItems: 'center',
133
+ gap: 6,
134
+ padding: expanded ? '5px 10px' : '5px 7px',
135
+ borderRadius: 8,
136
+ border: '1px solid ' + (count > 0 ? 'var(--dsw-alias-accent, #2f6fed)' : 'var(--dsw-alias-border, #3a3a3a)'),
137
+ background: count > 0 ? 'var(--dsw-alias-accent, #2f6fed)' : 'transparent',
138
+ color: 'var(--dsw-alias-text, #e8e8e8)',
139
+ cursor: 'pointer',
140
+ fontSize: 12,
141
+ whiteSpace: 'nowrap',
142
+ },
143
+ },
144
+ h('span', { 'aria-hidden': true }, '🛰️'),
145
+ expanded ? h('span', null, count > 0 ? '审批 ' + count : '审批') : null,
146
+ );
147
+ }
78
148
 
149
+ function Panel() {
150
+ const current = useStore();
79
151
  useEffect(() => {
80
- let disposed = false;
81
- let timer;
82
- const poll = async () => {
83
- try {
84
- const response = await fetch('/device-fleet/approvals', { headers: { accept: 'application/json' } });
85
- if (!response.ok) throw new Error('HTTP ' + response.status);
86
- const data = await response.json();
87
- if (!disposed) setItems(Array.isArray(data.approvals) ? data.approvals : []);
88
- } catch {
89
- if (!disposed) setItems([]);
152
+ if (!current.open) return undefined;
153
+ const onKey = (event) => {
154
+ if (event.key === 'Escape') {
155
+ store.open = false;
156
+ notify();
90
157
  }
91
- if (!disposed) timer = setTimeout(poll, POLL_MS);
92
- };
93
- poll();
94
- return () => {
95
- disposed = true;
96
- clearTimeout(timer);
97
158
  };
98
- }, []);
99
-
100
- const decide = async (id, action, scope) => {
101
- setBusy((current) => ({ ...current, [id]: true }));
102
- try {
103
- const response = await fetch('/device-fleet/approvals/decide', {
104
- method: 'POST',
105
- headers: { 'content-type': 'application/json' },
106
- body: JSON.stringify({ id, action, ...(scope ? { scope } : {}) }),
107
- });
108
- if (response.ok) setItems((current) => current.filter((item) => item.id !== id));
109
- } finally {
110
- setBusy((current) => ({ ...current, [id]: false }));
111
- }
112
- };
113
-
114
- const label = items.length > 0 ? 'Fleet 审批 (' + items.length + ')' : 'Fleet 审批';
159
+ window.addEventListener('keydown', onKey);
160
+ return () => window.removeEventListener('keydown', onKey);
161
+ }, [current.open]);
162
+ if (!current.open) return null;
115
163
  return h(
116
164
  'div',
117
- null,
118
- open
119
- ? h(
120
- 'div',
121
- { style: panelStyle, role: 'dialog', 'aria-label': 'Fleet 待审批请求' },
122
- h(
123
- 'div',
124
- { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 } },
125
- h('strong', null, '待审批请求'),
126
- h('button', { type: 'button', onClick: () => setOpen(false), style: { ...buttonStyle, position: 'static', boxShadow: 'none', padding: '3px 9px', fontSize: 12 } }, '收起'),
127
- ),
128
- items.length === 0
129
- ? h('div', { style: { opacity: 0.7 } }, '暂无待审批请求')
130
- : items.map((item) =>
131
- h(
132
- 'div',
133
- { key: item.id, style: { borderTop: '1px solid var(--dsw-alias-border, #333)', padding: '8px 0' } },
134
- h('div', { style: { fontWeight: 600 } }, (item.capability || 'tool') + (item.tool ? ' / ' + item.tool : '')),
135
- h('div', { style: { opacity: 0.75, fontSize: 12 } }, '来自 ' + item.fromDeviceId + ' · ' + Math.max(0, Math.round(item.expiresInMs / 1000)) + 's 后超时'),
136
- item.reason ? h('div', { style: { opacity: 0.75, fontSize: 12, marginTop: 2 } }, item.reason) : null,
137
- h('div', { style: { fontFamily: 'ui-monospace, monospace', fontSize: 11, margin: '4px 0', opacity: 0.8, wordBreak: 'break-all' } }, item.argsSummary),
138
- h(
139
- 'div',
140
- { style: { display: 'flex', gap: 6, marginTop: 4 } },
141
- h(ActionButton, { tone: 'allow', onClick: () => decide(item.id, 'approve') }, busy[item.id] ? '处理中…' : '允许一次'),
142
- h(ActionButton, { tone: 'allow', onClick: () => decide(item.id, 'approve', 'ttl') }, '允许 15 分钟'),
143
- h(ActionButton, { tone: 'deny', onClick: () => decide(item.id, 'deny') }, '拒绝'),
144
- ),
145
- ),
146
- ),
147
- )
148
- : null,
165
+ {
166
+ style: { position: 'fixed', inset: 0, zIndex: 940, background: 'rgba(0,0,0,.35)' },
167
+ onClick: () => {
168
+ store.open = false;
169
+ notify();
170
+ },
171
+ role: 'presentation',
172
+ },
149
173
  h(
150
- 'button',
151
- { type: 'button', style: { ...buttonStyle, borderColor: items.length > 0 ? 'var(--dsw-alias-accent, #2f6fed)' : undefined }, onClick: () => setOpen((value) => !value) },
152
- h('span', { 'aria-hidden': true }, '🛰️'),
153
- h('span', null, label),
174
+ 'div',
175
+ { style: dialogStyle, role: 'dialog', 'aria-label': 'Fleet 待审批请求', onClick: (event) => event.stopPropagation() },
176
+ h(
177
+ 'div',
178
+ { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10 } },
179
+ h('strong', null, '待审批请求'),
180
+ h(
181
+ 'button',
182
+ {
183
+ type: 'button',
184
+ onClick: () => {
185
+ store.open = false;
186
+ notify();
187
+ },
188
+ style: { background: 'transparent', border: 'none', color: 'inherit', cursor: 'pointer', fontSize: 12, opacity: 0.8 },
189
+ },
190
+ '收起',
191
+ ),
192
+ ),
193
+ current.items.length === 0
194
+ ? h('div', { style: { opacity: 0.7 } }, '暂无待审批请求')
195
+ : current.items.map((item) =>
196
+ h(
197
+ 'div',
198
+ { key: item.id, style: { borderTop: '1px solid var(--dsw-alias-border, #333)', padding: '9px 0' } },
199
+ h('div', { style: { fontWeight: 600 } }, (item.capability || 'tool') + (item.tool ? ' / ' + item.tool : '')),
200
+ h('div', { style: { opacity: 0.75, fontSize: 12 } }, '来自 ' + item.fromDeviceId + ' · ' + Math.max(0, Math.round(item.expiresInMs / 1000)) + 's 后超时'),
201
+ item.reason ? h('div', { style: { opacity: 0.75, fontSize: 12, marginTop: 2 } }, item.reason) : null,
202
+ h('div', { style: { fontFamily: 'ui-monospace, monospace', fontSize: 11, margin: '5px 0', opacity: 0.8, wordBreak: 'break-all' } }, item.argsSummary),
203
+ h(
204
+ 'div',
205
+ { style: { display: 'flex', gap: 6, marginTop: 4, flexWrap: 'wrap' } },
206
+ smallButton('允许一次', () => decide(item.id, 'approve'), 'allow'),
207
+ smallButton('允许 15 分钟', () => decide(item.id, 'approve', 'ttl'), 'allow'),
208
+ smallButton('拒绝', () => decide(item.id, 'deny'), 'deny'),
209
+ ),
210
+ ),
211
+ ),
154
212
  ),
155
213
  );
156
214
  }
@@ -158,9 +216,14 @@ window.__ModuleLoader__.load({
158
216
  return {
159
217
  inject: ['slots'],
160
218
  apply(ctx) {
161
- ctx.slots.inject('shell.overlay', () =>
162
- ctx.slots.register({ name: 'shell.overlay', id: 'fleet-approvals', order: 10 }, ApprovalPanel),
219
+ ctx.effect(() => startPolling());
220
+ ctx.slots.inject('sidebar.footer.action', () =>
221
+ ctx.slots.register({ name: 'sidebar.footer.action', id: 'fleet-approvals', order: 5, label: 'Fleet 审批' }, Trigger),
163
222
  );
223
+ // shell.overlay is a root slot that exists from boot (the dev.10 build
224
+ // rendered from a direct registration); register directly so the dialog
225
+ // mounts even if the lazy inject callback never fires.
226
+ ctx.slots.register({ name: 'shell.overlay', id: 'fleet-approvals-panel', order: 10 }, Panel);
164
227
  },
165
228
  };
166
229
  },
@@ -0,0 +1,38 @@
1
+ # 审批面板 UI(dev.9 / dev.10)
2
+
3
+ ## Host 数据面
4
+
5
+ `src/host/approval-web.js`:
6
+
7
+ - `GET /device-fleet/approvals` → `{ approvals: [...] }`(来自 ApprovalInbox.list);
8
+ - `POST /device-fleet/approvals/decide`,body `{ id, action: "approve"|"deny", scope?, reason? }` → 决策走 ApprovalInbox;
9
+ - 仅注册在 DSH web 载体的本地监听上;决策仍受 ApprovalInbox 的超时/关闭/未知 id 约束;
10
+ - 纯逻辑 `createApprovalApi` 与路由注册分离,便于单测;路由在插件卸载时 dispose。
11
+
12
+ ## Client 半
13
+
14
+ `client.js`(闭包工厂,`window.__ModuleLoader__.load`,id = `dsh-device-fleet`):
15
+
16
+ - 注册 `shell.overlay`(root)列表项 `fleet-approvals`,右下角浮动按钮「Fleet 审批 (n)」;
17
+ - 2.5s 轮询 `/device-fleet/approvals`,有请求时按钮高亮;点开面板显示来源设备/能力/工具/参数摘要/剩余超时;
18
+ - 按钮:允许一次 / 允许 15 分钟(ttl)/ 拒绝,POST 到 decide 后从列表移除;
19
+ - 使用 `--dsw-alias-*` token(带 fallback),不覆盖宿主根、不读其他插件 DOM。
20
+
21
+ `package.json` 增加 `dsh.client = { platform: web, immediately: true, inject: ["@deepseek-ai/dsh-client-runtime"] }` 与 `./client` 导出、`files` 包含 client.js。
22
+
23
+ ## 验证
24
+
25
+ - 单测:`approval HTTP API lists and decides requests, and registers two routes`(列表/决策/路由注册/GET handler);
26
+ - Host 单测总计 **14/14** 通过,网络层 9/9;
27
+ - Host 路由注册:插件 dev.9/10 激活成功(无重复路由错误);
28
+ - Client 视觉:**未验证**(无浏览器控制;需要刷新页面加载 client bundle),`node --check client.js` 通过。
29
+
30
+ ## 使用
31
+
32
+ 刷新当前 Web 页面(F5)后,右下角应出现「Fleet 审批」浮动按钮。用 `fleet_simulate_request` 造一条请求即可看到面板内容;点击允许/拒绝后 Host 端 `fleet_approvals` 同步清空。
33
+
34
+ ## 限制
35
+
36
+ - 面板轮询而非推送,规模大时应换 SSE/事件;
37
+ - 路由未额外鉴权(依赖 web 载体自身的本地/会话边界);
38
+ - 面板不含房间视图(下一版可加 Room 时间线/看板页)。
@@ -39,19 +39,19 @@ dsh plugin --profile desktop add dsh-device-fleet@dev
39
39
  - 邀请里新增 `endpoints`(创建者的局域网/热点 IPv4 + direct 端口),加入方重启后自动向这些地址回连,绕开组播发现失败;
40
40
  - 新增 `fleet_status`(deviceId/直连端口/发现/连接/roster/hints)与 `fleet_connect(host, port)`(手动直连,成功后会从握手学到对端 deviceId);
41
41
  - `fleet_list` 仍是本地视图;配对成功后双方 roster 会自动固定对端公钥。
42
- ## 0.1.0-dev.9(稳定化)
42
+ ## 0.1.0-dev.11(稳定化)
43
43
 
44
44
  - 直连端口可固定(profile `direct.port`),插件重载不再换端口;
45
45
  - 连接断开后自动重试已知地址(不再只在启动时试一次);
46
46
  - `fleet_connect` 手动地址会持久化到 `peer-hints.json`,重启后继续回连;
47
47
  - 新增 `fleet_delegate`(dev.5):目标设备用 `SubagentProvider` + `delivery:'caller'` 跑 agent 并返回最终结果。
48
- ## 0.1.0-dev.9(审批收件箱,逻辑层)
48
+ ## 0.1.0-dev.11(审批收件箱,逻辑层)
49
49
 
50
50
  - 新增 `ApprovalInbox`:ask 档请求排队、超时 fail-closed、approve(once/ttl)/deny、关闭时全部失败关闭;
51
51
  - 直调与委派的 ask 档改为等待本机收件箱结果(不再一律拒绝);
52
52
  - 新工具:`fleet_approvals` / `fleet_approve` / `fleet_deny`;`fleet_simulate_request`(仅 `approvals.simulation: true`)用于本机无第二台设备时演练流程;
53
53
  - 审批面板(Client UI)与通知尚未实现,当前通过工具/后续 UI 应答。
54
- ## 0.1.0-dev.9(Room 逻辑层,本机模拟)
54
+ ## 0.1.0-dev.11(Room 逻辑层,本机模拟)
55
55
 
56
56
  - `RoomService`:当前会话升级为 Room Host;本地模拟参与者(真实 DSH session/agent);
57
57
  - 寻址消息:`room_send` 只唤醒被点名成员,Host 会话写权威 `room/message` 事件;
@@ -59,9 +59,15 @@ dsh plugin --profile desktop add dsh-device-fleet@dev
59
59
  - Room Board:`room_task_add/update/list`,`dependsOn` 串行交接(前置完成时唤醒下一个 owner),无依赖任务并行不受影响;
60
60
  - 工具:`room_create / room_add_local / room_remove_member / room_send / room_messages / room_status / room_task_* / room_close`;
61
61
  - 远程参与者传输与 Client UI 未做(下一步)。
62
- ## 0.1.0-dev.9(审批面板 UI,第一版)
62
+ ## 0.1.0-dev.11(审批面板 UI,第一版)
63
63
 
64
64
  - Host 新增本地 HTTP 数据面:GET `/device-fleet/approvals`、POST `/device-fleet/approvals/decide`(仅本机 web 载体,决策仍走 ApprovalInbox);
65
65
  - Client half:`client.js` 闭包工厂,注册 `shell.overlay` 浮动「Fleet 审批」按钮 + 面板,轮询待审批列表,支持允许一次 / 允许 15 分钟 / 拒绝;
66
66
  - package.json 增加 `dsh.client` 与 `./client` 导出;`files` 增加 client.js;
67
- - Client 视觉验证需要刷新页面;当前无浏览器控制,仅验证了 bundle 语法与 Host 路由注册。
67
+ - Client 视觉验证需要刷新页面;当前无浏览器控制,仅验证了 bundle 语法与 Host 路由注册。
68
+ ## 0.1.0-dev.11(审批入口不遮挡)
69
+
70
+ - 触发按钮从右下角浮动改为 `sidebar.footer.action`(设置旁边),不再遮挡任务列表/内容面板;
71
+ - 审批 inbox 改为点击后居中的模态对话框(带遮罩,Esc/点遮罩关闭);
72
+ - 轮询在 Client apply 中启动并由 ctx.effect 清理;待审批数量作为按钮徽标;
73
+ - 已实测:面板点击「允许」后 Host `fleet_approvals` 清空。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-device-fleet",
3
- "version": "0.1.0-dev.10",
3
+ "version": "0.1.0-dev.12",
4
4
  "description": "DeepSeek Harness device fleet: enroll devices, direct-call remote tools, shared Rooms with lazy-woken agents, and delegated subagents across devices.",
5
5
  "type": "module",
6
6
  "main": "src/plugin/index.js",