@phnx-labs/agents-cli 1.20.35 → 1.20.36
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 +91 -0
- package/LICENSE +185 -21
- package/README.md +8 -4
- package/dist/commands/audit.d.ts +14 -0
- package/dist/commands/audit.js +68 -0
- package/dist/commands/browser.js +82 -8
- package/dist/commands/check.d.ts +15 -0
- package/dist/commands/check.js +84 -0
- package/dist/commands/cloud.js +143 -3
- package/dist/commands/computer.js +81 -0
- package/dist/commands/daemon.js +4 -1
- package/dist/commands/doctor.js +1 -89
- package/dist/commands/events.js +3 -3
- package/dist/commands/exec.d.ts +19 -0
- package/dist/commands/exec.js +277 -11
- package/dist/commands/hosts.js +10 -6
- package/dist/commands/inspect.js +8 -9
- package/dist/commands/lock.d.ts +12 -0
- package/dist/commands/lock.js +70 -0
- package/dist/commands/message.d.ts +15 -0
- package/dist/commands/message.js +56 -0
- package/dist/commands/routines.js +101 -5
- package/dist/commands/secrets-migrate.js +106 -57
- package/dist/commands/secrets.d.ts +31 -18
- package/dist/commands/secrets.js +156 -75
- package/dist/commands/serve.d.ts +10 -0
- package/dist/commands/serve.js +37 -0
- package/dist/commands/sessions-inject.d.ts +14 -0
- package/dist/commands/sessions-inject.js +111 -0
- package/dist/commands/sessions-picker.d.ts +2 -0
- package/dist/commands/sessions-picker.js +24 -3
- package/dist/commands/sessions-resume.js +20 -8
- package/dist/commands/sessions.d.ts +71 -1
- package/dist/commands/sessions.js +365 -37
- package/dist/commands/setup.js +4 -2
- package/dist/commands/sync.d.ts +3 -1
- package/dist/commands/sync.js +156 -4
- package/dist/commands/teams.js +217 -0
- package/dist/commands/versions.js +2 -4
- package/dist/commands/watchdog.d.ts +18 -0
- package/dist/commands/watchdog.js +238 -0
- package/dist/index.js +25 -2
- package/dist/lib/audit/log.d.ts +92 -0
- package/dist/lib/audit/log.js +177 -0
- package/dist/lib/auto-pull.js +2 -1
- package/dist/lib/browser/chrome.d.ts +10 -0
- package/dist/lib/browser/chrome.js +18 -7
- package/dist/lib/browser/drivers/ssh.js +2 -1
- package/dist/lib/browser/har.d.ts +84 -0
- package/dist/lib/browser/har.js +77 -0
- package/dist/lib/browser/ipc.js +24 -3
- package/dist/lib/browser/profiles.d.ts +1 -1
- package/dist/lib/browser/profiles.js +8 -10
- package/dist/lib/browser/refs.d.ts +65 -0
- package/dist/lib/browser/refs.js +73 -1
- package/dist/lib/browser/runtime-state.js +1 -0
- package/dist/lib/browser/service.d.ts +38 -2
- package/dist/lib/browser/service.js +112 -8
- package/dist/lib/browser/types.d.ts +14 -1
- package/dist/lib/budget/live-cloud.d.ts +42 -0
- package/dist/lib/budget/live-cloud.js +79 -0
- package/dist/lib/budget/live-team.d.ts +31 -0
- package/dist/lib/budget/live-team.js +115 -0
- package/dist/lib/cloud/codex.js +4 -0
- package/dist/lib/cloud/rush.d.ts +12 -1
- package/dist/lib/cloud/rush.js +13 -3
- package/dist/lib/cloud/types.d.ts +9 -0
- package/dist/lib/computer/dispatch.d.ts +8 -0
- package/dist/lib/computer/dispatch.js +125 -0
- package/dist/lib/computer/loop.d.ts +62 -0
- package/dist/lib/computer/loop.js +98 -0
- package/dist/lib/computer/model.d.ts +44 -0
- package/dist/lib/computer/model.js +157 -0
- package/dist/lib/concurrency.d.ts +19 -0
- package/dist/lib/concurrency.js +33 -0
- package/dist/lib/daemon.d.ts +57 -0
- package/dist/lib/daemon.js +192 -16
- package/dist/lib/devices/registry.d.ts +7 -0
- package/dist/lib/devices/registry.js +24 -0
- package/dist/lib/devices/tailscale.js +1 -1
- package/dist/lib/drift.d.ts +52 -0
- package/dist/lib/drift.js +112 -0
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/events.js +31 -13
- package/dist/lib/exec.d.ts +17 -0
- package/dist/lib/exec.js +79 -13
- package/dist/lib/git.d.ts +27 -0
- package/dist/lib/git.js +56 -1
- package/dist/lib/hooks/cache.d.ts +6 -0
- package/dist/lib/hooks/cache.js +54 -12
- package/dist/lib/hooks.d.ts +27 -0
- package/dist/lib/hooks.js +127 -8
- package/dist/lib/hosts/dispatch.d.ts +15 -0
- package/dist/lib/hosts/dispatch.js +39 -6
- package/dist/lib/hosts/logs.js +30 -1
- package/dist/lib/hosts/option.js +1 -1
- package/dist/lib/hosts/passthrough.js +3 -1
- package/dist/lib/hosts/ready.d.ts +29 -6
- package/dist/lib/hosts/ready.js +66 -15
- package/dist/lib/hosts/registry.d.ts +19 -2
- package/dist/lib/hosts/registry.js +58 -2
- package/dist/lib/hosts/remote-cmd.d.ts +62 -1
- package/dist/lib/hosts/remote-cmd.js +70 -1
- package/dist/lib/hosts/remote-os.d.ts +17 -0
- package/dist/lib/hosts/remote-os.js +30 -0
- package/dist/lib/hosts/session-index.d.ts +34 -0
- package/dist/lib/hosts/session-index.js +56 -0
- package/dist/lib/hosts/tasks.d.ts +14 -0
- package/dist/lib/hosts/tasks.js +15 -0
- package/dist/lib/lock.d.ts +93 -0
- package/dist/lib/lock.js +207 -0
- package/dist/lib/loop.js +16 -1
- package/dist/lib/machine-id.d.ts +21 -0
- package/dist/lib/machine-id.js +26 -0
- package/dist/lib/mailbox-target.d.ts +36 -0
- package/dist/lib/mailbox-target.js +45 -0
- package/dist/lib/mailbox.d.ts +47 -0
- package/dist/lib/mailbox.js +194 -0
- package/dist/lib/mcp.d.ts +5 -0
- package/dist/lib/mcp.js +24 -8
- package/dist/lib/migrate.d.ts +19 -0
- package/dist/lib/migrate.js +134 -26
- package/dist/lib/overdue.js +3 -0
- package/dist/lib/picker.d.ts +2 -0
- package/dist/lib/picker.js +4 -1
- package/dist/lib/platform/exec.d.ts +46 -0
- package/dist/lib/platform/exec.js +74 -0
- package/dist/lib/platform/process.d.ts +31 -0
- package/dist/lib/platform/process.js +34 -1
- package/dist/lib/platform/winpath.js +2 -0
- package/dist/lib/plugins.js +16 -6
- package/dist/lib/profiles.d.ts +25 -0
- package/dist/lib/profiles.js +22 -6
- package/dist/lib/pty-client.js +2 -1
- package/dist/lib/rotate.d.ts +61 -0
- package/dist/lib/rotate.js +52 -0
- package/dist/lib/routines.d.ts +40 -2
- package/dist/lib/routines.js +66 -8
- package/dist/lib/runner.d.ts +11 -2
- package/dist/lib/runner.js +49 -7
- package/dist/lib/scheduler.js +6 -1
- package/dist/lib/secrets/bundles.d.ts +60 -4
- package/dist/lib/secrets/bundles.js +131 -12
- package/dist/lib/secrets/filestore.d.ts +3 -0
- package/dist/lib/secrets/filestore.js +42 -16
- package/dist/lib/secrets/index.d.ts +43 -2
- package/dist/lib/secrets/index.js +102 -3
- package/dist/lib/secrets/mcp.d.ts +93 -0
- package/dist/lib/secrets/mcp.js +205 -0
- package/dist/lib/secrets/remote.js +12 -5
- package/dist/lib/secrets/sync.js +83 -4
- package/dist/lib/secrets/windows.js +14 -3
- package/dist/lib/serve/data.d.ts +81 -0
- package/dist/lib/serve/data.js +91 -0
- package/dist/lib/serve/page.d.ts +7 -0
- package/dist/lib/serve/page.js +140 -0
- package/dist/lib/serve/server.d.ts +46 -0
- package/dist/lib/serve/server.js +115 -0
- package/dist/lib/session/active.d.ts +54 -0
- package/dist/lib/session/active.js +190 -19
- package/dist/lib/session/discover.d.ts +37 -0
- package/dist/lib/session/discover.js +111 -28
- package/dist/lib/session/inject.d.ts +18 -0
- package/dist/lib/session/inject.js +21 -0
- package/dist/lib/session/parse.js +23 -20
- package/dist/lib/session/pid-registry.d.ts +1 -0
- package/dist/lib/session/pid-registry.js +24 -0
- package/dist/lib/session/provenance.d.ts +14 -2
- package/dist/lib/session/provenance.js +39 -8
- package/dist/lib/session/remote-active.js +19 -7
- package/dist/lib/session/remote-list.d.ts +51 -0
- package/dist/lib/session/remote-list.js +213 -0
- package/dist/lib/session/remote.d.ts +7 -1
- package/dist/lib/session/remote.js +16 -2
- package/dist/lib/session/sync/config.d.ts +1 -15
- package/dist/lib/session/sync/config.js +4 -20
- package/dist/lib/session/types.d.ts +17 -0
- package/dist/lib/shims.d.ts +36 -6
- package/dist/lib/shims.js +91 -29
- package/dist/lib/ssh-exec.js +2 -0
- package/dist/lib/ssh-tunnel.js +2 -1
- package/dist/lib/startup/command-registry.d.ts +6 -0
- package/dist/lib/startup/command-registry.js +13 -1
- package/dist/lib/state.d.ts +13 -0
- package/dist/lib/state.js +103 -9
- package/dist/lib/sync-umbrella.d.ts +14 -7
- package/dist/lib/sync-umbrella.js +17 -9
- package/dist/lib/teams/forEach.d.ts +110 -0
- package/dist/lib/teams/forEach.js +186 -0
- package/dist/lib/teams/index.d.ts +1 -0
- package/dist/lib/teams/index.js +1 -0
- package/dist/lib/teams/pr-watch.d.ts +226 -0
- package/dist/lib/teams/pr-watch.js +371 -0
- package/dist/lib/teams/supervisor.d.ts +14 -1
- package/dist/lib/teams/supervisor.js +19 -0
- package/dist/lib/teams/worktree.d.ts +9 -0
- package/dist/lib/teams/worktree.js +32 -0
- package/dist/lib/terminal/backends/index.d.ts +2 -1
- package/dist/lib/terminal/backends/index.js +3 -1
- package/dist/lib/terminal/backends/vscodium-agent.d.ts +31 -0
- package/dist/lib/terminal/backends/vscodium-agent.js +72 -0
- package/dist/lib/terminal/index.d.ts +4 -1
- package/dist/lib/terminal/index.js +4 -1
- package/dist/lib/terminal/inject.d.ts +204 -0
- package/dist/lib/terminal/inject.js +247 -0
- package/dist/lib/terminal/resolve.d.ts +64 -0
- package/dist/lib/terminal/resolve.js +90 -0
- package/dist/lib/terminal/types.d.ts +1 -1
- package/dist/lib/triggers/webhook.d.ts +85 -0
- package/dist/lib/triggers/webhook.js +141 -0
- package/dist/lib/versions.d.ts +23 -0
- package/dist/lib/versions.js +119 -13
- package/dist/lib/watchdog/index.d.ts +3 -0
- package/dist/lib/watchdog/index.js +5 -0
- package/dist/lib/watchdog/read.d.ts +35 -0
- package/dist/lib/watchdog/read.js +149 -0
- package/dist/lib/watchdog/runner.d.ts +127 -0
- package/dist/lib/watchdog/runner.js +322 -0
- package/dist/lib/watchdog/watchdog.d.ts +40 -0
- package/dist/lib/watchdog/watchdog.js +166 -0
- package/dist/lib/watchdog/watchdogTail.d.ts +5 -0
- package/dist/lib/watchdog/watchdogTail.js +154 -0
- package/dist/lib/workflows.d.ts +166 -0
- package/dist/lib/workflows.js +193 -0
- package/package.json +5 -4
|
@@ -15,15 +15,16 @@ import chalk from 'chalk';
|
|
|
15
15
|
import ora from 'ora';
|
|
16
16
|
import { SESSION_AGENTS } from '../lib/session/types.js';
|
|
17
17
|
import { discoverArtifacts, readArtifact, resolveArtifact } from '../lib/session/artifacts.js';
|
|
18
|
-
import { looksLikePath, toComparablePath, homeDir } from '../lib/platform/index.js';
|
|
18
|
+
import { looksLikePath, toComparablePath, homeDir, needsWindowsShell, findExecutable } from '../lib/platform/index.js';
|
|
19
19
|
import { getActiveSessions } from '../lib/session/active.js';
|
|
20
20
|
import { machineId, normalizeHost } from '../lib/session/sync/config.js';
|
|
21
|
-
import { gatherRemoteActive } from '../lib/session/remote-active.js';
|
|
21
|
+
import { gatherRemoteActive, NO_FANOUT_ENV } from '../lib/session/remote-active.js';
|
|
22
|
+
import { gatherRemoteList, runOnPeer } from '../lib/session/remote-list.js';
|
|
22
23
|
import { stringWidth, truncateToWidth, padToWidth, terminalWidth } from '../lib/session/width.js';
|
|
23
24
|
import { discoverSessions, countSessionsInScope, resolveSessionById, searchContentIndex } from '../lib/session/discover.js';
|
|
24
25
|
import { filterTeamSessions } from '../lib/session/team-filter.js';
|
|
25
26
|
import { parseSession } from '../lib/session/parse.js';
|
|
26
|
-
import { runRemoteSessions } from '../lib/session/remote.js';
|
|
27
|
+
import { runRemoteSessions, buildForwardedArgs } from '../lib/session/remote.js';
|
|
27
28
|
import { formatRelativeTime } from '../lib/session/relative-time.js';
|
|
28
29
|
import { renderConversationMarkdown, renderSummary, renderSummaryHeader, computeSummaryStats, renderJson, filterEvents, parseRoleList } from '../lib/session/render.js';
|
|
29
30
|
import { renderMarkdown } from '../lib/markdown.js';
|
|
@@ -36,6 +37,7 @@ import { setHelpSections } from '../lib/help.js';
|
|
|
36
37
|
import { registerSessionsTailCommand } from './sessions-tail.js';
|
|
37
38
|
import { registerSessionsSyncCommand } from './sessions-sync.js';
|
|
38
39
|
import { registerSessionsResumeCommand } from './sessions-resume.js';
|
|
40
|
+
import { registerSessionsInjectCommand } from './sessions-inject.js';
|
|
39
41
|
const SESSION_AGENT_FILTER_HELP = `Filter by agent, e.g. claude, codex, claude@2.0.65`;
|
|
40
42
|
const CLAUDE_RESUME_MATCH_WINDOW_MS = 10 * 60_000;
|
|
41
43
|
const LOAD_VERBS = ['Loading', 'Scanning', 'Gathering', 'Indexing', 'Reading'];
|
|
@@ -78,6 +80,12 @@ function createScanProgressTracker(verbs, suffix, spinner) {
|
|
|
78
80
|
}
|
|
79
81
|
const PICKER_RECENT_COUNT = 15;
|
|
80
82
|
const PICKER_POOL_LIMIT = 200;
|
|
83
|
+
// The grouped default view ("overview"): fetch a generous recency-ordered pool
|
|
84
|
+
// for accurate per-project totals, show each project's most-recent rows grouped
|
|
85
|
+
// by project, newest-active project first.
|
|
86
|
+
const OVERVIEW_ROWS_PER_PROJECT = 5; // recent rows shown per project before "· N more"
|
|
87
|
+
const OVERVIEW_POOL_LIMIT = 1000; // fetch cap — accurate per-project totals up to this
|
|
88
|
+
const OVERVIEW_MAX_PROJECTS = 12; // project groups shown before "+N more projects"
|
|
81
89
|
/**
|
|
82
90
|
* Resolve a path-like query to an absolute directory path.
|
|
83
91
|
*/
|
|
@@ -423,6 +431,41 @@ export function dedupeByMachineSession(sessions) {
|
|
|
423
431
|
}
|
|
424
432
|
return out;
|
|
425
433
|
}
|
|
434
|
+
/**
|
|
435
|
+
* Order a merged listing so the local machine's sessions come first, then each
|
|
436
|
+
* remote machine as a contiguous block (more sessions first, then name), with
|
|
437
|
+
* every machine keeping its incoming order (timestamp) within the block. Also
|
|
438
|
+
* dedupes: a session present both locally (a synced mirror copy) and via live
|
|
439
|
+
* fan-out collapses to one, keyed by machine + session id. Rows are keyed by
|
|
440
|
+
* `machine` (discover tags local rows with the local id; fan-out tags remote
|
|
441
|
+
* rows with the peer id) falling back to `localMachine` when untagged. Pure —
|
|
442
|
+
* `localMachine` is injected so the ordering is testable without os.hostname().
|
|
443
|
+
*/
|
|
444
|
+
export function mergeLocalFirst(sessions, localMachine) {
|
|
445
|
+
const byMachine = new Map();
|
|
446
|
+
const seen = new Set();
|
|
447
|
+
for (const s of sessions) {
|
|
448
|
+
const machine = s.machine || localMachine;
|
|
449
|
+
if (s.id) {
|
|
450
|
+
const dedupeKey = `${machine}:${s.id}`;
|
|
451
|
+
if (seen.has(dedupeKey))
|
|
452
|
+
continue;
|
|
453
|
+
seen.add(dedupeKey);
|
|
454
|
+
}
|
|
455
|
+
(byMachine.get(machine) ?? byMachine.set(machine, []).get(machine)).push(s);
|
|
456
|
+
}
|
|
457
|
+
const keys = Array.from(byMachine.keys()).sort((a, b) => {
|
|
458
|
+
if (a === localMachine)
|
|
459
|
+
return -1;
|
|
460
|
+
if (b === localMachine)
|
|
461
|
+
return 1;
|
|
462
|
+
const ac = byMachine.get(a).length, bc = byMachine.get(b).length;
|
|
463
|
+
if (ac !== bc)
|
|
464
|
+
return bc - ac;
|
|
465
|
+
return a.localeCompare(b);
|
|
466
|
+
});
|
|
467
|
+
return keys.flatMap((k) => byMachine.get(k));
|
|
468
|
+
}
|
|
426
469
|
/** Print one machine's workspace tree, indented under its machine header. */
|
|
427
470
|
function renderWorkspaceLayout(layout, base) {
|
|
428
471
|
let first = true;
|
|
@@ -599,8 +642,18 @@ async function sessionsAction(query, options) {
|
|
|
599
642
|
// Interactive picker loads a deep pool but shows only recent sessions
|
|
600
643
|
// until the user starts typing. Non-interactive/JSON uses the explicit limit.
|
|
601
644
|
const isInteractive = !options.json && isInteractiveTerminal();
|
|
602
|
-
|
|
603
|
-
|
|
645
|
+
// The grouped project overview is the default for a bare interactive listing:
|
|
646
|
+
// no query, no path drill-in, not explicitly --flat/--tree. It drops the silent
|
|
647
|
+
// cwd-scope + 50-cap + 30-day window that hide most of a large index.
|
|
648
|
+
const wantsOverview = isInteractive && !searchQuery && !pathFilter && !options.flat && !options.tree;
|
|
649
|
+
const limit = wantsOverview
|
|
650
|
+
? OVERVIEW_POOL_LIMIT
|
|
651
|
+
: parseInt(options.limit || (isInteractive ? String(PICKER_POOL_LIMIT) : '50'), 10);
|
|
652
|
+
// Overview: recency order across the whole index, no default window; an explicit
|
|
653
|
+
// --since still narrows. Non-overview keeps the prior interactive-30d default.
|
|
654
|
+
const since = wantsOverview
|
|
655
|
+
? options.since
|
|
656
|
+
: (options.since ?? (isInteractive && !options.all ? '30d' : undefined));
|
|
604
657
|
const spinner = options.json ? null : ora().start();
|
|
605
658
|
const tracker = createScanProgressTracker(LOAD_VERBS, 'sessions', spinner);
|
|
606
659
|
try {
|
|
@@ -613,7 +666,7 @@ async function sessionsAction(query, options) {
|
|
|
613
666
|
const scope = {
|
|
614
667
|
agent,
|
|
615
668
|
version,
|
|
616
|
-
all: pathFilter ? undefined : options.all,
|
|
669
|
+
all: pathFilter ? undefined : (wantsOverview ? true : options.all),
|
|
617
670
|
cwd: process.cwd(),
|
|
618
671
|
cwdPrefix: pathFilter,
|
|
619
672
|
project: options.project,
|
|
@@ -657,12 +710,42 @@ async function sessionsAction(query, options) {
|
|
|
657
710
|
if (options.json) {
|
|
658
711
|
const filtered = searchQuery ? filterSessionsByQuery(sessions, searchQuery) : sessions;
|
|
659
712
|
const serializable = filtered.map(s => {
|
|
660
|
-
const { _matchedTerms, _bm25Score, ...rest } = s;
|
|
713
|
+
const { _matchedTerms, _bm25Score, _remote, ...rest } = s;
|
|
661
714
|
return rest;
|
|
662
715
|
});
|
|
663
716
|
process.stdout.write(JSON.stringify(serializable, null, 2) + '\n');
|
|
664
717
|
return;
|
|
665
718
|
}
|
|
719
|
+
// Cross-machine fan-out: unless --local (or we ARE a peer answering a
|
|
720
|
+
// parent's sweep), fold in other online machines' sessions live over SSH so
|
|
721
|
+
// the list spans the fleet without any sync — each remote row carries the
|
|
722
|
+
// machine it came from, and the picker/table label + group by it. Only the
|
|
723
|
+
// interactive picker and the printed table get this; --json and single-id
|
|
724
|
+
// resolution above stay local (a peer answers for itself; scripts get a
|
|
725
|
+
// deterministic local slice). Best-effort: a fan-out failure leaves the
|
|
726
|
+
// local list intact rather than erroring the whole command.
|
|
727
|
+
const forceLocal = options.local === true || process.env[NO_FANOUT_ENV] === '1';
|
|
728
|
+
if (!forceLocal) {
|
|
729
|
+
// Pass the hosts set so a variadic `--host a b` never leaks a host as a
|
|
730
|
+
// query (defensive: the --host-without-active early return above already
|
|
731
|
+
// means we only get here in auto-discovery mode, with no --host in argv).
|
|
732
|
+
const forwarded = buildForwardedArgs(process.argv, new Set(options.host ?? []));
|
|
733
|
+
if (!forwarded.includes('--json'))
|
|
734
|
+
forwarded.push('--json');
|
|
735
|
+
const fanSpinner = isInteractiveTerminal() ? ora('Reaching other machines...').start() : null;
|
|
736
|
+
try {
|
|
737
|
+
const { sessions: remoteSessions } = await gatherRemoteList(forwarded, options.host);
|
|
738
|
+
if (remoteSessions.length > 0) {
|
|
739
|
+
sessions = mergeLocalFirst([...sessions, ...remoteSessions], machineId());
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
catch {
|
|
743
|
+
// fan-out is an enrichment, never a hard dependency
|
|
744
|
+
}
|
|
745
|
+
finally {
|
|
746
|
+
fanSpinner?.stop();
|
|
747
|
+
}
|
|
748
|
+
}
|
|
666
749
|
if (sessions.length === 0) {
|
|
667
750
|
if (pathFilter) {
|
|
668
751
|
console.log(chalk.gray(`No sessions found for ${pathFilter}.`));
|
|
@@ -675,9 +758,19 @@ async function sessionsAction(query, options) {
|
|
|
675
758
|
}
|
|
676
759
|
return;
|
|
677
760
|
}
|
|
678
|
-
//
|
|
679
|
-
//
|
|
680
|
-
|
|
761
|
+
// The grouped project overview is the bare interactive default: a scannable
|
|
762
|
+
// dashboard of the whole fleet grouped by project, newest-active first.
|
|
763
|
+
// Interact/resume via `agents sessions <project>` or `agents sessions resume`.
|
|
764
|
+
if (wantsOverview) {
|
|
765
|
+
const liveIndex = await maybeLiveIndex(options);
|
|
766
|
+
// Per-project row cap is fixed (--limit carries a default of 50 and drives
|
|
767
|
+
// the fetch pool, not the display); `--all` expands every group instead.
|
|
768
|
+
printSessionOverview(sessions, hiddenCount, liveIndex, { perProjectCap: OVERVIEW_ROWS_PER_PROJECT, expand: !!options.all });
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
// --tree / --flat are printed listings, not an interactive pick — render them
|
|
772
|
+
// directly even in a TTY. A search query keeps the interactive picker.
|
|
773
|
+
if (isInteractiveTerminal() && !options.tree && !options.flat) {
|
|
681
774
|
const message = pathFilter
|
|
682
775
|
? `Search sessions (${path.basename(pathFilter)}):`
|
|
683
776
|
: formatSearchMessage(options);
|
|
@@ -688,7 +781,7 @@ async function sessionsAction(query, options) {
|
|
|
688
781
|
}
|
|
689
782
|
return;
|
|
690
783
|
}
|
|
691
|
-
// Non-interactive fallback (piped output)
|
|
784
|
+
// Non-interactive fallback (piped output) or --flat/--tree.
|
|
692
785
|
const filtered = searchQuery ? filterSessionsByQuery(sessions, searchQuery) : sessions;
|
|
693
786
|
const liveIndex = await maybeLiveIndex(options);
|
|
694
787
|
printSessionTable(filtered, hiddenCount, options.tree === true, liveIndex);
|
|
@@ -724,7 +817,7 @@ function metaSignals(s) {
|
|
|
724
817
|
* (tracker/PR ref, pulled out of the badge blob so refs align) is only rendered
|
|
725
818
|
* when `showTicket` — otherwise a listing with no refs would waste a column of
|
|
726
819
|
* dashes and needlessly truncate the topic. Worktree stays a trailing badge. */
|
|
727
|
-
function flatSessionRow(session, live, showTicket = false) {
|
|
820
|
+
function flatSessionRow(session, live, showTicket = false, cols = {}) {
|
|
728
821
|
const agentColor = colorAgent(session.agent);
|
|
729
822
|
const when = formatRelativeTime(session.timestamp);
|
|
730
823
|
const project = session.project || '-';
|
|
@@ -735,17 +828,26 @@ function flatSessionRow(session, live, showTicket = false) {
|
|
|
735
828
|
// resting one falls back to its opening topic.
|
|
736
829
|
const doing = preview || (tag ? `${tag}${session.topic ?? ''}` : session.topic);
|
|
737
830
|
const wt = session.worktreeSlug ? chalk.magenta(`wt:${session.worktreeSlug}`) : '';
|
|
831
|
+
// The machine column only earns its width when the listing spans more than one
|
|
832
|
+
// box (i.e. the cross-machine fan-out folded remotes in) — same rule and
|
|
833
|
+
// pool-derived width as the picker.
|
|
834
|
+
const machineColW = cols.machineWidth ?? PICKER_MACHINE_W;
|
|
835
|
+
const machineCell = cols.showMachine
|
|
836
|
+
? chalk.gray(padToWidth(truncateToWidth((cols.machineLabel?.(session.machine ?? '') ?? session.machine ?? '') || '-', machineColW - 1), machineColW))
|
|
837
|
+
: '';
|
|
738
838
|
const TICKET_W = 10;
|
|
739
839
|
const ticketCell = showTicket
|
|
740
840
|
? chalk.blue(padToWidth(truncateToWidth(ticketLabel(session) || '-', TICKET_W), TICKET_W + 1))
|
|
741
841
|
: '';
|
|
742
842
|
const glyphW = glyph ? 2 : 0;
|
|
843
|
+
const machineW = cols.showMachine ? machineColW : 0;
|
|
743
844
|
const ticketW = showTicket ? TICKET_W + 1 : 0;
|
|
744
845
|
const wtW = wt ? stringWidth(wt) + 1 : 0;
|
|
745
|
-
const topicW = Math.max(16, terminalWidth() - (10 + 9 + 8 + 16) - glyphW - ticketW - wtW - stringWidth(when) - 1);
|
|
846
|
+
const topicW = Math.max(16, terminalWidth() - (10 + 9 + 8 + 16) - glyphW - machineW - ticketW - wtW - stringWidth(when) - 1);
|
|
746
847
|
return (chalk.white(padToWidth(truncateToWidth(session.shortId, 9), 10)) +
|
|
747
848
|
agentColor(padToWidth(truncateToWidth(session.agent, 8), 9)) +
|
|
748
849
|
chalk.yellow(padToWidth(truncateToWidth(session.version || '-', 7), 8)) +
|
|
850
|
+
machineCell +
|
|
749
851
|
chalk.cyan(padToWidth(truncateToWidth(project, 14), 16)) +
|
|
750
852
|
(glyph ? glyph + ' ' : '') +
|
|
751
853
|
renderTopicCell(label, doing, '', topicW, topicW) +
|
|
@@ -793,6 +895,79 @@ async function maybeLiveIndex(options) {
|
|
|
793
895
|
return undefined;
|
|
794
896
|
}
|
|
795
897
|
}
|
|
898
|
+
/**
|
|
899
|
+
* Group key for the overview: prefer the indexed project name; else fold the cwd
|
|
900
|
+
* to its repo — a worktree (`.../<repo>/.agents/worktrees/<slug>`) folds to the
|
|
901
|
+
* repo, and a monorepo subdir falls back to its leaf dir basename. Pure.
|
|
902
|
+
*/
|
|
903
|
+
export function overviewProjectKey(s) {
|
|
904
|
+
if (s.project && s.project.trim())
|
|
905
|
+
return s.project.trim();
|
|
906
|
+
const cwd = (s.cwd ?? '').replace(/\/+$/, '');
|
|
907
|
+
if (!cwd)
|
|
908
|
+
return '(no project)';
|
|
909
|
+
const wt = cwd.match(/\/([^/]+)\/\.agents\/worktrees\//);
|
|
910
|
+
if (wt)
|
|
911
|
+
return wt[1];
|
|
912
|
+
const parts = cwd.split('/');
|
|
913
|
+
return parts[parts.length - 1] || cwd;
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
916
|
+
* Turn a recency-descending pool into project groups: each group shows its
|
|
917
|
+
* `perProjectCap` most-recent sessions (the rest become `· N more`), and groups
|
|
918
|
+
* are ordered by their most-recent session so the newest-active project leads.
|
|
919
|
+
* `perProjectCap = Infinity` expands every group. Pure — unit-tested.
|
|
920
|
+
*/
|
|
921
|
+
export function buildOverviewGroups(pool, perProjectCap) {
|
|
922
|
+
const byKey = new Map();
|
|
923
|
+
for (const s of pool) {
|
|
924
|
+
const k = overviewProjectKey(s);
|
|
925
|
+
(byKey.get(k) ?? byKey.set(k, []).get(k)).push(s);
|
|
926
|
+
}
|
|
927
|
+
const cap = Math.max(1, perProjectCap);
|
|
928
|
+
const groups = [];
|
|
929
|
+
for (const [key, rows] of byKey) {
|
|
930
|
+
const shown = rows.slice(0, cap); // rows are recency-desc (pool was sorted)
|
|
931
|
+
groups.push({ key, total: rows.length, shown, more: rows.length - shown.length, maxTs: rows[0].timestamp });
|
|
932
|
+
}
|
|
933
|
+
groups.sort((a, b) => (a.maxTs < b.maxTs ? 1 : a.maxTs > b.maxTs ? -1 : a.key.localeCompare(b.key)));
|
|
934
|
+
return { groups, projectCount: byKey.size };
|
|
935
|
+
}
|
|
936
|
+
/**
|
|
937
|
+
* The grouped project overview — the bare interactive default. Shows the latest
|
|
938
|
+
* sessions grouped under their project, newest-active project first, with a
|
|
939
|
+
* `· N more` per project and a `+N more projects` when the list is capped.
|
|
940
|
+
*/
|
|
941
|
+
function printSessionOverview(pool, hiddenCount, liveIndex, opts) {
|
|
942
|
+
const { groups } = buildOverviewGroups(pool, opts.expand ? Infinity : opts.perProjectCap);
|
|
943
|
+
const shownGroups = opts.expand ? groups : groups.slice(0, OVERVIEW_MAX_PROJECTS);
|
|
944
|
+
const hiddenProjects = groups.length - shownGroups.length;
|
|
945
|
+
const total = pool.length;
|
|
946
|
+
const projWord = groups.length === 1 ? 'project' : 'projects';
|
|
947
|
+
console.log(chalk.gray(`${total} session${total === 1 ? '' : 's'} · ${groups.length} ${projWord} · recent activity\n`));
|
|
948
|
+
let first = true;
|
|
949
|
+
for (const g of shownGroups) {
|
|
950
|
+
if (!first)
|
|
951
|
+
console.log();
|
|
952
|
+
first = false;
|
|
953
|
+
const { glyph } = liveGlyphAndPreview(liveIndex?.get(g.shown[0].id));
|
|
954
|
+
const head = `${chalk.cyan('▸')} ${chalk.cyan.bold(g.key)} ${chalk.gray(String(g.total))}` +
|
|
955
|
+
`${glyph ? ' ' + glyph : ''} ${chalk.gray(formatRelativeTime(g.maxTs))}`;
|
|
956
|
+
console.log(head);
|
|
957
|
+
for (const s of g.shown)
|
|
958
|
+
console.log(treeSessionRow(s, liveIndex?.get(s.id)));
|
|
959
|
+
if (g.more > 0)
|
|
960
|
+
console.log(' ' + chalk.gray(`· ${g.more} more`));
|
|
961
|
+
}
|
|
962
|
+
console.log();
|
|
963
|
+
const parts = [chalk.gray('newest first')];
|
|
964
|
+
if (hiddenProjects > 0)
|
|
965
|
+
parts.push(chalk.gray(`+${hiddenProjects} more project${hiddenProjects === 1 ? '' : 's'} · agents sessions --all`));
|
|
966
|
+
parts.push(chalk.gray('agents sessions <project> to drill in · --flat for the plain list'));
|
|
967
|
+
console.log(parts.join(chalk.gray(' · ')));
|
|
968
|
+
if (hiddenCount > 0)
|
|
969
|
+
console.log(chalk.gray(formatTeamHiddenFooter(hiddenCount)));
|
|
970
|
+
}
|
|
796
971
|
function printSessionTable(sessions, hiddenCount = 0, tree = false, liveIndex) {
|
|
797
972
|
if (tree) {
|
|
798
973
|
// Group by directory; drop the id/version columns from view. The short id
|
|
@@ -823,10 +998,12 @@ function printSessionTable(sessions, hiddenCount = 0, tree = false, liveIndex) {
|
|
|
823
998
|
return;
|
|
824
999
|
}
|
|
825
1000
|
// Only show the ticket column when at least one row carries a ref — otherwise
|
|
826
|
-
// it's a column of dashes that steals width from every topic.
|
|
1001
|
+
// it's a column of dashes that steals width from every topic. The machine
|
|
1002
|
+
// column (and its compact labels) is computed the same way the picker does it.
|
|
827
1003
|
const showTicket = sessions.some((s) => ticketLabel(s) !== '');
|
|
1004
|
+
const cols = pickerColumnsFor(sessions);
|
|
828
1005
|
for (const session of sessions)
|
|
829
|
-
console.log(flatSessionRow(session, liveIndex?.get(session.id), showTicket));
|
|
1006
|
+
console.log(flatSessionRow(session, liveIndex?.get(session.id), showTicket, cols));
|
|
830
1007
|
const countLine = `${sessions.length} session${sessions.length === 1 ? '' : 's'}.`;
|
|
831
1008
|
console.log(chalk.gray(`\n${countLine}`));
|
|
832
1009
|
if (hiddenCount > 0) {
|
|
@@ -983,7 +1160,58 @@ function renderTopicCell(label, topic, query, visibleWidth, paddedWidth) {
|
|
|
983
1160
|
}
|
|
984
1161
|
return out + padding;
|
|
985
1162
|
}
|
|
986
|
-
|
|
1163
|
+
/** Fallback machine-column width when a pool-derived width isn't supplied.
|
|
1164
|
+
* `pickerColumnsFor` normally computes `machineWidth` sized to the actual
|
|
1165
|
+
* hostnames, floored/capped by these bounds so common ids like `yosemite-s0`
|
|
1166
|
+
* (11) fit whole while a pathological hostname can't devour the topic column. */
|
|
1167
|
+
const PICKER_MACHINE_W = 11;
|
|
1168
|
+
const PICKER_MACHINE_MIN = 8;
|
|
1169
|
+
const PICKER_MACHINE_MAX = 18;
|
|
1170
|
+
/** Column width that shows every compacted hostname in `machines` whole (one
|
|
1171
|
+
* trailing space for separation), bounded by MIN/MAX. */
|
|
1172
|
+
function machineColumnWidth(machines, label) {
|
|
1173
|
+
const widest = machines.reduce((w, m) => Math.max(w, stringWidth(label(m))), 0);
|
|
1174
|
+
return Math.min(PICKER_MACHINE_MAX, Math.max(PICKER_MACHINE_MIN, widest + 1));
|
|
1175
|
+
}
|
|
1176
|
+
/**
|
|
1177
|
+
* Compact display form for machine ids: strip the longest shared dash-delimited
|
|
1178
|
+
* prefix so "yosemite-s0"/"yosemite-s1" read as "s0"/"s1" while unrelated ids
|
|
1179
|
+
* ("zion", "mac-mini") stay whole. Stripping a *common* prefix can't collide,
|
|
1180
|
+
* and at least one segment is always kept.
|
|
1181
|
+
*/
|
|
1182
|
+
export function machineLabeler(machines) {
|
|
1183
|
+
const uniq = [...new Set(machines.filter(Boolean))];
|
|
1184
|
+
if (uniq.length < 2)
|
|
1185
|
+
return (m) => m;
|
|
1186
|
+
const parts = uniq.map((m) => m.split('-'));
|
|
1187
|
+
const min = Math.min(...parts.map((p) => p.length));
|
|
1188
|
+
let shared = 0;
|
|
1189
|
+
while (shared < min - 1 && parts.every((p) => p[shared] === parts[0][shared]))
|
|
1190
|
+
shared++;
|
|
1191
|
+
if (shared === 0)
|
|
1192
|
+
return (m) => m;
|
|
1193
|
+
return (m) => {
|
|
1194
|
+
const p = m.split('-');
|
|
1195
|
+
return p.length > shared ? p.slice(shared).join('-') : m;
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1199
|
+
* Column flags for a picker, computed once over the whole pool so every row
|
|
1200
|
+
* aligns: the machine column only earns its width when the listing spans more
|
|
1201
|
+
* than one box, the ticket column only when some row carries a PR/ticket ref.
|
|
1202
|
+
*/
|
|
1203
|
+
export function pickerColumnsFor(sessions) {
|
|
1204
|
+
const machines = sessions.map((s) => s.machine).filter((m) => !!m);
|
|
1205
|
+
const distinct = [...new Set(machines)];
|
|
1206
|
+
const machineLabel = machineLabeler(machines);
|
|
1207
|
+
return {
|
|
1208
|
+
showMachine: distinct.length > 1,
|
|
1209
|
+
machineLabel,
|
|
1210
|
+
machineWidth: machineColumnWidth(distinct, machineLabel),
|
|
1211
|
+
showTicket: sessions.some((s) => ticketLabel(s) !== ''),
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1214
|
+
export function formatPickerLabel(s, query, cols = {}) {
|
|
987
1215
|
const agentColor = colorAgent(s.agent);
|
|
988
1216
|
const when = formatRelativeTime(s.timestamp);
|
|
989
1217
|
const project = s.project || '-';
|
|
@@ -991,20 +1219,56 @@ export function formatPickerLabel(s, query) {
|
|
|
991
1219
|
const label = s.label;
|
|
992
1220
|
const topic = tag ? `${tag}${s.topic ?? ''}` : s.topic;
|
|
993
1221
|
const versionStr = s.version || '-';
|
|
1222
|
+
const wt = s.worktreeSlug ? chalk.magenta(`wt:${s.worktreeSlug}`) : '';
|
|
1223
|
+
const machineW = cols.machineWidth ?? PICKER_MACHINE_W;
|
|
1224
|
+
const machineCell = cols.showMachine
|
|
1225
|
+
? chalk.gray(padRight(truncate((cols.machineLabel?.(s.machine ?? '') ?? s.machine ?? '') || '-', machineW - 1), machineW))
|
|
1226
|
+
: '';
|
|
1227
|
+
const TICKET_W = 10;
|
|
1228
|
+
const ticketCell = cols.showTicket
|
|
1229
|
+
? chalk.blue(padRight(truncate(ticketLabel(s) || '-', TICKET_W), TICKET_W + 1))
|
|
1230
|
+
: '';
|
|
1231
|
+
// The picker prepends a gutter (cursor, plus a checkbox in multi-select mode);
|
|
1232
|
+
// reserve it, plus the conditional columns, so the topic shrinks to fit and
|
|
1233
|
+
// rows never wrap.
|
|
1234
|
+
const gutter = cols.gutter ?? 2;
|
|
1235
|
+
const machineColW = cols.showMachine ? machineW : 0;
|
|
1236
|
+
const ticketW = cols.showTicket ? TICKET_W + 1 : 0;
|
|
1237
|
+
const wtW = wt ? stringWidth(wt) + 1 : 0;
|
|
1238
|
+
const topicW = Math.max(16, terminalWidth() - gutter - (10 + 9 + 8 + 16) - machineColW - ticketW - wtW - stringWidth(when) - 1);
|
|
994
1239
|
return (chalk.white(padRight(s.shortId, 10)) +
|
|
995
1240
|
agentColor(padRight(truncate(s.agent, 8), 9)) +
|
|
996
1241
|
chalk.yellow(padRight(truncate(versionStr, 7), 8)) +
|
|
1242
|
+
machineCell +
|
|
997
1243
|
chalk.cyan(padRight(truncate(project, 14), 16)) +
|
|
998
|
-
renderTopicCell(label, topic, query,
|
|
1244
|
+
renderTopicCell(label, topic, query, topicW, topicW) +
|
|
1245
|
+
ticketCell +
|
|
1246
|
+
(wt ? wt + ' ' : '') +
|
|
999
1247
|
chalk.gray(when));
|
|
1000
1248
|
}
|
|
1249
|
+
/** Hints rotated above the picker so the flags/features stay discoverable. */
|
|
1250
|
+
const PICKER_TIPS = [
|
|
1251
|
+
'Tip: narrow with -a/--agent (e.g. -a codex), or --project <name> for another folder.',
|
|
1252
|
+
"Tip: --all searches every directory; -H/--host <machine> folds in another box's sessions.",
|
|
1253
|
+
'Tip: just type to fuzzy-search prompts and responses; press space to preview a session.',
|
|
1254
|
+
'Tip: --since 2d / --until <date> bound the time window; pass a session id to open it directly.',
|
|
1255
|
+
];
|
|
1256
|
+
/**
|
|
1257
|
+
* Pick a hint to show above the picker. Deterministic (keys off the pool size)
|
|
1258
|
+
* so it stays fixed across the picker's re-renders within a single run.
|
|
1259
|
+
*/
|
|
1260
|
+
export function formatPickerTip(sessions) {
|
|
1261
|
+
return chalk.gray(PICKER_TIPS[sessions.length % PICKER_TIPS.length]);
|
|
1262
|
+
}
|
|
1001
1263
|
export async function pickSessionInteractive(sessions, message = 'Search sessions:', initialSearch, hiddenCount = 0) {
|
|
1002
1264
|
if (hiddenCount > 0) {
|
|
1003
1265
|
console.log(chalk.gray(formatTeamHiddenFooter(hiddenCount)));
|
|
1004
1266
|
}
|
|
1267
|
+
const cols = pickerColumnsFor(sessions);
|
|
1005
1268
|
try {
|
|
1006
1269
|
return await sessionPicker({
|
|
1007
1270
|
message,
|
|
1271
|
+
subtitle: formatPickerTip(sessions),
|
|
1008
1272
|
sessions,
|
|
1009
1273
|
filter: (query) => {
|
|
1010
1274
|
// No query: show the full pool (picker viewport still paginates via pageSize).
|
|
@@ -1013,7 +1277,7 @@ export async function pickSessionInteractive(sessions, message = 'Search session
|
|
|
1013
1277
|
return sessions;
|
|
1014
1278
|
return filterSessionsByQuery(sessions, query);
|
|
1015
1279
|
},
|
|
1016
|
-
labelFor: (s, query) => formatPickerLabel(s, query),
|
|
1280
|
+
labelFor: (s, query) => formatPickerLabel(s, query, cols),
|
|
1017
1281
|
pageSize: PICKER_RECENT_COUNT,
|
|
1018
1282
|
initialSearch,
|
|
1019
1283
|
});
|
|
@@ -1024,7 +1288,41 @@ export async function pickSessionInteractive(sessions, message = 'Search session
|
|
|
1024
1288
|
throw err;
|
|
1025
1289
|
}
|
|
1026
1290
|
}
|
|
1291
|
+
/**
|
|
1292
|
+
* The machine a picked session lives on when its transcript is on that peer's
|
|
1293
|
+
* disk (folded in over the live fan-out), else undefined. Keys off `_remote`,
|
|
1294
|
+
* NOT `machine !== local`: a synced mirror is machine-tagged too, but its file
|
|
1295
|
+
* is a local mirror path, so it must be read/resumed locally like any other.
|
|
1296
|
+
*/
|
|
1297
|
+
function remoteMachineOf(session) {
|
|
1298
|
+
return session._remote ? session.machine : undefined;
|
|
1299
|
+
}
|
|
1300
|
+
/** True when the peer wasn't a dialable device; prints one clear line so a
|
|
1301
|
+
* remote pick never dead-ends silently. */
|
|
1302
|
+
function warnNoPeerTarget(machine, session) {
|
|
1303
|
+
console.log(chalk.yellow(`Session ${session.shortId} lives on ${machine}, which isn't a reachable device right now.`));
|
|
1304
|
+
console.log(chalk.gray(`Register/wake it (ag devices), or run there: agents ssh ${machine}`));
|
|
1305
|
+
}
|
|
1027
1306
|
async function handlePickedSession(picked) {
|
|
1307
|
+
// A session on another machine is read/resumed ON that machine over SSH — its
|
|
1308
|
+
// transcript and agent binary live there. Both actions execute on the peer
|
|
1309
|
+
// (not a local `--host` hop, which would discover locally and dead-end for a
|
|
1310
|
+
// session that exists only on the peer).
|
|
1311
|
+
const remote = remoteMachineOf(picked.session);
|
|
1312
|
+
if (remote) {
|
|
1313
|
+
if (picked.action === 'view') {
|
|
1314
|
+
const rc = await runOnPeer(['sessions', picked.session.shortId, '--markdown'], remote);
|
|
1315
|
+
if (rc === 'no-target')
|
|
1316
|
+
warnNoPeerTarget(remote, picked.session);
|
|
1317
|
+
}
|
|
1318
|
+
else {
|
|
1319
|
+
console.log(chalk.gray(`Resuming ${picked.session.shortId} on ${remote} over SSH...`));
|
|
1320
|
+
const rc = await runOnPeer(['sessions', 'resume', picked.session.shortId], remote, { tty: true });
|
|
1321
|
+
if (rc === 'no-target')
|
|
1322
|
+
warnNoPeerTarget(remote, picked.session);
|
|
1323
|
+
}
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1028
1326
|
if (picked.action === 'view') {
|
|
1029
1327
|
await renderSession(picked.session, 'summary', {});
|
|
1030
1328
|
return;
|
|
@@ -1049,26 +1347,49 @@ export async function resumeSessionInPlace(session) {
|
|
|
1049
1347
|
return;
|
|
1050
1348
|
}
|
|
1051
1349
|
console.log(chalk.gray(`Resuming: ${resume.join(' ')} (cwd: ${cwd})`));
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1350
|
+
// Resolve the (possibly version-pinned) launcher up front. On Windows the
|
|
1351
|
+
// agent shim is a `.cmd`/`.ps1` and, under the shell needed to run it (see
|
|
1352
|
+
// spawnResumeCommand), a missing command exits non-zero rather than emitting
|
|
1353
|
+
// an ENOENT `error` event — so detect a removed version here instead of
|
|
1354
|
+
// relying on that event, keeping the /continue fallback working on every OS.
|
|
1355
|
+
if (!findExecutable(resume[0]) && session.version) {
|
|
1356
|
+
const fallback = buildFallbackCommand(session);
|
|
1357
|
+
if (fallback) {
|
|
1358
|
+
console.log(chalk.gray(`Version ${session.version} is not installed. Falling back to current version via /continue...`));
|
|
1359
|
+
await spawnResumeCommand(fallback, cwd);
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
await spawnResumeCommand(resume, cwd);
|
|
1364
|
+
}
|
|
1365
|
+
/**
|
|
1366
|
+
* Spawn a resume command as a foreground takeover (inherited stdio), resolving
|
|
1367
|
+
* when it exits. On Windows the agent launcher is a `.cmd`/PATHEXT shim that
|
|
1368
|
+
* `spawn` can't exec directly — a bare-name `shell:false` spawn throws
|
|
1369
|
+
* `EFTYPE`/`ENOENT` there — so we go through the shell via `needsWindowsShell`.
|
|
1370
|
+
* The spawn is guarded because such a failure can be thrown synchronously;
|
|
1371
|
+
* without the guard it would surface under an unrelated "Failed to discover
|
|
1372
|
+
* sessions" catch upstream instead of a truthful launch error.
|
|
1373
|
+
*/
|
|
1374
|
+
function spawnResumeCommand(cmd, cwd) {
|
|
1375
|
+
return new Promise((resolve) => {
|
|
1376
|
+
let child;
|
|
1377
|
+
try {
|
|
1378
|
+
child = spawn(cmd[0], cmd.slice(1), {
|
|
1379
|
+
cwd,
|
|
1380
|
+
stdio: 'inherit',
|
|
1381
|
+
shell: needsWindowsShell(cmd[0]),
|
|
1382
|
+
});
|
|
1383
|
+
}
|
|
1384
|
+
catch (err) {
|
|
1385
|
+
console.error(chalk.red(`Failed to launch ${cmd[0]}: ${err.message}`));
|
|
1386
|
+
resolve();
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1058
1389
|
child.on('error', (err) => {
|
|
1059
|
-
|
|
1060
|
-
const fallback = buildFallbackCommand(session);
|
|
1061
|
-
if (fallback) {
|
|
1062
|
-
console.log(chalk.gray(`Version ${session.version} is not installed. Falling back to current version via /continue...`));
|
|
1063
|
-
const fb = spawn(fallback[0], fallback.slice(1), { cwd, stdio: 'inherit', shell: false });
|
|
1064
|
-
fb.on('error', (e) => { console.error(chalk.red(`Failed: ${e.message}`)); resolve(); });
|
|
1065
|
-
fb.on('close', () => resolve());
|
|
1066
|
-
return;
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1069
|
-
console.error(chalk.red(`Failed to launch ${resume[0]}: ${err.message}`));
|
|
1390
|
+
console.error(chalk.red(`Failed to launch ${cmd[0]}: ${err.message}`));
|
|
1070
1391
|
if (err.code === 'ENOENT') {
|
|
1071
|
-
console.error(chalk.gray(`Make sure '${
|
|
1392
|
+
console.error(chalk.gray(`Make sure '${cmd[0]}' is on your PATH.`));
|
|
1072
1393
|
}
|
|
1073
1394
|
resolve();
|
|
1074
1395
|
});
|
|
@@ -1481,9 +1802,10 @@ export function registerSessionsCommands(program) {
|
|
|
1481
1802
|
.option('--artifacts', 'List all files written or edited during a session')
|
|
1482
1803
|
.option('--artifact <name>', 'Read a specific artifact by filename or path (outputs to stdout)')
|
|
1483
1804
|
.option('--active', 'Show only sessions running right now across terminals, teams, cloud, and headless agents')
|
|
1484
|
-
.option('--local', '
|
|
1805
|
+
.option('--local', 'Only this machine — skip the cross-machine SSH fan-out (default listing and --active)')
|
|
1485
1806
|
.option('--waiting', 'With --active: show only sessions waiting on your input (exits non-zero if any)')
|
|
1486
1807
|
.option('--tree', 'Group the listing by directory; drops the id/version columns for readability')
|
|
1808
|
+
.option('--flat', 'Plain flat table (one row per session) instead of the grouped project overview')
|
|
1487
1809
|
.option('--no-live', 'Do not enrich the listing with live status/preview for running sessions')
|
|
1488
1810
|
.option('--cloud', 'Source sessions from Rush Cloud (captured runs) instead of local disk')
|
|
1489
1811
|
.option('-H, --host <target...>', 'Run this query on remote machine(s) over SSH (host alias or user@host; repeatable)');
|
|
@@ -1501,6 +1823,10 @@ export function registerSessionsCommands(program) {
|
|
|
1501
1823
|
# Show only what's running right now (terminals, teams, cloud, headless)
|
|
1502
1824
|
agents sessions --active
|
|
1503
1825
|
|
|
1826
|
+
# The interactive list folds in other online machines automatically,
|
|
1827
|
+
# labelled by host with this machine first. Stay local with --local:
|
|
1828
|
+
agents sessions --local
|
|
1829
|
+
|
|
1504
1830
|
# Search across every directory, not just this project
|
|
1505
1831
|
agents sessions "topic" --all
|
|
1506
1832
|
|
|
@@ -1514,6 +1840,7 @@ export function registerSessionsCommands(program) {
|
|
|
1514
1840
|
agents sessions --all "deploy script" --host box-a --host box-b
|
|
1515
1841
|
`,
|
|
1516
1842
|
notes: `
|
|
1843
|
+
- The interactive listing folds 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; --json and single-id lookups stay local.
|
|
1517
1844
|
- --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.
|
|
1518
1845
|
- --include and --exclude are mutually exclusive.
|
|
1519
1846
|
- --first and --last are mutually exclusive.
|
|
@@ -1528,6 +1855,7 @@ export function registerSessionsCommands(program) {
|
|
|
1528
1855
|
registerSessionsTailCommand(sessionsCmd);
|
|
1529
1856
|
registerSessionsSyncCommand(sessionsCmd);
|
|
1530
1857
|
registerSessionsResumeCommand(sessionsCmd);
|
|
1858
|
+
registerSessionsInjectCommand(sessionsCmd);
|
|
1531
1859
|
}
|
|
1532
1860
|
function formatNoSessionsMessage(showAll, project) {
|
|
1533
1861
|
const projectQuery = project?.trim();
|
package/dist/commands/setup.js
CHANGED
|
@@ -76,9 +76,11 @@ export async function runSetup(program, options = {}) {
|
|
|
76
76
|
console.log(chalk.gray(`Populate ~/.agents/.system/ yourself before running other commands that depend on it.\n`));
|
|
77
77
|
}
|
|
78
78
|
else {
|
|
79
|
-
console.log(chalk.gray(
|
|
79
|
+
console.log(chalk.gray(alreadyConfigured
|
|
80
|
+
? `Updating the system repo from ${systemRepoSlug(systemRepo)} in ~/.agents/.system/.\n`
|
|
81
|
+
: `Cloning the system repo from ${systemRepoSlug(systemRepo)} into ~/.agents/.system/.\n`));
|
|
80
82
|
ensureAgentsDir();
|
|
81
|
-
const spinner = ora('Cloning system repo...').start();
|
|
83
|
+
const spinner = ora(alreadyConfigured ? 'Updating system repo...' : 'Cloning system repo...').start();
|
|
82
84
|
if (isGitRepo(agentsDir)) {
|
|
83
85
|
// --force on an existing repo: pull instead of re-clone
|
|
84
86
|
const result = await pullRepo(agentsDir);
|
package/dist/commands/sync.d.ts
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
* `agents sync` — synchronize central resources into an installed agent version.
|
|
3
3
|
*
|
|
4
4
|
* Forms:
|
|
5
|
-
* agents sync # umbrella: fetch
|
|
5
|
+
* agents sync # umbrella: fetch config repos -> reconcile all (secrets/sessions opt-in)
|
|
6
6
|
* agents sync --repos|--secrets|--sessions # umbrella: fetch only those, then reconcile
|
|
7
7
|
* agents sync --cloud # umbrella: fetch all, skip reconcile
|
|
8
8
|
* agents sync --local # umbrella: reconcile all, no fetch
|
|
9
|
+
* agents sync system # one repo: git pull --rebase (pull-only mirror)
|
|
10
|
+
* agents sync user # one repo: git pull --rebase + push
|
|
9
11
|
* agents sync claude # one agent: uses default/sole installed version
|
|
10
12
|
* agents sync claude@2.1.142 # one agent: explicit version
|
|
11
13
|
* agents sync claude@latest # one agent: newest installed
|