@phnx-labs/agents-cli 1.20.33 → 1.20.35
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 +29 -0
- package/README.md +32 -2
- package/dist/commands/computer-actions.d.ts +15 -0
- package/dist/commands/computer-actions.js +120 -22
- package/dist/commands/computer.d.ts +23 -0
- package/dist/commands/computer.js +45 -3
- package/dist/commands/cost.js +17 -12
- package/dist/commands/doctor.d.ts +11 -0
- package/dist/commands/doctor.js +108 -14
- package/dist/commands/events.d.ts +16 -0
- package/dist/commands/events.js +173 -0
- package/dist/commands/exec.js +6 -1
- package/dist/commands/hosts.js +28 -24
- package/dist/commands/import.js +1 -1
- package/dist/commands/inspect.d.ts +2 -0
- package/dist/commands/inspect.js +54 -7
- package/dist/commands/logs.d.ts +17 -0
- package/dist/commands/logs.js +139 -0
- package/dist/commands/models.d.ts +3 -0
- package/dist/commands/models.js +27 -8
- package/dist/commands/permissions.js +9 -2
- package/dist/commands/repo.d.ts +34 -0
- package/dist/commands/repo.js +243 -65
- package/dist/commands/resource-view.d.ts +20 -0
- package/dist/commands/resource-view.js +90 -28
- package/dist/commands/rules.js +1 -1
- package/dist/commands/secrets-migrate.js +23 -11
- package/dist/commands/secrets.d.ts +20 -0
- package/dist/commands/secrets.js +93 -17
- package/dist/commands/sessions-resume.d.ts +2 -0
- package/dist/commands/sessions-resume.js +209 -0
- package/dist/commands/sessions-tail.d.ts +10 -0
- package/dist/commands/sessions-tail.js +11 -0
- package/dist/commands/sessions.d.ts +73 -1
- package/dist/commands/sessions.js +273 -53
- package/dist/commands/status.d.ts +12 -0
- package/dist/commands/status.js +81 -0
- package/dist/commands/teams.js +70 -6
- package/dist/commands/versions.js +2 -1
- package/dist/commands/view.d.ts +42 -0
- package/dist/commands/view.js +213 -83
- package/dist/commands/wallet.d.ts +6 -0
- package/dist/commands/wallet.js +22 -5
- package/dist/index.js +61 -32
- package/dist/lib/acp/harnesses.d.ts +1 -1
- package/dist/lib/acp/harnesses.js +2 -2
- package/dist/lib/agents.d.ts +12 -0
- package/dist/lib/agents.js +145 -41
- package/dist/lib/browser/cdp.js +5 -1
- package/dist/lib/browser/chrome.js +20 -0
- package/dist/lib/browser/drivers/ssh.d.ts +27 -0
- package/dist/lib/browser/drivers/ssh.js +94 -19
- package/dist/lib/browser/ipc.d.ts +3 -0
- package/dist/lib/browser/ipc.js +13 -9
- package/dist/lib/browser/service.js +71 -15
- package/dist/lib/daemon.d.ts +35 -0
- package/dist/lib/daemon.js +33 -5
- package/dist/lib/devices/connect.d.ts +3 -2
- package/dist/lib/devices/connect.js +5 -3
- package/dist/lib/doctor-diff.js +29 -2
- package/dist/lib/drift-sync.d.ts +43 -0
- package/dist/lib/drift-sync.js +179 -0
- package/dist/lib/events.d.ts +9 -2
- package/dist/lib/events.js +70 -11
- package/dist/lib/exec.d.ts +15 -0
- package/dist/lib/exec.js +43 -11
- package/dist/lib/hooks.js +8 -2
- package/dist/lib/hosts/dispatch.js +6 -7
- package/dist/lib/hosts/logs.d.ts +16 -0
- package/dist/lib/hosts/logs.js +45 -0
- package/dist/lib/hosts/progress.d.ts +66 -0
- package/dist/lib/hosts/progress.js +125 -17
- package/dist/lib/hosts/ready.d.ts +23 -2
- package/dist/lib/hosts/ready.js +35 -13
- package/dist/lib/hosts/reconcile.d.ts +53 -0
- package/dist/lib/hosts/reconcile.js +81 -0
- package/dist/lib/hosts/tasks.d.ts +8 -0
- package/dist/lib/hosts/tasks.js +14 -0
- package/dist/lib/permissions.d.ts +4 -0
- package/dist/lib/permissions.js +35 -0
- package/dist/lib/picker.d.ts +24 -0
- package/dist/lib/picker.js +124 -0
- package/dist/lib/platform/winpath.d.ts +31 -2
- package/dist/lib/platform/winpath.js +133 -24
- package/dist/lib/plugin-marketplace.d.ts +30 -0
- package/dist/lib/plugin-marketplace.js +215 -2
- package/dist/lib/plugins.d.ts +5 -0
- package/dist/lib/plugins.js +45 -4
- package/dist/lib/pwsh.d.ts +11 -0
- package/dist/lib/pwsh.js +13 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/agent.d.ts +42 -1
- package/dist/lib/secrets/agent.js +89 -11
- package/dist/lib/secrets/bundles.js +40 -9
- package/dist/lib/secrets/filestore.js +31 -1
- package/dist/lib/secrets/index.d.ts +33 -1
- package/dist/lib/secrets/index.js +90 -9
- package/dist/lib/secrets/windows.d.ts +74 -0
- package/dist/lib/secrets/windows.js +440 -0
- package/dist/lib/session/active.d.ts +7 -0
- package/dist/lib/session/active.js +11 -3
- package/dist/lib/session/discover.js +94 -0
- package/dist/lib/session/parse.d.ts +6 -0
- package/dist/lib/session/parse.js +283 -2
- package/dist/lib/session/pid-registry.d.ts +21 -0
- package/dist/lib/session/pid-registry.js +80 -0
- package/dist/lib/session/remote-active.d.ts +26 -0
- package/dist/lib/session/remote-active.js +141 -0
- package/dist/lib/session/remote.js +2 -6
- package/dist/lib/session/render.js +1 -1
- package/dist/lib/session/sync/config.d.ts +8 -0
- package/dist/lib/session/sync/config.js +11 -2
- package/dist/lib/session/types.d.ts +1 -1
- package/dist/lib/session/types.js +1 -1
- package/dist/lib/shims.d.ts +30 -2
- package/dist/lib/shims.js +107 -50
- package/dist/lib/ssh-exec.d.ts +26 -3
- package/dist/lib/ssh-exec.js +45 -3
- package/dist/lib/ssh-tunnel.d.ts +24 -5
- package/dist/lib/ssh-tunnel.js +60 -62
- package/dist/lib/staleness/writers/hooks.js +1 -1
- package/dist/lib/startup/command-registry.d.ts +3 -0
- package/dist/lib/startup/command-registry.js +6 -0
- package/dist/lib/sync-status.d.ts +102 -0
- package/dist/lib/sync-status.js +135 -0
- package/dist/lib/teams/agents.d.ts +24 -0
- package/dist/lib/teams/agents.js +30 -1
- package/dist/lib/teams/registry.js +25 -9
- package/dist/lib/terminal/backends/ghostty.d.ts +6 -0
- package/dist/lib/terminal/backends/ghostty.js +69 -0
- package/dist/lib/terminal/backends/index.d.ts +18 -0
- package/dist/lib/terminal/backends/index.js +29 -0
- package/dist/lib/terminal/backends/iterm.d.ts +6 -0
- package/dist/lib/terminal/backends/iterm.js +62 -0
- package/dist/lib/terminal/backends/tmux.d.ts +14 -0
- package/dist/lib/terminal/backends/tmux.js +23 -0
- package/dist/lib/terminal/engine.d.ts +39 -0
- package/dist/lib/terminal/engine.js +54 -0
- package/dist/lib/terminal/index.d.ts +14 -0
- package/dist/lib/terminal/index.js +5 -0
- package/dist/lib/terminal/policy.d.ts +11 -0
- package/dist/lib/terminal/policy.js +11 -0
- package/dist/lib/terminal/quote.d.ts +11 -0
- package/dist/lib/terminal/quote.js +13 -0
- package/dist/lib/terminal/shell.d.ts +6 -0
- package/dist/lib/terminal/shell.js +23 -0
- package/dist/lib/terminal/transport.d.ts +15 -0
- package/dist/lib/terminal/transport.js +40 -0
- package/dist/lib/terminal/types.d.ts +59 -0
- package/dist/lib/terminal/types.js +13 -0
- package/dist/lib/types.d.ts +27 -1
- package/dist/lib/usage.d.ts +30 -0
- package/dist/lib/usage.js +159 -2
- package/dist/lib/versions.d.ts +7 -3
- package/dist/lib/versions.js +10 -5
- package/dist/lib/whats-new.d.ts +9 -0
- package/dist/lib/whats-new.js +35 -0
- package/package.json +1 -1
|
@@ -17,6 +17,8 @@ import { SESSION_AGENTS } from '../lib/session/types.js';
|
|
|
17
17
|
import { discoverArtifacts, readArtifact, resolveArtifact } from '../lib/session/artifacts.js';
|
|
18
18
|
import { looksLikePath, toComparablePath, homeDir } from '../lib/platform/index.js';
|
|
19
19
|
import { getActiveSessions } from '../lib/session/active.js';
|
|
20
|
+
import { machineId, normalizeHost } from '../lib/session/sync/config.js';
|
|
21
|
+
import { gatherRemoteActive } from '../lib/session/remote-active.js';
|
|
20
22
|
import { stringWidth, truncateToWidth, padToWidth, terminalWidth } from '../lib/session/width.js';
|
|
21
23
|
import { discoverSessions, countSessionsInScope, resolveSessionById, searchContentIndex } from '../lib/session/discover.js';
|
|
22
24
|
import { filterTeamSessions } from '../lib/session/team-filter.js';
|
|
@@ -33,6 +35,7 @@ import { sessionPicker } from './sessions-picker.js';
|
|
|
33
35
|
import { setHelpSections } from '../lib/help.js';
|
|
34
36
|
import { registerSessionsTailCommand } from './sessions-tail.js';
|
|
35
37
|
import { registerSessionsSyncCommand } from './sessions-sync.js';
|
|
38
|
+
import { registerSessionsResumeCommand } from './sessions-resume.js';
|
|
36
39
|
const SESSION_AGENT_FILTER_HELP = `Filter by agent, e.g. claude, codex, claude@2.0.65`;
|
|
37
40
|
const CLAUDE_RESUME_MATCH_WINDOW_MS = 10 * 60_000;
|
|
38
41
|
const LOAD_VERBS = ['Loading', 'Scanning', 'Gathering', 'Indexing', 'Reading'];
|
|
@@ -194,6 +197,43 @@ function activityLabel(s) {
|
|
|
194
197
|
return 'idle';
|
|
195
198
|
return s.status === 'input_required' ? 'waiting' : s.status;
|
|
196
199
|
}
|
|
200
|
+
/**
|
|
201
|
+
* Index live sessions by their full session UUID so a historical `SessionMeta`
|
|
202
|
+
* row (`meta.id`) can be matched to the session that is still running now.
|
|
203
|
+
* Rows without a sessionId (some cloud/headless probes) are skipped — they
|
|
204
|
+
* can't be correlated back to a transcript on disk.
|
|
205
|
+
*/
|
|
206
|
+
export function indexActiveBySessionId(active) {
|
|
207
|
+
const byId = new Map();
|
|
208
|
+
for (const a of active) {
|
|
209
|
+
if (a.sessionId)
|
|
210
|
+
byId.set(a.sessionId, a);
|
|
211
|
+
}
|
|
212
|
+
return byId;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* The live decoration for a listing row: a status glyph and the latest-turn
|
|
216
|
+
* preview, when the session is still running. `●` running / `◐` waiting on the
|
|
217
|
+
* user / `○` idle, colored by the same `statusColor` the --active view uses.
|
|
218
|
+
* Returns empty strings when there is no live match, so callers render the
|
|
219
|
+
* plain historical row unchanged.
|
|
220
|
+
*/
|
|
221
|
+
export function liveGlyphAndPreview(a) {
|
|
222
|
+
if (!a)
|
|
223
|
+
return { glyph: '', preview: '' };
|
|
224
|
+
const waiting = a.status === 'input_required' || a.activity === 'waiting_input';
|
|
225
|
+
const running = a.status === 'running' || a.activity === 'working';
|
|
226
|
+
const shape = waiting ? '◐' : running ? '●' : '○';
|
|
227
|
+
return { glyph: statusColor(a.status)(shape), preview: buildSessionDescription(a) };
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* The tracker/PR ref for a session's dedicated column: the ticket id when known,
|
|
231
|
+
* else `PR#<n>`, else empty. Pulled out of the trailing badge blob so refs align
|
|
232
|
+
* into a scannable column instead of jamming against a truncated topic.
|
|
233
|
+
*/
|
|
234
|
+
export function ticketLabel(s) {
|
|
235
|
+
return s.ticketId ?? (s.prNumber ? `PR#${s.prNumber}` : '');
|
|
236
|
+
}
|
|
197
237
|
/**
|
|
198
238
|
* Compact, colour-coded badges for the durable/awaiting signals. Text-only (no
|
|
199
239
|
* emoji, per repo convention): `plan` / `ask` / `perm` for why it's waiting,
|
|
@@ -318,48 +358,148 @@ export function groupActiveSessions(sessions) {
|
|
|
318
358
|
});
|
|
319
359
|
return { workspaces };
|
|
320
360
|
}
|
|
321
|
-
/**
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
361
|
+
/**
|
|
362
|
+
* The machine a session belongs to: an explicit tag (set when merging
|
|
363
|
+
* cross-machine results) wins; else the process's provenance host (normalized
|
|
364
|
+
* to the same id form); else the local machine. Never keys off `ActiveSession.host`
|
|
365
|
+
* — that is the terminal *app* (code/tmux), not the computer.
|
|
366
|
+
*/
|
|
367
|
+
function machineKeyFor(s, localMachine) {
|
|
368
|
+
if (s.machine)
|
|
369
|
+
return s.machine;
|
|
370
|
+
if (s.provenance?.host)
|
|
371
|
+
return normalizeHost(s.provenance.host);
|
|
372
|
+
return localMachine;
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Group active sessions by machine, then delegate each machine's sessions to the
|
|
376
|
+
* existing workspace/window grouping. Local machine is pinned first and flagged;
|
|
377
|
+
* the rest sort by session count descending, then name. Pure — `localMachine` is
|
|
378
|
+
* injected so the function stays testable without reading os.hostname().
|
|
379
|
+
*/
|
|
380
|
+
export function groupSessionsByMachine(sessions, localMachine) {
|
|
381
|
+
const byMachine = new Map();
|
|
382
|
+
for (const s of sessions) {
|
|
383
|
+
const key = machineKeyFor(s, localMachine);
|
|
384
|
+
(byMachine.get(key) ?? byMachine.set(key, []).get(key)).push(s);
|
|
385
|
+
}
|
|
386
|
+
const keys = Array.from(byMachine.keys()).sort((a, b) => {
|
|
387
|
+
if (a === localMachine)
|
|
388
|
+
return -1;
|
|
389
|
+
if (b === localMachine)
|
|
390
|
+
return 1;
|
|
391
|
+
const ac = byMachine.get(a).length, bc = byMachine.get(b).length;
|
|
392
|
+
if (ac !== bc)
|
|
393
|
+
return bc - ac;
|
|
394
|
+
return a.localeCompare(b);
|
|
395
|
+
});
|
|
396
|
+
const machines = keys.map((machine) => ({
|
|
397
|
+
machine,
|
|
398
|
+
isLocal: machine === localMachine,
|
|
399
|
+
total: byMachine.get(machine).length,
|
|
400
|
+
layout: groupActiveSessions(byMachine.get(machine)),
|
|
401
|
+
}));
|
|
402
|
+
return { machines };
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Collapse duplicate sessions after a cross-machine merge. Two rows collapse
|
|
406
|
+
* only when they share both machine and session UUID (the same host listed
|
|
407
|
+
* twice, or a local/remote overlap); rows without a sessionId can't be
|
|
408
|
+
* correlated, so they're all kept.
|
|
409
|
+
*/
|
|
410
|
+
export function dedupeByMachineSession(sessions) {
|
|
411
|
+
const seen = new Set();
|
|
412
|
+
const out = [];
|
|
413
|
+
for (const s of sessions) {
|
|
414
|
+
if (!s.sessionId) {
|
|
415
|
+
out.push(s);
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
const key = `${s.machine ?? ''}:${s.sessionId}`;
|
|
419
|
+
if (seen.has(key))
|
|
420
|
+
continue;
|
|
421
|
+
seen.add(key);
|
|
422
|
+
out.push(s);
|
|
338
423
|
}
|
|
339
|
-
|
|
424
|
+
return out;
|
|
425
|
+
}
|
|
426
|
+
/** Print one machine's workspace tree, indented under its machine header. */
|
|
427
|
+
function renderWorkspaceLayout(layout, base) {
|
|
340
428
|
let first = true;
|
|
341
429
|
for (const ws of layout.workspaces) {
|
|
342
430
|
if (!first)
|
|
343
431
|
console.log();
|
|
344
432
|
first = false;
|
|
345
|
-
// Print workspace header
|
|
346
433
|
const header = ws.key === '__cloud__'
|
|
347
434
|
? chalk.magenta.bold('cloud')
|
|
348
435
|
: ws.key === '__unknown__'
|
|
349
436
|
? chalk.gray.bold('unknown')
|
|
350
437
|
: chalk.cyan.bold(shortCwd(ws.key));
|
|
351
|
-
console.log(`${header} ${chalk.gray(`(${ws.total})`)}`);
|
|
438
|
+
console.log(`${base}${header} ${chalk.gray(`(${ws.total})`)}`);
|
|
352
439
|
for (const win of ws.windows) {
|
|
353
440
|
// Host is per-process, but every terminal in the same IDE window shares
|
|
354
441
|
// an ancestor — take the first non-empty host as the window's label.
|
|
355
442
|
const host = win.sessions.find((s) => s.host)?.host ?? 'terminal';
|
|
356
443
|
const winHeader = `${chalk.gray(host)} ${chalk.gray('·')} ${chalk.gray(shortWindowLabel(win.windowId))} ${chalk.gray(`(${win.sessions.length})`)}`;
|
|
357
|
-
console.log(' ' + winHeader);
|
|
444
|
+
console.log(base + ' ' + winHeader);
|
|
358
445
|
for (const s of win.sessions)
|
|
359
|
-
printActiveRow(s, ' ');
|
|
446
|
+
printActiveRow(s, base + ' ');
|
|
360
447
|
}
|
|
361
448
|
for (const s of ws.flat)
|
|
362
|
-
printActiveRow(s, ' ');
|
|
449
|
+
printActiveRow(s, base + ' ');
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
/** Machine header: `▸ <name> ← this machine` for the local box (cyan), matching
|
|
453
|
+
* the `ag devices list` treatment; a plain `▸ <name>` for remotes. */
|
|
454
|
+
function printMachineHeader(mg) {
|
|
455
|
+
const marker = mg.isLocal ? chalk.cyan('▸ ') : chalk.gray('▸ ');
|
|
456
|
+
const name = mg.isLocal ? chalk.bold.cyan(mg.machine) : chalk.bold(mg.machine);
|
|
457
|
+
const here = mg.isLocal ? chalk.cyan(' ← this machine') : '';
|
|
458
|
+
console.log(`${marker}${name} ${chalk.gray(`(${mg.total})`)}${here}`);
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Render the unified active-session view, grouped by machine. Local sessions
|
|
462
|
+
* come from `getActiveSessions()`; unless `--local`, sessions from other
|
|
463
|
+
* machines are folded in over SSH (explicit `--host` targets, else the
|
|
464
|
+
* registered online devices from `ag devices`). A tip is shown when there are
|
|
465
|
+
* no other machines to include.
|
|
466
|
+
*/
|
|
467
|
+
async function renderActiveSessions(asJson, waitingOnly = false, opts = {}) {
|
|
468
|
+
const self = machineId();
|
|
469
|
+
const local = await getActiveSessions();
|
|
470
|
+
for (const s of local)
|
|
471
|
+
if (!s.machine)
|
|
472
|
+
s.machine = self;
|
|
473
|
+
let remoteDeviceCount = 0;
|
|
474
|
+
let merged = local;
|
|
475
|
+
if (!opts.local) {
|
|
476
|
+
const remote = await gatherRemoteActive(opts.hosts);
|
|
477
|
+
remoteDeviceCount = remote.deviceCount;
|
|
478
|
+
merged = dedupeByMachineSession([...local, ...remote.sessions]);
|
|
479
|
+
}
|
|
480
|
+
// --waiting: only sessions blocked on the user. Exits non-zero when any are
|
|
481
|
+
// present so a supervising agent or hook can poll it as a gate.
|
|
482
|
+
const sessions = waitingOnly ? merged.filter(s => s.status === 'input_required') : merged;
|
|
483
|
+
if (asJson) {
|
|
484
|
+
process.stdout.write(JSON.stringify(sessions, null, 2) + '\n');
|
|
485
|
+
if (waitingOnly && sessions.length > 0)
|
|
486
|
+
process.exitCode = 1;
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
if (sessions.length === 0) {
|
|
490
|
+
console.log(chalk.gray(waitingOnly ? 'No sessions waiting on input.' : 'No active agent sessions.'));
|
|
491
|
+
if (!opts.local && !opts.hosts?.length && remoteDeviceCount === 0)
|
|
492
|
+
printCrossMachineTip();
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
const grouped = groupSessionsByMachine(sessions, self);
|
|
496
|
+
let firstMachine = true;
|
|
497
|
+
for (const mg of grouped.machines) {
|
|
498
|
+
if (!firstMachine)
|
|
499
|
+
console.log();
|
|
500
|
+
firstMachine = false;
|
|
501
|
+
printMachineHeader(mg);
|
|
502
|
+
renderWorkspaceLayout(mg.layout, ' ');
|
|
363
503
|
}
|
|
364
504
|
const runningCount = sessions.filter(s => s.status === 'running').length;
|
|
365
505
|
const idleCount = sessions.filter(s => s.status === 'idle').length;
|
|
@@ -371,14 +511,25 @@ async function renderActiveSessions(asJson, waitingOnly = false) {
|
|
|
371
511
|
parts.push(`${idleCount} idle`);
|
|
372
512
|
if (queuedCount > 0)
|
|
373
513
|
parts.push(`${queuedCount} queued`);
|
|
374
|
-
|
|
514
|
+
const machineWord = grouped.machines.length === 1 ? 'machine' : 'machines';
|
|
515
|
+
console.log(chalk.gray(`\n${sessions.length} active (${parts.join(', ')}) across ${grouped.machines.length} ${machineWord}.`));
|
|
516
|
+
// Tip only when nothing else could be included and the user didn't opt out.
|
|
517
|
+
if (!opts.local && !opts.hosts?.length && remoteDeviceCount === 0)
|
|
518
|
+
printCrossMachineTip();
|
|
375
519
|
// Scriptable gate: a non-zero exit when anything is waiting on the user.
|
|
376
520
|
if (waitingOnly && sessions.length > 0)
|
|
377
521
|
process.exitCode = 1;
|
|
378
522
|
}
|
|
523
|
+
/** Nudge shown when `--active` has no other machines to fold in. */
|
|
524
|
+
function printCrossMachineTip() {
|
|
525
|
+
console.log(chalk.gray("\nTip: include sessions from your other machines — register them with 'ag devices sync', then rerun. Use --local to skip."));
|
|
526
|
+
}
|
|
379
527
|
/** Main action handler for `agents sessions`. Routes to picker, table, or single-session render. */
|
|
380
528
|
async function sessionsAction(query, options) {
|
|
381
|
-
|
|
529
|
+
// --host WITHOUT --active keeps the legacy per-host stream (each remote's raw
|
|
530
|
+
// stdout under a `── host ──` banner). With --active, the hosts are folded
|
|
531
|
+
// into the merged machine-grouped view instead (handled below).
|
|
532
|
+
if (options.host && options.host.length > 0 && !options.active) {
|
|
382
533
|
try {
|
|
383
534
|
runRemoteSessions(options.host);
|
|
384
535
|
}
|
|
@@ -389,7 +540,13 @@ async function sessionsAction(query, options) {
|
|
|
389
540
|
return;
|
|
390
541
|
}
|
|
391
542
|
if (options.active) {
|
|
392
|
-
|
|
543
|
+
// AGENTS_SESSIONS_LOCAL is set by a parent fan-out invocation (see
|
|
544
|
+
// remote-active.ts) so a peer answers for itself without recursing.
|
|
545
|
+
const forceLocal = options.local === true || process.env.AGENTS_SESSIONS_LOCAL === '1';
|
|
546
|
+
await renderActiveSessions(options.json === true, options.waiting === true, {
|
|
547
|
+
local: forceLocal,
|
|
548
|
+
hosts: options.host,
|
|
549
|
+
});
|
|
393
550
|
return;
|
|
394
551
|
}
|
|
395
552
|
if (options.cloud) {
|
|
@@ -533,7 +690,8 @@ async function sessionsAction(query, options) {
|
|
|
533
690
|
}
|
|
534
691
|
// Non-interactive fallback (piped output)
|
|
535
692
|
const filtered = searchQuery ? filterSessionsByQuery(sessions, searchQuery) : sessions;
|
|
536
|
-
|
|
693
|
+
const liveIndex = await maybeLiveIndex(options);
|
|
694
|
+
printSessionTable(filtered, hiddenCount, options.tree === true, liveIndex);
|
|
537
695
|
}
|
|
538
696
|
catch (err) {
|
|
539
697
|
tracker.stop();
|
|
@@ -560,45 +718,82 @@ function metaSignals(s) {
|
|
|
560
718
|
ticket: s.ticketId ? { id: s.ticketId } : undefined,
|
|
561
719
|
};
|
|
562
720
|
}
|
|
563
|
-
/** One flat table row:
|
|
564
|
-
|
|
721
|
+
/** One flat table row:
|
|
722
|
+
* shortId · agent · version · project · [glyph] label·doing · [ticket] · [wt] · time
|
|
723
|
+
* `doing` is the live preview when running, else the topic. The `ticket` column
|
|
724
|
+
* (tracker/PR ref, pulled out of the badge blob so refs align) is only rendered
|
|
725
|
+
* when `showTicket` — otherwise a listing with no refs would waste a column of
|
|
726
|
+
* dashes and needlessly truncate the topic. Worktree stays a trailing badge. */
|
|
727
|
+
function flatSessionRow(session, live, showTicket = false) {
|
|
565
728
|
const agentColor = colorAgent(session.agent);
|
|
566
729
|
const when = formatRelativeTime(session.timestamp);
|
|
567
730
|
const project = session.project || '-';
|
|
568
731
|
const tag = teamTag(session);
|
|
569
732
|
const label = session.label;
|
|
570
|
-
const
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
const
|
|
574
|
-
const
|
|
733
|
+
const { glyph, preview } = liveGlyphAndPreview(live);
|
|
734
|
+
// A running session's live preview says what the agent is doing now; a
|
|
735
|
+
// resting one falls back to its opening topic.
|
|
736
|
+
const doing = preview || (tag ? `${tag}${session.topic ?? ''}` : session.topic);
|
|
737
|
+
const wt = session.worktreeSlug ? chalk.magenta(`wt:${session.worktreeSlug}`) : '';
|
|
738
|
+
const TICKET_W = 10;
|
|
739
|
+
const ticketCell = showTicket
|
|
740
|
+
? chalk.blue(padToWidth(truncateToWidth(ticketLabel(session) || '-', TICKET_W), TICKET_W + 1))
|
|
741
|
+
: '';
|
|
742
|
+
const glyphW = glyph ? 2 : 0;
|
|
743
|
+
const ticketW = showTicket ? TICKET_W + 1 : 0;
|
|
744
|
+
const wtW = wt ? stringWidth(wt) + 1 : 0;
|
|
745
|
+
const topicW = Math.max(16, terminalWidth() - (10 + 9 + 8 + 16) - glyphW - ticketW - wtW - stringWidth(when) - 1);
|
|
575
746
|
return (chalk.white(padToWidth(truncateToWidth(session.shortId, 9), 10)) +
|
|
576
747
|
agentColor(padToWidth(truncateToWidth(session.agent, 8), 9)) +
|
|
577
|
-
chalk.yellow(padToWidth(truncateToWidth(
|
|
748
|
+
chalk.yellow(padToWidth(truncateToWidth(session.version || '-', 7), 8)) +
|
|
578
749
|
chalk.cyan(padToWidth(truncateToWidth(project, 14), 16)) +
|
|
579
|
-
|
|
580
|
-
(
|
|
750
|
+
(glyph ? glyph + ' ' : '') +
|
|
751
|
+
renderTopicCell(label, doing, '', topicW, topicW) +
|
|
752
|
+
ticketCell +
|
|
753
|
+
(wt ? wt + ' ' : '') +
|
|
581
754
|
chalk.gray(when));
|
|
582
755
|
}
|
|
583
756
|
/** One tree-mode row (grouped under a dir header): id · agent · badges · topic · time. No version/project column. */
|
|
584
|
-
function treeSessionRow(session) {
|
|
757
|
+
function treeSessionRow(session, live) {
|
|
585
758
|
const agentColor = colorAgent(session.agent);
|
|
586
759
|
const when = formatRelativeTime(session.timestamp);
|
|
587
760
|
const tag = teamTag(session);
|
|
588
761
|
const label = session.label;
|
|
589
|
-
const
|
|
762
|
+
const { glyph, preview } = liveGlyphAndPreview(live);
|
|
763
|
+
const topic = (preview || (tag ? `${tag}${session.topic ?? ''}` : session.topic)) || '-';
|
|
590
764
|
const badges = signalBadges(metaSignals(session));
|
|
591
765
|
const badgeW = badges ? stringWidth(badges) + 1 : 0;
|
|
592
766
|
const head = label ? `${label} · ${topic}` : topic;
|
|
593
|
-
const
|
|
767
|
+
const glyphW = glyph ? 2 : 0;
|
|
768
|
+
const topicW = Math.max(12, terminalWidth() - (2 + 9 + 8) - glyphW - badgeW - stringWidth(when) - 1);
|
|
594
769
|
return (' ' +
|
|
595
770
|
chalk.dim(padToWidth(session.shortId, 9)) +
|
|
596
771
|
agentColor(padToWidth(truncateToWidth(session.agent, 7), 8)) +
|
|
597
772
|
(badges ? badges + ' ' : '') +
|
|
773
|
+
(glyph ? glyph + ' ' : '') +
|
|
598
774
|
padToWidth(chalk.white(truncateToWidth(head, topicW)), topicW) +
|
|
599
775
|
' ' + chalk.gray(when));
|
|
600
776
|
}
|
|
601
|
-
|
|
777
|
+
/**
|
|
778
|
+
* Live-session index for enriching the default listing, or undefined when
|
|
779
|
+
* enrichment is off (`--no-live`) or irrelevant (`--json`, which serializes
|
|
780
|
+
* SessionMeta). Full detection (incl. the headless `ps` scan) is deliberate:
|
|
781
|
+
* bare-CLI and tmux agents are the common case here, and skipping them would
|
|
782
|
+
* leave the glyph almost never showing. The listing is a one-shot user action,
|
|
783
|
+
* not a hot loop, so the `ps`/`lsof` cost is acceptable; `--no-live` is the
|
|
784
|
+
* escape hatch. Never throws — a probe failure just yields a plain listing.
|
|
785
|
+
*/
|
|
786
|
+
async function maybeLiveIndex(options) {
|
|
787
|
+
if (options.live === false || options.json)
|
|
788
|
+
return undefined;
|
|
789
|
+
try {
|
|
790
|
+
return indexActiveBySessionId(await getActiveSessions());
|
|
791
|
+
}
|
|
792
|
+
catch {
|
|
793
|
+
return undefined;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
function printSessionTable(sessions, hiddenCount = 0, tree = false, liveIndex) {
|
|
602
797
|
if (tree) {
|
|
603
798
|
// Group by directory; drop the id/version columns from view. The short id
|
|
604
799
|
// stays as each row's leading handle (the address to read/resume it).
|
|
@@ -619,7 +814,7 @@ function printSessionTable(sessions, hiddenCount = 0, tree = false) {
|
|
|
619
814
|
const group = byDir.get(key);
|
|
620
815
|
console.log(`${chalk.cyan.bold(shortCwd(key))} ${chalk.gray(`(${group.length})`)}`);
|
|
621
816
|
for (const s of group)
|
|
622
|
-
console.log(treeSessionRow(s));
|
|
817
|
+
console.log(treeSessionRow(s, liveIndex?.get(s.id)));
|
|
623
818
|
}
|
|
624
819
|
const dirWord = keys.length === 1 ? 'directory' : 'directories';
|
|
625
820
|
console.log(chalk.gray(`\n${sessions.length} session${sessions.length === 1 ? '' : 's'} across ${keys.length} ${dirWord}.`));
|
|
@@ -627,8 +822,11 @@ function printSessionTable(sessions, hiddenCount = 0, tree = false) {
|
|
|
627
822
|
console.log(chalk.gray(formatTeamHiddenFooter(hiddenCount)));
|
|
628
823
|
return;
|
|
629
824
|
}
|
|
825
|
+
// 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.
|
|
827
|
+
const showTicket = sessions.some((s) => ticketLabel(s) !== '');
|
|
630
828
|
for (const session of sessions)
|
|
631
|
-
console.log(flatSessionRow(session));
|
|
829
|
+
console.log(flatSessionRow(session, liveIndex?.get(session.id), showTicket));
|
|
632
830
|
const countLine = `${sessions.length} session${sessions.length === 1 ? '' : 's'}.`;
|
|
633
831
|
console.log(chalk.gray(`\n${countLine}`));
|
|
634
832
|
if (hiddenCount > 0) {
|
|
@@ -677,6 +875,14 @@ function resolveViewMode(options, filters) {
|
|
|
677
875
|
return 'markdown';
|
|
678
876
|
return 'summary';
|
|
679
877
|
}
|
|
878
|
+
/**
|
|
879
|
+
* Render a session's full transcript to stdout — the non-follow view behind
|
|
880
|
+
* `agents logs <sessionId>`. Reuses the same markdown renderer as
|
|
881
|
+
* `agents sessions <id> --markdown`.
|
|
882
|
+
*/
|
|
883
|
+
export async function renderSessionLog(session) {
|
|
884
|
+
await renderSession(session, 'markdown', {});
|
|
885
|
+
}
|
|
680
886
|
async function renderSession(session, mode, filters, options = {}) {
|
|
681
887
|
// OpenCode stores sessions in SQLite; filePath is "db_path#session_id"
|
|
682
888
|
const realPath = session.filePath.split('#')[0];
|
|
@@ -777,7 +983,7 @@ function renderTopicCell(label, topic, query, visibleWidth, paddedWidth) {
|
|
|
777
983
|
}
|
|
778
984
|
return out + padding;
|
|
779
985
|
}
|
|
780
|
-
function formatPickerLabel(s, query) {
|
|
986
|
+
export function formatPickerLabel(s, query) {
|
|
781
987
|
const agentColor = colorAgent(s.agent);
|
|
782
988
|
const when = formatRelativeTime(s.timestamp);
|
|
783
989
|
const project = s.project || '-';
|
|
@@ -823,13 +1029,23 @@ async function handlePickedSession(picked) {
|
|
|
823
1029
|
await renderSession(picked.session, 'summary', {});
|
|
824
1030
|
return;
|
|
825
1031
|
}
|
|
826
|
-
|
|
827
|
-
|
|
1032
|
+
await resumeSessionInPlace(picked.session);
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* Resume a session in the current terminal — a foreground takeover of this
|
|
1036
|
+
* process. Used by the single-select picker and by `sessions resume` when the
|
|
1037
|
+
* chosen destination is "in place" (unknown emulator / off-macOS, single pick).
|
|
1038
|
+
* Falls back to the current version via `/continue` when the version-pinned
|
|
1039
|
+
* binary is missing (ENOENT).
|
|
1040
|
+
*/
|
|
1041
|
+
export async function resumeSessionInPlace(session) {
|
|
1042
|
+
const cwd = session.cwd && fs.existsSync(session.cwd)
|
|
1043
|
+
? session.cwd
|
|
828
1044
|
: process.cwd();
|
|
829
|
-
const resume = buildResumeCommand(
|
|
1045
|
+
const resume = buildResumeCommand(session);
|
|
830
1046
|
if (!resume) {
|
|
831
|
-
console.log(chalk.yellow(`Resume is not supported for ${
|
|
832
|
-
await renderSession(
|
|
1047
|
+
console.log(chalk.yellow(`Resume is not supported for ${session.agent} sessions yet. Showing summary instead.`));
|
|
1048
|
+
await renderSession(session, 'summary', {});
|
|
833
1049
|
return;
|
|
834
1050
|
}
|
|
835
1051
|
console.log(chalk.gray(`Resuming: ${resume.join(' ')} (cwd: ${cwd})`));
|
|
@@ -840,10 +1056,10 @@ async function handlePickedSession(picked) {
|
|
|
840
1056
|
shell: false,
|
|
841
1057
|
});
|
|
842
1058
|
child.on('error', (err) => {
|
|
843
|
-
if (err.code === 'ENOENT' &&
|
|
844
|
-
const fallback = buildFallbackCommand(
|
|
1059
|
+
if (err.code === 'ENOENT' && session.version) {
|
|
1060
|
+
const fallback = buildFallbackCommand(session);
|
|
845
1061
|
if (fallback) {
|
|
846
|
-
console.log(chalk.gray(`Version ${
|
|
1062
|
+
console.log(chalk.gray(`Version ${session.version} is not installed. Falling back to current version via /continue...`));
|
|
847
1063
|
const fb = spawn(fallback[0], fallback.slice(1), { cwd, stdio: 'inherit', shell: false });
|
|
848
1064
|
fb.on('error', (e) => { console.error(chalk.red(`Failed: ${e.message}`)); resolve(); });
|
|
849
1065
|
fb.on('close', () => resolve());
|
|
@@ -883,6 +1099,7 @@ export function buildResumeCommand(session) {
|
|
|
883
1099
|
case 'opencode':
|
|
884
1100
|
return ['opencode', '--session', session.id];
|
|
885
1101
|
case 'gemini':
|
|
1102
|
+
case 'antigravity':
|
|
886
1103
|
case 'openclaw':
|
|
887
1104
|
case 'rush':
|
|
888
1105
|
case 'hermes':
|
|
@@ -971,7 +1188,7 @@ async function runCloudSessions(query, options) {
|
|
|
971
1188
|
// Ensure the SessionMeta points at the local cache path for renderSession.
|
|
972
1189
|
await renderSession({ ...meta, filePath: cachedPath }, mode, filterOpts, options);
|
|
973
1190
|
}
|
|
974
|
-
function parseAgentFilter(agentName) {
|
|
1191
|
+
export function parseAgentFilter(agentName) {
|
|
975
1192
|
if (!agentName)
|
|
976
1193
|
return {};
|
|
977
1194
|
const [name, version] = agentName.split('@', 2);
|
|
@@ -1264,8 +1481,10 @@ export function registerSessionsCommands(program) {
|
|
|
1264
1481
|
.option('--artifacts', 'List all files written or edited during a session')
|
|
1265
1482
|
.option('--artifact <name>', 'Read a specific artifact by filename or path (outputs to stdout)')
|
|
1266
1483
|
.option('--active', 'Show only sessions running right now across terminals, teams, cloud, and headless agents')
|
|
1484
|
+
.option('--local', 'With --active: only this machine — skip the cross-machine SSH fan-out')
|
|
1267
1485
|
.option('--waiting', 'With --active: show only sessions waiting on your input (exits non-zero if any)')
|
|
1268
1486
|
.option('--tree', 'Group the listing by directory; drops the id/version columns for readability')
|
|
1487
|
+
.option('--no-live', 'Do not enrich the listing with live status/preview for running sessions')
|
|
1269
1488
|
.option('--cloud', 'Source sessions from Rush Cloud (captured runs) instead of local disk')
|
|
1270
1489
|
.option('-H, --host <target...>', 'Run this query on remote machine(s) over SSH (host alias or user@host; repeatable)');
|
|
1271
1490
|
setHelpSections(sessionsCmd, {
|
|
@@ -1308,6 +1527,7 @@ export function registerSessionsCommands(program) {
|
|
|
1308
1527
|
});
|
|
1309
1528
|
registerSessionsTailCommand(sessionsCmd);
|
|
1310
1529
|
registerSessionsSyncCommand(sessionsCmd);
|
|
1530
|
+
registerSessionsResumeCommand(sessionsCmd);
|
|
1311
1531
|
}
|
|
1312
1532
|
function formatNoSessionsMessage(showAll, project) {
|
|
1313
1533
|
const projectQuery = project?.trim();
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents status` — the unified sync-status surface.
|
|
3
|
+
*
|
|
4
|
+
* One command that answers "is my fleet in sync?" the same way every other
|
|
5
|
+
* surface does, because it reads the same engine (computeSyncStatus). Human mode
|
|
6
|
+
* renders the summary and, when a TTY finds drift, offers the interactive
|
|
7
|
+
* "sync now?" flow (promptDriftSync). `--json` emits the stable UnifiedSyncStatus
|
|
8
|
+
* contract the menu-bar and Agency consume. `--yes` reconciles everything with no
|
|
9
|
+
* prompts (the "kick it" path, safe in scripts).
|
|
10
|
+
*/
|
|
11
|
+
import { Command } from 'commander';
|
|
12
|
+
export declare function registerStatusCommand(program: Command): void;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents status` — the unified sync-status surface.
|
|
3
|
+
*
|
|
4
|
+
* One command that answers "is my fleet in sync?" the same way every other
|
|
5
|
+
* surface does, because it reads the same engine (computeSyncStatus). Human mode
|
|
6
|
+
* renders the summary and, when a TTY finds drift, offers the interactive
|
|
7
|
+
* "sync now?" flow (promptDriftSync). `--json` emits the stable UnifiedSyncStatus
|
|
8
|
+
* contract the menu-bar and Agency consume. `--yes` reconciles everything with no
|
|
9
|
+
* prompts (the "kick it" path, safe in scripts).
|
|
10
|
+
*/
|
|
11
|
+
import chalk from 'chalk';
|
|
12
|
+
import { AGENTS } from '../lib/agents.js';
|
|
13
|
+
import { setHelpSections } from '../lib/help.js';
|
|
14
|
+
import { computeSyncStatus } from '../lib/sync-status.js';
|
|
15
|
+
import { promptDriftSync } from '../lib/drift-sync.js';
|
|
16
|
+
const agentName = (id) => AGENTS[id]?.name ?? id;
|
|
17
|
+
function versionSummary(v) {
|
|
18
|
+
if (!v.everSynced)
|
|
19
|
+
return chalk.gray('never synced');
|
|
20
|
+
if (v.needsSync) {
|
|
21
|
+
const bits = [];
|
|
22
|
+
if (v.counts.drifted)
|
|
23
|
+
bits.push(`${v.counts.drifted} drifted`);
|
|
24
|
+
if (v.counts.missing)
|
|
25
|
+
bits.push(`${v.counts.missing} missing`);
|
|
26
|
+
return chalk.yellow(bits.join(' · '));
|
|
27
|
+
}
|
|
28
|
+
return chalk.green('in sync');
|
|
29
|
+
}
|
|
30
|
+
export function registerStatusCommand(program) {
|
|
31
|
+
const cmd = program
|
|
32
|
+
.command('status')
|
|
33
|
+
.description('Unified sync status across the fleet — what is drifted, missing, or behind, with an option to sync it.')
|
|
34
|
+
.option('--json', 'Output the machine-readable UnifiedSyncStatus contract')
|
|
35
|
+
.option('--yes', 'Reconcile everything detected (pull .system if behind + sync drifted/missing resources) without prompting')
|
|
36
|
+
.option('--cwd <path>', 'Resolution cwd for project layer detection (default: process.cwd())');
|
|
37
|
+
setHelpSections(cmd, {
|
|
38
|
+
examples: `
|
|
39
|
+
# Show what's out of sync; offer to fix it (interactive)
|
|
40
|
+
agents status
|
|
41
|
+
|
|
42
|
+
# Machine-readable status for the menu-bar / Agency
|
|
43
|
+
agents status --json
|
|
44
|
+
|
|
45
|
+
# Reconcile everything with no prompts (CI / scripts / the "kick it" path)
|
|
46
|
+
agents status --yes
|
|
47
|
+
`,
|
|
48
|
+
});
|
|
49
|
+
cmd.action(async (opts) => {
|
|
50
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
51
|
+
if (opts.json) {
|
|
52
|
+
const status = await computeSyncStatus({ cwd });
|
|
53
|
+
console.log(JSON.stringify(status, null, 2));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const status = await computeSyncStatus({ cwd });
|
|
57
|
+
// Human summary header (always) — the per-version readout.
|
|
58
|
+
console.log(chalk.bold('Fleet sync status'));
|
|
59
|
+
if (status.system.unknown) {
|
|
60
|
+
console.log(` ${'.system repo'.padEnd(28)} ${chalk.gray('freshness unknown (no upstream)')}`);
|
|
61
|
+
}
|
|
62
|
+
else if (status.system.behind > 0) {
|
|
63
|
+
console.log(` ${'.system repo'.padEnd(28)} ${chalk.yellow(`${status.system.behind} behind`)}`);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
console.log(` ${'.system repo'.padEnd(28)} ${chalk.green('up to date')}`);
|
|
67
|
+
}
|
|
68
|
+
if (status.agents.length === 0) {
|
|
69
|
+
console.log(chalk.gray(' (no installed agent versions)'));
|
|
70
|
+
}
|
|
71
|
+
for (const v of status.agents) {
|
|
72
|
+
const label = `${agentName(v.agent)}@${v.version}${v.isDefault ? chalk.gray(' (default)') : ''}`;
|
|
73
|
+
console.log(` ${label.padEnd(28)} ${versionSummary(v)}`);
|
|
74
|
+
}
|
|
75
|
+
if (status.totals.orphan > 0) {
|
|
76
|
+
console.log(chalk.gray(` (${status.totals.orphan} orphan${status.totals.orphan === 1 ? '' : 's'} — run \`agents prune cleanup\`)`));
|
|
77
|
+
}
|
|
78
|
+
// Hand off to the shared interactive/apply flow (summary already printed above).
|
|
79
|
+
await promptDriftSync({ cwd, yes: opts.yes, status, quiet: true });
|
|
80
|
+
});
|
|
81
|
+
}
|