@phnx-labs/agents-cli 1.22.29 → 1.22.31
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 +88 -0
- package/README.md +44 -5
- package/dist/bin/agents +0 -0
- package/dist/commands/accounts.d.ts +13 -0
- package/dist/commands/accounts.js +32 -0
- package/dist/commands/daemon.d.ts +18 -0
- package/dist/commands/daemon.js +581 -0
- package/dist/commands/exec.js +66 -20
- package/dist/commands/focus.d.ts +4 -1
- package/dist/commands/focus.js +19 -4
- package/dist/commands/routines.js +29 -11
- package/dist/commands/secrets.d.ts +37 -0
- package/dist/commands/secrets.js +86 -105
- package/dist/commands/sessions-bookmark.d.ts +20 -0
- package/dist/commands/{sessions-favorite.js → sessions-bookmark.js} +42 -42
- package/dist/commands/sessions-browser.d.ts +10 -8
- package/dist/commands/sessions-browser.js +61 -32
- package/dist/commands/sessions-picker.d.ts +33 -1
- package/dist/commands/sessions-picker.js +102 -27
- package/dist/commands/sessions-stats.js +1 -1
- package/dist/commands/sessions.d.ts +21 -8
- package/dist/commands/sessions.js +328 -74
- package/dist/commands/view.d.ts +11 -0
- package/dist/commands/view.js +56 -29
- package/dist/index.js +37 -2
- package/dist/lib/account-labels.d.ts +24 -0
- package/dist/lib/account-labels.js +72 -0
- package/dist/lib/agents.d.ts +32 -1
- package/dist/lib/agents.js +96 -31
- package/dist/lib/daemon-health.d.ts +24 -0
- package/dist/lib/daemon-health.js +84 -0
- package/dist/lib/daemon-ticks.d.ts +81 -0
- package/dist/lib/daemon-ticks.js +190 -0
- package/dist/lib/daemon.d.ts +68 -18
- package/dist/lib/daemon.js +303 -338
- package/dist/lib/device-config.d.ts +10 -0
- package/dist/lib/device-config.js +27 -0
- package/dist/lib/exec.d.ts +27 -0
- package/dist/lib/exec.js +49 -2
- package/dist/lib/hosts/dispatch.d.ts +4 -0
- package/dist/lib/hosts/dispatch.js +4 -0
- package/dist/lib/hosts/remote-cmd.js +1 -0
- package/dist/lib/hosts/run-target.d.ts +1 -0
- package/dist/lib/hosts/run-target.js +1 -0
- package/dist/lib/import.js +7 -6
- package/dist/lib/memory-cache.d.ts +19 -0
- package/dist/lib/memory-cache.js +31 -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/migrate.d.ts +1 -1
- package/dist/lib/migrate.js +13 -2
- package/dist/lib/picker.d.ts +6 -3
- package/dist/lib/picker.js +7 -2
- package/dist/lib/routine-activation.d.ts +2 -0
- package/dist/lib/routine-activation.js +16 -0
- package/dist/lib/runner.d.ts +18 -0
- package/dist/lib/runner.js +52 -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 +19 -1
- package/dist/lib/secrets/agent.js +32 -6
- package/dist/lib/secrets/scope.d.ts +3 -3
- package/dist/lib/secrets/scope.js +3 -3
- package/dist/lib/secrets/session-store.d.ts +0 -4
- package/dist/lib/secrets/session-store.js +0 -5
- package/dist/lib/session/{favorites.d.ts → bookmarks.d.ts} +15 -15
- package/dist/lib/session/{favorites.js → bookmarks.js} +23 -23
- package/dist/lib/session/db.d.ts +15 -0
- package/dist/lib/session/db.js +90 -15
- package/dist/lib/session/discover.js +91 -39
- package/dist/lib/session/parse.d.ts +63 -0
- package/dist/lib/session/parse.js +165 -20
- package/dist/lib/session/session-cache.d.ts +9 -6
- package/dist/lib/session/session-cache.js +23 -6
- package/dist/lib/shims.js +12 -0
- package/dist/lib/startup/command-registry.d.ts +15 -1
- package/dist/lib/startup/command-registry.js +49 -0
- package/dist/lib/usage-refresh.js +3 -2
- package/dist/lib/usage.d.ts +12 -10
- package/dist/lib/usage.js +63 -144
- package/package.json +4 -1
- package/dist/commands/sessions-favorite.d.ts +0 -20
|
@@ -46,17 +46,17 @@ import { getShimsDir } from '../lib/state.js';
|
|
|
46
46
|
import { fuzzyMatch, FUZZY_PRESETS } from '../lib/fuzzy.js';
|
|
47
47
|
import { itemPicker } from '../lib/picker.js';
|
|
48
48
|
import { resolveSessionAlias } from '../lib/session/actor-sidecar.js';
|
|
49
|
-
import { resolveVersionAliasLoose } from '../lib/versions.js';
|
|
49
|
+
import { listInstalledVersions, resolveVersionAliasLoose } from '../lib/versions.js';
|
|
50
50
|
import { getAgentsInvocation } from '../lib/daemon.js';
|
|
51
51
|
import { sessionRecoveryRunArgs } from '../lib/session/recovery.js';
|
|
52
52
|
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
53
|
-
import { sessionPicker, buildPreview, formatTodoCompact, githubRepoUrlFromCwd, } from './sessions-picker.js';
|
|
53
|
+
import { sessionPicker, buildPreview, loadSessionPreviewDigest, formatTodoCompact, githubRepoUrlFromCwd, } from './sessions-picker.js';
|
|
54
54
|
import { setHelpSections } from '../lib/help.js';
|
|
55
55
|
import { registerSessionsTailCommand } from './sessions-tail.js';
|
|
56
56
|
import { registerSessionsResumeCommand } from './sessions-resume.js';
|
|
57
57
|
import { registerSessionsForkCommand } from './fork.js';
|
|
58
|
-
import {
|
|
59
|
-
import {
|
|
58
|
+
import { registerSessionsBookmarkCommand } from './sessions-bookmark.js';
|
|
59
|
+
import { isBookmarked, listBookmarks } from '../lib/session/bookmarks.js';
|
|
60
60
|
import { registerGoCommand } from './go.js';
|
|
61
61
|
import { registerFocusCommand } from './focus.js';
|
|
62
62
|
import { registerReconnectCommand } from './reconnect.js';
|
|
@@ -95,6 +95,44 @@ function applyAgentShorthands(options) {
|
|
|
95
95
|
if (hit)
|
|
96
96
|
options.agent = hit;
|
|
97
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* Treat a positional `agent@version` as a structured filter only when it names
|
|
100
|
+
* a real installed version. Everything else remains ordinary free-text search.
|
|
101
|
+
*/
|
|
102
|
+
export function parseInstalledAgentVersionQuery(query, installedVersions = (agent) => (agent in AGENTS ? listInstalledVersions(agent) : [])) {
|
|
103
|
+
const trimmed = query?.trim();
|
|
104
|
+
if (!trimmed)
|
|
105
|
+
return undefined;
|
|
106
|
+
const at = trimmed.indexOf('@');
|
|
107
|
+
if (at <= 0 || at !== trimmed.lastIndexOf('@') || at === trimmed.length - 1)
|
|
108
|
+
return undefined;
|
|
109
|
+
const agentName = trimmed.slice(0, at).toLowerCase();
|
|
110
|
+
if (!SESSION_AGENTS.includes(agentName))
|
|
111
|
+
return undefined;
|
|
112
|
+
const agent = agentName;
|
|
113
|
+
const version = trimmed.slice(at + 1);
|
|
114
|
+
return installedVersions(agent).includes(version) ? `${agent}@${version}` : undefined;
|
|
115
|
+
}
|
|
116
|
+
function applyVersionFilters(query, options) {
|
|
117
|
+
const explicitVersion = options.version ?? options.sessionVersion;
|
|
118
|
+
if (explicitVersion) {
|
|
119
|
+
if (!options.agent) {
|
|
120
|
+
throw new Error('--version requires --agent (for example: --agent claude --version 2.1.181).');
|
|
121
|
+
}
|
|
122
|
+
if (options.agent.includes('@')) {
|
|
123
|
+
throw new Error('Pass the version either in --agent <agent@version> or with --version, not both.');
|
|
124
|
+
}
|
|
125
|
+
options.agent = `${options.agent}@${explicitVersion}`;
|
|
126
|
+
}
|
|
127
|
+
if (!options.agent) {
|
|
128
|
+
const positionalFilter = parseInstalledAgentVersionQuery(query);
|
|
129
|
+
if (positionalFilter) {
|
|
130
|
+
options.agent = positionalFilter;
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return query;
|
|
135
|
+
}
|
|
98
136
|
const CLAUDE_RESUME_MATCH_WINDOW_MS = 10 * 60_000;
|
|
99
137
|
const LOAD_VERBS = ['Loading', 'Scanning', 'Gathering', 'Indexing', 'Reading'];
|
|
100
138
|
const FIND_VERBS = ['Finding', 'Searching', 'Locating', 'Matching'];
|
|
@@ -1323,26 +1361,25 @@ async function renderActiveSessions(asJson, waitingOnly = false, opts = {}) {
|
|
|
1323
1361
|
const self = machineId();
|
|
1324
1362
|
const gathered = await gatherActiveSessions(opts);
|
|
1325
1363
|
const { remoteDeviceCount } = gathered;
|
|
1326
|
-
//
|
|
1364
|
+
// Backfill agent version, refs, created time, and routine provenance from the
|
|
1365
|
+
// historical index. Routine filtering needs that provenance before it narrows
|
|
1366
|
+
// the live pool; doing it here also enriches both JSON and human output.
|
|
1367
|
+
backfillActiveRowsFromIndex(gathered.sessions);
|
|
1368
|
+
// --bookmarks narrows the live view too. Applied HERE, not only in the
|
|
1327
1369
|
// browser: the browser is skipped for --json, --waiting, a pipe, a multi-host
|
|
1328
1370
|
// scope, and an SSH-fanout peer, and the flag silently did nothing on every
|
|
1329
|
-
// one of those paths — including `--active --
|
|
1371
|
+
// one of those paths — including `--active --bookmarks --json`, which is
|
|
1330
1372
|
// exactly what the browser's own `y` copy-cmd hands to an agent.
|
|
1331
|
-
const merged = opts.
|
|
1332
|
-
? gathered.sessions.filter((s) => !!s.sessionId &&
|
|
1373
|
+
const merged = opts.bookmarksOnly
|
|
1374
|
+
? gathered.sessions.filter((s) => !!s.sessionId && listBookmarks().has(s.sessionId))
|
|
1333
1375
|
: gathered.sessions;
|
|
1376
|
+
const routineFiltered = filterActiveSessionsByRoutine(merged, opts.routine);
|
|
1334
1377
|
// Status flags form a union. --waiting additionally retains its scriptable
|
|
1335
1378
|
// gate: exit non-zero when the union contains a session awaiting the user.
|
|
1336
1379
|
const statusFiltered = opts.statuses?.length
|
|
1337
|
-
?
|
|
1338
|
-
:
|
|
1380
|
+
? routineFiltered.filter((session) => opts.statuses.some((status) => matchesLiveStatus(session, status)))
|
|
1381
|
+
: routineFiltered.filter(isRunningLiveSession);
|
|
1339
1382
|
const sessions = statusFiltered;
|
|
1340
|
-
// Backfill agent version + ticket/PR/label/created onto the live rows from the
|
|
1341
|
-
// historical index (RUSH-2205) — a running process reports none of these, and
|
|
1342
|
-
// an orphan row usually lacks them. Done before both the JSON and human paths
|
|
1343
|
-
// so every consumer (incl. the SSH fan-out's remote --json) sees enriched rows;
|
|
1344
|
-
// transcripts sync across the fleet, so a remote row resolves from the local DB.
|
|
1345
|
-
backfillActiveRowsFromIndex(sessions);
|
|
1346
1383
|
if (asJson) {
|
|
1347
1384
|
// Resolve who is watching each local tmux pane before serializing: `viewingIn`
|
|
1348
1385
|
// is how a consumer distinguishes a session someone is looking at from one
|
|
@@ -1388,6 +1425,19 @@ async function renderActiveSessions(asJson, waitingOnly = false, opts = {}) {
|
|
|
1388
1425
|
if (waitingOnly && sessions.some(isAwaitingUser))
|
|
1389
1426
|
process.exitCode = 1;
|
|
1390
1427
|
}
|
|
1428
|
+
/** Apply the routine selector to enriched live rows for every non-browser active view. */
|
|
1429
|
+
export function filterActiveSessionsByRoutine(sessions, routine) {
|
|
1430
|
+
if (!routine)
|
|
1431
|
+
return sessions;
|
|
1432
|
+
const routineSessions = sessions.filter((session) => session.origin === 'routine' || !!session.routineName);
|
|
1433
|
+
if (typeof routine !== 'string')
|
|
1434
|
+
return routineSessions;
|
|
1435
|
+
const names = [...new Set(routineSessions.map((session) => session.routineName).filter((name) => !!name))];
|
|
1436
|
+
const selected = resolveRoutineName(routine, names);
|
|
1437
|
+
return selected
|
|
1438
|
+
? routineSessions.filter((session) => session.routineName === selected)
|
|
1439
|
+
: [];
|
|
1440
|
+
}
|
|
1391
1441
|
/** Match the status words users see, preserving activity's richer working signal. */
|
|
1392
1442
|
export function matchesLiveStatus(session, status) {
|
|
1393
1443
|
if (status === 'working')
|
|
@@ -1454,7 +1504,9 @@ export function isBareBrowserListing(options, query) {
|
|
|
1454
1504
|
*/
|
|
1455
1505
|
export function hasNoBrowserDisqualifyingFlags(options, query) {
|
|
1456
1506
|
return (!query &&
|
|
1457
|
-
|
|
1507
|
+
// A named team view is a flat selectable pool; the bare --teams report
|
|
1508
|
+
// stays grouped and printed because the browser cannot preserve its shape.
|
|
1509
|
+
(!options.teams || !!options.inTeam) &&
|
|
1458
1510
|
!options.flat &&
|
|
1459
1511
|
!options.tree &&
|
|
1460
1512
|
!options.markdown &&
|
|
@@ -1619,8 +1671,8 @@ function canonicalSessionsCommand(query, options) {
|
|
|
1619
1671
|
a.push('--local');
|
|
1620
1672
|
if (options.waiting)
|
|
1621
1673
|
a.push('--waiting');
|
|
1622
|
-
if (options.
|
|
1623
|
-
a.push('--
|
|
1674
|
+
if (options.bookmarks)
|
|
1675
|
+
a.push('--bookmarks');
|
|
1624
1676
|
const q = (query ?? '').trim();
|
|
1625
1677
|
if (q)
|
|
1626
1678
|
a.push(JSON.stringify(q));
|
|
@@ -1629,29 +1681,108 @@ function canonicalSessionsCommand(query, options) {
|
|
|
1629
1681
|
/** Resolve a session by id/query globally and print its compact preview (no pager).
|
|
1630
1682
|
* Backs `--preview` — the fast path for the "peek before resume" hot loop. */
|
|
1631
1683
|
export async function renderSessionPreview(query, scope) {
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
if (
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1684
|
+
let outcome = await resolveSessionMetadataValue(query, scope);
|
|
1685
|
+
// A just-created transcript may not have reached the incremental index yet.
|
|
1686
|
+
// Keep the indexed path hot, but repair a local cold miss once before saying
|
|
1687
|
+
// it does not exist. This also preserves Claude history-alias discovery.
|
|
1688
|
+
if (outcome.kind !== 'resolved'
|
|
1689
|
+
&& (!scope.hosts?.length || shouldIncludeLocal(scope.hosts, machineId()))) {
|
|
1690
|
+
const discovered = applyScopeFilters(await discoverSessions({ all: true, cwd: process.cwd(), limit: 5000 }), scope);
|
|
1691
|
+
const localMatches = resolveSessionQuery(discovered, query, { indexFallback: false }).matches
|
|
1692
|
+
.map(session => ({ ...session, machine: session.machine || machineId() }));
|
|
1693
|
+
const exact = localMatches.find(session => selectorAllowsEarlyExit(query)
|
|
1694
|
+
&& session.id.toLowerCase() === query.trim().toLowerCase());
|
|
1695
|
+
if (exact)
|
|
1696
|
+
outcome = { kind: 'resolved', session: exact };
|
|
1697
|
+
else if (outcome.kind === 'not-found' && localMatches.length > 0) {
|
|
1698
|
+
outcome = metadataResolveOutcome(localMatches, { sessions: [], unreachable: [] }, query);
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
if (outcome.kind === 'partial') {
|
|
1702
|
+
console.error(chalk.red(`Partial session resolution: ${outcome.failedPeers.join(', ')} did not answer.`));
|
|
1703
|
+
console.error(chalk.gray('No preview was rendered because the short ID may be ambiguous on an unreachable peer.'));
|
|
1704
|
+
process.exitCode = 2;
|
|
1705
|
+
return;
|
|
1706
|
+
}
|
|
1707
|
+
if (outcome.kind === 'not-found') {
|
|
1708
|
+
notFoundByIdMessage(query).forEach(l => console.error(l));
|
|
1709
|
+
process.exitCode = 1;
|
|
1710
|
+
return;
|
|
1711
|
+
}
|
|
1712
|
+
if (outcome.kind === 'ambiguous') {
|
|
1713
|
+
console.error(chalk.red(`Multiple sessions match "${query}" across the fleet:`));
|
|
1714
|
+
for (const candidate of outcome.candidates) {
|
|
1715
|
+
const match = candidate.hits[0].session;
|
|
1716
|
+
const machines = candidate.hits.map(hit => hit.machine).join(', ');
|
|
1717
|
+
console.error(chalk.cyan(` ${match.shortId} ${match.id}`) + chalk.gray(` ${machines} ${match.agent}${match.version ? ` ${match.version}` : ''}`));
|
|
1718
|
+
}
|
|
1719
|
+
console.error(chalk.gray('Pass the full session ID to narrow it down.'));
|
|
1720
|
+
process.exitCode = 1;
|
|
1721
|
+
return;
|
|
1722
|
+
}
|
|
1723
|
+
const session = outcome.session;
|
|
1724
|
+
if (session._remote && session.machine && session.machine !== machineId()) {
|
|
1725
|
+
const args = ['sessions', 'preview', session.id, '--local'];
|
|
1726
|
+
if (scope.json)
|
|
1727
|
+
args.push('--json');
|
|
1728
|
+
const rendered = await runOnPeer(args, session.machine);
|
|
1729
|
+
if (rendered === 'no-target') {
|
|
1730
|
+
console.error(chalk.red(`Session ${session.id} is on ${session.machine}, but that device is not reachable.`));
|
|
1731
|
+
process.exitCode = 1;
|
|
1732
|
+
}
|
|
1643
1733
|
return;
|
|
1644
1734
|
}
|
|
1645
1735
|
// Lead with the live status when the session is still running, so the preview
|
|
1646
1736
|
// says working / waiting / idle up front — not just the historical transcript.
|
|
1647
|
-
//
|
|
1648
|
-
//
|
|
1737
|
+
// The shared snapshot is accepted for at most 15 seconds. The durable preview
|
|
1738
|
+
// below contains no live status, so a long-lived process can never keep a
|
|
1739
|
+
// stale working/waiting headline in its transcript cache.
|
|
1649
1740
|
let live;
|
|
1650
1741
|
try {
|
|
1651
|
-
|
|
1742
|
+
const loaded = await loadLocalActiveSessions();
|
|
1743
|
+
live = indexActiveBySessionId(loaded.sessions).get(session.id);
|
|
1652
1744
|
}
|
|
1653
1745
|
catch { /* plain preview on any probe failure */ }
|
|
1654
|
-
|
|
1746
|
+
if (scope.json) {
|
|
1747
|
+
const { digest, error } = loadSessionPreviewDigest(session);
|
|
1748
|
+
console.log(JSON.stringify({
|
|
1749
|
+
schemaVersion: 1,
|
|
1750
|
+
session: {
|
|
1751
|
+
id: session.id,
|
|
1752
|
+
shortId: session.shortId,
|
|
1753
|
+
agent: session.agent,
|
|
1754
|
+
version: session.version,
|
|
1755
|
+
model: session.model,
|
|
1756
|
+
account: session.account,
|
|
1757
|
+
machine: session.machine ?? machineId(),
|
|
1758
|
+
cwd: session.cwd,
|
|
1759
|
+
project: session.project,
|
|
1760
|
+
gitBranch: session.gitBranch,
|
|
1761
|
+
createdAt: session.timestamp,
|
|
1762
|
+
lastActivity: session.lastActivity,
|
|
1763
|
+
durationMs: session.durationMs,
|
|
1764
|
+
messageCount: session.messageCount,
|
|
1765
|
+
tokenCount: session.tokenCount,
|
|
1766
|
+
costUsd: session.costUsd,
|
|
1767
|
+
label: session.label,
|
|
1768
|
+
ticketId: session.ticketId,
|
|
1769
|
+
prUrl: session.prUrl,
|
|
1770
|
+
},
|
|
1771
|
+
active: live ? {
|
|
1772
|
+
status: live.status,
|
|
1773
|
+
activity: live.activity,
|
|
1774
|
+
awaitingReason: live.awaitingReason,
|
|
1775
|
+
lastActivityMs: live.lastActivityMs,
|
|
1776
|
+
startedAtMs: live.startedAtMs,
|
|
1777
|
+
pid: live.pid,
|
|
1778
|
+
host: live.host,
|
|
1779
|
+
} : null,
|
|
1780
|
+
preview: digest ?? null,
|
|
1781
|
+
error: error ?? null,
|
|
1782
|
+
}));
|
|
1783
|
+
return;
|
|
1784
|
+
}
|
|
1785
|
+
const headline = formatLiveStatusHeadline(live, isBookmarked(session.id));
|
|
1655
1786
|
if (headline)
|
|
1656
1787
|
console.log(headline);
|
|
1657
1788
|
console.log(buildPreview(session));
|
|
@@ -1666,12 +1797,12 @@ export async function renderSessionPreview(query, scope) {
|
|
|
1666
1797
|
* reads "orphan" and knows it means "still running in tmux with no window
|
|
1667
1798
|
* attached", so those two spell it out.
|
|
1668
1799
|
*/
|
|
1669
|
-
export function formatLiveStatusHeadline(live,
|
|
1670
|
-
const star =
|
|
1800
|
+
export function formatLiveStatusHeadline(live, bookmarked = false) {
|
|
1801
|
+
const star = bookmarked ? chalk.yellow('★ ') : '';
|
|
1671
1802
|
// With no live row there is no status to lead with, so the star has to say
|
|
1672
1803
|
// what it means on its own — a bare `★` above a preview reads as noise.
|
|
1673
1804
|
if (!live)
|
|
1674
|
-
return
|
|
1805
|
+
return bookmarked ? chalk.yellow('★ bookmarked') : '';
|
|
1675
1806
|
const { glyph } = liveGlyphAndPreview(live);
|
|
1676
1807
|
const word = liveStatusWord(live) || live.status;
|
|
1677
1808
|
// One definition, shared with `--waiting`. A second local copy drifted: the
|
|
@@ -1879,6 +2010,14 @@ limitSource) {
|
|
|
1879
2010
|
// shorthands fold into --agent, and --device is an alias for --host (both
|
|
1880
2011
|
// resolve against the same device registry).
|
|
1881
2012
|
applyAgentShorthands(options);
|
|
2013
|
+
try {
|
|
2014
|
+
query = applyVersionFilters(query, options);
|
|
2015
|
+
}
|
|
2016
|
+
catch (error) {
|
|
2017
|
+
console.error(chalk.red(error instanceof Error ? error.message : String(error)));
|
|
2018
|
+
process.exitCode = 1;
|
|
2019
|
+
return;
|
|
2020
|
+
}
|
|
1882
2021
|
// --device / --devices both alias --host. A bare `all` / `fleet` sentinel means
|
|
1883
2022
|
// "search every peer" — which is already the default — so it resolves to no
|
|
1884
2023
|
// explicit host set rather than erroring on a device literally named "all".
|
|
@@ -1984,7 +2123,7 @@ limitSource) {
|
|
|
1984
2123
|
console.error(chalk.red('--preview requires a session id or query.'));
|
|
1985
2124
|
process.exit(1);
|
|
1986
2125
|
}
|
|
1987
|
-
await renderSessionPreview(query, { agent: options.agent, project: options.project, local: options.local });
|
|
2126
|
+
await renderSessionPreview(query, { agent: options.agent, project: options.project, local: options.local, hosts: options.host });
|
|
1988
2127
|
return;
|
|
1989
2128
|
}
|
|
1990
2129
|
if (liveOnly) {
|
|
@@ -2015,7 +2154,8 @@ limitSource) {
|
|
|
2015
2154
|
host: options.host,
|
|
2016
2155
|
since: options.since,
|
|
2017
2156
|
all: options.all,
|
|
2018
|
-
|
|
2157
|
+
bookmarks: options.bookmarks,
|
|
2158
|
+
routine: options.routine,
|
|
2019
2159
|
}), { local: options.local === true, hosts: options.host });
|
|
2020
2160
|
return;
|
|
2021
2161
|
}
|
|
@@ -2025,8 +2165,9 @@ limitSource) {
|
|
|
2025
2165
|
await renderActiveSessions(options.json === true, options.waiting === true, {
|
|
2026
2166
|
local: forceLocal,
|
|
2027
2167
|
hosts: options.host,
|
|
2028
|
-
|
|
2168
|
+
bookmarksOnly: options.bookmarks === true,
|
|
2029
2169
|
statuses: liveStatuses,
|
|
2170
|
+
routine: options.routine,
|
|
2030
2171
|
});
|
|
2031
2172
|
return;
|
|
2032
2173
|
}
|
|
@@ -2040,12 +2181,10 @@ limitSource) {
|
|
|
2040
2181
|
// printed/render paths (agents and scripts unaffected). An explicit --since seeds
|
|
2041
2182
|
// the browser's window so the flag is honored, not swallowed.
|
|
2042
2183
|
//
|
|
2043
|
-
// `--teams` diverts to the printed team-grouped report
|
|
2044
|
-
//
|
|
2045
|
-
//
|
|
2046
|
-
|
|
2047
|
-
// the picker so `<term> --teams` still searches.
|
|
2048
|
-
if (isBareBrowserListing(options, query) && !options.teams) {
|
|
2184
|
+
// Bare `--teams` still diverts to the printed team-grouped report because the
|
|
2185
|
+
// flat picker cannot preserve that shape. `--in-team <name> --teams` is one
|
|
2186
|
+
// flat lineage, so it qualifies through hasNoBrowserDisqualifyingFlags.
|
|
2187
|
+
if (isBareBrowserListing(options, query)) {
|
|
2049
2188
|
const { runSessionBrowser, bareBrowserSeed } = await import('./sessions-browser.js');
|
|
2050
2189
|
await runSessionBrowser(bareBrowserSeed({
|
|
2051
2190
|
teams: options.teams,
|
|
@@ -2054,7 +2193,8 @@ limitSource) {
|
|
|
2054
2193
|
since: options.since,
|
|
2055
2194
|
host: options.host,
|
|
2056
2195
|
inTeam: options.inTeam,
|
|
2057
|
-
|
|
2196
|
+
bookmarks: options.bookmarks,
|
|
2197
|
+
routine: options.routine,
|
|
2058
2198
|
}), { local: options.local === true, hosts: options.host });
|
|
2059
2199
|
return;
|
|
2060
2200
|
}
|
|
@@ -2095,6 +2235,12 @@ limitSource) {
|
|
|
2095
2235
|
await renderArtifactsGlobal(searchQuery, options.artifacts ?? false, options.artifact, artifactLookupScope(options.agent, options.project, options.routine));
|
|
2096
2236
|
return;
|
|
2097
2237
|
}
|
|
2238
|
+
// A supplied ID is already the lookup key. Resolve it directly from SQLite
|
|
2239
|
+
// and, when needed, the peer indexes before any broad transcript discovery.
|
|
2240
|
+
if (!toolEvidenceMode && searchQuery && looksLikeSessionId(searchQuery)) {
|
|
2241
|
+
await renderOneSession(searchQuery, mode, { agent: options.agent, project: options.project, routine: options.routine, filter: filterOpts, redact: options.redact, local: options.local, hosts: options.host });
|
|
2242
|
+
return;
|
|
2243
|
+
}
|
|
2098
2244
|
// When the user explicitly asks to render (via mode flag), resolve the
|
|
2099
2245
|
// query globally so sessions outside the default cwd/30d window are found.
|
|
2100
2246
|
if (wantsRender && searchQuery) {
|
|
@@ -2218,11 +2364,11 @@ limitSource) {
|
|
|
2218
2364
|
// read. Match either, after that pass has populated `teamOrigin`.
|
|
2219
2365
|
if (options.inTeam)
|
|
2220
2366
|
sessions = sessions.filter((s) => matchesTeam(s, options.inTeam));
|
|
2221
|
-
// --
|
|
2222
|
-
// emit, so `--
|
|
2223
|
-
if (options.
|
|
2224
|
-
const
|
|
2225
|
-
sessions = sessions.filter((s) =>
|
|
2367
|
+
// --bookmarks narrows to the bookmarked set. Applied here, before the JSON
|
|
2368
|
+
// emit, so `--bookmarks --json` is the machine-readable twin of the `b` key.
|
|
2369
|
+
if (options.bookmarks) {
|
|
2370
|
+
const bookmarks = listBookmarks();
|
|
2371
|
+
sessions = sessions.filter((s) => bookmarks.has(s.id));
|
|
2226
2372
|
}
|
|
2227
2373
|
if (toolEvidenceMode) {
|
|
2228
2374
|
const self = toolSelf;
|
|
@@ -2528,7 +2674,7 @@ function timeCell(age, topicSlack) {
|
|
|
2528
2674
|
* (tracker/PR ref, pulled out of the badge blob so refs align) is only rendered
|
|
2529
2675
|
* when `showTicket` — otherwise a listing with no refs would waste a column of
|
|
2530
2676
|
* dashes and needlessly truncate the topic. Worktree stays a trailing badge. */
|
|
2531
|
-
export function flatSessionRow(session, live, showTicket = false, cols = {},
|
|
2677
|
+
export function flatSessionRow(session, live, showTicket = false, cols = {}, bookmarked = false) {
|
|
2532
2678
|
const agentColor = colorAgent(session.agent);
|
|
2533
2679
|
const age = sessionAgeParts(session.timestamp, session.lastActivity);
|
|
2534
2680
|
const project = session.project || '-';
|
|
@@ -2565,18 +2711,18 @@ export function flatSessionRow(session, live, showTicket = false, cols = {}, fav
|
|
|
2565
2711
|
const width = terminalWidth();
|
|
2566
2712
|
const requestedModelW = cols.showModel ? (cols.modelWidth ?? PICKER_MODEL_MAX) : 0;
|
|
2567
2713
|
// Same conditional 2 cells as the picker's marker, for the same reason.
|
|
2568
|
-
const
|
|
2569
|
-
const
|
|
2714
|
+
const bookmarkW = cols.showBookmark ? 2 : 0;
|
|
2715
|
+
const bookmarkCell = cols.showBookmark ? (bookmarked ? chalk.yellow('★ ') : ' ') : '';
|
|
2570
2716
|
// Sized against the last-activity label alone, so the creation field is an
|
|
2571
2717
|
// additive decision the row makes only once it knows what space is left.
|
|
2572
|
-
const fixedW =
|
|
2718
|
+
const fixedW = bookmarkW + (10 + 9 + 8 + 16) + glyphW + statusW + machineW + ticketW + wtW + team.width + stringWidth(age.last) + 1;
|
|
2573
2719
|
const modelSlack = width - fixedW - MIN_TOPIC_W;
|
|
2574
2720
|
const modelW = requestedModelW <= modelSlack
|
|
2575
2721
|
? requestedModelW
|
|
2576
2722
|
: modelSlack >= PICKER_MODEL_MIN ? modelSlack : 0;
|
|
2577
2723
|
const when = timeCell(age, width - fixedW - modelW);
|
|
2578
2724
|
const topicW = Math.max(MIN_TOPIC_W, width - fixedW - modelW - when.extraW);
|
|
2579
|
-
return (
|
|
2725
|
+
return (bookmarkCell +
|
|
2580
2726
|
chalk.white(padToWidth(truncateToWidth(session.shortId, 9), 10)) +
|
|
2581
2727
|
agentColor(padToWidth(truncateToWidth(session.agent, 8), 9)) +
|
|
2582
2728
|
chalk.yellow(padToWidth(truncateToWidth(session.version || '-', 7), 8)) +
|
|
@@ -2776,9 +2922,9 @@ function printSessionTable(sessions, hiddenCount = 0, tree = false, liveIndex) {
|
|
|
2776
2922
|
// column (and its compact labels) is computed the same way the picker does it.
|
|
2777
2923
|
const showTicket = sessions.some((s) => ticketLabel(s) !== '');
|
|
2778
2924
|
const cols = pickerColumnsFor(sessions);
|
|
2779
|
-
const
|
|
2925
|
+
const bookmarks = listBookmarks();
|
|
2780
2926
|
for (const session of sessions) {
|
|
2781
|
-
console.log(flatSessionRow(session, liveIndex?.get(session.id), showTicket, cols,
|
|
2927
|
+
console.log(flatSessionRow(session, liveIndex?.get(session.id), showTicket, cols, bookmarks.has(session.id)));
|
|
2782
2928
|
}
|
|
2783
2929
|
const countLine = `${sessions.length} session${sessions.length === 1 ? '' : 's'}.`;
|
|
2784
2930
|
console.log(chalk.gray(`\n${countLine}`));
|
|
@@ -3121,10 +3267,10 @@ export function pickerColumnsFor(sessions) {
|
|
|
3121
3267
|
showModel: sessions.some((s) => !!s.model),
|
|
3122
3268
|
modelWidth: modelColumnWidth(sessions),
|
|
3123
3269
|
showTicket: sessions.some((s) => ticketLabel(s) !== ''),
|
|
3124
|
-
|
|
3270
|
+
showBookmark: (() => {
|
|
3125
3271
|
// One read of the store per pool, not one per row.
|
|
3126
|
-
const
|
|
3127
|
-
return
|
|
3272
|
+
const bookmarks = listBookmarks();
|
|
3273
|
+
return bookmarks.size > 0 && sessions.some((s) => bookmarks.has(s.id));
|
|
3128
3274
|
})(),
|
|
3129
3275
|
};
|
|
3130
3276
|
}
|
|
@@ -3144,7 +3290,7 @@ export function liveHostLabel(a) {
|
|
|
3144
3290
|
const viewer = a.viewingIn?.app;
|
|
3145
3291
|
return viewer && viewer !== a.host ? `${a.host}→${viewer}` : a.host;
|
|
3146
3292
|
}
|
|
3147
|
-
export function formatPickerLabel(s, query, cols = {}, ssh, host = '',
|
|
3293
|
+
export function formatPickerLabel(s, query, cols = {}, ssh, host = '', bookmarked = false, live) {
|
|
3148
3294
|
const agentColor = colorAgent(s.agent);
|
|
3149
3295
|
const age = sessionAgeParts(s.timestamp, s.lastActivity);
|
|
3150
3296
|
const project = s.project || '-';
|
|
@@ -3186,11 +3332,11 @@ export function formatPickerLabel(s, query, cols = {}, ssh, host = '', favorite
|
|
|
3186
3332
|
const ticketW = cols.showTicket ? TICKET_W + 1 : 0;
|
|
3187
3333
|
const hostW = cols.showHost ? PICKER_HOST_W : 0;
|
|
3188
3334
|
const wtW = wt ? stringWidth(wt) + 1 : 0;
|
|
3189
|
-
// Within a pool that HAS
|
|
3190
|
-
// row is
|
|
3191
|
-
// drops the column entirely (`
|
|
3192
|
-
const
|
|
3193
|
-
const
|
|
3335
|
+
// Within a pool that HAS bookmarked rows the marker holds its 2 cells whether
|
|
3336
|
+
// this row is bookmarked or not, so the columns after it never jog; a pool
|
|
3337
|
+
// with none drops the column entirely (`showBookmark`) and costs nothing.
|
|
3338
|
+
const bookmarkW = cols.showBookmark ? 2 : 0;
|
|
3339
|
+
const bookmarkCell = cols.showBookmark ? (bookmarked ? chalk.yellow('★ ') : ' ') : '';
|
|
3194
3340
|
// The same status word the flat listing shows, so a session that is `orphan`
|
|
3195
3341
|
// or `crashed` reads that way in the browser too — not only in its preview.
|
|
3196
3342
|
// Constant width whenever the column is on. `liveStatusCell` already pads its
|
|
@@ -3202,10 +3348,10 @@ export function formatPickerLabel(s, query, cols = {}, ssh, host = '', favorite
|
|
|
3202
3348
|
const statusCell = cols.showStatus ? (status.cell || ' '.repeat(LIVE_STATUS_W)) : '';
|
|
3203
3349
|
// Sized against the last-activity label alone; the creation field is then an
|
|
3204
3350
|
// additive decision made against whatever width is left (see the flat listing).
|
|
3205
|
-
const baseTopicW = terminalWidth() - gutter -
|
|
3351
|
+
const baseTopicW = terminalWidth() - gutter - bookmarkW - statusW - (10 + 9 + 8 + 16) - machineColW - hostW - ticketW - wtW - sshW - team.width - stringWidth(age.last) - 1;
|
|
3206
3352
|
const when = timeCell(age, baseTopicW);
|
|
3207
3353
|
const topicW = Math.max(MIN_TOPIC_W, baseTopicW - when.extraW);
|
|
3208
|
-
return (
|
|
3354
|
+
return (bookmarkCell +
|
|
3209
3355
|
// Truncated, not just padded: an indexed shortId is always 8 chars, but a
|
|
3210
3356
|
// live row with no session id is named by its pid or cloud task, which can
|
|
3211
3357
|
// run past the column and shunt every later column out of alignment.
|
|
@@ -3694,6 +3840,11 @@ export function filterSessionsByQuery(sessions, query) {
|
|
|
3694
3840
|
const trimmed = query?.trim().toLowerCase() || '';
|
|
3695
3841
|
if (!trimmed)
|
|
3696
3842
|
return sessions;
|
|
3843
|
+
const installedAgentVersion = parseInstalledAgentVersionQuery(trimmed);
|
|
3844
|
+
if (installedAgentVersion) {
|
|
3845
|
+
const { agent, version } = parseAgentFilter(installedAgentVersion);
|
|
3846
|
+
return sessions.filter((session) => session.agent === agent && session.version === version);
|
|
3847
|
+
}
|
|
3697
3848
|
const terms = trimmed.split(/\s+/).filter(Boolean);
|
|
3698
3849
|
const contentIndex = searchContentIndex(sessions, trimmed);
|
|
3699
3850
|
// If the query exactly matches a session label, short-circuit the structural
|
|
@@ -3863,6 +4014,53 @@ async function renderArtifactsGlobal(query, listAll, name, scope) {
|
|
|
3863
4014
|
}
|
|
3864
4015
|
}
|
|
3865
4016
|
async function renderOneSession(query, mode, scope) {
|
|
4017
|
+
if (looksLikeSessionId(query)) {
|
|
4018
|
+
const outcome = await resolveSessionMetadataValue(query, scope);
|
|
4019
|
+
if (outcome.kind === 'partial') {
|
|
4020
|
+
console.error(chalk.red(`Partial session resolution: ${outcome.failedPeers.join(', ')} did not answer.`));
|
|
4021
|
+
process.exit(2);
|
|
4022
|
+
}
|
|
4023
|
+
// An index miss can be a transcript created since the last incremental
|
|
4024
|
+
// scan, or a Claude history alias. Fall through to the established
|
|
4025
|
+
// discovery/history resolver below only on that cold miss.
|
|
4026
|
+
if (outcome.kind === 'ambiguous') {
|
|
4027
|
+
console.error(chalk.red(`Multiple sessions match "${query}" across the fleet:`));
|
|
4028
|
+
for (const candidate of outcome.candidates) {
|
|
4029
|
+
const match = candidate.hits[0].session;
|
|
4030
|
+
const machines = candidate.hits.map(hit => hit.machine).join(', ');
|
|
4031
|
+
console.error(chalk.cyan(` ${match.shortId} ${match.id}`) + chalk.gray(` ${machines} ${match.agent}${match.version ? ` ${match.version}` : ''}`));
|
|
4032
|
+
}
|
|
4033
|
+
console.error(chalk.gray('Pass the full session ID to narrow it down.'));
|
|
4034
|
+
process.exit(1);
|
|
4035
|
+
}
|
|
4036
|
+
if (outcome.kind === 'resolved') {
|
|
4037
|
+
const resolved = outcome.session;
|
|
4038
|
+
if (resolved._remote && resolved.machine && resolved.machine !== machineId()) {
|
|
4039
|
+
const args = ['sessions', resolved.id, '--local'];
|
|
4040
|
+
const flag = modeFlag(mode);
|
|
4041
|
+
if (flag)
|
|
4042
|
+
args.push(flag);
|
|
4043
|
+
if (scope.filter.include?.length)
|
|
4044
|
+
args.push('--include', scope.filter.include.join(','));
|
|
4045
|
+
if (scope.filter.exclude?.length)
|
|
4046
|
+
args.push('--exclude', scope.filter.exclude.join(','));
|
|
4047
|
+
if (scope.filter.first !== undefined)
|
|
4048
|
+
args.push('--first', String(scope.filter.first));
|
|
4049
|
+
if (scope.filter.last !== undefined)
|
|
4050
|
+
args.push('--last', String(scope.filter.last));
|
|
4051
|
+
if (scope.redact === false)
|
|
4052
|
+
args.push('--no-redact');
|
|
4053
|
+
const rendered = await runOnPeer(args, resolved.machine);
|
|
4054
|
+
if (rendered === 'no-target') {
|
|
4055
|
+
console.error(chalk.red(`Session ${resolved.id} is on ${resolved.machine}, but that device is not reachable.`));
|
|
4056
|
+
process.exit(1);
|
|
4057
|
+
}
|
|
4058
|
+
return;
|
|
4059
|
+
}
|
|
4060
|
+
await renderSession(resolved, mode, scope.filter, { redact: scope.redact });
|
|
4061
|
+
return;
|
|
4062
|
+
}
|
|
4063
|
+
}
|
|
3866
4064
|
const spinner = ora().start();
|
|
3867
4065
|
const tracker = createScanProgressTracker(FIND_VERBS, 'session', spinner);
|
|
3868
4066
|
try {
|
|
@@ -4070,6 +4268,16 @@ function resolveIndexedMetadataRows(indexed, selector) {
|
|
|
4070
4268
|
}
|
|
4071
4269
|
return resolveSessionQuery(indexed, selector, { indexFallback: false }).matches;
|
|
4072
4270
|
}
|
|
4271
|
+
/**
|
|
4272
|
+
* ID-shaped selectors go straight to SQLite's primary-key/short-id lookup.
|
|
4273
|
+
* Keyword and label selectors still need the broader metadata pool.
|
|
4274
|
+
*/
|
|
4275
|
+
function indexedRowsForSelector(selector, scope) {
|
|
4276
|
+
const indexed = looksLikeSessionId(selector)
|
|
4277
|
+
? findSessionsById(selector)
|
|
4278
|
+
: querySessions();
|
|
4279
|
+
return applyScopeFilters(indexed, scope);
|
|
4280
|
+
}
|
|
4073
4281
|
/** Fixed peer argv for the metadata resolver. Scope flags compose identically on
|
|
4074
4282
|
* every host; `--all` removes the SSH login cwd/time window, not agent/project filters. */
|
|
4075
4283
|
export function metadataResolveForwardedArgs(selector, scope) {
|
|
@@ -4121,7 +4329,7 @@ export function metadataResolveOutcome(localMatches, remote, selector) {
|
|
|
4121
4329
|
export async function resolveSessionMetadataValue(selector, scope = {}, deps = { gatherRemoteList }) {
|
|
4122
4330
|
const localMachine = machineId();
|
|
4123
4331
|
const includeLocal = !scope.hosts?.length || shouldIncludeLocal(scope.hosts, localMachine);
|
|
4124
|
-
const indexed = includeLocal ?
|
|
4332
|
+
const indexed = includeLocal ? indexedRowsForSelector(selector, scope) : [];
|
|
4125
4333
|
const localMatches = resolveIndexedMetadataRows(indexed, selector)
|
|
4126
4334
|
.map(session => ({ ...session, machine: session.machine || localMachine }));
|
|
4127
4335
|
// A full-UUID local hit resolves with ZERO SSH: a UUID is globally unique, so
|
|
@@ -4158,7 +4366,7 @@ export async function resolveSessionMetadataValue(selector, scope = {}, deps = {
|
|
|
4158
4366
|
async function resolveSessionMetadata(selector, scope, deps = { gatherRemoteList }) {
|
|
4159
4367
|
const localMachine = machineId();
|
|
4160
4368
|
const includeLocal = !scope.hosts?.length || shouldIncludeLocal(scope.hosts, localMachine);
|
|
4161
|
-
const indexed = includeLocal ?
|
|
4369
|
+
const indexed = includeLocal ? indexedRowsForSelector(selector, scope) : [];
|
|
4162
4370
|
const localMatches = resolveIndexedMetadataRows(indexed, selector)
|
|
4163
4371
|
.map(session => ({ ...session, machine: session.machine || localMachine }));
|
|
4164
4372
|
// A peer is already inside gatherRemoteList. Return all local candidates so
|
|
@@ -4257,6 +4465,8 @@ export function registerSessionsCommands(program) {
|
|
|
4257
4465
|
.addOption(new Option('--resolve-safe-v1 <selector>').hideHelp())
|
|
4258
4466
|
.description('Find, browse, and read agent conversation transcripts. Live roster: `agents sessions --active` (alias: `agents roster`).')
|
|
4259
4467
|
.option('-a, --agent <agent>', 'Filter by agent type and version (e.g., claude, codex@0.116.0)')
|
|
4468
|
+
.addOption(new Option('--session-version <version>', 'Internal spelling for the public sessions --version filter')
|
|
4469
|
+
.hideHelp())
|
|
4260
4470
|
.option('--claude', 'Shorthand for --agent claude')
|
|
4261
4471
|
.option('--codex', 'Shorthand for --agent codex')
|
|
4262
4472
|
.option('--kimi', 'Shorthand for --agent kimi')
|
|
@@ -4264,6 +4474,7 @@ export function registerSessionsCommands(program) {
|
|
|
4264
4474
|
.option('--grok', 'Shorthand for --agent grok')
|
|
4265
4475
|
.option('--opencode', 'Shorthand for --agent opencode')
|
|
4266
4476
|
.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.')
|
|
4477
|
+
.option('--bookmarks', 'Show only bookmarked sessions — bookmark them with `*` in the browser or `agents sessions bookmark <id>`')
|
|
4267
4478
|
.option('--unmanaged', "Also show sessions from your own ~/.<agent> installs (hidden once agents-cli manages that agent)")
|
|
4268
4479
|
.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')
|
|
4269
4480
|
.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.")
|
|
@@ -4297,7 +4508,6 @@ export function registerSessionsCommands(program) {
|
|
|
4297
4508
|
.option('--abandoned', 'Show sessions with no transcript progress for the abandonment window (implies --active)')
|
|
4298
4509
|
.option('--queued', 'Show queued sessions that have not started running (implies --active)')
|
|
4299
4510
|
.option('--unknown', 'Show sessions whose live state cannot be determined (implies --active)')
|
|
4300
|
-
.option('--favorites', 'Show only favorited (starred) sessions — star them with `*` in the browser or `agents sessions favorite <id>`')
|
|
4301
4511
|
.option('--tree', 'Group the listing by directory; drops the id/version columns for readability')
|
|
4302
4512
|
.option('--flat', 'Plain flat table (one row per session) instead of the grouped project overview')
|
|
4303
4513
|
.option('--no-live', 'Do not enrich the listing with live status/preview for running sessions')
|
|
@@ -4350,6 +4560,10 @@ export function registerSessionsCommands(program) {
|
|
|
4350
4560
|
# Search across every directory, not just this project
|
|
4351
4561
|
agents sessions "topic" --all
|
|
4352
4562
|
|
|
4563
|
+
# Filter one installed harness version (equivalent forms)
|
|
4564
|
+
agents sessions claude@2.1.181
|
|
4565
|
+
agents sessions --agent claude --version 2.1.181
|
|
4566
|
+
|
|
4353
4567
|
# Team-spawned sessions, grouped by team (spawner + spawn time per team,
|
|
4354
4568
|
# teammate mode/handle per row; team-flagged spawns with no team record
|
|
4355
4569
|
# in a trailing (no team) bucket)
|
|
@@ -4397,6 +4611,7 @@ export function registerSessionsCommands(program) {
|
|
|
4397
4611
|
resume [query] multi-select history → open tabs (or run --resume <id>)
|
|
4398
4612
|
- The interactive listing and every live-status flag fold in your other online machines automatically (live over SSH, no sync) — each row is labelled by host, this machine first. Use --local to skip the fan-out; single-id lookups stay local.
|
|
4399
4613
|
- --all is not a device flag: it widens historical directory and time filters. Fleet collection is already the default. A status flag (--working/--idle/--waiting/--orphan/--crashed/--closed/--abandoned/--queued/--unknown) implies --active; combine status flags for a union.
|
|
4614
|
+
- --version <version> requires --agent and is equivalent to --agent <agent@version>.
|
|
4400
4615
|
- --host runs the query on the remote's own index over SSH (host alias or user@host); repeat or pass several to fan out. SSH access is the only auth.
|
|
4401
4616
|
- --in-team matches both ends of the lineage: the session that ran 'agents teams create/add', and (with --teams) that team's teammates. In the interactive list, 't' cycles the same filter over the teams in view.
|
|
4402
4617
|
- --include and --exclude are mutually exclusive.
|
|
@@ -4420,10 +4635,49 @@ export function registerSessionsCommands(program) {
|
|
|
4420
4635
|
}
|
|
4421
4636
|
await sessionsAction(query, options, command.getOptionValueSource('limit'));
|
|
4422
4637
|
});
|
|
4638
|
+
const previewCmd = sessionsCmd
|
|
4639
|
+
.command('preview')
|
|
4640
|
+
.argument('<id>', 'Full session ID or displayed 8-character short ID')
|
|
4641
|
+
.description('Show one rich session card without rendering the full transcript')
|
|
4642
|
+
.option('-a, --agent <agent>', 'Narrow the ID to one agent type/version')
|
|
4643
|
+
.option('-p, --project <name>', 'Narrow the ID to one project')
|
|
4644
|
+
.option('--local', 'Only this machine; do not resolve the ID across the fleet')
|
|
4645
|
+
.option('-H, --host <target...>', 'Resolve only on the named device(s)')
|
|
4646
|
+
.option('--device <target...>', 'Alias for --host')
|
|
4647
|
+
.option('--json', 'Output the session preview as JSON');
|
|
4648
|
+
setHelpSections(previewCmd, {
|
|
4649
|
+
examples: `
|
|
4650
|
+
# Preview by the 8-character ID shown in agents sessions
|
|
4651
|
+
agents sessions preview 407b8dd5
|
|
4652
|
+
|
|
4653
|
+
# A full UUID resolves on the first device that owns it
|
|
4654
|
+
agents sessions preview c70ecdea-6210-4039-9845-246a3a7a9942
|
|
4655
|
+
|
|
4656
|
+
# Stay on this machine or restrict the authoritative lookup to one peer
|
|
4657
|
+
agents sessions preview 407b8dd5 --local
|
|
4658
|
+
agents sessions preview 407b8dd5 --device zion
|
|
4659
|
+
`,
|
|
4660
|
+
notes: `
|
|
4661
|
+
- Full UUIDs are globally unique and may stop the fleet lookup at the first exact hit.
|
|
4662
|
+
- Short IDs wait for every selected device so ambiguity is never hidden.
|
|
4663
|
+
- Active status is refreshed through the bounded live-state TTL; transcript-derived details use the durable session index.
|
|
4664
|
+
`,
|
|
4665
|
+
});
|
|
4666
|
+
previewCmd.action(async (id) => {
|
|
4667
|
+
const options = previewCmd.optsWithGlobals();
|
|
4668
|
+
const hosts = [...(options.host ?? []), ...(options.device ?? [])];
|
|
4669
|
+
await renderSessionPreview(id, {
|
|
4670
|
+
agent: options.agent,
|
|
4671
|
+
project: options.project,
|
|
4672
|
+
local: options.local,
|
|
4673
|
+
hosts: hosts.length > 0 ? hosts : undefined,
|
|
4674
|
+
json: options.json,
|
|
4675
|
+
});
|
|
4676
|
+
});
|
|
4423
4677
|
registerSessionsTailCommand(sessionsCmd);
|
|
4424
4678
|
registerSessionsResumeCommand(sessionsCmd);
|
|
4425
4679
|
registerSessionsForkCommand(sessionsCmd);
|
|
4426
|
-
|
|
4680
|
+
registerSessionsBookmarkCommand(sessionsCmd);
|
|
4427
4681
|
registerGoCommand(sessionsCmd);
|
|
4428
4682
|
registerFocusCommand(sessionsCmd);
|
|
4429
4683
|
registerReconnectCommand(sessionsCmd);
|