@phnx-labs/agents-cli 1.22.25 → 1.22.27
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 +427 -0
- package/README.md +35 -3
- package/dist/bin/agents +0 -0
- package/dist/browser.js +14 -4
- package/dist/commands/apply.js +52 -8
- package/dist/commands/bench.d.ts +2 -0
- package/dist/commands/bench.js +101 -0
- package/dist/commands/browser.js +35 -0
- package/dist/commands/doctor.js +32 -10
- package/dist/commands/exec.js +17 -16
- package/dist/commands/insights.d.ts +25 -19
- package/dist/commands/insights.js +146 -38
- package/dist/commands/output.js +100 -25
- package/dist/commands/reconnect.d.ts +46 -0
- package/dist/commands/reconnect.js +109 -0
- package/dist/commands/resume.d.ts +11 -0
- package/dist/commands/resume.js +51 -0
- package/dist/commands/routines.js +2 -2
- package/dist/commands/secrets.d.ts +2 -8
- package/dist/commands/secrets.js +29 -105
- package/dist/commands/sessions-picker.js +12 -7
- package/dist/commands/sessions.d.ts +47 -1
- package/dist/commands/sessions.js +349 -37
- package/dist/commands/setup-secrets.d.ts +1 -0
- package/dist/commands/setup-secrets.js +1 -1
- package/dist/commands/setup.d.ts +26 -3
- package/dist/commands/setup.js +105 -46
- package/dist/commands/ssh.js +184 -11
- package/dist/commands/teams.d.ts +6 -0
- package/dist/commands/teams.js +46 -3
- package/dist/commands/trends.d.ts +8 -0
- package/dist/commands/trends.js +10 -156
- package/dist/index.js +17 -21
- package/dist/lib/agents.d.ts +11 -0
- package/dist/lib/agents.js +29 -2
- package/dist/lib/analytics/dashboard.d.ts +10 -6
- package/dist/lib/analytics/dashboard.js +6 -4
- package/dist/lib/analytics/mix-commands.d.ts +53 -0
- package/dist/lib/analytics/mix-commands.js +229 -0
- package/dist/lib/analytics/recipes.d.ts +19 -14
- package/dist/lib/analytics/recipes.js +4 -2
- 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/browser/ipc.d.ts +26 -0
- package/dist/lib/browser/ipc.js +139 -24
- package/dist/lib/browser/profiles.d.ts +11 -0
- package/dist/lib/browser/profiles.js +1 -1
- package/dist/lib/browser/stream.d.ts +14 -0
- package/dist/lib/browser/stream.js +71 -0
- package/dist/lib/channels/owner-sink.d.ts +27 -0
- package/dist/lib/channels/owner-sink.js +93 -0
- package/dist/lib/claude-account-token.d.ts +2 -0
- package/dist/lib/claude-account-token.js +27 -1
- 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 +10 -1
- package/dist/lib/devices/doctor-findings.js +47 -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/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/exec.bench.d.ts +1 -0
- package/dist/lib/exec.bench.js +186 -0
- package/dist/lib/exec.js +18 -8
- package/dist/lib/fleet/apply.d.ts +59 -3
- package/dist/lib/fleet/apply.js +191 -12
- package/dist/lib/fleet/remote-login.d.ts +4 -3
- package/dist/lib/fleet/remote-login.js +11 -9
- package/dist/lib/fleet/types.d.ts +21 -2
- package/dist/lib/gemini-settings.d.ts +0 -1
- package/dist/lib/gemini-settings.js +12 -7
- package/dist/lib/hooks/cache.js +15 -0
- package/dist/lib/hooks.d.ts +8 -5
- package/dist/lib/hooks.js +10 -6
- package/dist/lib/hosts/dispatch.js +7 -3
- package/dist/lib/hosts/passthrough.d.ts +45 -0
- package/dist/lib/hosts/passthrough.js +59 -8
- package/dist/lib/hosts/progress.d.ts +2 -1
- package/dist/lib/hosts/progress.js +4 -3
- package/dist/lib/hosts/providers/devices.js +1 -0
- package/dist/lib/hosts/ready.d.ts +6 -2
- package/dist/lib/hosts/ready.js +33 -8
- package/dist/lib/hosts/reconcile.d.ts +1 -1
- package/dist/lib/hosts/reconcile.js +16 -7
- package/dist/lib/hosts/reconnect.d.ts +56 -21
- package/dist/lib/hosts/reconnect.js +128 -71
- package/dist/lib/hosts/registry.d.ts +2 -2
- package/dist/lib/hosts/registry.js +3 -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 +2 -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/models.d.ts +30 -1
- package/dist/lib/models.js +42 -144
- 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/redact.js +8 -3
- 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/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/routines.js +14 -2
- package/dist/lib/runner.d.ts +9 -4
- package/dist/lib/runner.js +182 -30
- 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.js +3 -2
- package/dist/lib/secrets/lease.d.ts +25 -0
- package/dist/lib/secrets/lease.js +44 -0
- package/dist/lib/secrets/push.d.ts +94 -0
- package/dist/lib/secrets/push.js +145 -0
- package/dist/lib/secrets/reaper.d.ts +15 -1
- package/dist/lib/secrets/reaper.js +30 -3
- package/dist/lib/self-update.d.ts +20 -5
- package/dist/lib/self-update.js +93 -16
- package/dist/lib/session/db.d.ts +38 -3
- package/dist/lib/session/db.js +273 -16
- package/dist/lib/session/discover.d.ts +22 -1
- package/dist/lib/session/discover.js +239 -51
- package/dist/lib/session/insights.d.ts +37 -0
- package/dist/lib/session/insights.js +219 -9
- package/dist/lib/session/recovery.d.ts +17 -1
- package/dist/lib/session/recovery.js +111 -4
- 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/tool-index.js +133 -22
- package/dist/lib/session/tool-store.d.ts +26 -2
- package/dist/lib/session/tool-store.js +36 -17
- 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 +42 -3
- package/dist/lib/ssh-tunnel.d.ts +3 -2
- package/dist/lib/ssh-tunnel.js +25 -16
- package/dist/lib/startup/command-registry.d.ts +2 -0
- package/dist/lib/startup/command-registry.js +6 -0
- package/dist/lib/teams/agents.d.ts +13 -0
- package/dist/lib/teams/agents.js +75 -7
- package/dist/lib/teams/placement-probe.d.ts +21 -0
- package/dist/lib/teams/placement-probe.js +135 -0
- package/dist/lib/teams/scheduler.d.ts +74 -1
- package/dist/lib/teams/scheduler.js +187 -10
- 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/package.json +1 -1
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents reconnect [session-id]` — get me back into an agent terminal whose ssh
|
|
3
|
+
* link dropped.
|
|
4
|
+
*
|
|
5
|
+
* The auto-reconnect in lib/hosts/reconnect.ts already re-attaches a live remote
|
|
6
|
+
* pane over its own bounded backoff on a transient blink. This is the MANUAL
|
|
7
|
+
* recovery for after it gave up — a sustained outage, or a VS Code terminal tab
|
|
8
|
+
* that closed with the dead ssh client. One verb that always tries hardest to put
|
|
9
|
+
* you back into that agent: attach the live pane if it still exists, else resume
|
|
10
|
+
* the session; live pane > resumed copy > a clear message about what was lost.
|
|
11
|
+
*
|
|
12
|
+
* It delegates to `focus`, which already does exactly attach-else-resume for a
|
|
13
|
+
* resolved id ({@link focusAction} → `focusResolvedSession`), so there is no second
|
|
14
|
+
* recovery path to keep in sync. What `reconnect` adds is the recovery framing and,
|
|
15
|
+
* with no id, targeting the MOST RECENT session in this shell's directory — the one
|
|
16
|
+
* that most likely just dropped — instead of opening the full fleet picker.
|
|
17
|
+
*/
|
|
18
|
+
import type { Command } from 'commander';
|
|
19
|
+
import type { SessionMeta } from '../lib/session/types.js';
|
|
20
|
+
/**
|
|
21
|
+
* The recency signal a no-id reconnect targets: last activity if the scan
|
|
22
|
+
* computed one, else the creation time. A row with neither parseable sorts last.
|
|
23
|
+
* Pure so target selection is unit-tested without the scanner.
|
|
24
|
+
*/
|
|
25
|
+
export declare function sessionRecency(s: Pick<SessionMeta, 'lastActivity' | 'timestamp'>): number;
|
|
26
|
+
/**
|
|
27
|
+
* Pick the session to reconnect when the user gave no id: the most recently
|
|
28
|
+
* active one in scope — the terminal that most likely just dropped. Pure; returns
|
|
29
|
+
* undefined for an empty scope so the caller can print guidance.
|
|
30
|
+
*/
|
|
31
|
+
export declare function pickMostRecentSession(sessions: SessionMeta[]): SessionMeta | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* Resolve the no-id target: prefer the most recent session started from THIS
|
|
34
|
+
* directory (the shell the user is reconnecting from), falling back to the most
|
|
35
|
+
* recent session anywhere when the current directory has none. Best-effort — a
|
|
36
|
+
* discovery failure yields undefined, never throws. Injectable `discover` keeps
|
|
37
|
+
* the two-step scope preference testable without touching the real scanner.
|
|
38
|
+
*/
|
|
39
|
+
export declare function resolveRecentTarget(cwd: string, discover?: (opts: {
|
|
40
|
+
cwd?: string;
|
|
41
|
+
all?: boolean;
|
|
42
|
+
since?: string;
|
|
43
|
+
limit?: number;
|
|
44
|
+
}) => Promise<SessionMeta[]>): Promise<SessionMeta | undefined>;
|
|
45
|
+
export declare function reconnectAction(id: string | undefined): Promise<void>;
|
|
46
|
+
export declare function registerReconnectCommand(program: Command): void;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents reconnect [session-id]` — get me back into an agent terminal whose ssh
|
|
3
|
+
* link dropped.
|
|
4
|
+
*
|
|
5
|
+
* The auto-reconnect in lib/hosts/reconnect.ts already re-attaches a live remote
|
|
6
|
+
* pane over its own bounded backoff on a transient blink. This is the MANUAL
|
|
7
|
+
* recovery for after it gave up — a sustained outage, or a VS Code terminal tab
|
|
8
|
+
* that closed with the dead ssh client. One verb that always tries hardest to put
|
|
9
|
+
* you back into that agent: attach the live pane if it still exists, else resume
|
|
10
|
+
* the session; live pane > resumed copy > a clear message about what was lost.
|
|
11
|
+
*
|
|
12
|
+
* It delegates to `focus`, which already does exactly attach-else-resume for a
|
|
13
|
+
* resolved id ({@link focusAction} → `focusResolvedSession`), so there is no second
|
|
14
|
+
* recovery path to keep in sync. What `reconnect` adds is the recovery framing and,
|
|
15
|
+
* with no id, targeting the MOST RECENT session in this shell's directory — the one
|
|
16
|
+
* that most likely just dropped — instead of opening the full fleet picker.
|
|
17
|
+
*/
|
|
18
|
+
import chalk from 'chalk';
|
|
19
|
+
import { focusAction } from './focus.js';
|
|
20
|
+
import { discoverSessions } from '../lib/session/discover.js';
|
|
21
|
+
import { setHelpSections } from '../lib/help.js';
|
|
22
|
+
/**
|
|
23
|
+
* The recency signal a no-id reconnect targets: last activity if the scan
|
|
24
|
+
* computed one, else the creation time. A row with neither parseable sorts last.
|
|
25
|
+
* Pure so target selection is unit-tested without the scanner.
|
|
26
|
+
*/
|
|
27
|
+
export function sessionRecency(s) {
|
|
28
|
+
const t = Date.parse(s.lastActivity ?? s.timestamp ?? '');
|
|
29
|
+
return Number.isFinite(t) ? t : Number.NEGATIVE_INFINITY;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Pick the session to reconnect when the user gave no id: the most recently
|
|
33
|
+
* active one in scope — the terminal that most likely just dropped. Pure; returns
|
|
34
|
+
* undefined for an empty scope so the caller can print guidance.
|
|
35
|
+
*/
|
|
36
|
+
export function pickMostRecentSession(sessions) {
|
|
37
|
+
let best;
|
|
38
|
+
for (const s of sessions) {
|
|
39
|
+
if (!best || sessionRecency(s) > sessionRecency(best))
|
|
40
|
+
best = s;
|
|
41
|
+
}
|
|
42
|
+
return best;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Resolve the no-id target: prefer the most recent session started from THIS
|
|
46
|
+
* directory (the shell the user is reconnecting from), falling back to the most
|
|
47
|
+
* recent session anywhere when the current directory has none. Best-effort — a
|
|
48
|
+
* discovery failure yields undefined, never throws. Injectable `discover` keeps
|
|
49
|
+
* the two-step scope preference testable without touching the real scanner.
|
|
50
|
+
*/
|
|
51
|
+
export async function resolveRecentTarget(cwd, discover = discoverSessions) {
|
|
52
|
+
try {
|
|
53
|
+
const here = await discover({ cwd, since: '7d', limit: 200 });
|
|
54
|
+
const local = pickMostRecentSession(here);
|
|
55
|
+
if (local)
|
|
56
|
+
return local;
|
|
57
|
+
const any = await discover({ all: true, since: '7d', limit: 200 });
|
|
58
|
+
return pickMostRecentSession(any);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export async function reconnectAction(id) {
|
|
65
|
+
if (id) {
|
|
66
|
+
// focus already attaches a live pane or recovers a dropped one for a resolved
|
|
67
|
+
// id — on the origin device for a remote session. Reconnect is that path with
|
|
68
|
+
// a recovery-first name; no reason to duplicate the resolution/attach logic.
|
|
69
|
+
await focusAction(id, {});
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const recent = await resolveRecentTarget(process.cwd());
|
|
73
|
+
if (!recent) {
|
|
74
|
+
console.log(chalk.gray('No recent session to reconnect from here.'));
|
|
75
|
+
console.log(chalk.gray(' reconnect a specific one: agents reconnect <session-id>'));
|
|
76
|
+
console.log(chalk.gray(' or pick from the fleet: agents sessions'));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const where = recent.machine ? ` on ${recent.machine}` : '';
|
|
80
|
+
console.log(chalk.gray(`Reconnecting ${recent.shortId}${where} (most recent here)…`));
|
|
81
|
+
await focusAction(recent.id, {});
|
|
82
|
+
}
|
|
83
|
+
export function registerReconnectCommand(program) {
|
|
84
|
+
const cmd = program
|
|
85
|
+
.command('reconnect')
|
|
86
|
+
.argument('[session-id]', 'Session id/prefix to reconnect (default: the most recent session started here)')
|
|
87
|
+
.description('Re-enter a dropped agent terminal: attach the live pane if it survived, else resume the session')
|
|
88
|
+
.action(async (id) => {
|
|
89
|
+
await reconnectAction(id);
|
|
90
|
+
});
|
|
91
|
+
setHelpSections(cmd, {
|
|
92
|
+
examples: `
|
|
93
|
+
# Reconnect the session that just dropped in this shell (most recent here)
|
|
94
|
+
agents reconnect
|
|
95
|
+
|
|
96
|
+
# Reconnect one specific agent terminal by id/prefix
|
|
97
|
+
agents reconnect 74b13b0b
|
|
98
|
+
|
|
99
|
+
# Same, spelled under the sessions group
|
|
100
|
+
agents sessions reconnect 74b13b0b
|
|
101
|
+
`,
|
|
102
|
+
notes: `
|
|
103
|
+
- Best-effort recovery: a living remote tmux pane is JOINED (a second client, no fork); a dropped one is RESUMED on its origin device (a copy if it was mid-run, a /continue if idle).
|
|
104
|
+
- With no id, targets the most recent session started from this directory (the terminal that most likely just dropped), not the full fleet picker. Falls back to the most recent session anywhere when this directory has none.
|
|
105
|
+
- This is the manual companion to the automatic reconnect that runs during a live 'agents run --device <box>' when the network blinks; use it after that gave up or the terminal tab closed.
|
|
106
|
+
- Related: 'agents sessions focus' (attach/recover with a picker) and 'agents sessions resume' (multi-select history -> tabs).
|
|
107
|
+
`,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
@@ -5,7 +5,18 @@ interface ResumeOptions {
|
|
|
5
5
|
headless?: boolean;
|
|
6
6
|
cwd?: string;
|
|
7
7
|
quiet?: boolean;
|
|
8
|
+
/** Run on THIS machine even when the session belongs to a peer (escape hatch). */
|
|
9
|
+
here?: boolean;
|
|
8
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* The argv to re-run this resume on the machine that owns the session.
|
|
13
|
+
*
|
|
14
|
+
* Deliberately carries no "don't route again" FLAG — that rides the
|
|
15
|
+
* {@link RESUME_PINNED_ENV} export instead, so the hop also works against a peer
|
|
16
|
+
* on an older CLI (see the constant's docs). Every argv token here exists in the
|
|
17
|
+
* released surface.
|
|
18
|
+
*/
|
|
19
|
+
export declare function buildResumeRemoteArgs(sessionId: string, prompt: string | undefined, options: ResumeOptions): string[];
|
|
9
20
|
/** Translate `agents resume <id>` to the canonical `agents run` surface after
|
|
10
21
|
* resolving the source identity. The run command remains the only executor. */
|
|
11
22
|
export declare function buildResumeRunArgs(session: {
|
package/dist/commands/resume.js
CHANGED
|
@@ -3,6 +3,29 @@ import { spawn } from 'child_process';
|
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import { setHelpSections } from '../lib/help.js';
|
|
5
5
|
import { resolveSessionMetadataValue } from './sessions.js';
|
|
6
|
+
import { sessionOwnerDevice, consumeResumePinned, RESUME_PINNED_ENV } from '../lib/session/resume-owner.js';
|
|
7
|
+
/**
|
|
8
|
+
* The argv to re-run this resume on the machine that owns the session.
|
|
9
|
+
*
|
|
10
|
+
* Deliberately carries no "don't route again" FLAG — that rides the
|
|
11
|
+
* {@link RESUME_PINNED_ENV} export instead, so the hop also works against a peer
|
|
12
|
+
* on an older CLI (see the constant's docs). Every argv token here exists in the
|
|
13
|
+
* released surface.
|
|
14
|
+
*/
|
|
15
|
+
export function buildResumeRemoteArgs(sessionId, prompt, options) {
|
|
16
|
+
const args = ['resume', sessionId, ...(prompt === undefined ? [] : [prompt])];
|
|
17
|
+
if (options.mode)
|
|
18
|
+
args.push('--mode', options.mode);
|
|
19
|
+
if (options.interactive)
|
|
20
|
+
args.push('--interactive');
|
|
21
|
+
if (options.headless)
|
|
22
|
+
args.push('--headless');
|
|
23
|
+
if (options.cwd)
|
|
24
|
+
args.push('--cwd', options.cwd);
|
|
25
|
+
if (options.quiet)
|
|
26
|
+
args.push('--quiet');
|
|
27
|
+
return args;
|
|
28
|
+
}
|
|
6
29
|
/** Translate `agents resume <id>` to the canonical `agents run` surface after
|
|
7
30
|
* resolving the source identity. The run command remains the only executor. */
|
|
8
31
|
export function buildResumeRunArgs(session, prompt, options) {
|
|
@@ -29,7 +52,11 @@ export function registerResumeCommand(program) {
|
|
|
29
52
|
.option('--headless', 'Resume headlessly (a prompt is required)')
|
|
30
53
|
.option('--cwd <path>', 'Override the recorded working directory')
|
|
31
54
|
.option('-q, --quiet', 'Suppress routing banners')
|
|
55
|
+
.option('--here', 'Run on this machine even if the session belongs to another device')
|
|
32
56
|
.action(async (sessionId, prompt, options) => {
|
|
57
|
+
// Read (and clear) the routing pin before anything else, so it can never
|
|
58
|
+
// reach the agent's own children.
|
|
59
|
+
const pinnedHere = consumeResumePinned() || !!options.here;
|
|
33
60
|
const outcome = await resolveSessionMetadataValue(sessionId.trim());
|
|
34
61
|
if (outcome.kind === 'partial') {
|
|
35
62
|
console.error(chalk.red(`Could not resolve session while these devices were unavailable: ${outcome.failedPeers.join(', ')}`));
|
|
@@ -46,6 +73,29 @@ export function registerResumeCommand(program) {
|
|
|
46
73
|
process.exitCode = 1;
|
|
47
74
|
return;
|
|
48
75
|
}
|
|
76
|
+
// The harness keeps its conversation state on the machine that produced
|
|
77
|
+
// the session, so a peer-owned session MUST resume there. Running it here
|
|
78
|
+
// starts the agent against state this box does not have — silently, since
|
|
79
|
+
// a synced mirror makes the transcript look local (RUSH-2022).
|
|
80
|
+
const owner = pinnedHere ? undefined : sessionOwnerDevice(outcome.session);
|
|
81
|
+
if (owner) {
|
|
82
|
+
if (!options.quiet) {
|
|
83
|
+
process.stderr.write(chalk.gray(`[agents] session ${outcome.session.shortId} belongs to ${owner} → resuming there\n`));
|
|
84
|
+
}
|
|
85
|
+
// `runOnPeer` is the existing transport for "this session's transcript
|
|
86
|
+
// and agent binary are on that box" (lib/session/remote-list.ts) — the
|
|
87
|
+
// same one the picker already uses. Not the `--host` passthrough: that
|
|
88
|
+
// one re-discovers locally and marks the run AGENTS_FLEET_REMOTE, which
|
|
89
|
+
// a long-lived resumed session must not inherit.
|
|
90
|
+
const { runOnPeer } = await import('../lib/session/remote-list.js');
|
|
91
|
+
const rc = await runOnPeer(buildResumeRemoteArgs(outcome.session.id, prompt, options), owner, { tty: !!process.stdout.isTTY, env: { [RESUME_PINNED_ENV]: '1' } });
|
|
92
|
+
if (rc === 'no-target') {
|
|
93
|
+
console.error(chalk.red(`Session ${outcome.session.shortId} lives on ${owner}, which isn't a reachable device right now.`));
|
|
94
|
+
console.error(chalk.gray(`Register/wake it (agents devices), or run there: agents ssh ${owner}`));
|
|
95
|
+
process.exitCode = 1;
|
|
96
|
+
}
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
49
99
|
const args = buildResumeRunArgs(outcome.session, prompt, options);
|
|
50
100
|
const child = spawn(process.execPath, [process.argv[1], ...args], {
|
|
51
101
|
stdio: 'inherit',
|
|
@@ -82,6 +132,7 @@ export function registerResumeCommand(program) {
|
|
|
82
132
|
agents resume 019fd0c8-b3e9-77a2-a1a4-444698c4d897 --mode edit`,
|
|
83
133
|
notes: `
|
|
84
134
|
A full ID resolves from the local session database first (zero SSH) and, on a local miss, fans out with the first peer holding it cancelling the rest. An exact label always consults the fleet (labels are not globally unique) and auto-resumes the one match; a cross-machine label collision surfaces as an ambiguity.
|
|
135
|
+
A session that ran on another device resumes ON that device over SSH — the harness's conversation state lives there, so running it here would start against state this machine does not have. --here overrides that and runs locally.
|
|
85
136
|
Use agents run auto --resume <id> when the original account is unavailable and another harness may continue.`,
|
|
86
137
|
});
|
|
87
138
|
}
|
|
@@ -12,7 +12,7 @@ import * as path from 'path';
|
|
|
12
12
|
import * as yaml from 'yaml';
|
|
13
13
|
import { isDaemonRunning, signalDaemonReload, startDaemon, stopDaemon, readDaemonLog, getDaemonStatus, } from '../lib/daemon.js';
|
|
14
14
|
import { assertSchedulerEnabled } from '../lib/device-config.js';
|
|
15
|
-
import { resolveAgentName, isAgentHardDeprecated, hardDeprecationError } from '../lib/agents.js';
|
|
15
|
+
import { resolveAgentName, isAgentHardDeprecated, hardDeprecationError, ROUTINE_AGENT_IDS } from '../lib/agents.js';
|
|
16
16
|
import { humanizeCron, humanizeNextRun, formatRepoLink, REPO_DISPLAY_MAX } from '../lib/routines-format.js';
|
|
17
17
|
import { listJobs as listAllJobs, deleteJob, readJob, validateJob, writeJob, setJobEnabled, listRuns, routineStats, getLatestRun, getRunDir, getJobPath, parseAtTime, hasCompletedOneShotRun, isOneShotLikeSchedule, isOneShotRoutine, isPastOneShotRoutine, jobRunsOnThisDevice, checkJobDeviceEligibility, normalizeTriggerEvent, parseHostStrategy, resolveHostStrategy, HOST_STRATEGIES, computeProjectGroup, computeProjectGroupKind, projectGroupKey, projectGroupTitle, projectGroupOrder, normalizeProjects, } from '../lib/routines.js';
|
|
18
18
|
import { listProjectDefs, isSafeProjectName } from '../lib/projects.js';
|
|
@@ -21,7 +21,7 @@ import { fireWebhookJobs, matchJobsToWebhook } from '../lib/triggers/webhook.js'
|
|
|
21
21
|
import { getRoutinesDir } from '../lib/state.js';
|
|
22
22
|
import { IS_WINDOWS } from '../lib/platform/index.js';
|
|
23
23
|
import { safeJoin } from '../lib/paths.js';
|
|
24
|
-
import { executeJob, monitorRunningJobs
|
|
24
|
+
import { executeJob, monitorRunningJobs } from '../lib/runner.js';
|
|
25
25
|
import { JobScheduler } from '../lib/scheduler.js';
|
|
26
26
|
import { detectOverdueJobs } from '../lib/overdue.js';
|
|
27
27
|
import { runCatchup } from '../lib/catchup.js';
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
import { type Command } from 'commander';
|
|
9
9
|
import { SSH_TARGET_RE, assertValidSshTarget } from '../lib/ssh-exec.js';
|
|
10
10
|
import { quoteWin32ExecArg } from '../lib/platform/index.js';
|
|
11
|
+
import { bundleEnvToDotenv } from '../lib/secrets/push.js';
|
|
12
|
+
export { bundleEnvToDotenv };
|
|
11
13
|
import { type SecretsBackend, type SecretsBundle, type SecretsPolicy } from '../lib/secrets/bundles.js';
|
|
12
14
|
import { type SecretsListFilterOpts } from '../lib/secrets/list-filter.js';
|
|
13
15
|
import { type BundleUsageSummary } from '../lib/secrets/usage-db.js';
|
|
@@ -78,14 +80,6 @@ export { SSH_TARGET_RE, assertValidSshTarget };
|
|
|
78
80
|
* the executed command.
|
|
79
81
|
*/
|
|
80
82
|
export declare function buildSecretsExecEnv(parentEnv: NodeJS.ProcessEnv, secretEnv: Record<string, string>): NodeJS.ProcessEnv;
|
|
81
|
-
/**
|
|
82
|
-
* Serialize a resolved env map to `.env` lines that round-trip losslessly through
|
|
83
|
-
* `parseDotenv` on the remote: `KEY="VALUE"`. parseDotenv strips exactly one outer
|
|
84
|
-
* quote pair and takes the inner bytes verbatim (no unescaping), so any single-line
|
|
85
|
-
* value survives unchanged with no escaping. Newlines would break its line-based
|
|
86
|
-
* parse, so multi-line values are rejected rather than silently corrupted.
|
|
87
|
-
*/
|
|
88
|
-
export declare function bundleEnvToDotenv(env: Record<string, string>): string;
|
|
89
83
|
/**
|
|
90
84
|
* Encrypt a resolved env map to an offline bundle file using AES-256-GCM
|
|
91
85
|
* (the same EncFile envelope as the per-item file store). Inner plaintext is
|
package/dist/commands/secrets.js
CHANGED
|
@@ -12,12 +12,12 @@ import { terminalWidth, truncateToWidth, stringWidth } from '../lib/session/widt
|
|
|
12
12
|
import * as fs from 'fs';
|
|
13
13
|
import * as os from 'os';
|
|
14
14
|
import * as path from 'path';
|
|
15
|
-
import { SSH_TARGET_RE, assertValidSshTarget
|
|
15
|
+
import { SSH_TARGET_RE, assertValidSshTarget } from '../lib/ssh-exec.js';
|
|
16
16
|
import { quoteWin32ExecArg, composeWin32CommandLine } from '../lib/platform/index.js';
|
|
17
17
|
import { ensureDaemonStarted, isDaemonRunning } from '../lib/daemon.js';
|
|
18
|
-
import { parseHostsOption, remoteResolveEnv, remoteSecretsRaw, remoteSecretsStream, resolveHostSshTarget,
|
|
19
|
-
import {
|
|
20
|
-
|
|
18
|
+
import { parseHostsOption, remoteResolveEnv, remoteSecretsRaw, remoteSecretsStream, resolveHostSshTarget, } from '../lib/secrets/remote.js';
|
|
19
|
+
import { resolveBundleForPush, pushResolvedBundleToHost, bundleEnvToDotenv } from '../lib/secrets/push.js';
|
|
20
|
+
export { bundleEnvToDotenv };
|
|
21
21
|
import { bundleBackend, bundleExists, bundleItemStore, bundlePolicy, deleteBundle, describeBundle, keychainItemsForBundle, keychainRef, listBundles, healKeychainBundleMetadataAclOnce, isHeadlessSecretsContext, migrateLegacyBundles, parseDotenv, readAndResolveBundleEnv, readBundle, readBundleIfDecryptable, reAclBundleItems, renameBundle, rotateBundleSecret, sanitizeProcessEnv, validateBundleName, validateEnvKey, validateExpiresFutureDated, validateSecretType, writeBundle, writeBundleWithItems, SECRET_TYPES, } from '../lib/secrets/bundles.js';
|
|
22
22
|
import { parseListFilters, bundleMatchesFilter, bundleExpiry, filterIsActive, describeFilter, parseSortField, sortBundles, SORT_FIELDS, REF_KINDS, DEFAULT_EXPIRING_DAYS, } from '../lib/secrets/list-filter.js';
|
|
23
23
|
import { encryptForFallback, decryptForFallback } from '../lib/secrets/filestore.js';
|
|
@@ -354,24 +354,6 @@ function getCliVersion() {
|
|
|
354
354
|
return '0.0.0';
|
|
355
355
|
}
|
|
356
356
|
}
|
|
357
|
-
/**
|
|
358
|
-
* Serialize a resolved env map to `.env` lines that round-trip losslessly through
|
|
359
|
-
* `parseDotenv` on the remote: `KEY="VALUE"`. parseDotenv strips exactly one outer
|
|
360
|
-
* quote pair and takes the inner bytes verbatim (no unescaping), so any single-line
|
|
361
|
-
* value survives unchanged with no escaping. Newlines would break its line-based
|
|
362
|
-
* parse, so multi-line values are rejected rather than silently corrupted.
|
|
363
|
-
*/
|
|
364
|
-
export function bundleEnvToDotenv(env) {
|
|
365
|
-
const lines = [];
|
|
366
|
-
for (const [k, v] of Object.entries(env)) {
|
|
367
|
-
if (/[\r\n]/.test(v)) {
|
|
368
|
-
throw new Error(`Key '${k}' has a multi-line value; the SSH .env transport can't carry newlines. ` +
|
|
369
|
-
`Set it directly on the remote with 'agents secrets add ${k} --value-stdin'.`);
|
|
370
|
-
}
|
|
371
|
-
lines.push(`${k}="${v}"`);
|
|
372
|
-
}
|
|
373
|
-
return lines.join('\n') + '\n';
|
|
374
|
-
}
|
|
375
357
|
/**
|
|
376
358
|
* Encrypt a resolved env map to an offline bundle file using AES-256-GCM
|
|
377
359
|
* (the same EncFile envelope as the per-item file store). Inner plaintext is
|
|
@@ -2115,100 +2097,42 @@ Examples:
|
|
|
2115
2097
|
if (hosts.length > 0) {
|
|
2116
2098
|
for (const h of hosts)
|
|
2117
2099
|
assertValidSshTarget(h);
|
|
2118
|
-
const
|
|
2100
|
+
const parsedBackend = parseBackendOpt(opts.remoteBackend);
|
|
2101
|
+
// `--remote-backend` documents keychain|file only, and parseBackendOpt
|
|
2102
|
+
// exits on anything else — but its return type still admits 'vault',
|
|
2103
|
+
// which has no remote-push path. Refuse it here rather than let it fall
|
|
2104
|
+
// through to the keychain branch and half-work.
|
|
2105
|
+
if (parsedBackend === 'vault') {
|
|
2106
|
+
console.error(chalk.red("--remote-backend vault is not supported; use 'keychain' or 'file'."));
|
|
2107
|
+
process.exit(1);
|
|
2108
|
+
}
|
|
2109
|
+
const remoteBackend = parsedBackend;
|
|
2119
2110
|
// For a file-backed remote bundle a passphrase is OPTIONAL. The file
|
|
2120
2111
|
// store is passphrase-free by default: with AGENTS_SECRETS_PASSPHRASE
|
|
2121
2112
|
// unset the remote `import --backend file` auto-provisions the remote's
|
|
2122
2113
|
// own machine-local key (0600 under ~/.agents/.secrets-key/), so reads
|
|
2123
2114
|
// are HEADLESS. We forward the LOCAL AGENTS_SECRETS_PASSPHRASE only when
|
|
2124
|
-
// the operator opts in by setting it
|
|
2125
|
-
//
|
|
2126
|
-
// lands in argv / `ps` / the remote shell history. Forcing a shared
|
|
2127
|
-
// passphrase would defeat headless reads, so we no longer require one.
|
|
2115
|
+
// the operator opts in by setting it. Forcing a shared passphrase would
|
|
2116
|
+
// defeat headless reads, so we no longer require one.
|
|
2128
2117
|
const remotePassphrase = remoteBackend === 'file' ? (process.env.AGENTS_SECRETS_PASSPHRASE ?? '') : '';
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
const
|
|
2132
|
-
|
|
2133
|
-
// land in its chosen backend, reading the .env off ssh stdin (never
|
|
2134
|
-
// parsed by a remote shell — `--from -` replaces the POSIX-only
|
|
2135
|
-
// `/dev/stdin`). The keychain path is built OS-aware via
|
|
2136
|
-
// `remoteSecretsRaw` (bash -lc on POSIX, PowerShell on Windows), so it
|
|
2137
|
-
// works on macOS, Linux AND Windows targets. `import` auto-creates the
|
|
2138
|
-
// bundle, so no separate `create` (the old `|| true` was a POSIXism
|
|
2139
|
-
// that broke on PowerShell: `'true' is not recognized`).
|
|
2118
|
+
// Resolve ONCE for N hosts — reading a bundle can prompt, and doing it
|
|
2119
|
+
// per host would prompt per host.
|
|
2120
|
+
const resolvedForPush = resolveBundleForPush(resolvedBundleName, 'ssh export');
|
|
2121
|
+
const keyCount = resolvedForPush.keyCount;
|
|
2140
2122
|
let failures = 0;
|
|
2141
2123
|
for (const host of hosts) {
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
// emit broken PowerShell.
|
|
2150
|
-
if (remoteShellFor(resolveRemoteOsSync(host.split('@').pop() ?? host)) === 'powershell') {
|
|
2151
|
-
failures++;
|
|
2152
|
-
console.error(chalk.red(`${host}: file backend export to a Windows target is not yet supported.`));
|
|
2153
|
-
continue;
|
|
2154
|
-
}
|
|
2155
|
-
const { remoteCmd, input } = buildRemoteFileImportCommand(resolvedBundleName, dotenv, {
|
|
2156
|
-
passphrase: remotePassphrase,
|
|
2157
|
-
force: opts.force,
|
|
2158
|
-
});
|
|
2159
|
-
res = sshExec(host, remoteCmd, { input });
|
|
2160
|
-
}
|
|
2161
|
-
else if (remoteShellFor(resolveRemoteOsSync(host.split('@').pop() ?? host)) === 'powershell') {
|
|
2162
|
-
// Keychain on a Windows target: the `agents.ps1` shim doesn't
|
|
2163
|
-
// forward ssh-piped stdin to node, so `--from -` would hang.
|
|
2164
|
-
// Bridge the piped .env through PowerShell into a temp file and
|
|
2165
|
-
// import `--from <file>` (deleted afterwards). Same hardened ssh
|
|
2166
|
-
// engine, .env still only ever crosses the wire over ssh stdin.
|
|
2167
|
-
res = sshExec(host, buildWindowsStdinImportCommand(resolvedBundleName, { force: opts.force }), { input: dotenv });
|
|
2168
|
-
}
|
|
2169
|
-
else {
|
|
2170
|
-
// Keychain on a POSIX target: OS-aware wrapping + hardened ssh
|
|
2171
|
-
// engine (BatchMode, ConnectTimeout, keepalive, control-socket
|
|
2172
|
-
// reuse) via the same path the READ inverse (`remoteResolveEnv`)
|
|
2173
|
-
// uses. `--from -` reads the .env off ssh stdin.
|
|
2174
|
-
res = remoteSecretsRaw(host, ['import', resolvedBundleName, '--from', '-', ...(opts.force ? ['--force'] : [])], { input: dotenv, osLookupName: host });
|
|
2175
|
-
}
|
|
2176
|
-
if (res.code === null) {
|
|
2177
|
-
failures++;
|
|
2178
|
-
console.error(chalk.red(`${host}: ${res.stderr.trim() || (res.timedOut ? 'ssh timed out' : 'ssh failed')}`));
|
|
2179
|
-
continue;
|
|
2180
|
-
}
|
|
2181
|
-
if (res.code !== 0) {
|
|
2124
|
+
const out = pushResolvedBundleToHost(resolvedForPush, resolvedBundleName, host, {
|
|
2125
|
+
remoteBackend,
|
|
2126
|
+
force: opts.force,
|
|
2127
|
+
passphrase: remotePassphrase,
|
|
2128
|
+
operation: 'ssh export',
|
|
2129
|
+
});
|
|
2130
|
+
if (!out.ok) {
|
|
2182
2131
|
failures++;
|
|
2183
|
-
|
|
2184
|
-
console.error(chalk.red(`${host}: remote import failed (exit ${res.code})${msg ? `: ${msg}` : ''}`));
|
|
2132
|
+
console.error(chalk.red(`${host}: ${out.message}`));
|
|
2185
2133
|
continue;
|
|
2186
2134
|
}
|
|
2187
|
-
|
|
2188
|
-
// the bundle metadata but no READABLE value items: the remote login
|
|
2189
|
-
// keychain is locked in the non-interactive SSH context, so Security
|
|
2190
|
-
// accepts the write but the biometry-ACL'd item is unreadable, and the
|
|
2191
|
-
// remote `import` still exits 0. Read the bundle back the same way a
|
|
2192
|
-
// release will (drops the plaintext, keeps only key presence; the
|
|
2193
|
-
// remote's headless `agentOnly` guard fails fast, so no Touch ID) and
|
|
2194
|
-
// FAIL LOUDLY if the keys didn't materialize — rather than leave a
|
|
2195
|
-
// metadata-only bundle that breaks later with "stored item not found".
|
|
2196
|
-
// The file backend is headless-readable by construction, so skip it.
|
|
2197
|
-
if (remoteBackend === 'keychain') {
|
|
2198
|
-
const verdict = verifyRemoteKeychainPush(host, resolvedBundleName, Object.keys(env), { osLookupName: host });
|
|
2199
|
-
if (!verdict.ok) {
|
|
2200
|
-
failures++;
|
|
2201
|
-
if (verdict.kind === 'locked-keychain') {
|
|
2202
|
-
console.error(chalk.red(keychainWriteFailureMessage(host, resolvedBundleName, verdict.reason)));
|
|
2203
|
-
}
|
|
2204
|
-
else {
|
|
2205
|
-
console.error(chalk.red(`${host}: pushed '${resolvedBundleName}' but could not verify it on the remote: ${verdict.reason}`));
|
|
2206
|
-
}
|
|
2207
|
-
continue;
|
|
2208
|
-
}
|
|
2209
|
-
}
|
|
2210
|
-
const remoteMsg = (res.stdout || '').trim().split('\n').map((l) => l.trim()).filter(Boolean).pop();
|
|
2211
|
-
console.log(chalk.green(`${host} -> '${resolvedBundleName}': ${remoteMsg || `${keyCount} key(s) exported`}`));
|
|
2135
|
+
console.log(chalk.green(`${host} -> '${resolvedBundleName}': ${out.message}`));
|
|
2212
2136
|
}
|
|
2213
2137
|
emitSecretAudit({ event: 'secrets.export', bundle: resolvedBundleName, operation: `export --host ${hosts.join(',')}`, source: 'ssh', host: hosts.join(','), status: failures > 0 ? 'error' : 'success', keyCount });
|
|
2214
2138
|
if (failures > 0)
|
|
@@ -19,12 +19,17 @@ import { renderMarkdown } from '../lib/markdown.js';
|
|
|
19
19
|
import { itemPicker } from '../lib/picker.js';
|
|
20
20
|
import { classifyFileChanges, changeCounts, toolHistogram, detectTestResult } from '../lib/session/digest.js';
|
|
21
21
|
import { extractArtifacts, extractHooks, extractLinks, extractRepos, extractSkills } from '../lib/session/highlights.js';
|
|
22
|
-
/** A session whose transcript
|
|
23
|
-
* cross-machine fan-out): its `filePath` is on that peer's disk, so the
|
|
24
|
-
* can't parse it locally — it shows metadata + a "resume there" note
|
|
25
|
-
* Keys off `_remote`, not the machine tag, so locally-readable synced
|
|
26
|
-
* still parse their file normally.
|
|
27
|
-
|
|
22
|
+
/** A session whose transcript FILE is on another machine (folded in over the
|
|
23
|
+
* live cross-machine fan-out): its `filePath` is on that peer's disk, so the
|
|
24
|
+
* preview can't parse it locally — it shows metadata + a "resume there" note
|
|
25
|
+
* instead. Keys off `_remote`, not the machine tag, so locally-readable synced
|
|
26
|
+
* mirrors still parse their file normally.
|
|
27
|
+
*
|
|
28
|
+
* This is the READ rule and only the read rule. Whether a session may be
|
|
29
|
+
* RESUMED here is a different question with a different answer — a mirror is
|
|
30
|
+
* readable but not resumable — and `sessionOwnerDevice`
|
|
31
|
+
* (lib/session/resume-owner.ts) is the single place that answers it (RUSH-2022). */
|
|
32
|
+
function transcriptOnPeerOf(session) {
|
|
28
33
|
return session._remote ? session.machine : undefined;
|
|
29
34
|
}
|
|
30
35
|
/**
|
|
@@ -98,7 +103,7 @@ function sanitizeMeta(s) {
|
|
|
98
103
|
const previewCache = new Map();
|
|
99
104
|
/** Build a cached multi-line preview string for display in the session picker. */
|
|
100
105
|
export function buildPreview(session) {
|
|
101
|
-
const remote =
|
|
106
|
+
const remote = transcriptOnPeerOf(session);
|
|
102
107
|
const cacheKey = remote ? `${remote}:${session.id}` : session.id;
|
|
103
108
|
const cached = previewCache.get(cacheKey);
|
|
104
109
|
if (cached)
|
|
@@ -11,7 +11,7 @@ interface SessionFilterOptions {
|
|
|
11
11
|
all?: boolean;
|
|
12
12
|
teams?: boolean;
|
|
13
13
|
inTeam?: string;
|
|
14
|
-
routine?: boolean;
|
|
14
|
+
routine?: boolean | string;
|
|
15
15
|
since?: string;
|
|
16
16
|
until?: string;
|
|
17
17
|
}
|
|
@@ -389,6 +389,22 @@ export declare function isBareBrowserListing(options: SessionsOptions, query: st
|
|
|
389
389
|
* `runSessionBrowser` picker cannot represent.
|
|
390
390
|
*/
|
|
391
391
|
export declare function hasNoBrowserDisqualifyingFlags(options: SessionsOptions, query: string | undefined): boolean;
|
|
392
|
+
/** Resolve a typed routine selector by exact name, substring, or one unambiguous typo. */
|
|
393
|
+
export declare function resolveRoutineName(query: string, names: readonly string[]): string | null;
|
|
394
|
+
export interface RoutineRunGroup {
|
|
395
|
+
runId: string;
|
|
396
|
+
timestamp: string;
|
|
397
|
+
sessions: SessionMeta[];
|
|
398
|
+
}
|
|
399
|
+
export interface RoutineChoice {
|
|
400
|
+
name: string;
|
|
401
|
+
lastRunAt: string;
|
|
402
|
+
runCount: number;
|
|
403
|
+
latestRunSessionCount: number;
|
|
404
|
+
}
|
|
405
|
+
export declare function buildRoutineRunGroups(sessions: SessionMeta[]): RoutineRunGroup[];
|
|
406
|
+
export declare function buildRoutineChoices(sessions: SessionMeta[]): RoutineChoice[];
|
|
407
|
+
export declare function filterSessionsByRoutine(sessions: SessionMeta[], routine: boolean | string, interactive: boolean): Promise<SessionMeta[] | null>;
|
|
392
408
|
/** Resolve a session by id/query globally and print its compact preview (no pager).
|
|
393
409
|
* Backs `--preview` — the fast path for the "peek before resume" hot loop. */
|
|
394
410
|
export declare function renderSessionPreview(query: string, scope: {
|
|
@@ -478,6 +494,10 @@ export declare function buildOverviewGroups(pool: SessionMeta[], perProjectCap:
|
|
|
478
494
|
groups: OverviewGroup[];
|
|
479
495
|
projectCount: number;
|
|
480
496
|
};
|
|
497
|
+
export declare function printRoutineRunOverview(sessions: SessionMeta[], liveIndex: Map<string, ActiveSession> | undefined, opts?: {
|
|
498
|
+
hiddenCount?: number;
|
|
499
|
+
hiddenUnmanaged?: number;
|
|
500
|
+
}): void;
|
|
481
501
|
/**
|
|
482
502
|
* Render a resolved session to stdout — the non-follow view behind
|
|
483
503
|
* `agents logs <sessionId>`. Defaults to the concise `summary` digest (same as
|
|
@@ -577,6 +597,16 @@ export declare function pickSessionInteractive(sessions: SessionMeta[], message?
|
|
|
577
597
|
export declare const LIVE_ROW_PREFIX = "live:";
|
|
578
598
|
export declare function isIdlessLiveRow(s: SessionMeta): boolean;
|
|
579
599
|
export declare function handlePickedSession(picked: PickedSession): Promise<void>;
|
|
600
|
+
/**
|
|
601
|
+
* Resume `session` on the machine that owns it, when that is not this one.
|
|
602
|
+
* Returns true when it handled the resume (hopped, or reported an unreachable
|
|
603
|
+
* owner), false when the session is local and the caller should take over here.
|
|
604
|
+
*
|
|
605
|
+
* The one hop for a foreground, one-session-at-a-time resume — shared by the
|
|
606
|
+
* `agents sessions` picker and by `sessions resume`'s no-tab-backend path, which
|
|
607
|
+
* would otherwise reach `resumeSessionInPlace` and be refused (RUSH-2022).
|
|
608
|
+
*/
|
|
609
|
+
export declare function resumeOnOwnerIfRemote(session: SessionMeta): Promise<boolean>;
|
|
580
610
|
/**
|
|
581
611
|
* Resume a session in the current terminal — a foreground takeover of this
|
|
582
612
|
* process. Used by the single-select picker and by `sessions resume` when the
|
|
@@ -647,6 +677,22 @@ export declare function resolveSessionQuery(pool: SessionMeta[], query: string,
|
|
|
647
677
|
export declare function fleetNotFoundMessage(query: string, deviceCount: number, unreachable: string[]): string[];
|
|
648
678
|
/** Filter and rank sessions by a multi-term search query across metadata and content. */
|
|
649
679
|
export declare function filterSessionsByQuery(sessions: SessionMeta[], query: string | undefined): SessionMeta[];
|
|
680
|
+
/**
|
|
681
|
+
* Narrow a session list by --project and --agent before search resolution.
|
|
682
|
+
* Without this, a query like "scoped search" could match sessions in BOTH
|
|
683
|
+
* the project you specified AND elsewhere, producing an ambiguity error
|
|
684
|
+
* even though the user already pointed at the correct scope.
|
|
685
|
+
*/
|
|
686
|
+
export declare function applyScopeFilters(sessions: SessionMeta[], scope: {
|
|
687
|
+
agent?: string;
|
|
688
|
+
project?: string;
|
|
689
|
+
routine?: boolean | string;
|
|
690
|
+
}): SessionMeta[];
|
|
691
|
+
export declare function artifactLookupScope(agent?: string, project?: string, routine?: boolean | string): {
|
|
692
|
+
agent?: string;
|
|
693
|
+
project?: string;
|
|
694
|
+
routine?: boolean | string;
|
|
695
|
+
};
|
|
650
696
|
/**
|
|
651
697
|
* Whether a missed local id lookup should widen to a cross-machine sweep.
|
|
652
698
|
*
|