@phnx-labs/agents-cli 1.22.33 → 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 +49 -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.d.ts +54 -4
- package/dist/commands/sessions.js +252 -46
- 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/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';
|
|
@@ -37,13 +37,14 @@ import { filterTeamSessions, shouldShowTeamSessions, safeTeamText, groupSessions
|
|
|
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) {
|
|
@@ -1631,33 +1632,238 @@ function safeListJobsWithRuns() {
|
|
|
1631
1632
|
return [];
|
|
1632
1633
|
}
|
|
1633
1634
|
}
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
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) {
|
|
1637
1647
|
const routineNames = [...new Set([
|
|
1638
1648
|
...sessions.map((session) => session.routineName).filter((name) => !!name),
|
|
1639
1649
|
...safeListJobsWithRuns(),
|
|
1640
1650
|
])].sort((a, b) => a.localeCompare(b));
|
|
1641
|
-
let selectedRoutine = null;
|
|
1642
1651
|
if (typeof routine === 'string') {
|
|
1643
|
-
|
|
1644
|
-
if (!
|
|
1652
|
+
const resolved = resolveRoutineName(routine, routineNames);
|
|
1653
|
+
if (!resolved) {
|
|
1645
1654
|
console.error(chalk.red(`No routine matches "${routine}".`));
|
|
1646
1655
|
if (routineNames.length > 0)
|
|
1647
1656
|
console.error(chalk.gray(`Available routines: ${routineNames.join(', ')}`));
|
|
1648
1657
|
process.exitCode = 1;
|
|
1649
1658
|
return null;
|
|
1650
1659
|
}
|
|
1660
|
+
return { name: resolved };
|
|
1651
1661
|
}
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
if (!
|
|
1662
|
+
if (interactive) {
|
|
1663
|
+
const picked = await selectRoutineName(sessions);
|
|
1664
|
+
if (!picked)
|
|
1655
1665
|
return null;
|
|
1666
|
+
return { name: picked };
|
|
1656
1667
|
}
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
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)));
|
|
1661
1867
|
}
|
|
1662
1868
|
/** The canonical `ag sessions …` command for a set of flags — the twin of the
|
|
1663
1869
|
* browser's `y` hotkey (see --print-cmd). Normalizes to the stable flag form. */
|
|
@@ -2550,15 +2756,36 @@ limitSource) {
|
|
|
2550
2756
|
}
|
|
2551
2757
|
}
|
|
2552
2758
|
if (options.routine) {
|
|
2553
|
-
const
|
|
2554
|
-
if (!
|
|
2555
|
-
return;
|
|
2556
|
-
|
|
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.
|
|
2557
2778
|
}
|
|
2558
2779
|
if (sessions.length === 0) {
|
|
2559
2780
|
if (pathFilter) {
|
|
2560
2781
|
console.log(chalk.gray(`No sessions found for ${pathFilter}.`));
|
|
2561
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
|
+
}
|
|
2562
2789
|
else {
|
|
2563
2790
|
console.log(chalk.gray(formatNoSessionsMessage(options.all, options.project)));
|
|
2564
2791
|
}
|
|
@@ -2584,10 +2811,6 @@ limitSource) {
|
|
|
2584
2811
|
// Interact/resume via `agents sessions <project>` or `agents sessions resume`.
|
|
2585
2812
|
if (wantsOverview) {
|
|
2586
2813
|
const liveIndex = await maybeLiveIndex(options);
|
|
2587
|
-
if (options.routine) {
|
|
2588
|
-
printRoutineRunOverview(sessions, liveIndex, { hiddenCount, hiddenUnmanaged });
|
|
2589
|
-
return;
|
|
2590
|
-
}
|
|
2591
2814
|
// Per-project row cap is fixed (--limit carries a default of 50 and drives
|
|
2592
2815
|
// the fetch pool, not the display); `--all` expands every group instead.
|
|
2593
2816
|
printSessionOverview(sessions, hiddenCount, liveIndex, { perProjectCap: OVERVIEW_ROWS_PER_PROJECT, expand: !!options.all, hiddenUnmanaged });
|
|
@@ -2911,25 +3134,6 @@ function printSessionOverview(pool, hiddenCount, liveIndex, opts) {
|
|
|
2911
3134
|
if (opts.hiddenUnmanaged)
|
|
2912
3135
|
console.log(chalk.gray(formatUnmanagedHiddenFooter(opts.hiddenUnmanaged)));
|
|
2913
3136
|
}
|
|
2914
|
-
export function printRoutineRunOverview(sessions, liveIndex, opts = {}) {
|
|
2915
|
-
const groups = buildRoutineRunGroups(sessions);
|
|
2916
|
-
console.log(chalk.gray(`${sessions.length} session${sessions.length === 1 ? '' : 's'} · ${groups.length} routine run${groups.length === 1 ? '' : 's'} · newest run first\n`));
|
|
2917
|
-
let first = true;
|
|
2918
|
-
for (const group of groups) {
|
|
2919
|
-
if (!first)
|
|
2920
|
-
console.log();
|
|
2921
|
-
first = false;
|
|
2922
|
-
console.log(`${chalk.cyan('▸')} ${chalk.cyan.bold(group.runId)} ` +
|
|
2923
|
-
`${chalk.gray(`${group.sessions.length} session${group.sessions.length === 1 ? '' : 's'} · ${formatRelativeTime(group.timestamp)}`)}`);
|
|
2924
|
-
for (const session of group.sessions)
|
|
2925
|
-
console.log(treeSessionRow(session, liveIndex?.get(session.id)));
|
|
2926
|
-
}
|
|
2927
|
-
console.log(chalk.gray('\nGrouped by routine run id and timestamp.'));
|
|
2928
|
-
if (opts.hiddenCount)
|
|
2929
|
-
console.log(chalk.gray(formatTeamHiddenFooter(opts.hiddenCount)));
|
|
2930
|
-
if (opts.hiddenUnmanaged)
|
|
2931
|
-
console.log(chalk.gray(formatUnmanagedHiddenFooter(opts.hiddenUnmanaged)));
|
|
2932
|
-
}
|
|
2933
3137
|
function printSessionTable(sessions, hiddenCount = 0, tree = false, liveIndex) {
|
|
2934
3138
|
if (tree) {
|
|
2935
3139
|
// Group by directory; drop the id/version columns from view. The short id
|
|
@@ -4673,8 +4877,10 @@ export function registerSessionsCommands(program) {
|
|
|
4673
4877
|
});
|
|
4674
4878
|
sessionsCmd.action(async (query, options, command) => {
|
|
4675
4879
|
if (options.browser) {
|
|
4676
|
-
// Alias for `agents browser sessions`: a profile positional narrows to one
|
|
4677
|
-
|
|
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 });
|
|
4678
4884
|
return;
|
|
4679
4885
|
}
|
|
4680
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
|