livedesk 0.1.418 → 0.1.420

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.
@@ -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 workstations. This is read-only.',
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 workstations.',
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 workstations.',
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 workstations.',
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 workstations. Use processName for a named process such as ComfyUI.',
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 workstations. Use serviceName when the user names a service.',
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 workstations.',
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 workstations. This changes process state.',
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 workstations.',
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 workstations.',
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 workstations, with sensitive credential paths rejected.',
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 workstations.',
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 workstations.',
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 workstations.',
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 workstation command interpreter. This is an audited high-risk action.',
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 workstation platform interpreter.',
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 workstations.',
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 workstations.',
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 workstation agent.',
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 workstations without ComfyUI, then check the service status only on those workstations.',
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 workstation.');
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} workstation${jobs.length === 1 ? '' : 's'}` : 'No changed files';
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
- for (const deviceId of targets) {
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
- job.failedTargets.add(deviceId);
203
- job.targetErrors.set(deviceId, String(result?.error || 'device-not-connected'));
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
 
@@ -2330,7 +2330,7 @@ export function createRemoteHub(options = {}) {
2330
2330
  const replace = options.replace !== false;
2331
2331
  const now = new Date();
2332
2332
  const platforms = ['win32', 'linux', 'darwin'];
2333
- const machineKinds = ['Mac mini', 'Mini PC', 'Workstation', 'Render node', 'Dev box'];
2333
+ const machineKinds = ['Mac mini', 'Mini PC', 'Client', 'Render node', 'Dev box'];
2334
2334
  const workloadRoles = ['LLM worker', 'Image worker', 'Video worker', 'Idle reserve', 'Build worker'];
2335
2335
  const gpuNames = ['Apple M-series GPU', 'NVIDIA RTX local', 'Radeon Pro', 'Intel Arc', 'Integrated GPU'];
2336
2336
  const npuNames = ['Apple Neural Engine', 'Ryzen AI NPU', 'Intel AI Boost', 'Qualcomm Hexagon', 'NPU not present'];
package/hub/src/server.js CHANGED
@@ -128,9 +128,10 @@ let verifiedLicense = {
128
128
  verifiedAt: 0
129
129
  };
130
130
  let frameClientSeq = 0;
131
- let inputClientSeq = 0;
132
- let audioClientSeq = 0;
131
+ let inputClientSeq = 0;
132
+ let audioClientSeq = 0;
133
133
  let liveDeskUpdateManager = null;
134
+ let hubTransferJobs = null;
134
135
  const HUB_HOST_TARGET_LEASE_MS = Math.max(5000, readPositiveIntegerEnv('LIVEDESK_HOST_TARGET_LEASE_MS', 60_000));
135
136
  const HUB_HOST_TARGET_RENEW_MS = Math.max(1000, Math.min(
136
137
  readPositiveIntegerEnv('LIVEDESK_HOST_TARGET_RENEW_MS', 20_000),
@@ -168,6 +169,7 @@ function readPositiveIntegerEnv(name, fallback) {
168
169
 
169
170
  function handleRemoteHubEvent(type, event) {
170
171
  liveDeskUpdateManager?.handleRemoteEvent(type, event);
172
+ hubTransferJobs?.handleRemoteEvent(type, event);
171
173
  if (type === 'RemoteInputApplied') {
172
174
  for (const ws of inputClients) {
173
175
  sendJson(ws, {
@@ -1208,11 +1210,12 @@ async function synchronizeAgentEnablement() {
1208
1210
  }
1209
1211
 
1210
1212
  const hubFilesystem = createHubFilesystem();
1211
- const hubTransferJobs = createHubTransferJobs({
1212
- filesystem: hubFilesystem,
1213
- remoteHub,
1214
- maxConcurrent: Number(process.env.LIVEDESK_MAX_FILE_JOBS || 2)
1215
- });
1213
+ hubTransferJobs = createHubTransferJobs({
1214
+ filesystem: hubFilesystem,
1215
+ remoteHub,
1216
+ maxConcurrent: Number(process.env.LIVEDESK_MAX_FILE_JOBS || 2),
1217
+ commandResultTimeoutMs: readPositiveIntegerEnv('LIVEDESK_FILE_CHUNK_ACK_TIMEOUT_MS', 30_000)
1218
+ });
1216
1219
  const hubSharedFolders = createHubSharedFolders({
1217
1220
  filesystem: hubFilesystem,
1218
1221
  transferJobs: hubTransferJobs,
@@ -2800,7 +2803,7 @@ app.post('/api/settings/agent/run', async (req, res) => {
2800
2803
  const deviceIds = normalizeDeviceIds(req.body?.deviceIds).slice(0, 500);
2801
2804
  const connectedDeviceIds = new Set(connectedAgentDeviceIds());
2802
2805
  if (deviceIds.length === 0 || deviceIds.some(deviceId => !connectedDeviceIds.has(deviceId))) {
2803
- throw new AgentProviderError('agent-target-not-connected', 'Every selected workstation must have an active Agent control channel.', { status: 409 });
2806
+ throw new AgentProviderError('agent-target-not-connected', 'Every selected Client must have an active Agent control channel.', { status: 409 });
2804
2807
  }
2805
2808
  const permissionMode = String(req.body?.permissionMode || 'ask').trim().toLowerCase();
2806
2809
  if (!AGENT_PERMISSION_MODES.includes(permissionMode)) {
@@ -3860,8 +3863,8 @@ app.post('/api/remote/devices/:deviceId/quick-actions', requireHubFeatureAccess,
3860
3863
 
3861
3864
  const label = action === 'shutdown' ? 'Shut down' : `${action.slice(0, 1).toUpperCase()}${action.slice(1)}`;
3862
3865
  const result = remoteHub.requestAgentTask(req.params.deviceId, {
3863
- instruction: `${label} this workstation. This action was explicitly requested and confirmed from the LiveDesk Hub action panel.`,
3864
- title: `${label} workstation`,
3866
+ instruction: `${label} this Client. This action was explicitly requested and confirmed from the LiveDesk Hub action panel.`,
3867
+ title: `${label} Client`,
3865
3868
  operation: 'system.power',
3866
3869
  approvalLevel: 'task-only',
3867
3870
  toolArguments: { action, delaySec: 0 },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "livedesk",
3
- "version": "0.1.418",
3
+ "version": "0.1.420",
4
4
  "livedeskClientVersion": "0.1.182",
5
5
  "buildFlavor": "production",
6
6
  "description": "LiveDesk Hub and client launcher",