@phnx-labs/agents-cli 1.21.2 → 1.22.0
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 +101 -0
- package/README.md +32 -3
- package/dist/bin/agents +0 -0
- package/dist/commands/computer-actions.d.ts +4 -0
- package/dist/commands/computer-actions.js +35 -0
- package/dist/commands/computer.js +4 -2
- package/dist/commands/exec.d.ts +27 -0
- package/dist/commands/exec.js +123 -6
- package/dist/commands/models.js +36 -1
- package/dist/commands/perf.d.ts +16 -0
- package/dist/commands/perf.js +11 -1
- package/dist/commands/projects.d.ts +11 -1
- package/dist/commands/projects.js +38 -4
- package/dist/commands/sessions-backfill.d.ts +32 -0
- package/dist/commands/sessions-backfill.js +186 -0
- package/dist/commands/sessions-picker.js +17 -2
- package/dist/commands/sessions.d.ts +22 -1
- package/dist/commands/sessions.js +331 -18
- package/dist/commands/teams.js +1 -1
- package/dist/commands/worktree.d.ts +3 -3
- package/dist/commands/worktree.js +35 -4
- package/dist/index.js +8 -0
- package/dist/lib/browser/service.js +13 -0
- package/dist/lib/computer/dispatch.d.ts +3 -1
- package/dist/lib/computer/dispatch.js +10 -2
- package/dist/lib/daemon.d.ts +5 -1
- package/dist/lib/daemon.js +63 -14
- package/dist/lib/devices/resolve-target.d.ts +6 -0
- package/dist/lib/devices/resolve-target.js +9 -3
- package/dist/lib/event-stream.d.ts +2 -0
- package/dist/lib/event-stream.js +3 -0
- package/dist/lib/events.d.ts +3 -1
- package/dist/lib/events.js +4 -2
- package/dist/lib/exec.js +39 -8
- package/dist/lib/git.d.ts +14 -0
- package/dist/lib/git.js +36 -0
- package/dist/lib/hooks/profile.js +1 -14
- package/dist/lib/hosts/dispatch.d.ts +12 -0
- package/dist/lib/hosts/dispatch.js +23 -6
- package/dist/lib/hosts/reconnect.d.ts +38 -0
- package/dist/lib/hosts/reconnect.js +85 -4
- package/dist/lib/hosts/run-target.js +14 -2
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/model-tiers.d.ts +54 -0
- package/dist/lib/model-tiers.js +229 -0
- package/dist/lib/models.d.ts +3 -0
- package/dist/lib/models.js +44 -7
- package/dist/lib/percentile.d.ts +12 -0
- package/dist/lib/percentile.js +24 -0
- package/dist/lib/perf/db.d.ts +1 -2
- package/dist/lib/perf/db.js +2 -14
- package/dist/lib/plugins.js +12 -1
- package/dist/lib/pricing/prices.json +16 -1
- package/dist/lib/project-focus.d.ts +42 -0
- package/dist/lib/project-focus.js +80 -0
- package/dist/lib/project-schedule.d.ts +75 -0
- package/dist/lib/project-schedule.js +110 -0
- package/dist/lib/redact.d.ts +2 -0
- package/dist/lib/redact.js +22 -0
- package/dist/lib/remote-agents-json.d.ts +2 -0
- package/dist/lib/remote-agents-json.js +3 -3
- package/dist/lib/resources.d.ts +16 -0
- package/dist/lib/resources.js +25 -14
- package/dist/lib/rotate.d.ts +84 -1
- package/dist/lib/rotate.js +155 -5
- package/dist/lib/routines.js +1 -14
- package/dist/lib/runner.d.ts +4 -2
- package/dist/lib/runner.js +21 -5
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/session/bash-command.js +60 -9
- package/dist/lib/session/db.d.ts +22 -1
- package/dist/lib/session/db.js +516 -24
- package/dist/lib/session/discover.d.ts +68 -7
- package/dist/lib/session/discover.js +186 -84
- package/dist/lib/session/highlights.d.ts +24 -4
- package/dist/lib/session/highlights.js +52 -7
- package/dist/lib/session/parse.d.ts +8 -1
- package/dist/lib/session/parse.js +102 -35
- package/dist/lib/session/prompt.d.ts +19 -0
- package/dist/lib/session/prompt.js +43 -0
- package/dist/lib/session/remote-list.d.ts +71 -0
- package/dist/lib/session/remote-list.js +410 -2
- package/dist/lib/session/shell-programs.d.ts +15 -0
- package/dist/lib/session/shell-programs.js +359 -0
- package/dist/lib/session/tool-calls.d.ts +88 -0
- package/dist/lib/session/tool-calls.js +612 -0
- package/dist/lib/session/tool-index.d.ts +100 -0
- package/dist/lib/session/tool-index.js +773 -0
- package/dist/lib/session/tool-store.d.ts +15 -0
- package/dist/lib/session/tool-store.js +198 -0
- package/dist/lib/session/types.d.ts +49 -0
- package/dist/lib/state.d.ts +10 -1
- package/dist/lib/state.js +11 -2
- package/dist/lib/teams/remoteWorktree.d.ts +3 -4
- package/dist/lib/teams/remoteWorktree.js +3 -4
- package/dist/lib/teams/worktree.d.ts +11 -1
- package/dist/lib/teams/worktree.js +42 -4
- package/dist/lib/types.d.ts +31 -0
- package/dist/lib/types.js +17 -0
- package/package.json +3 -1
|
@@ -3,6 +3,8 @@ import { type AppInfo } from '../../commands/computer-actions.js';
|
|
|
3
3
|
import type { VerbDispatcher } from './loop.js';
|
|
4
4
|
declare function rpcMethodFor(verb: string): string;
|
|
5
5
|
declare function toRpcParams(verb: string, input: Record<string, unknown>, pid?: number): Record<string, unknown>;
|
|
6
|
-
export declare function makeVerbDispatcher(client: ComputerClient
|
|
6
|
+
export declare function makeVerbDispatcher(client: ComputerClient, context?: {
|
|
7
|
+
host?: string;
|
|
8
|
+
}): VerbDispatcher;
|
|
7
9
|
export { rpcMethodFor, toRpcParams };
|
|
8
10
|
export type { AppInfo };
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// it names the same RPC methods the explicit `agents computer <verb>` commands
|
|
6
6
|
// call, so the external-agent verb surface is untouched. The daemon stays the
|
|
7
7
|
// single authority on permissions and targeting.
|
|
8
|
-
import { resolveTargetPidDecision } from '../../commands/computer-actions.js';
|
|
8
|
+
import { resolveTargetPidDecision, emitComputerAction } from '../../commands/computer-actions.js';
|
|
9
9
|
// CLI verb -> daemon RPC method. Only the rename cases need listing; the rest
|
|
10
10
|
// pass through by name.
|
|
11
11
|
const RPC_METHOD = {
|
|
@@ -102,7 +102,14 @@ function toRpcParams(verb, input, pid) {
|
|
|
102
102
|
// Build a dispatcher bound to a live client. Every call is a single daemon
|
|
103
103
|
// round-trip; errors are returned (never thrown) so the loop can feed them
|
|
104
104
|
// back to the model as a tool_result.
|
|
105
|
-
|
|
105
|
+
//
|
|
106
|
+
// `computer run`'s embedded model loop drives every verb (click, type,
|
|
107
|
+
// screenshot, ...) through THIS single seam rather than the explicit
|
|
108
|
+
// `agents computer <verb>` commands in computer-actions.ts, so it must emit
|
|
109
|
+
// the same `computer.action` event those commands emit — otherwise a session
|
|
110
|
+
// that only used the loop reads back usedComputer=false even though it drove
|
|
111
|
+
// real actions (reviewer-flagged regression on #1864).
|
|
112
|
+
export function makeVerbDispatcher(client, context = {}) {
|
|
106
113
|
return async (call) => {
|
|
107
114
|
const verb = call.name;
|
|
108
115
|
const input = call.input ?? {};
|
|
@@ -118,6 +125,7 @@ export function makeVerbDispatcher(client) {
|
|
|
118
125
|
const res = await client.call(method, params);
|
|
119
126
|
if (res.error)
|
|
120
127
|
return { ok: false, error: `${res.error.code}: ${res.error.message}` };
|
|
128
|
+
emitComputerAction(verb, pid, { bundle: input.bundle, host: context.host });
|
|
121
129
|
return { ok: true, result: res.result ?? {} };
|
|
122
130
|
};
|
|
123
131
|
}
|
package/dist/lib/daemon.d.ts
CHANGED
|
@@ -43,7 +43,11 @@ export declare function writeHeartbeat(pid?: number): void;
|
|
|
43
43
|
export declare function readHeartbeat(): DaemonHeartbeat | null;
|
|
44
44
|
export declare function removeHeartbeat(): void;
|
|
45
45
|
export declare function isDaemonWedged(): boolean;
|
|
46
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* Check whether a daemon is alive — via the pid file, or a fresh heartbeat when
|
|
48
|
+
* the pid file has been lost (see resolveLiveDaemonPid). Heals the pid file as a
|
|
49
|
+
* side effect so a subsequent read is consistent.
|
|
50
|
+
*/
|
|
47
51
|
export declare function isDaemonRunning(): boolean;
|
|
48
52
|
/**
|
|
49
53
|
* Single-instance claim for the daemon foreground entrypoint.
|
package/dist/lib/daemon.js
CHANGED
|
@@ -172,6 +172,16 @@ export function removeHeartbeat() {
|
|
|
172
172
|
}
|
|
173
173
|
catch { /* already removed */ }
|
|
174
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* A heartbeat is "fresh" when its last tick falls inside the wedge window — the
|
|
177
|
+
* same threshold isDaemonWedged() uses to decide a still-present daemon has gone
|
|
178
|
+
* unresponsive. A fresh heartbeat whose pid is alive is proof of a live, ticking
|
|
179
|
+
* daemon even when the pid file has been lost.
|
|
180
|
+
*/
|
|
181
|
+
function isHeartbeatFresh(hb) {
|
|
182
|
+
const elapsed = Date.now() - Date.parse(hb.lastTick);
|
|
183
|
+
return elapsed <= WEDGE_THRESHOLD_TICKS * MONITOR_TICK_MS;
|
|
184
|
+
}
|
|
175
185
|
export function isDaemonWedged() {
|
|
176
186
|
const pid = readDaemonPid();
|
|
177
187
|
if (!pid)
|
|
@@ -183,22 +193,49 @@ export function isDaemonWedged() {
|
|
|
183
193
|
return false;
|
|
184
194
|
if (hb.pid !== pid)
|
|
185
195
|
return false;
|
|
186
|
-
|
|
187
|
-
return elapsed > WEDGE_THRESHOLD_TICKS * MONITOR_TICK_MS;
|
|
196
|
+
return !isHeartbeatFresh(hb);
|
|
188
197
|
}
|
|
189
198
|
/** How long stopDaemon waits for a SIGTERMed daemon to exit before escalating. */
|
|
190
199
|
const STOP_GRACE_MS = 5000;
|
|
191
200
|
/** How long it waits after the hard tree-kill before giving up. */
|
|
192
201
|
const STOP_KILL_GRACE_MS = 2000;
|
|
193
|
-
/**
|
|
194
|
-
|
|
202
|
+
/**
|
|
203
|
+
* Resolve the PID of the live daemon, tolerant of a pid-file/heartbeat desync.
|
|
204
|
+
*
|
|
205
|
+
* The daemon writes the pid file once (on claim/start) but rewrites the
|
|
206
|
+
* heartbeat every tick. If the pid file is lost while the daemon keeps ticking
|
|
207
|
+
* — e.g. an earlier isDaemonRunning() found a stale/reused/dead pid and cleared
|
|
208
|
+
* the file, or it was removed out from under a live daemon — the pid file reads
|
|
209
|
+
* empty even though a daemon is genuinely alive and firing jobs. Reading only
|
|
210
|
+
* the pid file then reports "stopped" for a running scheduler, and (worse) lets
|
|
211
|
+
* claimDaemonInstance() start a SECOND daemon that double-fires every routine.
|
|
212
|
+
*
|
|
213
|
+
* So: trust the pid file when its pid is alive; otherwise trust a FRESH
|
|
214
|
+
* heartbeat whose pid is alive, and re-adopt the pid file so the desync heals.
|
|
215
|
+
* Returns null only when neither points at a live process (clearing a stale pid
|
|
216
|
+
* file on the way out).
|
|
217
|
+
*/
|
|
218
|
+
function resolveLiveDaemonPid() {
|
|
195
219
|
const pid = readDaemonPid();
|
|
196
|
-
if (
|
|
197
|
-
return
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
220
|
+
if (pid !== null && isAlive(pid))
|
|
221
|
+
return pid;
|
|
222
|
+
const hb = readHeartbeat();
|
|
223
|
+
if (hb && isAlive(hb.pid) && isHeartbeatFresh(hb)) {
|
|
224
|
+
if (pid !== hb.pid)
|
|
225
|
+
writeDaemonPid(hb.pid); // heal the pid-file/heartbeat desync
|
|
226
|
+
return hb.pid;
|
|
227
|
+
}
|
|
228
|
+
if (pid !== null)
|
|
229
|
+
removeDaemonPid();
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Check whether a daemon is alive — via the pid file, or a fresh heartbeat when
|
|
234
|
+
* the pid file has been lost (see resolveLiveDaemonPid). Heals the pid file as a
|
|
235
|
+
* side effect so a subsequent read is consistent.
|
|
236
|
+
*/
|
|
237
|
+
export function isDaemonRunning() {
|
|
238
|
+
return resolveLiveDaemonPid() !== null;
|
|
202
239
|
}
|
|
203
240
|
/**
|
|
204
241
|
* Single-instance claim for the daemon foreground entrypoint.
|
|
@@ -217,16 +254,28 @@ export function isDaemonRunning() {
|
|
|
217
254
|
*/
|
|
218
255
|
export function claimDaemonInstance() {
|
|
219
256
|
const release = acquireStartLock();
|
|
257
|
+
// acquireStartLock() returns null only when another __daemon-run currently
|
|
258
|
+
// holds the O_EXCL lock — a dead holder's lock is reclaimed and retried inside
|
|
259
|
+
// acquireStartLock, so null means a *live* claimer is mid-claim. Bail rather
|
|
260
|
+
// than run the read-decide-write unlocked: otherwise two first-start processes
|
|
261
|
+
// could each see no pid file (before either writes one) and both claim,
|
|
262
|
+
// running the concurrent JobScheduler this guard exists to prevent.
|
|
263
|
+
if (!release)
|
|
264
|
+
return false;
|
|
220
265
|
try {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
266
|
+
// resolveLiveDaemonPid() also consults a fresh heartbeat, so a live daemon
|
|
267
|
+
// whose pid file was lost still blocks a second claim — otherwise a missing
|
|
268
|
+
// pid file would let this instance start a concurrent JobScheduler and
|
|
269
|
+
// double-fire every routine.
|
|
270
|
+
const existing = resolveLiveDaemonPid();
|
|
271
|
+
if (existing !== null && existing !== process.pid) {
|
|
272
|
+
return false; // another live daemon already owns the instance
|
|
224
273
|
}
|
|
225
274
|
writeDaemonPid(process.pid);
|
|
226
275
|
return true;
|
|
227
276
|
}
|
|
228
277
|
finally {
|
|
229
|
-
release
|
|
278
|
+
release();
|
|
230
279
|
}
|
|
231
280
|
}
|
|
232
281
|
/**
|
|
@@ -9,6 +9,10 @@ export interface ResolvedSshTarget {
|
|
|
9
9
|
name: string;
|
|
10
10
|
os?: string;
|
|
11
11
|
}
|
|
12
|
+
export interface ResolvedExplicitTargetSet {
|
|
13
|
+
targets: ResolvedSshTarget[];
|
|
14
|
+
unresolved: string[];
|
|
15
|
+
}
|
|
12
16
|
/**
|
|
13
17
|
* Resolve a target token to a full {@link DeviceProfile} for `agents ssh`. Same
|
|
14
18
|
* grammar as the fan-out, but returns the whole profile (auth, shell, tailscale
|
|
@@ -27,3 +31,5 @@ export declare function resolveDeviceTarget(token: string): Promise<DeviceProfil
|
|
|
27
31
|
* cross-machine fan-out so they can never diverge onto two routes.
|
|
28
32
|
*/
|
|
29
33
|
export declare function resolveExplicitTargets(hosts: string[]): Promise<ResolvedSshTarget[]>;
|
|
34
|
+
/** Resolve explicit tokens while retaining failures for coverage-sensitive callers. */
|
|
35
|
+
export declare function resolveExplicitTargetSet(hosts: string[]): Promise<ResolvedExplicitTargetSet>;
|
|
@@ -96,14 +96,20 @@ export async function resolveDeviceTarget(token) {
|
|
|
96
96
|
* cross-machine fan-out so they can never diverge onto two routes.
|
|
97
97
|
*/
|
|
98
98
|
export async function resolveExplicitTargets(hosts) {
|
|
99
|
-
|
|
99
|
+
return (await resolveExplicitTargetSet(hosts)).targets;
|
|
100
|
+
}
|
|
101
|
+
/** Resolve explicit tokens while retaining failures for coverage-sensitive callers. */
|
|
102
|
+
export async function resolveExplicitTargetSet(hosts) {
|
|
103
|
+
const targets = [];
|
|
104
|
+
const unresolved = [];
|
|
100
105
|
for (const h of hosts) {
|
|
101
106
|
const resolved = await toResolvedTarget(h);
|
|
102
107
|
if (!resolved) {
|
|
103
108
|
process.stderr.write(chalk.gray(` ${h}: not a resolvable ssh target — skipped\n`));
|
|
109
|
+
unresolved.push(h);
|
|
104
110
|
continue;
|
|
105
111
|
}
|
|
106
|
-
|
|
112
|
+
targets.push(resolved);
|
|
107
113
|
}
|
|
108
|
-
return
|
|
114
|
+
return { targets, unresolved };
|
|
109
115
|
}
|
|
@@ -18,6 +18,8 @@ export interface UnifiedQuery {
|
|
|
18
18
|
eventTypes?: EventType[];
|
|
19
19
|
level?: EventLevel;
|
|
20
20
|
agent?: string;
|
|
21
|
+
/** Only events stamped with this session id (payload `sessionId`, the provenance floor). */
|
|
22
|
+
sessionId?: string;
|
|
21
23
|
caller?: string;
|
|
22
24
|
command?: string;
|
|
23
25
|
module?: string;
|
package/dist/lib/event-stream.js
CHANGED
|
@@ -29,6 +29,8 @@ function matches(r, q) {
|
|
|
29
29
|
return false;
|
|
30
30
|
if (q.agent && r.agent !== q.agent)
|
|
31
31
|
return false;
|
|
32
|
+
if (q.sessionId && r.sessionId !== q.sessionId)
|
|
33
|
+
return false;
|
|
32
34
|
if (q.caller && r.caller !== q.caller)
|
|
33
35
|
return false;
|
|
34
36
|
if (q.command && r.command !== q.command &&
|
|
@@ -51,6 +53,7 @@ export function readUnifiedEvents(q = {}) {
|
|
|
51
53
|
eventTypes: q.eventTypes,
|
|
52
54
|
level: q.level,
|
|
53
55
|
agent: q.agent,
|
|
56
|
+
sessionId: q.sessionId,
|
|
54
57
|
caller: q.caller,
|
|
55
58
|
command: q.command,
|
|
56
59
|
module: q.module,
|
package/dist/lib/events.d.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import type { ActorKind } from './actor.js';
|
|
15
15
|
export type EventLevel = 'audit' | 'warn' | 'info' | 'debug';
|
|
16
|
-
export type EventType = 'agent.run.start' | 'agent.run.end' | 'agent.spawn.start' | 'agent.spawn.end' | 'version.install' | 'version.switch' | 'version.remove' | 'skill.install' | 'skill.remove' | 'browser.launch' | 'browser.close' | 'browser.navigate' | 'browser.screenshot' | 'secrets.get' | 'secrets.unlocked' | 'secrets.create' | 'secrets.import' | 'secrets.export' | 'secrets.view' | 'secrets.set' | 'secrets.delete' | 'secrets.rename' | 'cloud.dispatch' | 'cloud.complete' | 'cloud.cancel' | 'cloud.message' | 'teams.create' | 'teams.add' | 'teams.start' | 'teams.complete' | 'teams.disband' | 'hook.fire' | 'hook.complete' | 'hook.error' | 'mcp.add' | 'mcp.remove' | 'mcp.register' | 'resource.sync' | 'rotation.resolved' | 'command.start' | 'command.end' | 'perf.timing' | 'session.start' | 'session.end' | 'webhook.received' | 'webhook.authorized' | 'webhook.rejected' | 'webhook.matched' | 'webhook.fired' | 'webhook.handler.start' | 'webhook.handler.end' | 'plan.created' | 'pr.opened' | 'pr.merged' | 'worktree.created' | 'worktree.removed' | 'commit.created' | 'pushed' | 'subagent.spawned' | 'artifact.created' | 'task.completed' | 'checklist.created' | 'status.posted' | 'file.edited' | 'factory.command' | 'factory.action' | 'factory.uri' | 'factory.launch' | 'friction' | 'error' | 'warn' | 'info' | 'debug';
|
|
16
|
+
export type EventType = 'agent.run.start' | 'agent.run.end' | 'agent.spawn.start' | 'agent.spawn.end' | 'version.install' | 'version.switch' | 'version.remove' | 'skill.install' | 'skill.remove' | 'browser.launch' | 'browser.close' | 'browser.navigate' | 'browser.screenshot' | 'computer.action' | 'secrets.get' | 'secrets.unlocked' | 'secrets.create' | 'secrets.import' | 'secrets.export' | 'secrets.view' | 'secrets.set' | 'secrets.delete' | 'secrets.rename' | 'cloud.dispatch' | 'cloud.complete' | 'cloud.cancel' | 'cloud.message' | 'teams.create' | 'teams.add' | 'teams.start' | 'teams.complete' | 'teams.disband' | 'hook.fire' | 'hook.complete' | 'hook.error' | 'mcp.add' | 'mcp.remove' | 'mcp.register' | 'resource.sync' | 'rotation.resolved' | 'command.start' | 'command.end' | 'perf.timing' | 'session.start' | 'session.end' | 'webhook.received' | 'webhook.authorized' | 'webhook.rejected' | 'webhook.matched' | 'webhook.fired' | 'webhook.handler.start' | 'webhook.handler.end' | 'plan.created' | 'pr.opened' | 'pr.merged' | 'worktree.created' | 'worktree.removed' | 'commit.created' | 'pushed' | 'subagent.spawned' | 'artifact.created' | 'task.completed' | 'checklist.created' | 'status.posted' | 'file.edited' | 'factory.command' | 'factory.action' | 'factory.uri' | 'factory.launch' | 'friction' | 'error' | 'warn' | 'info' | 'debug';
|
|
17
17
|
/** Every known event kind. Derived from {@link EVENT_TYPE_TABLE}, never hand-listed. */
|
|
18
18
|
export declare const EVENT_TYPES: readonly EventType[];
|
|
19
19
|
/** Runtime guard for an event kind arriving from outside this process. */
|
|
@@ -208,6 +208,8 @@ export declare function query(options: {
|
|
|
208
208
|
eventTypes?: EventType[];
|
|
209
209
|
level?: EventLevel;
|
|
210
210
|
agent?: string;
|
|
211
|
+
/** Only events stamped with this session id (payload `sessionId`, the provenance floor). */
|
|
212
|
+
sessionId?: string;
|
|
211
213
|
caller?: string;
|
|
212
214
|
command?: string;
|
|
213
215
|
module?: string;
|
package/dist/lib/events.js
CHANGED
|
@@ -84,6 +84,7 @@ const EVENT_TYPE_TABLE = {
|
|
|
84
84
|
'version.install': true, 'version.switch': true, 'version.remove': true,
|
|
85
85
|
'skill.install': true, 'skill.remove': true,
|
|
86
86
|
'browser.launch': true, 'browser.close': true, 'browser.navigate': true, 'browser.screenshot': true,
|
|
87
|
+
'computer.action': true,
|
|
87
88
|
'secrets.get': true, 'secrets.unlocked': true, 'secrets.create': true, 'secrets.import': true, 'secrets.export': true, 'secrets.view': true, 'secrets.set': true, 'secrets.delete': true, 'secrets.rename': true,
|
|
88
89
|
'cloud.dispatch': true, 'cloud.complete': true, 'cloud.cancel': true, 'cloud.message': true,
|
|
89
90
|
'teams.create': true, 'teams.add': true, 'teams.start': true, 'teams.complete': true, 'teams.disband': true,
|
|
@@ -327,7 +328,6 @@ export function detectCaller(env = process.env, stdoutIsTTY = Boolean(process.st
|
|
|
327
328
|
}
|
|
328
329
|
return { kind: stdoutIsTTY ? 'terminal' : 'script' };
|
|
329
330
|
}
|
|
330
|
-
// ─── Audit attribution ────────────────────────────────────────────────────────
|
|
331
331
|
// ─── Core API ─────────────────────────────────────────────────────────────────
|
|
332
332
|
/**
|
|
333
333
|
* Emit a structured event to the append-only audit log.
|
|
@@ -729,7 +729,7 @@ export function maybeRotate() {
|
|
|
729
729
|
* @returns Array of event records
|
|
730
730
|
*/
|
|
731
731
|
export function query(options) {
|
|
732
|
-
const { startDate, endDate = new Date(), eventTypes, level, agent, caller, command, module, limit } = options;
|
|
732
|
+
const { startDate, endDate = new Date(), eventTypes, level, agent, sessionId, caller, command, module, limit } = options;
|
|
733
733
|
const results = [];
|
|
734
734
|
if (!fs.existsSync(eventsDir()))
|
|
735
735
|
return results;
|
|
@@ -774,6 +774,8 @@ export function query(options) {
|
|
|
774
774
|
continue;
|
|
775
775
|
if (agent && record.agent !== agent)
|
|
776
776
|
continue;
|
|
777
|
+
if (sessionId && record.sessionId !== sessionId)
|
|
778
|
+
continue;
|
|
777
779
|
if (caller && record.caller !== caller)
|
|
778
780
|
continue;
|
|
779
781
|
if (command && record.command !== command &&
|
package/dist/lib/exec.js
CHANGED
|
@@ -13,6 +13,7 @@ import { AGENTS } from './agents.js';
|
|
|
13
13
|
import { parseTimeout } from './routines.js';
|
|
14
14
|
import { getBinaryPath, getVersionHomePath, isVersionInstalled, resolveVersion } from './versions.js';
|
|
15
15
|
import { resolveModel, buildReasoningFlags } from './models.js';
|
|
16
|
+
import { isTierToken, resolveTier } from './model-tiers.js';
|
|
16
17
|
import { maybeRotate, createTimer, redactPrompt, redactArgs } from './events.js';
|
|
17
18
|
import { sanitizeProcessEnv } from './secrets/bundles.js';
|
|
18
19
|
import { resolveActor, actorEnv } from './actor.js';
|
|
@@ -681,10 +682,24 @@ export function buildExecCommand(options) {
|
|
|
681
682
|
cmd[0] = realBinary && fs.existsSync(realBinary) ? realBinary : versionedName;
|
|
682
683
|
}
|
|
683
684
|
}
|
|
685
|
+
// Resolve the model up front so the reasoning-flag block can honor a cost tier
|
|
686
|
+
// that maps to reasoning effort on a single-model harness (e.g. Grok, where the
|
|
687
|
+
// tier IS the effort dial). `modelVersion` is null when no version resolves;
|
|
688
|
+
// `tierModel` is the concrete model a tier resolved to (null => drop the flag).
|
|
689
|
+
const effectiveModel = options.model
|
|
690
|
+
?? (options.agent === 'codex' ? readCodexConfiguredModel() : undefined);
|
|
691
|
+
const modelVersion = effectiveModel && template.modelFlag
|
|
692
|
+
? (options.version || resolveVersion(options.agent, options.cwd || process.cwd()))
|
|
693
|
+
: null;
|
|
694
|
+
const tierResolved = effectiveModel && modelVersion && isTierToken(effectiveModel)
|
|
695
|
+
? resolveTier(options.agent, modelVersion, effectiveModel)
|
|
696
|
+
: null;
|
|
697
|
+
// An explicit --effort wins; otherwise a single-model tier's effort applies.
|
|
698
|
+
const effortLevel = options.effort !== 'auto' ? options.effort : (tierResolved?.effort ?? options.effort);
|
|
684
699
|
// Add reasoning effort flags (before mode flags for codex -c positioning)
|
|
685
700
|
// For codex, -c must come before 'exec' subcommand, so we insert at position 1
|
|
686
|
-
if (
|
|
687
|
-
const reasoningFlags = buildReasoningFlags(options.agent,
|
|
701
|
+
if (effortLevel !== 'auto') {
|
|
702
|
+
const reasoningFlags = buildReasoningFlags(options.agent, effortLevel);
|
|
688
703
|
if (reasoningFlags.length > 0) {
|
|
689
704
|
if (options.agent === 'codex') {
|
|
690
705
|
// Insert after 'codex' (or 'codex@version') but before 'exec'
|
|
@@ -803,20 +818,36 @@ export function buildExecCommand(options) {
|
|
|
803
818
|
// carry that setting, so without this it silently defaults to gpt-5.3-codex,
|
|
804
819
|
// which a ChatGPT-tier account can't use (HTTP 400). Forwarding keeps the
|
|
805
820
|
// user's default model setup for both `agents run` and `agents teams`.
|
|
806
|
-
const effectiveModel = options.model
|
|
807
|
-
?? (options.agent === 'codex' ? readCodexConfiguredModel() : undefined);
|
|
808
821
|
if (effectiveModel && template.modelFlag) {
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
822
|
+
if (tierResolved) {
|
|
823
|
+
// Cost tier (cheap|default|best|ultra) -> a concrete model this harness+
|
|
824
|
+
// version actually ships. Covers `agents run` and `agents teams` (both
|
|
825
|
+
// funnel here). A null model means nothing resolved -> drop the flag and
|
|
826
|
+
// let the harness pick its default.
|
|
827
|
+
if (tierResolved.model) {
|
|
828
|
+
cmd.push(template.modelFlag, tierResolved.model);
|
|
829
|
+
if (tierResolved.note)
|
|
830
|
+
process.stderr.write(`[agents] --model ${effectiveModel} -> ${tierResolved.model} (${tierResolved.note})\n`);
|
|
831
|
+
}
|
|
832
|
+
else {
|
|
833
|
+
process.stderr.write(`[agents] no model for tier "${effectiveModel}" on ${options.agent}@${modelVersion}; using harness default\n`);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
else if (modelVersion) {
|
|
837
|
+
const resolved = resolveModel(options.agent, modelVersion, effectiveModel);
|
|
812
838
|
if (resolved.warning) {
|
|
813
839
|
process.stderr.write(`[agents] ${resolved.warning}\n`);
|
|
814
840
|
}
|
|
815
841
|
cmd.push(template.modelFlag, resolved.forwarded);
|
|
816
842
|
}
|
|
817
|
-
else {
|
|
843
|
+
else if (!isTierToken(effectiveModel)) {
|
|
818
844
|
cmd.push(template.modelFlag, effectiveModel);
|
|
819
845
|
}
|
|
846
|
+
else {
|
|
847
|
+
// Tier token but no version resolved -> forwarding the literal "best"/etc.
|
|
848
|
+
// would be rejected by the CLI, so drop the flag (harness default).
|
|
849
|
+
process.stderr.write(`[agents] cannot resolve tier "${effectiveModel}" without a version; using harness default\n`);
|
|
850
|
+
}
|
|
820
851
|
}
|
|
821
852
|
// Add JSON output flags if requested
|
|
822
853
|
if (options.json && template.jsonFlags) {
|
package/dist/lib/git.d.ts
CHANGED
|
@@ -103,6 +103,20 @@ export interface RepoStateSnapshot {
|
|
|
103
103
|
* result is stable across git versions and never mutates the tree. Returns null
|
|
104
104
|
* when the path is not a git worktree. */
|
|
105
105
|
export declare function readRepoState(repoPath: string): RepoStateSnapshot | null;
|
|
106
|
+
/**
|
|
107
|
+
* The short HEAD sha of the git repo at `repoRoot` (`git -C <repoRoot>
|
|
108
|
+
* rev-parse --short HEAD`), for provenance — "which commit of this DotAgents
|
|
109
|
+
* repo was this resource/plugin resolved from". `undefined` when `repoRoot`
|
|
110
|
+
* isn't a git repo (or has no commits yet), never a throw.
|
|
111
|
+
*
|
|
112
|
+
* Deliberately synchronous + resolved once and cached: callers (resources.ts,
|
|
113
|
+
* plugins.ts) attach this as a lazy getter on the resolved object, so a
|
|
114
|
+
* consumer that never inspects provenance never pays for the git shell-out —
|
|
115
|
+
* see {@link ResolvedResource.snapshotSha} / {@link DiscoveredPlugin.snapshotSha}.
|
|
116
|
+
*/
|
|
117
|
+
export declare function resolveSnapshotSha(repoRoot: string): string | undefined;
|
|
118
|
+
/** Test seam: clear the memoized snapshot-sha cache between test cases. */
|
|
119
|
+
export declare function _resetSnapshotShaCacheForTest(): void;
|
|
106
120
|
/**
|
|
107
121
|
* Get the current GitHub username using gh CLI.
|
|
108
122
|
* Returns null if gh is not installed or user is not authenticated.
|
package/dist/lib/git.js
CHANGED
|
@@ -371,6 +371,42 @@ export function readRepoState(repoPath) {
|
|
|
371
371
|
const dirty = porcelain != null && porcelain.length > 0;
|
|
372
372
|
return { branch, head, dirty };
|
|
373
373
|
}
|
|
374
|
+
/** Memoized per repoRoot — a resolveResource()/listResources()/plugin-discovery
|
|
375
|
+
* call that touches many resources from the SAME DotAgents repo must not shell
|
|
376
|
+
* out to git once per resource. */
|
|
377
|
+
const _snapshotShaCache = new Map();
|
|
378
|
+
/**
|
|
379
|
+
* The short HEAD sha of the git repo at `repoRoot` (`git -C <repoRoot>
|
|
380
|
+
* rev-parse --short HEAD`), for provenance — "which commit of this DotAgents
|
|
381
|
+
* repo was this resource/plugin resolved from". `undefined` when `repoRoot`
|
|
382
|
+
* isn't a git repo (or has no commits yet), never a throw.
|
|
383
|
+
*
|
|
384
|
+
* Deliberately synchronous + resolved once and cached: callers (resources.ts,
|
|
385
|
+
* plugins.ts) attach this as a lazy getter on the resolved object, so a
|
|
386
|
+
* consumer that never inspects provenance never pays for the git shell-out —
|
|
387
|
+
* see {@link ResolvedResource.snapshotSha} / {@link DiscoveredPlugin.snapshotSha}.
|
|
388
|
+
*/
|
|
389
|
+
export function resolveSnapshotSha(repoRoot) {
|
|
390
|
+
const cached = _snapshotShaCache.get(repoRoot);
|
|
391
|
+
if (cached !== undefined || _snapshotShaCache.has(repoRoot))
|
|
392
|
+
return cached;
|
|
393
|
+
let sha;
|
|
394
|
+
try {
|
|
395
|
+
const raw = execFileSync('git', ['-C', repoRoot, 'rev-parse', '--short', 'HEAD'], {
|
|
396
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
397
|
+
}).toString().trim();
|
|
398
|
+
sha = raw || undefined;
|
|
399
|
+
}
|
|
400
|
+
catch {
|
|
401
|
+
sha = undefined;
|
|
402
|
+
}
|
|
403
|
+
_snapshotShaCache.set(repoRoot, sha);
|
|
404
|
+
return sha;
|
|
405
|
+
}
|
|
406
|
+
/** Test seam: clear the memoized snapshot-sha cache between test cases. */
|
|
407
|
+
export function _resetSnapshotShaCacheForTest() {
|
|
408
|
+
_snapshotShaCache.clear();
|
|
409
|
+
}
|
|
374
410
|
/**
|
|
375
411
|
* Get the current GitHub username using gh CLI.
|
|
376
412
|
* Returns null if gh is not installed or user is not authenticated.
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import * as fs from 'fs';
|
|
13
13
|
import * as path from 'path';
|
|
14
14
|
import { getLogsDir } from '../state.js';
|
|
15
|
+
import { percentile } from '../percentile.js';
|
|
15
16
|
/**
|
|
16
17
|
* Load every `hook.fire` event from the last `days` daily log files.
|
|
17
18
|
* Lines that aren't JSON or aren't `hook.fire` events are silently skipped —
|
|
@@ -53,20 +54,6 @@ export function loadHookFireEvents(days = 7, logsDir = getLogsDir()) {
|
|
|
53
54
|
}
|
|
54
55
|
return events;
|
|
55
56
|
}
|
|
56
|
-
/** Percentile of a sorted-ascending array. p in [0,100]. Linear interpolation. */
|
|
57
|
-
function percentile(sorted, p) {
|
|
58
|
-
if (sorted.length === 0)
|
|
59
|
-
return 0;
|
|
60
|
-
if (sorted.length === 1)
|
|
61
|
-
return sorted[0];
|
|
62
|
-
const rank = (p / 100) * (sorted.length - 1);
|
|
63
|
-
const lo = Math.floor(rank);
|
|
64
|
-
const hi = Math.ceil(rank);
|
|
65
|
-
if (lo === hi)
|
|
66
|
-
return sorted[lo];
|
|
67
|
-
const frac = rank - lo;
|
|
68
|
-
return sorted[lo] * (1 - frac) + sorted[hi] * frac;
|
|
69
|
-
}
|
|
70
57
|
/** Aggregate fire events into a per-hook profile, sorted by p99 desc. */
|
|
71
58
|
export function aggregateHookProfile(events) {
|
|
72
59
|
const byHook = new Map();
|
|
@@ -52,6 +52,18 @@ export declare function remoteCdPrefix(remoteCwd?: string, opts?: {
|
|
|
52
52
|
* collision, mirroring `buildExecEnv`'s `...options.env` precedence (exec.ts).
|
|
53
53
|
*/
|
|
54
54
|
export declare function withActorEnv(env?: Record<string, string>): Record<string, string>;
|
|
55
|
+
/**
|
|
56
|
+
* The shell-export prelude prepended to EVERY remote `agents run` dispatch —
|
|
57
|
+
* actor provenance plus, for a `run auto` dispatch, the chain-hop guard
|
|
58
|
+
* (RUN_AUTO_HOST_RESOLVED_ENV): this dispatch already IS the affinity pick, so
|
|
59
|
+
* the remote CLI must not re-run host affinity and hop to a third host. The
|
|
60
|
+
* guard MUST be a shell export (landing in the remote CLI's own process.env,
|
|
61
|
+
* which `runAutoDefaultsToAffinity` reads) — a forwarded `--env` flag would
|
|
62
|
+
* only reach the spawned agent's env and the remote `run auto` would re-pick.
|
|
63
|
+
* Shared by the interactive (runInteractiveOnHost) and detached
|
|
64
|
+
* (launchDetached) paths so both behave identically.
|
|
65
|
+
*/
|
|
66
|
+
export declare function remoteRunShellPrelude(agent: string): string;
|
|
55
67
|
/**
|
|
56
68
|
* Launch a detached login-shell command in its own Unix session/process group.
|
|
57
69
|
*
|
|
@@ -20,6 +20,7 @@ import { followHostTask } from './progress.js';
|
|
|
20
20
|
import { wrapHostCommandWithCredentials } from './credentials.js';
|
|
21
21
|
import { hostKeyCheckingOpts } from '../devices/known-hosts.js';
|
|
22
22
|
import { toRemotePortable } from '../project-root.js';
|
|
23
|
+
import { RUN_AUTO_KEYWORD, RUN_AUTO_HOST_RESOLVED_ENV } from '../types.js';
|
|
23
24
|
// Use $HOME (not ~) so the path is correct whether or not it's quoted and
|
|
24
25
|
// regardless of the run's cwd. Task ids are 8 hex chars, so these paths are
|
|
25
26
|
// injection-safe to interpolate unquoted into remote commands.
|
|
@@ -95,6 +96,22 @@ export function remoteCdPrefix(remoteCwd, opts = {}) {
|
|
|
95
96
|
export function withActorEnv(env) {
|
|
96
97
|
return { ...actorEnv(resolveActor()), ...terminalIdEnv(), ...(env ?? {}) };
|
|
97
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* The shell-export prelude prepended to EVERY remote `agents run` dispatch —
|
|
101
|
+
* actor provenance plus, for a `run auto` dispatch, the chain-hop guard
|
|
102
|
+
* (RUN_AUTO_HOST_RESOLVED_ENV): this dispatch already IS the affinity pick, so
|
|
103
|
+
* the remote CLI must not re-run host affinity and hop to a third host. The
|
|
104
|
+
* guard MUST be a shell export (landing in the remote CLI's own process.env,
|
|
105
|
+
* which `runAutoDefaultsToAffinity` reads) — a forwarded `--env` flag would
|
|
106
|
+
* only reach the spawned agent's env and the remote `run auto` would re-pick.
|
|
107
|
+
* Shared by the interactive (runInteractiveOnHost) and detached
|
|
108
|
+
* (launchDetached) paths so both behave identically.
|
|
109
|
+
*/
|
|
110
|
+
export function remoteRunShellPrelude(agent) {
|
|
111
|
+
const guard = agent === RUN_AUTO_KEYWORD ? { [RUN_AUTO_HOST_RESOLVED_ENV]: '1' } : {};
|
|
112
|
+
const exports = posixEnvExports(withActorEnv(guard));
|
|
113
|
+
return exports ? `${exports}; ` : '';
|
|
114
|
+
}
|
|
98
115
|
/**
|
|
99
116
|
* Forward the launching editor tab's `AGENT_TERMINAL_ID` across the SSH hop.
|
|
100
117
|
*
|
|
@@ -233,11 +250,11 @@ async function launchDetached(host, target, opts) {
|
|
|
233
250
|
const remoteExit = `${REMOTE_DIR}/${id}.exit`;
|
|
234
251
|
// Inner command run under a login shell so PATH resolves `agents`. Export the
|
|
235
252
|
// resolved actor provenance first so the detached remote run inherits it
|
|
236
|
-
// instead of re-resolving from this box's SSH_CONNECTION (RUSH-2028)
|
|
253
|
+
// instead of re-resolving from this box's SSH_CONNECTION (RUSH-2028); a
|
|
254
|
+
// `run auto` dispatch also gets the chain-hop guard (remoteRunShellPrelude).
|
|
237
255
|
const invocation = ['agents', ...opts.forwardedArgs].map(shellQuote).join(' ');
|
|
238
256
|
const cwd = remoteCdPrefix(opts.remoteCwd, { mirror: opts.mirrorCwd });
|
|
239
|
-
const
|
|
240
|
-
const prelude = actorExports ? `${actorExports}; ` : '';
|
|
257
|
+
const prelude = remoteRunShellPrelude(opts.agentLabel);
|
|
241
258
|
let inner = `${prelude}${cwd}${invocation} > ${remoteLog} 2>&1; echo $? > ${remoteExit}`;
|
|
242
259
|
if (opts.copyCreds) {
|
|
243
260
|
inner = wrapHostCommandWithCredentials(inner, opts.copyCreds);
|
|
@@ -432,9 +449,9 @@ export async function runInteractiveOnHost(host, opts) {
|
|
|
432
449
|
const invocation = ['agents', ...buildInteractiveRunForwardedArgs(opts)].map(shellQuote).join(' ');
|
|
433
450
|
const cwd = remoteCdPrefix(opts.remoteCwd, { mirror: opts.mirrorCwd });
|
|
434
451
|
// Forward actor provenance so the interactive remote run inherits it rather
|
|
435
|
-
// than re-resolving from this box's SSH_CONNECTION (RUSH-2028)
|
|
436
|
-
|
|
437
|
-
const prelude =
|
|
452
|
+
// than re-resolving from this box's SSH_CONNECTION (RUSH-2028); a `run auto`
|
|
453
|
+
// dispatch also gets the chain-hop guard (remoteRunShellPrelude).
|
|
454
|
+
const prelude = remoteRunShellPrelude(opts.agent);
|
|
438
455
|
let remoteCmd = `${prelude}${cwd}${invocation}`;
|
|
439
456
|
if (opts.copyCreds) {
|
|
440
457
|
remoteCmd = wrapHostCommandWithCredentials(remoteCmd, opts.copyCreds);
|
|
@@ -2,6 +2,10 @@ import { type Host } from './types.js';
|
|
|
2
2
|
/** ssh's connection-layer failure code — the signal that the link dropped rather
|
|
3
3
|
* than the remote command exiting on its own. Mirrors ssh-exec.ts `sshStream`. */
|
|
4
4
|
export declare const SSH_CONN_FAILURE = 255;
|
|
5
|
+
/** What a would-be-255 remote-origin exit code is remapped to by
|
|
6
|
+
* {@link wrapRemoteExitCode} — see the file header. Never produced by the ssh
|
|
7
|
+
* transport itself, so it can never be confused with {@link SSH_CONN_FAILURE}. */
|
|
8
|
+
export declare const REMOTE_EXIT_255_REMAPPED = 254;
|
|
5
9
|
/** Consecutive failed-to-connect reattaches before giving up. Backoff is capped at
|
|
6
10
|
* {@link MAX_BACKOFF_MS}. A reattach that actually reconnected (then dropped again)
|
|
7
11
|
* refills the budget, so a long session that blinks all day reconnects every time
|
|
@@ -46,6 +50,40 @@ export declare function reconnectStep(state: ReconnectState, outcome: ReconnectO
|
|
|
46
50
|
export declare function reconnectNotice(sessionId: string, host: string, attempt: number, waitMs: number): string;
|
|
47
51
|
/** Notice shown once the retry budget is spent. */
|
|
48
52
|
export declare function exhaustedNotice(sessionId: string, host: string): string;
|
|
53
|
+
/** Notice shown when a reattach stops on a remapped remote-side exit
|
|
54
|
+
* ({@link REMOTE_EXIT_255_REMAPPED} — a would-be-255 the remote command decided
|
|
55
|
+
* on for its own reasons, not the ssh transport dropping; see
|
|
56
|
+
* {@link wrapRemoteExitCode}). Distinct from {@link exhaustedNotice}, which is
|
|
57
|
+
* only for a genuinely spent retry budget. */
|
|
58
|
+
export declare function remoteExitNotice(sessionId: string, host: string): string;
|
|
59
|
+
/**
|
|
60
|
+
* Wrap `cmd` in `bash -lc` (the login-shell pattern `buildRemoteAgentsInvocation`
|
|
61
|
+
* in remote-cmd.ts uses for its own POSIX callers — see its doc comment for why
|
|
62
|
+
* a login shell at all; the sibling interactive dispatch in dispatch.ts sends a
|
|
63
|
+
* bare `agents …` with no shell wrapper, so this is a NEW login-shell hop on the
|
|
64
|
+
* reattach path specifically, not something already universal here) with a
|
|
65
|
+
* trailing exit-code remap: whatever `cmd` itself exits with, a 255 becomes
|
|
66
|
+
* {@link REMOTE_EXIT_255_REMAPPED} before the wrapper exits — see the file
|
|
67
|
+
* header for why. Every other code (0, 1, …) passes through unchanged. This
|
|
68
|
+
* carries no PATH bootstrap of its own — `ensureHostReady`/`readyProbe` already
|
|
69
|
+
* gates every `--host` dispatch on `bash -lc 'agents --version'` succeeding
|
|
70
|
+
* before a run is attempted at all, so the peer's login shell resolving `agents`
|
|
71
|
+
* is an established precondition here too. Pure string-building, so it is
|
|
72
|
+
* unit-tested without SSH (and, since the constructed script is ordinary POSIX,
|
|
73
|
+
* also exercised by actually running it through a real shell in the test — no
|
|
74
|
+
* mock needed).
|
|
75
|
+
*/
|
|
76
|
+
export declare function wrapRemoteExitCode(cmd: string): string;
|
|
77
|
+
/**
|
|
78
|
+
* The remote command a reattach runs — the peer's own reconnect verb
|
|
79
|
+
* (`agents sessions focus <id> --local --attach-only`), wrapped by
|
|
80
|
+
* {@link wrapRemoteExitCode} so a stray remote-origin 255 (from this command,
|
|
81
|
+
* whatever produces it — see the file header) can never masquerade as a
|
|
82
|
+
* network drop. Split out from {@link reattachRemoteSession} so it is
|
|
83
|
+
* unit-tested without SSH — mirrors `remoteAgentsJsonCommand` in
|
|
84
|
+
* lib/remote-agents-json.ts.
|
|
85
|
+
*/
|
|
86
|
+
export declare function reattachRemoteCommand(sessionId: string): string;
|
|
49
87
|
/**
|
|
50
88
|
* Re-attach the live remote tmux pane for `sessionId` by driving the peer's own
|
|
51
89
|
* `agents sessions focus`. A fast, un-multiplexed preflight probe (`ssh … true`)
|