@livedesk/hub 0.1.5 → 0.1.6
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/package.json +2 -2
- package/src/agents/agent-tool-registry.js +19 -19
- package/src/agents/codex-agent-runtime.js +2 -2
- package/src/filesystem/shared-folders.js +1 -1
- package/src/filesystem/transfer-jobs.js +90 -5
- package/src/live-desk-update.js +585 -302
- package/src/mode4-atlas-pool.js +123 -0
- package/src/mode4-atlas-sizing.js +32 -0
- package/src/mode4-atlas-worker.js +58 -11
- package/src/mode4-atlas.js +9 -1
- package/src/remote-hub.js +1259 -423
- package/src/server.js +776 -275
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@livedesk/hub",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "LiveDesk local Hub API and browser frame bridge",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/server.js",
|
|
@@ -15,10 +15,10 @@
|
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"@ffmpeg-installer/ffmpeg": "^1.1.0",
|
|
18
|
+
"@livedesk/runtime-core": "0.1.0",
|
|
18
19
|
"@openai/codex-sdk": "0.145.0",
|
|
19
20
|
"cors": "^2.8.5",
|
|
20
21
|
"express": "^4.21.2",
|
|
21
|
-
"ffmpeg-static": "^5.3.0",
|
|
22
22
|
"path-to-regexp": "0.1.13",
|
|
23
23
|
"ws": "^8.18.3"
|
|
24
24
|
},
|
|
@@ -27,7 +27,7 @@ const readTaskPermission = { ask: 'allow', safeAuto: 'allow', fullAccess: 'allow
|
|
|
27
27
|
export const AGENT_TOOL_DEFINITIONS = Object.freeze([
|
|
28
28
|
{
|
|
29
29
|
name: 'livedesk.list_devices',
|
|
30
|
-
description: 'List connected LiveDesk
|
|
30
|
+
description: 'List connected LiveDesk Clients. This is read-only.',
|
|
31
31
|
category: 'read',
|
|
32
32
|
readOnly: true,
|
|
33
33
|
mutating: false,
|
|
@@ -40,7 +40,7 @@ export const AGENT_TOOL_DEFINITIONS = Object.freeze([
|
|
|
40
40
|
},
|
|
41
41
|
{
|
|
42
42
|
name: 'livedesk.get_system_health',
|
|
43
|
-
description: 'Read system health from the selected
|
|
43
|
+
description: 'Read system health from the selected Clients.',
|
|
44
44
|
category: 'read',
|
|
45
45
|
readOnly: true,
|
|
46
46
|
mutating: false,
|
|
@@ -53,7 +53,7 @@ export const AGENT_TOOL_DEFINITIONS = Object.freeze([
|
|
|
53
53
|
},
|
|
54
54
|
{
|
|
55
55
|
name: 'livedesk.get_gpu_status',
|
|
56
|
-
description: 'Read GPU status from the selected
|
|
56
|
+
description: 'Read GPU status from the selected Clients.',
|
|
57
57
|
category: 'read',
|
|
58
58
|
readOnly: true,
|
|
59
59
|
mutating: false,
|
|
@@ -66,7 +66,7 @@ export const AGENT_TOOL_DEFINITIONS = Object.freeze([
|
|
|
66
66
|
},
|
|
67
67
|
{
|
|
68
68
|
name: 'livedesk.get_disk_status',
|
|
69
|
-
description: 'Read disk status from the selected
|
|
69
|
+
description: 'Read disk status from the selected Clients.',
|
|
70
70
|
category: 'read',
|
|
71
71
|
readOnly: true,
|
|
72
72
|
mutating: false,
|
|
@@ -79,7 +79,7 @@ export const AGENT_TOOL_DEFINITIONS = Object.freeze([
|
|
|
79
79
|
},
|
|
80
80
|
{
|
|
81
81
|
name: 'livedesk.list_processes',
|
|
82
|
-
description: 'Read process status from the selected
|
|
82
|
+
description: 'Read process status from the selected Clients. Use processName for a named process such as ComfyUI.',
|
|
83
83
|
category: 'read',
|
|
84
84
|
readOnly: true,
|
|
85
85
|
mutating: false,
|
|
@@ -92,7 +92,7 @@ export const AGENT_TOOL_DEFINITIONS = Object.freeze([
|
|
|
92
92
|
},
|
|
93
93
|
{
|
|
94
94
|
name: 'livedesk.get_service_status',
|
|
95
|
-
description: 'Read service status from the selected
|
|
95
|
+
description: 'Read service status from the selected Clients. Use serviceName when the user names a service.',
|
|
96
96
|
category: 'read',
|
|
97
97
|
readOnly: true,
|
|
98
98
|
mutating: false,
|
|
@@ -105,7 +105,7 @@ export const AGENT_TOOL_DEFINITIONS = Object.freeze([
|
|
|
105
105
|
},
|
|
106
106
|
{
|
|
107
107
|
name: 'livedesk.collect_diagnostics',
|
|
108
|
-
description: 'Collect the existing safe LiveDesk diagnostics payload from the selected
|
|
108
|
+
description: 'Collect the existing safe LiveDesk diagnostics payload from the selected Clients.',
|
|
109
109
|
category: 'read',
|
|
110
110
|
readOnly: true,
|
|
111
111
|
mutating: false,
|
|
@@ -118,7 +118,7 @@ export const AGENT_TOOL_DEFINITIONS = Object.freeze([
|
|
|
118
118
|
},
|
|
119
119
|
{
|
|
120
120
|
name: 'livedesk.control_process',
|
|
121
|
-
description: 'Stop or restart a named process on the selected
|
|
121
|
+
description: 'Stop or restart a named process on the selected Clients. This changes process state.',
|
|
122
122
|
category: 'processControl',
|
|
123
123
|
readOnly: false,
|
|
124
124
|
mutating: true,
|
|
@@ -131,7 +131,7 @@ export const AGENT_TOOL_DEFINITIONS = Object.freeze([
|
|
|
131
131
|
},
|
|
132
132
|
{
|
|
133
133
|
name: 'livedesk.control_service',
|
|
134
|
-
description: 'Start, stop, or restart a named service on the selected
|
|
134
|
+
description: 'Start, stop, or restart a named service on the selected Clients.',
|
|
135
135
|
category: 'serviceControl',
|
|
136
136
|
readOnly: false,
|
|
137
137
|
mutating: true,
|
|
@@ -157,7 +157,7 @@ export const AGENT_TOOL_DEFINITIONS = Object.freeze([
|
|
|
157
157
|
},
|
|
158
158
|
{
|
|
159
159
|
name: 'livedesk.close_application',
|
|
160
|
-
description: 'Close a named application process on the selected
|
|
160
|
+
description: 'Close a named application process on the selected Clients.',
|
|
161
161
|
category: 'applicationControl',
|
|
162
162
|
readOnly: false,
|
|
163
163
|
mutating: true,
|
|
@@ -170,7 +170,7 @@ export const AGENT_TOOL_DEFINITIONS = Object.freeze([
|
|
|
170
170
|
},
|
|
171
171
|
{
|
|
172
172
|
name: 'livedesk.read_file',
|
|
173
|
-
description: 'Read a bounded UTF-8 text file from the selected
|
|
173
|
+
description: 'Read a bounded UTF-8 text file from the selected Clients, with sensitive credential paths rejected.',
|
|
174
174
|
category: 'fileRead',
|
|
175
175
|
readOnly: true,
|
|
176
176
|
mutating: false,
|
|
@@ -183,7 +183,7 @@ export const AGENT_TOOL_DEFINITIONS = Object.freeze([
|
|
|
183
183
|
},
|
|
184
184
|
{
|
|
185
185
|
name: 'livedesk.write_file',
|
|
186
|
-
description: 'Write bounded text content to a file on the selected
|
|
186
|
+
description: 'Write bounded text content to a file on the selected Clients.',
|
|
187
187
|
category: 'fileWrite',
|
|
188
188
|
readOnly: false,
|
|
189
189
|
mutating: true,
|
|
@@ -196,7 +196,7 @@ export const AGENT_TOOL_DEFINITIONS = Object.freeze([
|
|
|
196
196
|
},
|
|
197
197
|
{
|
|
198
198
|
name: 'livedesk.delete_file',
|
|
199
|
-
description: 'Delete a selected file or explicitly requested directory on the selected
|
|
199
|
+
description: 'Delete a selected file or explicitly requested directory on the selected Clients.',
|
|
200
200
|
category: 'fileDelete',
|
|
201
201
|
readOnly: false,
|
|
202
202
|
mutating: true,
|
|
@@ -209,7 +209,7 @@ export const AGENT_TOOL_DEFINITIONS = Object.freeze([
|
|
|
209
209
|
},
|
|
210
210
|
{
|
|
211
211
|
name: 'livedesk.list_directory',
|
|
212
|
-
description: 'List bounded file metadata from a directory on the selected
|
|
212
|
+
description: 'List bounded file metadata from a directory on the selected Clients.',
|
|
213
213
|
category: 'fileRead',
|
|
214
214
|
readOnly: true,
|
|
215
215
|
mutating: false,
|
|
@@ -222,7 +222,7 @@ export const AGENT_TOOL_DEFINITIONS = Object.freeze([
|
|
|
222
222
|
},
|
|
223
223
|
{
|
|
224
224
|
name: 'livedesk.run_command',
|
|
225
|
-
description: 'Run a bounded command through the selected
|
|
225
|
+
description: 'Run a bounded command through the selected Client command interpreter. This is an audited high-risk action.',
|
|
226
226
|
category: 'shell',
|
|
227
227
|
readOnly: false,
|
|
228
228
|
mutating: true,
|
|
@@ -235,7 +235,7 @@ export const AGENT_TOOL_DEFINITIONS = Object.freeze([
|
|
|
235
235
|
},
|
|
236
236
|
{
|
|
237
237
|
name: 'livedesk.run_script',
|
|
238
|
-
description: 'Run a bounded script file using the
|
|
238
|
+
description: 'Run a bounded script file using the Client platform interpreter.',
|
|
239
239
|
category: 'script',
|
|
240
240
|
readOnly: false,
|
|
241
241
|
mutating: true,
|
|
@@ -261,7 +261,7 @@ export const AGENT_TOOL_DEFINITIONS = Object.freeze([
|
|
|
261
261
|
},
|
|
262
262
|
{
|
|
263
263
|
name: 'livedesk.get_network_status',
|
|
264
|
-
description: 'Read bounded network adapter and connectivity status from the selected
|
|
264
|
+
description: 'Read bounded network adapter and connectivity status from the selected Clients.',
|
|
265
265
|
category: 'network',
|
|
266
266
|
readOnly: true,
|
|
267
267
|
mutating: false,
|
|
@@ -274,7 +274,7 @@ export const AGENT_TOOL_DEFINITIONS = Object.freeze([
|
|
|
274
274
|
},
|
|
275
275
|
{
|
|
276
276
|
name: 'livedesk.power_action',
|
|
277
|
-
description: 'Request an explicit power action on the selected
|
|
277
|
+
description: 'Request an explicit power action on the selected Clients.',
|
|
278
278
|
category: 'systemPower',
|
|
279
279
|
readOnly: false,
|
|
280
280
|
mutating: true,
|
|
@@ -287,7 +287,7 @@ export const AGENT_TOOL_DEFINITIONS = Object.freeze([
|
|
|
287
287
|
},
|
|
288
288
|
{
|
|
289
289
|
name: 'livedesk.system_configuration',
|
|
290
|
-
description: 'Apply one explicitly named system configuration action supported by the
|
|
290
|
+
description: 'Apply one explicitly named system configuration action supported by the Client agent.',
|
|
291
291
|
category: 'systemConfiguration',
|
|
292
292
|
readOnly: false,
|
|
293
293
|
mutating: true,
|
|
@@ -472,7 +472,7 @@ export function createCodexAgentRuntime({
|
|
|
472
472
|
'Never use arbitrary MCP servers, change permissions, forge approvals, request credentials, or invent a tool result.',
|
|
473
473
|
`The Hub has fixed this run to permission mode ${permissionPolicy?.mode || 'ask'} and enforces the policy independently of your instructions.`,
|
|
474
474
|
'Use only the selected connected device IDs below. If the Hub asks for user approval, wait for that approval result and do not work around it.',
|
|
475
|
-
'You may perform multiple safe read-only checks when the request requires a sequence. For example, find
|
|
475
|
+
'You may perform multiple safe read-only checks when the request requires a sequence. For example, find Clients without ComfyUI, then check the service status only on those Clients.',
|
|
476
476
|
`Selected device IDs: ${JSON.stringify(deviceIds)}`,
|
|
477
477
|
'Return a concise Korean or English summary grounded only in tool results. Do not invent results.',
|
|
478
478
|
`User request: ${safeText(instruction, 4000)}`
|
|
@@ -505,7 +505,7 @@ export function createCodexAgentRuntime({
|
|
|
505
505
|
const status = await getStatus();
|
|
506
506
|
if (!status.installed) throw new AgentProviderError('codex-sdk-not-installed', 'Codex SDK is not installed.', { status: 503 });
|
|
507
507
|
if (status.authenticated !== 'signed-in') throw new AgentProviderError('codex-auth-required', 'Sign in with the Codex CLI first.', { status: 401 });
|
|
508
|
-
reportProgress(run, 'preparing-tools', 'Codex is ready. Preparing the LiveDesk tools for this
|
|
508
|
+
reportProgress(run, 'preparing-tools', 'Codex is ready. Preparing the LiveDesk tools for this Client.');
|
|
509
509
|
session = createMcpSession({
|
|
510
510
|
runId: run.runId,
|
|
511
511
|
signal: run.abortController.signal,
|
|
@@ -139,7 +139,7 @@ export class HubSharedFolders {
|
|
|
139
139
|
}
|
|
140
140
|
}));
|
|
141
141
|
}
|
|
142
|
-
folder.message = jobs.length > 0 ? `Syncing ${jobs.length}
|
|
142
|
+
folder.message = jobs.length > 0 ? `Syncing ${jobs.length} Client${jobs.length === 1 ? '' : 's'}` : 'No changed files';
|
|
143
143
|
await this.persist();
|
|
144
144
|
return { ok: true, jobs, files: scan.files.length, totalBytes: scan.totalBytes };
|
|
145
145
|
} finally {
|
|
@@ -4,6 +4,7 @@ import { setImmediate as yieldToEventLoop } from 'node:timers/promises';
|
|
|
4
4
|
|
|
5
5
|
const CHUNK_BYTES = 512 * 1024;
|
|
6
6
|
const JOB_RETENTION_MS = 10 * 60 * 1000;
|
|
7
|
+
const COMMAND_RESULT_TIMEOUT_MS = 30 * 1000;
|
|
7
8
|
|
|
8
9
|
function snapshot(job) {
|
|
9
10
|
return {
|
|
@@ -28,13 +29,15 @@ function markUpdated(job) {
|
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
export class HubTransferJobs {
|
|
31
|
-
constructor({ filesystem, remoteHub, maxConcurrent = 2 } = {}) {
|
|
32
|
+
constructor({ filesystem, remoteHub, maxConcurrent = 2, commandResultTimeoutMs = COMMAND_RESULT_TIMEOUT_MS } = {}) {
|
|
32
33
|
this.filesystem = filesystem;
|
|
33
34
|
this.remoteHub = remoteHub;
|
|
34
35
|
this.maxConcurrent = Math.max(1, Number(maxConcurrent) || 2);
|
|
36
|
+
this.commandResultTimeoutMs = Math.max(1_000, Number(commandResultTimeoutMs) || COMMAND_RESULT_TIMEOUT_MS);
|
|
35
37
|
this.jobs = new Map();
|
|
36
38
|
this.queue = [];
|
|
37
39
|
this.running = 0;
|
|
40
|
+
this.pendingCommands = new Map();
|
|
38
41
|
}
|
|
39
42
|
|
|
40
43
|
create({ itemIds = [], deviceIds = [], remoteDirectory = '', files = null, onComplete = null } = {}) {
|
|
@@ -90,6 +93,7 @@ export class HubTransferJobs {
|
|
|
90
93
|
job.state = 'cancelled';
|
|
91
94
|
job.error = 'transfer-cancelled';
|
|
92
95
|
if (job.currentStream) job.currentStream.destroy();
|
|
96
|
+
this.finishPendingCommands(job.jobId, 'transfer-cancelled');
|
|
93
97
|
markUpdated(job);
|
|
94
98
|
return snapshot(job);
|
|
95
99
|
}
|
|
@@ -181,10 +185,13 @@ export class HubTransferJobs {
|
|
|
181
185
|
}
|
|
182
186
|
|
|
183
187
|
async sendChunk(job, file, offset, buffer, final, targets) {
|
|
184
|
-
|
|
185
|
-
if (job.cancelled) return;
|
|
188
|
+
const outcomes = await Promise.all(targets.map(async deviceId => {
|
|
189
|
+
if (job.cancelled) return { deviceId, ok: false, error: 'transfer-cancelled' };
|
|
190
|
+
const commandId = crypto.randomUUID();
|
|
191
|
+
const pending = this.waitForCommandResult(job.jobId, deviceId, commandId);
|
|
186
192
|
const result = this.remoteHub.sendCommand(deviceId, {
|
|
187
193
|
command: 'file.transfer.chunk',
|
|
194
|
+
commandId,
|
|
188
195
|
payload: {
|
|
189
196
|
transferId: job.jobId,
|
|
190
197
|
remoteDirectory: job.remoteDirectory,
|
|
@@ -199,9 +206,87 @@ export class HubTransferJobs {
|
|
|
199
206
|
}
|
|
200
207
|
});
|
|
201
208
|
if (!result?.ok) {
|
|
202
|
-
|
|
203
|
-
|
|
209
|
+
this.settlePendingCommand(commandId, {
|
|
210
|
+
ok: false,
|
|
211
|
+
error: String(result?.error || 'device-not-connected')
|
|
212
|
+
});
|
|
204
213
|
}
|
|
214
|
+
return { deviceId, ...await pending };
|
|
215
|
+
}));
|
|
216
|
+
|
|
217
|
+
for (const outcome of outcomes) {
|
|
218
|
+
if (outcome.ok || outcome.error === 'transfer-cancelled') continue;
|
|
219
|
+
job.failedTargets.add(outcome.deviceId);
|
|
220
|
+
job.targetErrors.set(outcome.deviceId, outcome.error || 'transfer-failed');
|
|
221
|
+
}
|
|
222
|
+
if (!job.cancelled && outcomes.length > 0 && outcomes.every(outcome => !outcome.ok)) {
|
|
223
|
+
throw new Error('all-targets-failed');
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
waitForCommandResult(jobId, deviceId, commandId) {
|
|
228
|
+
return new Promise(resolve => {
|
|
229
|
+
const timer = setTimeout(() => {
|
|
230
|
+
this.settlePendingCommand(commandId, {
|
|
231
|
+
ok: false,
|
|
232
|
+
error: 'file-transfer-ack-timeout'
|
|
233
|
+
});
|
|
234
|
+
}, this.commandResultTimeoutMs);
|
|
235
|
+
timer.unref?.();
|
|
236
|
+
this.pendingCommands.set(commandId, {
|
|
237
|
+
jobId,
|
|
238
|
+
deviceId,
|
|
239
|
+
timer,
|
|
240
|
+
resolve
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
settlePendingCommand(commandId, outcome) {
|
|
246
|
+
const pending = this.pendingCommands.get(String(commandId || ''));
|
|
247
|
+
if (!pending) return false;
|
|
248
|
+
this.pendingCommands.delete(commandId);
|
|
249
|
+
clearTimeout(pending.timer);
|
|
250
|
+
pending.resolve(outcome);
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
finishPendingCommands(jobId, error) {
|
|
255
|
+
for (const [commandId, pending] of this.pendingCommands) {
|
|
256
|
+
if (pending.jobId !== jobId) continue;
|
|
257
|
+
this.settlePendingCommand(commandId, { ok: false, error });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
handleRemoteEvent(type, event) {
|
|
262
|
+
if (type === 'RemoteCommandResult') {
|
|
263
|
+
const commandId = String(event?.commandId || '');
|
|
264
|
+
const pending = this.pendingCommands.get(commandId);
|
|
265
|
+
if (!pending) return;
|
|
266
|
+
const eventDeviceId = String(event?.deviceId || event?.device?.deviceId || '');
|
|
267
|
+
if (eventDeviceId && eventDeviceId !== pending.deviceId) return;
|
|
268
|
+
const result = event?.result;
|
|
269
|
+
const error = String(
|
|
270
|
+
event?.error
|
|
271
|
+
|| (result?.ok === false || result?.status === 'failed' || result?.status === 'rejected'
|
|
272
|
+
? result?.error || 'file-transfer-rejected'
|
|
273
|
+
: '')
|
|
274
|
+
);
|
|
275
|
+
this.settlePendingCommand(commandId, error
|
|
276
|
+
? { ok: false, error }
|
|
277
|
+
: { ok: true, result });
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (type !== 'RemoteDeviceDisconnected') return;
|
|
282
|
+
const deviceId = String(event?.deviceId || event?.device?.deviceId || '');
|
|
283
|
+
if (!deviceId) return;
|
|
284
|
+
for (const [commandId, pending] of this.pendingCommands) {
|
|
285
|
+
if (pending.deviceId !== deviceId) continue;
|
|
286
|
+
this.settlePendingCommand(commandId, {
|
|
287
|
+
ok: false,
|
|
288
|
+
error: 'device-disconnected'
|
|
289
|
+
});
|
|
205
290
|
}
|
|
206
291
|
}
|
|
207
292
|
|