@phnx-labs/agents-cli 1.21.3 → 1.22.0
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 +67 -0
- package/README.md +32 -3
- package/dist/bin/agents +0 -0
- package/dist/commands/computer-actions.js +1 -0
- package/dist/commands/exec.d.ts +27 -0
- package/dist/commands/exec.js +123 -6
- package/dist/commands/models.js +36 -1
- package/dist/commands/projects.js +22 -2
- package/dist/commands/sessions-backfill.d.ts +32 -0
- package/dist/commands/sessions-backfill.js +186 -0
- package/dist/commands/sessions.d.ts +17 -1
- package/dist/commands/sessions.js +317 -18
- package/dist/commands/teams.js +1 -1
- package/dist/commands/worktree.d.ts +3 -3
- package/dist/commands/worktree.js +35 -4
- package/dist/lib/daemon.d.ts +5 -1
- package/dist/lib/daemon.js +63 -14
- package/dist/lib/devices/resolve-target.d.ts +6 -0
- package/dist/lib/devices/resolve-target.js +9 -3
- package/dist/lib/exec.js +39 -8
- package/dist/lib/hosts/dispatch.d.ts +12 -0
- package/dist/lib/hosts/dispatch.js +23 -6
- package/dist/lib/hosts/reconnect.d.ts +38 -0
- package/dist/lib/hosts/reconnect.js +85 -4
- package/dist/lib/hosts/run-target.js +14 -2
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/model-tiers.d.ts +54 -0
- package/dist/lib/model-tiers.js +229 -0
- package/dist/lib/models.d.ts +3 -0
- package/dist/lib/models.js +44 -7
- package/dist/lib/pricing/prices.json +16 -1
- package/dist/lib/project-focus.d.ts +42 -0
- package/dist/lib/project-focus.js +80 -0
- package/dist/lib/project-schedule.d.ts +75 -0
- package/dist/lib/project-schedule.js +110 -0
- package/dist/lib/redact.d.ts +2 -0
- package/dist/lib/redact.js +22 -0
- package/dist/lib/remote-agents-json.d.ts +2 -0
- package/dist/lib/remote-agents-json.js +3 -3
- package/dist/lib/rotate.d.ts +84 -1
- package/dist/lib/rotate.js +155 -5
- package/dist/lib/runner.d.ts +4 -2
- package/dist/lib/runner.js +13 -4
- 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/bash-command.js +60 -9
- package/dist/lib/session/db.d.ts +7 -1
- package/dist/lib/session/db.js +301 -32
- package/dist/lib/session/discover.d.ts +40 -7
- package/dist/lib/session/discover.js +144 -83
- package/dist/lib/session/parse.d.ts +8 -1
- package/dist/lib/session/parse.js +83 -32
- package/dist/lib/session/remote-list.d.ts +71 -0
- package/dist/lib/session/remote-list.js +410 -2
- package/dist/lib/session/shell-programs.d.ts +15 -0
- package/dist/lib/session/shell-programs.js +359 -0
- package/dist/lib/session/tool-calls.d.ts +88 -0
- package/dist/lib/session/tool-calls.js +612 -0
- package/dist/lib/session/tool-index.d.ts +100 -0
- package/dist/lib/session/tool-index.js +773 -0
- package/dist/lib/session/tool-store.d.ts +15 -0
- package/dist/lib/session/tool-store.js +198 -0
- package/dist/lib/session/types.d.ts +7 -0
- package/dist/lib/state.d.ts +10 -1
- package/dist/lib/state.js +11 -2
- package/dist/lib/teams/remoteWorktree.d.ts +3 -4
- package/dist/lib/teams/remoteWorktree.js +3 -4
- package/dist/lib/teams/worktree.d.ts +11 -1
- package/dist/lib/teams/worktree.js +42 -4
- package/dist/lib/types.d.ts +17 -0
- package/dist/lib/types.js +17 -0
- package/package.json +3 -1
|
@@ -14,6 +14,7 @@ import { spawn } from 'child_process';
|
|
|
14
14
|
import { Option } from 'commander';
|
|
15
15
|
import chalk from 'chalk';
|
|
16
16
|
import { truncate, padRight } from '../lib/format.js';
|
|
17
|
+
import { sanitizeForTerminal } from '../lib/redact.js';
|
|
17
18
|
import { resolveProjectKey } from '../lib/project-key.js';
|
|
18
19
|
import { listProjectDefs, resolveProjectNameForCwd } from '../lib/projects.js';
|
|
19
20
|
import ora from 'ora';
|
|
@@ -26,10 +27,10 @@ import { mapPanesToTargets, listClients } from '../lib/tmux/session.js';
|
|
|
26
27
|
import { resolveViewingIn, viewingInLabel } from '../lib/session/viewing-in.js';
|
|
27
28
|
import { machineId, normalizeHost } from '../lib/session/sync/config.js';
|
|
28
29
|
import { gatherRemoteActive, NO_FANOUT_ENV } from '../lib/session/remote-active.js';
|
|
29
|
-
import { gatherRemoteList, runOnPeer } from '../lib/session/remote-list.js';
|
|
30
|
+
import { gatherRemoteList, gatherRemoteToolProgramCounts, gatherRemoteToolSearch, runOnPeer } from '../lib/session/remote-list.js';
|
|
30
31
|
import { stringWidth, truncateToWidth, padToWidth, terminalWidth } from '../lib/session/width.js';
|
|
31
32
|
import { inferSessionState } from '../lib/session/state.js';
|
|
32
|
-
import { discoverSessions, countSessionsInScope, resolveSessionById, isCompleteSessionId, looksLikeSessionId, searchContentIndex, getSessionRoots } from '../lib/session/discover.js';
|
|
33
|
+
import { discoverSessions, queryIndexedSessions, countSessionsInScope, resolveSessionById, isCompleteSessionId, looksLikeSessionId, searchContentIndex, getSessionRoots, scopeToManaged } from '../lib/session/discover.js';
|
|
33
34
|
import { findSessionsById, querySessions } from '../lib/session/db.js';
|
|
34
35
|
import { filterTeamSessions, safeTeamText } from '../lib/session/team-filter.js';
|
|
35
36
|
import { parseSession } from '../lib/session/parse.js';
|
|
@@ -57,8 +58,13 @@ import { registerSessionsInjectCommand } from './sessions-inject.js';
|
|
|
57
58
|
import { registerSessionsExportCommand } from './sessions-export.js';
|
|
58
59
|
import { registerSessionsImportCommand } from './sessions-import.js';
|
|
59
60
|
import { registerSessionsMigrateCommand, registerSessionsMigrationsCommand } from './sessions-migrate.js';
|
|
61
|
+
import { registerSessionsBackfillCommand } from './sessions-backfill.js';
|
|
60
62
|
import { runBrowserSessions } from '../lib/browser/sessions-list.js';
|
|
63
|
+
import { countToolProgramOccurrences, parseToolProgramCountClause, readToolIndexCoverage, searchToolCalls, TOOL_QUERY_MAX_CLAUSE_BYTES, TOOL_QUERY_MAX_CLAUSES, TOOL_QUERY_MAX_RESULT_SESSIONS, serializeToolSearchEnvelope, toolSearchRemoteReceiveBudget, } from '../lib/session/tool-index.js';
|
|
61
64
|
const SESSION_AGENT_FILTER_HELP = `Filter by agent, e.g. claude, codex, claude@2.0.65`;
|
|
65
|
+
function collectQueryClause(value, previous) {
|
|
66
|
+
return [...previous, value];
|
|
67
|
+
}
|
|
62
68
|
/**
|
|
63
69
|
* The prioritized harnesses that get a boolean shorthand flag (e.g. `--claude`
|
|
64
70
|
* === `--agent claude`). The rest stay reachable via `--agent <name>`, which
|
|
@@ -1296,6 +1302,114 @@ export function formatLiveStatusHeadline(live, favorite = false) {
|
|
|
1296
1302
|
}
|
|
1297
1303
|
return `${star}${glyph} ${statusColor(live.status)(word)}${suffix}`;
|
|
1298
1304
|
}
|
|
1305
|
+
/** Merge local and peer envelopes without changing the versioned JSON shape. */
|
|
1306
|
+
export function mergeToolSearchEnvelopes(local, remotes) {
|
|
1307
|
+
const all = [local, ...remotes];
|
|
1308
|
+
const sessions = new Map();
|
|
1309
|
+
for (const envelope of all) {
|
|
1310
|
+
for (const session of envelope.sessions) {
|
|
1311
|
+
sessions.set(`${session.machine ?? 'local'}\0${session.id}`, session);
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
return {
|
|
1315
|
+
schemaVersion: 1,
|
|
1316
|
+
generatedAt: new Date().toISOString(),
|
|
1317
|
+
query: local.query,
|
|
1318
|
+
coverage: {
|
|
1319
|
+
indexedFiles: all.reduce((n, envelope) => n + envelope.coverage.indexedFiles, 0),
|
|
1320
|
+
indexedCalls: all.reduce((n, envelope) => n + envelope.coverage.indexedCalls, 0),
|
|
1321
|
+
skippedFiles: all.reduce((n, envelope) => n + envelope.coverage.skippedFiles, 0),
|
|
1322
|
+
limitedFiles: all.reduce((n, envelope) => n + envelope.coverage.limitedFiles, 0),
|
|
1323
|
+
remainingFiles: all.reduce((n, envelope) => n + envelope.coverage.remainingFiles, 0),
|
|
1324
|
+
complete: all.every((envelope) => envelope.coverage.complete),
|
|
1325
|
+
},
|
|
1326
|
+
sessions: [...sessions.values()].sort((a, b) => b.timestamp.localeCompare(a.timestamp)),
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
export function mergeToolProgramCountEnvelopes(local, remotes) {
|
|
1330
|
+
const all = [local, ...remotes];
|
|
1331
|
+
return {
|
|
1332
|
+
schemaVersion: 1,
|
|
1333
|
+
kind: 'tool-program-count',
|
|
1334
|
+
generatedAt: new Date().toISOString(),
|
|
1335
|
+
query: local.query,
|
|
1336
|
+
coverage: {
|
|
1337
|
+
indexedFiles: all.reduce((sum, envelope) => sum + envelope.coverage.indexedFiles, 0),
|
|
1338
|
+
indexedCalls: all.reduce((sum, envelope) => sum + envelope.coverage.indexedCalls, 0),
|
|
1339
|
+
skippedFiles: all.reduce((sum, envelope) => sum + envelope.coverage.skippedFiles, 0),
|
|
1340
|
+
limitedFiles: all.reduce((sum, envelope) => sum + envelope.coverage.limitedFiles, 0),
|
|
1341
|
+
remainingFiles: all.reduce((sum, envelope) => sum + envelope.coverage.remainingFiles, 0),
|
|
1342
|
+
complete: all.every((envelope) => envelope.coverage.complete),
|
|
1343
|
+
},
|
|
1344
|
+
totals: {
|
|
1345
|
+
occurrences: all.reduce((sum, envelope) => sum + envelope.totals.occurrences, 0),
|
|
1346
|
+
toolCalls: all.reduce((sum, envelope) => sum + envelope.totals.toolCalls, 0),
|
|
1347
|
+
sessions: all.reduce((sum, envelope) => sum + envelope.totals.sessions, 0),
|
|
1348
|
+
},
|
|
1349
|
+
machines: all.flatMap((envelope) => envelope.machines),
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
/** Partition a fleet query by transcript origin so synced mirrors cannot duplicate results. */
|
|
1353
|
+
export function toolOriginSessions(sessions, machine, originOnly) {
|
|
1354
|
+
return originOnly
|
|
1355
|
+
? sessions.filter((session) => (session.machine ?? machine) === machine)
|
|
1356
|
+
: sessions;
|
|
1357
|
+
}
|
|
1358
|
+
export function printToolProgramCount(envelope) {
|
|
1359
|
+
const { totals } = envelope;
|
|
1360
|
+
const qualifier = envelope.coverage.complete ? '' : 'at least ';
|
|
1361
|
+
console.log(`${envelope.query.program}: ${qualifier}${totals.occurrences.toLocaleString()} static occurrence${totals.occurrences === 1 ? '' : 's'} `
|
|
1362
|
+
+ `in ${totals.toolCalls.toLocaleString()} tool call${totals.toolCalls === 1 ? '' : 's'} `
|
|
1363
|
+
+ `across ${totals.sessions.toLocaleString()} session${totals.sessions === 1 ? '' : 's'}.`);
|
|
1364
|
+
if (!envelope.coverage.complete) {
|
|
1365
|
+
console.log(chalk.yellow(`Partial tool index: ${envelope.coverage.remainingFiles.toLocaleString()} transcript${envelope.coverage.remainingFiles === 1 ? '' : 's'} still need `
|
|
1366
|
+
+ '`agents sessions backfill tools`.'));
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
export function toolSearchFleetSortError(sort, spansDevices) {
|
|
1370
|
+
if (!spansDevices || !sort || sort === 'recent')
|
|
1371
|
+
return undefined;
|
|
1372
|
+
return 'Tool search across devices supports only --sort recent; cost and duration are local-only.';
|
|
1373
|
+
}
|
|
1374
|
+
/** Compact grouped evidence for humans; JSON retains every bounded field. */
|
|
1375
|
+
export function printToolSearch(envelope) {
|
|
1376
|
+
for (const session of envelope.sessions) {
|
|
1377
|
+
const machineName = session.machine
|
|
1378
|
+
? truncate(sanitizeForTerminal(session.machine).replace(/\s+/g, ' '), 80)
|
|
1379
|
+
: '';
|
|
1380
|
+
const machine = machineName ? ` @ ${machineName}` : '';
|
|
1381
|
+
const rawHeading = session.label || session.topic || session.project || session.shortId;
|
|
1382
|
+
const heading = truncate(sanitizeForTerminal(rawHeading).replace(/\s+/g, ' '), Math.max(30, terminalWidth() - 20));
|
|
1383
|
+
console.log(`${chalk.cyan(session.shortId)}${chalk.gray(machine)} ${heading}`);
|
|
1384
|
+
for (const call of session.calls) {
|
|
1385
|
+
const tool = truncate(sanitizeForTerminal(call.tool).replace(/\s+/g, ' '), 80);
|
|
1386
|
+
const safePrograms = call.programs.map((program) => truncate(sanitizeForTerminal(program).replace(/\s+/g, ' '), 80));
|
|
1387
|
+
const programs = safePrograms.length > 0 ? ` [${safePrograms.join(', ')}]` : '';
|
|
1388
|
+
const status = call.outcome === 'unknown' ? '' : ` ${call.outcome}`;
|
|
1389
|
+
const input = truncate(sanitizeForTerminal(call.input).replace(/\s+/g, ' '), Math.max(30, terminalWidth() - 26));
|
|
1390
|
+
console.log(` ${chalk.gray(`#${call.ordinal + 1}`)} ${tool}${programs}${status} ${input}`);
|
|
1391
|
+
const snippet = call.error || call.output;
|
|
1392
|
+
if (snippet) {
|
|
1393
|
+
console.log(` ${chalk.gray(truncate(sanitizeForTerminal(snippet).replace(/\s+/g, ' '), Math.max(30, terminalWidth() - 8)))}`);
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
console.log();
|
|
1397
|
+
}
|
|
1398
|
+
const count = envelope.sessions.length;
|
|
1399
|
+
console.log(chalk.gray(`${count} matching session${count === 1 ? '' : 's'}.`));
|
|
1400
|
+
if (!envelope.coverage.complete) {
|
|
1401
|
+
const skipped = envelope.coverage.skippedFiles > 0
|
|
1402
|
+
? ` ${envelope.coverage.skippedFiles} transcript${envelope.coverage.skippedFiles === 1 ? ' was' : 's were'} skipped.`
|
|
1403
|
+
: '';
|
|
1404
|
+
const limited = envelope.coverage.limitedFiles > 0
|
|
1405
|
+
? ` ${envelope.coverage.limitedFiles} transcript${envelope.coverage.limitedFiles === 1 ? ' has' : 's have'} incomplete evidence because a safety limit was reached.`
|
|
1406
|
+
: '';
|
|
1407
|
+
const retry = envelope.coverage.remainingFiles > 0
|
|
1408
|
+
? ' Run `agents sessions backfill tools` to index historical transcripts.'
|
|
1409
|
+
: '';
|
|
1410
|
+
console.log(chalk.yellow(`Tool index coverage is partial: ${envelope.coverage.remainingFiles} transcript${envelope.coverage.remainingFiles === 1 ? '' : 's'} remain.${skipped}${limited}${retry}`));
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1299
1413
|
/** Main action handler for `agents sessions`. Routes to picker, table, or single-session render. */
|
|
1300
1414
|
async function sessionsAction(query, options,
|
|
1301
1415
|
/**
|
|
@@ -1304,9 +1418,70 @@ async function sessionsAction(query, options,
|
|
|
1304
1418
|
* because the default arrives as a string like any typed value.
|
|
1305
1419
|
*/
|
|
1306
1420
|
limitSource) {
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1421
|
+
const queryClauses = options.query ?? [];
|
|
1422
|
+
const toolOnly = options.include?.split(',').map((role) => role.trim()).filter(Boolean).join(',') === 'tools';
|
|
1423
|
+
const toolEvidenceMode = toolOnly;
|
|
1424
|
+
if (options.count && !toolOnly) {
|
|
1425
|
+
console.error(chalk.red('--count requires --include tools.'));
|
|
1426
|
+
process.exitCode = 1;
|
|
1427
|
+
return;
|
|
1428
|
+
}
|
|
1429
|
+
if (!toolEvidenceMode) {
|
|
1430
|
+
if (queryClauses.length > 1) {
|
|
1431
|
+
console.error(chalk.red('Repeated --query clauses require --include tools.'));
|
|
1432
|
+
process.exitCode = 1;
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
// Outside tool evidence mode, --query retains its original positional-search
|
|
1436
|
+
// meaning. The collector only makes it repeatable for tool clauses.
|
|
1437
|
+
query = query ?? queryClauses[0];
|
|
1438
|
+
}
|
|
1439
|
+
if (options.fleet && !toolEvidenceMode) {
|
|
1440
|
+
console.error(chalk.red('--fleet applies to tool-call queries: add --include tools.'));
|
|
1441
|
+
process.exitCode = 1;
|
|
1442
|
+
return;
|
|
1443
|
+
}
|
|
1444
|
+
if (toolEvidenceMode && (options.markdown || options.redact === false)) {
|
|
1445
|
+
const incompatible = [
|
|
1446
|
+
options.markdown ? '--markdown' : undefined,
|
|
1447
|
+
options.redact === false ? '--no-redact' : undefined,
|
|
1448
|
+
].filter((flag) => flag !== undefined);
|
|
1449
|
+
console.error(chalk.red(`${incompatible.join(' and ')} cannot be used with --include tools.`));
|
|
1450
|
+
console.error(chalk.gray('Tool evidence is always redacted and byte-bounded; drop the conflicting render flag.'));
|
|
1451
|
+
process.exitCode = 1;
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
if (toolEvidenceMode && queryClauses.length > TOOL_QUERY_MAX_CLAUSES) {
|
|
1455
|
+
console.error(chalk.red(`Tool search accepts at most ${TOOL_QUERY_MAX_CLAUSES} --query clauses.`));
|
|
1456
|
+
process.exitCode = 1;
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1459
|
+
if (toolEvidenceMode && queryClauses.some((clause) => Buffer.byteLength(clause) > TOOL_QUERY_MAX_CLAUSE_BYTES)) {
|
|
1460
|
+
console.error(chalk.red(`Each tool --query clause is limited to ${TOOL_QUERY_MAX_CLAUSE_BYTES} bytes.`));
|
|
1461
|
+
process.exitCode = 1;
|
|
1462
|
+
return;
|
|
1463
|
+
}
|
|
1464
|
+
if (options.count && (queryClauses.length !== 1 || query !== undefined)) {
|
|
1465
|
+
console.error(chalk.red('--count requires exactly one --query program:<name> clause and no positional query.'));
|
|
1466
|
+
process.exitCode = 1;
|
|
1467
|
+
return;
|
|
1468
|
+
}
|
|
1469
|
+
if (options.count && (limitSource === 'cli' || limitSource === 'env')) {
|
|
1470
|
+
console.error(chalk.red('--count covers the complete filtered scope and cannot be combined with --limit.'));
|
|
1471
|
+
process.exitCode = 1;
|
|
1472
|
+
return;
|
|
1473
|
+
}
|
|
1474
|
+
let countProgram;
|
|
1475
|
+
if (options.count) {
|
|
1476
|
+
try {
|
|
1477
|
+
countProgram = parseToolProgramCountClause(queryClauses[0]);
|
|
1478
|
+
}
|
|
1479
|
+
catch (error) {
|
|
1480
|
+
console.error(chalk.red(error instanceof Error ? error.message : String(error)));
|
|
1481
|
+
process.exitCode = 1;
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1310
1485
|
// Normalize convenience flags before any routing reads them: per-agent
|
|
1311
1486
|
// shorthands fold into --agent, and --device is an alias for --host (both
|
|
1312
1487
|
// resolve against the same device registry).
|
|
@@ -1362,13 +1537,19 @@ limitSource) {
|
|
|
1362
1537
|
});
|
|
1363
1538
|
return;
|
|
1364
1539
|
}
|
|
1540
|
+
if (toolEvidenceMode && options.local === true
|
|
1541
|
+
&& options.host && !shouldIncludeLocal(options.host, machineId())) {
|
|
1542
|
+
console.error(chalk.red('--local and --device name opposite scopes: --local skips the SSH fan-out that --device needs.'));
|
|
1543
|
+
console.error(chalk.gray('Drop one — `--device <box>` to read that machine, `--local` to stay on this one.'));
|
|
1544
|
+
process.exit(1);
|
|
1545
|
+
}
|
|
1365
1546
|
// --host WITHOUT --active. `--json` fans the recent listing out and emits ONE
|
|
1366
1547
|
// clean merged SessionMeta[] array (same shape as the local --json path), for
|
|
1367
1548
|
// scripts/extensions that JSON.parse a remote's history. Without --json it
|
|
1368
1549
|
// keeps the legacy per-host stream (each remote's raw stdout under a
|
|
1369
1550
|
// `── host ──` banner). With --active, the hosts are folded into the merged
|
|
1370
1551
|
// machine-grouped view instead (handled below).
|
|
1371
|
-
if (options.host && options.host.length > 0 && !options.active) {
|
|
1552
|
+
if (options.host && options.host.length > 0 && !options.active && !toolEvidenceMode) {
|
|
1372
1553
|
// --local means "skip the SSH fan-out"; --host means "look only over there".
|
|
1373
1554
|
// Together they ask for a peer's sessions without dialing the peer, which can
|
|
1374
1555
|
// only ever be empty — so say that instead of rendering a blank list.
|
|
@@ -1503,7 +1684,7 @@ limitSource) {
|
|
|
1503
1684
|
}
|
|
1504
1685
|
const mode = resolveViewMode(options, filterOpts);
|
|
1505
1686
|
// --markdown or any filter flag forces single-session render.
|
|
1506
|
-
const wantsRender = mode === 'markdown' || hasAnyFilter(filterOpts);
|
|
1687
|
+
const wantsRender = !toolEvidenceMode && (mode === 'markdown' || hasAnyFilter(filterOpts));
|
|
1507
1688
|
// Artifact-list or artifact-read paths: widen scope and resolve session globally.
|
|
1508
1689
|
if ((options.artifacts || options.artifact !== undefined) && searchQuery) {
|
|
1509
1690
|
await renderArtifactsGlobal(searchQuery, options.artifacts ?? false, options.artifact, { agent: options.agent, project: options.project });
|
|
@@ -1536,11 +1717,24 @@ limitSource) {
|
|
|
1536
1717
|
const limit = wantsOverview
|
|
1537
1718
|
? OVERVIEW_POOL_LIMIT
|
|
1538
1719
|
: parseInt(userSetLimit ? options.limit : wantsWholeTeam ? String(WHOLE_TEAM_POOL_LIMIT) : DEFAULT_LIMIT, 10);
|
|
1720
|
+
if (toolEvidenceMode && (!Number.isSafeInteger(limit) || limit < 1 || limit > TOOL_QUERY_MAX_RESULT_SESSIONS)) {
|
|
1721
|
+
console.error(chalk.red(`Tool search --limit must be from 1 to ${TOOL_QUERY_MAX_RESULT_SESSIONS}.`));
|
|
1722
|
+
process.exitCode = 1;
|
|
1723
|
+
return;
|
|
1724
|
+
}
|
|
1539
1725
|
// Overview: recency order across the whole index, no default window; an explicit
|
|
1540
1726
|
// --since still narrows. Non-overview keeps the prior interactive-30d default.
|
|
1541
1727
|
const since = wantsOverview
|
|
1542
1728
|
? options.since
|
|
1543
1729
|
: (options.since ?? (isInteractive && !options.all && !wantsWholeTeam ? '30d' : undefined));
|
|
1730
|
+
const toolSpansDevices = toolEvidenceMode
|
|
1731
|
+
&& (options.fleet || (options.host?.length ?? 0) > 0);
|
|
1732
|
+
const toolSortError = toolSearchFleetSortError(options.sort, toolSpansDevices);
|
|
1733
|
+
if (toolSortError) {
|
|
1734
|
+
console.error(chalk.red(toolSortError));
|
|
1735
|
+
process.exitCode = 1;
|
|
1736
|
+
return;
|
|
1737
|
+
}
|
|
1544
1738
|
const spinner = options.json ? null : ora().start();
|
|
1545
1739
|
const tracker = createScanProgressTracker(LOAD_VERBS, 'sessions', spinner);
|
|
1546
1740
|
try {
|
|
@@ -1556,29 +1750,53 @@ limitSource) {
|
|
|
1556
1750
|
// --in-team spans directories by construction: a team's teammates run in
|
|
1557
1751
|
// their own worktrees, so scoping to the current cwd hides most of the
|
|
1558
1752
|
// lineage the flag exists to show.
|
|
1559
|
-
all: pathFilter ? undefined : options.all || wantsWholeTeam,
|
|
1753
|
+
all: pathFilter ? undefined : options.all || wantsWholeTeam || toolSpansDevices,
|
|
1560
1754
|
cwd: process.cwd(),
|
|
1561
1755
|
// Default overview scopes to the current repo SUBTREE (prefix match), so a
|
|
1562
1756
|
// monorepo shows its sub-projects grouped instead of collapsing to the one
|
|
1563
1757
|
// exact-cwd project. `--all` clears the prefix and spans the whole index.
|
|
1564
|
-
cwdPrefix: pathFilter ?? (wantsOverview && !options.all && !wantsWholeTeam ? process.cwd() : undefined),
|
|
1758
|
+
cwdPrefix: pathFilter ?? (wantsOverview && !options.all && !wantsWholeTeam && !toolSpansDevices ? process.cwd() : undefined),
|
|
1565
1759
|
project: options.project,
|
|
1566
1760
|
since,
|
|
1567
1761
|
until: options.until,
|
|
1568
1762
|
sortBy,
|
|
1569
1763
|
origin: options.routine ? 'routine' : undefined,
|
|
1764
|
+
skipExistenceCheck: toolEvidenceMode,
|
|
1765
|
+
unbounded: toolEvidenceMode,
|
|
1570
1766
|
skill: options.skill,
|
|
1571
1767
|
plugin: options.plugin,
|
|
1572
1768
|
};
|
|
1573
1769
|
let hiddenUnmanaged = 0;
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
includeUnmanaged: options.unmanaged
|
|
1580
|
-
|
|
1581
|
-
|
|
1770
|
+
const toolSelf = toolEvidenceMode ? machineId() : undefined;
|
|
1771
|
+
const toolIncludesLocal = !toolEvidenceMode
|
|
1772
|
+
|| process.env[NO_FANOUT_ENV] === '1'
|
|
1773
|
+
|| shouldIncludeLocal(options.host, toolSelf);
|
|
1774
|
+
const indexedIdMatches = toolIncludesLocal && toolEvidenceMode && searchQuery && looksLikeSessionId(searchQuery)
|
|
1775
|
+
? scopeToManaged(findSessionsById(searchQuery, { agent, version, project: options.project }), agent ? [agent] : SESSION_AGENTS, { agent, includeUnmanaged: options.unmanaged })
|
|
1776
|
+
: [];
|
|
1777
|
+
let sessions;
|
|
1778
|
+
if (!toolIncludesLocal) {
|
|
1779
|
+
sessions = [];
|
|
1780
|
+
}
|
|
1781
|
+
else if (indexedIdMatches.length > 0) {
|
|
1782
|
+
sessions = indexedIdMatches.map((session) => ({
|
|
1783
|
+
...session,
|
|
1784
|
+
machine: session.machine ?? toolSelf,
|
|
1785
|
+
}));
|
|
1786
|
+
}
|
|
1787
|
+
else {
|
|
1788
|
+
const readOptions = {
|
|
1789
|
+
...scope,
|
|
1790
|
+
limit,
|
|
1791
|
+
excludeTeamOrigin: !options.teams,
|
|
1792
|
+
onProgress: tracker.onProgress,
|
|
1793
|
+
includeUnmanaged: options.unmanaged,
|
|
1794
|
+
onHiddenUnmanaged: (n) => { hiddenUnmanaged = n; },
|
|
1795
|
+
};
|
|
1796
|
+
sessions = toolEvidenceMode
|
|
1797
|
+
? await queryIndexedSessions(readOptions, { resolveLinear: false })
|
|
1798
|
+
: await discoverSessions(readOptions);
|
|
1799
|
+
}
|
|
1582
1800
|
tracker.stop();
|
|
1583
1801
|
spinner?.stop();
|
|
1584
1802
|
// Version filter is pushed down to SQL via scope.version above; no
|
|
@@ -1601,6 +1819,58 @@ limitSource) {
|
|
|
1601
1819
|
const starred = listFavorites();
|
|
1602
1820
|
sessions = sessions.filter((s) => starred.has(s.id));
|
|
1603
1821
|
}
|
|
1822
|
+
if (toolEvidenceMode) {
|
|
1823
|
+
const self = toolSelf;
|
|
1824
|
+
const selectedSessions = searchQuery
|
|
1825
|
+
? filterSessionsByQuery(sessions, searchQuery)
|
|
1826
|
+
: sessions;
|
|
1827
|
+
const localSessions = selectedSessions;
|
|
1828
|
+
const mayFanOut = options.local !== true && process.env[NO_FANOUT_ENV] !== '1';
|
|
1829
|
+
const hosts = remoteHostsToDial(options.host, self);
|
|
1830
|
+
const originOnly = process.env[NO_FANOUT_ENV] === '1'
|
|
1831
|
+
|| (mayFanOut && (options.fleet || (options.host?.length ?? 0) > 0));
|
|
1832
|
+
const querySessions = toolOriginSessions(localSessions, self, originOnly);
|
|
1833
|
+
if (countProgram) {
|
|
1834
|
+
const countCoverage = readToolIndexCoverage(querySessions);
|
|
1835
|
+
let countEnvelope = countToolProgramOccurrences(querySessions, countProgram, countCoverage, self);
|
|
1836
|
+
if (!toolIncludesLocal)
|
|
1837
|
+
countEnvelope.machines = [];
|
|
1838
|
+
if (mayFanOut && (options.fleet || (options.host?.length ?? 0) > 0)
|
|
1839
|
+
&& (!options.host?.length || (hosts && hosts.length > 0))) {
|
|
1840
|
+
const stripped = toolSearchForwardedArgs(process.argv, options.host ?? []);
|
|
1841
|
+
const remote = await gatherRemoteToolProgramCounts(stripped, options.host?.length ? hosts : undefined, countProgram);
|
|
1842
|
+
countEnvelope = mergeToolProgramCountEnvelopes(countEnvelope, remote.envelopes.map((item) => item.envelope));
|
|
1843
|
+
if (remote.unreachable.length > 0)
|
|
1844
|
+
countEnvelope.coverage.complete = false;
|
|
1845
|
+
}
|
|
1846
|
+
if (options.json)
|
|
1847
|
+
process.stdout.write(JSON.stringify(countEnvelope, null, 2) + '\n');
|
|
1848
|
+
else
|
|
1849
|
+
printToolProgramCount(countEnvelope);
|
|
1850
|
+
return;
|
|
1851
|
+
}
|
|
1852
|
+
const coverage = readToolIndexCoverage(querySessions);
|
|
1853
|
+
let envelope = searchToolCalls(querySessions, queryClauses, coverage, limit);
|
|
1854
|
+
if (mayFanOut && (options.fleet || (options.host?.length ?? 0) > 0)) {
|
|
1855
|
+
if (!options.host?.length || (hosts && hosts.length > 0)) {
|
|
1856
|
+
const stripped = toolSearchForwardedArgs(process.argv, options.host ?? []);
|
|
1857
|
+
const remote = await gatherRemoteToolSearch(stripped, options.host?.length ? hosts : undefined, toolSearchRemoteReceiveBudget(envelope), queryClauses);
|
|
1858
|
+
envelope = mergeToolSearchEnvelopes(envelope, remote.envelopes.map((item) => item.envelope));
|
|
1859
|
+
if (remote.truncated.length > 0 || remote.unreachable.length > 0) {
|
|
1860
|
+
envelope.coverage.complete = false;
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
envelope.sessions = envelope.sessions.slice(0, limit);
|
|
1865
|
+
const serializedEnvelope = serializeToolSearchEnvelope(envelope);
|
|
1866
|
+
if (options.json) {
|
|
1867
|
+
process.stdout.write(serializedEnvelope);
|
|
1868
|
+
}
|
|
1869
|
+
else {
|
|
1870
|
+
printToolSearch(envelope);
|
|
1871
|
+
}
|
|
1872
|
+
return;
|
|
1873
|
+
}
|
|
1604
1874
|
// Under --in-team the visible list is one team, so the whole-index team-origin
|
|
1605
1875
|
// count would be a non-sequitur next to it.
|
|
1606
1876
|
const hiddenCount = options.teams || options.inTeam
|
|
@@ -3166,6 +3436,15 @@ export function metadataResolveForwardedArgs(selector, scope) {
|
|
|
3166
3436
|
args.push('--project', scope.project);
|
|
3167
3437
|
return args;
|
|
3168
3438
|
}
|
|
3439
|
+
/** Build the local-only peer argv for a distributed tool search. */
|
|
3440
|
+
export function toolSearchForwardedArgs(argv, hosts) {
|
|
3441
|
+
const args = ensureWholeIndex(buildForwardedArgs(argv, new Set(hosts)).filter((arg) => arg !== '--fleet'));
|
|
3442
|
+
if (!args.includes('--json'))
|
|
3443
|
+
args.push('--json');
|
|
3444
|
+
if (!args.includes('--local'))
|
|
3445
|
+
args.push('--local');
|
|
3446
|
+
return args;
|
|
3447
|
+
}
|
|
3169
3448
|
/** Resolution must fail closed when any selected peer did not answer. Choosing
|
|
3170
3449
|
* from a partial fleet can turn an unseen candidate into a false unique match. */
|
|
3171
3450
|
export function metadataResolveOutcome(localMatches, remote, selector) {
|
|
@@ -3301,7 +3580,7 @@ export function registerSessionsCommands(program) {
|
|
|
3301
3580
|
const sessionsCmd = program
|
|
3302
3581
|
.command('sessions')
|
|
3303
3582
|
.argument('[query]', 'Session ID, search query, or path (., ../, /path) to filter by project')
|
|
3304
|
-
.option('--query <
|
|
3583
|
+
.option('--query <clause>', 'Search text; repeat with --include tools to require distinct matching calls', collectQueryClause, [])
|
|
3305
3584
|
.option('--resolve <selector>', 'Resolve one full ID, unique prefix, or keyword query to safe session metadata (requires --json; searches the fleet unless --local)')
|
|
3306
3585
|
.addOption(new Option('--resolve-safe-v1 <selector>').hideHelp())
|
|
3307
3586
|
.description('Find, browse, and read agent conversation transcripts across Claude, Codex, Gemini, and OpenCode.')
|
|
@@ -3344,6 +3623,8 @@ export function registerSessionsCommands(program) {
|
|
|
3344
3623
|
.option('--cloud', 'Source sessions from Rush Cloud (captured runs) instead of local disk')
|
|
3345
3624
|
.option('-H, --host <target...>', 'Run this query on remote machine(s) over SSH (host alias or user@host; repeatable)')
|
|
3346
3625
|
.option('--device <target...>', 'Alias for --host (device alias from `agents devices`; repeatable)')
|
|
3626
|
+
.option('--fleet', 'With --include tools: query every registered online compute device and merge compact matches')
|
|
3627
|
+
.option('--count', 'With one program:<name> tool query: count static occurrences, containing calls, and sessions')
|
|
3347
3628
|
.option('--browser', 'List browser-profile captures (screenshots, PDFs, recordings, downloads) instead of agent transcripts — alias of `agents browser sessions`')
|
|
3348
3629
|
.option('--no-interactive', 'Print the listing instead of opening the interactive browser (default on a TTY for the bare listing and --active)')
|
|
3349
3630
|
.option('--print-cmd', 'Print the canonical `ag sessions …` command for the given flags and exit (the twin of the browser’s `y` hotkey)')
|
|
@@ -3380,6 +3661,18 @@ export function registerSessionsCommands(program) {
|
|
|
3380
3661
|
# Export for analysis
|
|
3381
3662
|
agents sessions --since 30d --limit 200 --json > sessions.json
|
|
3382
3663
|
|
|
3664
|
+
# List indexed tool calls in recent Codex sessions on one device
|
|
3665
|
+
agents sessions --include tools --agent codex --device mac-mini --since 7d
|
|
3666
|
+
|
|
3667
|
+
# Each repeated clause must match a different call in the same session
|
|
3668
|
+
agents sessions --include tools --query 'program:git input:merge' --query 'program:gh output:CONFLICT' --fleet --json
|
|
3669
|
+
|
|
3670
|
+
# Count every pre-indexed static git site without reparsing transcripts
|
|
3671
|
+
agents sessions --include tools --query 'program:git' --count --fleet --json
|
|
3672
|
+
|
|
3673
|
+
# Explicitly populate historical tool rows once on every device
|
|
3674
|
+
agents sessions backfill tools --fleet
|
|
3675
|
+
|
|
3383
3676
|
# Resolve one historical selector to metadata only, across the fleet
|
|
3384
3677
|
agents sessions --resolve d3470b57 --json
|
|
3385
3678
|
|
|
@@ -3394,6 +3687,11 @@ export function registerSessionsCommands(program) {
|
|
|
3394
3687
|
- --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.
|
|
3395
3688
|
- --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.
|
|
3396
3689
|
- --include and --exclude are mutually exclusive.
|
|
3690
|
+
- With --include tools, repeat --query for same-session AND across distinct calls. Fields: tool, program, input, output, status, exit, error.
|
|
3691
|
+
- --count accepts exactly one program:<name> clause and reports static source occurrences, containing tool calls, and sessions.
|
|
3692
|
+
- Tool queries read SQLite only. Run 'agents sessions backfill tools' once for historical transcripts; normal scans index new and changed sessions.
|
|
3693
|
+
- Tool evidence is redacted and bounded before it reaches SQLite. --markdown and --no-redact conflict with --include tools.
|
|
3694
|
+
- Tool queries accept 32 clauses (4 KiB each), --limit 1–1,000, and at most 8 MiB of materialized evidence.
|
|
3397
3695
|
- --first and --last are mutually exclusive.
|
|
3398
3696
|
- A filter flag (--include/--exclude/--first/--last) without --markdown/--json defaults to --markdown output.
|
|
3399
3697
|
- --cloud sources from Rush Cloud captured runs instead of local disk.
|
|
@@ -3421,6 +3719,7 @@ export function registerSessionsCommands(program) {
|
|
|
3421
3719
|
registerSessionsImportCommand(sessionsCmd);
|
|
3422
3720
|
registerSessionsMigrateCommand(sessionsCmd);
|
|
3423
3721
|
registerSessionsMigrationsCommand(sessionsCmd);
|
|
3722
|
+
registerSessionsBackfillCommand(sessionsCmd);
|
|
3424
3723
|
}
|
|
3425
3724
|
function formatNoSessionsMessage(showAll, project) {
|
|
3426
3725
|
const projectQuery = project?.trim();
|
package/dist/commands/teams.js
CHANGED
|
@@ -1279,7 +1279,7 @@ export function registerTeamsCommands(program) {
|
|
|
1279
1279
|
.option('-n, --name <name>', 'Friendly name for this teammate (e.g. alice). Required if using --after. Unique within team.')
|
|
1280
1280
|
.option('-m, --mode <mode>', `Permissions: plan (read-only) | edit (can write files) | auto (smart classifier auto-approves safe ops) | skip (bypass all permission prompts). 'full' accepted as alias for skip. Teammates run headless: plan works headless on claude/codex/droid/opencode; kimi/grok/cursor/antigravity have no headless plan mode and auto-downgrade a plan request to auto.`, 'edit')
|
|
1281
1281
|
.option('-e, --effort <effort>', `Reasoning intensity: ${VALID_EFFORTS.join('|')}`, 'medium')
|
|
1282
|
-
.option('--model <model>', '
|
|
1282
|
+
.option('--model <model>', 'Cost tier (cheap|default|best|ultra) or a concrete id (e.g. claude-opus-4-8); tiers resolve per harness+version to a supported model')
|
|
1283
1283
|
.option('--env <key=value>', 'Set an environment variable for this teammate (repeatable for multiple vars)', (val, prev) => [...prev, val], [])
|
|
1284
1284
|
.option('--cwd <dir>', 'Working directory for this teammate (default: current directory)')
|
|
1285
1285
|
.option('--worktree <name>', 'Run this teammate in a dedicated git worktree (required when team has --enable-worktrees)')
|
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
* agents worktree prune -> removes every clean+merged one
|
|
12
12
|
*
|
|
13
13
|
* Worktrees live at <repo>/.agents/worktrees/<terminal-id>, on a branch
|
|
14
|
-
* named agents/<terminal-id>.
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* named agents/<terminal-id>. New branches start at the freshly-fetched
|
|
15
|
+
* origin/<default> (not local HEAD), matching lib/teams/worktree.ts and the
|
|
16
|
+
* agent-system rule that keeps coding-agent worktrees under .agents/.
|
|
17
17
|
*/
|
|
18
18
|
import type { Command } from 'commander';
|
|
19
19
|
export declare function registerWorktreeCommands(program: Command): void;
|
|
@@ -67,6 +67,25 @@ async function inspect(root, terminalId) {
|
|
|
67
67
|
}
|
|
68
68
|
return { exists, dirty, aheadOfUpstream, hasUpstream, branchMerged };
|
|
69
69
|
}
|
|
70
|
+
/** Resolve origin/HEAD → default branch name; falls back to `main`. */
|
|
71
|
+
async function defaultBranch(root) {
|
|
72
|
+
try {
|
|
73
|
+
await execFileAsync('git', ['remote', 'set-head', 'origin', '--auto'], { cwd: root });
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// offline / no origin
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
const { stdout } = await execFileAsync('git', ['symbolic-ref', '--short', 'refs/remotes/origin/HEAD'], { cwd: root });
|
|
80
|
+
const base = stdout.trim().replace(/^origin\//, '');
|
|
81
|
+
if (base)
|
|
82
|
+
return base;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// no origin/HEAD
|
|
86
|
+
}
|
|
87
|
+
return 'main';
|
|
88
|
+
}
|
|
70
89
|
async function provision(root, terminalId) {
|
|
71
90
|
const wt = worktreePathFor(root, terminalId);
|
|
72
91
|
const branch = branchNameFor(terminalId);
|
|
@@ -84,10 +103,22 @@ async function provision(root, terminalId) {
|
|
|
84
103
|
catch {
|
|
85
104
|
branchExists = false;
|
|
86
105
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
106
|
+
if (branchExists) {
|
|
107
|
+
await execFileAsync('git', ['worktree', 'add', wt, branch], { cwd: root });
|
|
108
|
+
return wt;
|
|
109
|
+
}
|
|
110
|
+
// New branch: fetch + base on origin/<default>, never local HEAD (stale trap).
|
|
111
|
+
const base = await defaultBranch(root);
|
|
112
|
+
try {
|
|
113
|
+
await execFileAsync('git', ['fetch', 'origin'], { cwd: root });
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
const detail = (err?.stderr || err?.message || String(err)).toString().trim();
|
|
117
|
+
die(`git fetch origin failed in ${root}` +
|
|
118
|
+
(detail ? `: ${detail}` : '') +
|
|
119
|
+
`. Cannot provision a worktree from a stale remote-tracking ref.`);
|
|
120
|
+
}
|
|
121
|
+
await execFileAsync('git', ['worktree', 'add', '-b', branch, wt, `origin/${base}`], { cwd: root });
|
|
91
122
|
return wt;
|
|
92
123
|
}
|
|
93
124
|
async function release(root, terminalId, force) {
|
package/dist/lib/daemon.d.ts
CHANGED
|
@@ -43,7 +43,11 @@ export declare function writeHeartbeat(pid?: number): void;
|
|
|
43
43
|
export declare function readHeartbeat(): DaemonHeartbeat | null;
|
|
44
44
|
export declare function removeHeartbeat(): void;
|
|
45
45
|
export declare function isDaemonWedged(): boolean;
|
|
46
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* Check whether a daemon is alive — via the pid file, or a fresh heartbeat when
|
|
48
|
+
* the pid file has been lost (see resolveLiveDaemonPid). Heals the pid file as a
|
|
49
|
+
* side effect so a subsequent read is consistent.
|
|
50
|
+
*/
|
|
47
51
|
export declare function isDaemonRunning(): boolean;
|
|
48
52
|
/**
|
|
49
53
|
* Single-instance claim for the daemon foreground entrypoint.
|