livedesk 0.1.447 → 0.1.448
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/bin/livedesk.js +150 -17
- package/client/README.md +3 -3
- package/client/bin/livedesk-client-node.js +4 -3
- package/client/package.json +5 -5
- package/hub/package.json +1 -1
- package/hub/src/agents/agent-manager.js +132 -175
- package/hub/src/agents/agent-settings.js +7 -68
- package/hub/src/agents/agent-tool-registry.js +1 -1
- package/hub/src/agents/codex-agent-runtime.js +2 -2
- package/hub/src/remote-hub.js +92 -124
- package/hub/src/server.js +50 -38
- package/package.json +6 -6
- package/web/dist/assets/{icons-BIyRF-ou.js → icons-CTHUbfAT.js} +1 -1
- package/web/dist/assets/index-DKPdDVYW.js +25 -0
- package/web/dist/assets/{react-C7a9r614.js → react-BOnb-QRJ.js} +1 -1
- package/web/dist/index.html +3 -3
- package/hub/src/agents/opencode-go-provider.js +0 -260
- package/hub/src/agents/secret-store.js +0 -165
- package/web/dist/assets/index-5opXQY8X.js +0 -25
package/bin/livedesk.js
CHANGED
|
@@ -1422,30 +1422,163 @@ async function collectWindowsPortRecords(ports) {
|
|
|
1422
1422
|
return records;
|
|
1423
1423
|
}
|
|
1424
1424
|
|
|
1425
|
-
async function
|
|
1426
|
-
const
|
|
1427
|
-
const
|
|
1428
|
-
for (const record of records) {
|
|
1425
|
+
async function terminateExactWindowsPortOwners(records) {
|
|
1426
|
+
const roots = [];
|
|
1427
|
+
const seenRoots = new Set();
|
|
1428
|
+
for (const record of Array.isArray(records) ? records : []) {
|
|
1429
1429
|
const pid = Number(record?.Pid || 0);
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
||
|
|
1434
|
-
||
|
|
1430
|
+
const startOrder = String(record?.CreationUtcTicks || '').trim();
|
|
1431
|
+
if (record?.KnownLiveDesk !== true
|
|
1432
|
+
|| !Number.isInteger(pid)
|
|
1433
|
+
|| pid <= 1
|
|
1434
|
+
|| !/^\d+$/.test(startOrder)
|
|
1435
|
+
|| seenRoots.has(`${pid}:${startOrder}`)) {
|
|
1435
1436
|
continue;
|
|
1436
1437
|
}
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1438
|
+
seenRoots.add(`${pid}:${startOrder}`);
|
|
1439
|
+
roots.push({ pid, startOrder });
|
|
1440
|
+
}
|
|
1441
|
+
if (roots.length === 0) return;
|
|
1442
|
+
|
|
1443
|
+
const encodedRoots = Buffer.from(JSON.stringify(roots), 'utf8').toString('base64');
|
|
1444
|
+
const script = [
|
|
1445
|
+
'$ErrorActionPreference = "Stop";',
|
|
1446
|
+
`$rootsJson = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String("${encodedRoots}"));`,
|
|
1447
|
+
'$requestedRoots = @(ConvertFrom-Json -InputObject $rootsJson);',
|
|
1448
|
+
'$validatedRoots = [System.Collections.Generic.List[object]]::new();',
|
|
1449
|
+
'foreach ($item in $requestedRoots) {',
|
|
1450
|
+
' $targetPid = [int]$item.pid;',
|
|
1451
|
+
' $expectedStartTicks = [string]$item.startOrder;',
|
|
1452
|
+
' $proc = Get-CimInstance Win32_Process -Filter "ProcessId = $targetPid" -Property ProcessId,ParentProcessId,Name,CommandLine,CreationDate -ErrorAction SilentlyContinue | Select-Object -First 1;',
|
|
1453
|
+
' if (-not $proc) { continue };',
|
|
1454
|
+
' $actualStartTicks = if ($proc.CreationDate) { [string]$proc.CreationDate.ToUniversalTime().Ticks } else { "" };',
|
|
1455
|
+
' $processName = [string]$proc.Name;',
|
|
1456
|
+
' $commandLine = [string]$proc.CommandLine;',
|
|
1457
|
+
' $isNodeProcess = $processName -match "(?i)^node(?:\\.exe)?$";',
|
|
1458
|
+
' $hasKnownLiveDeskHubPath = $commandLine -match "(?i)(?:packages[\\\\/](?:livedesk[\\\\/])?hub|node_modules[\\\\/]@?livedesk[\\\\/]hub)[\\\\/]src[\\\\/]server\\.js";',
|
|
1459
|
+
' $hasUnifiedLauncherPath = $commandLine -match "(?i)(?:node_modules[\\\\/]livedesk|packages[\\\\/]livedesk)[\\\\/]bin[\\\\/]livedesk\\.js";',
|
|
1460
|
+
' $hasLegacyClientPath = $commandLine -match "(?i)(?:node_modules[\\\\/]@livedesk[\\\\/]client|packages[\\\\/]client|packages[\\\\/]livedesk[\\\\/]client)[\\\\/]bin[\\\\/]livedesk-client\\.js";',
|
|
1461
|
+
' if ($actualStartTicks -ne $expectedStartTicks -or -not $isNodeProcess -or -not ($hasKnownLiveDeskHubPath -or $hasUnifiedLauncherPath -or $hasLegacyClientPath)) { continue };',
|
|
1462
|
+
' $validatedRoots.Add([pscustomobject]@{ ProcessId = [int]$proc.ProcessId; ParentProcessId = [int]$proc.ParentProcessId; CreationUtcTicks = $actualStartTicks; Depth = 0 });',
|
|
1463
|
+
'}',
|
|
1464
|
+
'if ($validatedRoots.Count -eq 0) { exit 0 };',
|
|
1465
|
+
'$snapshot = @(Get-CimInstance Win32_Process -Property ProcessId,ParentProcessId,CreationDate | ForEach-Object {',
|
|
1466
|
+
' $ticks = if ($_.CreationDate) { [string]$_.CreationDate.ToUniversalTime().Ticks } else { "" };',
|
|
1467
|
+
' if ($ticks) { [pscustomobject]@{ ProcessId = [int]$_.ProcessId; ParentProcessId = [int]$_.ParentProcessId; CreationUtcTicks = $ticks } }',
|
|
1468
|
+
'});',
|
|
1469
|
+
'$targets = @{};',
|
|
1470
|
+
'foreach ($root in $validatedRoots) {',
|
|
1471
|
+
' $currentRoot = @($snapshot | Where-Object { [int]$_.ProcessId -eq [int]$root.ProcessId }) | Select-Object -First 1;',
|
|
1472
|
+
' if (-not $currentRoot -or [string]$currentRoot.CreationUtcTicks -ne [string]$root.CreationUtcTicks) { continue };',
|
|
1473
|
+
' $queue = [System.Collections.Generic.Queue[object]]::new();',
|
|
1474
|
+
' $queue.Enqueue([pscustomobject]@{ Record = $currentRoot; Depth = 0 });',
|
|
1475
|
+
' $seen = [System.Collections.Generic.HashSet[int]]::new();',
|
|
1476
|
+
' while ($queue.Count -gt 0) {',
|
|
1477
|
+
' $current = $queue.Dequeue();',
|
|
1478
|
+
' $currentRecord = $current.Record;',
|
|
1479
|
+
' $currentPid = [int]$currentRecord.ProcessId;',
|
|
1480
|
+
' if (-not $seen.Add($currentPid)) { continue };',
|
|
1481
|
+
' $key = "$currentPid`:$([string]$currentRecord.CreationUtcTicks)";',
|
|
1482
|
+
' $targets[$key] = [pscustomobject]@{ ProcessId = $currentPid; CreationUtcTicks = [string]$currentRecord.CreationUtcTicks; Depth = [int]$current.Depth };',
|
|
1483
|
+
' foreach ($child in @($snapshot | Where-Object { [int]$_.ParentProcessId -eq $currentPid })) {',
|
|
1484
|
+
' try { if ([System.Numerics.BigInteger]::Parse([string]$child.CreationUtcTicks) -lt [System.Numerics.BigInteger]::Parse([string]$currentRecord.CreationUtcTicks)) { continue } } catch { continue };',
|
|
1485
|
+
' $queue.Enqueue([pscustomobject]@{ Record = $child; Depth = [int]$current.Depth + 1 });',
|
|
1486
|
+
' }',
|
|
1487
|
+
' }',
|
|
1488
|
+
'}',
|
|
1489
|
+
'$failures = [System.Collections.Generic.List[string]]::new();',
|
|
1490
|
+
'$exitTicks = @{};',
|
|
1491
|
+
'$orderedTargets = @($targets.Values | Sort-Object @{ Expression = { -[int]$_.Depth } });',
|
|
1492
|
+
'foreach ($targetRecord in $orderedTargets) {',
|
|
1493
|
+
' $targetPid = [int]$targetRecord.ProcessId;',
|
|
1494
|
+
' $expectedStartTicks = [string]$targetRecord.CreationUtcTicks;',
|
|
1495
|
+
' $targetKey = "$targetPid`:$expectedStartTicks";',
|
|
1496
|
+
' try {',
|
|
1497
|
+
' $current = Get-CimInstance Win32_Process -Filter "ProcessId = $targetPid" -Property ProcessId,CreationDate -ErrorAction SilentlyContinue | Select-Object -First 1;',
|
|
1498
|
+
' if (-not $current) { continue };',
|
|
1499
|
+
' $currentStartTicks = if ($current.CreationDate) { [string]$current.CreationDate.ToUniversalTime().Ticks } else { "" };',
|
|
1500
|
+
' if ($currentStartTicks -ne $expectedStartTicks) { continue };',
|
|
1501
|
+
' Stop-Process -Id $targetPid -Force -ErrorAction Stop;',
|
|
1502
|
+
' $exitTicks[$targetKey] = [string][DateTime]::UtcNow.Ticks;',
|
|
1503
|
+
' } catch {',
|
|
1504
|
+
' $failures.Add("pid=$targetPid $($_.Exception.Message)");',
|
|
1505
|
+
' }',
|
|
1506
|
+
'}',
|
|
1507
|
+
'Start-Sleep -Milliseconds 75;',
|
|
1508
|
+
'$lateSnapshot = @(Get-CimInstance Win32_Process -Property ProcessId,ParentProcessId,CreationDate | ForEach-Object {',
|
|
1509
|
+
' $ticks = if ($_.CreationDate) { [string]$_.CreationDate.ToUniversalTime().Ticks } else { "" };',
|
|
1510
|
+
' if ($ticks) { [pscustomobject]@{ ProcessId = [int]$_.ProcessId; ParentProcessId = [int]$_.ParentProcessId; CreationUtcTicks = $ticks } }',
|
|
1511
|
+
'});',
|
|
1512
|
+
'$lateTargets = @{};',
|
|
1513
|
+
'$lateQueue = [System.Collections.Generic.Queue[object]]::new();',
|
|
1514
|
+
'foreach ($tracked in $targets.Values) {',
|
|
1515
|
+
' $trackedKey = "$([int]$tracked.ProcessId)`:$([string]$tracked.CreationUtcTicks)";',
|
|
1516
|
+
' if (-not $exitTicks.ContainsKey($trackedKey)) { $exitTicks[$trackedKey] = [string][DateTime]::UtcNow.Ticks };',
|
|
1517
|
+
' $lateQueue.Enqueue([pscustomobject]@{ Record = $tracked; Depth = [int]$tracked.Depth; ExitTicks = [string]$exitTicks[$trackedKey] });',
|
|
1518
|
+
'}',
|
|
1519
|
+
'$lateSeen = [System.Collections.Generic.HashSet[string]]::new();',
|
|
1520
|
+
'while ($lateQueue.Count -gt 0) {',
|
|
1521
|
+
' $parent = $lateQueue.Dequeue();',
|
|
1522
|
+
' $parentRecord = $parent.Record;',
|
|
1523
|
+
' $parentKey = "$([int]$parentRecord.ProcessId)`:$([string]$parentRecord.CreationUtcTicks)";',
|
|
1524
|
+
' if (-not $lateSeen.Add($parentKey)) { continue };',
|
|
1525
|
+
' foreach ($child in @($lateSnapshot | Where-Object { [int]$_.ParentProcessId -eq [int]$parentRecord.ProcessId })) {',
|
|
1526
|
+
' try {',
|
|
1527
|
+
' $childStart = [System.Numerics.BigInteger]::Parse([string]$child.CreationUtcTicks);',
|
|
1528
|
+
' if ($childStart -lt [System.Numerics.BigInteger]::Parse([string]$parentRecord.CreationUtcTicks)) { continue };',
|
|
1529
|
+
' if ($parent.ExitTicks -and $childStart -gt [System.Numerics.BigInteger]::Parse([string]$parent.ExitTicks)) { continue };',
|
|
1530
|
+
' } catch { continue };',
|
|
1531
|
+
' $childKey = "$([int]$child.ProcessId)`:$([string]$child.CreationUtcTicks)";',
|
|
1532
|
+
' $childDepth = [int]$parent.Depth + 1;',
|
|
1533
|
+
' $lateTargets[$childKey] = [pscustomobject]@{ ProcessId = [int]$child.ProcessId; CreationUtcTicks = [string]$child.CreationUtcTicks; Depth = $childDepth };',
|
|
1534
|
+
' $lateQueue.Enqueue([pscustomobject]@{ Record = $child; Depth = $childDepth; ExitTicks = "" });',
|
|
1535
|
+
' }',
|
|
1536
|
+
'}',
|
|
1537
|
+
'$orderedLateTargets = @($lateTargets.Values | Sort-Object @{ Expression = { -[int]$_.Depth } });',
|
|
1538
|
+
'foreach ($targetRecord in $orderedLateTargets) {',
|
|
1539
|
+
' $targetPid = [int]$targetRecord.ProcessId;',
|
|
1540
|
+
' $expectedStartTicks = [string]$targetRecord.CreationUtcTicks;',
|
|
1541
|
+
' try {',
|
|
1542
|
+
' $current = Get-CimInstance Win32_Process -Filter "ProcessId = $targetPid" -Property ProcessId,CreationDate -ErrorAction SilentlyContinue | Select-Object -First 1;',
|
|
1543
|
+
' if (-not $current) { continue };',
|
|
1544
|
+
' $currentStartTicks = if ($current.CreationDate) { [string]$current.CreationDate.ToUniversalTime().Ticks } else { "" };',
|
|
1545
|
+
' if ($currentStartTicks -ne $expectedStartTicks) { continue };',
|
|
1546
|
+
' Stop-Process -Id $targetPid -Force -ErrorAction Stop;',
|
|
1547
|
+
' } catch {',
|
|
1548
|
+
' $failures.Add("late pid=$targetPid $($_.Exception.Message)");',
|
|
1549
|
+
' }',
|
|
1550
|
+
'}',
|
|
1551
|
+
'if ($failures.Count -gt 0) { [Console]::Error.WriteLine(($failures -join "; ")); exit 1 };',
|
|
1552
|
+
'exit 0;'
|
|
1553
|
+
].join(' ');
|
|
1554
|
+
const result = await runQuiet(
|
|
1555
|
+
'powershell.exe',
|
|
1556
|
+
['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', script],
|
|
1557
|
+
{ timeout: 15000, killSignal: 'SIGKILL', maxBuffer: 4 * 1024 * 1024 }
|
|
1558
|
+
);
|
|
1559
|
+
if (!result.ok) {
|
|
1560
|
+
throw new Error(
|
|
1561
|
+
`Could not stop exact LiveDesk Windows port owner tree: `
|
|
1562
|
+
+ `${result.stderr || result.error?.message || 'PowerShell failed'}`
|
|
1563
|
+
);
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
async function stopWindowsProcessesOnPorts(ports) {
|
|
1568
|
+
const records = await collectWindowsPortRecords(ports);
|
|
1569
|
+
const killable = records.filter(record => record?.KnownLiveDesk === true);
|
|
1570
|
+
if (killable.length > 0) {
|
|
1571
|
+
await terminateExactWindowsPortOwners(killable);
|
|
1572
|
+
for (const record of killable) {
|
|
1573
|
+
record.Action = 'stopped';
|
|
1574
|
+
record.KillAttempted = true;
|
|
1444
1575
|
}
|
|
1445
1576
|
}
|
|
1446
1577
|
|
|
1447
1578
|
const probes = await Promise.all(ports.map(port => waitForPortBindable(port)));
|
|
1448
|
-
const finalRecords =
|
|
1579
|
+
const finalRecords = probes.every(probe => probe.ok)
|
|
1580
|
+
? []
|
|
1581
|
+
: await collectWindowsPortRecords(ports);
|
|
1449
1582
|
const portStates = ports.map((port, index) => {
|
|
1450
1583
|
const busy = finalRecords.some(record => Number(record?.Port) === Number(port));
|
|
1451
1584
|
return {
|
package/client/README.md
CHANGED
|
@@ -62,9 +62,9 @@ System Settings path when access is missing; after granting access, restart the
|
|
|
62
62
|
client. When file transfer is enabled by the Hub, received files are saved
|
|
63
63
|
to `Desktop/LiveDeskFiles` unless the Hub sets another destination folder.
|
|
64
64
|
|
|
65
|
-
By default, the launcher uses the packaged C# RemoteFast engine when supported.
|
|
66
|
-
It falls back to the Node engine
|
|
67
|
-
|
|
65
|
+
By default, the launcher uses the packaged C# RemoteFast engine when supported.
|
|
66
|
+
It falls back to the Node engine only when a compatible RemoteFast runtime is
|
|
67
|
+
unavailable. Windows, macOS, and Linux all try RemoteFast first so
|
|
68
68
|
the Hub can request the Mode 3 hardware video path when it is available.
|
|
69
69
|
RemoteFast is shipped as a platform-specific self-contained .NET executable,
|
|
70
70
|
so client computers do not need a separate .NET installation.
|
|
@@ -1571,15 +1571,16 @@ function buildClientUpdateBootstrapScript() {
|
|
|
1571
1571
|
'const isGroupAlive = pid => { try { process.kill(-pid, 0); return true; } catch (error) { return error?.code === "EPERM"; } };',
|
|
1572
1572
|
'const runWindowsPowerShell = script => new Promise((resolve, reject) => { execFile("powershell.exe", ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", script], { windowsHide: true, timeout: 15000, killSignal: "SIGKILL", maxBuffer: 4 * 1024 * 1024 }, (error, stdout, stderr) => { if (error) { reject(new Error(String(stderr || error.message || "PowerShell failed").trim())); return; } resolve(String(stdout || "").trim()); }); });',
|
|
1573
1573
|
'const queryWindowsProcessTable = async () => { const stdout = await runWindowsPowerShell(`$ErrorActionPreference = "Stop"; $records = @(Get-CimInstance Win32_Process -Property ProcessId,ParentProcessId,CreationDate | ForEach-Object { $ticks = if ($_.CreationDate) { [string]$_.CreationDate.ToUniversalTime().Ticks } else { "" }; [pscustomobject]@{ ProcessId = [int]$_.ProcessId; ParentProcessId = [int]$_.ParentProcessId; CreationUtcTicks = $ticks } }); [pscustomobject]@{ Records = $records } | ConvertTo-Json -Compress -Depth 4`); if (!stdout) throw new Error("Windows process snapshot returned no data"); const parsed = JSON.parse(stdout); const values = parsed?.Records == null ? [] : (Array.isArray(parsed.Records) ? parsed.Records : [parsed.Records]); return values.map(item => ({ pid: Number(item?.ProcessId || 0), parentPid: Number(item?.ParentProcessId || 0), startOrder: String(item?.CreationUtcTicks || "") })).filter(item => Number.isInteger(item.pid) && item.pid > 1 && /^\\d+$/.test(item.startOrder)); };',
|
|
1574
|
+
'const queryTrackedWindowsProcessTable = child => { if (child.livedeskWindowsSnapshotPromise) return child.livedeskWindowsSnapshotPromise; const pending = queryWindowsProcessTable(); child.livedeskWindowsSnapshotPromise = pending; const clear = () => { if (child.livedeskWindowsSnapshotPromise === pending) child.livedeskWindowsSnapshotPromise = null; }; pending.then(clear, clear); return pending; };',
|
|
1574
1575
|
'const sameWindowsIdentity = (left, right) => Number(left?.pid || 0) === Number(right?.pid || 0) && String(left?.startOrder || "") === String(right?.startOrder || "");',
|
|
1575
|
-
'const captureWindowsIdentity = async
|
|
1576
|
+
'const captureWindowsIdentity = async child => { const processId = Number(child?.pid || 0); const deadline = Date.now() + 2500; let lastError = null; do { try { const snapshot = await queryTrackedWindowsProcessTable(child); const identity = snapshot.find(item => item.pid === processId) || null; if (identity) return identity; } catch (error) { lastError = error; } if (Date.now() < deadline) await sleep(50); } while (Date.now() < deadline); throw new Error(`Could not capture immutable Windows CreationDate for pid=${processId}: ${lastError?.message || "process not visible"}`); };',
|
|
1576
1577
|
'const windowsTicksAtMilliseconds = value => BigInt(Math.trunc(Number(value) || Date.now())) * 10000n + 621355968000000000n;',
|
|
1577
1578
|
'const setWindowsRootLifetimeEnd = (child, ticks) => { const candidate = BigInt(ticks); const previous = child.livedeskWindowsRootLifetimeEndTicks; if (!previous || candidate < BigInt(previous)) child.livedeskWindowsRootLifetimeEndTicks = String(candidate); return BigInt(child.livedeskWindowsRootLifetimeEndTicks); };',
|
|
1578
1579
|
'const mergeRootAbsentWindowsTree = (child, rootPid, snapshot) => { const tracked = child.livedeskWindowsTrackedRecords || (child.livedeskWindowsTrackedRecords = new Map()); const currentByPid = new Map(snapshot.map(item => [item.pid, item])); const exactCurrentByPid = new Map(); for (const record of tracked.values()) { const current = currentByPid.get(record.pid); if (sameWindowsIdentity(current, record)) exactCurrentByPid.set(record.pid, record); } const lowerBound = windowsTicksAtMilliseconds(Number(child.livedeskWindowsSpawnedAtMs || Date.now()) - 2000); const upperBound = child.livedeskWindowsRootLifetimeEndTicks ? BigInt(child.livedeskWindowsRootLifetimeEndTicks) : null; let changed = true; while (changed) { changed = false; for (const record of snapshot) { const key = `${record.pid}:${record.startOrder}`; if (record.pid === rootPid || tracked.has(key)) continue; let recordStart; try { recordStart = BigInt(record.startOrder); if (recordStart < lowerBound || (upperBound && recordStart > upperBound)) continue; } catch { continue; } const parent = record.parentPid === rootPid ? { depth: 0 } : exactCurrentByPid.get(record.parentPid); if (!parent) continue; const descendant = { ...record, depth: Number(parent.depth || 0) + 1 }; tracked.set(key, descendant); exactCurrentByPid.set(descendant.pid, descendant); changed = true; } } return tracked; };',
|
|
1579
1580
|
'const mergeExactWindowsTree = (child, root, snapshot) => { const tracked = child.livedeskWindowsTrackedRecords || (child.livedeskWindowsTrackedRecords = new Map()); const currentByPid = new Map(snapshot.map(item => [item.pid, item])); const currentRoot = currentByPid.get(root.pid) || null; const rootIsExact = sameWindowsIdentity(currentRoot, root); const rootWasReused = Boolean(currentRoot && !rootIsExact); if (rootWasReused) setWindowsRootLifetimeEnd(child, BigInt(currentRoot.startOrder) - 1n); else if (!currentRoot && child.livedeskWindowsRootExitedAtMs) setWindowsRootLifetimeEnd(child, windowsTicksAtMilliseconds(child.livedeskWindowsRootExitedAtMs)); const rootLifetimeClosed = Boolean(child.livedeskWindowsRootLifetimeEndTicks); if (rootIsExact) tracked.set(`${root.pid}:${root.startOrder}`, { ...root, depth: 0 }); const exactCurrentByPid = new Map(); if (rootIsExact) exactCurrentByPid.set(root.pid, { ...root, depth: 0 }); for (const record of tracked.values()) { const current = currentByPid.get(record.pid); if (sameWindowsIdentity(current, record)) exactCurrentByPid.set(record.pid, record); } let changed = true; while (changed) { changed = false; for (const record of snapshot) { const key = `${record.pid}:${record.startOrder}`; if (record.pid === root.pid || tracked.has(key)) continue; let recordStart; try { recordStart = BigInt(record.startOrder); if (recordStart < BigInt(root.startOrder)) continue; if (rootLifetimeClosed && recordStart > BigInt(child.livedeskWindowsRootLifetimeEndTicks)) continue; } catch { continue; } let parent = exactCurrentByPid.get(record.parentPid) || null; if (!parent && !currentRoot && rootLifetimeClosed && record.parentPid === root.pid) parent = { ...root, depth: 0 }; if (!parent) continue; const descendant = { ...record, depth: Number(parent.depth || 0) + 1 }; tracked.set(key, descendant); exactCurrentByPid.set(descendant.pid, descendant); changed = true; } } return tracked; };',
|
|
1580
|
-
'const refreshTrackedWindowsTree = async (child, root = child.livedeskWindowsIdentity || null) => { const snapshot = await
|
|
1581
|
+
'const refreshTrackedWindowsTree = async (child, root = child.livedeskWindowsIdentity || null) => { const snapshot = await queryTrackedWindowsProcessTable(child); if (root) mergeExactWindowsTree(child, root, snapshot); else mergeRootAbsentWindowsTree(child, Number(child.pid || 0), snapshot); return snapshot; };',
|
|
1581
1582
|
'const stopExactWindowsRecords = async records => { if (!Array.isArray(records) || records.length === 0) return; const encoded = Buffer.from(JSON.stringify(records.map(record => ({ pid: record.pid, startOrder: record.startOrder }))), "utf8").toString("base64"); const script = `$ErrorActionPreference = "Stop"; $targetsJson = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String("${encoded}")); $targets = ConvertFrom-Json -InputObject $targetsJson; $failures = [System.Collections.Generic.List[string]]::new(); foreach ($item in $targets) { $targetPid = [int]$item.pid; $expectedStartTicks = [string]$item.startOrder; try { $target = Get-CimInstance Win32_Process -Filter "ProcessId = $targetPid" -Property ProcessId,CreationDate -ErrorAction SilentlyContinue | Select-Object -First 1; if (-not $target) { continue }; $targetStartTicks = if ($target.CreationDate) { [string]$target.CreationDate.ToUniversalTime().Ticks } else { "" }; if ($targetStartTicks -ne $expectedStartTicks) { continue }; Stop-Process -Id $targetPid -Force -ErrorAction Stop } catch { $failures.Add("pid=$targetPid $($_.Exception.Message)") } }; if ($failures.Count -gt 0) { [Console]::Error.WriteLine(($failures -join "; ")); exit 1 }`; await runWindowsPowerShell(script); };',
|
|
1582
|
-
'const trackWindowsChild = child => { if (process.platform !== "win32" || Number(child?.pid || 0) <= 1 || child.livedeskWindowsIdentityPromise) return; child.livedeskWindowsTrackedRecords = new Map(); child.livedeskWindowsSpawnedAtMs = Date.now(); child.livedeskWindowsRootExitedAtMs = null; child.livedeskWindowsTrackingStopped = false; child.once("exit", () => { if (!child.livedeskWindowsRootExitedAtMs) child.livedeskWindowsRootExitedAtMs = Date.now(); setWindowsRootLifetimeEnd(child, windowsTicksAtMilliseconds(child.livedeskWindowsRootExitedAtMs)); }); const refresh = () => { void refreshTrackedWindowsTree(child).then(() => { child.livedeskWindowsTrackingError = null; }).catch(error => { child.livedeskWindowsTrackingError = error; }); }; refresh(); child.livedeskWindowsTrackingTimer = setInterval(refresh, 250); child.livedeskWindowsTrackingTimer.unref?.(); child.livedeskWindowsIdentityPromise = (async () => { while (!child.livedeskWindowsTrackingStopped && !child.livedeskWindowsRootExitedAtMs) { try { const identity = await captureWindowsIdentity(child
|
|
1583
|
+
'const trackWindowsChild = child => { if (process.platform !== "win32" || Number(child?.pid || 0) <= 1 || child.livedeskWindowsIdentityPromise) return; child.livedeskWindowsTrackedRecords = new Map(); child.livedeskWindowsSpawnedAtMs = Date.now(); child.livedeskWindowsRootExitedAtMs = null; child.livedeskWindowsTrackingStopped = false; child.once("exit", () => { if (!child.livedeskWindowsRootExitedAtMs) child.livedeskWindowsRootExitedAtMs = Date.now(); setWindowsRootLifetimeEnd(child, windowsTicksAtMilliseconds(child.livedeskWindowsRootExitedAtMs)); }); const refresh = () => { void refreshTrackedWindowsTree(child).then(() => { child.livedeskWindowsTrackingError = null; }).catch(error => { child.livedeskWindowsTrackingError = error; }); }; refresh(); child.livedeskWindowsTrackingTimer = setInterval(refresh, 250); child.livedeskWindowsTrackingTimer.unref?.(); child.livedeskWindowsIdentityPromise = (async () => { while (!child.livedeskWindowsTrackingStopped && !child.livedeskWindowsRootExitedAtMs) { try { const identity = await captureWindowsIdentity(child); child.livedeskWindowsIdentity = identity; refresh(); return identity; } catch (error) { child.livedeskWindowsIdentityError = error; if (!child.livedeskWindowsTrackingStopped && !child.livedeskWindowsRootExitedAtMs) await sleep(250); } } return null; })(); };',
|
|
1583
1584
|
'const stopWindowsTracking = child => { child.livedeskWindowsTrackingStopped = true; if (child?.livedeskWindowsTrackingTimer) clearInterval(child.livedeskWindowsTrackingTimer); child.livedeskWindowsTrackingTimer = null; };',
|
|
1584
1585
|
'const terminateExactWindowsTrackedTree = (child, root) => { if (child.livedeskWindowsDrainPromise) return child.livedeskWindowsDrainPromise; child.livedeskWindowsDrainPromise = (async () => { let attempt = 0; let emptyPasses = 0; for (;;) { attempt += 1; try { const snapshot = await refreshTrackedWindowsTree(child, root); const currentByPid = new Map(snapshot.map(item => [item.pid, item])); const remaining = [...child.livedeskWindowsTrackedRecords.values()].filter(target => sameWindowsIdentity(currentByPid.get(target.pid), target)).sort((left, right) => Number(right.depth || 0) - Number(left.depth || 0)); if (remaining.length === 0) { emptyPasses += 1; if (emptyPasses >= 2) { stopWindowsTracking(child); return; } } else { emptyPasses = 0; await stopExactWindowsRecords(remaining); } child.livedeskWindowsTrackingError = null; } catch (error) { emptyPasses = 0; child.livedeskWindowsTrackingError = error; if (attempt === 1 || attempt % 10 === 0) process.stderr.write(`LiveDesk update cleanup is retrying exact Windows process-tree drain: ${error?.message || error}\\n`); } await sleep(Math.min(1000, 100 + (attempt * 50))); } })(); return child.livedeskWindowsDrainPromise; };',
|
|
1585
1586
|
'const stateLockPath = statePath + ".lock"; const lockWaitBuffer = new Int32Array(new SharedArrayBuffer(4));',
|
package/client/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@livedesk/client",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.203",
|
|
4
4
|
"description": "LiveDesk local remote client",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -41,10 +41,10 @@
|
|
|
41
41
|
"ws": "^8.18.3"
|
|
42
42
|
},
|
|
43
43
|
"optionalDependencies": {
|
|
44
|
-
"@livedesk/fast-linux-x64": "0.1.
|
|
45
|
-
"@livedesk/fast-osx-arm64": "0.1.
|
|
46
|
-
"@livedesk/fast-osx-x64": "0.1.
|
|
47
|
-
"@livedesk/fast-win-x64": "0.1.
|
|
44
|
+
"@livedesk/fast-linux-x64": "0.1.411",
|
|
45
|
+
"@livedesk/fast-osx-arm64": "0.1.411",
|
|
46
|
+
"@livedesk/fast-osx-x64": "0.1.411",
|
|
47
|
+
"@livedesk/fast-win-x64": "0.1.411"
|
|
48
48
|
},
|
|
49
49
|
"publishConfig": {
|
|
50
50
|
"access": "public"
|
package/hub/package.json
CHANGED
|
@@ -1,175 +1,132 @@
|
|
|
1
|
-
import os from 'node:os';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { AgentSettingsStore, AGENT_PROVIDER_CODEX
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
},
|
|
134
|
-
async testConnection() {
|
|
135
|
-
const { current, active } = await currentProvider();
|
|
136
|
-
if (current.provider === AGENT_PROVIDER_CODEX) {
|
|
137
|
-
if (!active) throw new AgentProviderError('codex-sdk-not-installed', 'Codex SDK is not installed.', { status: 503 });
|
|
138
|
-
statusCache = { expiresAt: 0, value: null };
|
|
139
|
-
return active.testConnection();
|
|
140
|
-
}
|
|
141
|
-
return active.testConnection(current, await legacyApiKey());
|
|
142
|
-
},
|
|
143
|
-
async createPlan(input) {
|
|
144
|
-
const { current, active } = await currentProvider();
|
|
145
|
-
if (!current.enabled) throw new AgentProviderError('agent-ai-disabled', 'Agent AI is disabled in Settings.', { status: 409 });
|
|
146
|
-
if (current.provider === AGENT_PROVIDER_CODEX) {
|
|
147
|
-
throw new AgentProviderError('agent-codex-run-required', 'Codex runs use the LiveDesk tool loop.', { status: 409 });
|
|
148
|
-
}
|
|
149
|
-
return active.createPlan(input, current, await legacyApiKey());
|
|
150
|
-
},
|
|
151
|
-
async createSummary(input) {
|
|
152
|
-
const { current, active } = await currentProvider();
|
|
153
|
-
if (!current.enabled || !current.generateSummary) throw new AgentProviderError('agent-summary-disabled', 'Agent summaries are disabled in Settings.', { status: 409 });
|
|
154
|
-
if (current.provider === AGENT_PROVIDER_CODEX) {
|
|
155
|
-
const source = input && typeof input === 'object' ? input : {};
|
|
156
|
-
const results = Array.isArray(source.results) ? source.results : [];
|
|
157
|
-
return { summary: String(results.length ? `${source.completed || 0} completed, ${source.failed || 0} failed.` : 'LiveDesk Agent run completed.').slice(0, 1200), modelId: current.codexModelId || 'Codex default' };
|
|
158
|
-
}
|
|
159
|
-
const source = input && typeof input === 'object' ? input : {};
|
|
160
|
-
const results = Array.isArray(source.results) ? source.results : [];
|
|
161
|
-
const safeResults = current.includeRawDeviceDetails
|
|
162
|
-
? results.map(result => ({ deviceName: String(result?.deviceName || '').slice(0, 120), status: String(result?.status || '').slice(0, 40), result: String(result?.result || '').slice(0, 1200), error: String(result?.error || '').slice(0, 500) }))
|
|
163
|
-
: results.map(result => ({ deviceName: String(result?.deviceName || '').slice(0, 120), status: String(result?.status || '').slice(0, 40), hasResult: Boolean(result?.result), hasError: Boolean(result?.error) }));
|
|
164
|
-
return active.createSummary({ instruction: String(source.instruction || '').slice(0, 500), operation: String(source.operation || '').slice(0, 80), summary: { total: Number(source.total || results.length), completed: Number(source.completed || 0), failed: Number(source.failed || 0), results: safeResults } }, current, await legacyApiKey());
|
|
165
|
-
},
|
|
166
|
-
async startRun(input) {
|
|
167
|
-
const current = await settings.get();
|
|
168
|
-
if (current.provider !== AGENT_PROVIDER_CODEX || !runtime) throw new AgentProviderError('agent-codex-not-active', 'Codex SDK is not the active Agent provider.', { status: 409 });
|
|
169
|
-
return runtime.start(input);
|
|
170
|
-
},
|
|
171
|
-
getRun(runId) { return runtime?.get(runId) || null; },
|
|
172
|
-
cancelRun(runId) { return runtime?.cancel(runId) || null; },
|
|
173
|
-
cancelAllRuns() { return runtime?.cancelAll?.() || 0; }
|
|
174
|
-
};
|
|
175
|
-
}
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { AgentSettingsStore, AGENT_PROVIDER_CODEX } from './agent-settings.js';
|
|
4
|
+
import { AgentProviderError } from './provider-errors.js';
|
|
5
|
+
|
|
6
|
+
function publicCodexStatus(status = {}) {
|
|
7
|
+
return {
|
|
8
|
+
installed: status.installed === true,
|
|
9
|
+
authenticated: ['signed-in', 'not-signed-in'].includes(status.authenticated) ? status.authenticated : 'unknown',
|
|
10
|
+
status: String(status.status || 'unknown').slice(0, 40),
|
|
11
|
+
codexPath: String(status.codexPath || '').slice(0, 500),
|
|
12
|
+
detail: String(status.detail || '').slice(0, 300)
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function createAgentManager({
|
|
17
|
+
dataDir = path.join(os.homedir(), '.livedesk'),
|
|
18
|
+
settingsStore,
|
|
19
|
+
codexRuntime
|
|
20
|
+
} = {}) {
|
|
21
|
+
const settings = settingsStore || new AgentSettingsStore({ dataDir });
|
|
22
|
+
const runtime = codexRuntime;
|
|
23
|
+
let statusCache = { expiresAt: 0, value: null };
|
|
24
|
+
|
|
25
|
+
async function getCodexStatus() {
|
|
26
|
+
if (!runtime) return { installed: false, authenticated: 'unknown', status: 'unavailable', codexPath: '' };
|
|
27
|
+
if (statusCache.value && statusCache.expiresAt > Date.now()) return statusCache.value;
|
|
28
|
+
let value;
|
|
29
|
+
try {
|
|
30
|
+
value = publicCodexStatus(await runtime.getStatus());
|
|
31
|
+
} catch (error) {
|
|
32
|
+
value = publicCodexStatus({
|
|
33
|
+
installed: true,
|
|
34
|
+
authenticated: 'unknown',
|
|
35
|
+
status: error?.code || 'security-unavailable',
|
|
36
|
+
detail: error?.message || 'Codex security isolation is unavailable.'
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
statusCache = { value, expiresAt: Date.now() + 10000 };
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function publicSettings() {
|
|
44
|
+
const current = await settings.get();
|
|
45
|
+
const { codexMaxTurns: _codexMaxTurns, ...exposedSettings } = current;
|
|
46
|
+
const codex = await getCodexStatus();
|
|
47
|
+
const securityStatus = runtime?.getSecurityStatus?.() || {
|
|
48
|
+
isolatedCodexHome: false,
|
|
49
|
+
restrictedEnvironment: false,
|
|
50
|
+
livedeskMcpOnly: false,
|
|
51
|
+
selectedDeviceScopeEnforced: false,
|
|
52
|
+
failClosed: true,
|
|
53
|
+
verified: false,
|
|
54
|
+
state: 'unavailable'
|
|
55
|
+
};
|
|
56
|
+
return {
|
|
57
|
+
...exposedSettings,
|
|
58
|
+
provider: AGENT_PROVIDER_CODEX,
|
|
59
|
+
codexInstallation: codex.installed ? 'installed' : 'not-installed',
|
|
60
|
+
codexAuth: codex.authenticated,
|
|
61
|
+
codexStatus: codex.status,
|
|
62
|
+
codexPath: codex.codexPath,
|
|
63
|
+
agentWorkspacePath: current.codexWorkingDirectory,
|
|
64
|
+
securityStatus
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
getSettings: publicSettings,
|
|
70
|
+
async updateSettings(patch = {}) {
|
|
71
|
+
const nextPatch = { ...patch };
|
|
72
|
+
const requestedProvider = String(nextPatch.provider || AGENT_PROVIDER_CODEX).trim().toLowerCase();
|
|
73
|
+
const suppliedLegacyKey = typeof nextPatch.apiKey === 'string' && nextPatch.apiKey.trim().length > 0;
|
|
74
|
+
delete nextPatch.provider;
|
|
75
|
+
delete nextPatch.apiKey;
|
|
76
|
+
if (requestedProvider !== AGENT_PROVIDER_CODEX || suppliedLegacyKey) {
|
|
77
|
+
throw new AgentProviderError(
|
|
78
|
+
'agent-provider-retired',
|
|
79
|
+
'LiveDesk uses the Hub-hosted Codex Agent and does not configure alternate model providers.',
|
|
80
|
+
{ status: 400 }
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
await settings.update({ ...nextPatch, provider: AGENT_PROVIDER_CODEX });
|
|
84
|
+
statusCache = { expiresAt: 0, value: null };
|
|
85
|
+
return publicSettings();
|
|
86
|
+
},
|
|
87
|
+
async resetSettings() {
|
|
88
|
+
await settings.reset();
|
|
89
|
+
statusCache = { expiresAt: 0, value: null };
|
|
90
|
+
return publicSettings();
|
|
91
|
+
},
|
|
92
|
+
// Kept as a harmless compatibility endpoint for an older cached web UI.
|
|
93
|
+
// No credential is read, written, or returned by the current Hub.
|
|
94
|
+
async deleteApiKey() {
|
|
95
|
+
return publicSettings();
|
|
96
|
+
},
|
|
97
|
+
async getModels() {
|
|
98
|
+
return [{
|
|
99
|
+
id: '',
|
|
100
|
+
name: 'Codex default',
|
|
101
|
+
protocol: 'codex-sdk',
|
|
102
|
+
available: true,
|
|
103
|
+
capabilities: { supportsReasoningLevel: true, supportsStreaming: true, supportsTools: true }
|
|
104
|
+
}];
|
|
105
|
+
},
|
|
106
|
+
async testConnection() {
|
|
107
|
+
if (!runtime) throw new AgentProviderError('codex-sdk-not-installed', 'Codex SDK is not installed.', { status: 503 });
|
|
108
|
+
statusCache = { expiresAt: 0, value: null };
|
|
109
|
+
return runtime.testConnection();
|
|
110
|
+
},
|
|
111
|
+
async createPlan() {
|
|
112
|
+
throw new AgentProviderError('agent-codex-run-required', 'Codex Agent requests use the LiveDesk tool loop.', { status: 409 });
|
|
113
|
+
},
|
|
114
|
+
async createSummary(input) {
|
|
115
|
+
const source = input && typeof input === 'object' ? input : {};
|
|
116
|
+
const results = Array.isArray(source.results) ? source.results : [];
|
|
117
|
+
return {
|
|
118
|
+
summary: String(results.length ? `${source.completed || 0} completed, ${source.failed || 0} failed.` : 'LiveDesk Agent run completed.').slice(0, 1200)
|
|
119
|
+
};
|
|
120
|
+
},
|
|
121
|
+
async startRun(input) {
|
|
122
|
+
const current = await settings.get();
|
|
123
|
+
if (current.provider !== AGENT_PROVIDER_CODEX || !runtime) {
|
|
124
|
+
throw new AgentProviderError('agent-codex-not-active', 'Codex SDK is not the active Agent runtime.', { status: 409 });
|
|
125
|
+
}
|
|
126
|
+
return runtime.start(input);
|
|
127
|
+
},
|
|
128
|
+
getRun(runId) { return runtime?.get(runId) || null; },
|
|
129
|
+
cancelRun(runId) { return runtime?.cancel(runId) || null; },
|
|
130
|
+
cancelAllRuns() { return runtime?.cancelAll?.() || 0; }
|
|
131
|
+
};
|
|
132
|
+
}
|