@phnx-labs/agents-cli 1.20.74 → 1.20.77
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 +1055 -0
- package/README.md +36 -10
- package/dist/bin/agents +0 -0
- package/dist/commands/apply.js +21 -3
- package/dist/commands/attach.d.ts +10 -0
- package/dist/commands/attach.js +41 -0
- package/dist/commands/computer.js +3 -3
- package/dist/commands/detach-core.d.ts +46 -0
- package/dist/commands/detach-core.js +61 -0
- package/dist/commands/detach.d.ts +5 -0
- package/dist/commands/detach.js +171 -0
- package/dist/commands/doctor.js +18 -0
- package/dist/commands/exec.js +83 -13
- package/dist/commands/feed.d.ts +7 -1
- package/dist/commands/feed.js +47 -4
- package/dist/commands/go.d.ts +9 -2
- package/dist/commands/go.js +16 -6
- package/dist/commands/harness.d.ts +14 -0
- package/dist/commands/harness.js +145 -0
- package/dist/commands/import.js +38 -8
- package/dist/commands/inspect.js +5 -2
- package/dist/commands/profiles.d.ts +20 -0
- package/dist/commands/profiles.js +68 -21
- package/dist/commands/repo.js +14 -0
- package/dist/commands/routines.js +286 -15
- package/dist/commands/secrets.js +102 -37
- package/dist/commands/sessions-browser.js +12 -2
- package/dist/commands/sessions-export.js +19 -5
- package/dist/commands/sessions-migrate.d.ts +29 -0
- package/dist/commands/sessions-migrate.js +596 -0
- package/dist/commands/sessions-picker.d.ts +14 -1
- package/dist/commands/sessions-picker.js +184 -16
- package/dist/commands/sessions.d.ts +56 -14
- package/dist/commands/sessions.js +322 -62
- package/dist/commands/setup-computer.js +2 -2
- package/dist/commands/ssh.d.ts +13 -0
- package/dist/commands/ssh.js +110 -37
- package/dist/commands/status.js +10 -2
- package/dist/commands/versions.js +15 -6
- package/dist/commands/view.d.ts +5 -0
- package/dist/commands/view.js +24 -4
- package/dist/commands/watchdog.d.ts +7 -2
- package/dist/commands/watchdog.js +73 -57
- package/dist/index.js +59 -7
- package/dist/lib/activity.d.ts +17 -6
- package/dist/lib/activity.js +466 -40
- package/dist/lib/actor.d.ts +50 -0
- package/dist/lib/actor.js +166 -0
- package/dist/lib/agent-spec/provider.js +2 -1
- package/dist/lib/agent-spec/resolve.js +19 -5
- package/dist/lib/agent-spec/types.d.ts +9 -1
- package/dist/lib/agents.d.ts +21 -0
- package/dist/lib/agents.js +70 -6
- package/dist/lib/cloud/codex.d.ts +2 -0
- package/dist/lib/cloud/codex.js +14 -3
- package/dist/lib/cloud/session-index.d.ts +32 -0
- package/dist/lib/cloud/session-index.js +58 -0
- package/dist/lib/cloud/store.d.ts +7 -0
- package/dist/lib/cloud/store.js +25 -0
- package/dist/lib/config-transfer.js +4 -0
- package/dist/lib/daemon.d.ts +10 -1
- package/dist/lib/daemon.js +114 -11
- package/dist/lib/devices/connect.d.ts +15 -1
- package/dist/lib/devices/connect.js +15 -1
- package/dist/lib/devices/fleet.d.ts +37 -1
- package/dist/lib/devices/fleet.js +36 -2
- package/dist/lib/devices/health-report.d.ts +38 -0
- package/dist/lib/devices/health-report.js +214 -0
- package/dist/lib/devices/health.js +4 -1
- package/dist/lib/devices/reachability.d.ts +31 -0
- package/dist/lib/devices/reachability.js +40 -0
- package/dist/lib/devices/registry.d.ts +33 -0
- package/dist/lib/devices/registry.js +37 -0
- package/dist/lib/devices/resolve-target.d.ts +15 -27
- package/dist/lib/devices/resolve-target.js +63 -102
- package/dist/lib/devices/sync.d.ts +18 -0
- package/dist/lib/devices/sync.js +23 -1
- package/dist/lib/devices/tailscale.d.ts +3 -0
- package/dist/lib/devices/tailscale.js +1 -0
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/exec.d.ts +82 -0
- package/dist/lib/exec.js +218 -6
- package/dist/lib/feed-post.d.ts +63 -0
- package/dist/lib/feed-post.js +204 -0
- package/dist/lib/feed.d.ts +7 -1
- package/dist/lib/feed.js +96 -6
- package/dist/lib/fleet/apply.d.ts +32 -2
- package/dist/lib/fleet/apply.js +97 -10
- package/dist/lib/fleet/types.d.ts +11 -0
- package/dist/lib/fs-walk.d.ts +13 -0
- package/dist/lib/fs-walk.js +16 -7
- package/dist/lib/heal.d.ts +7 -4
- package/dist/lib/heal.js +10 -22
- package/dist/lib/hooks.d.ts +1 -0
- package/dist/lib/hooks.js +156 -0
- package/dist/lib/hosts/dispatch.d.ts +16 -0
- package/dist/lib/hosts/dispatch.js +26 -4
- package/dist/lib/hosts/passthrough.js +9 -2
- package/dist/lib/hosts/reconnect.d.ts +78 -0
- package/dist/lib/hosts/reconnect.js +127 -0
- package/dist/lib/hosts/registry.d.ts +75 -14
- package/dist/lib/hosts/registry.js +205 -30
- package/dist/lib/hosts/remote-cmd.d.ts +11 -0
- package/dist/lib/hosts/remote-cmd.js +32 -8
- package/dist/lib/hosts/remote-session-id.d.ts +45 -0
- package/dist/lib/hosts/remote-session-id.js +84 -0
- package/dist/lib/hosts/run-target.d.ts +17 -5
- package/dist/lib/hosts/run-target.js +30 -10
- package/dist/lib/hosts/session-index.d.ts +18 -1
- package/dist/lib/hosts/session-index.js +40 -5
- package/dist/lib/hosts/session-marker.d.ts +33 -0
- package/dist/lib/hosts/session-marker.js +51 -0
- package/dist/lib/hosts/tasks.d.ts +8 -4
- package/dist/lib/import.d.ts +2 -0
- package/dist/lib/import.js +35 -2
- package/dist/lib/menubar/install-menubar.d.ts +29 -0
- package/dist/lib/menubar/install-menubar.js +59 -1
- package/dist/lib/menubar/notify-desktop.d.ts +44 -0
- package/dist/lib/menubar/notify-desktop.js +78 -0
- package/dist/lib/migrate.d.ts +16 -0
- package/dist/lib/migrate.js +36 -0
- package/dist/lib/models.d.ts +27 -0
- package/dist/lib/models.js +54 -1
- package/dist/lib/overdue.d.ts +6 -2
- package/dist/lib/overdue.js +10 -36
- package/dist/lib/picker.js +4 -1
- package/dist/lib/platform/process.d.ts +17 -0
- package/dist/lib/platform/process.js +70 -0
- package/dist/lib/profiles-presets.js +9 -7
- package/dist/lib/profiles.d.ts +31 -0
- package/dist/lib/profiles.js +70 -0
- package/dist/lib/pty-server.d.ts +2 -10
- package/dist/lib/pty-server.js +4 -38
- package/dist/lib/registry.d.ts +1 -1
- package/dist/lib/registry.js +48 -8
- package/dist/lib/rotate.d.ts +18 -0
- package/dist/lib/rotate.js +28 -0
- package/dist/lib/routine-notify.d.ts +76 -0
- package/dist/lib/routine-notify.js +190 -0
- package/dist/lib/routines-placement.d.ts +38 -0
- package/dist/lib/routines-placement.js +79 -0
- package/dist/lib/routines-project.d.ts +97 -0
- package/dist/lib/routines-project.js +349 -0
- package/dist/lib/routines.d.ts +94 -0
- package/dist/lib/routines.js +93 -9
- package/dist/lib/runner.d.ts +12 -2
- package/dist/lib/runner.js +242 -20
- package/dist/lib/sandbox.js +10 -0
- package/dist/lib/secrets/account-token.d.ts +20 -0
- package/dist/lib/secrets/account-token.js +64 -0
- package/dist/lib/secrets/agent.d.ts +74 -4
- package/dist/lib/secrets/agent.js +181 -107
- package/dist/lib/secrets/bundles.d.ts +54 -6
- package/dist/lib/secrets/bundles.js +230 -34
- package/dist/lib/secrets/index.d.ts +26 -3
- package/dist/lib/secrets/index.js +42 -12
- package/dist/lib/secrets/rc-hygiene.d.ts +55 -0
- package/dist/lib/secrets/rc-hygiene.js +154 -0
- package/dist/lib/secrets/remote.d.ts +7 -4
- package/dist/lib/secrets/remote.js +7 -4
- package/dist/lib/secrets/session-store.d.ts +6 -2
- package/dist/lib/secrets/session-store.js +65 -15
- package/dist/lib/secrets/sync-commands.d.ts +21 -0
- package/dist/lib/secrets/sync-commands.js +21 -0
- package/dist/lib/secrets/unlock-hints.d.ts +27 -0
- package/dist/lib/secrets/unlock-hints.js +36 -0
- package/dist/lib/self-update.d.ts +23 -2
- package/dist/lib/self-update.js +86 -5
- package/dist/lib/session/active.d.ts +124 -7
- package/dist/lib/session/active.js +390 -56
- package/dist/lib/session/cloud.js +2 -1
- package/dist/lib/session/db.d.ts +54 -0
- package/dist/lib/session/db.js +320 -19
- package/dist/lib/session/detached.d.ts +30 -0
- package/dist/lib/session/detached.js +92 -0
- package/dist/lib/session/discover.d.ts +478 -3
- package/dist/lib/session/discover.js +1740 -472
- package/dist/lib/session/fork.js +2 -1
- package/dist/lib/session/hook-sessions.d.ts +43 -0
- package/dist/lib/session/hook-sessions.js +135 -0
- package/dist/lib/session/linear.d.ts +6 -0
- package/dist/lib/session/linear.js +55 -0
- package/dist/lib/session/migrate-targets.d.ts +65 -0
- package/dist/lib/session/migrate-targets.js +94 -0
- package/dist/lib/session/migrations.d.ts +37 -0
- package/dist/lib/session/migrations.js +60 -0
- package/dist/lib/session/parse.d.ts +18 -0
- package/dist/lib/session/parse.js +141 -32
- package/dist/lib/session/pid-registry.d.ts +34 -2
- package/dist/lib/session/pid-registry.js +49 -2
- package/dist/lib/session/prompt.d.ts +7 -0
- package/dist/lib/session/prompt.js +37 -0
- package/dist/lib/session/provenance.d.ts +7 -0
- package/dist/lib/session/render.d.ts +7 -2
- package/dist/lib/session/render.js +40 -26
- package/dist/lib/session/short-id.d.ts +17 -0
- package/dist/lib/session/short-id.js +20 -0
- package/dist/lib/session/state.d.ts +4 -0
- package/dist/lib/session/state.js +99 -13
- package/dist/lib/session/types.d.ts +25 -1
- package/dist/lib/session/types.js +8 -0
- package/dist/lib/shims.d.ts +1 -1
- package/dist/lib/shims.js +20 -6
- package/dist/lib/sqlite.d.ts +3 -2
- package/dist/lib/sqlite.js +27 -4
- package/dist/lib/staleness/detectors/commands.js +1 -1
- package/dist/lib/staleness/detectors/workflows.js +13 -2
- package/dist/lib/staleness/writers/commands.js +7 -7
- package/dist/lib/staleness/writers/workflows.d.ts +4 -2
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +3 -0
- package/dist/lib/state.d.ts +8 -4
- package/dist/lib/state.js +21 -37
- package/dist/lib/teams/agents.d.ts +19 -10
- package/dist/lib/teams/agents.js +40 -39
- package/dist/lib/types.d.ts +47 -2
- package/dist/lib/usage.d.ts +33 -1
- package/dist/lib/usage.js +172 -12
- package/dist/lib/versions.d.ts +9 -2
- package/dist/lib/versions.js +37 -8
- package/dist/lib/watchdog/log.d.ts +43 -0
- package/dist/lib/watchdog/log.js +69 -0
- package/dist/lib/watchdog/routine.d.ts +44 -0
- package/dist/lib/watchdog/routine.js +69 -0
- package/dist/lib/watchdog/runner.d.ts +51 -7
- package/dist/lib/watchdog/runner.js +239 -64
- package/dist/lib/watchdog/watchdog.d.ts +1 -1
- package/dist/lib/watchdog/watchdog.js +31 -16
- package/dist/lib/workflows.d.ts +16 -0
- package/dist/lib/workflows.js +110 -1
- package/package.json +1 -1
|
@@ -87,6 +87,14 @@ export interface DeviceProbe {
|
|
|
87
87
|
cliVersion?: string;
|
|
88
88
|
/** Agent ids currently installed on the device. */
|
|
89
89
|
installedAgents: string[];
|
|
90
|
+
/**
|
|
91
|
+
* Installed version strings per agent id (e.g. `{ claude: ['2.1.170', '2.1.207'] }`),
|
|
92
|
+
* parsed from `agents view --json` on the device. Only populated when the plan
|
|
93
|
+
* involves a version-pinned spec (`claude@2.1.170`, or a `claude@all` expansion)
|
|
94
|
+
* — a bare `claude`/`claude@latest` roster never pays for the extra probe. When
|
|
95
|
+
* undefined, version-pinned specs fall back to id-level presence.
|
|
96
|
+
*/
|
|
97
|
+
installedVersions?: Record<string, string[]>;
|
|
90
98
|
/** Reason string when `reachable` is false or the probe partially failed. */
|
|
91
99
|
note?: string;
|
|
92
100
|
}
|
|
@@ -100,6 +108,9 @@ export interface FleetAction {
|
|
|
100
108
|
kind: FleetActionKind;
|
|
101
109
|
/** Agent id for agent/login actions; undefined for cli/config actions. */
|
|
102
110
|
agent?: string;
|
|
111
|
+
/** Full agent spec for `add-agent` (e.g. `claude@2.1.170`) so the plan can show
|
|
112
|
+
* the exact version being installed; equals the id for a bare/latest spec. */
|
|
113
|
+
spec?: string;
|
|
103
114
|
/** Human, one-line description of the action. */
|
|
104
115
|
detail: string;
|
|
105
116
|
}
|
package/dist/lib/fs-walk.d.ts
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
/** A file surfaced by the walk, carrying the mtime+size from the walk's own stat. */
|
|
2
|
+
export interface WalkedFile {
|
|
3
|
+
path: string;
|
|
4
|
+
mtimeMs: number;
|
|
5
|
+
size: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Walk a directory recursively for files with a given extension, newest first,
|
|
9
|
+
* keeping each match's mtime and size from the walk's own stat. Callers that
|
|
10
|
+
* then compare against the scan ledger reuse these instead of re-stat'ing every
|
|
11
|
+
* path — eliminating a second stat per file on the Codex/Droid/routine scans.
|
|
12
|
+
*/
|
|
13
|
+
export declare function walkForFilesWithStat(dir: string, ext: string, limit: number): WalkedFile[];
|
|
1
14
|
/** Walk a directory recursively for files with a given extension, newest first. */
|
|
2
15
|
export declare function walkForFiles(dir: string, ext: string, limit: number): string[];
|
|
3
16
|
/**
|
package/dist/lib/fs-walk.js
CHANGED
|
@@ -36,20 +36,29 @@ function walkEntries(dir, ext, onFile) {
|
|
|
36
36
|
else if (entry.name.endsWith(ext)) {
|
|
37
37
|
const stat = safeStatSync(full);
|
|
38
38
|
if (stat)
|
|
39
|
-
onFile(full, stat.mtimeMs);
|
|
39
|
+
onFile(full, stat.mtimeMs, stat.size);
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
walk(dir, 0);
|
|
44
44
|
}
|
|
45
|
-
/**
|
|
46
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Walk a directory recursively for files with a given extension, newest first,
|
|
47
|
+
* keeping each match's mtime and size from the walk's own stat. Callers that
|
|
48
|
+
* then compare against the scan ledger reuse these instead of re-stat'ing every
|
|
49
|
+
* path — eliminating a second stat per file on the Codex/Droid/routine scans.
|
|
50
|
+
*/
|
|
51
|
+
export function walkForFilesWithStat(dir, ext, limit) {
|
|
47
52
|
const results = [];
|
|
48
|
-
walkEntries(dir, ext, (filePath, mtimeMs) => {
|
|
49
|
-
results.push({ path: filePath,
|
|
53
|
+
walkEntries(dir, ext, (filePath, mtimeMs, size) => {
|
|
54
|
+
results.push({ path: filePath, mtimeMs, size });
|
|
50
55
|
});
|
|
51
|
-
results.sort((a, b) => b.
|
|
52
|
-
return results.slice(0, limit)
|
|
56
|
+
results.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
57
|
+
return results.slice(0, limit);
|
|
58
|
+
}
|
|
59
|
+
/** Walk a directory recursively for files with a given extension, newest first. */
|
|
60
|
+
export function walkForFiles(dir, ext, limit) {
|
|
61
|
+
return walkForFilesWithStat(dir, ext, limit).map(r => r.path);
|
|
53
62
|
}
|
|
54
63
|
/**
|
|
55
64
|
* Return the newest mtime (ms) among files with the given extension, or null
|
package/dist/lib/heal.d.ts
CHANGED
|
@@ -87,10 +87,13 @@ export declare function healChangedAnything(r: HealResult): boolean;
|
|
|
87
87
|
/** One-line summary of a heal pass for daemon logs. */
|
|
88
88
|
export declare function summarizeHeal(r: HealResult): string;
|
|
89
89
|
/**
|
|
90
|
-
* Fire a
|
|
91
|
-
* noteworthy.
|
|
92
|
-
*
|
|
93
|
-
*
|
|
90
|
+
* Fire a branded desktop notification when a background heal did something
|
|
91
|
+
* noteworthy. Routed through the MenubarHelper companion (notify-desktop.ts) so
|
|
92
|
+
* it carries the agents-cli mark; clicking opens the runs folder
|
|
93
|
+
* (~/.agents/.history/runs, via the `routines:list` action the companion
|
|
94
|
+
* understands). Best-effort — a missing
|
|
95
|
+
* notifier or no display is swallowed. Silent when the pass auto-fixed everything
|
|
96
|
+
* and nothing needs the operator (no point pinging them for routine self-healing).
|
|
94
97
|
*/
|
|
95
98
|
export declare function notifyHeal(r: HealResult): void;
|
|
96
99
|
/**
|
package/dist/lib/heal.js
CHANGED
|
@@ -30,7 +30,7 @@ import { repairPluginManifestFile } from './plugin-marketplace.js';
|
|
|
30
30
|
import * as fs from 'fs';
|
|
31
31
|
import * as path from 'path';
|
|
32
32
|
import * as os from 'os';
|
|
33
|
-
import {
|
|
33
|
+
import { notifyDesktop } from './menubar/notify-desktop.js';
|
|
34
34
|
// ─── diff → selection mapping ────────────────────────────────────────────────
|
|
35
35
|
// Which ResourceSelection key each healable diff kind writes through. `rules`
|
|
36
36
|
// re-syncs via the whole-memory channel (not name-scoped); `promptcuts` is not
|
|
@@ -68,10 +68,13 @@ export function summarizeHeal(r) {
|
|
|
68
68
|
return parts.length > 0 ? parts.join(', ') : 'nothing to heal';
|
|
69
69
|
}
|
|
70
70
|
/**
|
|
71
|
-
* Fire a
|
|
72
|
-
* noteworthy.
|
|
73
|
-
*
|
|
74
|
-
*
|
|
71
|
+
* Fire a branded desktop notification when a background heal did something
|
|
72
|
+
* noteworthy. Routed through the MenubarHelper companion (notify-desktop.ts) so
|
|
73
|
+
* it carries the agents-cli mark; clicking opens the runs folder
|
|
74
|
+
* (~/.agents/.history/runs, via the `routines:list` action the companion
|
|
75
|
+
* understands). Best-effort — a missing
|
|
76
|
+
* notifier or no display is swallowed. Silent when the pass auto-fixed everything
|
|
77
|
+
* and nothing needs the operator (no point pinging them for routine self-healing).
|
|
75
78
|
*/
|
|
76
79
|
export function notifyHeal(r) {
|
|
77
80
|
const needsAttention = r.skippedPlugins.length;
|
|
@@ -79,27 +82,12 @@ export function notifyHeal(r) {
|
|
|
79
82
|
if (needsAttention === 0 && healed === 0)
|
|
80
83
|
return;
|
|
81
84
|
const title = needsAttention > 0
|
|
82
|
-
?
|
|
85
|
+
? `${needsAttention} plugin${needsAttention === 1 ? '' : 's'} need attention`
|
|
83
86
|
: 'agents: auto-healed config gaps';
|
|
84
87
|
const body = needsAttention > 0
|
|
85
88
|
? `${summarizeHeal(r)}. Run: agents doctor --fix`
|
|
86
89
|
: summarizeHeal(r);
|
|
87
|
-
|
|
88
|
-
try {
|
|
89
|
-
if (platform === 'darwin') {
|
|
90
|
-
const safeTitle = title.replace(/"/g, '\\"');
|
|
91
|
-
const safeBody = body.replace(/"/g, '\\"');
|
|
92
|
-
const child = spawn('osascript', ['-e', `display notification "${safeBody}" with title "${safeTitle}"`], { detached: true, stdio: 'ignore' });
|
|
93
|
-
child.unref();
|
|
94
|
-
}
|
|
95
|
-
else if (platform === 'linux') {
|
|
96
|
-
const child = spawn('notify-send', [title, body], { detached: true, stdio: 'ignore' });
|
|
97
|
-
child.unref();
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
catch {
|
|
101
|
-
// Notification is best-effort; nothing to do.
|
|
102
|
-
}
|
|
90
|
+
notifyDesktop({ title, body, action: 'routines:list' });
|
|
103
91
|
}
|
|
104
92
|
// ─── central plugin layer (version-independent, runs once per heal) ──────────
|
|
105
93
|
/**
|
package/dist/lib/hooks.d.ts
CHANGED
|
@@ -139,6 +139,7 @@ export declare function listCentralHooks(): HookEntry[];
|
|
|
139
139
|
export declare function parseHookManifest(opts?: {
|
|
140
140
|
warn?: boolean;
|
|
141
141
|
}): Record<string, ManifestHook>;
|
|
142
|
+
export declare function selectHookManifest(manifest: Record<string, ManifestHook>, selected: string[]): Record<string, ManifestHook>;
|
|
142
143
|
/**
|
|
143
144
|
* Hook script files present on disk that no manifest entry declares — "dead"
|
|
144
145
|
* hooks. The registrar only wires manifest-declared hooks into an agent's
|
package/dist/lib/hooks.js
CHANGED
|
@@ -801,6 +801,10 @@ export function parseHookManifest(opts = {}) {
|
|
|
801
801
|
}
|
|
802
802
|
return merged;
|
|
803
803
|
}
|
|
804
|
+
export function selectHookManifest(manifest, selected) {
|
|
805
|
+
const selectedHooks = new Set(selected);
|
|
806
|
+
return Object.fromEntries(Object.entries(manifest).filter(([name, hook]) => selectedHooks.has(name) || selectedHooks.has(path.basename(hook.script))));
|
|
807
|
+
}
|
|
804
808
|
/**
|
|
805
809
|
* Hook script files present on disk that no manifest entry declares — "dead"
|
|
806
810
|
* hooks. The registrar only wires manifest-declared hooks into an agent's
|
|
@@ -934,6 +938,15 @@ function sweepOrphanShims(manifest) {
|
|
|
934
938
|
export function registerHooksToSettings(agentId, versionHome, hookManifest, agentsDirOverride) {
|
|
935
939
|
const manifest = hookManifest || parseHookManifest();
|
|
936
940
|
if (Object.keys(manifest).length === 0) {
|
|
941
|
+
if (agentId === 'opencode') {
|
|
942
|
+
const pluginPath = path.join(versionHome, '.config', 'opencode', 'plugins', 'agents-cli-hooks.ts');
|
|
943
|
+
try {
|
|
944
|
+
fs.rmSync(pluginPath, { force: true });
|
|
945
|
+
}
|
|
946
|
+
catch (e) {
|
|
947
|
+
return { registered: [], errors: [`Failed to remove agents-cli-hooks.ts: ${e.message}`] };
|
|
948
|
+
}
|
|
949
|
+
}
|
|
937
950
|
return { registered: [], errors: [] };
|
|
938
951
|
}
|
|
939
952
|
sweepOrphanShims(manifest);
|
|
@@ -991,6 +1004,9 @@ export function registerHooksToSettings(agentId, versionHome, hookManifest, agen
|
|
|
991
1004
|
if (agentId === 'grok') {
|
|
992
1005
|
return registerHooksForGrok(versionHome, manifest, resolveScript, managedPrefixes);
|
|
993
1006
|
}
|
|
1007
|
+
if (agentId === 'opencode') {
|
|
1008
|
+
return registerHooksForOpenCode(versionHome, manifest, resolveScript);
|
|
1009
|
+
}
|
|
994
1010
|
if (agentId === 'kimi') {
|
|
995
1011
|
return registerHooksForKimi(versionHome, manifest, resolveScript, managedPrefixes);
|
|
996
1012
|
}
|
|
@@ -1011,6 +1027,146 @@ export function registerHooksToSettings(agentId, versionHome, hookManifest, agen
|
|
|
1011
1027
|
}
|
|
1012
1028
|
return { registered: [], errors: [] };
|
|
1013
1029
|
}
|
|
1030
|
+
const OPENCODE_DIRECT_EVENT_MAP = {
|
|
1031
|
+
PreToolUse: 'tool.execute.before',
|
|
1032
|
+
PostToolUse: 'tool.execute.after',
|
|
1033
|
+
UserPromptSubmit: 'chat.message',
|
|
1034
|
+
};
|
|
1035
|
+
const OPENCODE_LIFECYCLE_EVENT_MAP = {
|
|
1036
|
+
SessionStart: ['session.created'],
|
|
1037
|
+
SessionEnd: ['session.deleted'],
|
|
1038
|
+
Stop: ['session.idle', 'session.error'],
|
|
1039
|
+
PreCompact: ['session.compacted'],
|
|
1040
|
+
OnError: ['session.error'],
|
|
1041
|
+
Notification: ['permission.asked'],
|
|
1042
|
+
};
|
|
1043
|
+
/**
|
|
1044
|
+
* Compile canonical hooks.yaml entries into a local OpenCode plugin.
|
|
1045
|
+
*
|
|
1046
|
+
* OpenCode has no declarative shell-hook config block. It auto-loads direct
|
|
1047
|
+
* TS/JS modules from ~/.config/opencode/plugins/, and supplies Bun's `$` shell
|
|
1048
|
+
* primitive to each plugin. The generated module subscribes to native plugin
|
|
1049
|
+
* events, sends the event payload to the managed hook script as JSON on stdin,
|
|
1050
|
+
* and surfaces a non-zero script exit as a plugin error.
|
|
1051
|
+
*/
|
|
1052
|
+
function registerHooksForOpenCode(versionHome, manifest, resolveScript) {
|
|
1053
|
+
const registered = [];
|
|
1054
|
+
const errors = [];
|
|
1055
|
+
const direct = new Map();
|
|
1056
|
+
const lifecycle = new Map();
|
|
1057
|
+
for (const [name, hookDef] of Object.entries(manifest)) {
|
|
1058
|
+
if (!hookDef.events || hookDef.events.length === 0)
|
|
1059
|
+
continue;
|
|
1060
|
+
const command = resolveHookCommand(name, hookDef, resolveScript);
|
|
1061
|
+
if (!command) {
|
|
1062
|
+
errors.push(`${name}: script not found`);
|
|
1063
|
+
continue;
|
|
1064
|
+
}
|
|
1065
|
+
const generated = {
|
|
1066
|
+
name,
|
|
1067
|
+
command,
|
|
1068
|
+
...(hookDef.timeout !== undefined ? { timeout: hookDef.timeout } : {}),
|
|
1069
|
+
...(hookDef.matcher ? { matcher: hookDef.matcher } : {}),
|
|
1070
|
+
};
|
|
1071
|
+
for (const event of hookDef.events) {
|
|
1072
|
+
const directEvent = OPENCODE_DIRECT_EVENT_MAP[event];
|
|
1073
|
+
if (directEvent) {
|
|
1074
|
+
const hooks = direct.get(directEvent) ?? [];
|
|
1075
|
+
hooks.push(generated);
|
|
1076
|
+
direct.set(directEvent, hooks);
|
|
1077
|
+
registered.push(`${name} -> ${directEvent}`);
|
|
1078
|
+
}
|
|
1079
|
+
for (const lifecycleEvent of OPENCODE_LIFECYCLE_EVENT_MAP[event] ?? []) {
|
|
1080
|
+
const hooks = lifecycle.get(lifecycleEvent) ?? [];
|
|
1081
|
+
hooks.push(generated);
|
|
1082
|
+
lifecycle.set(lifecycleEvent, hooks);
|
|
1083
|
+
registered.push(`${name} -> ${lifecycleEvent}`);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
const serializedDirect = JSON.stringify(Object.fromEntries(direct), null, 2);
|
|
1088
|
+
const serializedLifecycle = JSON.stringify(Object.fromEntries(lifecycle), null, 2);
|
|
1089
|
+
const pluginSource = `// Generated by agents-cli. Re-run agents sync to update.
|
|
1090
|
+
const directHooks = ${serializedDirect}
|
|
1091
|
+
const lifecycleHooks = ${serializedLifecycle}
|
|
1092
|
+
|
|
1093
|
+
function matches(hook, tool) {
|
|
1094
|
+
if (!hook.matcher) return true
|
|
1095
|
+
try {
|
|
1096
|
+
return new RegExp(hook.matcher).test(tool)
|
|
1097
|
+
} catch {
|
|
1098
|
+
return hook.matcher === tool
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
async function runHooks(hooks, payload, $, matchTool = false) {
|
|
1103
|
+
for (const hook of hooks ?? []) {
|
|
1104
|
+
if (matchTool && !matches(hook, payload.tool_name ?? "")) continue
|
|
1105
|
+
const input = JSON.stringify(payload)
|
|
1106
|
+
const home = Bun.env.HOME ?? Bun.env.USERPROFILE ?? ""
|
|
1107
|
+
const command = hook.command.startsWith("~/")
|
|
1108
|
+
? \`\${home}/\${hook.command.slice(2)}\`
|
|
1109
|
+
: hook.command
|
|
1110
|
+
const shell = Bun.which("bash") ?? Bun.which("sh")
|
|
1111
|
+
if (!shell) throw new Error(\`\${hook.name} requires bash or sh\`)
|
|
1112
|
+
const execArgs = [shell, "-c", 'exec "$1"', "agents-hook", command]
|
|
1113
|
+
if (hook.timeout !== undefined) {
|
|
1114
|
+
const child = Bun.spawn(execArgs, {
|
|
1115
|
+
stdin: new Response(input),
|
|
1116
|
+
stdout: "ignore",
|
|
1117
|
+
stderr: "pipe",
|
|
1118
|
+
})
|
|
1119
|
+
let timedOut = false
|
|
1120
|
+
const timer = setTimeout(() => {
|
|
1121
|
+
timedOut = true
|
|
1122
|
+
if (process.platform === "win32") {
|
|
1123
|
+
Bun.spawnSync(["taskkill", "/PID", String(child.pid), "/T", "/F"])
|
|
1124
|
+
} else {
|
|
1125
|
+
child.kill()
|
|
1126
|
+
}
|
|
1127
|
+
}, hook.timeout * 1000)
|
|
1128
|
+
const exitCode = await child.exited.finally(() => clearTimeout(timer))
|
|
1129
|
+
const stderr = await new Response(child.stderr).text()
|
|
1130
|
+
if (timedOut) {
|
|
1131
|
+
throw new Error(\`\${hook.name} timed out after \${hook.timeout} seconds\`)
|
|
1132
|
+
}
|
|
1133
|
+
if (exitCode !== 0) {
|
|
1134
|
+
throw new Error(\`\${hook.name} failed with exit code \${exitCode}: \${stderr.trim()}\`)
|
|
1135
|
+
}
|
|
1136
|
+
continue
|
|
1137
|
+
}
|
|
1138
|
+
const result = await $\`\${shell} -c \${'exec "$1"'} \${"agents-hook"} \${command} < \${new Response(input)}\`.nothrow().quiet()
|
|
1139
|
+
if (result.exitCode !== 0) {
|
|
1140
|
+
throw new Error(\`\${hook.name} failed with exit code \${result.exitCode}: \${result.stderr.toString().trim()}\`)
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
export const AgentsCliHooks = async ({ $ }) => ({
|
|
1146
|
+
event: async ({ event }) => {
|
|
1147
|
+
await runHooks(lifecycleHooks[event.type], { hook_event_name: event.type, ...event }, $)
|
|
1148
|
+
},
|
|
1149
|
+
"chat.message": async (input, output) => {
|
|
1150
|
+
await runHooks(directHooks["chat.message"], { hook_event_name: "UserPromptSubmit", ...input, ...output }, $)
|
|
1151
|
+
},
|
|
1152
|
+
"tool.execute.before": async (input, output) => {
|
|
1153
|
+
await runHooks(directHooks["tool.execute.before"], { hook_event_name: "PreToolUse", tool_name: input.tool, tool_input: output.args, ...input }, $, true)
|
|
1154
|
+
},
|
|
1155
|
+
"tool.execute.after": async (input, output) => {
|
|
1156
|
+
await runHooks(directHooks["tool.execute.after"], { hook_event_name: "PostToolUse", tool_name: input.tool, tool_input: input.args, tool_response: output, ...input }, $, true)
|
|
1157
|
+
},
|
|
1158
|
+
})
|
|
1159
|
+
`;
|
|
1160
|
+
const pluginDir = path.join(versionHome, '.config', 'opencode', 'plugins');
|
|
1161
|
+
try {
|
|
1162
|
+
fs.mkdirSync(pluginDir, { recursive: true });
|
|
1163
|
+
fs.writeFileSync(path.join(pluginDir, 'agents-cli-hooks.ts'), pluginSource, 'utf-8');
|
|
1164
|
+
}
|
|
1165
|
+
catch (e) {
|
|
1166
|
+
errors.push(`Failed to write agents-cli-hooks.ts: ${e.message}`);
|
|
1167
|
+
}
|
|
1168
|
+
return { registered, errors };
|
|
1169
|
+
}
|
|
1014
1170
|
/**
|
|
1015
1171
|
* Antigravity (agy) event names differ from agents-cli manifest names. The
|
|
1016
1172
|
* mapping below is the documented agy schema. PostToolUse has no exact
|
|
@@ -20,6 +20,15 @@ import { type HostCredentials } from './credentials.js';
|
|
|
20
20
|
* remainder. Any other path (absolute or relative) is quoted verbatim.
|
|
21
21
|
*/
|
|
22
22
|
export declare function remoteCdPrefix(remoteCwd?: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* Merge the resolved actor's provenance env UNDER a caller-supplied env, so every
|
|
25
|
+
* remote `agents …` invocation forwards `AGENTS_ACTOR*` / `GIT_*` across the SSH
|
|
26
|
+
* hop. Without it the remote re-resolves the actor from the ORIGINATING box's
|
|
27
|
+
* `SSH_CONNECTION` (the wrong IP) and mis-credits the run to the shared machine or
|
|
28
|
+
* `UNRESOLVED@<host>` (RUSH-2028). A caller-supplied value wins on any key
|
|
29
|
+
* collision, mirroring `buildExecEnv`'s `...options.env` precedence (exec.ts).
|
|
30
|
+
*/
|
|
31
|
+
export declare function withActorEnv(env?: Record<string, string>): Record<string, string>;
|
|
23
32
|
/**
|
|
24
33
|
* Launch a detached login-shell command in its own Unix session/process group.
|
|
25
34
|
*
|
|
@@ -107,6 +116,13 @@ export interface DispatchOptions {
|
|
|
107
116
|
name?: string;
|
|
108
117
|
/** Resume an existing session on the host by id (via `agents run --resume`). */
|
|
109
118
|
resume?: string;
|
|
119
|
+
/**
|
|
120
|
+
* Forward `--emit-session-id` so the remote run prints its resolved session id
|
|
121
|
+
* as a stdout sentinel (hosts/session-marker.ts). The launcher parses that id
|
|
122
|
+
* out of the followed log and stamps it on the task — the join that maps a
|
|
123
|
+
* remote-created session back home for agents that don't take `--session-id`.
|
|
124
|
+
*/
|
|
125
|
+
emitSessionId?: boolean;
|
|
110
126
|
/** Stream progress and block until completion (default true). */
|
|
111
127
|
follow?: boolean;
|
|
112
128
|
timeoutMs?: number;
|
|
@@ -12,8 +12,9 @@ import { randomUUID } from 'crypto';
|
|
|
12
12
|
import { sshExec, sshStream, shellQuote } from '../ssh-exec.js';
|
|
13
13
|
import { sshTargetFor } from './types.js';
|
|
14
14
|
import { ensureHostReady } from './ready.js';
|
|
15
|
-
import { remoteShellFor } from './remote-cmd.js';
|
|
15
|
+
import { remoteShellFor, posixEnvExports } from './remote-cmd.js';
|
|
16
16
|
import { resolveRemoteOsSync } from './remote-os.js';
|
|
17
|
+
import { resolveActor, actorEnv } from '../actor.js';
|
|
17
18
|
import { saveTask, updateTask, terminalPatch } from './tasks.js';
|
|
18
19
|
import { followHostTask } from './progress.js';
|
|
19
20
|
import { wrapHostCommandWithCredentials } from './credentials.js';
|
|
@@ -56,6 +57,17 @@ export function remoteCdPrefix(remoteCwd) {
|
|
|
56
57
|
return `cd "$HOME"/${shellQuote(rest)} && `;
|
|
57
58
|
return `cd ${shellQuote(remoteCwd)} && `;
|
|
58
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Merge the resolved actor's provenance env UNDER a caller-supplied env, so every
|
|
62
|
+
* remote `agents …` invocation forwards `AGENTS_ACTOR*` / `GIT_*` across the SSH
|
|
63
|
+
* hop. Without it the remote re-resolves the actor from the ORIGINATING box's
|
|
64
|
+
* `SSH_CONNECTION` (the wrong IP) and mis-credits the run to the shared machine or
|
|
65
|
+
* `UNRESOLVED@<host>` (RUSH-2028). A caller-supplied value wins on any key
|
|
66
|
+
* collision, mirroring `buildExecEnv`'s `...options.env` precedence (exec.ts).
|
|
67
|
+
*/
|
|
68
|
+
export function withActorEnv(env) {
|
|
69
|
+
return { ...actorEnv(resolveActor()), ...(env ?? {}) };
|
|
70
|
+
}
|
|
59
71
|
/**
|
|
60
72
|
* Launch a detached login-shell command in its own Unix session/process group.
|
|
61
73
|
*
|
|
@@ -174,10 +186,14 @@ async function launchDetached(host, target, opts) {
|
|
|
174
186
|
const id = randomUUID().slice(0, 8);
|
|
175
187
|
const remoteLog = `${REMOTE_DIR}/${id}.log`;
|
|
176
188
|
const remoteExit = `${REMOTE_DIR}/${id}.exit`;
|
|
177
|
-
// Inner command run under a login shell so PATH resolves `agents`.
|
|
189
|
+
// Inner command run under a login shell so PATH resolves `agents`. Export the
|
|
190
|
+
// resolved actor provenance first so the detached remote run inherits it
|
|
191
|
+
// instead of re-resolving from this box's SSH_CONNECTION (RUSH-2028).
|
|
178
192
|
const invocation = ['agents', ...opts.forwardedArgs].map(shellQuote).join(' ');
|
|
179
193
|
const cwd = remoteCdPrefix(opts.remoteCwd);
|
|
180
|
-
|
|
194
|
+
const actorExports = posixEnvExports(withActorEnv());
|
|
195
|
+
const prelude = actorExports ? `${actorExports}; ` : '';
|
|
196
|
+
let inner = `${prelude}${cwd}${invocation} > ${remoteLog} 2>&1; echo $? > ${remoteExit}`;
|
|
181
197
|
if (opts.copyCreds) {
|
|
182
198
|
inner = wrapHostCommandWithCredentials(inner, opts.copyCreds);
|
|
183
199
|
}
|
|
@@ -298,6 +314,8 @@ export function buildRunForwardedArgs(opts) {
|
|
|
298
314
|
args.push('--resume', opts.resume);
|
|
299
315
|
else if (opts.sessionId)
|
|
300
316
|
args.push('--session-id', opts.sessionId);
|
|
317
|
+
if (opts.emitSessionId)
|
|
318
|
+
args.push('--emit-session-id');
|
|
301
319
|
if (opts.passthroughArgs && opts.passthroughArgs.length > 0)
|
|
302
320
|
args.push('--', ...opts.passthroughArgs);
|
|
303
321
|
return args;
|
|
@@ -368,7 +386,11 @@ export async function runInteractiveOnHost(host, opts) {
|
|
|
368
386
|
process.stderr.write(`[hosts] warning: ${w}\n`);
|
|
369
387
|
const invocation = ['agents', ...buildInteractiveRunForwardedArgs(opts)].map(shellQuote).join(' ');
|
|
370
388
|
const cwd = remoteCdPrefix(opts.remoteCwd);
|
|
371
|
-
|
|
389
|
+
// Forward actor provenance so the interactive remote run inherits it rather
|
|
390
|
+
// than re-resolving from this box's SSH_CONNECTION (RUSH-2028).
|
|
391
|
+
const actorExports = posixEnvExports(withActorEnv());
|
|
392
|
+
const prelude = actorExports ? `${actorExports}; ` : '';
|
|
393
|
+
let remoteCmd = `${prelude}${cwd}${invocation}`;
|
|
372
394
|
if (opts.copyCreds) {
|
|
373
395
|
remoteCmd = wrapHostCommandWithCredentials(remoteCmd, opts.copyCreds);
|
|
374
396
|
}
|
|
@@ -20,7 +20,7 @@ import chalk from 'chalk';
|
|
|
20
20
|
import { assertValidSshTarget, sshStream } from '../ssh-exec.js';
|
|
21
21
|
import { resolveHost, resolveHostByCap } from './registry.js';
|
|
22
22
|
import { sshTargetFor } from './types.js';
|
|
23
|
-
import { dispatchAgentsCommand } from './dispatch.js';
|
|
23
|
+
import { dispatchAgentsCommand, withActorEnv } from './dispatch.js';
|
|
24
24
|
import { stripRoutingFlags, buildRemoteAgentsInvocation, HOST_ROUTING_SPECS, } from './remote-cmd.js';
|
|
25
25
|
import { resolveRemoteOsSync } from './remote-os.js';
|
|
26
26
|
import { machineId } from '../session/sync/config.js';
|
|
@@ -98,6 +98,8 @@ const REMOTE_PASSTHROUGH = {
|
|
|
98
98
|
const OWN_HOST_COMMANDS = new Set([
|
|
99
99
|
'run',
|
|
100
100
|
'exec', // deprecated alias of run
|
|
101
|
+
'harness', // `--host <agent>` names the host CLI to run under, not a remote device
|
|
102
|
+
'harnesses',
|
|
101
103
|
'sessions',
|
|
102
104
|
'feed',
|
|
103
105
|
'computer',
|
|
@@ -265,9 +267,14 @@ export async function maybeRunOnHost(command, allArgs) {
|
|
|
265
267
|
// profile, and single-quoted env values would not expand $HOME/$PATH.
|
|
266
268
|
const isDoctorCommand = command === 'doctor' || (command === 'teams' && forwarded[1] === 'doctor');
|
|
267
269
|
const remoteOs = resolveRemoteOsSync(host.name);
|
|
268
|
-
const
|
|
270
|
+
const doctorPath = isDoctorCommand && !/^win/i.test((remoteOs ?? '').trim())
|
|
269
271
|
? { PATH: '$HOME/.agents/.cache/shims:$HOME/.local/bin:$PATH' }
|
|
270
272
|
: undefined;
|
|
273
|
+
// Forward actor provenance (AGENTS_ACTOR*/GIT_*) across the SSH hop, merged
|
|
274
|
+
// UNDER the doctor PATH so that PATH still wins — without this the remote
|
|
275
|
+
// re-resolves the actor from THIS box's SSH_CONNECTION and mis-credits it
|
|
276
|
+
// (RUSH-2028). Flows to both POSIX (export) and Windows ($env:) dialects.
|
|
277
|
+
const env = withActorEnv(doctorPath);
|
|
271
278
|
const remoteCmd = buildRemoteAgentsInvocation(forwarded, remoteCwd, remoteOs, env);
|
|
272
279
|
const code = sshStream(target, remoteCmd, { tty: interactive, multiplex: true });
|
|
273
280
|
if (code === 255) {
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { type Host } from './types.js';
|
|
2
|
+
/** ssh's connection-layer failure code — the signal that the link dropped rather
|
|
3
|
+
* than the remote command exiting on its own. Mirrors ssh-exec.ts `sshStream`. */
|
|
4
|
+
export declare const SSH_CONN_FAILURE = 255;
|
|
5
|
+
/** Consecutive failed-to-connect reattaches before giving up. Backoff is capped at
|
|
6
|
+
* {@link MAX_BACKOFF_MS}. A reattach that actually reconnected (then dropped again)
|
|
7
|
+
* refills the budget, so a long session that blinks all day reconnects every time
|
|
8
|
+
* — only consecutive UNREACHABLE attempts exhaust it. */
|
|
9
|
+
export declare const MAX_ATTEMPTS = 6;
|
|
10
|
+
export interface ReconnectState {
|
|
11
|
+
/** Consecutive failed-to-connect reattaches since the last genuine reconnection. */
|
|
12
|
+
attempt: number;
|
|
13
|
+
}
|
|
14
|
+
export interface ReconnectOutcome {
|
|
15
|
+
/** Exit code of the run (initial) or the last re-attach. */
|
|
16
|
+
code: number;
|
|
17
|
+
/** Whether the ssh handshake for this attempt actually completed. The initial run
|
|
18
|
+
* and any reattach whose preflight probe succeeded are `connected`; a reattach
|
|
19
|
+
* that couldn't reach the host is not. Drives the budget refill (see file head). */
|
|
20
|
+
connected: boolean;
|
|
21
|
+
}
|
|
22
|
+
export type ReconnectDecision = {
|
|
23
|
+
action: 'stop';
|
|
24
|
+
code: number;
|
|
25
|
+
} | {
|
|
26
|
+
action: 'retry';
|
|
27
|
+
waitMs: number;
|
|
28
|
+
state: ReconnectState;
|
|
29
|
+
};
|
|
30
|
+
export declare function initialReconnectState(): ReconnectState;
|
|
31
|
+
/** Exponential backoff capped at {@link MAX_BACKOFF_MS}: 2s, 4s, 8s, 16s, 30s… */
|
|
32
|
+
export declare function backoffMs(attempt: number): number;
|
|
33
|
+
/**
|
|
34
|
+
* Decide what to do after a run/re-attach returned `outcome`. Pure — the only
|
|
35
|
+
* input is the prior state and the outcome, the only output is the next action.
|
|
36
|
+
*
|
|
37
|
+
* - a non-255 code means the remote command spoke for itself (clean detach = 0,
|
|
38
|
+
* agent exit / no live session = non-zero) → stop and surface that code.
|
|
39
|
+
* - a 255 means the link dropped → retry, unless the budget is spent.
|
|
40
|
+
* - a 255 from an attempt that DID connect (a genuine reconnection that then
|
|
41
|
+
* dropped) refills the budget first; a 255 that never connected counts against
|
|
42
|
+
* it, so a host that stays unreachable gives up after MAX_ATTEMPTS.
|
|
43
|
+
*/
|
|
44
|
+
export declare function reconnectStep(state: ReconnectState, outcome: ReconnectOutcome): ReconnectDecision;
|
|
45
|
+
/** Human-readable notice shown before each reconnect wait. "13 seconds", not "12.8s". */
|
|
46
|
+
export declare function reconnectNotice(sessionId: string, host: string, attempt: number, waitMs: number): string;
|
|
47
|
+
/** Notice shown once the retry budget is spent. */
|
|
48
|
+
export declare function exhaustedNotice(sessionId: string, host: string): string;
|
|
49
|
+
/**
|
|
50
|
+
* Re-attach the live remote tmux pane for `sessionId` by driving the peer's own
|
|
51
|
+
* `agents sessions focus`. A fast, un-multiplexed preflight probe (`ssh … true`)
|
|
52
|
+
* first establishes whether the host is actually reachable this attempt — that
|
|
53
|
+
* `connected` bit, not the call duration, is what the retry policy keys on. Only on
|
|
54
|
+
* a reachable host do we run the interactive attach (which carries no credentials —
|
|
55
|
+
* the agent already runs on the peer — so it rides the normal transport). Returns
|
|
56
|
+
* the ssh exit code (255 = dropped again / unreachable; 0 = clean detach; other =
|
|
57
|
+
* session ended) plus whether this attempt connected.
|
|
58
|
+
*/
|
|
59
|
+
export declare function reattachRemoteSession(host: Host, sessionId: string): ReconnectOutcome;
|
|
60
|
+
export interface ReconnectLoopOpts {
|
|
61
|
+
host: Host;
|
|
62
|
+
sessionId: string;
|
|
63
|
+
/** The exit code from the initial interactive run (which, having run the agent,
|
|
64
|
+
* is treated as a connected attempt). */
|
|
65
|
+
initialExit: number;
|
|
66
|
+
/** Injected for tests: the real re-attach and wait are swapped for deterministic
|
|
67
|
+
* fakes so the loop's control flow is exercised without SSH. Production uses the
|
|
68
|
+
* real {@link reattachRemoteSession} + `sleep`. */
|
|
69
|
+
reattach?: (host: Host, sessionId: string) => ReconnectOutcome;
|
|
70
|
+
wait?: (ms: number) => Promise<void>;
|
|
71
|
+
write?: (s: string) => void;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Drive the reconnect loop from the initial run's outcome to a terminal code.
|
|
75
|
+
* Only the real SSH re-attach and the wait are side effects; the decision is
|
|
76
|
+
* {@link reconnectStep}. Returns the exit code the process should ultimately use.
|
|
77
|
+
*/
|
|
78
|
+
export declare function reconnectInteractiveSession(opts: ReconnectLoopOpts): Promise<number>;
|