@phnx-labs/agents-cli 1.20.86 → 1.20.87
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 +112 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/sessions-browser.d.ts +18 -0
- package/dist/commands/sessions-browser.js +126 -24
- package/dist/commands/sessions-picker.d.ts +21 -8
- package/dist/commands/sessions-picker.js +83 -7
- package/dist/commands/sessions.d.ts +19 -0
- package/dist/commands/sessions.js +147 -18
- package/dist/commands/ssh.js +59 -1
- package/dist/commands/teams-picker.d.ts +2 -0
- package/dist/commands/teams-picker.js +2 -1
- package/dist/commands/teams.d.ts +4 -1
- package/dist/commands/teams.js +106 -70
- package/dist/commands/view.js +14 -3
- package/dist/index.js +31 -1
- package/dist/lib/claude-account-token.d.ts +12 -0
- package/dist/lib/claude-account-token.js +63 -0
- package/dist/lib/devices/registry.d.ts +25 -0
- package/dist/lib/devices/registry.js +82 -1
- package/dist/lib/events.d.ts +8 -1
- package/dist/lib/events.js +13 -0
- package/dist/lib/exec.js +10 -1
- package/dist/lib/format.d.ts +7 -0
- package/dist/lib/format.js +11 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -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/session/db.d.ts +21 -0
- package/dist/lib/session/db.js +45 -4
- package/dist/lib/session/remote-list.d.ts +7 -0
- package/dist/lib/session/remote-list.js +8 -4
- package/dist/lib/session/state.js +69 -2
- package/dist/lib/session/team-filter.d.ts +22 -3
- package/dist/lib/session/team-filter.js +106 -17
- package/dist/lib/session/types.d.ts +8 -0
- package/dist/lib/signin-badge.d.ts +17 -0
- package/dist/lib/signin-badge.js +19 -0
- package/dist/lib/state.d.ts +2 -0
- package/dist/lib/state.js +2 -0
- package/dist/lib/usage.js +1 -60
- package/package.json +1 -1
|
@@ -28,7 +28,7 @@ import { stringWidth, truncateToWidth, padToWidth, terminalWidth } from '../lib/
|
|
|
28
28
|
import { inferSessionState } from '../lib/session/state.js';
|
|
29
29
|
import { discoverSessions, countSessionsInScope, resolveSessionById, isCompleteSessionId, looksLikeSessionId, searchContentIndex, getSessionRoots } from '../lib/session/discover.js';
|
|
30
30
|
import { findSessionsById } from '../lib/session/db.js';
|
|
31
|
-
import { filterTeamSessions } from '../lib/session/team-filter.js';
|
|
31
|
+
import { filterTeamSessions, safeTeamText } from '../lib/session/team-filter.js';
|
|
32
32
|
import { parseSession } from '../lib/session/parse.js';
|
|
33
33
|
import { runRemoteSessions, buildForwardedArgs, ensureWholeIndex } from '../lib/session/remote.js';
|
|
34
34
|
import { formatRelativeTime } from '../lib/session/relative-time.js';
|
|
@@ -113,7 +113,15 @@ function createScanProgressTracker(verbs, suffix, spinner) {
|
|
|
113
113
|
};
|
|
114
114
|
}
|
|
115
115
|
const PICKER_RECENT_COUNT = 15;
|
|
116
|
-
|
|
116
|
+
/**
|
|
117
|
+
* The `--limit` default, shared with its `.option()` registration. Commander fills
|
|
118
|
+
* the default in, so `options.limit` is never falsy — code that wants to know
|
|
119
|
+
* whether the USER set a limit has to compare against this rather than test
|
|
120
|
+
* truthiness.
|
|
121
|
+
*/
|
|
122
|
+
const DEFAULT_LIMIT = '50';
|
|
123
|
+
/** Pool size for `--in-team`: one team's rows can sit anywhere in the history. */
|
|
124
|
+
const WHOLE_TEAM_POOL_LIMIT = 5000;
|
|
117
125
|
// The grouped default view ("overview"): fetch a generous recency-ordered pool
|
|
118
126
|
// for accurate per-project totals, show each project's most-recent rows grouped
|
|
119
127
|
// by project, newest-active project first.
|
|
@@ -1016,7 +1024,12 @@ function useInteractiveBrowser(options) {
|
|
|
1016
1024
|
* browser (preview-rich, selectable) instead of the legacy per-host raw stream.
|
|
1017
1025
|
*/
|
|
1018
1026
|
export function isBareBrowserListing(options, query) {
|
|
1019
|
-
return useInteractiveBrowser(options) &&
|
|
1027
|
+
return (useInteractiveBrowser(options) &&
|
|
1028
|
+
// A peer answering a fan-out must never open a TUI. It has no TTY either, so
|
|
1029
|
+
// this is the explicit half of a guard that otherwise rests on the implicit
|
|
1030
|
+
// invariant that peers are always dialed with --json (see remote-list.ts).
|
|
1031
|
+
process.env.AGENTS_SESSIONS_LOCAL !== '1' &&
|
|
1032
|
+
hasNoBrowserDisqualifyingFlags(options, query));
|
|
1020
1033
|
}
|
|
1021
1034
|
/**
|
|
1022
1035
|
* Pure flag-gate half of {@link isBareBrowserListing} (TTY-independent, so it is
|
|
@@ -1033,7 +1046,15 @@ export function hasNoBrowserDisqualifyingFlags(options, query) {
|
|
|
1033
1046
|
!options.project &&
|
|
1034
1047
|
!options.sort &&
|
|
1035
1048
|
!options.artifacts &&
|
|
1036
|
-
options.artifact === undefined
|
|
1049
|
+
options.artifact === undefined &&
|
|
1050
|
+
// --cloud lists a provider's tasks, not the transcript index, and
|
|
1051
|
+
// runCloudSessions has no host scope — letting a `--device X --cloud` fall
|
|
1052
|
+
// through to the browser gate would silently drop the X the user asked for.
|
|
1053
|
+
!options.cloud &&
|
|
1054
|
+
// The browser carries ONE device in its filter and `y` copies back exactly
|
|
1055
|
+
// one --device, so a multi-host scope can't round-trip. Those stay on the
|
|
1056
|
+
// legacy per-host stream, which prints each peer under its own banner.
|
|
1057
|
+
(options.host?.length ?? 0) <= 1);
|
|
1037
1058
|
}
|
|
1038
1059
|
/** The canonical `ag sessions …` command for a set of flags — the twin of the
|
|
1039
1060
|
* browser's `y` hotkey (see --print-cmd). Normalizes to the stable flag form. */
|
|
@@ -1043,6 +1064,8 @@ function canonicalSessionsCommand(query, options) {
|
|
|
1043
1064
|
a.push('--active');
|
|
1044
1065
|
if (options.teams)
|
|
1045
1066
|
a.push('--teams');
|
|
1067
|
+
if (options.inTeam)
|
|
1068
|
+
a.push('--in-team', options.inTeam);
|
|
1046
1069
|
if (options.routine)
|
|
1047
1070
|
a.push('--routine');
|
|
1048
1071
|
if (options.agent)
|
|
@@ -1100,7 +1123,13 @@ async function renderSessionPreview(query, scope) {
|
|
|
1100
1123
|
console.log(buildPreview(session));
|
|
1101
1124
|
}
|
|
1102
1125
|
/** Main action handler for `agents sessions`. Routes to picker, table, or single-session render. */
|
|
1103
|
-
async function sessionsAction(query, options
|
|
1126
|
+
async function sessionsAction(query, options,
|
|
1127
|
+
/**
|
|
1128
|
+
* Where commander got `--limit` from: 'cli'/'env' when the user supplied it,
|
|
1129
|
+
* 'default' when it filled in its own. Truthiness can't tell those apart,
|
|
1130
|
+
* because the default arrives as a string like any typed value.
|
|
1131
|
+
*/
|
|
1132
|
+
limitSource) {
|
|
1104
1133
|
// Explicit --query is interchangeable with the positional; it's how you search
|
|
1105
1134
|
// for text that collides with a subcommand name (e.g. `sessions --query go`).
|
|
1106
1135
|
query = query ?? options.query;
|
|
@@ -1133,6 +1162,14 @@ async function sessionsAction(query, options) {
|
|
|
1133
1162
|
// `── host ──` banner). With --active, the hosts are folded into the merged
|
|
1134
1163
|
// machine-grouped view instead (handled below).
|
|
1135
1164
|
if (options.host && options.host.length > 0 && !options.active) {
|
|
1165
|
+
// --local means "skip the SSH fan-out"; --host means "look only over there".
|
|
1166
|
+
// Together they ask for a peer's sessions without dialing the peer, which can
|
|
1167
|
+
// only ever be empty — so say that instead of rendering a blank list.
|
|
1168
|
+
if (options.local === true && !shouldIncludeLocal(options.host, machineId())) {
|
|
1169
|
+
console.error(chalk.red('--local and --device name opposite scopes: --local skips the SSH fan-out that --device needs.'));
|
|
1170
|
+
console.error(chalk.gray('Drop one — `--device <box>` to read that machine, `--local` to stay on this one.'));
|
|
1171
|
+
process.exit(1);
|
|
1172
|
+
}
|
|
1136
1173
|
if (options.json) {
|
|
1137
1174
|
await runRemoteSessionsJson(options.host);
|
|
1138
1175
|
return;
|
|
@@ -1165,6 +1202,14 @@ async function sessionsAction(query, options) {
|
|
|
1165
1202
|
return;
|
|
1166
1203
|
}
|
|
1167
1204
|
if (options.active) {
|
|
1205
|
+
// The running view is built from the live scan, which carries no team lineage
|
|
1206
|
+
// (that comes off the transcript index and the teams meta dir), so --in-team
|
|
1207
|
+
// has nothing to match on here. Say so rather than ignoring the flag.
|
|
1208
|
+
if (options.inTeam) {
|
|
1209
|
+
console.error(chalk.red('--in-team does not apply to --active: the running view carries no team lineage.'));
|
|
1210
|
+
console.error(chalk.gray('Drop --active to filter by team, or use `agents teams status <name>` for a live team.'));
|
|
1211
|
+
process.exit(1);
|
|
1212
|
+
}
|
|
1168
1213
|
// On a TTY (and not a scripting path), open the interactive browser seeded to
|
|
1169
1214
|
// running-only. --json / --waiting / --no-interactive / a peer fan-out keep the
|
|
1170
1215
|
// static dump untouched, so scripts and agents are unaffected. An explicit
|
|
@@ -1212,6 +1257,8 @@ async function sessionsAction(query, options) {
|
|
|
1212
1257
|
agent: options.agent,
|
|
1213
1258
|
all: options.all,
|
|
1214
1259
|
since: options.since,
|
|
1260
|
+
host: options.host,
|
|
1261
|
+
inTeam: options.inTeam,
|
|
1215
1262
|
}), { local: options.local === true, hosts: options.host });
|
|
1216
1263
|
return;
|
|
1217
1264
|
}
|
|
@@ -1265,14 +1312,25 @@ async function sessionsAction(query, options) {
|
|
|
1265
1312
|
// no query, no path drill-in, not explicitly --flat/--tree. It drops the silent
|
|
1266
1313
|
// cwd-scope + 50-cap + 30-day window that hide most of a large index.
|
|
1267
1314
|
const wantsOverview = isInteractive && !searchQuery && !pathFilter && !options.flat && !options.tree;
|
|
1315
|
+
// --in-team asks for ONE team's whole lineage, which is a handful of rows that
|
|
1316
|
+
// can sit anywhere in history. Bounding it by the default top-50 / 30-day window
|
|
1317
|
+
// silently returns nothing for any team older than that — so the flag widens its
|
|
1318
|
+
// own scope, the way --all does, unless the caller set an explicit --limit.
|
|
1319
|
+
const wantsWholeTeam = !!options.inTeam;
|
|
1320
|
+
// `--limit` has a commander default, so an untouched flag still arrives as a
|
|
1321
|
+
// string and truthiness can't tell it from a typed one. Commander records where
|
|
1322
|
+
// each value came from, which is the only signal that distinguishes an explicit
|
|
1323
|
+
// `--limit 50` from no flag at all — a script asking for 50 must get 50, not the
|
|
1324
|
+
// whole-team pool.
|
|
1325
|
+
const userSetLimit = limitSource === 'cli' || limitSource === 'env';
|
|
1268
1326
|
const limit = wantsOverview
|
|
1269
1327
|
? OVERVIEW_POOL_LIMIT
|
|
1270
|
-
: parseInt(options.limit
|
|
1328
|
+
: parseInt(userSetLimit ? options.limit : wantsWholeTeam ? String(WHOLE_TEAM_POOL_LIMIT) : DEFAULT_LIMIT, 10);
|
|
1271
1329
|
// Overview: recency order across the whole index, no default window; an explicit
|
|
1272
1330
|
// --since still narrows. Non-overview keeps the prior interactive-30d default.
|
|
1273
1331
|
const since = wantsOverview
|
|
1274
1332
|
? options.since
|
|
1275
|
-
: (options.since ?? (isInteractive && !options.all ? '30d' : undefined));
|
|
1333
|
+
: (options.since ?? (isInteractive && !options.all && !wantsWholeTeam ? '30d' : undefined));
|
|
1276
1334
|
const spinner = options.json ? null : ora().start();
|
|
1277
1335
|
const tracker = createScanProgressTracker(LOAD_VERBS, 'sessions', spinner);
|
|
1278
1336
|
try {
|
|
@@ -1285,12 +1343,15 @@ async function sessionsAction(query, options) {
|
|
|
1285
1343
|
const scope = {
|
|
1286
1344
|
agent,
|
|
1287
1345
|
version,
|
|
1288
|
-
|
|
1346
|
+
// --in-team spans directories by construction: a team's teammates run in
|
|
1347
|
+
// their own worktrees, so scoping to the current cwd hides most of the
|
|
1348
|
+
// lineage the flag exists to show.
|
|
1349
|
+
all: pathFilter ? undefined : options.all || wantsWholeTeam,
|
|
1289
1350
|
cwd: process.cwd(),
|
|
1290
1351
|
// Default overview scopes to the current repo SUBTREE (prefix match), so a
|
|
1291
1352
|
// monorepo shows its sub-projects grouped instead of collapsing to the one
|
|
1292
1353
|
// exact-cwd project. `--all` clears the prefix and spans the whole index.
|
|
1293
|
-
cwdPrefix: pathFilter ?? (wantsOverview && !options.all ? process.cwd() : undefined),
|
|
1354
|
+
cwdPrefix: pathFilter ?? (wantsOverview && !options.all && !wantsWholeTeam ? process.cwd() : undefined),
|
|
1294
1355
|
project: options.project,
|
|
1295
1356
|
since,
|
|
1296
1357
|
until: options.until,
|
|
@@ -1316,7 +1377,15 @@ async function sessionsAction(query, options) {
|
|
|
1316
1377
|
// enriched/hidden.
|
|
1317
1378
|
const { visible: visibleSessions } = filterTeamSessions(sessions, !!options.teams);
|
|
1318
1379
|
sessions = visibleSessions;
|
|
1319
|
-
|
|
1380
|
+
// --in-team spans both ends of the lineage, so it can't be one SQL predicate:
|
|
1381
|
+
// the orchestrator matches on the scan-derived `spawnedTeam` column, while a
|
|
1382
|
+
// teammate only knows its team from the meta.json filterTeamSessions just
|
|
1383
|
+
// read. Match either, after that pass has populated `teamOrigin`.
|
|
1384
|
+
if (options.inTeam)
|
|
1385
|
+
sessions = sessions.filter((s) => matchesTeam(s, options.inTeam));
|
|
1386
|
+
// Under --in-team the visible list is one team, so the whole-index team-origin
|
|
1387
|
+
// count would be a non-sequitur next to it.
|
|
1388
|
+
const hiddenCount = options.teams || options.inTeam
|
|
1320
1389
|
? 0
|
|
1321
1390
|
: countSessionsInScope({ ...scope, onlyTeamOrigin: true });
|
|
1322
1391
|
// Smart ID routing: a bare query that resolves to one session renders
|
|
@@ -1437,12 +1506,55 @@ async function sessionsAction(query, options) {
|
|
|
1437
1506
|
process.exit(1);
|
|
1438
1507
|
}
|
|
1439
1508
|
}
|
|
1509
|
+
/**
|
|
1510
|
+
* Prefix marking a row as somebody's teammate: `[<team>/<handle>] `, falling back
|
|
1511
|
+
* to the handle alone when the record predates team-name capture. The mode is
|
|
1512
|
+
* deliberately dropped here (it survives in the preview pane) — this tag is
|
|
1513
|
+
* folded into the topic cell, whose floor is 16 columns, so every character it
|
|
1514
|
+
* takes is one the actual prompt loses.
|
|
1515
|
+
*/
|
|
1440
1516
|
function teamTag(session) {
|
|
1441
1517
|
const origin = session.teamOrigin;
|
|
1442
1518
|
if (!origin)
|
|
1443
1519
|
return '';
|
|
1444
|
-
const
|
|
1445
|
-
|
|
1520
|
+
const handle = safeTeamText(origin.handle);
|
|
1521
|
+
const team = safeTeamText(origin.team);
|
|
1522
|
+
if (team)
|
|
1523
|
+
return `[${team}${handle ? `/${handle}` : ''}] `;
|
|
1524
|
+
return handle ? `[${handle}] ` : '[team] ';
|
|
1525
|
+
}
|
|
1526
|
+
/**
|
|
1527
|
+
* Whether a session belongs to `team`, from either end: it spawned the team, or
|
|
1528
|
+
* it is one of the team's teammates. Case-insensitive, matching the SQL
|
|
1529
|
+
* predicate behind `querySessions({ spawnedTeam })`.
|
|
1530
|
+
*/
|
|
1531
|
+
export function matchesTeam(session, team) {
|
|
1532
|
+
// The needle is peer-derived in the browser: `f.team` comes off the team cycle,
|
|
1533
|
+
// which is built from rows another machine sent. Guard it the same way as the
|
|
1534
|
+
// fields it is compared against, so a non-string can't throw out of a filter
|
|
1535
|
+
// that runs over every row.
|
|
1536
|
+
const want = safeTeamText(team)?.trim().toLowerCase();
|
|
1537
|
+
if (!want)
|
|
1538
|
+
return true;
|
|
1539
|
+
return (safeTeamText(session.spawnedTeam)?.toLowerCase() === want ||
|
|
1540
|
+
safeTeamText(session.teamOrigin?.team)?.toLowerCase() === want);
|
|
1541
|
+
}
|
|
1542
|
+
/** Longest team name rendered in the `team:` row badge before truncation. */
|
|
1543
|
+
const TEAM_BADGE_MAX = 10;
|
|
1544
|
+
/**
|
|
1545
|
+
* The `team:<name>` badge for a session that SPAWNED a team — the orchestrator
|
|
1546
|
+
* end of the lineage, from the scan-derived `spawnedTeam`. Returned as a plain
|
|
1547
|
+
* (uncolored) string plus its display width so callers can reserve the width
|
|
1548
|
+
* from the topic budget and color it as their own segment: folding it into the
|
|
1549
|
+
* topic string would lose the color, since renderTopicCell strips ANSI and
|
|
1550
|
+
* re-whitens every slice.
|
|
1551
|
+
*/
|
|
1552
|
+
export function teamBadge(session) {
|
|
1553
|
+
const team = safeTeamText(session.spawnedTeam);
|
|
1554
|
+
if (!team)
|
|
1555
|
+
return { plain: '', width: 0 };
|
|
1556
|
+
const plain = `team:${truncate(team, TEAM_BADGE_MAX)} `;
|
|
1557
|
+
return { plain, width: stringWidth(plain) };
|
|
1446
1558
|
}
|
|
1447
1559
|
function originTag(session) {
|
|
1448
1560
|
if (session.origin !== 'routine')
|
|
@@ -1477,6 +1589,8 @@ export function flatSessionRow(session, live, showTicket = false, cols = {}) {
|
|
|
1477
1589
|
const topicBase = tag ? `${tag}${session.topic ?? ''}` : session.topic;
|
|
1478
1590
|
const doing = [restingTodo, preview || topicBase].filter(Boolean).join(' · ') || undefined;
|
|
1479
1591
|
const wt = session.worktreeSlug ? chalk.magenta(`wt:${session.worktreeSlug}`) : '';
|
|
1592
|
+
const team = teamBadge(session);
|
|
1593
|
+
const teamSeg = team.plain ? chalk.green(team.plain) : '';
|
|
1480
1594
|
// The machine column only earns its width when the listing spans more than one
|
|
1481
1595
|
// box (i.e. the cross-machine fan-out folded remotes in) — same rule and
|
|
1482
1596
|
// pool-derived width as the picker.
|
|
@@ -1497,7 +1611,7 @@ export function flatSessionRow(session, live, showTicket = false, cols = {}) {
|
|
|
1497
1611
|
const wtW = wt ? stringWidth(wt) + 1 : 0;
|
|
1498
1612
|
const width = terminalWidth();
|
|
1499
1613
|
const requestedModelW = cols.showModel ? (cols.modelWidth ?? PICKER_MODEL_MAX) : 0;
|
|
1500
|
-
const fixedW = (10 + 9 + 8 + 16) + glyphW + statusW + machineW + ticketW + wtW + stringWidth(when) + 1;
|
|
1614
|
+
const fixedW = (10 + 9 + 8 + 16) + glyphW + statusW + machineW + ticketW + wtW + team.width + stringWidth(when) + 1;
|
|
1501
1615
|
const modelSlack = width - fixedW - 16;
|
|
1502
1616
|
const modelW = requestedModelW <= modelSlack
|
|
1503
1617
|
? requestedModelW
|
|
@@ -1511,6 +1625,7 @@ export function flatSessionRow(session, live, showTicket = false, cols = {}) {
|
|
|
1511
1625
|
chalk.cyan(linkCwdCell(session, padToWidth(truncateToWidth(project, 14), 16))) +
|
|
1512
1626
|
(glyph ? glyph + ' ' : '') +
|
|
1513
1627
|
statusCell +
|
|
1628
|
+
teamSeg +
|
|
1514
1629
|
renderTopicCell(label, doing, '', topicW, topicW) +
|
|
1515
1630
|
ticketCell +
|
|
1516
1631
|
(wt ? wt + ' ' : '') +
|
|
@@ -1530,16 +1645,19 @@ function treeSessionRow(session, live) {
|
|
|
1530
1645
|
const topic = [restingTodo, topicBase].filter(Boolean).join(' · ') || '-';
|
|
1531
1646
|
const badges = signalBadges(metaSignals(session));
|
|
1532
1647
|
const badgeW = badges ? stringWidth(badges) + 1 : 0;
|
|
1648
|
+
const team = teamBadge(session);
|
|
1649
|
+
const teamSeg = team.plain ? chalk.green(team.plain) : '';
|
|
1533
1650
|
const head = label ? `${label} · ${topic}` : topic;
|
|
1534
1651
|
const { cell: statusCell, width: statusW } = liveStatusCell(live);
|
|
1535
1652
|
const glyphW = glyph ? 2 : 0;
|
|
1536
|
-
const topicW = Math.max(12, terminalWidth() - (2 + 9 + 8) - glyphW - statusW - badgeW - stringWidth(when) - 1);
|
|
1653
|
+
const topicW = Math.max(12, terminalWidth() - (2 + 9 + 8) - glyphW - statusW - badgeW - team.width - stringWidth(when) - 1);
|
|
1537
1654
|
return (' ' +
|
|
1538
1655
|
chalk.dim(padToWidth(session.shortId, 9)) +
|
|
1539
1656
|
agentColor(padToWidth(truncateToWidth(session.agent, 7), 8)) +
|
|
1540
1657
|
(badges ? badges + ' ' : '') +
|
|
1541
1658
|
(glyph ? glyph + ' ' : '') +
|
|
1542
1659
|
statusCell +
|
|
1660
|
+
teamSeg +
|
|
1543
1661
|
padToWidth(chalk.white(truncateToWidth(head, topicW)), topicW) +
|
|
1544
1662
|
' ' + chalk.gray(when));
|
|
1545
1663
|
}
|
|
@@ -1934,6 +2052,10 @@ export function formatPickerLabel(s, query, cols = {}, ssh, host = '') {
|
|
|
1934
2052
|
const sshPlain = ssh ? (ssh.device ? `ssh←${ssh.device} ` : 'ssh ') : '';
|
|
1935
2053
|
const sshSeg = sshPlain ? chalk.red(sshPlain) : '';
|
|
1936
2054
|
const sshW = sshPlain ? stringWidth(sshPlain) : 0;
|
|
2055
|
+
// Orchestrator badge — same own-segment treatment as `ssh` above, for the same
|
|
2056
|
+
// reason: renderTopicCell would strip its colour if it rode inside the topic.
|
|
2057
|
+
const team = teamBadge(s);
|
|
2058
|
+
const teamSeg = team.plain ? chalk.green(team.plain) : '';
|
|
1937
2059
|
const tag = originTag(s) || teamTag(s);
|
|
1938
2060
|
const label = s.label;
|
|
1939
2061
|
const topic = tag ? `${tag}${s.topic ?? ''}` : s.topic;
|
|
@@ -1960,7 +2082,7 @@ export function formatPickerLabel(s, query, cols = {}, ssh, host = '') {
|
|
|
1960
2082
|
const ticketW = cols.showTicket ? TICKET_W + 1 : 0;
|
|
1961
2083
|
const hostW = cols.showHost ? PICKER_HOST_W : 0;
|
|
1962
2084
|
const wtW = wt ? stringWidth(wt) + 1 : 0;
|
|
1963
|
-
const topicW = Math.max(16, terminalWidth() - gutter - (10 + 9 + 8 + 16) - machineColW - hostW - ticketW - wtW - sshW - stringWidth(when) - 1);
|
|
2085
|
+
const topicW = Math.max(16, terminalWidth() - gutter - (10 + 9 + 8 + 16) - machineColW - hostW - ticketW - wtW - sshW - team.width - stringWidth(when) - 1);
|
|
1964
2086
|
return (
|
|
1965
2087
|
// Truncated, not just padded: an indexed shortId is always 8 chars, but a
|
|
1966
2088
|
// live row with no session id is named by its pid or cloud task, which can
|
|
@@ -1972,6 +2094,7 @@ export function formatPickerLabel(s, query, cols = {}, ssh, host = '') {
|
|
|
1972
2094
|
hostCell +
|
|
1973
2095
|
chalk.cyan(padRight(truncate(project, 14), 16)) +
|
|
1974
2096
|
sshSeg +
|
|
2097
|
+
teamSeg +
|
|
1975
2098
|
renderTopicCell(label, topic, query, topicW, topicW) +
|
|
1976
2099
|
ticketCell +
|
|
1977
2100
|
(wt ? wt + ' ' : '') +
|
|
@@ -2816,11 +2939,12 @@ export function registerSessionsCommands(program) {
|
|
|
2816
2939
|
.option('--all', 'Widen every non-status filter to "all": every directory (not just this project) and all time (no window cap). Status filters like --active still compose; -a/--device/--since still narrow their axis.')
|
|
2817
2940
|
.option('--unmanaged', "Also show sessions from your own ~/.<agent> installs (hidden once agents-cli manages that agent)")
|
|
2818
2941
|
.option('--teams', 'Include team-spawned sessions (hidden by default)')
|
|
2942
|
+
.option('--in-team <name>', "Only this team: the session that spawned it plus (with --teams) its teammates. Spans every directory and all time, since a team's worktrees and history sit outside the default window.")
|
|
2819
2943
|
.option('--routine', 'Show only sessions archived from routine runs')
|
|
2820
2944
|
.option('-p, --project <name>', 'Filter by project name (searches across all directories)')
|
|
2821
2945
|
.option('--since <time>', 'Only sessions newer than this (e.g., 2h, 7d, 4w, or ISO date)')
|
|
2822
2946
|
.option('--until <time>', 'Only sessions older than this (ISO timestamp)')
|
|
2823
|
-
.option('-n, --limit <n>', 'Maximum number of sessions to return',
|
|
2947
|
+
.option('-n, --limit <n>', 'Maximum number of sessions to return', DEFAULT_LIMIT)
|
|
2824
2948
|
.option('--sort <field>', 'Sort the list by: recent (default), cost, or duration')
|
|
2825
2949
|
.option('--markdown', 'Render the session as markdown (user, assistant, thinking, tool calls)')
|
|
2826
2950
|
.option('--no-redact', 'Disable default secret redaction in rendered session output (--markdown and --json)')
|
|
@@ -2866,6 +2990,10 @@ export function registerSessionsCommands(program) {
|
|
|
2866
2990
|
# Search across every directory, not just this project
|
|
2867
2991
|
agents sessions "topic" --all
|
|
2868
2992
|
|
|
2993
|
+
# Who spawned which team: an orchestrator row carries team:<name>, and a
|
|
2994
|
+
# teammate row [<team>/<handle>]. --in-team narrows to one team's lineage.
|
|
2995
|
+
agents sessions --in-team redesign --teams
|
|
2996
|
+
|
|
2869
2997
|
# Show routine-run sessions and open one by routine run id
|
|
2870
2998
|
agents sessions --routine --all
|
|
2871
2999
|
agents sessions 2026-07-21T10-30-00-000Z
|
|
@@ -2882,6 +3010,7 @@ export function registerSessionsCommands(program) {
|
|
|
2882
3010
|
notes: `
|
|
2883
3011
|
- 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.
|
|
2884
3012
|
- --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.
|
|
3013
|
+
- --in-team matches both ends of the lineage: the session that ran 'agents teams create/add', and (with --teams) that team's teammates. In the interactive list, 't' cycles the same filter over the teams in view.
|
|
2885
3014
|
- --include and --exclude are mutually exclusive.
|
|
2886
3015
|
- --first and --last are mutually exclusive.
|
|
2887
3016
|
- A filter flag (--include/--exclude/--first/--last) without --markdown/--json defaults to --markdown output.
|
|
@@ -2890,13 +3019,13 @@ export function registerSessionsCommands(program) {
|
|
|
2890
3019
|
- Without --teams, team-spawned sessions are hidden by default.
|
|
2891
3020
|
`,
|
|
2892
3021
|
});
|
|
2893
|
-
sessionsCmd.action(async (query, options) => {
|
|
3022
|
+
sessionsCmd.action(async (query, options, command) => {
|
|
2894
3023
|
if (options.browser) {
|
|
2895
3024
|
// Alias for `agents browser sessions`: a profile positional narrows to one profile.
|
|
2896
3025
|
runBrowserSessions({ profile: query, json: options.json });
|
|
2897
3026
|
return;
|
|
2898
3027
|
}
|
|
2899
|
-
await sessionsAction(query, options);
|
|
3028
|
+
await sessionsAction(query, options, command.getOptionValueSource('limit'));
|
|
2900
3029
|
});
|
|
2901
3030
|
registerSessionsTailCommand(sessionsCmd);
|
|
2902
3031
|
registerSessionsSyncCommand(sessionsCmd);
|
package/dist/commands/ssh.js
CHANGED
|
@@ -20,7 +20,7 @@ import { readAndResolveBundleEnv, isHeadlessSecretsContext } from '../lib/secret
|
|
|
20
20
|
import { machineId } from '../lib/session/sync/config.js';
|
|
21
21
|
import { registerFleetCaptureCommand } from './fleet-capture.js';
|
|
22
22
|
import { registerFleetApplyAlias } from './apply.js';
|
|
23
|
-
import { addIgnored, getDevice, loadDevices, loadIgnored, removeDevice, removeIgnored, upsertDevice, writeReachability, } from '../lib/devices/registry.js';
|
|
23
|
+
import { addIgnored, getDevice, loadDevices, loadIgnored, removeDevice, removeIgnored, setAutoLaunchEnabled, setAutoLaunchPreferred, upsertDevice, writeReachability, } from '../lib/devices/registry.js';
|
|
24
24
|
import { collectReachabilityWriteBacks, deviceOnlineState } from '../lib/devices/reachability.js';
|
|
25
25
|
import { addControlToken } from '../lib/serve/token.js';
|
|
26
26
|
import { DEFAULT_SERVE_PORT } from '../lib/serve/server.js';
|
|
@@ -687,6 +687,8 @@ Typical workflow:
|
|
|
687
687
|
agents devices sync --yes # non-interactive: register all non-ignored nodes
|
|
688
688
|
agents devices list # see what's registered
|
|
689
689
|
agents devices ignore ipad165 # dismiss a node so it's never re-suggested
|
|
690
|
+
agents devices disable zion # exclude a device from Factory auto-launch
|
|
691
|
+
agents devices prefer mac-mini # boost a device in Factory auto-launch ranking
|
|
690
692
|
agents devices set win-mini --auth password --bundle muqsit
|
|
691
693
|
agents devices render --write # write ~/.ssh/config.d/agents include
|
|
692
694
|
agents fleet update # roll out latest agents-cli to every online device
|
|
@@ -766,6 +768,62 @@ Typical workflow:
|
|
|
766
768
|
}
|
|
767
769
|
console.log(chalk.green(`No longer ignoring '${name}'`) + chalk.gray(' — run `agents devices sync` to register it.'));
|
|
768
770
|
});
|
|
771
|
+
devicesCmd
|
|
772
|
+
.command('enable <name>')
|
|
773
|
+
.description('Allow a registered device to be auto-picked by Factory agent launches.')
|
|
774
|
+
.action(async (name) => {
|
|
775
|
+
try {
|
|
776
|
+
await mustGetDevice(name);
|
|
777
|
+
await setAutoLaunchEnabled(name, true);
|
|
778
|
+
console.log(chalk.green(`Enabled '${name}'`) + chalk.gray(' for Factory auto-launch.'));
|
|
779
|
+
}
|
|
780
|
+
catch (err) {
|
|
781
|
+
console.error(chalk.red(err.message));
|
|
782
|
+
process.exit(1);
|
|
783
|
+
}
|
|
784
|
+
});
|
|
785
|
+
devicesCmd
|
|
786
|
+
.command('disable <name>')
|
|
787
|
+
.description('Exclude a registered device from Factory auto-launch. It can still be picked manually via (Pick Host).')
|
|
788
|
+
.action(async (name) => {
|
|
789
|
+
try {
|
|
790
|
+
await mustGetDevice(name);
|
|
791
|
+
await setAutoLaunchEnabled(name, false);
|
|
792
|
+
console.log(chalk.green(`Disabled '${name}'`) + chalk.gray(' for Factory auto-launch.'));
|
|
793
|
+
}
|
|
794
|
+
catch (err) {
|
|
795
|
+
console.error(chalk.red(err.message));
|
|
796
|
+
process.exit(1);
|
|
797
|
+
}
|
|
798
|
+
});
|
|
799
|
+
devicesCmd
|
|
800
|
+
.command('prefer <name>')
|
|
801
|
+
.description('Boost a registered device in Factory auto-launch ranking.')
|
|
802
|
+
.action(async (name) => {
|
|
803
|
+
try {
|
|
804
|
+
await mustGetDevice(name);
|
|
805
|
+
await setAutoLaunchPreferred(name, true);
|
|
806
|
+
console.log(chalk.green(`Preferred '${name}'`) + chalk.gray(' for Factory auto-launch.'));
|
|
807
|
+
}
|
|
808
|
+
catch (err) {
|
|
809
|
+
console.error(chalk.red(err.message));
|
|
810
|
+
process.exit(1);
|
|
811
|
+
}
|
|
812
|
+
});
|
|
813
|
+
devicesCmd
|
|
814
|
+
.command('unprefer <name>')
|
|
815
|
+
.description('Remove the auto-launch preference boost from a device.')
|
|
816
|
+
.action(async (name) => {
|
|
817
|
+
try {
|
|
818
|
+
await mustGetDevice(name);
|
|
819
|
+
await setAutoLaunchPreferred(name, false);
|
|
820
|
+
console.log(chalk.green(`No longer preferring '${name}'`) + chalk.gray(' for Factory auto-launch.'));
|
|
821
|
+
}
|
|
822
|
+
catch (err) {
|
|
823
|
+
console.error(chalk.red(err.message));
|
|
824
|
+
process.exit(1);
|
|
825
|
+
}
|
|
826
|
+
});
|
|
769
827
|
const runList = async (opts = {}) => {
|
|
770
828
|
const reg = await loadDevices();
|
|
771
829
|
const names = Object.keys(reg).sort();
|
|
@@ -132,7 +132,8 @@ export function formatTeamRow(row, nameWidth, compositionWidth) {
|
|
|
132
132
|
const work = workCell(row.agents);
|
|
133
133
|
const runtime = runtimeSpan(t);
|
|
134
134
|
const age = chalk.gray(relTime(t.modified_at));
|
|
135
|
-
const
|
|
135
|
+
const spawnedBy = row.spawnedBy ? chalk.green(`by ${row.spawnedBy}`) : '';
|
|
136
|
+
const middleParts = [status, work, runtime, spawnedBy].filter(Boolean);
|
|
136
137
|
const middle = middleParts.join(chalk.gray(' · '));
|
|
137
138
|
return `${name} ${composition} ${middle}${middle ? chalk.gray(' · ') : ''}${age}`;
|
|
138
139
|
}
|
package/dist/commands/teams.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type { Command } from 'commander';
|
|
|
9
9
|
import { AgentManager, AgentStatus, type TaskType } from '../lib/teams/agents.js';
|
|
10
10
|
import { type TaskInfo } from '../lib/teams/api.js';
|
|
11
11
|
import { type TeamRow } from './teams-picker.js';
|
|
12
|
+
import { type TeamSpawner } from '../lib/session/db.js';
|
|
12
13
|
type TeamRegistry = Record<string, {
|
|
13
14
|
created_at: string;
|
|
14
15
|
description?: string;
|
|
@@ -58,7 +59,9 @@ export declare function decideTeamMessageRoute(status: AgentStatus, hasMessage:
|
|
|
58
59
|
* the teammate itself so we don't need the original --cloud CLI args.
|
|
59
60
|
*/
|
|
60
61
|
export declare function wireCloudDispatcher(mgr: AgentManager): void;
|
|
61
|
-
export declare function buildTeamRowsFromSnapshots(registry: TeamRegistry, agents: TeamListAgentSnapshot[]
|
|
62
|
+
export declare function buildTeamRowsFromSnapshots(registry: TeamRegistry, agents: TeamListAgentSnapshot[],
|
|
63
|
+
/** team name -> the session that spawned it (see teamSpawners). */
|
|
64
|
+
spawners?: Map<string, TeamSpawner>): {
|
|
62
65
|
rows: TeamRow[];
|
|
63
66
|
teams: TaskInfo[];
|
|
64
67
|
names: string[];
|