@phnx-labs/agents-cli 1.20.35 → 1.20.37
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-sync.d.ts +3 -0
- package/dist/commands/sessions-sync.js +44 -4
- package/dist/commands/sessions.d.ts +78 -1
- package/dist/commands/sessions.js +506 -66
- 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 +196 -18
- package/dist/lib/devices/registry.d.ts +7 -0
- package/dist/lib/devices/registry.js +24 -0
- package/dist/lib/devices/resolve-target.d.ts +24 -0
- package/dist/lib/devices/resolve-target.js +80 -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 +73 -0
- package/dist/lib/session/active.js +199 -22
- package/dist/lib/session/db.d.ts +2 -1
- package/dist/lib/session/db.js +41 -5
- package/dist/lib/session/discover.d.ts +39 -0
- package/dist/lib/session/discover.js +127 -29
- package/dist/lib/session/ghostty-tabs.d.ts +33 -0
- package/dist/lib/session/ghostty-tabs.js +126 -0
- 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/relative-time.js +6 -2
- package/dist/lib/session/remote-active.js +19 -17
- package/dist/lib/session/remote-list.d.ts +51 -0
- package/dist/lib/session/remote-list.js +205 -0
- package/dist/lib/session/remote.d.ts +7 -1
- package/dist/lib/session/remote.js +20 -4
- package/dist/lib/session/sync/config.d.ts +14 -15
- package/dist/lib/session/sync/config.js +60 -20
- package/dist/lib/session/types.d.ts +23 -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 +21 -13
- 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/tmux/session.d.ts +10 -0
- package/dist/lib/tmux/session.js +31 -0
- 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,18 @@ 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
|
+
import { enumerateGhosttyTabs, assignGhosttyTabs } from '../lib/session/ghostty-tabs.js';
|
|
21
|
+
import { mapPanesToTargets } from '../lib/tmux/session.js';
|
|
20
22
|
import { machineId, normalizeHost } from '../lib/session/sync/config.js';
|
|
21
|
-
import { gatherRemoteActive } from '../lib/session/remote-active.js';
|
|
23
|
+
import { gatherRemoteActive, NO_FANOUT_ENV } from '../lib/session/remote-active.js';
|
|
24
|
+
import { gatherRemoteList, runOnPeer } from '../lib/session/remote-list.js';
|
|
22
25
|
import { stringWidth, truncateToWidth, padToWidth, terminalWidth } from '../lib/session/width.js';
|
|
23
26
|
import { discoverSessions, countSessionsInScope, resolveSessionById, searchContentIndex } from '../lib/session/discover.js';
|
|
24
27
|
import { filterTeamSessions } from '../lib/session/team-filter.js';
|
|
25
28
|
import { parseSession } from '../lib/session/parse.js';
|
|
26
|
-
import { runRemoteSessions } from '../lib/session/remote.js';
|
|
29
|
+
import { runRemoteSessions, buildForwardedArgs } from '../lib/session/remote.js';
|
|
27
30
|
import { formatRelativeTime } from '../lib/session/relative-time.js';
|
|
28
31
|
import { renderConversationMarkdown, renderSummary, renderSummaryHeader, computeSummaryStats, renderJson, filterEvents, parseRoleList } from '../lib/session/render.js';
|
|
29
32
|
import { renderMarkdown } from '../lib/markdown.js';
|
|
@@ -36,7 +39,26 @@ import { setHelpSections } from '../lib/help.js';
|
|
|
36
39
|
import { registerSessionsTailCommand } from './sessions-tail.js';
|
|
37
40
|
import { registerSessionsSyncCommand } from './sessions-sync.js';
|
|
38
41
|
import { registerSessionsResumeCommand } from './sessions-resume.js';
|
|
42
|
+
import { registerSessionsInjectCommand } from './sessions-inject.js';
|
|
39
43
|
const SESSION_AGENT_FILTER_HELP = `Filter by agent, e.g. claude, codex, claude@2.0.65`;
|
|
44
|
+
/**
|
|
45
|
+
* The prioritized harnesses that get a boolean shorthand flag (e.g. `--claude`
|
|
46
|
+
* === `--agent claude`). The rest stay reachable via `--agent <name>`, which
|
|
47
|
+
* also carries version pins like `codex@0.116.0`.
|
|
48
|
+
*/
|
|
49
|
+
const AGENT_SHORTHANDS = ['claude', 'codex', 'kimi', 'antigravity', 'grok', 'opencode'];
|
|
50
|
+
/**
|
|
51
|
+
* Resolve a per-agent shorthand (`--claude`, `--kimi`, …) into `options.agent`.
|
|
52
|
+
* An explicit `--agent` wins; if two shorthands are passed we take the first and
|
|
53
|
+
* ignore the rest (commander gives no ordering, so this is a best-effort alias).
|
|
54
|
+
*/
|
|
55
|
+
function applyAgentShorthands(options) {
|
|
56
|
+
if (options.agent)
|
|
57
|
+
return;
|
|
58
|
+
const hit = AGENT_SHORTHANDS.find((name) => options[name] === true);
|
|
59
|
+
if (hit)
|
|
60
|
+
options.agent = hit;
|
|
61
|
+
}
|
|
40
62
|
const CLAUDE_RESUME_MATCH_WINDOW_MS = 10 * 60_000;
|
|
41
63
|
const LOAD_VERBS = ['Loading', 'Scanning', 'Gathering', 'Indexing', 'Reading'];
|
|
42
64
|
const FIND_VERBS = ['Finding', 'Searching', 'Locating', 'Matching'];
|
|
@@ -78,6 +100,12 @@ function createScanProgressTracker(verbs, suffix, spinner) {
|
|
|
78
100
|
}
|
|
79
101
|
const PICKER_RECENT_COUNT = 15;
|
|
80
102
|
const PICKER_POOL_LIMIT = 200;
|
|
103
|
+
// The grouped default view ("overview"): fetch a generous recency-ordered pool
|
|
104
|
+
// for accurate per-project totals, show each project's most-recent rows grouped
|
|
105
|
+
// by project, newest-active project first.
|
|
106
|
+
const OVERVIEW_ROWS_PER_PROJECT = 5; // recent rows shown per project before "· N more"
|
|
107
|
+
const OVERVIEW_POOL_LIMIT = 1000; // fetch cap — accurate per-project totals up to this
|
|
108
|
+
const OVERVIEW_MAX_PROJECTS = 12; // project groups shown before "+N more projects"
|
|
81
109
|
/**
|
|
82
110
|
* Resolve a path-like query to an absolute directory path.
|
|
83
111
|
*/
|
|
@@ -166,13 +194,28 @@ function formatStartedAt(startedAtMs) {
|
|
|
166
194
|
return '-';
|
|
167
195
|
return formatRelativeTime(new Date(startedAtMs).toISOString());
|
|
168
196
|
}
|
|
197
|
+
/**
|
|
198
|
+
* Strip terminal/harness noise from a preview so the column stays a single line
|
|
199
|
+
* of plain prose: OSC title escapes, CSI/SGR ANSI, and the harness wrapper tags
|
|
200
|
+
* (`<local-command-stdout>`, `<task-notification>`, `<command-*>`) that leak from
|
|
201
|
+
* a captured transcript tail. Collapses runs of whitespace.
|
|
202
|
+
*/
|
|
203
|
+
export function cleanPreview(text) {
|
|
204
|
+
// eslint-disable-next-line no-control-regex
|
|
205
|
+
return text
|
|
206
|
+
.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, '') // OSC (title) sequences
|
|
207
|
+
.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, '') // CSI / SGR ANSI
|
|
208
|
+
.replace(/<\/?(?:local-command-stdout|command-name|command-message|command-args|task-notification|system-reminder)>/g, '')
|
|
209
|
+
.replace(/\s+/g, ' ')
|
|
210
|
+
.trim();
|
|
211
|
+
}
|
|
169
212
|
/**
|
|
170
213
|
* Build the live description for an active session: prefer the state engine's
|
|
171
214
|
* preview (the latest turn), then a user label, then the first-prompt topic.
|
|
172
215
|
*/
|
|
173
216
|
function buildSessionDescription(s) {
|
|
174
217
|
if (s.context === 'cloud') {
|
|
175
|
-
return s.preview || `${s.cloudProvider ?? ''}${s.cloudTaskId ? ` · ${s.cloudTaskId.slice(0, 12)}` : ''}
|
|
218
|
+
return cleanPreview(s.preview || `${s.cloudProvider ?? ''}${s.cloudTaskId ? ` · ${s.cloudTaskId.slice(0, 12)}` : ''}`);
|
|
176
219
|
}
|
|
177
220
|
if (s.context === 'teams') {
|
|
178
221
|
const parts = [s.teamName];
|
|
@@ -182,10 +225,10 @@ function buildSessionDescription(s) {
|
|
|
182
225
|
parts.push(s.label);
|
|
183
226
|
else if (s.topic)
|
|
184
227
|
parts.push(s.topic);
|
|
185
|
-
return parts.filter(Boolean).join(' · ');
|
|
228
|
+
return cleanPreview(parts.filter(Boolean).join(' · '));
|
|
186
229
|
}
|
|
187
230
|
// Terminal or headless: prefer the live preview, then label, then topic.
|
|
188
|
-
return s.preview || s.label || s.topic || '';
|
|
231
|
+
return cleanPreview(s.preview || s.label || s.topic || '');
|
|
189
232
|
}
|
|
190
233
|
/** Short human word for a session's activity (falls back to the coarse status). */
|
|
191
234
|
function activityLabel(s) {
|
|
@@ -256,20 +299,25 @@ function signalBadges(s) {
|
|
|
256
299
|
return parts.join(' ');
|
|
257
300
|
}
|
|
258
301
|
/**
|
|
259
|
-
* Compact
|
|
260
|
-
* `ssh` flags a remote host
|
|
261
|
-
*
|
|
302
|
+
* Compact locator badge: how to JUMP to the session, not what it's doing.
|
|
303
|
+
* `ssh` flags a remote host. For tmux, prefer the resolved `session:window.pane`
|
|
304
|
+
* (a real `tmux attach -t <session:window>` target) over the raw `%pane` id. For
|
|
305
|
+
* a local Ghostty session we know the tab, show `tab N`. Local, unlocatable
|
|
306
|
+
* sessions add nothing (the common case).
|
|
262
307
|
*/
|
|
263
|
-
function
|
|
264
|
-
|
|
265
|
-
return '';
|
|
308
|
+
function locatorBadge(s) {
|
|
309
|
+
const p = s.provenance;
|
|
266
310
|
const parts = [];
|
|
267
|
-
if (p
|
|
311
|
+
if (p?.transport === 'ssh')
|
|
268
312
|
parts.push(chalk.red('ssh'));
|
|
269
|
-
if (p
|
|
270
|
-
parts.push(chalk.green(
|
|
271
|
-
|
|
313
|
+
if (p?.mux?.kind === 'tmux' && (s.tmuxTarget || p.mux.pane)) {
|
|
314
|
+
parts.push(chalk.green(s.tmuxTarget ?? p.mux.pane));
|
|
315
|
+
}
|
|
316
|
+
else if (p?.mux?.kind === 'screen') {
|
|
272
317
|
parts.push(chalk.green('screen'));
|
|
318
|
+
}
|
|
319
|
+
if (s.ghosttyTab != null)
|
|
320
|
+
parts.push(chalk.green(`tab ${s.ghosttyTab}`));
|
|
273
321
|
return parts.join(' ');
|
|
274
322
|
}
|
|
275
323
|
/**
|
|
@@ -285,7 +333,7 @@ function printActiveRow(s, indent) {
|
|
|
285
333
|
const hostCol = chalk.gray(padToWidth(truncateToWidth(s.host ?? '-', 8), 9));
|
|
286
334
|
const statusCol = statusColor(s.status)(padToWidth(truncateToWidth(activityLabel(s), 8), 9));
|
|
287
335
|
const fork = s.pidCount && s.pidCount > 1 ? chalk.dim(`×${s.pidCount} `) : '';
|
|
288
|
-
const badges = (fork ? fork : '') + [signalBadges(s),
|
|
336
|
+
const badges = (fork ? fork : '') + [signalBadges(s), locatorBadge(s)].filter(Boolean).join(' ');
|
|
289
337
|
const desc = buildSessionDescription(s) || '-';
|
|
290
338
|
// Fill the remaining width with the preview so nothing wraps under tmux/SSH.
|
|
291
339
|
const fixed = stringWidth(indent) + 9 + 9 + 9 + 9 + (badges ? stringWidth(badges) + 1 : 0);
|
|
@@ -423,6 +471,62 @@ export function dedupeByMachineSession(sessions) {
|
|
|
423
471
|
}
|
|
424
472
|
return out;
|
|
425
473
|
}
|
|
474
|
+
/**
|
|
475
|
+
* Order a merged listing so the local machine's sessions come first, then each
|
|
476
|
+
* remote machine as a contiguous block (more sessions first, then name), with
|
|
477
|
+
* every machine keeping its incoming order (timestamp) within the block. Also
|
|
478
|
+
* dedupes: a session present both locally (a synced mirror copy) and via live
|
|
479
|
+
* fan-out collapses to one, keyed by machine + session id. Rows are keyed by
|
|
480
|
+
* `machine` (discover tags local rows with the local id; fan-out tags remote
|
|
481
|
+
* rows with the peer id) falling back to `localMachine` when untagged. Pure —
|
|
482
|
+
* `localMachine` is injected so the ordering is testable without os.hostname().
|
|
483
|
+
*/
|
|
484
|
+
export function mergeLocalFirst(sessions, localMachine) {
|
|
485
|
+
const byMachine = new Map();
|
|
486
|
+
const seen = new Set();
|
|
487
|
+
for (const s of sessions) {
|
|
488
|
+
const machine = s.machine || localMachine;
|
|
489
|
+
if (s.id) {
|
|
490
|
+
const dedupeKey = `${machine}:${s.id}`;
|
|
491
|
+
if (seen.has(dedupeKey))
|
|
492
|
+
continue;
|
|
493
|
+
seen.add(dedupeKey);
|
|
494
|
+
}
|
|
495
|
+
(byMachine.get(machine) ?? byMachine.set(machine, []).get(machine)).push(s);
|
|
496
|
+
}
|
|
497
|
+
const keys = Array.from(byMachine.keys()).sort((a, b) => {
|
|
498
|
+
if (a === localMachine)
|
|
499
|
+
return -1;
|
|
500
|
+
if (b === localMachine)
|
|
501
|
+
return 1;
|
|
502
|
+
const ac = byMachine.get(a).length, bc = byMachine.get(b).length;
|
|
503
|
+
if (ac !== bc)
|
|
504
|
+
return bc - ac;
|
|
505
|
+
return a.localeCompare(b);
|
|
506
|
+
});
|
|
507
|
+
return keys.flatMap((k) => byMachine.get(k));
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* `running N · idle N · waiting N · queued N` for a bucket of sessions (zero
|
|
511
|
+
* buckets omitted). Same bucketing as the grand-total summary so per-group
|
|
512
|
+
* counts reconcile with the `(total)` beside the header. Empty when nothing.
|
|
513
|
+
*/
|
|
514
|
+
function groupTally(sessions) {
|
|
515
|
+
const running = sessions.filter(s => s.status === 'running').length;
|
|
516
|
+
const idle = sessions.filter(s => s.status === 'idle').length;
|
|
517
|
+
const waiting = sessions.filter(s => s.status === 'input_required').length;
|
|
518
|
+
const queued = sessions.filter(s => s.status === 'queued').length;
|
|
519
|
+
const parts = [];
|
|
520
|
+
if (running)
|
|
521
|
+
parts.push(`${running} running`);
|
|
522
|
+
if (idle)
|
|
523
|
+
parts.push(`${idle} idle`);
|
|
524
|
+
if (waiting)
|
|
525
|
+
parts.push(`${waiting} waiting`);
|
|
526
|
+
if (queued)
|
|
527
|
+
parts.push(`${queued} queued`);
|
|
528
|
+
return parts.join(' · ');
|
|
529
|
+
}
|
|
426
530
|
/** Print one machine's workspace tree, indented under its machine header. */
|
|
427
531
|
function renderWorkspaceLayout(layout, base) {
|
|
428
532
|
let first = true;
|
|
@@ -435,7 +539,9 @@ function renderWorkspaceLayout(layout, base) {
|
|
|
435
539
|
: ws.key === '__unknown__'
|
|
436
540
|
? chalk.gray.bold('unknown')
|
|
437
541
|
: chalk.cyan.bold(shortCwd(ws.key));
|
|
438
|
-
|
|
542
|
+
const wsSessions = [...ws.windows.flatMap(w => w.sessions), ...ws.flat];
|
|
543
|
+
const tally = groupTally(wsSessions);
|
|
544
|
+
console.log(`${base}${header} ${chalk.gray(`(${ws.total})`)}${tally ? chalk.gray(` ${tally}`) : ''}`);
|
|
439
545
|
for (const win of ws.windows) {
|
|
440
546
|
// Host is per-process, but every terminal in the same IDE window shares
|
|
441
547
|
// an ancestor — take the first non-empty host as the window's label.
|
|
@@ -457,6 +563,43 @@ function printMachineHeader(mg) {
|
|
|
457
563
|
const here = mg.isLocal ? chalk.cyan(' ← this machine') : '';
|
|
458
564
|
console.log(`${marker}${name} ${chalk.gray(`(${mg.total})`)}${here}`);
|
|
459
565
|
}
|
|
566
|
+
/**
|
|
567
|
+
* Attach display-only jump locators onto LOCAL sessions: the Ghostty tab number
|
|
568
|
+
* (one batched read-only osascript, only when a local ghostty session exists)
|
|
569
|
+
* and the tmux `session:window.pane` target (one `list-panes -a` per socket).
|
|
570
|
+
* Every step is best-effort and swallowed — a failure just leaves the raw pane
|
|
571
|
+
* id / no tab number, and the rows render as before. Mutates the sessions.
|
|
572
|
+
*/
|
|
573
|
+
async function enrichLocalLocators(local) {
|
|
574
|
+
// Ghostty tab numbers.
|
|
575
|
+
try {
|
|
576
|
+
const ghostty = local.filter(s => s.host === 'ghostty' && s.provenance?.transport !== 'ssh');
|
|
577
|
+
if (ghostty.length > 0) {
|
|
578
|
+
const surfaces = await enumerateGhosttyTabs();
|
|
579
|
+
for (const [sess, tab] of assignGhosttyTabs(ghostty, surfaces))
|
|
580
|
+
sess.ghosttyTab = tab;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
catch { /* non-fatal */ }
|
|
584
|
+
// tmux attach targets, one batched query per distinct socket.
|
|
585
|
+
try {
|
|
586
|
+
const tmux = local.filter(s => s.provenance?.mux?.kind === 'tmux' && s.provenance.mux.pane);
|
|
587
|
+
const sockets = new Set(tmux.map(s => s.provenance.mux.socket));
|
|
588
|
+
for (const socket of sockets) {
|
|
589
|
+
const paneMap = await mapPanesToTargets(socket);
|
|
590
|
+
if (paneMap.size === 0)
|
|
591
|
+
continue;
|
|
592
|
+
for (const s of tmux) {
|
|
593
|
+
if (s.provenance.mux.socket !== socket)
|
|
594
|
+
continue;
|
|
595
|
+
const target = paneMap.get(s.provenance.mux.pane);
|
|
596
|
+
if (target)
|
|
597
|
+
s.tmuxTarget = target;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
catch { /* non-fatal */ }
|
|
602
|
+
}
|
|
460
603
|
/**
|
|
461
604
|
* Render the unified active-session view, grouped by machine. Local sessions
|
|
462
605
|
* come from `getActiveSessions()`; unless `--local`, sessions from other
|
|
@@ -492,6 +635,10 @@ async function renderActiveSessions(asJson, waitingOnly = false, opts = {}) {
|
|
|
492
635
|
printCrossMachineTip();
|
|
493
636
|
return;
|
|
494
637
|
}
|
|
638
|
+
// Enrich LOCAL sessions with jump locators (display-only, after the --json /
|
|
639
|
+
// --waiting gates so scriptable output stays osascript-free). Remote sessions
|
|
640
|
+
// keep their raw pane id — their tmux/Ghostty live on the other machine.
|
|
641
|
+
await enrichLocalLocators(sessions.filter(s => !s.machine || s.machine === self));
|
|
495
642
|
const grouped = groupSessionsByMachine(sessions, self);
|
|
496
643
|
let firstMachine = true;
|
|
497
644
|
for (const mg of grouped.machines) {
|
|
@@ -501,16 +648,7 @@ async function renderActiveSessions(asJson, waitingOnly = false, opts = {}) {
|
|
|
501
648
|
printMachineHeader(mg);
|
|
502
649
|
renderWorkspaceLayout(mg.layout, ' ');
|
|
503
650
|
}
|
|
504
|
-
const
|
|
505
|
-
const idleCount = sessions.filter(s => s.status === 'idle').length;
|
|
506
|
-
const queuedCount = sessions.filter(s => s.status === 'queued' || s.status === 'input_required').length;
|
|
507
|
-
const parts = [];
|
|
508
|
-
if (runningCount > 0)
|
|
509
|
-
parts.push(`${runningCount} running`);
|
|
510
|
-
if (idleCount > 0)
|
|
511
|
-
parts.push(`${idleCount} idle`);
|
|
512
|
-
if (queuedCount > 0)
|
|
513
|
-
parts.push(`${queuedCount} queued`);
|
|
651
|
+
const parts = groupTally(sessions).split(' · ').filter(Boolean);
|
|
514
652
|
const machineWord = grouped.machines.length === 1 ? 'machine' : 'machines';
|
|
515
653
|
console.log(chalk.gray(`\n${sessions.length} active (${parts.join(', ')}) across ${grouped.machines.length} ${machineWord}.`));
|
|
516
654
|
// Tip only when nothing else could be included and the user didn't opt out.
|
|
@@ -526,6 +664,13 @@ function printCrossMachineTip() {
|
|
|
526
664
|
}
|
|
527
665
|
/** Main action handler for `agents sessions`. Routes to picker, table, or single-session render. */
|
|
528
666
|
async function sessionsAction(query, options) {
|
|
667
|
+
// Normalize convenience flags before any routing reads them: per-agent
|
|
668
|
+
// shorthands fold into --agent, and --device is an alias for --host (both
|
|
669
|
+
// resolve against the same device registry).
|
|
670
|
+
applyAgentShorthands(options);
|
|
671
|
+
if (options.device && options.device.length > 0) {
|
|
672
|
+
options.host = [...(options.host ?? []), ...options.device];
|
|
673
|
+
}
|
|
529
674
|
// --host WITHOUT --active keeps the legacy per-host stream (each remote's raw
|
|
530
675
|
// stdout under a `── host ──` banner). With --active, the hosts are folded
|
|
531
676
|
// into the merged machine-grouped view instead (handled below).
|
|
@@ -599,8 +744,18 @@ async function sessionsAction(query, options) {
|
|
|
599
744
|
// Interactive picker loads a deep pool but shows only recent sessions
|
|
600
745
|
// until the user starts typing. Non-interactive/JSON uses the explicit limit.
|
|
601
746
|
const isInteractive = !options.json && isInteractiveTerminal();
|
|
602
|
-
|
|
603
|
-
|
|
747
|
+
// The grouped project overview is the default for a bare interactive listing:
|
|
748
|
+
// no query, no path drill-in, not explicitly --flat/--tree. It drops the silent
|
|
749
|
+
// cwd-scope + 50-cap + 30-day window that hide most of a large index.
|
|
750
|
+
const wantsOverview = isInteractive && !searchQuery && !pathFilter && !options.flat && !options.tree;
|
|
751
|
+
const limit = wantsOverview
|
|
752
|
+
? OVERVIEW_POOL_LIMIT
|
|
753
|
+
: parseInt(options.limit || (isInteractive ? String(PICKER_POOL_LIMIT) : '50'), 10);
|
|
754
|
+
// Overview: recency order across the whole index, no default window; an explicit
|
|
755
|
+
// --since still narrows. Non-overview keeps the prior interactive-30d default.
|
|
756
|
+
const since = wantsOverview
|
|
757
|
+
? options.since
|
|
758
|
+
: (options.since ?? (isInteractive && !options.all ? '30d' : undefined));
|
|
604
759
|
const spinner = options.json ? null : ora().start();
|
|
605
760
|
const tracker = createScanProgressTracker(LOAD_VERBS, 'sessions', spinner);
|
|
606
761
|
try {
|
|
@@ -615,7 +770,10 @@ async function sessionsAction(query, options) {
|
|
|
615
770
|
version,
|
|
616
771
|
all: pathFilter ? undefined : options.all,
|
|
617
772
|
cwd: process.cwd(),
|
|
618
|
-
|
|
773
|
+
// Default overview scopes to the current repo SUBTREE (prefix match), so a
|
|
774
|
+
// monorepo shows its sub-projects grouped instead of collapsing to the one
|
|
775
|
+
// exact-cwd project. `--all` clears the prefix and spans the whole index.
|
|
776
|
+
cwdPrefix: pathFilter ?? (wantsOverview && !options.all ? process.cwd() : undefined),
|
|
619
777
|
project: options.project,
|
|
620
778
|
since,
|
|
621
779
|
until: options.until,
|
|
@@ -657,12 +815,42 @@ async function sessionsAction(query, options) {
|
|
|
657
815
|
if (options.json) {
|
|
658
816
|
const filtered = searchQuery ? filterSessionsByQuery(sessions, searchQuery) : sessions;
|
|
659
817
|
const serializable = filtered.map(s => {
|
|
660
|
-
const { _matchedTerms, _bm25Score, ...rest } = s;
|
|
818
|
+
const { _matchedTerms, _bm25Score, _remote, ...rest } = s;
|
|
661
819
|
return rest;
|
|
662
820
|
});
|
|
663
821
|
process.stdout.write(JSON.stringify(serializable, null, 2) + '\n');
|
|
664
822
|
return;
|
|
665
823
|
}
|
|
824
|
+
// Cross-machine fan-out: unless --local (or we ARE a peer answering a
|
|
825
|
+
// parent's sweep), fold in other online machines' sessions live over SSH so
|
|
826
|
+
// the list spans the fleet without any sync — each remote row carries the
|
|
827
|
+
// machine it came from, and the picker/table label + group by it. Only the
|
|
828
|
+
// interactive picker and the printed table get this; --json and single-id
|
|
829
|
+
// resolution above stay local (a peer answers for itself; scripts get a
|
|
830
|
+
// deterministic local slice). Best-effort: a fan-out failure leaves the
|
|
831
|
+
// local list intact rather than erroring the whole command.
|
|
832
|
+
const forceLocal = options.local === true || process.env[NO_FANOUT_ENV] === '1';
|
|
833
|
+
if (!forceLocal) {
|
|
834
|
+
// Pass the hosts set so a variadic `--host a b` never leaks a host as a
|
|
835
|
+
// query (defensive: the --host-without-active early return above already
|
|
836
|
+
// means we only get here in auto-discovery mode, with no --host in argv).
|
|
837
|
+
const forwarded = buildForwardedArgs(process.argv, new Set(options.host ?? []));
|
|
838
|
+
if (!forwarded.includes('--json'))
|
|
839
|
+
forwarded.push('--json');
|
|
840
|
+
const fanSpinner = isInteractiveTerminal() ? ora('Reaching other machines...').start() : null;
|
|
841
|
+
try {
|
|
842
|
+
const { sessions: remoteSessions } = await gatherRemoteList(forwarded, options.host);
|
|
843
|
+
if (remoteSessions.length > 0) {
|
|
844
|
+
sessions = mergeLocalFirst([...sessions, ...remoteSessions], machineId());
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
catch {
|
|
848
|
+
// fan-out is an enrichment, never a hard dependency
|
|
849
|
+
}
|
|
850
|
+
finally {
|
|
851
|
+
fanSpinner?.stop();
|
|
852
|
+
}
|
|
853
|
+
}
|
|
666
854
|
if (sessions.length === 0) {
|
|
667
855
|
if (pathFilter) {
|
|
668
856
|
console.log(chalk.gray(`No sessions found for ${pathFilter}.`));
|
|
@@ -675,9 +863,19 @@ async function sessionsAction(query, options) {
|
|
|
675
863
|
}
|
|
676
864
|
return;
|
|
677
865
|
}
|
|
678
|
-
//
|
|
679
|
-
//
|
|
680
|
-
|
|
866
|
+
// The grouped project overview is the bare interactive default: a scannable
|
|
867
|
+
// dashboard of the whole fleet grouped by project, newest-active first.
|
|
868
|
+
// Interact/resume via `agents sessions <project>` or `agents sessions resume`.
|
|
869
|
+
if (wantsOverview) {
|
|
870
|
+
const liveIndex = await maybeLiveIndex(options);
|
|
871
|
+
// Per-project row cap is fixed (--limit carries a default of 50 and drives
|
|
872
|
+
// the fetch pool, not the display); `--all` expands every group instead.
|
|
873
|
+
printSessionOverview(sessions, hiddenCount, liveIndex, { perProjectCap: OVERVIEW_ROWS_PER_PROJECT, expand: !!options.all });
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
// --tree / --flat are printed listings, not an interactive pick — render them
|
|
877
|
+
// directly even in a TTY. A search query keeps the interactive picker.
|
|
878
|
+
if (isInteractiveTerminal() && !options.tree && !options.flat) {
|
|
681
879
|
const message = pathFilter
|
|
682
880
|
? `Search sessions (${path.basename(pathFilter)}):`
|
|
683
881
|
: formatSearchMessage(options);
|
|
@@ -688,7 +886,7 @@ async function sessionsAction(query, options) {
|
|
|
688
886
|
}
|
|
689
887
|
return;
|
|
690
888
|
}
|
|
691
|
-
// Non-interactive fallback (piped output)
|
|
889
|
+
// Non-interactive fallback (piped output) or --flat/--tree.
|
|
692
890
|
const filtered = searchQuery ? filterSessionsByQuery(sessions, searchQuery) : sessions;
|
|
693
891
|
const liveIndex = await maybeLiveIndex(options);
|
|
694
892
|
printSessionTable(filtered, hiddenCount, options.tree === true, liveIndex);
|
|
@@ -724,9 +922,9 @@ function metaSignals(s) {
|
|
|
724
922
|
* (tracker/PR ref, pulled out of the badge blob so refs align) is only rendered
|
|
725
923
|
* when `showTicket` — otherwise a listing with no refs would waste a column of
|
|
726
924
|
* dashes and needlessly truncate the topic. Worktree stays a trailing badge. */
|
|
727
|
-
function flatSessionRow(session, live, showTicket = false) {
|
|
925
|
+
function flatSessionRow(session, live, showTicket = false, cols = {}) {
|
|
728
926
|
const agentColor = colorAgent(session.agent);
|
|
729
|
-
const when = formatRelativeTime(session.timestamp);
|
|
927
|
+
const when = formatRelativeTime(session.lastActivity ?? session.timestamp);
|
|
730
928
|
const project = session.project || '-';
|
|
731
929
|
const tag = teamTag(session);
|
|
732
930
|
const label = session.label;
|
|
@@ -735,17 +933,26 @@ function flatSessionRow(session, live, showTicket = false) {
|
|
|
735
933
|
// resting one falls back to its opening topic.
|
|
736
934
|
const doing = preview || (tag ? `${tag}${session.topic ?? ''}` : session.topic);
|
|
737
935
|
const wt = session.worktreeSlug ? chalk.magenta(`wt:${session.worktreeSlug}`) : '';
|
|
936
|
+
// The machine column only earns its width when the listing spans more than one
|
|
937
|
+
// box (i.e. the cross-machine fan-out folded remotes in) — same rule and
|
|
938
|
+
// pool-derived width as the picker.
|
|
939
|
+
const machineColW = cols.machineWidth ?? PICKER_MACHINE_W;
|
|
940
|
+
const machineCell = cols.showMachine
|
|
941
|
+
? chalk.gray(padToWidth(truncateToWidth((cols.machineLabel?.(session.machine ?? '') ?? session.machine ?? '') || '-', machineColW - 1), machineColW))
|
|
942
|
+
: '';
|
|
738
943
|
const TICKET_W = 10;
|
|
739
944
|
const ticketCell = showTicket
|
|
740
945
|
? chalk.blue(padToWidth(truncateToWidth(ticketLabel(session) || '-', TICKET_W), TICKET_W + 1))
|
|
741
946
|
: '';
|
|
742
947
|
const glyphW = glyph ? 2 : 0;
|
|
948
|
+
const machineW = cols.showMachine ? machineColW : 0;
|
|
743
949
|
const ticketW = showTicket ? TICKET_W + 1 : 0;
|
|
744
950
|
const wtW = wt ? stringWidth(wt) + 1 : 0;
|
|
745
|
-
const topicW = Math.max(16, terminalWidth() - (10 + 9 + 8 + 16) - glyphW - ticketW - wtW - stringWidth(when) - 1);
|
|
951
|
+
const topicW = Math.max(16, terminalWidth() - (10 + 9 + 8 + 16) - glyphW - machineW - ticketW - wtW - stringWidth(when) - 1);
|
|
746
952
|
return (chalk.white(padToWidth(truncateToWidth(session.shortId, 9), 10)) +
|
|
747
953
|
agentColor(padToWidth(truncateToWidth(session.agent, 8), 9)) +
|
|
748
954
|
chalk.yellow(padToWidth(truncateToWidth(session.version || '-', 7), 8)) +
|
|
955
|
+
machineCell +
|
|
749
956
|
chalk.cyan(padToWidth(truncateToWidth(project, 14), 16)) +
|
|
750
957
|
(glyph ? glyph + ' ' : '') +
|
|
751
958
|
renderTopicCell(label, doing, '', topicW, topicW) +
|
|
@@ -756,7 +963,7 @@ function flatSessionRow(session, live, showTicket = false) {
|
|
|
756
963
|
/** One tree-mode row (grouped under a dir header): id · agent · badges · topic · time. No version/project column. */
|
|
757
964
|
function treeSessionRow(session, live) {
|
|
758
965
|
const agentColor = colorAgent(session.agent);
|
|
759
|
-
const when = formatRelativeTime(session.timestamp);
|
|
966
|
+
const when = formatRelativeTime(session.lastActivity ?? session.timestamp);
|
|
760
967
|
const tag = teamTag(session);
|
|
761
968
|
const label = session.label;
|
|
762
969
|
const { glyph, preview } = liveGlyphAndPreview(live);
|
|
@@ -793,6 +1000,79 @@ async function maybeLiveIndex(options) {
|
|
|
793
1000
|
return undefined;
|
|
794
1001
|
}
|
|
795
1002
|
}
|
|
1003
|
+
/**
|
|
1004
|
+
* Group key for the overview: prefer the indexed project name; else fold the cwd
|
|
1005
|
+
* to its repo — a worktree (`.../<repo>/.agents/worktrees/<slug>`) folds to the
|
|
1006
|
+
* repo, and a monorepo subdir falls back to its leaf dir basename. Pure.
|
|
1007
|
+
*/
|
|
1008
|
+
export function overviewProjectKey(s) {
|
|
1009
|
+
if (s.project && s.project.trim())
|
|
1010
|
+
return s.project.trim();
|
|
1011
|
+
const cwd = (s.cwd ?? '').replace(/\/+$/, '');
|
|
1012
|
+
if (!cwd)
|
|
1013
|
+
return '(no project)';
|
|
1014
|
+
const wt = cwd.match(/\/([^/]+)\/\.agents\/worktrees\//);
|
|
1015
|
+
if (wt)
|
|
1016
|
+
return wt[1];
|
|
1017
|
+
const parts = cwd.split('/');
|
|
1018
|
+
return parts[parts.length - 1] || cwd;
|
|
1019
|
+
}
|
|
1020
|
+
/**
|
|
1021
|
+
* Turn a recency-descending pool into project groups: each group shows its
|
|
1022
|
+
* `perProjectCap` most-recent sessions (the rest become `· N more`), and groups
|
|
1023
|
+
* are ordered by their most-recent session so the newest-active project leads.
|
|
1024
|
+
* `perProjectCap = Infinity` expands every group. Pure — unit-tested.
|
|
1025
|
+
*/
|
|
1026
|
+
export function buildOverviewGroups(pool, perProjectCap) {
|
|
1027
|
+
const byKey = new Map();
|
|
1028
|
+
for (const s of pool) {
|
|
1029
|
+
const k = overviewProjectKey(s);
|
|
1030
|
+
(byKey.get(k) ?? byKey.set(k, []).get(k)).push(s);
|
|
1031
|
+
}
|
|
1032
|
+
const cap = Math.max(1, perProjectCap);
|
|
1033
|
+
const groups = [];
|
|
1034
|
+
for (const [key, rows] of byKey) {
|
|
1035
|
+
const shown = rows.slice(0, cap); // rows are recency-desc (pool was sorted)
|
|
1036
|
+
groups.push({ key, total: rows.length, shown, more: rows.length - shown.length, maxTs: rows[0].lastActivity ?? rows[0].timestamp });
|
|
1037
|
+
}
|
|
1038
|
+
groups.sort((a, b) => (a.maxTs < b.maxTs ? 1 : a.maxTs > b.maxTs ? -1 : a.key.localeCompare(b.key)));
|
|
1039
|
+
return { groups, projectCount: byKey.size };
|
|
1040
|
+
}
|
|
1041
|
+
/**
|
|
1042
|
+
* The grouped project overview — the bare interactive default. Shows the latest
|
|
1043
|
+
* sessions grouped under their project, newest-active project first, with a
|
|
1044
|
+
* `· N more` per project and a `+N more projects` when the list is capped.
|
|
1045
|
+
*/
|
|
1046
|
+
function printSessionOverview(pool, hiddenCount, liveIndex, opts) {
|
|
1047
|
+
const { groups } = buildOverviewGroups(pool, opts.expand ? Infinity : opts.perProjectCap);
|
|
1048
|
+
const shownGroups = opts.expand ? groups : groups.slice(0, OVERVIEW_MAX_PROJECTS);
|
|
1049
|
+
const hiddenProjects = groups.length - shownGroups.length;
|
|
1050
|
+
const total = pool.length;
|
|
1051
|
+
const projWord = groups.length === 1 ? 'project' : 'projects';
|
|
1052
|
+
console.log(chalk.gray(`${total} session${total === 1 ? '' : 's'} · ${groups.length} ${projWord} · recent activity\n`));
|
|
1053
|
+
let first = true;
|
|
1054
|
+
for (const g of shownGroups) {
|
|
1055
|
+
if (!first)
|
|
1056
|
+
console.log();
|
|
1057
|
+
first = false;
|
|
1058
|
+
const { glyph } = liveGlyphAndPreview(liveIndex?.get(g.shown[0].id));
|
|
1059
|
+
const head = `${chalk.cyan('▸')} ${chalk.cyan.bold(g.key)} ${chalk.gray(String(g.total))}` +
|
|
1060
|
+
`${glyph ? ' ' + glyph : ''} ${chalk.gray(formatRelativeTime(g.maxTs))}`;
|
|
1061
|
+
console.log(head);
|
|
1062
|
+
for (const s of g.shown)
|
|
1063
|
+
console.log(treeSessionRow(s, liveIndex?.get(s.id)));
|
|
1064
|
+
if (g.more > 0)
|
|
1065
|
+
console.log(' ' + chalk.gray(`· ${g.more} more`));
|
|
1066
|
+
}
|
|
1067
|
+
console.log();
|
|
1068
|
+
const parts = [chalk.gray('newest first (by last activity)')];
|
|
1069
|
+
if (hiddenProjects > 0)
|
|
1070
|
+
parts.push(chalk.gray(`+${hiddenProjects} more project${hiddenProjects === 1 ? '' : 's'}`));
|
|
1071
|
+
parts.push(chalk.gray('agents sessions --all spans every project on disk · <project> to drill in · --flat for the plain list'));
|
|
1072
|
+
console.log(parts.join(chalk.gray(' · ')));
|
|
1073
|
+
if (hiddenCount > 0)
|
|
1074
|
+
console.log(chalk.gray(formatTeamHiddenFooter(hiddenCount)));
|
|
1075
|
+
}
|
|
796
1076
|
function printSessionTable(sessions, hiddenCount = 0, tree = false, liveIndex) {
|
|
797
1077
|
if (tree) {
|
|
798
1078
|
// Group by directory; drop the id/version columns from view. The short id
|
|
@@ -823,10 +1103,12 @@ function printSessionTable(sessions, hiddenCount = 0, tree = false, liveIndex) {
|
|
|
823
1103
|
return;
|
|
824
1104
|
}
|
|
825
1105
|
// 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.
|
|
1106
|
+
// it's a column of dashes that steals width from every topic. The machine
|
|
1107
|
+
// column (and its compact labels) is computed the same way the picker does it.
|
|
827
1108
|
const showTicket = sessions.some((s) => ticketLabel(s) !== '');
|
|
1109
|
+
const cols = pickerColumnsFor(sessions);
|
|
828
1110
|
for (const session of sessions)
|
|
829
|
-
console.log(flatSessionRow(session, liveIndex?.get(session.id), showTicket));
|
|
1111
|
+
console.log(flatSessionRow(session, liveIndex?.get(session.id), showTicket, cols));
|
|
830
1112
|
const countLine = `${sessions.length} session${sessions.length === 1 ? '' : 's'}.`;
|
|
831
1113
|
console.log(chalk.gray(`\n${countLine}`));
|
|
832
1114
|
if (hiddenCount > 0) {
|
|
@@ -983,28 +1265,115 @@ function renderTopicCell(label, topic, query, visibleWidth, paddedWidth) {
|
|
|
983
1265
|
}
|
|
984
1266
|
return out + padding;
|
|
985
1267
|
}
|
|
986
|
-
|
|
1268
|
+
/** Fallback machine-column width when a pool-derived width isn't supplied.
|
|
1269
|
+
* `pickerColumnsFor` normally computes `machineWidth` sized to the actual
|
|
1270
|
+
* hostnames, floored/capped by these bounds so common ids like `yosemite-s0`
|
|
1271
|
+
* (11) fit whole while a pathological hostname can't devour the topic column. */
|
|
1272
|
+
const PICKER_MACHINE_W = 11;
|
|
1273
|
+
const PICKER_MACHINE_MIN = 8;
|
|
1274
|
+
const PICKER_MACHINE_MAX = 18;
|
|
1275
|
+
/** Column width that shows every compacted hostname in `machines` whole (one
|
|
1276
|
+
* trailing space for separation), bounded by MIN/MAX. */
|
|
1277
|
+
function machineColumnWidth(machines, label) {
|
|
1278
|
+
const widest = machines.reduce((w, m) => Math.max(w, stringWidth(label(m))), 0);
|
|
1279
|
+
return Math.min(PICKER_MACHINE_MAX, Math.max(PICKER_MACHINE_MIN, widest + 1));
|
|
1280
|
+
}
|
|
1281
|
+
/**
|
|
1282
|
+
* Compact display form for machine ids: strip the longest shared dash-delimited
|
|
1283
|
+
* prefix so "yosemite-s0"/"yosemite-s1" read as "s0"/"s1" while unrelated ids
|
|
1284
|
+
* ("zion", "mac-mini") stay whole. Stripping a *common* prefix can't collide,
|
|
1285
|
+
* and at least one segment is always kept.
|
|
1286
|
+
*/
|
|
1287
|
+
export function machineLabeler(machines) {
|
|
1288
|
+
const uniq = [...new Set(machines.filter(Boolean))];
|
|
1289
|
+
if (uniq.length < 2)
|
|
1290
|
+
return (m) => m;
|
|
1291
|
+
const parts = uniq.map((m) => m.split('-'));
|
|
1292
|
+
const min = Math.min(...parts.map((p) => p.length));
|
|
1293
|
+
let shared = 0;
|
|
1294
|
+
while (shared < min - 1 && parts.every((p) => p[shared] === parts[0][shared]))
|
|
1295
|
+
shared++;
|
|
1296
|
+
if (shared === 0)
|
|
1297
|
+
return (m) => m;
|
|
1298
|
+
return (m) => {
|
|
1299
|
+
const p = m.split('-');
|
|
1300
|
+
return p.length > shared ? p.slice(shared).join('-') : m;
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
/**
|
|
1304
|
+
* Column flags for a picker, computed once over the whole pool so every row
|
|
1305
|
+
* aligns: the machine column only earns its width when the listing spans more
|
|
1306
|
+
* than one box, the ticket column only when some row carries a PR/ticket ref.
|
|
1307
|
+
*/
|
|
1308
|
+
export function pickerColumnsFor(sessions) {
|
|
1309
|
+
const machines = sessions.map((s) => s.machine).filter((m) => !!m);
|
|
1310
|
+
const distinct = [...new Set(machines)];
|
|
1311
|
+
const machineLabel = machineLabeler(machines);
|
|
1312
|
+
return {
|
|
1313
|
+
showMachine: distinct.length > 1,
|
|
1314
|
+
machineLabel,
|
|
1315
|
+
machineWidth: machineColumnWidth(distinct, machineLabel),
|
|
1316
|
+
showTicket: sessions.some((s) => ticketLabel(s) !== ''),
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
export function formatPickerLabel(s, query, cols = {}) {
|
|
987
1320
|
const agentColor = colorAgent(s.agent);
|
|
988
|
-
const when = formatRelativeTime(s.timestamp);
|
|
1321
|
+
const when = formatRelativeTime(s.lastActivity ?? s.timestamp);
|
|
989
1322
|
const project = s.project || '-';
|
|
990
1323
|
const tag = teamTag(s);
|
|
991
1324
|
const label = s.label;
|
|
992
1325
|
const topic = tag ? `${tag}${s.topic ?? ''}` : s.topic;
|
|
993
1326
|
const versionStr = s.version || '-';
|
|
1327
|
+
const wt = s.worktreeSlug ? chalk.magenta(`wt:${s.worktreeSlug}`) : '';
|
|
1328
|
+
const machineW = cols.machineWidth ?? PICKER_MACHINE_W;
|
|
1329
|
+
const machineCell = cols.showMachine
|
|
1330
|
+
? chalk.gray(padRight(truncate((cols.machineLabel?.(s.machine ?? '') ?? s.machine ?? '') || '-', machineW - 1), machineW))
|
|
1331
|
+
: '';
|
|
1332
|
+
const TICKET_W = 10;
|
|
1333
|
+
const ticketCell = cols.showTicket
|
|
1334
|
+
? chalk.blue(padRight(truncate(ticketLabel(s) || '-', TICKET_W), TICKET_W + 1))
|
|
1335
|
+
: '';
|
|
1336
|
+
// The picker prepends a gutter (cursor, plus a checkbox in multi-select mode);
|
|
1337
|
+
// reserve it, plus the conditional columns, so the topic shrinks to fit and
|
|
1338
|
+
// rows never wrap.
|
|
1339
|
+
const gutter = cols.gutter ?? 2;
|
|
1340
|
+
const machineColW = cols.showMachine ? machineW : 0;
|
|
1341
|
+
const ticketW = cols.showTicket ? TICKET_W + 1 : 0;
|
|
1342
|
+
const wtW = wt ? stringWidth(wt) + 1 : 0;
|
|
1343
|
+
const topicW = Math.max(16, terminalWidth() - gutter - (10 + 9 + 8 + 16) - machineColW - ticketW - wtW - stringWidth(when) - 1);
|
|
994
1344
|
return (chalk.white(padRight(s.shortId, 10)) +
|
|
995
1345
|
agentColor(padRight(truncate(s.agent, 8), 9)) +
|
|
996
1346
|
chalk.yellow(padRight(truncate(versionStr, 7), 8)) +
|
|
1347
|
+
machineCell +
|
|
997
1348
|
chalk.cyan(padRight(truncate(project, 14), 16)) +
|
|
998
|
-
renderTopicCell(label, topic, query,
|
|
1349
|
+
renderTopicCell(label, topic, query, topicW, topicW) +
|
|
1350
|
+
ticketCell +
|
|
1351
|
+
(wt ? wt + ' ' : '') +
|
|
999
1352
|
chalk.gray(when));
|
|
1000
1353
|
}
|
|
1354
|
+
/** Hints rotated above the picker so the flags/features stay discoverable. */
|
|
1355
|
+
const PICKER_TIPS = [
|
|
1356
|
+
'Tip: narrow with -a/--agent (e.g. -a codex), or --project <name> for another folder.',
|
|
1357
|
+
"Tip: --all searches every directory; -H/--host <machine> folds in another box's sessions.",
|
|
1358
|
+
'Tip: just type to fuzzy-search prompts and responses; press space to preview a session.',
|
|
1359
|
+
'Tip: --since 2d / --until <date> bound the time window; pass a session id to open it directly.',
|
|
1360
|
+
];
|
|
1361
|
+
/**
|
|
1362
|
+
* Pick a hint to show above the picker. Deterministic (keys off the pool size)
|
|
1363
|
+
* so it stays fixed across the picker's re-renders within a single run.
|
|
1364
|
+
*/
|
|
1365
|
+
export function formatPickerTip(sessions) {
|
|
1366
|
+
return chalk.gray(PICKER_TIPS[sessions.length % PICKER_TIPS.length]);
|
|
1367
|
+
}
|
|
1001
1368
|
export async function pickSessionInteractive(sessions, message = 'Search sessions:', initialSearch, hiddenCount = 0) {
|
|
1002
1369
|
if (hiddenCount > 0) {
|
|
1003
1370
|
console.log(chalk.gray(formatTeamHiddenFooter(hiddenCount)));
|
|
1004
1371
|
}
|
|
1372
|
+
const cols = pickerColumnsFor(sessions);
|
|
1005
1373
|
try {
|
|
1006
1374
|
return await sessionPicker({
|
|
1007
1375
|
message,
|
|
1376
|
+
subtitle: formatPickerTip(sessions),
|
|
1008
1377
|
sessions,
|
|
1009
1378
|
filter: (query) => {
|
|
1010
1379
|
// No query: show the full pool (picker viewport still paginates via pageSize).
|
|
@@ -1013,7 +1382,7 @@ export async function pickSessionInteractive(sessions, message = 'Search session
|
|
|
1013
1382
|
return sessions;
|
|
1014
1383
|
return filterSessionsByQuery(sessions, query);
|
|
1015
1384
|
},
|
|
1016
|
-
labelFor: (s, query) => formatPickerLabel(s, query),
|
|
1385
|
+
labelFor: (s, query) => formatPickerLabel(s, query, cols),
|
|
1017
1386
|
pageSize: PICKER_RECENT_COUNT,
|
|
1018
1387
|
initialSearch,
|
|
1019
1388
|
});
|
|
@@ -1024,7 +1393,41 @@ export async function pickSessionInteractive(sessions, message = 'Search session
|
|
|
1024
1393
|
throw err;
|
|
1025
1394
|
}
|
|
1026
1395
|
}
|
|
1396
|
+
/**
|
|
1397
|
+
* The machine a picked session lives on when its transcript is on that peer's
|
|
1398
|
+
* disk (folded in over the live fan-out), else undefined. Keys off `_remote`,
|
|
1399
|
+
* NOT `machine !== local`: a synced mirror is machine-tagged too, but its file
|
|
1400
|
+
* is a local mirror path, so it must be read/resumed locally like any other.
|
|
1401
|
+
*/
|
|
1402
|
+
function remoteMachineOf(session) {
|
|
1403
|
+
return session._remote ? session.machine : undefined;
|
|
1404
|
+
}
|
|
1405
|
+
/** True when the peer wasn't a dialable device; prints one clear line so a
|
|
1406
|
+
* remote pick never dead-ends silently. */
|
|
1407
|
+
function warnNoPeerTarget(machine, session) {
|
|
1408
|
+
console.log(chalk.yellow(`Session ${session.shortId} lives on ${machine}, which isn't a reachable device right now.`));
|
|
1409
|
+
console.log(chalk.gray(`Register/wake it (ag devices), or run there: agents ssh ${machine}`));
|
|
1410
|
+
}
|
|
1027
1411
|
async function handlePickedSession(picked) {
|
|
1412
|
+
// A session on another machine is read/resumed ON that machine over SSH — its
|
|
1413
|
+
// transcript and agent binary live there. Both actions execute on the peer
|
|
1414
|
+
// (not a local `--host` hop, which would discover locally and dead-end for a
|
|
1415
|
+
// session that exists only on the peer).
|
|
1416
|
+
const remote = remoteMachineOf(picked.session);
|
|
1417
|
+
if (remote) {
|
|
1418
|
+
if (picked.action === 'view') {
|
|
1419
|
+
const rc = await runOnPeer(['sessions', picked.session.shortId, '--markdown'], remote);
|
|
1420
|
+
if (rc === 'no-target')
|
|
1421
|
+
warnNoPeerTarget(remote, picked.session);
|
|
1422
|
+
}
|
|
1423
|
+
else {
|
|
1424
|
+
console.log(chalk.gray(`Resuming ${picked.session.shortId} on ${remote} over SSH...`));
|
|
1425
|
+
const rc = await runOnPeer(['sessions', 'resume', picked.session.shortId], remote, { tty: true });
|
|
1426
|
+
if (rc === 'no-target')
|
|
1427
|
+
warnNoPeerTarget(remote, picked.session);
|
|
1428
|
+
}
|
|
1429
|
+
return;
|
|
1430
|
+
}
|
|
1028
1431
|
if (picked.action === 'view') {
|
|
1029
1432
|
await renderSession(picked.session, 'summary', {});
|
|
1030
1433
|
return;
|
|
@@ -1049,26 +1452,49 @@ export async function resumeSessionInPlace(session) {
|
|
|
1049
1452
|
return;
|
|
1050
1453
|
}
|
|
1051
1454
|
console.log(chalk.gray(`Resuming: ${resume.join(' ')} (cwd: ${cwd})`));
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1455
|
+
// Resolve the (possibly version-pinned) launcher up front. On Windows the
|
|
1456
|
+
// agent shim is a `.cmd`/`.ps1` and, under the shell needed to run it (see
|
|
1457
|
+
// spawnResumeCommand), a missing command exits non-zero rather than emitting
|
|
1458
|
+
// an ENOENT `error` event — so detect a removed version here instead of
|
|
1459
|
+
// relying on that event, keeping the /continue fallback working on every OS.
|
|
1460
|
+
if (!findExecutable(resume[0]) && session.version) {
|
|
1461
|
+
const fallback = buildFallbackCommand(session);
|
|
1462
|
+
if (fallback) {
|
|
1463
|
+
console.log(chalk.gray(`Version ${session.version} is not installed. Falling back to current version via /continue...`));
|
|
1464
|
+
await spawnResumeCommand(fallback, cwd);
|
|
1465
|
+
return;
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
await spawnResumeCommand(resume, cwd);
|
|
1469
|
+
}
|
|
1470
|
+
/**
|
|
1471
|
+
* Spawn a resume command as a foreground takeover (inherited stdio), resolving
|
|
1472
|
+
* when it exits. On Windows the agent launcher is a `.cmd`/PATHEXT shim that
|
|
1473
|
+
* `spawn` can't exec directly — a bare-name `shell:false` spawn throws
|
|
1474
|
+
* `EFTYPE`/`ENOENT` there — so we go through the shell via `needsWindowsShell`.
|
|
1475
|
+
* The spawn is guarded because such a failure can be thrown synchronously;
|
|
1476
|
+
* without the guard it would surface under an unrelated "Failed to discover
|
|
1477
|
+
* sessions" catch upstream instead of a truthful launch error.
|
|
1478
|
+
*/
|
|
1479
|
+
function spawnResumeCommand(cmd, cwd) {
|
|
1480
|
+
return new Promise((resolve) => {
|
|
1481
|
+
let child;
|
|
1482
|
+
try {
|
|
1483
|
+
child = spawn(cmd[0], cmd.slice(1), {
|
|
1484
|
+
cwd,
|
|
1485
|
+
stdio: 'inherit',
|
|
1486
|
+
shell: needsWindowsShell(cmd[0]),
|
|
1487
|
+
});
|
|
1488
|
+
}
|
|
1489
|
+
catch (err) {
|
|
1490
|
+
console.error(chalk.red(`Failed to launch ${cmd[0]}: ${err.message}`));
|
|
1491
|
+
resolve();
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1058
1494
|
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}`));
|
|
1495
|
+
console.error(chalk.red(`Failed to launch ${cmd[0]}: ${err.message}`));
|
|
1070
1496
|
if (err.code === 'ENOENT') {
|
|
1071
|
-
console.error(chalk.gray(`Make sure '${
|
|
1497
|
+
console.error(chalk.gray(`Make sure '${cmd[0]}' is on your PATH.`));
|
|
1072
1498
|
}
|
|
1073
1499
|
resolve();
|
|
1074
1500
|
});
|
|
@@ -1464,6 +1890,12 @@ export function registerSessionsCommands(program) {
|
|
|
1464
1890
|
.argument('[query]', 'Session ID, search query, or path (., ../, /path) to filter by project')
|
|
1465
1891
|
.description('Find, browse, and read agent conversation transcripts across Claude, Codex, Gemini, and OpenCode.')
|
|
1466
1892
|
.option('-a, --agent <agent>', 'Filter by agent type and version (e.g., claude, codex@0.116.0)')
|
|
1893
|
+
.option('--claude', 'Shorthand for --agent claude')
|
|
1894
|
+
.option('--codex', 'Shorthand for --agent codex')
|
|
1895
|
+
.option('--kimi', 'Shorthand for --agent kimi')
|
|
1896
|
+
.option('--antigravity', 'Shorthand for --agent antigravity')
|
|
1897
|
+
.option('--grok', 'Shorthand for --agent grok')
|
|
1898
|
+
.option('--opencode', 'Shorthand for --agent opencode')
|
|
1467
1899
|
.option('--all', 'Include sessions from every directory (not just current project)')
|
|
1468
1900
|
.option('--teams', 'Include team-spawned sessions (hidden by default)')
|
|
1469
1901
|
.option('--project <name>', 'Filter by project name (searches across all directories)')
|
|
@@ -1481,12 +1913,14 @@ export function registerSessionsCommands(program) {
|
|
|
1481
1913
|
.option('--artifacts', 'List all files written or edited during a session')
|
|
1482
1914
|
.option('--artifact <name>', 'Read a specific artifact by filename or path (outputs to stdout)')
|
|
1483
1915
|
.option('--active', 'Show only sessions running right now across terminals, teams, cloud, and headless agents')
|
|
1484
|
-
.option('--local', '
|
|
1916
|
+
.option('--local', 'Only this machine — skip the cross-machine SSH fan-out (default listing and --active)')
|
|
1485
1917
|
.option('--waiting', 'With --active: show only sessions waiting on your input (exits non-zero if any)')
|
|
1486
1918
|
.option('--tree', 'Group the listing by directory; drops the id/version columns for readability')
|
|
1919
|
+
.option('--flat', 'Plain flat table (one row per session) instead of the grouped project overview')
|
|
1487
1920
|
.option('--no-live', 'Do not enrich the listing with live status/preview for running sessions')
|
|
1488
1921
|
.option('--cloud', 'Source sessions from Rush Cloud (captured runs) instead of local disk')
|
|
1489
|
-
.option('-H, --host <target...>', 'Run this query on remote machine(s) over SSH (host alias or user@host; repeatable)')
|
|
1922
|
+
.option('-H, --host <target...>', 'Run this query on remote machine(s) over SSH (host alias or user@host; repeatable)')
|
|
1923
|
+
.option('--device <target...>', 'Alias for --host (device alias from `agents devices`; repeatable)');
|
|
1490
1924
|
setHelpSections(sessionsCmd, {
|
|
1491
1925
|
examples: `
|
|
1492
1926
|
# Search prior sessions in this project by topic, file path, or command
|
|
@@ -1501,6 +1935,10 @@ export function registerSessionsCommands(program) {
|
|
|
1501
1935
|
# Show only what's running right now (terminals, teams, cloud, headless)
|
|
1502
1936
|
agents sessions --active
|
|
1503
1937
|
|
|
1938
|
+
# The interactive list folds in other online machines automatically,
|
|
1939
|
+
# labelled by host with this machine first. Stay local with --local:
|
|
1940
|
+
agents sessions --local
|
|
1941
|
+
|
|
1504
1942
|
# Search across every directory, not just this project
|
|
1505
1943
|
agents sessions "topic" --all
|
|
1506
1944
|
|
|
@@ -1514,6 +1952,7 @@ export function registerSessionsCommands(program) {
|
|
|
1514
1952
|
agents sessions --all "deploy script" --host box-a --host box-b
|
|
1515
1953
|
`,
|
|
1516
1954
|
notes: `
|
|
1955
|
+
- 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
1956
|
- --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
1957
|
- --include and --exclude are mutually exclusive.
|
|
1519
1958
|
- --first and --last are mutually exclusive.
|
|
@@ -1528,6 +1967,7 @@ export function registerSessionsCommands(program) {
|
|
|
1528
1967
|
registerSessionsTailCommand(sessionsCmd);
|
|
1529
1968
|
registerSessionsSyncCommand(sessionsCmd);
|
|
1530
1969
|
registerSessionsResumeCommand(sessionsCmd);
|
|
1970
|
+
registerSessionsInjectCommand(sessionsCmd);
|
|
1531
1971
|
}
|
|
1532
1972
|
function formatNoSessionsMessage(showAll, project) {
|
|
1533
1973
|
const projectQuery = project?.trim();
|