@phnx-labs/agents-cli 1.22.26 → 1.22.28
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 +293 -0
- package/README.md +80 -27
- package/dist/bin/agents +0 -0
- package/dist/commands/bench.d.ts +2 -0
- package/dist/commands/bench.js +101 -0
- package/dist/commands/doctor.js +24 -10
- package/dist/commands/exec.js +71 -26
- package/dist/commands/hosts.js +4 -3
- package/dist/commands/insights.js +39 -5
- package/dist/commands/output.js +100 -25
- package/dist/commands/perf.d.ts +10 -0
- package/dist/commands/perf.js +14 -6
- package/dist/commands/projects.js +24 -10
- package/dist/commands/resume.d.ts +11 -0
- package/dist/commands/resume.js +51 -0
- package/dist/commands/routines.js +31 -4
- package/dist/commands/run-account-picker.d.ts +37 -0
- package/dist/commands/run-account-picker.js +101 -17
- package/dist/commands/secrets.js +86 -2
- package/dist/commands/sessions-picker.js +12 -7
- package/dist/commands/sessions.d.ts +73 -7
- package/dist/commands/sessions.js +391 -49
- package/dist/commands/ssh.js +184 -11
- package/dist/commands/teams.js +22 -11
- package/dist/index.js +16 -20
- package/dist/lib/activity.js +45 -99
- package/dist/lib/auth-health.d.ts +47 -3
- package/dist/lib/auth-health.js +74 -15
- package/dist/lib/bench/index.d.ts +4 -0
- package/dist/lib/bench/index.js +4 -0
- package/dist/lib/bench/runner.d.ts +16 -0
- package/dist/lib/bench/runner.js +111 -0
- package/dist/lib/bench/schema.d.ts +5 -0
- package/dist/lib/bench/schema.js +91 -0
- package/dist/lib/bench/storage.d.ts +5 -0
- package/dist/lib/bench/storage.js +32 -0
- package/dist/lib/bench/types.d.ts +40 -0
- package/dist/lib/bench/types.js +1 -0
- package/dist/lib/claude-account-token.d.ts +2 -0
- package/dist/lib/claude-account-token.js +56 -3
- package/dist/lib/cloud/host.js +1 -0
- package/dist/lib/crabbox/cli.d.ts +2 -0
- package/dist/lib/crabbox/cli.js +2 -0
- package/dist/lib/crabbox/lease.js +7 -1
- package/dist/lib/daemon.js +20 -0
- package/dist/lib/devices/connect.d.ts +2 -0
- package/dist/lib/devices/connect.js +7 -0
- package/dist/lib/devices/doctor-findings.d.ts +4 -1
- package/dist/lib/devices/doctor-findings.js +15 -1
- package/dist/lib/devices/harness-inventory.d.ts +97 -0
- package/dist/lib/devices/harness-inventory.js +0 -0
- package/dist/lib/devices/registry.d.ts +2 -0
- package/dist/lib/devices/resolve-target.d.ts +1 -0
- package/dist/lib/devices/resolve-target.js +9 -2
- package/dist/lib/devices/ssh-config.js +3 -0
- package/dist/lib/devices/windows-ssh-enrollment.d.ts +20 -0
- package/dist/lib/devices/windows-ssh-enrollment.js +98 -0
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/events.js +2 -2
- package/dist/lib/exec.js +18 -8
- package/dist/lib/fleet/apply.js +9 -7
- package/dist/lib/fleet/remote-login.d.ts +4 -3
- package/dist/lib/fleet/remote-login.js +11 -9
- package/dist/lib/gemini-settings.d.ts +0 -1
- package/dist/lib/gemini-settings.js +12 -7
- package/dist/lib/git.d.ts +9 -3
- package/dist/lib/git.js +39 -22
- package/dist/lib/hooks/profile.d.ts +12 -1
- package/dist/lib/hooks/profile.js +6 -1
- package/dist/lib/hooks.d.ts +8 -5
- package/dist/lib/hooks.js +14 -7
- package/dist/lib/hosts/dispatch.d.ts +10 -0
- package/dist/lib/hosts/dispatch.js +85 -25
- package/dist/lib/hosts/logs.js +11 -1
- package/dist/lib/hosts/passthrough.d.ts +27 -0
- package/dist/lib/hosts/passthrough.js +19 -10
- package/dist/lib/hosts/progress.d.ts +12 -1
- package/dist/lib/hosts/progress.js +41 -8
- package/dist/lib/hosts/providers/devices.js +1 -0
- package/dist/lib/hosts/ready.d.ts +45 -4
- package/dist/lib/hosts/ready.js +124 -20
- package/dist/lib/hosts/reconcile.d.ts +2 -1
- package/dist/lib/hosts/reconcile.js +24 -7
- package/dist/lib/hosts/reconnect.d.ts +45 -12
- package/dist/lib/hosts/reconnect.js +94 -36
- package/dist/lib/hosts/registry.d.ts +2 -2
- package/dist/lib/hosts/registry.js +4 -5
- package/dist/lib/hosts/remote-cmd.d.ts +17 -0
- package/dist/lib/hosts/remote-cmd.js +29 -0
- package/dist/lib/hosts/tasks.d.ts +3 -0
- package/dist/lib/hosts/types.d.ts +1 -0
- package/dist/lib/hosts/types.js +3 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +9 -6
- package/dist/lib/menubar/install-menubar.js +20 -9
- package/dist/lib/menubar/snapshot.js +15 -2
- package/dist/lib/models.d.ts +42 -6
- package/dist/lib/models.js +55 -153
- package/dist/lib/perf/db.js +16 -3
- package/dist/lib/perf/types.d.ts +12 -1
- package/dist/lib/pricing/cost.d.ts +9 -0
- package/dist/lib/pricing/cost.js +24 -0
- package/dist/lib/pricing/index.d.ts +1 -1
- package/dist/lib/pricing/index.js +1 -1
- package/dist/lib/project-probe.d.ts +22 -3
- package/dist/lib/project-probe.js +105 -17
- package/dist/lib/project-status.d.ts +9 -0
- package/dist/lib/project-status.js +15 -0
- package/dist/lib/redact.js +8 -3
- package/dist/lib/refresh.js +22 -11
- package/dist/lib/remote-agents-json.d.ts +32 -0
- package/dist/lib/remote-agents-json.js +47 -16
- package/dist/lib/resource-profiles.js +1 -2
- package/dist/lib/rotate.d.ts +22 -0
- package/dist/lib/rotate.js +26 -0
- package/dist/lib/routine-notify-owner.d.ts +102 -0
- package/dist/lib/routine-notify-owner.js +232 -0
- package/dist/lib/routines.d.ts +10 -0
- package/dist/lib/runner.d.ts +9 -1
- package/dist/lib/runner.js +182 -17
- package/dist/lib/sandbox.d.ts +0 -2
- package/dist/lib/sandbox.js +2 -19
- 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/secrets/agent.d.ts +19 -3
- package/dist/lib/secrets/agent.js +61 -22
- package/dist/lib/secrets/audit.d.ts +1 -1
- package/dist/lib/secrets/audit.js +2 -0
- package/dist/lib/secrets/bundles.d.ts +3 -3
- package/dist/lib/secrets/bundles.js +15 -5
- package/dist/lib/secrets/filestore.d.ts +2 -0
- package/dist/lib/secrets/filestore.js +4 -0
- package/dist/lib/secrets/lease.d.ts +25 -0
- package/dist/lib/secrets/lease.js +44 -0
- package/dist/lib/secrets/session-store.d.ts +7 -0
- package/dist/lib/secrets/session-store.js +21 -0
- package/dist/lib/self-update.d.ts +20 -5
- package/dist/lib/self-update.js +93 -16
- package/dist/lib/session/active.d.ts +49 -0
- package/dist/lib/session/active.js +175 -11
- package/dist/lib/session/bash-command.d.ts +16 -0
- package/dist/lib/session/bash-command.js +65 -0
- package/dist/lib/session/db.d.ts +23 -2
- package/dist/lib/session/db.js +86 -20
- package/dist/lib/session/discover.d.ts +21 -1
- package/dist/lib/session/discover.js +124 -32
- package/dist/lib/session/insights.d.ts +19 -0
- package/dist/lib/session/insights.js +78 -10
- package/dist/lib/session/recovery.d.ts +17 -1
- package/dist/lib/session/recovery.js +111 -4
- package/dist/lib/session/remote-bundle.js +1 -1
- package/dist/lib/session/remote-list.d.ts +10 -9
- package/dist/lib/session/remote-list.js +25 -23
- package/dist/lib/session/resume-owner.d.ts +55 -0
- package/dist/lib/session/resume-owner.js +69 -0
- package/dist/lib/session/team-filter.d.ts +65 -0
- package/dist/lib/session/team-filter.js +98 -3
- package/dist/lib/session/types.d.ts +33 -0
- package/dist/lib/smart-launch.d.ts +31 -5
- package/dist/lib/smart-launch.js +43 -7
- package/dist/lib/ssh-exec.d.ts +27 -0
- package/dist/lib/ssh-exec.js +34 -1
- package/dist/lib/ssh-tunnel.d.ts +3 -2
- package/dist/lib/ssh-tunnel.js +25 -16
- package/dist/lib/staleness/detectors/skills.d.ts +2 -0
- package/dist/lib/staleness/detectors/skills.js +17 -2
- package/dist/lib/staleness/index.d.ts +10 -1
- package/dist/lib/staleness/index.js +28 -3
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/teams/agents.d.ts +1 -0
- package/dist/lib/teams/agents.js +38 -11
- package/dist/lib/teams/placement-probe.d.ts +1 -1
- package/dist/lib/teams/placement-probe.js +24 -24
- package/dist/lib/teams/remoteWorktree.d.ts +11 -7
- package/dist/lib/teams/remoteWorktree.js +29 -27
- package/dist/lib/tmux/session.d.ts +8 -0
- package/dist/lib/tmux/session.js +22 -0
- package/dist/lib/types.d.ts +2 -0
- package/dist/lib/versions.d.ts +1 -0
- package/dist/lib/versions.js +40 -22
- package/package.json +3 -1
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a session may be resumed — the one place that answers "does this
|
|
3
|
+
* transcript belong to another machine?" and, when it does, runs the resume
|
|
4
|
+
* THERE instead of here.
|
|
5
|
+
*
|
|
6
|
+
* A session's agent state lives on the box that produced it: the harness keeps
|
|
7
|
+
* its own conversation store under that machine's home, and `machine` on a
|
|
8
|
+
* SessionMeta records where the transcript originated (see session/types.ts).
|
|
9
|
+
* A row can reach this box two ways and BOTH are remote-owned:
|
|
10
|
+
*
|
|
11
|
+
* - a **synced mirror** — locally readable at `backups/<agent>/<machine>/…`,
|
|
12
|
+
* which is exactly why the old code could not tell: the transcript file is
|
|
13
|
+
* right there, so nothing failed until the harness was asked to resume a
|
|
14
|
+
* conversation it has never seen;
|
|
15
|
+
* - a **live fan-out row** (`_remote`), whose `filePath` is on the peer's disk.
|
|
16
|
+
*
|
|
17
|
+
* Resuming either one locally starts the harness against state it does not
|
|
18
|
+
* have. Before RUSH-2022 that happened silently — `sessions-resume.ts` even fell
|
|
19
|
+
* back to `process.cwd()` when the recorded cwd did not exist locally, so a
|
|
20
|
+
* remote session resumed in whatever directory the user happened to be in.
|
|
21
|
+
*
|
|
22
|
+
* Callers act on the answer differently, and the difference is deliberate:
|
|
23
|
+
* `agents resume` and `agents sessions attach` HOP to the owner over SSH, while
|
|
24
|
+
* `resumeSessionInPlace` — the local takeover every routed caller reaches only
|
|
25
|
+
* after deciding — REFUSES. The batch `sessions resume` needs no check of its
|
|
26
|
+
* own: each of its tabs runs the canonical `agents resume <id>`
|
|
27
|
+
* (lib/session/resume-command.ts), which routes itself.
|
|
28
|
+
*/
|
|
29
|
+
import { isSelfHost } from '../devices/self-host.js';
|
|
30
|
+
/**
|
|
31
|
+
* Set on the SSH hop that sends a resume to its owning device: the far side must
|
|
32
|
+
* run it, never route again.
|
|
33
|
+
*
|
|
34
|
+
* An env var rather than a flag, deliberately. The fleet is mixed-version — a
|
|
35
|
+
* peer on the released CLI would die on an unknown `--here` with
|
|
36
|
+
* `error: unknown option '--here'`, breaking the very hop this feature adds. An
|
|
37
|
+
* unrecognized exported variable is inert on every version, so routing works
|
|
38
|
+
* against old and new peers alike. The far side deletes it after reading
|
|
39
|
+
* ({@link consumeResumePinned}) so it cannot leak into the agent's own children.
|
|
40
|
+
*/
|
|
41
|
+
export const RESUME_PINNED_ENV = 'AGENTS_RESUME_PINNED';
|
|
42
|
+
/**
|
|
43
|
+
* Whether this process was handed a resume by its owner-routing hop — read once,
|
|
44
|
+
* then cleared so a nested `agents resume` inside the running agent still routes
|
|
45
|
+
* normally.
|
|
46
|
+
*/
|
|
47
|
+
export function consumeResumePinned() {
|
|
48
|
+
const pinned = process.env[RESUME_PINNED_ENV] === '1';
|
|
49
|
+
delete process.env[RESUME_PINNED_ENV];
|
|
50
|
+
return pinned;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The peer that owns `session`'s state, or `undefined` when this machine does.
|
|
54
|
+
*
|
|
55
|
+
* Undefined is also the answer for an untagged row (`machine` unset — a session
|
|
56
|
+
* obtained outside `discoverSessions`): there is nothing to route to, so the
|
|
57
|
+
* caller keeps the local behaviour rather than inventing a target.
|
|
58
|
+
*/
|
|
59
|
+
export function sessionOwnerDevice(session) {
|
|
60
|
+
const owner = session.machine?.trim();
|
|
61
|
+
if (!owner)
|
|
62
|
+
return undefined;
|
|
63
|
+
// `isSelfHost` matches every identity this box answers to — `machineId()`
|
|
64
|
+
// itself, the tailnet dnsName and its short form, loopback (devices/
|
|
65
|
+
// self-host.ts `selfAliases`) — so a mirror tagged with this machine's tailnet
|
|
66
|
+
// name is local, not a peer (cf. RUSH-2114). No separate `machineId()`
|
|
67
|
+
// comparison: it is one of those aliases.
|
|
68
|
+
return isSelfHost(owner) ? undefined : owner;
|
|
69
|
+
}
|
|
@@ -51,3 +51,68 @@ export interface FilterResult {
|
|
|
51
51
|
* excluded and counted in `hiddenCount`.
|
|
52
52
|
*/
|
|
53
53
|
export declare function filterTeamSessions(sessions: SessionMeta[], showTeams: boolean): FilterResult;
|
|
54
|
+
/**
|
|
55
|
+
* Whether a team-origin session is a real `agents teams` teammate or a bare SDK
|
|
56
|
+
* spawn with no teammate record. The two share the `sdk-cli` entrypoint that
|
|
57
|
+
* sets `isTeamOrigin`, so they are told apart by whether a `meta.json` teammate
|
|
58
|
+
* record backs the origin ({@link TeamOrigin.source} === `'meta'`).
|
|
59
|
+
*
|
|
60
|
+
* `source` is newer than the rest of {@link TeamOrigin}, so a row that arrived
|
|
61
|
+
* from a pre-`source` peer over the `--host` fan-out carries none. There the
|
|
62
|
+
* meta-only fields settle it: the entrypoint fallback is a bare `{ handle }`, so
|
|
63
|
+
* any `team` / `mode` / `parentSessionId` / `startedAt` means a real teammate
|
|
64
|
+
* record produced it — otherwise a genuine teammate from an unupgraded peer
|
|
65
|
+
* would be misfiled into the no-team bucket, the exact conflation this splits.
|
|
66
|
+
*/
|
|
67
|
+
export declare function teamRowKind(origin: TeamOrigin | undefined): 'teammate' | 'subagent';
|
|
68
|
+
/** A team-origin session with its resolved {@link TeamOrigin} guaranteed present. */
|
|
69
|
+
type TeamSession = SessionMeta & {
|
|
70
|
+
teamOrigin: TeamOrigin;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Key/label for the residual bucket — sessions flagged `isTeamOrigin` (an SDK /
|
|
74
|
+
* `sdk-cli` entrypoint) that carry no teammate `meta.json`. In practice this is
|
|
75
|
+
* headless `agents run` spawns and teammates whose team record aged out; a true
|
|
76
|
+
* `Task` sub-agent's transcript is never indexed, so it cannot land here. Named
|
|
77
|
+
* "(no team)" rather than "(sub-agents)" so the label matches what it holds.
|
|
78
|
+
*/
|
|
79
|
+
export declare const NO_TEAM_GROUP_KEY = "(no team)";
|
|
80
|
+
/** Key/label for teammates whose record carries no team name (`task_name`). */
|
|
81
|
+
export declare const UNNAMED_TEAM_KEY = "(unnamed team)";
|
|
82
|
+
/** One team's sessions, grouped for the `agents sessions --teams` view. */
|
|
83
|
+
export interface TeamSessionGroup {
|
|
84
|
+
/**
|
|
85
|
+
* The grouping key: the team name, {@link UNNAMED_TEAM_KEY} for teammates that
|
|
86
|
+
* carry no `task_name`, or {@link NO_TEAM_GROUP_KEY} for the residual bucket.
|
|
87
|
+
*/
|
|
88
|
+
key: string;
|
|
89
|
+
/** A named team of `agents teams` teammates vs the catch-all no-team bucket. */
|
|
90
|
+
kind: 'team' | 'noTeam';
|
|
91
|
+
/** Team name when this is a named team, else undefined. */
|
|
92
|
+
team?: string;
|
|
93
|
+
/**
|
|
94
|
+
* The orchestrator session id that spawned this team (`parent_session_id`),
|
|
95
|
+
* when every teammate agrees on one. Undefined for the no-team bucket, an
|
|
96
|
+
* unparented team, or a group whose teammates disagree.
|
|
97
|
+
*/
|
|
98
|
+
spawnerSessionId?: string;
|
|
99
|
+
/** The team's rows, newest-active first. */
|
|
100
|
+
sessions: TeamSession[];
|
|
101
|
+
/** Most-recent activity across the group, for ordering groups. */
|
|
102
|
+
maxTs: string;
|
|
103
|
+
/** Earliest spawn time across the group (`started_at`, else `timestamp`). */
|
|
104
|
+
firstSpawnTs: string;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Group team-origin sessions for the `--teams` view: each named team becomes one
|
|
108
|
+
* group of its teammates, and every bare SDK spawn with no teammate record falls
|
|
109
|
+
* into a single trailing {@link NO_TEAM_GROUP_KEY} bucket, so a real teammate and
|
|
110
|
+
* a bare spawn are never shown as the same thing.
|
|
111
|
+
*
|
|
112
|
+
* Only rows with a resolved {@link TeamOrigin} participate (the caller has
|
|
113
|
+
* already run {@link filterTeamSessions}); any non-team session is ignored.
|
|
114
|
+
* Groups are ordered newest-active first; sub-agents always sort last. Pure —
|
|
115
|
+
* unit-tested.
|
|
116
|
+
*/
|
|
117
|
+
export declare function groupSessionsByTeam(sessions: SessionMeta[]): TeamSessionGroup[];
|
|
118
|
+
export {};
|
|
@@ -35,7 +35,7 @@ export function classifyTeamSession(session) {
|
|
|
35
35
|
if (origin)
|
|
36
36
|
return origin;
|
|
37
37
|
if (session.isTeamOrigin) {
|
|
38
|
-
return { handle: session.id.slice(0, 8) };
|
|
38
|
+
return { handle: session.id.slice(0, 8), source: 'entrypoint' };
|
|
39
39
|
}
|
|
40
40
|
return null;
|
|
41
41
|
}
|
|
@@ -54,12 +54,16 @@ function readTeamOrigin(metaPath, agentId) {
|
|
|
54
54
|
mode: str(meta.mode),
|
|
55
55
|
team: str(meta.task_name),
|
|
56
56
|
parentSessionId: str(meta.parent_session_id),
|
|
57
|
+
startedAt: str(meta.started_at),
|
|
58
|
+
source: 'meta',
|
|
57
59
|
},
|
|
58
60
|
sessionId: str(meta.remote_session_id),
|
|
59
61
|
};
|
|
60
62
|
}
|
|
61
63
|
catch {
|
|
62
|
-
|
|
64
|
+
// An unreadable meta.json still lives under the teams agents dir, so the
|
|
65
|
+
// session IS a teammate — mark the source accordingly, just without fields.
|
|
66
|
+
return { origin: { handle: agentId.slice(0, 8), source: 'meta' } };
|
|
63
67
|
}
|
|
64
68
|
}
|
|
65
69
|
/**
|
|
@@ -133,7 +137,8 @@ export function enrichTeamOrigins(sessions) {
|
|
|
133
137
|
// here would find no local record and downgrade a named teammate to a bare id.
|
|
134
138
|
if (session.teamOrigin)
|
|
135
139
|
return session;
|
|
136
|
-
const origin = index.get(session.id) ??
|
|
140
|
+
const origin = index.get(session.id) ??
|
|
141
|
+
(session.isTeamOrigin ? { handle: session.id.slice(0, 8), source: 'entrypoint' } : null);
|
|
137
142
|
return origin ? { ...session, teamOrigin: origin } : session;
|
|
138
143
|
});
|
|
139
144
|
}
|
|
@@ -162,3 +167,93 @@ export function filterTeamSessions(sessions, showTeams) {
|
|
|
162
167
|
}
|
|
163
168
|
return { visible, hiddenCount };
|
|
164
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* Whether a team-origin session is a real `agents teams` teammate or a bare SDK
|
|
172
|
+
* spawn with no teammate record. The two share the `sdk-cli` entrypoint that
|
|
173
|
+
* sets `isTeamOrigin`, so they are told apart by whether a `meta.json` teammate
|
|
174
|
+
* record backs the origin ({@link TeamOrigin.source} === `'meta'`).
|
|
175
|
+
*
|
|
176
|
+
* `source` is newer than the rest of {@link TeamOrigin}, so a row that arrived
|
|
177
|
+
* from a pre-`source` peer over the `--host` fan-out carries none. There the
|
|
178
|
+
* meta-only fields settle it: the entrypoint fallback is a bare `{ handle }`, so
|
|
179
|
+
* any `team` / `mode` / `parentSessionId` / `startedAt` means a real teammate
|
|
180
|
+
* record produced it — otherwise a genuine teammate from an unupgraded peer
|
|
181
|
+
* would be misfiled into the no-team bucket, the exact conflation this splits.
|
|
182
|
+
*/
|
|
183
|
+
export function teamRowKind(origin) {
|
|
184
|
+
if (!origin)
|
|
185
|
+
return 'subagent';
|
|
186
|
+
if (origin.source === 'meta')
|
|
187
|
+
return 'teammate';
|
|
188
|
+
if (origin.source === 'entrypoint')
|
|
189
|
+
return 'subagent';
|
|
190
|
+
return origin.team || origin.mode || origin.parentSessionId || origin.startedAt
|
|
191
|
+
? 'teammate'
|
|
192
|
+
: 'subagent';
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Key/label for the residual bucket — sessions flagged `isTeamOrigin` (an SDK /
|
|
196
|
+
* `sdk-cli` entrypoint) that carry no teammate `meta.json`. In practice this is
|
|
197
|
+
* headless `agents run` spawns and teammates whose team record aged out; a true
|
|
198
|
+
* `Task` sub-agent's transcript is never indexed, so it cannot land here. Named
|
|
199
|
+
* "(no team)" rather than "(sub-agents)" so the label matches what it holds.
|
|
200
|
+
*/
|
|
201
|
+
export const NO_TEAM_GROUP_KEY = '(no team)';
|
|
202
|
+
/** Key/label for teammates whose record carries no team name (`task_name`). */
|
|
203
|
+
export const UNNAMED_TEAM_KEY = '(unnamed team)';
|
|
204
|
+
/** Recency signal for a row: last activity, else the creation timestamp. */
|
|
205
|
+
function rowTs(s) {
|
|
206
|
+
return s.lastActivity ?? s.timestamp;
|
|
207
|
+
}
|
|
208
|
+
/** Spawn time for a row: the meta `started_at`, else the transcript creation time. */
|
|
209
|
+
function spawnTs(s) {
|
|
210
|
+
return s.teamOrigin.startedAt ?? s.timestamp;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Group team-origin sessions for the `--teams` view: each named team becomes one
|
|
214
|
+
* group of its teammates, and every bare SDK spawn with no teammate record falls
|
|
215
|
+
* into a single trailing {@link NO_TEAM_GROUP_KEY} bucket, so a real teammate and
|
|
216
|
+
* a bare spawn are never shown as the same thing.
|
|
217
|
+
*
|
|
218
|
+
* Only rows with a resolved {@link TeamOrigin} participate (the caller has
|
|
219
|
+
* already run {@link filterTeamSessions}); any non-team session is ignored.
|
|
220
|
+
* Groups are ordered newest-active first; sub-agents always sort last. Pure —
|
|
221
|
+
* unit-tested.
|
|
222
|
+
*/
|
|
223
|
+
export function groupSessionsByTeam(sessions) {
|
|
224
|
+
const byKey = new Map();
|
|
225
|
+
for (const s of sessions) {
|
|
226
|
+
if (!s.teamOrigin)
|
|
227
|
+
continue;
|
|
228
|
+
const kind = teamRowKind(s.teamOrigin);
|
|
229
|
+
const key = kind === 'subagent'
|
|
230
|
+
? NO_TEAM_GROUP_KEY
|
|
231
|
+
: safeTeamText(s.teamOrigin.team) ?? UNNAMED_TEAM_KEY;
|
|
232
|
+
const ts = s;
|
|
233
|
+
(byKey.get(key) ?? byKey.set(key, []).get(key)).push(ts);
|
|
234
|
+
}
|
|
235
|
+
const groups = [];
|
|
236
|
+
for (const [key, rows] of byKey) {
|
|
237
|
+
rows.sort((a, b) => (rowTs(a) < rowTs(b) ? 1 : rowTs(a) > rowTs(b) ? -1 : 0));
|
|
238
|
+
const kind = key === NO_TEAM_GROUP_KEY ? 'noTeam' : 'team';
|
|
239
|
+
// A single agreed-on spawner names the orchestrator for the whole team; a
|
|
240
|
+
// mix (or none) leaves it unset rather than claiming a wrong parent.
|
|
241
|
+
const parents = new Set(rows.map((r) => r.teamOrigin.parentSessionId).filter((p) => !!p));
|
|
242
|
+
groups.push({
|
|
243
|
+
key,
|
|
244
|
+
kind,
|
|
245
|
+
team: kind === 'team' && key !== UNNAMED_TEAM_KEY ? key : undefined,
|
|
246
|
+
spawnerSessionId: parents.size === 1 ? [...parents][0] : undefined,
|
|
247
|
+
sessions: rows,
|
|
248
|
+
maxTs: rowTs(rows[0]),
|
|
249
|
+
firstSpawnTs: rows.map(spawnTs).sort()[0],
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
// Newest-active team first; the no-team bucket always sinks to the bottom.
|
|
253
|
+
groups.sort((a, b) => {
|
|
254
|
+
if (a.kind !== b.kind)
|
|
255
|
+
return a.kind === 'noTeam' ? 1 : -1;
|
|
256
|
+
return a.maxTs < b.maxTs ? 1 : a.maxTs > b.maxTs ? -1 : a.key.localeCompare(b.key);
|
|
257
|
+
});
|
|
258
|
+
return groups;
|
|
259
|
+
}
|
|
@@ -117,6 +117,24 @@ export interface TeamOrigin {
|
|
|
117
117
|
* meta dir has aged past the teams cleanup window.
|
|
118
118
|
*/
|
|
119
119
|
parentSessionId?: string;
|
|
120
|
+
/**
|
|
121
|
+
* Spawn time (`started_at` in the teammate meta.json) — when `agents teams`
|
|
122
|
+
* launched this teammate. Distinct from the session's own `timestamp` (first
|
|
123
|
+
* transcript line), and present even before the harness writes a transcript.
|
|
124
|
+
* Absent for the entrypoint-only fallback (no meta record).
|
|
125
|
+
*/
|
|
126
|
+
startedAt?: string;
|
|
127
|
+
/**
|
|
128
|
+
* How this origin was established, which is what separates a real
|
|
129
|
+
* `agents teams` teammate from a plain SDK sub-agent (a `Task` / `Agent()`
|
|
130
|
+
* spawn). Both carry the `sdk-cli` entrypoint that sets `isTeamOrigin`, so the
|
|
131
|
+
* entrypoint flag alone cannot tell them apart — only a teammate has a
|
|
132
|
+
* `meta.json` under the teams agents dir.
|
|
133
|
+
* - `'meta'` — a teammate: read from its `meta.json` record.
|
|
134
|
+
* - `'entrypoint'` — an SDK spawn with no team record (a sub-agent, or a
|
|
135
|
+
* teammate whose meta dir aged past the cleanup window).
|
|
136
|
+
*/
|
|
137
|
+
source?: 'meta' | 'entrypoint';
|
|
120
138
|
}
|
|
121
139
|
/** Lightweight metadata for a discovered session, used in listings and pickers. */
|
|
122
140
|
export interface SessionMeta {
|
|
@@ -144,8 +162,23 @@ export interface SessionMeta {
|
|
|
144
162
|
tokenCount?: number;
|
|
145
163
|
/** Real generated (output) tokens — excludes cache-read/-write context (issue: `agents output`). */
|
|
146
164
|
outputTokens?: number;
|
|
165
|
+
/**
|
|
166
|
+
* Uncached input tokens, cache-read tokens, and cache-write (cache-creation)
|
|
167
|
+
* tokens — the burn split `agents output` reports, kept only for harnesses that
|
|
168
|
+
* record a per-message cache split (Claude/Codex/Gemini/Droid). Undefined for
|
|
169
|
+
* harnesses that expose no split (RUSH-2287).
|
|
170
|
+
*/
|
|
171
|
+
inputTokens?: number;
|
|
172
|
+
cacheReadTokens?: number;
|
|
173
|
+
cacheWriteTokens?: number;
|
|
147
174
|
/** Total USD cost, computed at scan time from per-model token usage (issue #323). */
|
|
148
175
|
costUsd?: number;
|
|
176
|
+
/**
|
|
177
|
+
* USD cost priced as if caching were off — cache read/write billed at the full
|
|
178
|
+
* input rate. Backs `agents output --pricing no-cache` (RUSH-2287). Undefined
|
|
179
|
+
* when the harness records no cache split (then no-cache == actual by definition).
|
|
180
|
+
*/
|
|
181
|
+
costUsdNoCache?: number;
|
|
149
182
|
/** Wall-clock duration in ms (lastTs − firstTs), persisted at scan time. */
|
|
150
183
|
durationMs?: number;
|
|
151
184
|
/** Underlying LLM model observed in the transcript, when the agent records one. */
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Device
|
|
2
|
+
* Device placement for `--device auto` / `--host auto`.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Explicit auto placement probes live fleet health and harness readiness.
|
|
5
|
+
* Historical affinity remains for generic host resolution callers.
|
|
6
6
|
* Account selection is separate (`--strategy balanced` / rotate.ts).
|
|
7
7
|
*/
|
|
8
8
|
import { type AffinityRow } from './session/db.js';
|
|
9
|
+
import { type DevicePlacementSignal } from './teams/scheduler.js';
|
|
10
|
+
import type { AgentType } from './teams/agents.js';
|
|
9
11
|
/** Peakiness of usage weights; >1 amplifies the most-used option. */
|
|
10
12
|
export declare const DEFAULT_AFFINITY_ALPHA = 1.3;
|
|
11
13
|
export interface WeightedCandidate {
|
|
@@ -45,6 +47,29 @@ export interface DeviceAffinityPlan {
|
|
|
45
47
|
deviceCandidates: WeightedCandidate[];
|
|
46
48
|
pickedDeviceKey: string | null;
|
|
47
49
|
}
|
|
50
|
+
/** Live placement plan used by the explicit `--device auto` surface. */
|
|
51
|
+
export interface DeviceAutoPlan {
|
|
52
|
+
/** null means the local machine won the comparison. */
|
|
53
|
+
host: string | null;
|
|
54
|
+
candidates: Array<{
|
|
55
|
+
key: string;
|
|
56
|
+
loadPercent?: number;
|
|
57
|
+
installed?: boolean;
|
|
58
|
+
signedIn?: boolean;
|
|
59
|
+
}>;
|
|
60
|
+
pickedDeviceKey: string;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Pick the least-loaded healthy device that can run `agent` when the harness is
|
|
64
|
+
* known. `run auto` omits the agent and ranks the same live health/load signals
|
|
65
|
+
* without an installed-harness filter. The local machine participates in the
|
|
66
|
+
* same probe; a fully unusable fleet degrades to local instead of stranding a run.
|
|
67
|
+
*/
|
|
68
|
+
export declare function resolveDeviceAuto(agent?: string, opts?: {
|
|
69
|
+
eligibleHosts?: string[];
|
|
70
|
+
localMachine?: string;
|
|
71
|
+
probe?: (pool: string[], agent?: AgentType) => Promise<Map<string, DevicePlacementSignal>>;
|
|
72
|
+
}): Promise<DeviceAutoPlan>;
|
|
48
73
|
/**
|
|
49
74
|
* Resolve host for `--device auto`. Does NOT pick harness or accounts.
|
|
50
75
|
*/
|
|
@@ -81,6 +106,7 @@ export type DeviceAutoApplyResult = {
|
|
|
81
106
|
* returns `skipped` — never throws; callers must not hard-exit.
|
|
82
107
|
*/
|
|
83
108
|
export declare function applyDeviceAutoToOptions(options: DeviceAutoHostOptions, deps?: {
|
|
84
|
-
resolve?: () =>
|
|
109
|
+
resolve?: () => DeviceAutoPlan | Promise<DeviceAutoPlan>;
|
|
110
|
+
agent?: string;
|
|
85
111
|
accountPickerRequested?: boolean;
|
|
86
|
-
}): DeviceAutoApplyResult
|
|
112
|
+
}): Promise<DeviceAutoApplyResult>;
|
package/dist/lib/smart-launch.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Device
|
|
2
|
+
* Device placement for `--device auto` / `--host auto`.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Explicit auto placement probes live fleet health and harness readiness.
|
|
5
|
+
* Historical affinity remains for generic host resolution callers.
|
|
6
6
|
* Account selection is separate (`--strategy balanced` / rotate.ts).
|
|
7
7
|
*/
|
|
8
8
|
import { queryAffinityRollup } from './session/db.js';
|
|
9
9
|
import { localMachineId } from './session/origin-machine.js';
|
|
10
10
|
import { loadDevicesSync } from './devices/registry.js';
|
|
11
11
|
import { normalizeHost } from './machine-id.js';
|
|
12
|
+
import { probePoolSignals } from './teams/placement-probe.js';
|
|
13
|
+
import { pickBestDevice } from './teams/scheduler.js';
|
|
12
14
|
/** Peakiness of usage weights; >1 amplifies the most-used option. */
|
|
13
15
|
export const DEFAULT_AFFINITY_ALPHA = 1.3;
|
|
14
16
|
/**
|
|
@@ -62,6 +64,39 @@ export function listOnlineDeviceNames(localName = localMachineId()) {
|
|
|
62
64
|
}
|
|
63
65
|
return [...names];
|
|
64
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Pick the least-loaded healthy device that can run `agent` when the harness is
|
|
69
|
+
* known. `run auto` omits the agent and ranks the same live health/load signals
|
|
70
|
+
* without an installed-harness filter. The local machine participates in the
|
|
71
|
+
* same probe; a fully unusable fleet degrades to local instead of stranding a run.
|
|
72
|
+
*/
|
|
73
|
+
export async function resolveDeviceAuto(agent, opts = {}) {
|
|
74
|
+
const local = normalizeHost(opts.localMachine ?? localMachineId());
|
|
75
|
+
const pool = [...new Set((opts.eligibleHosts ?? listOnlineDeviceNames(local)).map(normalizeHost))];
|
|
76
|
+
if (!pool.includes(local))
|
|
77
|
+
pool.push(local);
|
|
78
|
+
try {
|
|
79
|
+
const signals = await (opts.probe ?? probePoolSignals)(pool, agent);
|
|
80
|
+
const picked = pickBestDevice(pool, [], { signals, agentLabel: agent });
|
|
81
|
+
return {
|
|
82
|
+
host: picked === local ? null : picked,
|
|
83
|
+
pickedDeviceKey: picked,
|
|
84
|
+
candidates: pool.map((key) => ({
|
|
85
|
+
key,
|
|
86
|
+
loadPercent: signals.get(key)?.loadPercent,
|
|
87
|
+
installed: signals.get(key)?.installed,
|
|
88
|
+
signedIn: signals.get(key)?.signedIn,
|
|
89
|
+
})),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return {
|
|
94
|
+
host: null,
|
|
95
|
+
pickedDeviceKey: local,
|
|
96
|
+
candidates: [{ key: local }],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
65
100
|
/**
|
|
66
101
|
* Resolve host for `--device auto`. Does NOT pick harness or accounts.
|
|
67
102
|
*/
|
|
@@ -120,7 +155,7 @@ const HOST_SLOTS = ['host', 'device', 'on', 'computer'];
|
|
|
120
155
|
* Mutates `options` in place. On affinity failure, clears auto slots (local) and
|
|
121
156
|
* returns `skipped` — never throws; callers must not hard-exit.
|
|
122
157
|
*/
|
|
123
|
-
export function applyDeviceAutoToOptions(options, deps = {}) {
|
|
158
|
+
export async function applyDeviceAutoToOptions(options, deps = {}) {
|
|
124
159
|
let deprecationSmart = false;
|
|
125
160
|
if (options.smart) {
|
|
126
161
|
const anyHost = HOST_SLOTS.some((k) => typeof options[k] === 'string' && options[k].trim() !== '');
|
|
@@ -133,7 +168,8 @@ export function applyDeviceAutoToOptions(options, deps = {}) {
|
|
|
133
168
|
return { attempted: false, deprecationSmart };
|
|
134
169
|
}
|
|
135
170
|
try {
|
|
136
|
-
const
|
|
171
|
+
const resolve = deps.resolve ?? (() => resolveDeviceAuto(deps.agent));
|
|
172
|
+
const plan = await resolve();
|
|
137
173
|
const concrete = plan.host; // null = local
|
|
138
174
|
for (const k of HOST_SLOTS) {
|
|
139
175
|
if (isDeviceAuto(options[k])) {
|
|
@@ -145,9 +181,9 @@ export function applyDeviceAutoToOptions(options, deps = {}) {
|
|
|
145
181
|
options.balanced = true;
|
|
146
182
|
}
|
|
147
183
|
const hostLabel = concrete ?? 'local';
|
|
148
|
-
const deviceHint = plan.
|
|
184
|
+
const deviceHint = plan.candidates
|
|
149
185
|
.slice(0, 4)
|
|
150
|
-
.map((c) => `${c.key}:${c.
|
|
186
|
+
.map((c) => `${c.key}:${c.loadPercent === undefined ? '?' : `${Math.round(c.loadPercent)}%`}`)
|
|
151
187
|
.join(', ');
|
|
152
188
|
const acctNote = accountPickerRequested ? 'accounts=picker' : 'accounts=balanced';
|
|
153
189
|
return {
|
package/dist/lib/ssh-exec.d.ts
CHANGED
|
@@ -28,6 +28,31 @@ export declare function shellQuote(s: string): string;
|
|
|
28
28
|
* long-lived `-N` tunnel can't leave a zombie ssh + socket pinned on the laptop.
|
|
29
29
|
*/
|
|
30
30
|
export declare const SSH_OPTS: readonly string[];
|
|
31
|
+
/**
|
|
32
|
+
* Hard ceiling on the stdout one peer may return before its capture is aborted.
|
|
33
|
+
* A cross-machine fan-out streams every peer's `agents … --json` into memory in
|
|
34
|
+
* parallel, so an unbounded buffer means one runaway peer (a corrupt or
|
|
35
|
+
* pathologically large payload) can exhaust the caller's heap and take the whole
|
|
36
|
+
* sweep down with it — RUSH-2065 observed ~170MB retained across a single
|
|
37
|
+
* `--active` gather. 16 MiB is far above any legitimate JSON listing yet small
|
|
38
|
+
* enough that N peers in flight stay bounded. A peer that overflows is treated as
|
|
39
|
+
* unreachable rather than trusted with partial output.
|
|
40
|
+
*/
|
|
41
|
+
export declare const REMOTE_STDOUT_MAX_BYTES: number;
|
|
42
|
+
/**
|
|
43
|
+
* Accumulate SSH stdout as UTF-8 without corrupting a multi-byte code point that
|
|
44
|
+
* a chunk boundary splits — the streaming `on('data')` callbacks hand us raw
|
|
45
|
+
* `Buffer`s, and a naive `buf.toString()` per chunk mangles any character the
|
|
46
|
+
* kernel cut in half. `StringDecoder` holds the trailing partial bytes until the
|
|
47
|
+
* next chunk completes them.
|
|
48
|
+
*/
|
|
49
|
+
export declare class RemoteUtf8Accumulator {
|
|
50
|
+
private readonly decoder;
|
|
51
|
+
private value;
|
|
52
|
+
write(chunk: Buffer): void;
|
|
53
|
+
end(): string;
|
|
54
|
+
current(): string;
|
|
55
|
+
}
|
|
31
56
|
export declare function controlOpts(): string[];
|
|
32
57
|
/**
|
|
33
58
|
* Compose an ssh connection-option prefix.
|
|
@@ -128,6 +153,8 @@ export interface SshStreamOptions {
|
|
|
128
153
|
* interactive credential-copy path (RUSH-1767).
|
|
129
154
|
*/
|
|
130
155
|
hostKeyOpts?: string[];
|
|
156
|
+
/** Additional OpenSSH argv placed before the target (for example `-i <path>`). */
|
|
157
|
+
extraSshArgs?: string[];
|
|
131
158
|
}
|
|
132
159
|
/**
|
|
133
160
|
* Foreground counterpart to `sshExec`: run `remoteCmd` on `target` with the
|
package/dist/lib/ssh-exec.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* canonical definition; `commands/secrets.ts` re-exports it.
|
|
9
9
|
*/
|
|
10
10
|
import { spawn, spawnSync } from 'child_process';
|
|
11
|
+
import { StringDecoder } from 'string_decoder';
|
|
11
12
|
import * as fs from 'fs';
|
|
12
13
|
import * as path from 'path';
|
|
13
14
|
import { getCacheDir } from './state.js';
|
|
@@ -46,6 +47,38 @@ export const SSH_OPTS = [
|
|
|
46
47
|
'-o', 'ServerAliveInterval=15',
|
|
47
48
|
'-o', 'ServerAliveCountMax=3',
|
|
48
49
|
];
|
|
50
|
+
/**
|
|
51
|
+
* Hard ceiling on the stdout one peer may return before its capture is aborted.
|
|
52
|
+
* A cross-machine fan-out streams every peer's `agents … --json` into memory in
|
|
53
|
+
* parallel, so an unbounded buffer means one runaway peer (a corrupt or
|
|
54
|
+
* pathologically large payload) can exhaust the caller's heap and take the whole
|
|
55
|
+
* sweep down with it — RUSH-2065 observed ~170MB retained across a single
|
|
56
|
+
* `--active` gather. 16 MiB is far above any legitimate JSON listing yet small
|
|
57
|
+
* enough that N peers in flight stay bounded. A peer that overflows is treated as
|
|
58
|
+
* unreachable rather than trusted with partial output.
|
|
59
|
+
*/
|
|
60
|
+
export const REMOTE_STDOUT_MAX_BYTES = 16 * 1024 * 1024;
|
|
61
|
+
/**
|
|
62
|
+
* Accumulate SSH stdout as UTF-8 without corrupting a multi-byte code point that
|
|
63
|
+
* a chunk boundary splits — the streaming `on('data')` callbacks hand us raw
|
|
64
|
+
* `Buffer`s, and a naive `buf.toString()` per chunk mangles any character the
|
|
65
|
+
* kernel cut in half. `StringDecoder` holds the trailing partial bytes until the
|
|
66
|
+
* next chunk completes them.
|
|
67
|
+
*/
|
|
68
|
+
export class RemoteUtf8Accumulator {
|
|
69
|
+
decoder = new StringDecoder('utf8');
|
|
70
|
+
value = '';
|
|
71
|
+
write(chunk) {
|
|
72
|
+
this.value += this.decoder.write(chunk);
|
|
73
|
+
}
|
|
74
|
+
end() {
|
|
75
|
+
this.value += this.decoder.end();
|
|
76
|
+
return this.value;
|
|
77
|
+
}
|
|
78
|
+
current() {
|
|
79
|
+
return this.value;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
49
82
|
/**
|
|
50
83
|
* OpenSSH connection-multiplexing options. The first connection to a host opens
|
|
51
84
|
* a control socket; subsequent connections (even from a *separate* `agents`
|
|
@@ -292,7 +325,7 @@ export function sshStream(target, remoteCmd, opts = {}) {
|
|
|
292
325
|
assertValidSshTarget(target);
|
|
293
326
|
const mux = opts.multiplex === false ? [] : controlOpts();
|
|
294
327
|
const tty = opts.tty ? ['-tt'] : [];
|
|
295
|
-
const args = [...sshConnectOpts(mux, opts.hostKeyOpts), ...tty, target, remoteCmd];
|
|
328
|
+
const args = [...sshConnectOpts(mux, opts.hostKeyOpts), ...(opts.extraSshArgs ?? []), ...tty, target, remoteCmd];
|
|
296
329
|
const res = spawnSync('ssh', args, { stdio: 'inherit' });
|
|
297
330
|
if (typeof res.status === 'number')
|
|
298
331
|
return res.status;
|
package/dist/lib/ssh-tunnel.d.ts
CHANGED
|
@@ -27,13 +27,14 @@ export interface StartTunnelOptions {
|
|
|
27
27
|
* of one CDP session and kills it on cleanup.
|
|
28
28
|
*/
|
|
29
29
|
detached?: boolean;
|
|
30
|
+
extraSshArgs?: string[];
|
|
30
31
|
}
|
|
31
32
|
/** Build the ssh argv (after the `ssh` program name) for an `-L` tunnel. Pure.
|
|
32
33
|
*
|
|
33
34
|
* Composes the shared hardened baseline (`SSH_OPTS`) rather than re-listing it,
|
|
34
35
|
* so the tunnel inherits the same options — crucially the keepalive, which lets
|
|
35
36
|
* a dropped `-N` tunnel exit instead of lingering as a zombie on the laptop. */
|
|
36
|
-
export declare function buildTunnelArgs(user: string, host: string, localPort: number, remotePort: number): string[];
|
|
37
|
+
export declare function buildTunnelArgs(user: string, host: string, localPort: number, remotePort: number, extraSshArgs?: string[]): string[];
|
|
37
38
|
/**
|
|
38
39
|
* Spawn `ssh -L localPort:127.0.0.1:remotePort -N user@host`.
|
|
39
40
|
*
|
|
@@ -151,7 +152,7 @@ export declare function scpRemotePath(remotePath: string): string;
|
|
|
151
152
|
* assert the real binary copy path keeps BatchMode and does not route bytes
|
|
152
153
|
* through a PowerShell decoder.
|
|
153
154
|
*/
|
|
154
|
-
export declare function buildScpArgs(target: string, remotePath: string, filePath: string): string[];
|
|
155
|
+
export declare function buildScpArgs(target: string, remotePath: string, filePath: string, extraSshArgs?: string[]): string[];
|
|
155
156
|
/**
|
|
156
157
|
* `setup --host`: push the exe, then register + start the LOGON task. Remote
|
|
157
158
|
* PowerShell hops go through `sshExec` (BatchMode key auth — the same hardening
|