@phnx-labs/agents-cli 1.22.26 → 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 +244 -0
- package/README.md +18 -1
- 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 +17 -16
- package/dist/commands/insights.js +39 -5
- package/dist/commands/output.js +100 -25
- package/dist/commands/resume.d.ts +11 -0
- package/dist/commands/resume.js +51 -0
- package/dist/commands/sessions-picker.js +12 -7
- package/dist/commands/sessions.d.ts +47 -1
- package/dist/commands/sessions.js +345 -37
- package/dist/commands/ssh.js +184 -11
- package/dist/commands/teams.js +3 -3
- package/dist/index.js +16 -20
- 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 +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 +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/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.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/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 +22 -0
- package/dist/lib/hosts/passthrough.js +14 -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 +4 -2
- package/dist/lib/hosts/ready.js +24 -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 +45 -12
- package/dist/lib/hosts/reconnect.js +90 -34
- 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/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.js +3 -2
- package/dist/lib/secrets/lease.d.ts +25 -0
- package/dist/lib/secrets/lease.js +44 -0
- package/dist/lib/self-update.d.ts +20 -5
- package/dist/lib/self-update.js +93 -16
- package/dist/lib/session/db.d.ts +19 -2
- package/dist/lib/session/db.js +54 -5
- 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-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/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/teams/placement-probe.d.ts +1 -1
- package/dist/lib/teams/placement-probe.js +24 -24
- 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
|
@@ -33,16 +33,18 @@ import { stringWidth, truncateToWidth, padToWidth, terminalWidth } from '../lib/
|
|
|
33
33
|
import { inferSessionState } from '../lib/session/state.js';
|
|
34
34
|
import { discoverSessions, queryIndexedSessions, countSessionsInScope, resolveSessionById, isCompleteSessionId, looksLikeSessionId, searchContentIndex, getSessionRoots, scopeToManaged } from '../lib/session/discover.js';
|
|
35
35
|
import { findSessionsById, querySessions, getSessionById } from '../lib/session/db.js';
|
|
36
|
-
import { filterTeamSessions, safeTeamText } from '../lib/session/team-filter.js';
|
|
36
|
+
import { filterTeamSessions, safeTeamText, groupSessionsByTeam, NO_TEAM_GROUP_KEY, } from '../lib/session/team-filter.js';
|
|
37
37
|
import { parseSession } from '../lib/session/parse.js';
|
|
38
38
|
import { runRemoteSessions, buildForwardedArgs, ensureWholeIndex } from '../lib/session/remote.js';
|
|
39
39
|
import { formatRelativeTime, formatCompactAge, sessionAgeParts } from '../lib/session/relative-time.js';
|
|
40
40
|
import { renderConversationMarkdown, renderSummary, renderSummaryHeader, computeSummaryStats, renderJson, filterEvents, parseRoleList, linkPath, linkUrl, shortenModel } from '../lib/session/render.js';
|
|
41
41
|
import { linearIssueUrl } from '../lib/session/linear.js';
|
|
42
|
+
import { sessionOwnerDevice, RESUME_PINNED_ENV } from '../lib/session/resume-owner.js';
|
|
42
43
|
import { renderMarkdown } from '../lib/markdown.js';
|
|
43
44
|
import { AGENTS, colorAgent, resolveAgentName } from '../lib/agents.js';
|
|
44
45
|
import { getShimsDir } from '../lib/state.js';
|
|
45
46
|
import { fuzzyMatch, FUZZY_PRESETS } from '../lib/fuzzy.js';
|
|
47
|
+
import { itemPicker } from '../lib/picker.js';
|
|
46
48
|
import { resolveSessionAlias } from '../lib/session/actor-sidecar.js';
|
|
47
49
|
import { resolveVersionAliasLoose } from '../lib/versions.js';
|
|
48
50
|
import { getAgentsInvocation } from '../lib/daemon.js';
|
|
@@ -1442,6 +1444,96 @@ export function hasNoBrowserDisqualifyingFlags(options, query) {
|
|
|
1442
1444
|
// legacy per-host stream, which prints each peer under its own banner.
|
|
1443
1445
|
(options.host?.length ?? 0) <= 1);
|
|
1444
1446
|
}
|
|
1447
|
+
/** Resolve a typed routine selector by exact name, substring, or one unambiguous typo. */
|
|
1448
|
+
export function resolveRoutineName(query, names) {
|
|
1449
|
+
const normalized = query.trim().toLowerCase();
|
|
1450
|
+
if (!normalized)
|
|
1451
|
+
return null;
|
|
1452
|
+
const exact = names.find((name) => name.toLowerCase() === normalized);
|
|
1453
|
+
if (exact)
|
|
1454
|
+
return exact;
|
|
1455
|
+
const containing = names.filter((name) => name.toLowerCase().includes(normalized));
|
|
1456
|
+
if (containing.length === 1)
|
|
1457
|
+
return containing[0];
|
|
1458
|
+
return fuzzyMatch(query, names, FUZZY_PRESETS.dynamic);
|
|
1459
|
+
}
|
|
1460
|
+
export function buildRoutineRunGroups(sessions) {
|
|
1461
|
+
const byRun = new Map();
|
|
1462
|
+
for (const session of sessions) {
|
|
1463
|
+
const runId = session.routineRunId ?? session.timestamp;
|
|
1464
|
+
(byRun.get(runId) ?? byRun.set(runId, []).get(runId)).push(session);
|
|
1465
|
+
}
|
|
1466
|
+
return [...byRun.entries()]
|
|
1467
|
+
.map(([runId, rows]) => ({
|
|
1468
|
+
runId,
|
|
1469
|
+
timestamp: rows.reduce((latest, row) => (row.lastActivity ?? row.timestamp) > latest ? (row.lastActivity ?? row.timestamp) : latest, rows[0].lastActivity ?? rows[0].timestamp),
|
|
1470
|
+
sessions: rows.sort((a, b) => (a.timestamp < b.timestamp ? 1 : -1)),
|
|
1471
|
+
}))
|
|
1472
|
+
.sort((a, b) => (a.timestamp < b.timestamp ? 1 : a.timestamp > b.timestamp ? -1 : a.runId.localeCompare(b.runId)));
|
|
1473
|
+
}
|
|
1474
|
+
export function buildRoutineChoices(sessions) {
|
|
1475
|
+
const byName = new Map();
|
|
1476
|
+
for (const session of sessions) {
|
|
1477
|
+
if (!session.routineName)
|
|
1478
|
+
continue;
|
|
1479
|
+
(byName.get(session.routineName) ?? byName.set(session.routineName, []).get(session.routineName)).push(session);
|
|
1480
|
+
}
|
|
1481
|
+
return [...byName.entries()]
|
|
1482
|
+
.map(([name, rows]) => {
|
|
1483
|
+
const runs = buildRoutineRunGroups(rows);
|
|
1484
|
+
return {
|
|
1485
|
+
name,
|
|
1486
|
+
lastRunAt: runs[0].timestamp,
|
|
1487
|
+
runCount: runs.length,
|
|
1488
|
+
latestRunSessionCount: runs[0].sessions.length,
|
|
1489
|
+
};
|
|
1490
|
+
})
|
|
1491
|
+
.sort((a, b) => (a.lastRunAt < b.lastRunAt ? 1 : a.lastRunAt > b.lastRunAt ? -1 : a.name.localeCompare(b.name)));
|
|
1492
|
+
}
|
|
1493
|
+
async function selectRoutineName(sessions) {
|
|
1494
|
+
const choices = buildRoutineChoices(sessions);
|
|
1495
|
+
const picked = await itemPicker({
|
|
1496
|
+
message: 'Select a routine:',
|
|
1497
|
+
items: choices,
|
|
1498
|
+
filter: (query) => {
|
|
1499
|
+
const normalized = query.trim().toLowerCase();
|
|
1500
|
+
if (!normalized)
|
|
1501
|
+
return choices;
|
|
1502
|
+
return choices.filter((choice) => choice.name.toLowerCase().includes(normalized));
|
|
1503
|
+
},
|
|
1504
|
+
labelFor: (choice) => {
|
|
1505
|
+
const runs = `${choice.runCount} run${choice.runCount === 1 ? '' : 's'}`;
|
|
1506
|
+
const sessions = `${choice.latestRunSessionCount} session${choice.latestRunSessionCount === 1 ? '' : 's'} in latest`;
|
|
1507
|
+
return `${choice.name} ${chalk.gray(`${runs} · ${sessions} · ${formatRelativeTime(choice.lastRunAt)}`)}`;
|
|
1508
|
+
},
|
|
1509
|
+
shortIdFor: (choice) => choice.name,
|
|
1510
|
+
emptyMessage: 'No routines match.',
|
|
1511
|
+
enterHint: 'show sessions',
|
|
1512
|
+
});
|
|
1513
|
+
return picked?.item.name ?? null;
|
|
1514
|
+
}
|
|
1515
|
+
export async function filterSessionsByRoutine(sessions, routine, interactive) {
|
|
1516
|
+
const routineNames = [...new Set(sessions.map((session) => session.routineName).filter((name) => !!name))].sort((a, b) => a.localeCompare(b));
|
|
1517
|
+
let selectedRoutine = null;
|
|
1518
|
+
if (typeof routine === 'string') {
|
|
1519
|
+
selectedRoutine = resolveRoutineName(routine, routineNames);
|
|
1520
|
+
if (!selectedRoutine) {
|
|
1521
|
+
console.error(chalk.red(`No routine matches "${routine}".`));
|
|
1522
|
+
if (routineNames.length > 0)
|
|
1523
|
+
console.error(chalk.gray(`Available routines: ${routineNames.join(', ')}`));
|
|
1524
|
+
process.exitCode = 1;
|
|
1525
|
+
return null;
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
else if (interactive) {
|
|
1529
|
+
selectedRoutine = await selectRoutineName(sessions);
|
|
1530
|
+
if (!selectedRoutine)
|
|
1531
|
+
return null;
|
|
1532
|
+
}
|
|
1533
|
+
return selectedRoutine
|
|
1534
|
+
? sessions.filter((session) => session.routineName === selectedRoutine)
|
|
1535
|
+
: sessions;
|
|
1536
|
+
}
|
|
1445
1537
|
/** The canonical `ag sessions …` command for a set of flags — the twin of the
|
|
1446
1538
|
* browser's `y` hotkey (see --print-cmd). Normalizes to the stable flag form. */
|
|
1447
1539
|
function canonicalSessionsCommand(query, options) {
|
|
@@ -1468,8 +1560,11 @@ function canonicalSessionsCommand(query, options) {
|
|
|
1468
1560
|
a.push('--teams');
|
|
1469
1561
|
if (options.inTeam)
|
|
1470
1562
|
a.push('--in-team', options.inTeam);
|
|
1471
|
-
if (options.routine)
|
|
1563
|
+
if (options.routine) {
|
|
1472
1564
|
a.push('--routine');
|
|
1565
|
+
if (typeof options.routine === 'string')
|
|
1566
|
+
a.push(options.routine);
|
|
1567
|
+
}
|
|
1473
1568
|
if (options.agent)
|
|
1474
1569
|
a.push('-a', options.agent);
|
|
1475
1570
|
for (const h of options.host ?? [])
|
|
@@ -1910,7 +2005,13 @@ limitSource) {
|
|
|
1910
2005
|
// filter the browser can't represent), or --no-interactive keep the existing
|
|
1911
2006
|
// printed/render paths (agents and scripts unaffected). An explicit --since seeds
|
|
1912
2007
|
// the browser's window so the flag is honored, not swallowed.
|
|
1913
|
-
|
|
2008
|
+
//
|
|
2009
|
+
// `--teams` diverts to the printed team-grouped report (printTeamsView, below):
|
|
2010
|
+
// grouping teammates under their team is a shape the flat fuzzy picker cannot
|
|
2011
|
+
// represent, so like --tree it is a printed listing, not an interactive pick.
|
|
2012
|
+
// --teams --flat/--tree keep their inline table rendering; a search query keeps
|
|
2013
|
+
// the picker so `<term> --teams` still searches.
|
|
2014
|
+
if (isBareBrowserListing(options, query) && !options.teams) {
|
|
1914
2015
|
const { runSessionBrowser, bareBrowserSeed } = await import('./sessions-browser.js');
|
|
1915
2016
|
await runSessionBrowser(bareBrowserSeed({
|
|
1916
2017
|
teams: options.teams,
|
|
@@ -1957,13 +2058,13 @@ limitSource) {
|
|
|
1957
2058
|
const wantsRender = !toolEvidenceMode && (mode === 'markdown' || hasAnyFilter(filterOpts));
|
|
1958
2059
|
// Artifact-list or artifact-read paths: widen scope and resolve session globally.
|
|
1959
2060
|
if ((options.artifacts || options.artifact !== undefined) && searchQuery) {
|
|
1960
|
-
await renderArtifactsGlobal(searchQuery, options.artifacts ?? false, options.artifact,
|
|
2061
|
+
await renderArtifactsGlobal(searchQuery, options.artifacts ?? false, options.artifact, artifactLookupScope(options.agent, options.project, options.routine));
|
|
1961
2062
|
return;
|
|
1962
2063
|
}
|
|
1963
2064
|
// When the user explicitly asks to render (via mode flag), resolve the
|
|
1964
2065
|
// query globally so sessions outside the default cwd/30d window are found.
|
|
1965
2066
|
if (wantsRender && searchQuery) {
|
|
1966
|
-
await renderOneSession(searchQuery, mode, { agent: options.agent, project: options.project, filter: filterOpts, redact: options.redact, local: options.local, hosts: options.host });
|
|
2067
|
+
await renderOneSession(searchQuery, mode, { agent: options.agent, project: options.project, routine: options.routine, filter: filterOpts, redact: options.redact, local: options.local, hosts: options.host });
|
|
1967
2068
|
return;
|
|
1968
2069
|
}
|
|
1969
2070
|
// Interactive picker loads a deep pool but shows only recent sessions
|
|
@@ -2146,6 +2247,14 @@ limitSource) {
|
|
|
2146
2247
|
const hiddenCount = options.teams || options.inTeam
|
|
2147
2248
|
? 0
|
|
2148
2249
|
: countSessionsInScope({ ...scope, onlyTeamOrigin: true });
|
|
2250
|
+
// A typed routine scope must apply before smart ID routing. Otherwise an ID
|
|
2251
|
+
// from another routine resolves and renders before the routine filter below.
|
|
2252
|
+
if (typeof options.routine === 'string') {
|
|
2253
|
+
const filteredByRoutine = await filterSessionsByRoutine(sessions, options.routine, false);
|
|
2254
|
+
if (!filteredByRoutine)
|
|
2255
|
+
return;
|
|
2256
|
+
sessions = filteredByRoutine;
|
|
2257
|
+
}
|
|
2149
2258
|
// Smart ID routing: a bare query that resolves to one session renders
|
|
2150
2259
|
// directly. If nothing matches in the scoped window and the query looks
|
|
2151
2260
|
// like a session ID, widen to global scope (incl. Claude /resume history).
|
|
@@ -2163,11 +2272,17 @@ limitSource) {
|
|
|
2163
2272
|
return;
|
|
2164
2273
|
}
|
|
2165
2274
|
if (idMatches.length === 0 && looksLikeSessionId(searchQuery)) {
|
|
2166
|
-
await renderOneSession(searchQuery, mode, { agent: options.agent, project: options.project, filter: filterOpts, redact: options.redact, local: options.local, hosts: options.host });
|
|
2275
|
+
await renderOneSession(searchQuery, mode, { agent: options.agent, project: options.project, routine: options.routine, filter: filterOpts, redact: options.redact, local: options.local, hosts: options.host });
|
|
2167
2276
|
return;
|
|
2168
2277
|
}
|
|
2169
2278
|
}
|
|
2170
2279
|
if (options.json) {
|
|
2280
|
+
if (options.routine === true) {
|
|
2281
|
+
const filteredByRoutine = await filterSessionsByRoutine(sessions, options.routine, false);
|
|
2282
|
+
if (!filteredByRoutine)
|
|
2283
|
+
return;
|
|
2284
|
+
sessions = filteredByRoutine;
|
|
2285
|
+
}
|
|
2171
2286
|
// An id-shaped query resolves by id ONLY — the same rule the render path
|
|
2172
2287
|
// uses (resolveSessionQuery). Without this a `--json <uuid>` (as issued by
|
|
2173
2288
|
// the fleet locate sweep, which runs `sessions <uuid> --json --local` on
|
|
@@ -2210,6 +2325,12 @@ limitSource) {
|
|
|
2210
2325
|
fanSpinner?.stop();
|
|
2211
2326
|
}
|
|
2212
2327
|
}
|
|
2328
|
+
if (options.routine) {
|
|
2329
|
+
const filteredByRoutine = await filterSessionsByRoutine(sessions, options.routine, isInteractive);
|
|
2330
|
+
if (!filteredByRoutine)
|
|
2331
|
+
return;
|
|
2332
|
+
sessions = filteredByRoutine;
|
|
2333
|
+
}
|
|
2213
2334
|
if (sessions.length === 0) {
|
|
2214
2335
|
if (pathFilter) {
|
|
2215
2336
|
console.log(chalk.gray(`No sessions found for ${pathFilter}.`));
|
|
@@ -2225,11 +2346,24 @@ limitSource) {
|
|
|
2225
2346
|
}
|
|
2226
2347
|
return;
|
|
2227
2348
|
}
|
|
2349
|
+
// `--teams` prints the team-grouped report: teammates under their team (with
|
|
2350
|
+
// spawner + spawn time), bare SDK spawns in their own bucket. --flat/--tree
|
|
2351
|
+
// keep their inline table rendering; a search query keeps the picker. Placed
|
|
2352
|
+
// before the project overview and picker so it wins the bare `--teams` case.
|
|
2353
|
+
if (options.teams && !options.flat && !options.tree && !searchQuery && !pathFilter) {
|
|
2354
|
+
const liveIndex = await maybeLiveIndex(options);
|
|
2355
|
+
printTeamsView(sessions, liveIndex, hiddenUnmanaged);
|
|
2356
|
+
return;
|
|
2357
|
+
}
|
|
2228
2358
|
// The grouped project overview is the bare interactive default: a scannable
|
|
2229
2359
|
// dashboard of the whole fleet grouped by project, newest-active first.
|
|
2230
2360
|
// Interact/resume via `agents sessions <project>` or `agents sessions resume`.
|
|
2231
2361
|
if (wantsOverview) {
|
|
2232
2362
|
const liveIndex = await maybeLiveIndex(options);
|
|
2363
|
+
if (options.routine) {
|
|
2364
|
+
printRoutineRunOverview(sessions, liveIndex, { hiddenCount, hiddenUnmanaged });
|
|
2365
|
+
return;
|
|
2366
|
+
}
|
|
2233
2367
|
// Per-project row cap is fixed (--limit carries a default of 50 and drives
|
|
2234
2368
|
// the fetch pool, not the display); `--all` expands every group instead.
|
|
2235
2369
|
printSessionOverview(sessions, hiddenCount, liveIndex, { perProjectCap: OVERVIEW_ROWS_PER_PROJECT, expand: !!options.all, hiddenUnmanaged });
|
|
@@ -2553,6 +2687,25 @@ function printSessionOverview(pool, hiddenCount, liveIndex, opts) {
|
|
|
2553
2687
|
if (opts.hiddenUnmanaged)
|
|
2554
2688
|
console.log(chalk.gray(formatUnmanagedHiddenFooter(opts.hiddenUnmanaged)));
|
|
2555
2689
|
}
|
|
2690
|
+
export function printRoutineRunOverview(sessions, liveIndex, opts = {}) {
|
|
2691
|
+
const groups = buildRoutineRunGroups(sessions);
|
|
2692
|
+
console.log(chalk.gray(`${sessions.length} session${sessions.length === 1 ? '' : 's'} · ${groups.length} routine run${groups.length === 1 ? '' : 's'} · newest run first\n`));
|
|
2693
|
+
let first = true;
|
|
2694
|
+
for (const group of groups) {
|
|
2695
|
+
if (!first)
|
|
2696
|
+
console.log();
|
|
2697
|
+
first = false;
|
|
2698
|
+
console.log(`${chalk.cyan('▸')} ${chalk.cyan.bold(group.runId)} ` +
|
|
2699
|
+
`${chalk.gray(`${group.sessions.length} session${group.sessions.length === 1 ? '' : 's'} · ${formatRelativeTime(group.timestamp)}`)}`);
|
|
2700
|
+
for (const session of group.sessions)
|
|
2701
|
+
console.log(treeSessionRow(session, liveIndex?.get(session.id)));
|
|
2702
|
+
}
|
|
2703
|
+
console.log(chalk.gray('\nGrouped by routine run id and timestamp.'));
|
|
2704
|
+
if (opts.hiddenCount)
|
|
2705
|
+
console.log(chalk.gray(formatTeamHiddenFooter(opts.hiddenCount)));
|
|
2706
|
+
if (opts.hiddenUnmanaged)
|
|
2707
|
+
console.log(chalk.gray(formatUnmanagedHiddenFooter(opts.hiddenUnmanaged)));
|
|
2708
|
+
}
|
|
2556
2709
|
function printSessionTable(sessions, hiddenCount = 0, tree = false, liveIndex) {
|
|
2557
2710
|
if (tree) {
|
|
2558
2711
|
// Group by directory; drop the id/version columns from view. The short id
|
|
@@ -2599,6 +2752,118 @@ function printSessionTable(sessions, hiddenCount = 0, tree = false, liveIndex) {
|
|
|
2599
2752
|
console.log(chalk.gray(formatTeamHiddenFooter(hiddenCount)));
|
|
2600
2753
|
}
|
|
2601
2754
|
}
|
|
2755
|
+
/** Width of the mode cell (plan/edit/auto/skip) in a team-member row. */
|
|
2756
|
+
const TEAM_MODE_W = 5;
|
|
2757
|
+
/** Longest teammate handle folded into a team-member row before it truncates. */
|
|
2758
|
+
const TEAM_HANDLE_W = 16;
|
|
2759
|
+
/**
|
|
2760
|
+
* One row under a team group: `shortId · agent · mode · handle · doing · time`.
|
|
2761
|
+
* Unlike {@link flatSessionRow}/{@link treeSessionRow} it drops the
|
|
2762
|
+
* `[team/handle]` topic prefix — the group header already names the team — and
|
|
2763
|
+
* promotes the teammate's mode and handle to their own cells, the richer
|
|
2764
|
+
* identity the `--teams` view exists to show (RUSH-1997).
|
|
2765
|
+
*/
|
|
2766
|
+
function teamMemberRow(session, live) {
|
|
2767
|
+
const agentColor = colorAgent(session.agent);
|
|
2768
|
+
const origin = session.teamOrigin;
|
|
2769
|
+
const age = sessionAgeParts(session.timestamp, session.lastActivity);
|
|
2770
|
+
const handle = safeTeamText(origin?.handle) ?? session.shortId;
|
|
2771
|
+
const mode = safeTeamText(origin?.mode) ?? '';
|
|
2772
|
+
const { glyph, preview } = liveGlyphAndPreview(live);
|
|
2773
|
+
// Match flatSessionRow: a live preview already folds ActiveSession.todos; a
|
|
2774
|
+
// resting row surfaces SessionMeta.todos when the scan attached it.
|
|
2775
|
+
const restingTodo = !live ? formatTodoCompact(session.todos) : '';
|
|
2776
|
+
const doing = [restingTodo, preview || session.topic || ''].filter(Boolean).join(' · ');
|
|
2777
|
+
const shownHandle = truncateToWidth(handle, TEAM_HANDLE_W);
|
|
2778
|
+
const head = doing ? `${shownHandle} · ${doing}` : shownHandle;
|
|
2779
|
+
const badges = signalBadges(metaSignals(session));
|
|
2780
|
+
const badgeW = badges ? stringWidth(badges) + 1 : 0;
|
|
2781
|
+
const { cell: statusCell, width: statusW } = liveStatusCell(live);
|
|
2782
|
+
const glyphW = glyph ? 2 : 0;
|
|
2783
|
+
const baseTopicW = terminalWidth() - (2 + 9 + 8 + (TEAM_MODE_W + 1)) - glyphW - statusW - badgeW - stringWidth(age.last) - 1;
|
|
2784
|
+
const when = timeCell(age, baseTopicW);
|
|
2785
|
+
const topicW = Math.max(12, baseTopicW - when.extraW);
|
|
2786
|
+
return (' ' +
|
|
2787
|
+
chalk.dim(padToWidth(session.shortId, 9)) +
|
|
2788
|
+
agentColor(padToWidth(truncateToWidth(session.agent, 7), 8)) +
|
|
2789
|
+
chalk.yellow(padToWidth(truncateToWidth(mode || '-', TEAM_MODE_W), TEAM_MODE_W + 1)) +
|
|
2790
|
+
(badges ? badges + ' ' : '') +
|
|
2791
|
+
(glyph ? glyph + ' ' : '') +
|
|
2792
|
+
statusCell +
|
|
2793
|
+
padToWidth(chalk.white(truncateToWidth(head, topicW)), topicW) +
|
|
2794
|
+
' ' +
|
|
2795
|
+
when.text);
|
|
2796
|
+
}
|
|
2797
|
+
/** The header line for one team group (or the residual no-team bucket). */
|
|
2798
|
+
function teamGroupHeader(g, glyph, labelById) {
|
|
2799
|
+
const count = chalk.gray(`(${g.sessions.length})`);
|
|
2800
|
+
if (g.kind === 'noTeam') {
|
|
2801
|
+
// These carry the isTeamOrigin flag but no teammate meta.json — a Task
|
|
2802
|
+
// sub-agent's transcript is never indexed, so in practice this bucket is
|
|
2803
|
+
// headless `agents run` spawns or teammates whose meta aged out. Named
|
|
2804
|
+
// honestly rather than claimed as real teammates.
|
|
2805
|
+
return (chalk.magenta('▸') + ' ' + chalk.magenta.bold(NO_TEAM_GROUP_KEY) + ' ' + count +
|
|
2806
|
+
' ' + chalk.gray('team-flagged spawns with no team record (`agents run`, or aged-out teammates)'));
|
|
2807
|
+
}
|
|
2808
|
+
const bits = [chalk.cyan('▸') + ' ' + chalk.cyan.bold(g.key), count];
|
|
2809
|
+
if (glyph)
|
|
2810
|
+
bits.push(glyph);
|
|
2811
|
+
if (g.spawnerSessionId) {
|
|
2812
|
+
const who = labelById.get(g.spawnerSessionId) ?? g.spawnerSessionId.slice(0, 8);
|
|
2813
|
+
bits.push(chalk.gray(`by ${who}`));
|
|
2814
|
+
}
|
|
2815
|
+
bits.push(chalk.gray(`spawned ${formatRelativeTime(g.firstSpawnTs)}`));
|
|
2816
|
+
return bits.join(' ');
|
|
2817
|
+
}
|
|
2818
|
+
/**
|
|
2819
|
+
* The `agents sessions --teams` view: team-origin sessions grouped by team, the
|
|
2820
|
+
* richer display RUSH-1997 asks for. Each named team names its spawner (the
|
|
2821
|
+
* orchestrator session that created it) and spawn time, with its teammates'
|
|
2822
|
+
* mode + handle under it; team-flagged spawns that carry no teammate record fall
|
|
2823
|
+
* into a trailing no-team bucket, so a real teammate and a bare spawn are never
|
|
2824
|
+
* conflated. A printed report like `--tree`, not an interactive pick.
|
|
2825
|
+
*/
|
|
2826
|
+
function printTeamsView(pool, liveIndex, hiddenUnmanaged = 0) {
|
|
2827
|
+
const groups = groupSessionsByTeam(pool);
|
|
2828
|
+
if (groups.length === 0) {
|
|
2829
|
+
console.log(chalk.gray('No team sessions found.'));
|
|
2830
|
+
console.log(chalk.gray('Team sessions are spawned by `agents teams` — see `agents teams status`.'));
|
|
2831
|
+
return;
|
|
2832
|
+
}
|
|
2833
|
+
// Resolve a spawner session id to its human label from the pool. The
|
|
2834
|
+
// orchestrator may or may not be in view; fall back to the short id, the same
|
|
2835
|
+
// degradation the --active teams rows use (active.ts resolveOrchestratorLabels).
|
|
2836
|
+
const labelById = new Map();
|
|
2837
|
+
for (const s of pool) {
|
|
2838
|
+
const label = s.label || s.topic;
|
|
2839
|
+
if (label)
|
|
2840
|
+
labelById.set(s.id, cleanPreview(label));
|
|
2841
|
+
}
|
|
2842
|
+
const total = groups.reduce((n, g) => n + g.sessions.length, 0);
|
|
2843
|
+
const teamCount = groups.filter((g) => g.kind === 'team').length;
|
|
2844
|
+
const noTeam = groups.find((g) => g.kind === 'noTeam');
|
|
2845
|
+
const parts = [
|
|
2846
|
+
`${total} team session${total === 1 ? '' : 's'}`,
|
|
2847
|
+
`${teamCount} team${teamCount === 1 ? '' : 's'}`,
|
|
2848
|
+
];
|
|
2849
|
+
if (noTeam)
|
|
2850
|
+
parts.push(`${noTeam.sessions.length} without a team record`);
|
|
2851
|
+
console.log(chalk.gray(parts.join(' · ') + '\n'));
|
|
2852
|
+
let first = true;
|
|
2853
|
+
for (const g of groups) {
|
|
2854
|
+
if (!first)
|
|
2855
|
+
console.log();
|
|
2856
|
+
first = false;
|
|
2857
|
+
const { glyph } = liveGlyphAndPreview(liveIndex?.get(g.sessions[0].id));
|
|
2858
|
+
console.log(teamGroupHeader(g, glyph, labelById));
|
|
2859
|
+
for (const s of g.sessions)
|
|
2860
|
+
console.log(teamMemberRow(s, liveIndex?.get(s.id)));
|
|
2861
|
+
}
|
|
2862
|
+
console.log();
|
|
2863
|
+
console.log(chalk.gray('newest-active team first · resume any row with `agents sessions resume <id>`'));
|
|
2864
|
+
if (hiddenUnmanaged > 0)
|
|
2865
|
+
console.log(chalk.gray(formatUnmanagedHiddenFooter(hiddenUnmanaged)));
|
|
2866
|
+
}
|
|
2602
2867
|
function buildFilterOptions(options) {
|
|
2603
2868
|
const opts = {};
|
|
2604
2869
|
if (options.include)
|
|
@@ -2973,15 +3238,6 @@ export async function pickSessionInteractive(sessions, message = 'Search session
|
|
|
2973
3238
|
throw err;
|
|
2974
3239
|
}
|
|
2975
3240
|
}
|
|
2976
|
-
/**
|
|
2977
|
-
* The machine a picked session lives on when its transcript is on that peer's
|
|
2978
|
-
* disk (folded in over the live fan-out), else undefined. Keys off `_remote`,
|
|
2979
|
-
* NOT `machine !== local`: a synced mirror is machine-tagged too, but its file
|
|
2980
|
-
* is a local mirror path, so it must be read/resumed locally like any other.
|
|
2981
|
-
*/
|
|
2982
|
-
function remoteMachineOf(session) {
|
|
2983
|
-
return session._remote ? session.machine : undefined;
|
|
2984
|
-
}
|
|
2985
3241
|
/** True when the peer wasn't a dialable device; prints one clear line so a
|
|
2986
3242
|
* remote pick never dead-ends silently. */
|
|
2987
3243
|
function warnNoPeerTarget(machine, session) {
|
|
@@ -3007,31 +3263,50 @@ export async function handlePickedSession(picked) {
|
|
|
3007
3263
|
console.log(chalk.gray(`Watch for it with: agents sessions --active${picked.session.machine ? ` --host ${picked.session.machine}` : ''}`));
|
|
3008
3264
|
return;
|
|
3009
3265
|
}
|
|
3010
|
-
//
|
|
3011
|
-
//
|
|
3012
|
-
//
|
|
3013
|
-
//
|
|
3014
|
-
|
|
3015
|
-
if (
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
warnNoPeerTarget(remote, picked.session);
|
|
3020
|
-
}
|
|
3021
|
-
else {
|
|
3022
|
-
console.log(chalk.gray(`Resuming ${picked.session.shortId} on ${remote} over SSH...`));
|
|
3023
|
-
const rc = await runOnPeer(['sessions', 'resume', picked.session.shortId], remote, { tty: true });
|
|
3266
|
+
// Reading and resuming are on DIFFERENT machines' terms, and conflating them
|
|
3267
|
+
// is what RUSH-2022 was. Reading follows the FILE: a synced mirror sits on
|
|
3268
|
+
// this disk, so only a live fan-out row (`_remote`) has to be read on the
|
|
3269
|
+
// peer. Resuming follows the HARNESS STATE, which is on the owning machine
|
|
3270
|
+
// whatever the transcript's location — a mirror included.
|
|
3271
|
+
if (picked.action === 'view') {
|
|
3272
|
+
const readFrom = picked.session._remote ? picked.session.machine : undefined;
|
|
3273
|
+
if (readFrom) {
|
|
3274
|
+
const rc = await runOnPeer(['sessions', picked.session.shortId, '--markdown'], readFrom);
|
|
3024
3275
|
if (rc === 'no-target')
|
|
3025
|
-
warnNoPeerTarget(
|
|
3276
|
+
warnNoPeerTarget(readFrom, picked.session);
|
|
3277
|
+
return;
|
|
3026
3278
|
}
|
|
3027
|
-
return;
|
|
3028
|
-
}
|
|
3029
|
-
if (picked.action === 'view') {
|
|
3030
3279
|
await renderSession(picked.session, 'summary', {});
|
|
3031
3280
|
return;
|
|
3032
3281
|
}
|
|
3282
|
+
if (await resumeOnOwnerIfRemote(picked.session))
|
|
3283
|
+
return;
|
|
3033
3284
|
await resumeSessionInPlace(picked.session);
|
|
3034
3285
|
}
|
|
3286
|
+
/**
|
|
3287
|
+
* Resume `session` on the machine that owns it, when that is not this one.
|
|
3288
|
+
* Returns true when it handled the resume (hopped, or reported an unreachable
|
|
3289
|
+
* owner), false when the session is local and the caller should take over here.
|
|
3290
|
+
*
|
|
3291
|
+
* The one hop for a foreground, one-session-at-a-time resume — shared by the
|
|
3292
|
+
* `agents sessions` picker and by `sessions resume`'s no-tab-backend path, which
|
|
3293
|
+
* would otherwise reach `resumeSessionInPlace` and be refused (RUSH-2022).
|
|
3294
|
+
*/
|
|
3295
|
+
export async function resumeOnOwnerIfRemote(session) {
|
|
3296
|
+
const owner = sessionOwnerDevice(session);
|
|
3297
|
+
if (!owner)
|
|
3298
|
+
return false;
|
|
3299
|
+
console.log(chalk.gray(`Resuming ${session.shortId} on ${owner} over SSH...`));
|
|
3300
|
+
// `agents resume <id>` is the strict single-session path — one pick, one
|
|
3301
|
+
// resume. (`sessions resume <shortId>` would re-open a picker over there.)
|
|
3302
|
+
const rc = await runOnPeer(['resume', session.id], owner, {
|
|
3303
|
+
tty: true,
|
|
3304
|
+
env: { [RESUME_PINNED_ENV]: '1' },
|
|
3305
|
+
});
|
|
3306
|
+
if (rc === 'no-target')
|
|
3307
|
+
warnNoPeerTarget(owner, session);
|
|
3308
|
+
return true;
|
|
3309
|
+
}
|
|
3035
3310
|
/**
|
|
3036
3311
|
* Resume a session in the current terminal — a foreground takeover of this
|
|
3037
3312
|
* process. Used by the single-select picker and by `sessions resume` when the
|
|
@@ -3040,6 +3315,20 @@ export async function handlePickedSession(picked) {
|
|
|
3040
3315
|
* version-pinned launcher is genuinely missing.
|
|
3041
3316
|
*/
|
|
3042
3317
|
export async function resumeSessionInPlace(session) {
|
|
3318
|
+
// This function is the LOCAL takeover, and every caller is responsible for
|
|
3319
|
+
// routing a peer-owned session before it gets here (the picker above,
|
|
3320
|
+
// `agents resume`, `sessions attach`). Reaching it with one anyway means a
|
|
3321
|
+
// caller skipped that step, so refuse rather than start the harness against
|
|
3322
|
+
// state this box does not have — the `fs.existsSync(session.cwd)` fallback
|
|
3323
|
+
// just below is exactly how that used to pass unnoticed, quietly swapping in
|
|
3324
|
+
// `process.cwd()` when the recorded directory was a peer's path (RUSH-2022).
|
|
3325
|
+
const owner = sessionOwnerDevice(session);
|
|
3326
|
+
if (owner) {
|
|
3327
|
+
console.error(chalk.red(`Session ${session.shortId} belongs to ${owner} — it cannot resume on this machine.`));
|
|
3328
|
+
console.error(chalk.gray(` Resume it there: agents resume ${session.id}`));
|
|
3329
|
+
process.exitCode = 1;
|
|
3330
|
+
return;
|
|
3331
|
+
}
|
|
3043
3332
|
const cwd = session.cwd && fs.existsSync(session.cwd)
|
|
3044
3333
|
? session.cwd
|
|
3045
3334
|
: process.cwd();
|
|
@@ -3459,7 +3748,7 @@ function scoreSessionQuery(session, terms) {
|
|
|
3459
3748
|
* the project you specified AND elsewhere, producing an ambiguity error
|
|
3460
3749
|
* even though the user already pointed at the correct scope.
|
|
3461
3750
|
*/
|
|
3462
|
-
function applyScopeFilters(sessions, scope) {
|
|
3751
|
+
export function applyScopeFilters(sessions, scope) {
|
|
3463
3752
|
let filtered = sessions;
|
|
3464
3753
|
if (scope.project) {
|
|
3465
3754
|
const projectQuery = scope.project.toLowerCase();
|
|
@@ -3482,8 +3771,21 @@ function applyScopeFilters(sessions, scope) {
|
|
|
3482
3771
|
return true;
|
|
3483
3772
|
});
|
|
3484
3773
|
}
|
|
3774
|
+
if (scope.routine) {
|
|
3775
|
+
filtered = filtered.filter((session) => session.origin === 'routine');
|
|
3776
|
+
if (typeof scope.routine === 'string') {
|
|
3777
|
+
const names = [...new Set(filtered.map((session) => session.routineName).filter((name) => !!name))];
|
|
3778
|
+
const selected = resolveRoutineName(scope.routine, names);
|
|
3779
|
+
filtered = selected
|
|
3780
|
+
? filtered.filter((session) => session.routineName === selected)
|
|
3781
|
+
: [];
|
|
3782
|
+
}
|
|
3783
|
+
}
|
|
3485
3784
|
return filtered;
|
|
3486
3785
|
}
|
|
3786
|
+
export function artifactLookupScope(agent, project, routine) {
|
|
3787
|
+
return { agent, project, routine };
|
|
3788
|
+
}
|
|
3487
3789
|
async function renderArtifactsGlobal(query, listAll, name, scope) {
|
|
3488
3790
|
const spinner = ora().start();
|
|
3489
3791
|
const tracker = createScanProgressTracker(FIND_VERBS, 'session', spinner);
|
|
@@ -3929,9 +4231,9 @@ export function registerSessionsCommands(program) {
|
|
|
3929
4231
|
.option('--opencode', 'Shorthand for --agent opencode')
|
|
3930
4232
|
.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.')
|
|
3931
4233
|
.option('--unmanaged', "Also show sessions from your own ~/.<agent> installs (hidden once agents-cli manages that agent)")
|
|
3932
|
-
.option('--team, --teams', '
|
|
4234
|
+
.option('--team, --teams', 'Show team-spawned sessions (hidden by default), grouped by team — each team names its spawner and spawn time, teammates show their mode + handle, and team-flagged spawns with no teammate record sink into a (no team) bucket. --flat/--tree keep the plain inline table')
|
|
3933
4235
|
.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.")
|
|
3934
|
-
.option('--routine', 'Show
|
|
4236
|
+
.option('--routines, --routine [name]', 'Show routine-run sessions; omit the name on a TTY to pick one interactively (fuzzy name matching)')
|
|
3935
4237
|
.option('-p, --project <name>', 'Filter by project name (searches across all directories)')
|
|
3936
4238
|
.option('--skill <name>', 'Only sessions that invoked this skill (matches a bare name or a namespaced plugin skill\'s short name, e.g. --skill design finds rush:design)')
|
|
3937
4239
|
.option('--plugin <name>', 'Only sessions that used a skill/command owned by this plugin')
|
|
@@ -4014,12 +4316,18 @@ export function registerSessionsCommands(program) {
|
|
|
4014
4316
|
# Search across every directory, not just this project
|
|
4015
4317
|
agents sessions "topic" --all
|
|
4016
4318
|
|
|
4319
|
+
# Team-spawned sessions, grouped by team (spawner + spawn time per team,
|
|
4320
|
+
# teammate mode/handle per row; team-flagged spawns with no team record
|
|
4321
|
+
# in a trailing (no team) bucket)
|
|
4322
|
+
agents sessions --teams
|
|
4323
|
+
|
|
4017
4324
|
# Who spawned which team: an orchestrator row carries team:<name>, and a
|
|
4018
4325
|
# teammate row [<team>/<handle>]. --in-team narrows to one team's lineage.
|
|
4019
4326
|
agents sessions --in-team redesign --teams
|
|
4020
4327
|
|
|
4021
4328
|
# Show routine-run sessions and open one by routine run id
|
|
4022
4329
|
agents sessions --routine --all
|
|
4330
|
+
agents sessions --routine nightly-review --all
|
|
4023
4331
|
agents sessions 2026-07-21T10-30-00-000Z
|
|
4024
4332
|
|
|
4025
4333
|
# Export for analysis
|
|
@@ -4066,7 +4374,7 @@ export function registerSessionsCommands(program) {
|
|
|
4066
4374
|
- --first and --last are mutually exclusive.
|
|
4067
4375
|
- A filter flag (--include/--exclude/--first/--last) without --markdown/--json defaults to --markdown output.
|
|
4068
4376
|
- --cloud sources from Rush Cloud captured runs instead of local disk.
|
|
4069
|
-
- --routine shows
|
|
4377
|
+
- --routine [name] shows transcripts archived from routine runs. On a TTY, omit the name to pick a routine; a name accepts exact, substring, or unambiguous typo matches. --routines is an alias. Routine rows also resolve by run id.
|
|
4070
4378
|
- Without --teams, team-spawned sessions are hidden by default.
|
|
4071
4379
|
`,
|
|
4072
4380
|
});
|