@phnx-labs/agents-cli 1.20.34 → 1.20.36
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/CHANGELOG.md +113 -0
- package/LICENSE +185 -21
- package/README.md +13 -5
- package/dist/commands/audit.d.ts +14 -0
- package/dist/commands/audit.js +68 -0
- package/dist/commands/browser.js +82 -8
- package/dist/commands/check.d.ts +15 -0
- package/dist/commands/check.js +84 -0
- package/dist/commands/cloud.js +143 -3
- package/dist/commands/computer-actions.d.ts +15 -0
- package/dist/commands/computer-actions.js +120 -22
- package/dist/commands/computer.js +81 -0
- package/dist/commands/cost.js +17 -12
- package/dist/commands/daemon.js +4 -1
- package/dist/commands/doctor.d.ts +1 -0
- package/dist/commands/doctor.js +54 -97
- package/dist/commands/events.d.ts +16 -0
- package/dist/commands/events.js +173 -0
- package/dist/commands/exec.d.ts +19 -0
- package/dist/commands/exec.js +283 -12
- package/dist/commands/hosts.js +37 -29
- package/dist/commands/inspect.d.ts +2 -0
- package/dist/commands/inspect.js +62 -16
- package/dist/commands/lock.d.ts +12 -0
- package/dist/commands/lock.js +70 -0
- package/dist/commands/logs.d.ts +17 -0
- package/dist/commands/logs.js +139 -0
- package/dist/commands/message.d.ts +15 -0
- package/dist/commands/message.js +56 -0
- package/dist/commands/models.d.ts +3 -0
- package/dist/commands/models.js +27 -8
- package/dist/commands/permissions.js +9 -2
- package/dist/commands/repo.d.ts +34 -0
- package/dist/commands/repo.js +243 -65
- package/dist/commands/resource-view.d.ts +20 -0
- package/dist/commands/resource-view.js +90 -28
- package/dist/commands/routines.js +101 -5
- package/dist/commands/secrets-migrate.js +106 -57
- package/dist/commands/secrets.d.ts +31 -18
- package/dist/commands/secrets.js +196 -91
- package/dist/commands/serve.d.ts +10 -0
- package/dist/commands/serve.js +37 -0
- package/dist/commands/sessions-inject.d.ts +14 -0
- package/dist/commands/sessions-inject.js +111 -0
- package/dist/commands/sessions-picker.d.ts +2 -0
- package/dist/commands/sessions-picker.js +24 -3
- package/dist/commands/sessions-resume.d.ts +2 -0
- package/dist/commands/sessions-resume.js +221 -0
- package/dist/commands/sessions-tail.d.ts +10 -0
- package/dist/commands/sessions-tail.js +11 -0
- package/dist/commands/sessions.d.ts +143 -1
- package/dist/commands/sessions.js +628 -80
- package/dist/commands/setup.js +4 -2
- package/dist/commands/sync.d.ts +3 -1
- package/dist/commands/sync.js +156 -4
- package/dist/commands/teams.js +217 -0
- package/dist/commands/versions.js +2 -4
- package/dist/commands/view.d.ts +3 -0
- package/dist/commands/view.js +19 -8
- package/dist/commands/wallet.d.ts +6 -0
- package/dist/commands/wallet.js +22 -5
- package/dist/commands/watchdog.d.ts +18 -0
- package/dist/commands/watchdog.js +238 -0
- package/dist/index.js +82 -32
- package/dist/lib/agents.js +30 -9
- package/dist/lib/audit/log.d.ts +92 -0
- package/dist/lib/audit/log.js +177 -0
- package/dist/lib/auto-pull.js +2 -1
- package/dist/lib/browser/cdp.js +5 -1
- package/dist/lib/browser/chrome.d.ts +10 -0
- package/dist/lib/browser/chrome.js +18 -7
- package/dist/lib/browser/drivers/ssh.d.ts +8 -0
- package/dist/lib/browser/drivers/ssh.js +77 -16
- package/dist/lib/browser/har.d.ts +84 -0
- package/dist/lib/browser/har.js +77 -0
- package/dist/lib/browser/ipc.d.ts +3 -0
- package/dist/lib/browser/ipc.js +37 -12
- package/dist/lib/browser/profiles.d.ts +1 -1
- package/dist/lib/browser/profiles.js +8 -10
- package/dist/lib/browser/refs.d.ts +65 -0
- package/dist/lib/browser/refs.js +73 -1
- package/dist/lib/browser/runtime-state.js +1 -0
- package/dist/lib/browser/service.d.ts +38 -2
- package/dist/lib/browser/service.js +183 -23
- package/dist/lib/browser/types.d.ts +14 -1
- package/dist/lib/budget/live-cloud.d.ts +42 -0
- package/dist/lib/budget/live-cloud.js +79 -0
- package/dist/lib/budget/live-team.d.ts +31 -0
- package/dist/lib/budget/live-team.js +115 -0
- package/dist/lib/cloud/codex.js +4 -0
- package/dist/lib/cloud/rush.d.ts +12 -1
- package/dist/lib/cloud/rush.js +13 -3
- package/dist/lib/cloud/types.d.ts +9 -0
- package/dist/lib/computer/dispatch.d.ts +8 -0
- package/dist/lib/computer/dispatch.js +125 -0
- package/dist/lib/computer/loop.d.ts +62 -0
- package/dist/lib/computer/loop.js +98 -0
- package/dist/lib/computer/model.d.ts +44 -0
- package/dist/lib/computer/model.js +157 -0
- package/dist/lib/concurrency.d.ts +19 -0
- package/dist/lib/concurrency.js +33 -0
- package/dist/lib/daemon.d.ts +92 -0
- package/dist/lib/daemon.js +222 -18
- package/dist/lib/devices/connect.d.ts +3 -2
- package/dist/lib/devices/connect.js +5 -3
- package/dist/lib/devices/registry.d.ts +7 -0
- package/dist/lib/devices/registry.js +24 -0
- package/dist/lib/devices/tailscale.js +1 -1
- package/dist/lib/drift.d.ts +52 -0
- package/dist/lib/drift.js +112 -0
- package/dist/lib/events.d.ts +10 -3
- package/dist/lib/events.js +101 -24
- package/dist/lib/exec.d.ts +17 -0
- package/dist/lib/exec.js +100 -12
- package/dist/lib/git.d.ts +27 -0
- package/dist/lib/git.js +56 -1
- package/dist/lib/hooks/cache.d.ts +6 -0
- package/dist/lib/hooks/cache.js +54 -12
- package/dist/lib/hooks.d.ts +27 -0
- package/dist/lib/hooks.js +135 -10
- package/dist/lib/hosts/dispatch.d.ts +15 -0
- package/dist/lib/hosts/dispatch.js +45 -13
- package/dist/lib/hosts/logs.d.ts +16 -0
- package/dist/lib/hosts/logs.js +74 -0
- package/dist/lib/hosts/option.js +1 -1
- package/dist/lib/hosts/passthrough.js +3 -1
- package/dist/lib/hosts/progress.d.ts +66 -0
- package/dist/lib/hosts/progress.js +125 -17
- package/dist/lib/hosts/ready.d.ts +51 -7
- package/dist/lib/hosts/ready.js +98 -25
- package/dist/lib/hosts/reconcile.d.ts +53 -0
- package/dist/lib/hosts/reconcile.js +81 -0
- package/dist/lib/hosts/registry.d.ts +19 -2
- package/dist/lib/hosts/registry.js +58 -2
- package/dist/lib/hosts/remote-cmd.d.ts +62 -1
- package/dist/lib/hosts/remote-cmd.js +70 -1
- package/dist/lib/hosts/remote-os.d.ts +17 -0
- package/dist/lib/hosts/remote-os.js +30 -0
- package/dist/lib/hosts/session-index.d.ts +34 -0
- package/dist/lib/hosts/session-index.js +56 -0
- package/dist/lib/hosts/tasks.d.ts +22 -0
- package/dist/lib/hosts/tasks.js +29 -0
- package/dist/lib/lock.d.ts +93 -0
- package/dist/lib/lock.js +207 -0
- package/dist/lib/loop.js +16 -1
- package/dist/lib/machine-id.d.ts +21 -0
- package/dist/lib/machine-id.js +26 -0
- package/dist/lib/mailbox-target.d.ts +36 -0
- package/dist/lib/mailbox-target.js +45 -0
- package/dist/lib/mailbox.d.ts +47 -0
- package/dist/lib/mailbox.js +194 -0
- package/dist/lib/mcp.d.ts +5 -0
- package/dist/lib/mcp.js +24 -8
- package/dist/lib/migrate.d.ts +19 -0
- package/dist/lib/migrate.js +134 -26
- package/dist/lib/overdue.js +3 -0
- package/dist/lib/permissions.d.ts +4 -0
- package/dist/lib/permissions.js +35 -0
- package/dist/lib/picker.d.ts +26 -0
- package/dist/lib/picker.js +127 -0
- package/dist/lib/platform/exec.d.ts +46 -0
- package/dist/lib/platform/exec.js +74 -0
- package/dist/lib/platform/process.d.ts +31 -0
- package/dist/lib/platform/process.js +34 -1
- package/dist/lib/platform/winpath.js +2 -0
- package/dist/lib/plugin-marketplace.d.ts +30 -0
- package/dist/lib/plugin-marketplace.js +215 -2
- package/dist/lib/plugins.d.ts +5 -0
- package/dist/lib/plugins.js +61 -10
- package/dist/lib/profiles.d.ts +25 -0
- package/dist/lib/profiles.js +22 -6
- package/dist/lib/pty-client.js +2 -1
- package/dist/lib/rotate.d.ts +61 -0
- package/dist/lib/rotate.js +52 -0
- package/dist/lib/routines.d.ts +40 -2
- package/dist/lib/routines.js +66 -8
- package/dist/lib/runner.d.ts +11 -2
- package/dist/lib/runner.js +49 -7
- package/dist/lib/scheduler.js +6 -1
- package/dist/lib/secrets/bundles.d.ts +60 -4
- package/dist/lib/secrets/bundles.js +131 -12
- package/dist/lib/secrets/filestore.d.ts +3 -0
- package/dist/lib/secrets/filestore.js +42 -16
- package/dist/lib/secrets/index.d.ts +43 -2
- package/dist/lib/secrets/index.js +102 -3
- package/dist/lib/secrets/mcp.d.ts +93 -0
- package/dist/lib/secrets/mcp.js +205 -0
- package/dist/lib/secrets/remote.js +12 -5
- package/dist/lib/secrets/sync.js +83 -4
- package/dist/lib/secrets/windows.js +14 -3
- package/dist/lib/serve/data.d.ts +81 -0
- package/dist/lib/serve/data.js +91 -0
- package/dist/lib/serve/page.d.ts +7 -0
- package/dist/lib/serve/page.js +140 -0
- package/dist/lib/serve/server.d.ts +46 -0
- package/dist/lib/serve/server.js +115 -0
- package/dist/lib/session/active.d.ts +61 -0
- package/dist/lib/session/active.js +196 -17
- package/dist/lib/session/discover.d.ts +37 -0
- package/dist/lib/session/discover.js +204 -27
- package/dist/lib/session/inject.d.ts +18 -0
- package/dist/lib/session/inject.js +21 -0
- package/dist/lib/session/parse.d.ts +6 -0
- package/dist/lib/session/parse.js +286 -2
- package/dist/lib/session/pid-registry.d.ts +22 -0
- package/dist/lib/session/pid-registry.js +104 -0
- package/dist/lib/session/provenance.d.ts +14 -2
- package/dist/lib/session/provenance.js +39 -8
- package/dist/lib/session/remote-active.d.ts +26 -0
- package/dist/lib/session/remote-active.js +153 -0
- package/dist/lib/session/remote-list.d.ts +51 -0
- package/dist/lib/session/remote-list.js +213 -0
- package/dist/lib/session/remote.d.ts +7 -1
- package/dist/lib/session/remote.js +18 -8
- package/dist/lib/session/render.js +1 -1
- package/dist/lib/session/sync/config.d.ts +1 -7
- package/dist/lib/session/sync/config.js +4 -11
- package/dist/lib/session/types.d.ts +18 -1
- package/dist/lib/session/types.js +1 -1
- package/dist/lib/shims.d.ts +44 -6
- package/dist/lib/shims.js +143 -57
- package/dist/lib/ssh-exec.d.ts +26 -3
- package/dist/lib/ssh-exec.js +47 -3
- package/dist/lib/ssh-tunnel.d.ts +24 -5
- package/dist/lib/ssh-tunnel.js +62 -63
- package/dist/lib/staleness/writers/hooks.js +1 -1
- package/dist/lib/startup/command-registry.d.ts +8 -0
- package/dist/lib/startup/command-registry.js +17 -1
- package/dist/lib/state.d.ts +13 -0
- package/dist/lib/state.js +103 -9
- package/dist/lib/sync-umbrella.d.ts +14 -7
- package/dist/lib/sync-umbrella.js +17 -9
- package/dist/lib/teams/forEach.d.ts +110 -0
- package/dist/lib/teams/forEach.js +186 -0
- package/dist/lib/teams/index.d.ts +1 -0
- package/dist/lib/teams/index.js +1 -0
- package/dist/lib/teams/pr-watch.d.ts +226 -0
- package/dist/lib/teams/pr-watch.js +371 -0
- package/dist/lib/teams/registry.js +25 -9
- package/dist/lib/teams/supervisor.d.ts +14 -1
- package/dist/lib/teams/supervisor.js +19 -0
- package/dist/lib/teams/worktree.d.ts +9 -0
- package/dist/lib/teams/worktree.js +32 -0
- package/dist/lib/terminal/backends/ghostty.d.ts +6 -0
- package/dist/lib/terminal/backends/ghostty.js +69 -0
- package/dist/lib/terminal/backends/index.d.ts +19 -0
- package/dist/lib/terminal/backends/index.js +31 -0
- package/dist/lib/terminal/backends/iterm.d.ts +6 -0
- package/dist/lib/terminal/backends/iterm.js +62 -0
- package/dist/lib/terminal/backends/tmux.d.ts +14 -0
- package/dist/lib/terminal/backends/tmux.js +23 -0
- package/dist/lib/terminal/backends/vscodium-agent.d.ts +31 -0
- package/dist/lib/terminal/backends/vscodium-agent.js +72 -0
- package/dist/lib/terminal/engine.d.ts +39 -0
- package/dist/lib/terminal/engine.js +54 -0
- package/dist/lib/terminal/index.d.ts +17 -0
- package/dist/lib/terminal/index.js +8 -0
- package/dist/lib/terminal/inject.d.ts +204 -0
- package/dist/lib/terminal/inject.js +247 -0
- package/dist/lib/terminal/policy.d.ts +11 -0
- package/dist/lib/terminal/policy.js +11 -0
- package/dist/lib/terminal/quote.d.ts +11 -0
- package/dist/lib/terminal/quote.js +13 -0
- package/dist/lib/terminal/resolve.d.ts +64 -0
- package/dist/lib/terminal/resolve.js +90 -0
- package/dist/lib/terminal/shell.d.ts +6 -0
- package/dist/lib/terminal/shell.js +23 -0
- package/dist/lib/terminal/transport.d.ts +15 -0
- package/dist/lib/terminal/transport.js +40 -0
- package/dist/lib/terminal/types.d.ts +59 -0
- package/dist/lib/terminal/types.js +13 -0
- package/dist/lib/triggers/webhook.d.ts +85 -0
- package/dist/lib/triggers/webhook.js +141 -0
- package/dist/lib/types.d.ts +7 -0
- package/dist/lib/versions.d.ts +30 -3
- package/dist/lib/versions.js +129 -18
- package/dist/lib/watchdog/index.d.ts +3 -0
- package/dist/lib/watchdog/index.js +5 -0
- package/dist/lib/watchdog/read.d.ts +35 -0
- package/dist/lib/watchdog/read.js +149 -0
- package/dist/lib/watchdog/runner.d.ts +127 -0
- package/dist/lib/watchdog/runner.js +322 -0
- package/dist/lib/watchdog/watchdog.d.ts +40 -0
- package/dist/lib/watchdog/watchdog.js +166 -0
- package/dist/lib/watchdog/watchdogTail.d.ts +5 -0
- package/dist/lib/watchdog/watchdogTail.js +154 -0
- package/dist/lib/whats-new.d.ts +9 -0
- package/dist/lib/whats-new.js +35 -0
- package/dist/lib/workflows.d.ts +166 -0
- package/dist/lib/workflows.js +193 -0
- package/package.json +5 -4
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reconcile a local host-task record against the remote run's ground truth.
|
|
3
|
+
*
|
|
4
|
+
* A detached `--host` run outlives the local follower: it keeps running and
|
|
5
|
+
* writes its exit code to `<id>.exit` on the host even if the laptop sleeps or
|
|
6
|
+
* the SSH connection drops mid-follow. When that happens the local record is
|
|
7
|
+
* left at `status:'running'` forever, because the only path that finalizes it
|
|
8
|
+
* (dispatch's post-follow `updateTask`) never runs. This module re-reads the
|
|
9
|
+
* remote `.exit` on demand — from `agents hosts ps` / `logs` — and heals the
|
|
10
|
+
* record. We only ever CONFIRM completion; an unreachable host or an absent
|
|
11
|
+
* `.exit` leaves the record `running` (we never guess failure).
|
|
12
|
+
*/
|
|
13
|
+
import { type SshExecResult } from '../ssh-exec.js';
|
|
14
|
+
import { type HostTask } from './tasks.js';
|
|
15
|
+
export type RemoteExitState = {
|
|
16
|
+
state: 'running';
|
|
17
|
+
} | {
|
|
18
|
+
state: 'done';
|
|
19
|
+
code: number;
|
|
20
|
+
} | {
|
|
21
|
+
state: 'unreachable';
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Classify a `cat <remoteExit>` result into a remote run state. Pure: all the
|
|
25
|
+
* bug-prone branching (ssh-failure vs absent vs empty vs coded) lives here so it
|
|
26
|
+
* can be unit-tested without a live host. ssh's own connection failure surfaces
|
|
27
|
+
* as code 255, a spawn error/timeout as `code === null`; neither is the remote
|
|
28
|
+
* command's exit and both mean "unreachable". An empty read is "still running"
|
|
29
|
+
* (the `.exit` is written only after the run ends, so absent `cat` → exit 1 →
|
|
30
|
+
* empty stdout, and a truncate-then-write mid-race is a sub-ms empty window).
|
|
31
|
+
*/
|
|
32
|
+
export declare function classifyExit(res: Pick<SshExecResult, 'code' | 'stdout' | 'timedOut'>): RemoteExitState;
|
|
33
|
+
/**
|
|
34
|
+
* Read a task's remote `.exit` over ssh and classify it. `remoteExit` is a
|
|
35
|
+
* $HOME-prefixed path with a safe (hex) basename — intentionally unquoted so the
|
|
36
|
+
* remote shell expands $HOME (same contract as progress.ts's fetch).
|
|
37
|
+
*/
|
|
38
|
+
export declare function readRemoteExit(target: string, remoteExit: string, timeoutMs?: number): RemoteExitState;
|
|
39
|
+
/**
|
|
40
|
+
* Heal one record. Terminal records are immutable (and never re-probed); a
|
|
41
|
+
* `running` record is resolved to completed/failed only when the remote `.exit`
|
|
42
|
+
* holds a code. Returns the (possibly updated) task.
|
|
43
|
+
*/
|
|
44
|
+
export declare function reconcileTask(task: HostTask): HostTask;
|
|
45
|
+
/**
|
|
46
|
+
* Heal a list of records for a listing (`agents hosts ps`). Only `running` tasks
|
|
47
|
+
* are probed; each host is reachability-checked ONCE (deduped by target) so a
|
|
48
|
+
* down host costs a single short timeout instead of one per task, and its tasks
|
|
49
|
+
* are left `running` rather than falsely failed. Sequential by design — with the
|
|
50
|
+
* shared ssh control socket the live-host reads are sub-100ms, and a parallel
|
|
51
|
+
* (async) ssh path is deliberately out of scope.
|
|
52
|
+
*/
|
|
53
|
+
export declare function reconcileRunningTasks(tasks: HostTask[]): HostTask[];
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reconcile a local host-task record against the remote run's ground truth.
|
|
3
|
+
*
|
|
4
|
+
* A detached `--host` run outlives the local follower: it keeps running and
|
|
5
|
+
* writes its exit code to `<id>.exit` on the host even if the laptop sleeps or
|
|
6
|
+
* the SSH connection drops mid-follow. When that happens the local record is
|
|
7
|
+
* left at `status:'running'` forever, because the only path that finalizes it
|
|
8
|
+
* (dispatch's post-follow `updateTask`) never runs. This module re-reads the
|
|
9
|
+
* remote `.exit` on demand — from `agents hosts ps` / `logs` — and heals the
|
|
10
|
+
* record. We only ever CONFIRM completion; an unreachable host or an absent
|
|
11
|
+
* `.exit` leaves the record `running` (we never guess failure).
|
|
12
|
+
*/
|
|
13
|
+
import { sshExec, sshReachable } from '../ssh-exec.js';
|
|
14
|
+
import { updateTask, terminalPatch } from './tasks.js';
|
|
15
|
+
/**
|
|
16
|
+
* Classify a `cat <remoteExit>` result into a remote run state. Pure: all the
|
|
17
|
+
* bug-prone branching (ssh-failure vs absent vs empty vs coded) lives here so it
|
|
18
|
+
* can be unit-tested without a live host. ssh's own connection failure surfaces
|
|
19
|
+
* as code 255, a spawn error/timeout as `code === null`; neither is the remote
|
|
20
|
+
* command's exit and both mean "unreachable". An empty read is "still running"
|
|
21
|
+
* (the `.exit` is written only after the run ends, so absent `cat` → exit 1 →
|
|
22
|
+
* empty stdout, and a truncate-then-write mid-race is a sub-ms empty window).
|
|
23
|
+
*/
|
|
24
|
+
export function classifyExit(res) {
|
|
25
|
+
if (res.timedOut || res.code === null || res.code === 255)
|
|
26
|
+
return { state: 'unreachable' };
|
|
27
|
+
const out = res.stdout.trim();
|
|
28
|
+
if (out === '')
|
|
29
|
+
return { state: 'running' };
|
|
30
|
+
const code = parseInt(out, 10);
|
|
31
|
+
return { state: 'done', code: Number.isFinite(code) ? code : 0 };
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Read a task's remote `.exit` over ssh and classify it. `remoteExit` is a
|
|
35
|
+
* $HOME-prefixed path with a safe (hex) basename — intentionally unquoted so the
|
|
36
|
+
* remote shell expands $HOME (same contract as progress.ts's fetch).
|
|
37
|
+
*/
|
|
38
|
+
export function readRemoteExit(target, remoteExit, timeoutMs = 6000) {
|
|
39
|
+
return classifyExit(sshExec(target, `cat ${remoteExit} 2>/dev/null`, { timeoutMs, multiplex: true }));
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Heal one record. Terminal records are immutable (and never re-probed); a
|
|
43
|
+
* `running` record is resolved to completed/failed only when the remote `.exit`
|
|
44
|
+
* holds a code. Returns the (possibly updated) task.
|
|
45
|
+
*/
|
|
46
|
+
export function reconcileTask(task) {
|
|
47
|
+
if (task.status !== 'running')
|
|
48
|
+
return task;
|
|
49
|
+
const st = readRemoteExit(task.target, task.remoteExit);
|
|
50
|
+
if (st.state !== 'done')
|
|
51
|
+
return task;
|
|
52
|
+
return updateTask(task.id, terminalPatch(st.code)) ?? task;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Heal a list of records for a listing (`agents hosts ps`). Only `running` tasks
|
|
56
|
+
* are probed; each host is reachability-checked ONCE (deduped by target) so a
|
|
57
|
+
* down host costs a single short timeout instead of one per task, and its tasks
|
|
58
|
+
* are left `running` rather than falsely failed. Sequential by design — with the
|
|
59
|
+
* shared ssh control socket the live-host reads are sub-100ms, and a parallel
|
|
60
|
+
* (async) ssh path is deliberately out of scope.
|
|
61
|
+
*/
|
|
62
|
+
export function reconcileRunningTasks(tasks) {
|
|
63
|
+
const running = tasks.filter((t) => t.status === 'running');
|
|
64
|
+
if (running.length === 0)
|
|
65
|
+
return tasks;
|
|
66
|
+
const reachable = new Map();
|
|
67
|
+
const patched = new Map();
|
|
68
|
+
for (const t of running) {
|
|
69
|
+
if (!reachable.has(t.target))
|
|
70
|
+
reachable.set(t.target, sshReachable(t.target, 6000));
|
|
71
|
+
if (!reachable.get(t.target))
|
|
72
|
+
continue; // host down → leave running
|
|
73
|
+
const st = readRemoteExit(t.target, t.remoteExit);
|
|
74
|
+
if (st.state === 'done') {
|
|
75
|
+
const updated = updateTask(t.id, terminalPatch(st.code));
|
|
76
|
+
if (updated)
|
|
77
|
+
patched.set(t.id, updated);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return tasks.map((t) => patched.get(t.id) ?? t);
|
|
81
|
+
}
|
|
@@ -11,8 +11,25 @@ export declare function getAllProviders(): HostProvider[];
|
|
|
11
11
|
/** Every host across all registered providers, deduped by name (first wins). */
|
|
12
12
|
export declare function listAllHosts(): Promise<Host[]>;
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
* Thrown when a device resolves but can't be used as an offload target because
|
|
15
|
+
* it authenticates with a password. The offload path runs over `sshExec`, whose
|
|
16
|
+
* `SSH_OPTS` force `BatchMode=yes` (no password prompts), so only key / ssh-config
|
|
17
|
+
* auth can carry a `--host` run. Named so the top-level catch prints the message
|
|
18
|
+
* cleanly instead of a stack trace.
|
|
19
|
+
*/
|
|
20
|
+
export declare class DeviceOffloadUnsupportedError extends Error {
|
|
21
|
+
constructor(name: string);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Resolve a host name to a single host, or null if unknown. Resolution order:
|
|
25
|
+
* 1. host providers — the `agents hosts` registry (agents.yaml overlay + ssh-config)
|
|
26
|
+
* 2. the devices registry (`agents devices`) — a machine registered once with
|
|
27
|
+
* `agents devices sync` is reachable by `--host`/`--device` with no second enroll
|
|
28
|
+
* 3. an ad-hoc `user@host` (must contain `@`, validated) — nothing to register
|
|
29
|
+
*
|
|
30
|
+
* A bare unknown name returns null so capability-tag routing (`resolveHostByCap`)
|
|
31
|
+
* stays reachable: `--host gpu` must fall through to a cap lookup, not be misread
|
|
32
|
+
* as an ad-hoc target.
|
|
16
33
|
*/
|
|
17
34
|
export declare function resolveHost(name: string): Promise<Host | null>;
|
|
18
35
|
/**
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* `crabbox` later is a one-line `providers.set(...)` with no caller changes.
|
|
7
7
|
*/
|
|
8
8
|
import { LocalHostProvider } from './providers/local.js';
|
|
9
|
+
import { getDevice } from '../devices/registry.js';
|
|
10
|
+
import { assertValidSshTarget } from '../ssh-exec.js';
|
|
9
11
|
const providers = new Map();
|
|
10
12
|
function initProviders() {
|
|
11
13
|
if (providers.size > 0)
|
|
@@ -39,8 +41,54 @@ export async function listAllHosts() {
|
|
|
39
41
|
return out;
|
|
40
42
|
}
|
|
41
43
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
+
* Thrown when a device resolves but can't be used as an offload target because
|
|
45
|
+
* it authenticates with a password. The offload path runs over `sshExec`, whose
|
|
46
|
+
* `SSH_OPTS` force `BatchMode=yes` (no password prompts), so only key / ssh-config
|
|
47
|
+
* auth can carry a `--host` run. Named so the top-level catch prints the message
|
|
48
|
+
* cleanly instead of a stack trace.
|
|
49
|
+
*/
|
|
50
|
+
export class DeviceOffloadUnsupportedError extends Error {
|
|
51
|
+
constructor(name) {
|
|
52
|
+
super(`Device "${name}" uses password auth, which --host offload can't use yet ` +
|
|
53
|
+
`(runs go over ssh with BatchMode=yes). Switch it to key auth with ` +
|
|
54
|
+
`\`agents devices set ${name} --auth key\`, or enroll it as a host with ` +
|
|
55
|
+
`\`agents hosts add ${name}\`.`);
|
|
56
|
+
this.name = 'DeviceOffloadUnsupportedError';
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Bridge a registered device into a `Host` so `--host <device>` just works.
|
|
61
|
+
* A device's dnsName (preferred) or ip becomes the ssh address; `source: 'inline'`
|
|
62
|
+
* makes `sshTargetFor` emit `user@address`.
|
|
63
|
+
*/
|
|
64
|
+
function deviceToHost(device) {
|
|
65
|
+
if (device.auth.method === 'password') {
|
|
66
|
+
throw new DeviceOffloadUnsupportedError(device.name);
|
|
67
|
+
}
|
|
68
|
+
const address = device.address.dnsName ?? device.address.ip;
|
|
69
|
+
if (!address) {
|
|
70
|
+
throw new Error(`Device "${device.name}" has no address (Tailscale DNS name or IP) to reach it by.`);
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
name: device.name,
|
|
74
|
+
provider: 'local',
|
|
75
|
+
source: 'inline',
|
|
76
|
+
address,
|
|
77
|
+
user: device.user,
|
|
78
|
+
...(device.platform !== 'unknown' ? { os: device.platform } : {}),
|
|
79
|
+
enrolled: true,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Resolve a host name to a single host, or null if unknown. Resolution order:
|
|
84
|
+
* 1. host providers — the `agents hosts` registry (agents.yaml overlay + ssh-config)
|
|
85
|
+
* 2. the devices registry (`agents devices`) — a machine registered once with
|
|
86
|
+
* `agents devices sync` is reachable by `--host`/`--device` with no second enroll
|
|
87
|
+
* 3. an ad-hoc `user@host` (must contain `@`, validated) — nothing to register
|
|
88
|
+
*
|
|
89
|
+
* A bare unknown name returns null so capability-tag routing (`resolveHostByCap`)
|
|
90
|
+
* stays reachable: `--host gpu` must fall through to a cap lookup, not be misread
|
|
91
|
+
* as an ad-hoc target.
|
|
44
92
|
*/
|
|
45
93
|
export async function resolveHost(name) {
|
|
46
94
|
for (const provider of getAllProviders()) {
|
|
@@ -48,6 +96,14 @@ export async function resolveHost(name) {
|
|
|
48
96
|
if (host)
|
|
49
97
|
return host;
|
|
50
98
|
}
|
|
99
|
+
const device = await getDevice(name);
|
|
100
|
+
if (device)
|
|
101
|
+
return deviceToHost(device);
|
|
102
|
+
if (name.includes('@')) {
|
|
103
|
+
assertValidSshTarget(name);
|
|
104
|
+
const at = name.indexOf('@');
|
|
105
|
+
return { name, provider: 'local', source: 'inline', user: name.slice(0, at), address: name.slice(at + 1) };
|
|
106
|
+
}
|
|
51
107
|
return null;
|
|
52
108
|
}
|
|
53
109
|
/**
|
|
@@ -32,5 +32,66 @@ export declare const HOST_ROUTING_SPECS: StripSpec[];
|
|
|
32
32
|
* are quoted for the inner login shell, then the whole `agents …` invocation is
|
|
33
33
|
* quoted again so it survives `bash -lc <...>` — `bash -lc` so the remote login
|
|
34
34
|
* PATH resolves `agents`. An optional `cd` runs first for `--remote-cwd`.
|
|
35
|
+
*
|
|
36
|
+
* `os` selects the remote shell dialect: a Windows target gets a PowerShell
|
|
37
|
+
* invocation instead (ssh lands in cmd.exe/PowerShell there, where `bash -lc`
|
|
38
|
+
* does not exist). Anything else — including an unknown/absent OS — keeps the
|
|
39
|
+
* POSIX form, so linux/macos are byte-for-byte unchanged.
|
|
40
|
+
*/
|
|
41
|
+
export declare function buildRemoteAgentsInvocation(forwardedArgs: string[], remoteCwd?: string, os?: string): string;
|
|
42
|
+
/** The two remote shell dialects we build commands for. */
|
|
43
|
+
export type RemoteShell = 'posix' | 'powershell';
|
|
44
|
+
/**
|
|
45
|
+
* Pick the remote shell dialect from a recorded OS/platform string. A Windows
|
|
46
|
+
* host (device-registry `platform: 'windows'`, or an enrolled `HostEntry.os`
|
|
47
|
+
* that reads `windows`/`Windows`/`win32`/…) speaks PowerShell; everything else,
|
|
48
|
+
* including `undefined`/unknown, defaults to POSIX so linux/macos never regress.
|
|
49
|
+
*/
|
|
50
|
+
export declare function remoteShellFor(os: string | undefined): RemoteShell;
|
|
51
|
+
/**
|
|
52
|
+
* PowerShell single-quoted literal: wrap in `'…'` and double any embedded `'`.
|
|
53
|
+
* Single-quoted PS strings are fully literal (no `$var`, no backtick escapes),
|
|
54
|
+
* so this neutralises every metacharacter the same way POSIX `shellQuote` does.
|
|
55
|
+
*/
|
|
56
|
+
export declare function powershellQuote(s: string): string;
|
|
57
|
+
/**
|
|
58
|
+
* Encode a PowerShell script for `powershell -EncodedCommand`: base64 of its
|
|
59
|
+
* UTF-16LE bytes. The payload is a bare base64 word (no spaces or shell
|
|
60
|
+
* metacharacters), so it survives being handed to ssh as a single argument and
|
|
61
|
+
* re-parsed by the remote's cmd.exe/PowerShell with zero quoting hazards —
|
|
62
|
+
* the robust way to ship a complex command to a Windows box over SSH.
|
|
63
|
+
*/
|
|
64
|
+
export declare function encodePowershell(script: string): string;
|
|
65
|
+
/** Inverse of {@link encodePowershell} — decode an `-EncodedCommand` payload
|
|
66
|
+
* back to its script. Used by tests to assert on the built command. */
|
|
67
|
+
export declare function decodePowershell(encoded: string): string;
|
|
68
|
+
/** A single `agents …` invocation to run on a Windows remote. */
|
|
69
|
+
export interface WindowsAgentsCommand {
|
|
70
|
+
/** `agents` argv (command name NOT included; `agents` is prepended). */
|
|
71
|
+
args: string[];
|
|
72
|
+
/** Env vars scoped to this invocation (POSIX `VAR=val` ↔ PS `$env:VAR=…`). */
|
|
73
|
+
env?: Record<string, string>;
|
|
74
|
+
/** Directory to enter before running (`--remote-cwd`). */
|
|
75
|
+
cwd?: string;
|
|
76
|
+
/**
|
|
77
|
+
* Append `exit $LASTEXITCODE` so a native `agents` exit code propagates out
|
|
78
|
+
* through `powershell.exe` (which otherwise exits 0 regardless). Default true;
|
|
79
|
+
* pass false for probes whose reachability keys off a sentinel, not the code.
|
|
80
|
+
*/
|
|
81
|
+
propagateExit?: boolean;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The PowerShell script (pre-encoding) that {@link buildWindowsAgentsCommand}
|
|
85
|
+
* runs. Exposed so tests can decode the `-EncodedCommand` payload and compare
|
|
86
|
+
* against the exact script, without needing PowerShell on the test host.
|
|
87
|
+
*
|
|
88
|
+
* `& agents …` invokes the CLI from the machine PATH (Windows has no login
|
|
89
|
+
* shell, so there is no `bash -lc` equivalent — the shim is simply on PATH).
|
|
90
|
+
*/
|
|
91
|
+
export declare function windowsAgentsScript(cmd: WindowsAgentsCommand): string;
|
|
92
|
+
/**
|
|
93
|
+
* Build the `ssh <target> <cmd>` string for one `agents …` invocation on a
|
|
94
|
+
* Windows remote: a `powershell -NoProfile -EncodedCommand <base64>` call. The
|
|
95
|
+
* Windows counterpart of `bash -lc '<...>'`, shared by every `--host` site.
|
|
35
96
|
*/
|
|
36
|
-
export declare function
|
|
97
|
+
export declare function buildWindowsAgentsCommand(cmd: WindowsAgentsCommand): string;
|
|
@@ -48,9 +48,78 @@ export const HOST_ROUTING_SPECS = [
|
|
|
48
48
|
* are quoted for the inner login shell, then the whole `agents …` invocation is
|
|
49
49
|
* quoted again so it survives `bash -lc <...>` — `bash -lc` so the remote login
|
|
50
50
|
* PATH resolves `agents`. An optional `cd` runs first for `--remote-cwd`.
|
|
51
|
+
*
|
|
52
|
+
* `os` selects the remote shell dialect: a Windows target gets a PowerShell
|
|
53
|
+
* invocation instead (ssh lands in cmd.exe/PowerShell there, where `bash -lc`
|
|
54
|
+
* does not exist). Anything else — including an unknown/absent OS — keeps the
|
|
55
|
+
* POSIX form, so linux/macos are byte-for-byte unchanged.
|
|
51
56
|
*/
|
|
52
|
-
export function buildRemoteAgentsInvocation(forwardedArgs, remoteCwd) {
|
|
57
|
+
export function buildRemoteAgentsInvocation(forwardedArgs, remoteCwd, os) {
|
|
58
|
+
if (remoteShellFor(os) === 'powershell') {
|
|
59
|
+
return buildWindowsAgentsCommand({ args: forwardedArgs, cwd: remoteCwd });
|
|
60
|
+
}
|
|
53
61
|
const inner = ['agents', ...forwardedArgs].map(shellQuote).join(' ');
|
|
54
62
|
const withCwd = remoteCwd ? `cd ${shellQuote(remoteCwd)} && ${inner}` : inner;
|
|
55
63
|
return `bash -lc ${shellQuote(withCwd)}`;
|
|
56
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Pick the remote shell dialect from a recorded OS/platform string. A Windows
|
|
67
|
+
* host (device-registry `platform: 'windows'`, or an enrolled `HostEntry.os`
|
|
68
|
+
* that reads `windows`/`Windows`/`win32`/…) speaks PowerShell; everything else,
|
|
69
|
+
* including `undefined`/unknown, defaults to POSIX so linux/macos never regress.
|
|
70
|
+
*/
|
|
71
|
+
export function remoteShellFor(os) {
|
|
72
|
+
return /^win/i.test((os ?? '').trim()) ? 'powershell' : 'posix';
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* PowerShell single-quoted literal: wrap in `'…'` and double any embedded `'`.
|
|
76
|
+
* Single-quoted PS strings are fully literal (no `$var`, no backtick escapes),
|
|
77
|
+
* so this neutralises every metacharacter the same way POSIX `shellQuote` does.
|
|
78
|
+
*/
|
|
79
|
+
export function powershellQuote(s) {
|
|
80
|
+
return "'" + s.replace(/'/g, "''") + "'";
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Encode a PowerShell script for `powershell -EncodedCommand`: base64 of its
|
|
84
|
+
* UTF-16LE bytes. The payload is a bare base64 word (no spaces or shell
|
|
85
|
+
* metacharacters), so it survives being handed to ssh as a single argument and
|
|
86
|
+
* re-parsed by the remote's cmd.exe/PowerShell with zero quoting hazards —
|
|
87
|
+
* the robust way to ship a complex command to a Windows box over SSH.
|
|
88
|
+
*/
|
|
89
|
+
export function encodePowershell(script) {
|
|
90
|
+
return Buffer.from(script, 'utf16le').toString('base64');
|
|
91
|
+
}
|
|
92
|
+
/** Inverse of {@link encodePowershell} — decode an `-EncodedCommand` payload
|
|
93
|
+
* back to its script. Used by tests to assert on the built command. */
|
|
94
|
+
export function decodePowershell(encoded) {
|
|
95
|
+
return Buffer.from(encoded, 'base64').toString('utf16le');
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* The PowerShell script (pre-encoding) that {@link buildWindowsAgentsCommand}
|
|
99
|
+
* runs. Exposed so tests can decode the `-EncodedCommand` payload and compare
|
|
100
|
+
* against the exact script, without needing PowerShell on the test host.
|
|
101
|
+
*
|
|
102
|
+
* `& agents …` invokes the CLI from the machine PATH (Windows has no login
|
|
103
|
+
* shell, so there is no `bash -lc` equivalent — the shim is simply on PATH).
|
|
104
|
+
*/
|
|
105
|
+
export function windowsAgentsScript(cmd) {
|
|
106
|
+
const { args, env, cwd, propagateExit = true } = cmd;
|
|
107
|
+
const parts = [];
|
|
108
|
+
if (env)
|
|
109
|
+
for (const [k, v] of Object.entries(env))
|
|
110
|
+
parts.push(`$env:${k} = ${powershellQuote(v)}`);
|
|
111
|
+
if (cwd)
|
|
112
|
+
parts.push(`Set-Location -LiteralPath ${powershellQuote(cwd)}`);
|
|
113
|
+
parts.push(`& ${['agents', ...args].map(powershellQuote).join(' ')}`);
|
|
114
|
+
if (propagateExit)
|
|
115
|
+
parts.push('exit $LASTEXITCODE');
|
|
116
|
+
return parts.join('; ');
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Build the `ssh <target> <cmd>` string for one `agents …` invocation on a
|
|
120
|
+
* Windows remote: a `powershell -NoProfile -EncodedCommand <base64>` call. The
|
|
121
|
+
* Windows counterpart of `bash -lc '<...>'`, shared by every `--host` site.
|
|
122
|
+
*/
|
|
123
|
+
export function buildWindowsAgentsCommand(cmd) {
|
|
124
|
+
return `powershell -NoProfile -EncodedCommand ${encodePowershell(windowsAgentsScript(cmd))}`;
|
|
125
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve a remote host's OS family so the SSH command layer can pick the right
|
|
3
|
+
* shell dialect (POSIX `bash -lc` vs Windows PowerShell). See `remoteShellFor`
|
|
4
|
+
* in `remote-cmd.ts` for how the string is consumed.
|
|
5
|
+
*
|
|
6
|
+
* Two sources, in priority order:
|
|
7
|
+
* 1. The device registry `platform` (`windows`/`linux`/`macos`), which is
|
|
8
|
+
* populated fleet-wide by Tailscale sync — the reliable answer for a box
|
|
9
|
+
* like `win-mini` that was discovered, not hand-enrolled.
|
|
10
|
+
* 2. The enrolled `HostEntry.os` overlay in agents.yaml (the `uname` captured
|
|
11
|
+
* at `agents hosts add` time), for hosts that live only in that overlay.
|
|
12
|
+
*
|
|
13
|
+
* Missing/unknown from both → `undefined`, which `remoteShellFor` maps to POSIX.
|
|
14
|
+
* Kept synchronous so the sync `agents sessions --host` fan-out can use it.
|
|
15
|
+
*/
|
|
16
|
+
/** Resolve the OS/platform string for a host name, or undefined if unknown. */
|
|
17
|
+
export declare function resolveRemoteOsSync(name: string): string | undefined;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve a remote host's OS family so the SSH command layer can pick the right
|
|
3
|
+
* shell dialect (POSIX `bash -lc` vs Windows PowerShell). See `remoteShellFor`
|
|
4
|
+
* in `remote-cmd.ts` for how the string is consumed.
|
|
5
|
+
*
|
|
6
|
+
* Two sources, in priority order:
|
|
7
|
+
* 1. The device registry `platform` (`windows`/`linux`/`macos`), which is
|
|
8
|
+
* populated fleet-wide by Tailscale sync — the reliable answer for a box
|
|
9
|
+
* like `win-mini` that was discovered, not hand-enrolled.
|
|
10
|
+
* 2. The enrolled `HostEntry.os` overlay in agents.yaml (the `uname` captured
|
|
11
|
+
* at `agents hosts add` time), for hosts that live only in that overlay.
|
|
12
|
+
*
|
|
13
|
+
* Missing/unknown from both → `undefined`, which `remoteShellFor` maps to POSIX.
|
|
14
|
+
* Kept synchronous so the sync `agents sessions --host` fan-out can use it.
|
|
15
|
+
*/
|
|
16
|
+
import { loadDevicesSync } from '../devices/registry.js';
|
|
17
|
+
import { readMeta } from '../state.js';
|
|
18
|
+
/** Resolve the OS/platform string for a host name, or undefined if unknown. */
|
|
19
|
+
export function resolveRemoteOsSync(name) {
|
|
20
|
+
try {
|
|
21
|
+
const platform = loadDevicesSync()[name]?.platform;
|
|
22
|
+
if (platform && platform !== 'unknown')
|
|
23
|
+
return platform;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// A corrupt/unreadable device registry must never break command building —
|
|
27
|
+
// fall through to the host overlay and ultimately the POSIX default.
|
|
28
|
+
}
|
|
29
|
+
return readMeta().hosts?.[name]?.os;
|
|
30
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Register host-dispatched runs (`agents run --host <h>`) into the LOCAL session
|
|
3
|
+
* index so they show up in `agents sessions` and are resolvable by id — even
|
|
4
|
+
* though the transcript itself lives on the remote host.
|
|
5
|
+
*
|
|
6
|
+
* The transcript file is remote, so the row is registered with an EMPTY
|
|
7
|
+
* `file_path`. The DB's stale-row filter keeps rows with no file_path
|
|
8
|
+
* (`!file_path || existsSync(file_path)` in db.ts querySessions) precisely for
|
|
9
|
+
* this "synthetic file_path" case, and the scanner never prunes session rows —
|
|
10
|
+
* so the entry survives rescans. `cwd` is the LOCAL directory the dispatch was
|
|
11
|
+
* issued from, so the run appears in that project's session listing like any
|
|
12
|
+
* local run. The `[host/<name>]` label mirrors the cloud path's
|
|
13
|
+
* `[cloud/<status>]` convention.
|
|
14
|
+
*/
|
|
15
|
+
import type { SessionMeta } from '../session/types.js';
|
|
16
|
+
import type { HostTask } from './tasks.js';
|
|
17
|
+
export interface HostSessionContext {
|
|
18
|
+
/** Local directory the `agents run --host` was invoked from. */
|
|
19
|
+
cwd: string;
|
|
20
|
+
/** Prompt the run was launched with, used for the session topic. */
|
|
21
|
+
prompt: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Build the SessionMeta for a host-dispatched run. Returns null when the run has
|
|
25
|
+
* no captured session id (nothing stable to key/resume on) or its agent isn't a
|
|
26
|
+
* known session agent. Pure — no I/O — so the mapping is unit-testable.
|
|
27
|
+
*/
|
|
28
|
+
export declare function hostSessionMeta(task: HostTask, ctx: HostSessionContext): SessionMeta | null;
|
|
29
|
+
/**
|
|
30
|
+
* Register (or refresh) a host-dispatched run in the local session index. No-op
|
|
31
|
+
* when the task carries no session id. Best-effort: a failed write must never
|
|
32
|
+
* break the dispatch itself, which has already been launched on the host.
|
|
33
|
+
*/
|
|
34
|
+
export declare function registerHostSession(task: HostTask, ctx: HostSessionContext): void;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Register host-dispatched runs (`agents run --host <h>`) into the LOCAL session
|
|
3
|
+
* index so they show up in `agents sessions` and are resolvable by id — even
|
|
4
|
+
* though the transcript itself lives on the remote host.
|
|
5
|
+
*
|
|
6
|
+
* The transcript file is remote, so the row is registered with an EMPTY
|
|
7
|
+
* `file_path`. The DB's stale-row filter keeps rows with no file_path
|
|
8
|
+
* (`!file_path || existsSync(file_path)` in db.ts querySessions) precisely for
|
|
9
|
+
* this "synthetic file_path" case, and the scanner never prunes session rows —
|
|
10
|
+
* so the entry survives rescans. `cwd` is the LOCAL directory the dispatch was
|
|
11
|
+
* issued from, so the run appears in that project's session listing like any
|
|
12
|
+
* local run. The `[host/<name>]` label mirrors the cloud path's
|
|
13
|
+
* `[cloud/<status>]` convention.
|
|
14
|
+
*/
|
|
15
|
+
import { upsertSession } from '../session/db.js';
|
|
16
|
+
import { SESSION_AGENTS } from '../session/types.js';
|
|
17
|
+
/**
|
|
18
|
+
* Build the SessionMeta for a host-dispatched run. Returns null when the run has
|
|
19
|
+
* no captured session id (nothing stable to key/resume on) or its agent isn't a
|
|
20
|
+
* known session agent. Pure — no I/O — so the mapping is unit-testable.
|
|
21
|
+
*/
|
|
22
|
+
export function hostSessionMeta(task, ctx) {
|
|
23
|
+
const id = task.sessionId;
|
|
24
|
+
if (!id)
|
|
25
|
+
return null;
|
|
26
|
+
if (!SESSION_AGENTS.includes(task.agent))
|
|
27
|
+
return null;
|
|
28
|
+
return {
|
|
29
|
+
id,
|
|
30
|
+
shortId: id.slice(0, 8),
|
|
31
|
+
agent: task.agent,
|
|
32
|
+
timestamp: task.createdAt,
|
|
33
|
+
cwd: ctx.cwd,
|
|
34
|
+
// Remote transcript — no local file. Empty file_path is the sentinel the DB
|
|
35
|
+
// stale-filter treats as "always live" (see module doc).
|
|
36
|
+
filePath: '',
|
|
37
|
+
topic: ctx.prompt.split('\n')[0]?.slice(0, 120) || undefined,
|
|
38
|
+
label: `[host/${task.host}]`,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Register (or refresh) a host-dispatched run in the local session index. No-op
|
|
43
|
+
* when the task carries no session id. Best-effort: a failed write must never
|
|
44
|
+
* break the dispatch itself, which has already been launched on the host.
|
|
45
|
+
*/
|
|
46
|
+
export function registerHostSession(task, ctx) {
|
|
47
|
+
const meta = hostSessionMeta(task, ctx);
|
|
48
|
+
if (!meta)
|
|
49
|
+
return;
|
|
50
|
+
try {
|
|
51
|
+
upsertSession(meta, '');
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
/* index write is best-effort; the run is already live on the host */
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -15,6 +15,13 @@ export interface HostTask {
|
|
|
15
15
|
agent: string;
|
|
16
16
|
prompt: string;
|
|
17
17
|
pid?: number;
|
|
18
|
+
/**
|
|
19
|
+
* The agent session id the remote run was launched with (Claude only — the
|
|
20
|
+
* only agent that accepts `--session-id` to force a NEW session's id). Lets
|
|
21
|
+
* `agents sessions`/resume-by-id map a discovered session back to the host it
|
|
22
|
+
* lives on. Absent for agents that don't take an explicit session id.
|
|
23
|
+
*/
|
|
24
|
+
sessionId?: string;
|
|
18
25
|
/** Remote paths (under the host's ~/.agents/.cache/hosts/). */
|
|
19
26
|
remoteLog: string;
|
|
20
27
|
remoteExit: string;
|
|
@@ -29,4 +36,19 @@ export declare function localLogPath(id: string): string;
|
|
|
29
36
|
export declare function saveTask(task: HostTask): void;
|
|
30
37
|
export declare function loadTask(id: string): HostTask | null;
|
|
31
38
|
export declare function updateTask(id: string, patch: Partial<HostTask>): HostTask | null;
|
|
39
|
+
/**
|
|
40
|
+
* The record patch for a run that has finished with `code`. The single authority
|
|
41
|
+
* for the exit-code → status mapping, so the dispatch, reconcile, and log-follow
|
|
42
|
+
* paths can never disagree. A genuine remote exit code is never -1 (that sentinel
|
|
43
|
+
* means "follow window closed while the run continues"), so callers must resolve
|
|
44
|
+
* -1 as still-running and never pass it here.
|
|
45
|
+
*/
|
|
46
|
+
export declare function terminalPatch(code: number): Partial<HostTask>;
|
|
32
47
|
export declare function listTasks(): HostTask[];
|
|
48
|
+
/**
|
|
49
|
+
* Find the host task that launched a given agent session id, so a resume-by-id
|
|
50
|
+
* can re-dispatch to the host the session actually lives on. Newest task wins
|
|
51
|
+
* (listTasks is createdAt-desc) — a session id should be unique, but a re-run
|
|
52
|
+
* with the same forced id resolves to the most recent dispatch.
|
|
53
|
+
*/
|
|
54
|
+
export declare function findTaskBySessionId(sessionId: string): HostTask | null;
|
package/dist/lib/hosts/tasks.js
CHANGED
|
@@ -40,6 +40,20 @@ export function updateTask(id, patch) {
|
|
|
40
40
|
saveTask(next);
|
|
41
41
|
return next;
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* The record patch for a run that has finished with `code`. The single authority
|
|
45
|
+
* for the exit-code → status mapping, so the dispatch, reconcile, and log-follow
|
|
46
|
+
* paths can never disagree. A genuine remote exit code is never -1 (that sentinel
|
|
47
|
+
* means "follow window closed while the run continues"), so callers must resolve
|
|
48
|
+
* -1 as still-running and never pass it here.
|
|
49
|
+
*/
|
|
50
|
+
export function terminalPatch(code) {
|
|
51
|
+
return {
|
|
52
|
+
status: code === 0 ? 'completed' : 'failed',
|
|
53
|
+
exitCode: code,
|
|
54
|
+
finishedAt: new Date().toISOString(),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
43
57
|
export function listTasks() {
|
|
44
58
|
let files;
|
|
45
59
|
try {
|
|
@@ -56,3 +70,18 @@ export function listTasks() {
|
|
|
56
70
|
}
|
|
57
71
|
return tasks.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
58
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Find the host task that launched a given agent session id, so a resume-by-id
|
|
75
|
+
* can re-dispatch to the host the session actually lives on. Newest task wins
|
|
76
|
+
* (listTasks is createdAt-desc) — a session id should be unique, but a re-run
|
|
77
|
+
* with the same forced id resolves to the most recent dispatch.
|
|
78
|
+
*/
|
|
79
|
+
export function findTaskBySessionId(sessionId) {
|
|
80
|
+
if (!sessionId)
|
|
81
|
+
return null;
|
|
82
|
+
for (const task of listTasks()) {
|
|
83
|
+
if (task.sessionId === sessionId)
|
|
84
|
+
return task;
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|