@phnx-labs/agents-cli 1.22.23 → 1.22.25
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 +359 -0
- package/README.md +19 -8
- package/dist/bin/agents +0 -0
- package/dist/commands/attach.d.ts +2 -0
- package/dist/commands/attach.js +32 -7
- package/dist/commands/cloud.js +9 -5
- package/dist/commands/defaults.js +2 -0
- package/dist/commands/doctor.d.ts +24 -0
- package/dist/commands/doctor.js +105 -9
- package/dist/commands/exec.js +99 -44
- package/dist/commands/feed.d.ts +23 -0
- package/dist/commands/feed.js +65 -2
- package/dist/commands/focus.d.ts +34 -3
- package/dist/commands/focus.js +301 -57
- package/dist/commands/go.d.ts +14 -1
- package/dist/commands/go.js +49 -5
- package/dist/commands/insights.js +6 -2
- package/dist/commands/inspect.js +39 -5
- package/dist/commands/menubar.js +14 -1
- package/dist/commands/models.js +1 -0
- package/dist/commands/modes.d.ts +12 -0
- package/dist/commands/modes.js +147 -0
- package/dist/commands/routines.d.ts +3 -0
- package/dist/commands/routines.js +70 -47
- package/dist/commands/run-cloud.js +1 -1
- package/dist/commands/secrets-sync.js +11 -13
- package/dist/commands/secrets.d.ts +2 -0
- package/dist/commands/secrets.js +54 -17
- package/dist/commands/sessions-browser.d.ts +36 -1
- package/dist/commands/sessions-browser.js +165 -19
- package/dist/commands/sessions-resume.d.ts +2 -0
- package/dist/commands/sessions-resume.js +28 -12
- package/dist/commands/sessions.d.ts +23 -1
- package/dist/commands/sessions.js +58 -61
- package/dist/commands/setup-secrets.js +1 -1
- package/dist/commands/setup-watchdog.js +5 -10
- package/dist/commands/setup.js +1 -1
- package/dist/commands/sync.js +246 -42
- package/dist/commands/teams.js +3 -2
- package/dist/commands/view.js +2 -0
- package/dist/commands/watchdog.d.ts +3 -4
- package/dist/commands/watchdog.js +26 -66
- package/dist/index.js +38 -2
- package/dist/lib/agent-modes.d.ts +49 -0
- package/dist/lib/agent-modes.js +70 -0
- package/dist/lib/agents.js +126 -21
- package/dist/lib/cloud/cursor.d.ts +79 -0
- package/dist/lib/cloud/cursor.js +228 -0
- package/dist/lib/cloud/registry.js +2 -0
- package/dist/lib/cloud/types.d.ts +7 -2
- package/dist/lib/cloud/types.js +14 -0
- package/dist/lib/crabbox/cli.d.ts +2 -2
- package/dist/lib/crabbox/config.d.ts +7 -8
- package/dist/lib/crabbox/config.js +14 -14
- package/dist/lib/crabbox/lease.d.ts +11 -4
- package/dist/lib/crabbox/lease.js +40 -8
- package/dist/lib/crabbox/setup-copy.d.ts +5 -0
- package/dist/lib/crabbox/setup-copy.js +17 -1
- package/dist/lib/daemon.js +27 -1
- package/dist/lib/device-config.js +7 -0
- package/dist/lib/devices/doctor-findings.d.ts +7 -1
- package/dist/lib/devices/doctor-findings.js +40 -1
- package/dist/lib/doctor-diff.d.ts +3 -0
- package/dist/lib/doctor-diff.js +15 -13
- package/dist/lib/event-stream.d.ts +3 -1
- package/dist/lib/event-stream.js +14 -1
- package/dist/lib/events.d.ts +9 -0
- package/dist/lib/events.js +58 -0
- package/dist/lib/exec.d.ts +15 -3
- package/dist/lib/exec.js +112 -19
- package/dist/lib/feed-outcome.d.ts +3 -0
- package/dist/lib/feed-outcome.js +18 -10
- package/dist/lib/feed.d.ts +4 -0
- package/dist/lib/hooks/cache.js +36 -3
- package/dist/lib/hooks.d.ts +19 -7
- package/dist/lib/hooks.js +88 -38
- package/dist/lib/hosts/passthrough.d.ts +21 -0
- package/dist/lib/hosts/passthrough.js +39 -12
- package/dist/lib/hosts/session-index.d.ts +4 -0
- package/dist/lib/hosts/session-index.js +7 -0
- package/dist/lib/manifest.d.ts +12 -2
- package/dist/lib/manifest.js +60 -5
- package/dist/lib/mcp.js +5 -1
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +5 -1
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +15 -2
- package/dist/lib/menubar/install-menubar.d.ts +52 -2
- package/dist/lib/menubar/install-menubar.js +128 -6
- package/dist/lib/menubar/snapshot.d.ts +15 -0
- package/dist/lib/menubar/snapshot.js +40 -0
- package/dist/lib/plugins.js +13 -1
- package/dist/lib/refresh.d.ts +5 -0
- package/dist/lib/refresh.js +37 -33
- package/dist/lib/resource-inventory.d.ts +79 -0
- package/dist/lib/resource-inventory.js +122 -0
- package/dist/lib/resources/mcp.js +3 -0
- package/dist/lib/resources.js +8 -5
- package/dist/lib/routine-process-cleanup.d.ts +9 -0
- package/dist/lib/routine-process-cleanup.js +73 -0
- package/dist/lib/run-defaults.d.ts +2 -0
- package/dist/lib/run-defaults.js +23 -2
- package/dist/lib/runner.js +27 -22
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +2 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +13 -1
- package/dist/lib/secrets/bundles.js +158 -85
- package/dist/lib/secrets/filestore.d.ts +7 -3
- package/dist/lib/secrets/filestore.js +25 -8
- package/dist/lib/secrets/index.js +34 -8
- package/dist/lib/secrets/rc-hygiene.d.ts +14 -0
- package/dist/lib/secrets/rc-hygiene.js +14 -1
- package/dist/lib/secrets/reaper.d.ts +10 -0
- package/dist/lib/secrets/reaper.js +50 -15
- package/dist/lib/secrets/sync-passphrase.d.ts +27 -0
- package/dist/lib/secrets/sync-passphrase.js +78 -0
- package/dist/lib/session/active.d.ts +4 -0
- package/dist/lib/session/db.js +8 -2
- package/dist/lib/session/recovery.d.ts +37 -0
- package/dist/lib/session/recovery.js +95 -0
- package/dist/lib/session/remote-list.d.ts +2 -0
- package/dist/lib/session/remote-list.js +1 -0
- package/dist/lib/session/session-cache.d.ts +4 -4
- package/dist/lib/session/session-cache.js +4 -4
- package/dist/lib/shims.d.ts +2 -2
- package/dist/lib/shims.js +45 -6
- package/dist/lib/signin-badge.js +2 -0
- package/dist/lib/startup/command-registry.d.ts +16 -0
- package/dist/lib/startup/command-registry.js +44 -0
- package/dist/lib/sync-umbrella.d.ts +5 -0
- package/dist/lib/sync-umbrella.js +5 -4
- package/dist/lib/teams/agents.js +1 -1
- package/dist/lib/teams/parsers.d.ts +1 -1
- package/dist/lib/tmux/session.d.ts +4 -2
- package/dist/lib/tmux/session.js +5 -5
- package/dist/lib/types.d.ts +3 -1
- package/dist/lib/versions.js +16 -1
- package/dist/lib/watchdog/service.d.ts +17 -0
- package/dist/lib/watchdog/service.js +39 -0
- package/package.json +1 -1
|
@@ -14,14 +14,16 @@ import { discoverSessions } from '../lib/session/discover.js';
|
|
|
14
14
|
import { filterTeamSessions } from '../lib/session/team-filter.js';
|
|
15
15
|
import { multiItemPicker, itemPicker } from '../lib/picker.js';
|
|
16
16
|
import { buildPreview } from './sessions-picker.js';
|
|
17
|
-
import {
|
|
17
|
+
import { formatPickerLabel, pickerColumnsFor, buildSessionRecoveryCommand, resumeSessionInPlace, parseAgentFilter, } from './sessions.js';
|
|
18
|
+
import { sessionMatchesQuery } from './sessions-browser.js';
|
|
18
19
|
import { openSurfaces, availableBackends, detectCurrentBackend, currentContext, } from '../lib/terminal/index.js';
|
|
19
20
|
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
20
21
|
import { setHelpSections } from '../lib/help.js';
|
|
21
22
|
import { confirm } from '@inquirer/prompts';
|
|
22
23
|
import { spawn } from 'node:child_process';
|
|
23
|
-
import { buildCanonicalResumeCommand } from '../lib/session/resume-command.js';
|
|
24
24
|
import { looksLikeSessionId } from '../lib/session/discover.js';
|
|
25
|
+
import { machineId } from '../lib/session/sync/config.js';
|
|
26
|
+
import { sessionOriginDevice, sessionRecoveryDestinationMatches } from '../lib/session/recovery.js';
|
|
25
27
|
/** Opening more than this many live sessions at once asks for confirmation first. */
|
|
26
28
|
export const CONFIRM_THRESHOLD = 5;
|
|
27
29
|
export function registerSessionsResumeCommand(sessionsCmd) {
|
|
@@ -34,7 +36,7 @@ export function registerSessionsResumeCommand(sessionsCmd) {
|
|
|
34
36
|
.option('--teams', 'Include team-spawned sessions (hidden by default)')
|
|
35
37
|
.option('--since <time>', 'Only sessions newer than this (e.g., 2h, 7d, 4w, or ISO date)')
|
|
36
38
|
.option('-n, --limit <n>', 'Maximum number of sessions to load into the picker', '200')
|
|
37
|
-
.option('--host <alias>', '
|
|
39
|
+
.option('--host <alias>', 'Open on the session origin host over SSH; the host must match every selected session')
|
|
38
40
|
.option('--iterm', 'Force the iTerm backend')
|
|
39
41
|
.option('--ghostty', 'Force the Ghostty backend')
|
|
40
42
|
.option('--tmux', 'Force the tmux backend')
|
|
@@ -65,8 +67,8 @@ export function registerSessionsResumeCommand(sessionsCmd) {
|
|
|
65
67
|
- Layout: one tab per session by default. --splits packs session pairs side by side in each tab.
|
|
66
68
|
- Backend: auto-detected from the terminal you're in (iTerm / Ghostty / tmux); override with --iterm/--ghostty/--tmux/--vscodium.
|
|
67
69
|
- --vscodium opens each session as an agent terminal tab in VSCodium via the swarm-ext extension (works with --host too).
|
|
68
|
-
- --host <alias>
|
|
69
|
-
-
|
|
70
|
+
- --host <alias> opens the terminal surface on that host only when it is the selected sessions' origin; recovery never migrates a session to another device.
|
|
71
|
+
- Recovery runs on the session's origin device: exact healthy origin uses native resume; otherwise a healthy version of the same harness receives /continue <id>.
|
|
70
72
|
`,
|
|
71
73
|
});
|
|
72
74
|
cmd.action(async (query, options) => {
|
|
@@ -110,7 +112,7 @@ async function sessionsResumeAction(query, options) {
|
|
|
110
112
|
chosen = await multiItemPicker({
|
|
111
113
|
message: 'Select sessions to resume:',
|
|
112
114
|
items: sessions,
|
|
113
|
-
filter: (q) => (q.trim() ?
|
|
115
|
+
filter: (q) => (q.trim() ? sessions.filter((s) => sessionMatchesQuery(s, q)) : sessions),
|
|
114
116
|
labelFor: (s, q) => formatPickerLabel(s, q, cols),
|
|
115
117
|
keyFor: (s) => s.id,
|
|
116
118
|
buildPreview,
|
|
@@ -127,17 +129,25 @@ async function sessionsResumeAction(query, options) {
|
|
|
127
129
|
}
|
|
128
130
|
if (!chosen || chosen.length === 0)
|
|
129
131
|
return;
|
|
130
|
-
|
|
132
|
+
if (options.host) {
|
|
133
|
+
const requestedHost = options.host;
|
|
134
|
+
const mismatches = chosen
|
|
135
|
+
.map((session) => resumeHostMismatch(session, requestedHost))
|
|
136
|
+
.filter((message) => message !== null);
|
|
137
|
+
if (mismatches.length > 0) {
|
|
138
|
+
for (const message of mismatches)
|
|
139
|
+
console.error(chalk.red(message));
|
|
140
|
+
process.exitCode = 1;
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// 2. Route every selection through the owning device's recovery resolver.
|
|
131
145
|
const items = [];
|
|
132
146
|
for (const s of chosen) {
|
|
133
|
-
const command =
|
|
147
|
+
const command = buildSessionRecoveryCommand(s, !!options.host);
|
|
134
148
|
const cwd = s.cwd && fs.existsSync(s.cwd) ? s.cwd : process.cwd();
|
|
135
149
|
items.push({ session: s, cwd, command });
|
|
136
150
|
}
|
|
137
|
-
if (items.length === 0) {
|
|
138
|
-
console.log(chalk.gray('Nothing resumable in the selection.'));
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
151
|
// 3. Resolve the backend (and host).
|
|
142
152
|
const ctx = currentContext();
|
|
143
153
|
const backend = await resolveBackend(options, ctx, items.length);
|
|
@@ -221,6 +231,12 @@ async function spawnCliInPlace(args) {
|
|
|
221
231
|
export function resolveResumePacking(options) {
|
|
222
232
|
return options.splits ? 'two-per-tab' : 'tabs';
|
|
223
233
|
}
|
|
234
|
+
export function resumeHostMismatch(session, requestedHost, self = machineId()) {
|
|
235
|
+
const origin = sessionOriginDevice(session, self);
|
|
236
|
+
return sessionRecoveryDestinationMatches(session, requestedHost, self)
|
|
237
|
+
? null
|
|
238
|
+
: `Session ${session.shortId} originated on ${origin}; --host ${requestedHost} cannot move recovery to another device.`;
|
|
239
|
+
}
|
|
224
240
|
/**
|
|
225
241
|
* Decide which backend to launch into. Returns a concrete backend, `'inplace'`
|
|
226
242
|
* (resume in the current process — no GUI/tmux available), or `'cancel'` (the
|
|
@@ -131,7 +131,7 @@ export declare function formatActiveRowDescription(s: ActiveSession): string;
|
|
|
131
131
|
*/
|
|
132
132
|
export declare function indexActiveBySessionId(active: ActiveSession[]): Map<string, ActiveSession>;
|
|
133
133
|
/** The SessionMeta fields the live-row backfill reads — the enrichment a running process cannot report. */
|
|
134
|
-
type BackfillMeta = Pick<SessionMeta, 'version' | 'timestamp' | 'label' | 'ticketId' | 'prUrl' | 'prNumber'>;
|
|
134
|
+
export type BackfillMeta = Pick<SessionMeta, 'version' | 'timestamp' | 'label' | 'ticketId' | 'prUrl' | 'prNumber' | 'origin' | 'routineName'>;
|
|
135
135
|
/**
|
|
136
136
|
* Backfill display-only fields onto live rows from the indexed SessionMeta, by
|
|
137
137
|
* full session id (RUSH-2205). A running process reports no agent version, and a
|
|
@@ -141,6 +141,8 @@ type BackfillMeta = Pick<SessionMeta, 'version' | 'timestamp' | 'label' | 'ticke
|
|
|
141
141
|
* the DB read that builds `metaById` is the caller's concern.
|
|
142
142
|
*/
|
|
143
143
|
export declare function backfillActiveRowsFromMeta(sessions: ActiveSession[], metaById: Map<string, BackfillMeta>): void;
|
|
144
|
+
/** Add indexed display metadata to active rows for non-renderer consumers. */
|
|
145
|
+
export declare function backfillActiveRowsFromIndex(sessions: ActiveSession[]): void;
|
|
144
146
|
/**
|
|
145
147
|
* The live decoration for a listing row: a status glyph and the latest-turn
|
|
146
148
|
* preview, when the session is still running. `●` running / `◐` waiting on the
|
|
@@ -184,6 +186,16 @@ export declare function liveStatusWord(a: ActiveSession | undefined): string;
|
|
|
184
186
|
* stays excluded rather than inventing a human who can answer.
|
|
185
187
|
*/
|
|
186
188
|
export declare function isAwaitingUser(s: ActiveSession): boolean;
|
|
189
|
+
/**
|
|
190
|
+
* Whether a row belongs in an unqualified `--active` view.
|
|
191
|
+
*
|
|
192
|
+
* The live registry deliberately retains terminally-dead rows long enough for
|
|
193
|
+
* `--closed` / `--crashed` and recovery to find them. Presence in that registry
|
|
194
|
+
* therefore is not, by itself, evidence that a session is still active.
|
|
195
|
+
* Explicit lifecycle filters bypass this predicate and select those retained
|
|
196
|
+
* rows through {@link matchesLiveStatus} instead.
|
|
197
|
+
*/
|
|
198
|
+
export declare function isRunningLiveSession(s: ActiveSession): boolean;
|
|
187
199
|
/**
|
|
188
200
|
* The tracker/PR ref for a session's dedicated column: the ticket id when known,
|
|
189
201
|
* else `PR#<n>`, else empty. Pulled out of the trailing badge blob so refs align
|
|
@@ -573,6 +585,13 @@ export declare function handlePickedSession(picked: PickedSession): Promise<void
|
|
|
573
585
|
* version-pinned launcher is genuinely missing.
|
|
574
586
|
*/
|
|
575
587
|
export declare function resumeSessionInPlace(session: SessionMeta): Promise<void>;
|
|
588
|
+
/**
|
|
589
|
+
* Relaunch this CLI's one recovery path. The owning device resolves account
|
|
590
|
+
* health and either performs exact-home native resume or same-harness
|
|
591
|
+
* `/continue`; callers never guess which version home can see the transcript.
|
|
592
|
+
* `portable` is for an SSH/terminal-engine command that executes on a peer.
|
|
593
|
+
*/
|
|
594
|
+
export declare function buildSessionRecoveryCommand(session: Pick<SessionMeta, 'id'>, portable?: boolean): string[];
|
|
576
595
|
/**
|
|
577
596
|
* Map a resume argv to the spawn(command, args, {shell}) triple.
|
|
578
597
|
*
|
|
@@ -594,6 +613,9 @@ interface AgentFilter {
|
|
|
594
613
|
agent?: SessionAgentId;
|
|
595
614
|
version?: string;
|
|
596
615
|
}
|
|
616
|
+
/** Resolve the harness portion of a sessions selector with the CLI's canonical
|
|
617
|
+
* alias and single-typo rules. Kept pure so focus and the browser cannot drift. */
|
|
618
|
+
export declare function resolveSessionAgentName(name: string): SessionAgentId | null;
|
|
597
619
|
export declare function parseAgentFilter(agentName?: string): AgentFilter;
|
|
598
620
|
/**
|
|
599
621
|
* How a `sessions <query>` argument was resolved against the pool.
|
|
@@ -20,7 +20,7 @@ import { listProjectDefs, resolveProjectNameForCwd } from '../lib/projects.js';
|
|
|
20
20
|
import ora from 'ora';
|
|
21
21
|
import { SESSION_AGENTS } from '../lib/session/types.js';
|
|
22
22
|
import { discoverArtifacts, readArtifact, resolveArtifact } from '../lib/session/artifacts.js';
|
|
23
|
-
import { looksLikePath, toComparablePath, homeDir, needsWindowsShell,
|
|
23
|
+
import { looksLikePath, toComparablePath, homeDir, needsWindowsShell, composeWin32CommandLine } from '../lib/platform/index.js';
|
|
24
24
|
import { getActiveSessions } from '../lib/session/active.js';
|
|
25
25
|
import { enumerateGhosttyTabs, assignGhosttyTabs } from '../lib/session/ghostty-tabs.js';
|
|
26
26
|
import { mapPanesToTargets, listClients } from '../lib/tmux/session.js';
|
|
@@ -45,6 +45,8 @@ import { getShimsDir } from '../lib/state.js';
|
|
|
45
45
|
import { fuzzyMatch, FUZZY_PRESETS } from '../lib/fuzzy.js';
|
|
46
46
|
import { resolveSessionAlias } from '../lib/session/actor-sidecar.js';
|
|
47
47
|
import { resolveVersionAliasLoose } from '../lib/versions.js';
|
|
48
|
+
import { getAgentsInvocation } from '../lib/daemon.js';
|
|
49
|
+
import { sessionRecoveryRunArgs } from '../lib/session/recovery.js';
|
|
48
50
|
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
49
51
|
import { sessionPicker, buildPreview, formatTodoCompact, githubRepoUrlFromCwd, } from './sessions-picker.js';
|
|
50
52
|
import { setHelpSections } from '../lib/help.js';
|
|
@@ -409,6 +411,10 @@ export function backfillActiveRowsFromMeta(sessions, metaById) {
|
|
|
409
411
|
if (!Number.isNaN(ts))
|
|
410
412
|
s.startedAtMs = ts;
|
|
411
413
|
}
|
|
414
|
+
if (!s.origin && m.origin)
|
|
415
|
+
s.origin = m.origin;
|
|
416
|
+
if (!s.routineName && m.routineName)
|
|
417
|
+
s.routineName = m.routineName;
|
|
412
418
|
}
|
|
413
419
|
}
|
|
414
420
|
/**
|
|
@@ -433,6 +439,10 @@ function loadBackfillMetaFor(sessions) {
|
|
|
433
439
|
}
|
|
434
440
|
return byId;
|
|
435
441
|
}
|
|
442
|
+
/** Add indexed display metadata to active rows for non-renderer consumers. */
|
|
443
|
+
export function backfillActiveRowsFromIndex(sessions) {
|
|
444
|
+
backfillActiveRowsFromMeta(sessions, loadBackfillMetaFor(sessions));
|
|
445
|
+
}
|
|
436
446
|
/**
|
|
437
447
|
* The live decoration for a listing row: a status glyph and the latest-turn
|
|
438
448
|
* preview, when the session is still running. `●` running / `◐` waiting on the
|
|
@@ -525,6 +535,20 @@ export function isAwaitingUser(s) {
|
|
|
525
535
|
return false;
|
|
526
536
|
return s.status === 'input_required' || s.activity === 'waiting_input';
|
|
527
537
|
}
|
|
538
|
+
/**
|
|
539
|
+
* Whether a row belongs in an unqualified `--active` view.
|
|
540
|
+
*
|
|
541
|
+
* The live registry deliberately retains terminally-dead rows long enough for
|
|
542
|
+
* `--closed` / `--crashed` and recovery to find them. Presence in that registry
|
|
543
|
+
* therefore is not, by itself, evidence that a session is still active.
|
|
544
|
+
* Explicit lifecycle filters bypass this predicate and select those retained
|
|
545
|
+
* rows through {@link matchesLiveStatus} instead.
|
|
546
|
+
*/
|
|
547
|
+
export function isRunningLiveSession(s) {
|
|
548
|
+
if (s.status === 'closed' || s.status === 'crashed')
|
|
549
|
+
return false;
|
|
550
|
+
return s.pidAlive !== false;
|
|
551
|
+
}
|
|
528
552
|
/** Width of the live status column — `crashed` is the longest word it renders. */
|
|
529
553
|
const LIVE_STATUS_W = 8;
|
|
530
554
|
/** The colored, space-padded status cell for a listing row (empty when not live). */
|
|
@@ -1273,14 +1297,14 @@ async function renderActiveSessions(asJson, waitingOnly = false, opts = {}) {
|
|
|
1273
1297
|
// gate: exit non-zero when the union contains a session awaiting the user.
|
|
1274
1298
|
const statusFiltered = opts.statuses?.length
|
|
1275
1299
|
? merged.filter((session) => opts.statuses.some((status) => matchesLiveStatus(session, status)))
|
|
1276
|
-
: merged;
|
|
1300
|
+
: merged.filter(isRunningLiveSession);
|
|
1277
1301
|
const sessions = statusFiltered;
|
|
1278
1302
|
// Backfill agent version + ticket/PR/label/created onto the live rows from the
|
|
1279
1303
|
// historical index (RUSH-2205) — a running process reports none of these, and
|
|
1280
1304
|
// an orphan row usually lacks them. Done before both the JSON and human paths
|
|
1281
1305
|
// so every consumer (incl. the SSH fan-out's remote --json) sees enriched rows;
|
|
1282
1306
|
// transcripts sync across the fleet, so a remote row resolves from the local DB.
|
|
1283
|
-
|
|
1307
|
+
backfillActiveRowsFromIndex(sessions);
|
|
1284
1308
|
if (asJson) {
|
|
1285
1309
|
// Resolve who is watching each local tmux pane before serializing: `viewingIn`
|
|
1286
1310
|
// is how a consumer distinguishes a session someone is looking at from one
|
|
@@ -3017,34 +3041,23 @@ export async function resumeSessionInPlace(session) {
|
|
|
3017
3041
|
const cwd = session.cwd && fs.existsSync(session.cwd)
|
|
3018
3042
|
? session.cwd
|
|
3019
3043
|
: process.cwd();
|
|
3020
|
-
const resume =
|
|
3021
|
-
if (!resume) {
|
|
3022
|
-
console.log(chalk.yellow(`Resume is not supported for ${session.agent} sessions yet. Showing summary instead.`));
|
|
3023
|
-
await renderSession(session, 'summary', {});
|
|
3024
|
-
return;
|
|
3025
|
-
}
|
|
3044
|
+
const resume = buildSessionRecoveryCommand(session);
|
|
3026
3045
|
console.log(chalk.gray(`Resuming: ${resume.join(' ')} (cwd: ${cwd})`));
|
|
3027
|
-
// Resolve the (possibly version-pinned) launcher up front. On Windows the
|
|
3028
|
-
// agent shim is a `.cmd`/`.ps1` and, under the shell needed to run it (see
|
|
3029
|
-
// spawnResumeCommand), a missing command exits non-zero rather than emitting
|
|
3030
|
-
// an ENOENT `error` event — so detect a removed version here instead of
|
|
3031
|
-
// relying on that event, keeping the fallback working on every OS.
|
|
3032
|
-
// `resume[0]` is an absolute alias path when one exists on disk, so only a bare
|
|
3033
|
-
// name still needs a PATH lookup. Checking existsSync first is what keeps an
|
|
3034
|
-
// isolated install (shims deliberately off PATH) out of the fallback.
|
|
3035
|
-
const launcherFound = path.isAbsolute(resume[0])
|
|
3036
|
-
? fs.existsSync(resume[0])
|
|
3037
|
-
: !!findExecutable(resume[0]);
|
|
3038
|
-
if (!launcherFound && session.version) {
|
|
3039
|
-
const fallback = buildFallbackCommand(session);
|
|
3040
|
-
if (fallback) {
|
|
3041
|
-
console.log(chalk.gray(`Version ${session.version} is not installed. Resuming with the current version instead...`));
|
|
3042
|
-
await spawnResumeCommand(fallback, cwd);
|
|
3043
|
-
return;
|
|
3044
|
-
}
|
|
3045
|
-
}
|
|
3046
3046
|
await spawnResumeCommand(resume, cwd);
|
|
3047
3047
|
}
|
|
3048
|
+
/**
|
|
3049
|
+
* Relaunch this CLI's one recovery path. The owning device resolves account
|
|
3050
|
+
* health and either performs exact-home native resume or same-harness
|
|
3051
|
+
* `/continue`; callers never guess which version home can see the transcript.
|
|
3052
|
+
* `portable` is for an SSH/terminal-engine command that executes on a peer.
|
|
3053
|
+
*/
|
|
3054
|
+
export function buildSessionRecoveryCommand(session, portable = false) {
|
|
3055
|
+
const args = sessionRecoveryRunArgs(session);
|
|
3056
|
+
if (portable)
|
|
3057
|
+
return ['agents', ...args];
|
|
3058
|
+
const invocation = getAgentsInvocation(args);
|
|
3059
|
+
return [invocation.command, ...invocation.args];
|
|
3060
|
+
}
|
|
3048
3061
|
/**
|
|
3049
3062
|
* Map a resume argv to the spawn(command, args, {shell}) triple.
|
|
3050
3063
|
*
|
|
@@ -3112,8 +3125,6 @@ function spawnResumeCommand(cmd, cwd) {
|
|
|
3112
3125
|
* isolated HOME where the JSONL was written — regardless of which version is
|
|
3113
3126
|
* currently the default. Falls back to the bare shim when version is unknown.
|
|
3114
3127
|
*
|
|
3115
|
-
* If the versioned binary is missing (version was removed), the ENOENT
|
|
3116
|
-
* handler in handlePickedSession retries via buildFallbackCommand.
|
|
3117
3128
|
*/
|
|
3118
3129
|
/**
|
|
3119
3130
|
* The agent's own resume invocation, given whichever launcher we resolved.
|
|
@@ -3180,19 +3191,6 @@ export function buildResumeCommand(session) {
|
|
|
3180
3191
|
return null;
|
|
3181
3192
|
}
|
|
3182
3193
|
}
|
|
3183
|
-
/**
|
|
3184
|
-
* Fallback when the pinned version really is gone: the same resume invocation
|
|
3185
|
-
* against the current version.
|
|
3186
|
-
*
|
|
3187
|
-
* This used to spawn `<cli> "/continue <id>"`, feeding a slash command into the
|
|
3188
|
-
* TUI as a prompt. Neither CLI has `/continue` — codex documents `/resume` — so
|
|
3189
|
-
* the agent received an unrecognised command and the session was not resumed at
|
|
3190
|
-
* all. Reusing resumeArgv keeps the two paths from drifting apart again.
|
|
3191
|
-
*/
|
|
3192
|
-
function buildFallbackCommand(session) {
|
|
3193
|
-
const cli = AGENTS[session.agent]?.cliCommand ?? session.agent;
|
|
3194
|
-
return resumeArgv(session.agent, session.id, cli);
|
|
3195
|
-
}
|
|
3196
3194
|
// ---------------------------------------------------------------------------
|
|
3197
3195
|
// Cloud session source (--cloud)
|
|
3198
3196
|
// ---------------------------------------------------------------------------
|
|
@@ -3263,25 +3261,24 @@ async function runCloudSessions(query, options) {
|
|
|
3263
3261
|
// Ensure the SessionMeta points at the local cache path for renderSession.
|
|
3264
3262
|
await renderSession({ ...meta, filePath: cachedPath }, mode, filterOpts, options);
|
|
3265
3263
|
}
|
|
3264
|
+
/** Resolve the harness portion of a sessions selector with the CLI's canonical
|
|
3265
|
+
* alias and single-typo rules. Kept pure so focus and the browser cannot drift. */
|
|
3266
|
+
export function resolveSessionAgentName(name) {
|
|
3267
|
+
const normalized = name.toLowerCase();
|
|
3268
|
+
if (SESSION_AGENTS.includes(normalized)) {
|
|
3269
|
+
return normalized;
|
|
3270
|
+
}
|
|
3271
|
+
const resolved = resolveAgentName(normalized);
|
|
3272
|
+
if (resolved && SESSION_AGENTS.includes(resolved)) {
|
|
3273
|
+
return resolved;
|
|
3274
|
+
}
|
|
3275
|
+
return fuzzyMatch(normalized, SESSION_AGENTS, FUZZY_PRESETS.agents);
|
|
3276
|
+
}
|
|
3266
3277
|
export function parseAgentFilter(agentName) {
|
|
3267
3278
|
if (!agentName)
|
|
3268
3279
|
return {};
|
|
3269
3280
|
const [name, version] = agentName.split('@', 2);
|
|
3270
|
-
|
|
3271
|
-
? name
|
|
3272
|
-
: null;
|
|
3273
|
-
if (!agent) {
|
|
3274
|
-
// Aliases and single-typo corrections (cladue -> claude). SESSION_AGENTS
|
|
3275
|
-
// includes ids (rush, hermes) that resolveAgentName doesn't know, so fall
|
|
3276
|
-
// back to fuzzy-matching the session list directly.
|
|
3277
|
-
const resolved = resolveAgentName(name);
|
|
3278
|
-
if (resolved && SESSION_AGENTS.includes(resolved)) {
|
|
3279
|
-
agent = resolved;
|
|
3280
|
-
}
|
|
3281
|
-
else {
|
|
3282
|
-
agent = fuzzyMatch(name, SESSION_AGENTS, FUZZY_PRESETS.agents);
|
|
3283
|
-
}
|
|
3284
|
-
}
|
|
3281
|
+
const agent = resolveSessionAgentName(name);
|
|
3285
3282
|
if (!agent) {
|
|
3286
3283
|
console.error(chalk.red(`Unknown agent: ${name}. Use: ${SESSION_AGENTS.join(', ')}`));
|
|
3287
3284
|
process.exit(1);
|
|
@@ -3930,7 +3927,7 @@ export function registerSessionsCommands(program) {
|
|
|
3930
3927
|
.option('--opencode', 'Shorthand for --agent opencode')
|
|
3931
3928
|
.option('--all', 'Widen every non-status filter to "all": every directory (not just this project) and all time (no window cap). Status filters like --active still compose; -a/--device/--since still narrow their axis.')
|
|
3932
3929
|
.option('--unmanaged', "Also show sessions from your own ~/.<agent> installs (hidden once agents-cli manages that agent)")
|
|
3933
|
-
.option('--teams', 'Include team-spawned sessions (hidden by default)')
|
|
3930
|
+
.option('--team, --teams', 'Include team-spawned sessions (hidden by default)')
|
|
3934
3931
|
.option('--in-team <name>', "Only this team: the session that spawned it plus (with --teams) its teammates. Spans every directory and all time, since a team's worktrees and history sit outside the default window.")
|
|
3935
3932
|
.option('--routine', 'Show only sessions archived from routine runs')
|
|
3936
3933
|
.option('-p, --project <name>', 'Filter by project name (searches across all directories)')
|
|
@@ -3997,7 +3994,7 @@ export function registerSessionsCommands(program) {
|
|
|
3997
3994
|
agents sessions --crashed
|
|
3998
3995
|
|
|
3999
3996
|
# --- Session lifecycle (one verb per intent) ---
|
|
4000
|
-
#
|
|
3997
|
+
# Focus a session (attach its living terminal, or recover an ended one)
|
|
4001
3998
|
agents sessions focus a1b2c3d4
|
|
4002
3999
|
# Attach only — never fork a copy (old: sessions go)
|
|
4003
4000
|
agents sessions focus a1b2c3d4 --attach-only
|
|
@@ -4049,7 +4046,7 @@ export function registerSessionsCommands(program) {
|
|
|
4049
4046
|
`,
|
|
4050
4047
|
notes: `
|
|
4051
4048
|
Session lifecycle (pick one verb — they are not synonyms):
|
|
4052
|
-
focus [
|
|
4049
|
+
focus [selector] attach a living pane, or recover on the origin device
|
|
4053
4050
|
focus [id] --attach-only attach only; never fork (replaces sessions go)
|
|
4054
4051
|
detach <id> interactive → headless continuation
|
|
4055
4052
|
attach <id> headless → interactive in this terminal
|
|
@@ -66,7 +66,7 @@ function printBackendNotes(backend) {
|
|
|
66
66
|
console.log(chalk.gray('backend: keychain — macOS reads may ask for Touch ID or the device password.'));
|
|
67
67
|
}
|
|
68
68
|
else if (backend === 'file') {
|
|
69
|
-
console.log(chalk.gray('backend: file —
|
|
69
|
+
console.log(chalk.gray('backend: file — encrypted at rest; headless reads need no passphrase by default, via a machine-local key at ~/.agents/.secrets-key/passphrase.'));
|
|
70
70
|
}
|
|
71
71
|
else {
|
|
72
72
|
console.log(chalk.gray('backend: vault — synced ~/.agents/vault.age storage; unlock it with agents login.'));
|
|
@@ -3,17 +3,12 @@ import { spawnSync } from 'node:child_process';
|
|
|
3
3
|
import { getCliLaunch } from '../lib/cli-entry.js';
|
|
4
4
|
import { loadDevices } from '../lib/devices/registry.js';
|
|
5
5
|
import { machineId, normalizeHost } from '../lib/machine-id.js';
|
|
6
|
-
import {
|
|
7
|
-
import { readJob, setJobEnabled } from '../lib/routines.js';
|
|
6
|
+
import { getConfigValue, setConfigValue } from '../lib/device-config.js';
|
|
8
7
|
import { isPromptCancelled, requireInteractiveSelection } from './utils.js';
|
|
9
|
-
const ROUTINE = 'watchdog';
|
|
10
8
|
export async function runWatchdogSetupWizard() {
|
|
11
|
-
if (!readJob(ROUTINE)) {
|
|
12
|
-
throw new Error("Built-in routine 'watchdog' is missing. Run: agents repo pull system");
|
|
13
|
-
}
|
|
14
9
|
const registry = await loadDevices();
|
|
15
10
|
const devices = [...new Set([...Object.keys(registry), machineId()].map(normalizeHost))].sort();
|
|
16
|
-
const enabled = new Set(
|
|
11
|
+
const enabled = new Set(devices.filter((device) => getConfigValue('watchdog.enabled', { device }).value === true));
|
|
17
12
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
18
13
|
requireInteractiveSelection('watchdog devices', ['agents watchdog on', 'agents watchdog on --host <device>']);
|
|
19
14
|
}
|
|
@@ -27,10 +22,10 @@ export async function runWatchdogSetupWizard() {
|
|
|
27
22
|
for (const device of devices) {
|
|
28
23
|
const on = selectedSet.has(device);
|
|
29
24
|
if (device === normalizeHost(machineId())) {
|
|
30
|
-
|
|
25
|
+
setConfigValue('watchdog.enabled', on);
|
|
31
26
|
continue;
|
|
32
27
|
}
|
|
33
|
-
const launch = getCliLaunch(['
|
|
28
|
+
const launch = getCliLaunch(['watchdog', on ? 'on' : 'off', '--host', device]);
|
|
34
29
|
const result = spawnSync(launch.command, launch.args, { stdio: 'inherit', env: process.env });
|
|
35
30
|
if ((result.status ?? 1) !== 0)
|
|
36
31
|
throw new Error(`Could not configure watchdog on ${device}`);
|
|
@@ -49,6 +44,6 @@ export async function runWatchdogSetupWizard() {
|
|
|
49
44
|
}
|
|
50
45
|
export function registerSetupWatchdogCommand(setup) {
|
|
51
46
|
setup.command('watchdog')
|
|
52
|
-
.description('Choose the devices where the
|
|
47
|
+
.description('Choose the devices where the daemon watchdog pass runs.')
|
|
53
48
|
.action(runWatchdogSetupWizard);
|
|
54
49
|
}
|
package/dist/commands/setup.js
CHANGED
|
@@ -319,7 +319,7 @@ export function registerSetupCommand(program) {
|
|
|
319
319
|
agents setup share # provision or join a Cloudflare share endpoint
|
|
320
320
|
agents setup secrets # choose secrets backend/policy defaults + import
|
|
321
321
|
agents setup fleet # discover Tailscale devices + configure SSH access
|
|
322
|
-
agents setup watchdog # choose which devices run the watchdog
|
|
322
|
+
agents setup watchdog # choose which devices run the daemon watchdog pass
|
|
323
323
|
|
|
324
324
|
To install CLIs from agents.yaml and sync resources into version homes:
|
|
325
325
|
agents sync --local -y
|