@phnx-labs/agents-cli 1.22.32 → 1.22.34
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 +55 -0
- package/README.md +2 -1
- package/dist/bin/agents +0 -0
- package/dist/commands/accounts.js +3 -3
- package/dist/commands/browser-sessions-picker.d.ts +16 -0
- package/dist/commands/browser-sessions-picker.js +179 -0
- package/dist/commands/browser.js +9 -4
- package/dist/commands/hosts.js +1 -5
- package/dist/commands/inspect.js +174 -41
- package/dist/commands/message.d.ts +6 -1
- package/dist/commands/message.js +60 -3
- package/dist/commands/sessions-browser.js +2 -2
- package/dist/commands/sessions.d.ts +54 -4
- package/dist/commands/sessions.js +274 -58
- package/dist/commands/ssh.js +17 -5
- package/dist/commands/teams.d.ts +28 -0
- package/dist/commands/teams.js +148 -13
- package/dist/commands/upgrade.d.ts +7 -0
- package/dist/commands/upgrade.js +10 -0
- package/dist/commands/watchdog.d.ts +2 -0
- package/dist/commands/watchdog.js +112 -27
- package/dist/index.js +51 -59
- package/dist/lib/agents.js +6 -0
- package/dist/lib/browser/sessions-list.d.ts +81 -0
- package/dist/lib/browser/sessions-list.js +179 -4
- package/dist/lib/daemon.js +45 -5
- package/dist/lib/devices/connect.d.ts +33 -0
- package/dist/lib/devices/connect.js +61 -3
- package/dist/lib/devices/doctor-findings.d.ts +4 -2
- package/dist/lib/devices/doctor-findings.js +4 -2
- package/dist/lib/exec.js +63 -6
- package/dist/lib/help.d.ts +3 -2
- package/dist/lib/help.js +4 -0
- package/dist/lib/hosts/dispatch.d.ts +2 -32
- package/dist/lib/hosts/dispatch.js +6 -61
- package/dist/lib/hosts/tasks.d.ts +7 -0
- package/dist/lib/hosts/tasks.js +9 -0
- package/dist/lib/mailbox-target.d.ts +27 -0
- package/dist/lib/mailbox-target.js +21 -0
- package/dist/lib/mcp.d.ts +10 -0
- package/dist/lib/mcp.js +21 -2
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/migrate.d.ts +11 -0
- package/dist/lib/migrate.js +40 -0
- package/dist/lib/project-root.d.ts +47 -0
- package/dist/lib/project-root.js +68 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/agent.d.ts +9 -2
- package/dist/lib/secrets/agent.js +52 -7
- package/dist/lib/secrets/reaper.d.ts +24 -3
- package/dist/lib/secrets/reaper.js +55 -6
- package/dist/lib/session/discover.js +12 -0
- package/dist/lib/session/render.d.ts +2 -0
- package/dist/lib/session/render.js +1 -1
- package/dist/lib/session/team-filter.d.ts +11 -0
- package/dist/lib/session/team-filter.js +10 -0
- package/dist/lib/shims.js +9 -2
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +8 -3
- package/dist/lib/startup/root-command.d.ts +3 -0
- package/dist/lib/startup/root-command.js +10 -0
- package/dist/lib/teams/agents.d.ts +136 -6
- package/dist/lib/teams/agents.js +324 -58
- package/dist/lib/teams/worktree.d.ts +39 -2
- package/dist/lib/teams/worktree.js +60 -4
- package/dist/lib/types.d.ts +11 -0
- package/dist/lib/versions.js +2 -2
- package/dist/lib/watchdog/history.d.ts +20 -0
- package/dist/lib/watchdog/history.js +46 -0
- package/dist/lib/watchdog/log.d.ts +16 -1
- package/dist/lib/watchdog/log.js +82 -2
- package/dist/lib/watchdog/runner.d.ts +12 -0
- package/dist/lib/watchdog/runner.js +20 -0
- package/package.json +1 -1
|
@@ -13,7 +13,7 @@ import * as path from 'path';
|
|
|
13
13
|
import { spawn } from 'child_process';
|
|
14
14
|
import { Option } from 'commander';
|
|
15
15
|
import chalk from 'chalk';
|
|
16
|
-
import { truncate, padRight } from '../lib/format.js';
|
|
16
|
+
import { truncate, padRight, humanDuration } from '../lib/format.js';
|
|
17
17
|
import { sanitizeForTerminal } from '../lib/redact.js';
|
|
18
18
|
import { resolveProjectKey } from '../lib/project-key.js';
|
|
19
19
|
import { listProjectDefs, resolveProjectNameForCwd } from '../lib/projects.js';
|
|
@@ -33,17 +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, groupSessionsByTeam, NO_TEAM_GROUP_KEY, } from '../lib/session/team-filter.js';
|
|
36
|
+
import { filterTeamSessions, shouldShowTeamSessions, 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
|
-
import { renderConversationMarkdown, renderSummary, renderSummaryHeader, computeSummaryStats, renderJson, filterEvents, parseRoleList, linkPath, linkUrl, shortenModel } from '../lib/session/render.js';
|
|
40
|
+
import { renderConversationMarkdown, renderSummary, renderSummaryHeader, computeSummaryStats, renderJson, filterEvents, parseRoleList, linkPath, linkUrl, shortenModel, formatTokenCount } from '../lib/session/render.js';
|
|
41
41
|
import { linearIssueUrl } from '../lib/session/linear.js';
|
|
42
42
|
import { sessionOwnerDevice, RESUME_PINNED_ENV } from '../lib/session/resume-owner.js';
|
|
43
43
|
import { renderMarkdown } from '../lib/markdown.js';
|
|
44
44
|
import { AGENTS, colorAgent, resolveAgentName } from '../lib/agents.js';
|
|
45
45
|
import { getShimsDir } from '../lib/state.js';
|
|
46
|
-
import { listJobs, listJobsWithRuns, listRuns } from '../lib/routines.js';
|
|
46
|
+
import { listJobs, listJobsWithRuns, listRuns, getRunDir } from '../lib/routines.js';
|
|
47
|
+
import { formatUsd } from '../lib/pricing/cost.js';
|
|
47
48
|
import { fuzzyMatch, FUZZY_PRESETS } from '../lib/fuzzy.js';
|
|
48
49
|
import { itemPicker } from '../lib/picker.js';
|
|
49
50
|
import { resolveSessionAlias } from '../lib/session/actor-sidecar.js';
|
|
@@ -72,7 +73,7 @@ import { registerSessionsBackfillCommand } from './sessions-backfill.js';
|
|
|
72
73
|
import { registerSessionsStatsCommand } from './sessions-stats.js';
|
|
73
74
|
import { registerSessionsInsightsCommand } from './insights.js';
|
|
74
75
|
import { registerSessionsOptimizeCommand } from './sessions-optimize.js';
|
|
75
|
-
import {
|
|
76
|
+
import { runBrowserSessionsCommand } from './browser-sessions-picker.js';
|
|
76
77
|
import { countToolProgramOccurrences, parseToolProgramCountClause, readToolIndexCoverage, searchToolCalls, TOOL_QUERY_MAX_CLAUSE_BYTES, TOOL_QUERY_MAX_CLAUSES, TOOL_QUERY_MAX_RESULT_SESSIONS, serializeToolSearchEnvelope, toolSearchRemoteReceiveBudget, } from '../lib/session/tool-index.js';
|
|
77
78
|
const SESSION_AGENT_FILTER_HELP = `Filter by agent, e.g. claude, codex, claude@2.0.65`;
|
|
78
79
|
function collectQueryClause(value, previous) {
|
|
@@ -1520,6 +1521,11 @@ export function hasNoBrowserDisqualifyingFlags(options, query) {
|
|
|
1520
1521
|
!options.skill &&
|
|
1521
1522
|
!options.plugin &&
|
|
1522
1523
|
!options.sort &&
|
|
1524
|
+
// --routine without a name owns a two-stage picker (routine -> run ->
|
|
1525
|
+
// session), and a named routine owns the run-grouped overview. The generic
|
|
1526
|
+
// browser can only flatten routine sessions into its ordinary row list, so
|
|
1527
|
+
// routing this flag there silently bypasses both routine-specific views.
|
|
1528
|
+
!options.routine &&
|
|
1523
1529
|
!options.artifacts &&
|
|
1524
1530
|
options.artifact === undefined &&
|
|
1525
1531
|
// --cloud lists a provider's tasks, not the transcript index, and
|
|
@@ -1626,33 +1632,238 @@ function safeListJobsWithRuns() {
|
|
|
1626
1632
|
return [];
|
|
1627
1633
|
}
|
|
1628
1634
|
}
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1635
|
+
/**
|
|
1636
|
+
* Resolve which routine the user is targeting for `--routine`. Returns `null` to
|
|
1637
|
+
* ABORT the command (a bad `--routine <name>` printed an error, or the user
|
|
1638
|
+
* cancelled the interactive pick). `{ name: null }` means "no routine was
|
|
1639
|
+
* selected, don't filter" — the degenerate bare `--routine` piped to a
|
|
1640
|
+
* non-interactive sink. `{ name }` is a chosen routine.
|
|
1641
|
+
*
|
|
1642
|
+
* The name universe is transcripts UNION every routine with a definition or a
|
|
1643
|
+
* run record (`safeListJobsWithRuns`), so a command-only or never-ran routine —
|
|
1644
|
+
* which produces no `SessionMeta` — still resolves and drills down.
|
|
1645
|
+
*/
|
|
1646
|
+
export async function selectRoutineTarget(sessions, routine, interactive) {
|
|
1632
1647
|
const routineNames = [...new Set([
|
|
1633
1648
|
...sessions.map((session) => session.routineName).filter((name) => !!name),
|
|
1634
1649
|
...safeListJobsWithRuns(),
|
|
1635
1650
|
])].sort((a, b) => a.localeCompare(b));
|
|
1636
|
-
let selectedRoutine = null;
|
|
1637
1651
|
if (typeof routine === 'string') {
|
|
1638
|
-
|
|
1639
|
-
if (!
|
|
1652
|
+
const resolved = resolveRoutineName(routine, routineNames);
|
|
1653
|
+
if (!resolved) {
|
|
1640
1654
|
console.error(chalk.red(`No routine matches "${routine}".`));
|
|
1641
1655
|
if (routineNames.length > 0)
|
|
1642
1656
|
console.error(chalk.gray(`Available routines: ${routineNames.join(', ')}`));
|
|
1643
1657
|
process.exitCode = 1;
|
|
1644
1658
|
return null;
|
|
1645
1659
|
}
|
|
1660
|
+
return { name: resolved };
|
|
1646
1661
|
}
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
if (!
|
|
1662
|
+
if (interactive) {
|
|
1663
|
+
const picked = await selectRoutineName(sessions);
|
|
1664
|
+
if (!picked)
|
|
1650
1665
|
return null;
|
|
1666
|
+
return { name: picked };
|
|
1651
1667
|
}
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1668
|
+
return { name: null };
|
|
1669
|
+
}
|
|
1670
|
+
export async function filterSessionsByRoutine(sessions, routine, interactive) {
|
|
1671
|
+
const target = await selectRoutineTarget(sessions, routine, interactive);
|
|
1672
|
+
if (!target)
|
|
1673
|
+
return null;
|
|
1674
|
+
return target.name
|
|
1675
|
+
? sessions.filter((session) => session.routineName === target.name)
|
|
1676
|
+
: sessions;
|
|
1677
|
+
}
|
|
1678
|
+
export function executionKind(meta) {
|
|
1679
|
+
if (meta.workflow)
|
|
1680
|
+
return 'workflow';
|
|
1681
|
+
if (meta.agent)
|
|
1682
|
+
return 'agent';
|
|
1683
|
+
return 'command';
|
|
1684
|
+
}
|
|
1685
|
+
/**
|
|
1686
|
+
* Reconcile a routine's canonical run records (`listRuns`, local disk) with the
|
|
1687
|
+
* indexed sessions for that routine (fleet-wide, keyed by `routineRunId`).
|
|
1688
|
+
*/
|
|
1689
|
+
export function buildRoutineDrilldown(name, sessions) {
|
|
1690
|
+
const runs = listRuns(name);
|
|
1691
|
+
const forRoutine = sessions.filter((s) => s.routineName === name);
|
|
1692
|
+
const byRun = new Map();
|
|
1693
|
+
for (const s of forRoutine) {
|
|
1694
|
+
const rid = s.routineRunId ?? s.timestamp;
|
|
1695
|
+
(byRun.get(rid) ?? byRun.set(rid, []).get(rid)).push(s);
|
|
1696
|
+
}
|
|
1697
|
+
const runIds = new Set(runs.map((r) => r.runId));
|
|
1698
|
+
const entries = runs
|
|
1699
|
+
.map((meta) => ({
|
|
1700
|
+
meta,
|
|
1701
|
+
sessions: (byRun.get(meta.runId) ?? []).sort((a, b) => (a.timestamp < b.timestamp ? 1 : -1)),
|
|
1702
|
+
}))
|
|
1703
|
+
// Newest run first: `startedAt`, then run id (an ISO-derived slot) as a tie-break.
|
|
1704
|
+
.sort((a, b) => {
|
|
1705
|
+
const at = a.meta.startedAt || a.meta.runId;
|
|
1706
|
+
const bt = b.meta.startedAt || b.meta.runId;
|
|
1707
|
+
return at < bt ? 1 : at > bt ? -1 : b.meta.runId.localeCompare(a.meta.runId);
|
|
1708
|
+
});
|
|
1709
|
+
const orphanSessions = buildRoutineRunGroups(forRoutine.filter((s) => !runIds.has(s.routineRunId ?? s.timestamp)));
|
|
1710
|
+
return {
|
|
1711
|
+
name,
|
|
1712
|
+
runs: entries,
|
|
1713
|
+
orphanSessions,
|
|
1714
|
+
runRecordCount: runs.length,
|
|
1715
|
+
linkedSessionCount: forRoutine.length,
|
|
1716
|
+
// "Agent routine" = anything that isn't purely command runs. A workflow run
|
|
1717
|
+
// carries no `agent` field but still expects a session, so key off
|
|
1718
|
+
// executionKind, not `agent` alone.
|
|
1719
|
+
isAgentRoutine: runs.some((r) => executionKind(r) !== 'command') || forRoutine.length > 0,
|
|
1720
|
+
};
|
|
1721
|
+
}
|
|
1722
|
+
/** Glyph + word for a run's terminal status. */
|
|
1723
|
+
function runStatusCell(status) {
|
|
1724
|
+
switch (status) {
|
|
1725
|
+
case 'completed': return `${chalk.green('✓')} ${chalk.green('completed')}`;
|
|
1726
|
+
case 'running': return `${chalk.cyan('◍')} ${chalk.cyan('running')}`;
|
|
1727
|
+
case 'failed': return `${chalk.red('✗')} ${chalk.red('failed')}`;
|
|
1728
|
+
case 'timeout': return `${chalk.red('✗')} ${chalk.red('timeout')}`;
|
|
1729
|
+
case 'blocked': return `${chalk.yellow('⚠')} ${chalk.yellow('blocked')}`;
|
|
1730
|
+
case 'skipped': return `${chalk.gray('↷')} ${chalk.gray('skipped')}`;
|
|
1731
|
+
case 'missed': return `${chalk.gray('·')} ${chalk.gray('missed')}`;
|
|
1732
|
+
default: return chalk.gray(status);
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
/** Where a run's body executed: a host, a cloud provider, or this machine. */
|
|
1736
|
+
function runPlacement(meta) {
|
|
1737
|
+
if (meta.host)
|
|
1738
|
+
return `host:${meta.host}`;
|
|
1739
|
+
if (meta.cloudProvider)
|
|
1740
|
+
return `cloud:${meta.cloudProvider}`;
|
|
1741
|
+
return 'local';
|
|
1742
|
+
}
|
|
1743
|
+
/** The trigger clause: kind + who triggered it (dropping the noisy UNRESOLVED). */
|
|
1744
|
+
function runTrigger(meta) {
|
|
1745
|
+
const kind = meta.triggerKind ?? 'schedule';
|
|
1746
|
+
const who = meta.triggeredBy && !meta.triggeredBy.startsWith('UNRESOLVED') ? ` by ${meta.triggeredBy}` : '';
|
|
1747
|
+
return `${kind}${who}`;
|
|
1748
|
+
}
|
|
1749
|
+
/** The tail clause explaining a run's outcome (exit code / error / skip reason). */
|
|
1750
|
+
function runOutcomeDetail(meta) {
|
|
1751
|
+
if (meta.status === 'skipped' && meta.skipReason) {
|
|
1752
|
+
const ref = meta.activeRunId ? ` → ${meta.activeRunId}` : '';
|
|
1753
|
+
return `${meta.skipReason.replace(/_/g, ' ')}${ref}`;
|
|
1754
|
+
}
|
|
1755
|
+
if (meta.status === 'blocked') {
|
|
1756
|
+
return meta.readiness ? `${meta.readiness.code}: ${meta.readiness.message}` : (meta.errorMessage ?? 'not ready');
|
|
1757
|
+
}
|
|
1758
|
+
if (meta.status === 'missed')
|
|
1759
|
+
return 'daemon down at fire time';
|
|
1760
|
+
if (meta.errorMessage)
|
|
1761
|
+
return meta.errorMessage;
|
|
1762
|
+
// A clean `exit 0` is already implied by the green "completed" status — only a
|
|
1763
|
+
// non-zero code carries information worth a tail clause.
|
|
1764
|
+
if (typeof meta.exitCode === 'number' && meta.exitCode !== 0)
|
|
1765
|
+
return `exit ${meta.exitCode}`;
|
|
1766
|
+
return '';
|
|
1767
|
+
}
|
|
1768
|
+
/** Compact per-session metadata line under a linked session row. */
|
|
1769
|
+
function linkedSessionMeta(session) {
|
|
1770
|
+
const parts = [];
|
|
1771
|
+
parts.push(session.version ? `${session.agent} v${session.version}` : session.agent);
|
|
1772
|
+
if (session.account)
|
|
1773
|
+
parts.push(session.account);
|
|
1774
|
+
if (session.model)
|
|
1775
|
+
parts.push(shortenModel(session.model));
|
|
1776
|
+
if (typeof session.outputTokens === 'number')
|
|
1777
|
+
parts.push(`${formatTokenCount(session.outputTokens)} out`);
|
|
1778
|
+
else if (typeof session.tokenCount === 'number')
|
|
1779
|
+
parts.push(`${formatTokenCount(session.tokenCount)} tok`);
|
|
1780
|
+
if (typeof session.costUsd === 'number' && session.costUsd > 0)
|
|
1781
|
+
parts.push(formatUsd(session.costUsd));
|
|
1782
|
+
if (typeof session.durationMs === 'number' && session.durationMs > 0)
|
|
1783
|
+
parts.push(humanDuration(session.durationMs));
|
|
1784
|
+
if (typeof session.toolCallCount === 'number')
|
|
1785
|
+
parts.push(`${session.toolCallCount} tools`);
|
|
1786
|
+
return chalk.gray(' ' + parts.join(' · '));
|
|
1787
|
+
}
|
|
1788
|
+
/**
|
|
1789
|
+
* Render the routine drilldown: canonical run history first, each run linked to
|
|
1790
|
+
* its indexed agent session(s). This is the run/session seam made visible — a
|
|
1791
|
+
* command routine shows its runs and states plainly that no session is expected;
|
|
1792
|
+
* an agent routine shows the same runs plus each run's session metadata.
|
|
1793
|
+
*/
|
|
1794
|
+
export function printRoutineDrilldown(drill, liveIndex, opts = {}) {
|
|
1795
|
+
const runWord = drill.runRecordCount === 1 ? 'run record' : 'run records';
|
|
1796
|
+
const sessWord = drill.linkedSessionCount === 1 ? 'linked session' : 'linked sessions';
|
|
1797
|
+
console.log(`${chalk.cyan.bold(drill.name)} ` +
|
|
1798
|
+
chalk.gray(`${drill.runRecordCount} ${runWord} · ${drill.linkedSessionCount} ${sessWord}`));
|
|
1799
|
+
// A routine with no session ever produced is a command routine — say so up
|
|
1800
|
+
// front so the empty session column reads as expected, not as missing data.
|
|
1801
|
+
if (!drill.isAgentRoutine) {
|
|
1802
|
+
console.log(chalk.gray('Command routine — runs execute a shell command; no agent session is produced.'));
|
|
1803
|
+
}
|
|
1804
|
+
console.log();
|
|
1805
|
+
if (drill.runs.length === 0 && drill.orphanSessions.length === 0) {
|
|
1806
|
+
console.log(chalk.gray('No run records yet for this routine.'));
|
|
1807
|
+
return;
|
|
1808
|
+
}
|
|
1809
|
+
for (const entry of drill.runs) {
|
|
1810
|
+
const m = entry.meta;
|
|
1811
|
+
console.log(`${chalk.cyan('▸')} ${chalk.cyan.bold(m.runId)} ` +
|
|
1812
|
+
`${runStatusCell(m.status)} ` +
|
|
1813
|
+
chalk.gray(`${runTrigger(m)} · ${formatRelativeTime(m.startedAt)}`));
|
|
1814
|
+
const kind = executionKind(m);
|
|
1815
|
+
const detailParts = [kind];
|
|
1816
|
+
if (typeof m.duration === 'number' && m.duration > 0)
|
|
1817
|
+
detailParts.push(humanDuration(m.duration));
|
|
1818
|
+
detailParts.push(runPlacement(m));
|
|
1819
|
+
const outcome = runOutcomeDetail(m);
|
|
1820
|
+
if (outcome)
|
|
1821
|
+
detailParts.push(outcome);
|
|
1822
|
+
console.log(chalk.gray(' ' + detailParts.join(' · ')));
|
|
1823
|
+
// Log/report access — both are children of the run dir that a pre-execution
|
|
1824
|
+
// terminal (blocked/skipped/missed) never wrote, so gate each on existence
|
|
1825
|
+
// rather than pointing at a file that isn't there.
|
|
1826
|
+
const runDir = getRunDir(drill.name, m.runId);
|
|
1827
|
+
const logHints = [];
|
|
1828
|
+
if (fs.existsSync(path.join(runDir, 'stdout.log')))
|
|
1829
|
+
logHints.push(`log: ${path.join(runDir, 'stdout.log')}`);
|
|
1830
|
+
if (fs.existsSync(path.join(runDir, 'report.md')))
|
|
1831
|
+
logHints.push(`report: ${path.join(runDir, 'report.md')}`);
|
|
1832
|
+
if (logHints.length > 0)
|
|
1833
|
+
console.log(chalk.gray(' ' + logHints.join(' · ')));
|
|
1834
|
+
if (entry.sessions.length > 0) {
|
|
1835
|
+
for (const session of entry.sessions) {
|
|
1836
|
+
console.log(treeSessionRow(session, liveIndex?.get(session.id)));
|
|
1837
|
+
console.log(linkedSessionMeta(session));
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
else if (kind !== 'command') {
|
|
1841
|
+
// An agent run that produced no session — say why (the run terminal
|
|
1842
|
+
// explains it), never fake a session row.
|
|
1843
|
+
console.log(chalk.gray(' no agent session archived for this run'));
|
|
1844
|
+
}
|
|
1845
|
+
console.log();
|
|
1846
|
+
}
|
|
1847
|
+
if (drill.orphanSessions.length > 0) {
|
|
1848
|
+
console.log(chalk.gray('Sessions with no local run record (run archived on another host):'));
|
|
1849
|
+
for (const group of drill.orphanSessions) {
|
|
1850
|
+
console.log(`${chalk.cyan('▸')} ${chalk.cyan.bold(group.runId)} ` +
|
|
1851
|
+
chalk.gray(`${group.sessions.length} session${group.sessions.length === 1 ? '' : 's'} · ${formatRelativeTime(group.timestamp)}`));
|
|
1852
|
+
for (const session of group.sessions) {
|
|
1853
|
+
console.log(treeSessionRow(session, liveIndex?.get(session.id)));
|
|
1854
|
+
console.log(linkedSessionMeta(session));
|
|
1855
|
+
}
|
|
1856
|
+
console.log();
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
console.log(chalk.gray(`Run history from .history/runs/${drill.name}/ · resume a session with agents sessions resume <id>.`));
|
|
1860
|
+
// Every listing path must say what it dropped (sessions.ts invariant). Under
|
|
1861
|
+
// --routine team-origin sessions are shown (hiddenCount is 0), but unmanaged
|
|
1862
|
+
// installs may still be hidden — surface both footers when non-zero.
|
|
1863
|
+
if (opts.hiddenCount && opts.hiddenCount > 0)
|
|
1864
|
+
console.log(chalk.gray(formatTeamHiddenFooter(opts.hiddenCount)));
|
|
1865
|
+
if (opts.hiddenUnmanaged && opts.hiddenUnmanaged > 0)
|
|
1866
|
+
console.log(chalk.gray(formatUnmanagedHiddenFooter(opts.hiddenUnmanaged)));
|
|
1656
1867
|
}
|
|
1657
1868
|
/** The canonical `ag sessions …` command for a set of flags — the twin of the
|
|
1658
1869
|
* browser's `y` hotkey (see --print-cmd). Normalizes to the stable flag form. */
|
|
@@ -2293,6 +2504,11 @@ limitSource) {
|
|
|
2293
2504
|
// silently returns nothing for any team older than that — so the flag widens its
|
|
2294
2505
|
// own scope, the way --all does, unless the caller set an explicit --limit.
|
|
2295
2506
|
const wantsWholeTeam = !!options.inTeam;
|
|
2507
|
+
// A routine is not tied to the shell's cwd: its archived sessions retain the
|
|
2508
|
+
// working directory in which the scheduled agent actually ran. Scope the
|
|
2509
|
+
// routine catalog across directories so invoking it from ~/.agents/.system,
|
|
2510
|
+
// a project repo, or $HOME yields the same routine history.
|
|
2511
|
+
const wantsWholeRoutine = !!options.routine;
|
|
2296
2512
|
// `--limit` has a commander default, so an untouched flag still arrives as a
|
|
2297
2513
|
// string and truthiness can't tell it from a typed one. Commander records where
|
|
2298
2514
|
// each value came from, which is the only signal that distinguishes an explicit
|
|
@@ -2301,7 +2517,7 @@ limitSource) {
|
|
|
2301
2517
|
const userSetLimit = limitSource === 'cli' || limitSource === 'env';
|
|
2302
2518
|
const limit = wantsOverview
|
|
2303
2519
|
? OVERVIEW_POOL_LIMIT
|
|
2304
|
-
: parseInt(userSetLimit ? options.limit : wantsWholeTeam ? String(WHOLE_TEAM_POOL_LIMIT) : DEFAULT_LIMIT, 10);
|
|
2520
|
+
: parseInt(userSetLimit ? options.limit : wantsWholeTeam || wantsWholeRoutine ? String(WHOLE_TEAM_POOL_LIMIT) : DEFAULT_LIMIT, 10);
|
|
2305
2521
|
if (toolEvidenceMode && (!Number.isSafeInteger(limit) || limit < 1 || limit > TOOL_QUERY_MAX_RESULT_SESSIONS)) {
|
|
2306
2522
|
console.error(chalk.red(`Tool search --limit must be from 1 to ${TOOL_QUERY_MAX_RESULT_SESSIONS}.`));
|
|
2307
2523
|
process.exitCode = 1;
|
|
@@ -2311,7 +2527,7 @@ limitSource) {
|
|
|
2311
2527
|
// --since still narrows. Non-overview keeps the prior interactive-30d default.
|
|
2312
2528
|
const since = wantsOverview
|
|
2313
2529
|
? options.since
|
|
2314
|
-
: (options.since ?? (isInteractive && !options.all && !wantsWholeTeam ? '30d' : undefined));
|
|
2530
|
+
: (options.since ?? (isInteractive && !options.all && !wantsWholeTeam && !wantsWholeRoutine ? '30d' : undefined));
|
|
2315
2531
|
const toolSpansDevices = toolEvidenceMode
|
|
2316
2532
|
&& (options.fleet || (options.host?.length ?? 0) > 0);
|
|
2317
2533
|
const toolSortError = toolSearchFleetSortError(options.sort, toolSpansDevices);
|
|
@@ -2335,12 +2551,12 @@ limitSource) {
|
|
|
2335
2551
|
// --in-team spans directories by construction: a team's teammates run in
|
|
2336
2552
|
// their own worktrees, so scoping to the current cwd hides most of the
|
|
2337
2553
|
// lineage the flag exists to show.
|
|
2338
|
-
all: pathFilter ? undefined : options.all || wantsWholeTeam || toolSpansDevices,
|
|
2554
|
+
all: pathFilter ? undefined : options.all || wantsWholeTeam || wantsWholeRoutine || toolSpansDevices,
|
|
2339
2555
|
cwd: process.cwd(),
|
|
2340
2556
|
// Default overview scopes to the current repo SUBTREE (prefix match), so a
|
|
2341
2557
|
// monorepo shows its sub-projects grouped instead of collapsing to the one
|
|
2342
2558
|
// exact-cwd project. `--all` clears the prefix and spans the whole index.
|
|
2343
|
-
cwdPrefix: pathFilter ?? (wantsOverview && !options.all && !wantsWholeTeam && !toolSpansDevices ? process.cwd() : undefined),
|
|
2559
|
+
cwdPrefix: pathFilter ?? (wantsOverview && !options.all && !wantsWholeTeam && !wantsWholeRoutine && !toolSpansDevices ? process.cwd() : undefined),
|
|
2344
2560
|
project: options.project,
|
|
2345
2561
|
since,
|
|
2346
2562
|
until: options.until,
|
|
@@ -2373,7 +2589,7 @@ limitSource) {
|
|
|
2373
2589
|
const readOptions = {
|
|
2374
2590
|
...scope,
|
|
2375
2591
|
limit,
|
|
2376
|
-
excludeTeamOrigin: !options
|
|
2592
|
+
excludeTeamOrigin: !shouldShowTeamSessions(options),
|
|
2377
2593
|
onProgress: tracker.onProgress,
|
|
2378
2594
|
includeUnmanaged: options.unmanaged,
|
|
2379
2595
|
onHiddenUnmanaged: (n) => { hiddenUnmanaged = n; },
|
|
@@ -2390,7 +2606,7 @@ limitSource) {
|
|
|
2390
2606
|
// meta.json in ~/.agents/teams/agents whose is_team_origin flag was
|
|
2391
2607
|
// never set (legacy rows). Keep the in-memory pass so those are still
|
|
2392
2608
|
// enriched/hidden.
|
|
2393
|
-
const { visible: visibleSessions } = filterTeamSessions(sessions,
|
|
2609
|
+
const { visible: visibleSessions } = filterTeamSessions(sessions, shouldShowTeamSessions(options));
|
|
2394
2610
|
sessions = visibleSessions;
|
|
2395
2611
|
// --in-team spans both ends of the lineage, so it can't be one SQL predicate:
|
|
2396
2612
|
// the orchestrator matches on the scan-derived `spawnedTeam` column, while a
|
|
@@ -2458,7 +2674,7 @@ limitSource) {
|
|
|
2458
2674
|
}
|
|
2459
2675
|
// Under --in-team the visible list is one team, so the whole-index team-origin
|
|
2460
2676
|
// count would be a non-sequitur next to it.
|
|
2461
|
-
const hiddenCount = options
|
|
2677
|
+
const hiddenCount = shouldShowTeamSessions(options) || options.inTeam
|
|
2462
2678
|
? 0
|
|
2463
2679
|
: countSessionsInScope({ ...scope, onlyTeamOrigin: true });
|
|
2464
2680
|
// A typed routine scope must apply before smart ID routing. Otherwise an ID
|
|
@@ -2540,15 +2756,36 @@ limitSource) {
|
|
|
2540
2756
|
}
|
|
2541
2757
|
}
|
|
2542
2758
|
if (options.routine) {
|
|
2543
|
-
const
|
|
2544
|
-
if (!
|
|
2545
|
-
return;
|
|
2546
|
-
|
|
2759
|
+
const target = await selectRoutineTarget(sessions, options.routine, isInteractive);
|
|
2760
|
+
if (!target)
|
|
2761
|
+
return; // bad --routine <name> or cancelled pick (message printed)
|
|
2762
|
+
if (target.name) {
|
|
2763
|
+
sessions = sessions.filter((s) => s.routineName === target.name);
|
|
2764
|
+
// A routine browse renders the run-first drilldown — canonical run history
|
|
2765
|
+
// plus each run's linked session — instead of dead-ending a
|
|
2766
|
+
// command/pre-session routine into the generic empty copy. An explicit
|
|
2767
|
+
// --flat/--tree or a search query keeps the scoped session listing/picker,
|
|
2768
|
+
// so `--routine x --flat` prints the plain table and `--routine x <id>`
|
|
2769
|
+
// resolves a specific session within the routine.
|
|
2770
|
+
if (!searchQuery && !options.flat && !options.tree) {
|
|
2771
|
+
const liveIndex = await maybeLiveIndex(options);
|
|
2772
|
+
printRoutineDrilldown(buildRoutineDrilldown(target.name, sessions), liveIndex, { hiddenCount, hiddenUnmanaged });
|
|
2773
|
+
return;
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
// target.name === null: bare `--routine` piped to a non-interactive sink —
|
|
2777
|
+
// no routine chosen, fall through to the normal listing unfiltered.
|
|
2547
2778
|
}
|
|
2548
2779
|
if (sessions.length === 0) {
|
|
2549
2780
|
if (pathFilter) {
|
|
2550
2781
|
console.log(chalk.gray(`No sessions found for ${pathFilter}.`));
|
|
2551
2782
|
}
|
|
2783
|
+
else if (options.routine) {
|
|
2784
|
+
// A routine scoped to a flat/tree/query listing with no matching session
|
|
2785
|
+
// (e.g. a command routine has none) — say so in routine terms, not the
|
|
2786
|
+
// generic cwd/--all copy, and point at the run-history drilldown.
|
|
2787
|
+
console.log(chalk.gray('No indexed agent sessions for this routine. Drop --flat/--tree to see its run history.'));
|
|
2788
|
+
}
|
|
2552
2789
|
else {
|
|
2553
2790
|
console.log(chalk.gray(formatNoSessionsMessage(options.all, options.project)));
|
|
2554
2791
|
}
|
|
@@ -2574,10 +2811,6 @@ limitSource) {
|
|
|
2574
2811
|
// Interact/resume via `agents sessions <project>` or `agents sessions resume`.
|
|
2575
2812
|
if (wantsOverview) {
|
|
2576
2813
|
const liveIndex = await maybeLiveIndex(options);
|
|
2577
|
-
if (options.routine) {
|
|
2578
|
-
printRoutineRunOverview(sessions, liveIndex, { hiddenCount, hiddenUnmanaged });
|
|
2579
|
-
return;
|
|
2580
|
-
}
|
|
2581
2814
|
// Per-project row cap is fixed (--limit carries a default of 50 and drives
|
|
2582
2815
|
// the fetch pool, not the display); `--all` expands every group instead.
|
|
2583
2816
|
printSessionOverview(sessions, hiddenCount, liveIndex, { perProjectCap: OVERVIEW_ROWS_PER_PROJECT, expand: !!options.all, hiddenUnmanaged });
|
|
@@ -2901,25 +3134,6 @@ function printSessionOverview(pool, hiddenCount, liveIndex, opts) {
|
|
|
2901
3134
|
if (opts.hiddenUnmanaged)
|
|
2902
3135
|
console.log(chalk.gray(formatUnmanagedHiddenFooter(opts.hiddenUnmanaged)));
|
|
2903
3136
|
}
|
|
2904
|
-
export function printRoutineRunOverview(sessions, liveIndex, opts = {}) {
|
|
2905
|
-
const groups = buildRoutineRunGroups(sessions);
|
|
2906
|
-
console.log(chalk.gray(`${sessions.length} session${sessions.length === 1 ? '' : 's'} · ${groups.length} routine run${groups.length === 1 ? '' : 's'} · newest run first\n`));
|
|
2907
|
-
let first = true;
|
|
2908
|
-
for (const group of groups) {
|
|
2909
|
-
if (!first)
|
|
2910
|
-
console.log();
|
|
2911
|
-
first = false;
|
|
2912
|
-
console.log(`${chalk.cyan('▸')} ${chalk.cyan.bold(group.runId)} ` +
|
|
2913
|
-
`${chalk.gray(`${group.sessions.length} session${group.sessions.length === 1 ? '' : 's'} · ${formatRelativeTime(group.timestamp)}`)}`);
|
|
2914
|
-
for (const session of group.sessions)
|
|
2915
|
-
console.log(treeSessionRow(session, liveIndex?.get(session.id)));
|
|
2916
|
-
}
|
|
2917
|
-
console.log(chalk.gray('\nGrouped by routine run id and timestamp.'));
|
|
2918
|
-
if (opts.hiddenCount)
|
|
2919
|
-
console.log(chalk.gray(formatTeamHiddenFooter(opts.hiddenCount)));
|
|
2920
|
-
if (opts.hiddenUnmanaged)
|
|
2921
|
-
console.log(chalk.gray(formatUnmanagedHiddenFooter(opts.hiddenUnmanaged)));
|
|
2922
|
-
}
|
|
2923
3137
|
function printSessionTable(sessions, hiddenCount = 0, tree = false, liveIndex) {
|
|
2924
3138
|
if (tree) {
|
|
2925
3139
|
// Group by directory; drop the id/version columns from view. The short id
|
|
@@ -4607,9 +4821,9 @@ export function registerSessionsCommands(program) {
|
|
|
4607
4821
|
# teammate row [<team>/<handle>]. --in-team narrows to one team's lineage.
|
|
4608
4822
|
agents sessions --in-team redesign --teams
|
|
4609
4823
|
|
|
4610
|
-
#
|
|
4611
|
-
agents sessions --routine
|
|
4612
|
-
agents sessions --routine nightly-review
|
|
4824
|
+
# Pick a routine across every directory, then open one of its run sessions
|
|
4825
|
+
agents sessions --routine
|
|
4826
|
+
agents sessions --routine nightly-review
|
|
4613
4827
|
agents sessions 2026-07-21T10-30-00-000Z
|
|
4614
4828
|
|
|
4615
4829
|
# Export for analysis
|
|
@@ -4657,14 +4871,16 @@ export function registerSessionsCommands(program) {
|
|
|
4657
4871
|
- --first and --last are mutually exclusive.
|
|
4658
4872
|
- A filter flag (--include/--exclude/--first/--last) without --markdown/--json defaults to --markdown output.
|
|
4659
4873
|
- --cloud sources from Rush Cloud captured runs instead of local disk.
|
|
4660
|
-
- --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.
|
|
4874
|
+
- --routine [name] spans every directory and 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.
|
|
4661
4875
|
- Without --teams, team-spawned sessions are hidden by default.
|
|
4662
4876
|
`,
|
|
4663
4877
|
});
|
|
4664
4878
|
sessionsCmd.action(async (query, options, command) => {
|
|
4665
4879
|
if (options.browser) {
|
|
4666
|
-
// Alias for `agents browser sessions`: a profile positional narrows to one
|
|
4667
|
-
|
|
4880
|
+
// Alias for `agents browser sessions`: a profile positional narrows to one
|
|
4881
|
+
// profile. `--no-interactive` is already a top-level sessionsCmd option
|
|
4882
|
+
// (options.interactive), so it carries through for free.
|
|
4883
|
+
await runBrowserSessionsCommand({ profile: query, json: options.json, interactive: options.interactive });
|
|
4668
4884
|
return;
|
|
4669
4885
|
}
|
|
4670
4886
|
await sessionsAction(query, options, command.getOptionValueSource('limit'));
|
package/dist/commands/ssh.js
CHANGED
|
@@ -28,6 +28,7 @@ import { DEFAULT_SERVE_PORT } from '../lib/serve/server.js';
|
|
|
28
28
|
import { nodeToDeviceInput, parseTailscaleStatus, tailscaleStatusJson, } from '../lib/devices/tailscale.js';
|
|
29
29
|
import { defaultPickerChecked, localLoginUser, planDeviceReconciliation, runDeviceSync, withDefaultUser } from '../lib/devices/sync.js';
|
|
30
30
|
import { resolveDeviceTarget, splitUserHost } from '../lib/devices/resolve-target.js';
|
|
31
|
+
import { deriveMirroredCwd } from '../lib/project-root.js';
|
|
31
32
|
import { clearPendingSentinel } from '../lib/devices/pending.js';
|
|
32
33
|
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
33
34
|
import { hostNameFor, renderSshConfig } from '../lib/devices/ssh-config.js';
|
|
@@ -1598,15 +1599,21 @@ function registerSshWrapper(program) {
|
|
|
1598
1599
|
.allowUnknownOption()
|
|
1599
1600
|
.addHelpText('after', `
|
|
1600
1601
|
Examples:
|
|
1601
|
-
agents ssh
|
|
1602
|
-
agents ssh win-mini
|
|
1603
|
-
agents ssh
|
|
1604
|
-
agents ssh
|
|
1602
|
+
agents ssh yosemite-s0 # interactive login (mirrors your project dir)
|
|
1603
|
+
agents ssh win-mini # interactive login
|
|
1604
|
+
agents ssh win-mini hostname # run a command (PowerShell on Windows)
|
|
1605
|
+
agents ssh yosemite-s0 uptime # run a command (POSIX)
|
|
1606
|
+
agents ssh auto # affinity-pick a device (same engine as 'agents run --device auto')
|
|
1605
1607
|
|
|
1606
1608
|
Devices come from 'agents devices'. Password auth pulls the secret from a
|
|
1607
1609
|
secrets bundle via an askpass shim — the password never touches argv.
|
|
1608
1610
|
'auto' picks a remote device by 14-day usage; a pick landing on this machine
|
|
1609
1611
|
is refused with a clear message instead of self-dialing.
|
|
1612
|
+
|
|
1613
|
+
An interactive login with no command mirrors the home-relative directory you
|
|
1614
|
+
launched from — 'agents ssh yosemite-s0' from ~/src/app lands in ~/src/app on
|
|
1615
|
+
the target when it exists, else the remote home. Same portable-cwd rule as
|
|
1616
|
+
'agents run --host'. Passing a command keeps the remote home.
|
|
1610
1617
|
`)
|
|
1611
1618
|
.action(async (name, cmd) => {
|
|
1612
1619
|
// Hidden askpass bridge: ssh execs the shim, which re-invokes us here.
|
|
@@ -1664,7 +1671,12 @@ is refused with a clear message instead of self-dialing.
|
|
|
1664
1671
|
ensureManagedKnownHostsDir();
|
|
1665
1672
|
const addr = hostNameFor(device);
|
|
1666
1673
|
const pinned = addr ? isHostPinned(addr) : false;
|
|
1667
|
-
|
|
1674
|
+
// Interactive login (no cmd): mirror the caller's project directory on
|
|
1675
|
+
// the target when the same home-relative checkout exists there, matching
|
|
1676
|
+
// `agents run --host` (deriveMirroredCwd). Best-effort — a missing dir
|
|
1677
|
+
// falls back to the remote home. An explicit `cmd` keeps its cwd (RUSH-2412).
|
|
1678
|
+
const mirrorCwd = cmd.length === 0 ? deriveMirroredCwd(process.cwd()) : undefined;
|
|
1679
|
+
const { args, env } = buildSshInvocation(device, cmd, shim, { pinned }, { interactiveCwd: mirrorCwd });
|
|
1668
1680
|
// Interactive login: make the local terminal's terminfo (e.g.
|
|
1669
1681
|
// xterm-ghostty) available on the remote so backspace/colors/clear work.
|
|
1670
1682
|
// Best-effort + cached per host — never blocks the login (see terminfo.ts).
|
package/dist/commands/teams.d.ts
CHANGED
|
@@ -59,6 +59,34 @@ export type TeamMessageRoute = {
|
|
|
59
59
|
* - any actionable + no message -> need-message
|
|
60
60
|
*/
|
|
61
61
|
export declare function decideTeamMessageRoute(status: AgentStatus, hasMessage: boolean): TeamMessageRoute;
|
|
62
|
+
/**
|
|
63
|
+
* Tell the user a worktree from a failed `teams add` is still on disk, and give
|
|
64
|
+
* them the exact commands to remove it. Reached only when we could not remove it
|
|
65
|
+
* ourselves — a leftover `agents/<name>` branch is what makes the retry fail with
|
|
66
|
+
* `fatal: a branch ... already exists`, so it must never be silent (RUSH-2356).
|
|
67
|
+
*/
|
|
68
|
+
/**
|
|
69
|
+
* Tear down a worktree a failed operation left behind — but ONLY if it is a
|
|
70
|
+
* genuine orphan.
|
|
71
|
+
*
|
|
72
|
+
* Extracted because this guard was hand-duplicated at two call sites (`teams
|
|
73
|
+
* add` and the pr-watch fixer) and the copy at the fixer path was written
|
|
74
|
+
* WITHOUT it, which is RUSH-2356 reoccurring at the one site that skipped the
|
|
75
|
+
* check. One implementation, one place to get it right.
|
|
76
|
+
*
|
|
77
|
+
* The two errors here are not symmetric, so the guard deliberately fails
|
|
78
|
+
* CLOSED: a spurious "claimed" strands a branch a human can delete, while a
|
|
79
|
+
* spurious "unclaimed" runs `git worktree remove --force` over a live
|
|
80
|
+
* teammate's checkout and destroys uncommitted work. So an unreadable record
|
|
81
|
+
* means "leave it alone and tell the user", never "assume it is free".
|
|
82
|
+
*
|
|
83
|
+
* A staged teammate is exactly the case that makes this necessary: `spawn()`
|
|
84
|
+
* persists its meta.json and only THEN runs the retention pass, which refreshes
|
|
85
|
+
* every sibling and can throw on a distributed one — so a failure can land here
|
|
86
|
+
* with a live, durably-recorded, merely-pending `--after` teammate already
|
|
87
|
+
* owning this worktree.
|
|
88
|
+
*/
|
|
89
|
+
export declare function tearDownOrphanWorktree(mgr: AgentManager, baseCwd: string, name: string): Promise<void>;
|
|
62
90
|
/**
|
|
63
91
|
* Register the generic cloud dispatcher — staged cloud teammates get
|
|
64
92
|
* dispatched when their --after deps resolve, using repo/branch stored on
|