@phnx-labs/agents-cli 1.21.2 → 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.
Files changed (102) hide show
  1. package/CHANGELOG.md +101 -0
  2. package/README.md +32 -3
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/computer-actions.d.ts +4 -0
  5. package/dist/commands/computer-actions.js +35 -0
  6. package/dist/commands/computer.js +4 -2
  7. package/dist/commands/exec.d.ts +27 -0
  8. package/dist/commands/exec.js +123 -6
  9. package/dist/commands/models.js +36 -1
  10. package/dist/commands/perf.d.ts +16 -0
  11. package/dist/commands/perf.js +11 -1
  12. package/dist/commands/projects.d.ts +11 -1
  13. package/dist/commands/projects.js +38 -4
  14. package/dist/commands/sessions-backfill.d.ts +32 -0
  15. package/dist/commands/sessions-backfill.js +186 -0
  16. package/dist/commands/sessions-picker.js +17 -2
  17. package/dist/commands/sessions.d.ts +22 -1
  18. package/dist/commands/sessions.js +331 -18
  19. package/dist/commands/teams.js +1 -1
  20. package/dist/commands/worktree.d.ts +3 -3
  21. package/dist/commands/worktree.js +35 -4
  22. package/dist/index.js +8 -0
  23. package/dist/lib/browser/service.js +13 -0
  24. package/dist/lib/computer/dispatch.d.ts +3 -1
  25. package/dist/lib/computer/dispatch.js +10 -2
  26. package/dist/lib/daemon.d.ts +5 -1
  27. package/dist/lib/daemon.js +63 -14
  28. package/dist/lib/devices/resolve-target.d.ts +6 -0
  29. package/dist/lib/devices/resolve-target.js +9 -3
  30. package/dist/lib/event-stream.d.ts +2 -0
  31. package/dist/lib/event-stream.js +3 -0
  32. package/dist/lib/events.d.ts +3 -1
  33. package/dist/lib/events.js +4 -2
  34. package/dist/lib/exec.js +39 -8
  35. package/dist/lib/git.d.ts +14 -0
  36. package/dist/lib/git.js +36 -0
  37. package/dist/lib/hooks/profile.js +1 -14
  38. package/dist/lib/hosts/dispatch.d.ts +12 -0
  39. package/dist/lib/hosts/dispatch.js +23 -6
  40. package/dist/lib/hosts/reconnect.d.ts +38 -0
  41. package/dist/lib/hosts/reconnect.js +85 -4
  42. package/dist/lib/hosts/run-target.js +14 -2
  43. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  44. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  45. package/dist/lib/model-tiers.d.ts +54 -0
  46. package/dist/lib/model-tiers.js +229 -0
  47. package/dist/lib/models.d.ts +3 -0
  48. package/dist/lib/models.js +44 -7
  49. package/dist/lib/percentile.d.ts +12 -0
  50. package/dist/lib/percentile.js +24 -0
  51. package/dist/lib/perf/db.d.ts +1 -2
  52. package/dist/lib/perf/db.js +2 -14
  53. package/dist/lib/plugins.js +12 -1
  54. package/dist/lib/pricing/prices.json +16 -1
  55. package/dist/lib/project-focus.d.ts +42 -0
  56. package/dist/lib/project-focus.js +80 -0
  57. package/dist/lib/project-schedule.d.ts +75 -0
  58. package/dist/lib/project-schedule.js +110 -0
  59. package/dist/lib/redact.d.ts +2 -0
  60. package/dist/lib/redact.js +22 -0
  61. package/dist/lib/remote-agents-json.d.ts +2 -0
  62. package/dist/lib/remote-agents-json.js +3 -3
  63. package/dist/lib/resources.d.ts +16 -0
  64. package/dist/lib/resources.js +25 -14
  65. package/dist/lib/rotate.d.ts +84 -1
  66. package/dist/lib/rotate.js +155 -5
  67. package/dist/lib/routines.js +1 -14
  68. package/dist/lib/runner.d.ts +4 -2
  69. package/dist/lib/runner.js +21 -5
  70. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  71. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  72. package/dist/lib/session/bash-command.js +60 -9
  73. package/dist/lib/session/db.d.ts +22 -1
  74. package/dist/lib/session/db.js +516 -24
  75. package/dist/lib/session/discover.d.ts +68 -7
  76. package/dist/lib/session/discover.js +186 -84
  77. package/dist/lib/session/highlights.d.ts +24 -4
  78. package/dist/lib/session/highlights.js +52 -7
  79. package/dist/lib/session/parse.d.ts +8 -1
  80. package/dist/lib/session/parse.js +102 -35
  81. package/dist/lib/session/prompt.d.ts +19 -0
  82. package/dist/lib/session/prompt.js +43 -0
  83. package/dist/lib/session/remote-list.d.ts +71 -0
  84. package/dist/lib/session/remote-list.js +410 -2
  85. package/dist/lib/session/shell-programs.d.ts +15 -0
  86. package/dist/lib/session/shell-programs.js +359 -0
  87. package/dist/lib/session/tool-calls.d.ts +88 -0
  88. package/dist/lib/session/tool-calls.js +612 -0
  89. package/dist/lib/session/tool-index.d.ts +100 -0
  90. package/dist/lib/session/tool-index.js +773 -0
  91. package/dist/lib/session/tool-store.d.ts +15 -0
  92. package/dist/lib/session/tool-store.js +198 -0
  93. package/dist/lib/session/types.d.ts +49 -0
  94. package/dist/lib/state.d.ts +10 -1
  95. package/dist/lib/state.js +11 -2
  96. package/dist/lib/teams/remoteWorktree.d.ts +3 -4
  97. package/dist/lib/teams/remoteWorktree.js +3 -4
  98. package/dist/lib/teams/worktree.d.ts +11 -1
  99. package/dist/lib/teams/worktree.js +42 -4
  100. package/dist/lib/types.d.ts +31 -0
  101. package/dist/lib/types.js +17 -0
  102. package/package.json +3 -1
@@ -11,9 +11,11 @@ import { homeDir } from '../lib/platform/index.js';
11
11
  import { resolveAgentName, formatAgentError, agentLabel, } from '../lib/agents.js';
12
12
  import { listInstalledVersions, getGlobalDefault, resolveVersion, resolveVersionAlias } from '../lib/versions.js';
13
13
  import { getModelCatalog, locateModelSource } from '../lib/models.js';
14
+ import { resolveTierMap, MODEL_TIERS } from '../lib/model-tiers.js';
15
+ import { getModelPricing } from '../lib/pricing/index.js';
14
16
  import { terminalWidth, truncateToWidth, stringWidth } from '../lib/session/width.js';
15
17
  import { wrapJoined } from './inspect.js';
16
- const MODEL_CAPABLE_AGENTS = ['claude', 'codex', 'opencode', 'cursor', 'openclaw', 'antigravity', 'kimi', 'grok'];
18
+ const MODEL_CAPABLE_AGENTS = ['claude', 'codex', 'opencode', 'cursor', 'openclaw', 'antigravity', 'kimi', 'grok', 'droid'];
17
19
  /**
18
20
  * Agents that don't necessarily install under ~/.agents/versions (cursor ships
19
21
  * via a curl script). For these, fall back to the PATH binary and synthesize
@@ -52,6 +54,7 @@ export function registerModelsCommand(program) {
52
54
  agent,
53
55
  version,
54
56
  catalog: getModelCatalog(agent, version),
57
+ tiers: resolveTierMap(agent, version),
55
58
  }));
56
59
  console.log(JSON.stringify(out, null, 2));
57
60
  return;
@@ -122,8 +125,16 @@ function printCatalog(agent, version, isDefault, options) {
122
125
  const tag = isDefault ? chalk.gray(' (default)') : '';
123
126
  const header = `${agentLabel(agent)} ${chalk.bold(version)}${tag}`;
124
127
  console.log(header);
128
+ // Cost tiers first -- the thing an orchestrating agent reads to pick a model.
129
+ printTiers(agent, version);
125
130
  const src = locateModelSource(agent, version);
126
131
  if (!src) {
132
+ if (agent === 'droid') {
133
+ // Droid has no extractable catalog (no models CLI/API/config); the curated
134
+ // tier map above is the whole surface.
135
+ console.log(chalk.gray(' (Droid has no model list command; tiers are a curated, credit-multiplier map.)'));
136
+ return;
137
+ }
127
138
  console.log(chalk.yellow(` Could not locate model source for ${agent}@${version}.`));
128
139
  console.log(chalk.gray(` Expected the agent's CLI bundle or native binary under ~/.agents/.history/versions/${agent}/${version}/.`));
129
140
  return;
@@ -171,6 +182,30 @@ function printCatalog(agent, version, isDefault, options) {
171
182
  }
172
183
  }
173
184
  }
185
+ /** Rough blended $/Mtok label for a model id, or '' when unpriced. */
186
+ function priceLabel(id) {
187
+ const p = getModelPricing(id);
188
+ if (!p)
189
+ return chalk.gray(' --');
190
+ const perM = (p.inputPerToken + p.outputPerToken) * 1e6;
191
+ return chalk.gray(` ~$${perM.toFixed(0)}/Mtok`);
192
+ }
193
+ /** Print the cheap/default/best/ultra tier map for an (agent, version). */
194
+ function printTiers(agent, version) {
195
+ const map = resolveTierMap(agent, version);
196
+ if (!MODEL_TIERS.some((t) => map[t].model))
197
+ return;
198
+ console.log(chalk.gray(' tiers:'));
199
+ for (const t of MODEL_TIERS) {
200
+ const r = map[t];
201
+ if (!r.model)
202
+ continue;
203
+ const eff = r.effort ? chalk.gray(` @${r.effort}`) : '';
204
+ const clamp = r.clampedFrom ? chalk.gray(' (clamped)') : '';
205
+ console.log(` ${chalk.cyan(t.padEnd(8))} ${chalk.bold(r.model)}${eff}${priceLabel(r.model)}${clamp}`);
206
+ }
207
+ console.log();
208
+ }
174
209
  /** Abbreviate a path by replacing the home directory with ~. */
175
210
  function shortPath(p) {
176
211
  return p.replace(homeDir(), '~');
@@ -14,6 +14,13 @@
14
14
  import type { Command } from 'commander';
15
15
  import { type PerfAggregateRow } from '../lib/perf/db.js';
16
16
  import { type HookProfileRow } from '../lib/hooks/profile.js';
17
+ interface PerfGlobalOpts {
18
+ days?: string;
19
+ warnMs?: string;
20
+ json?: boolean;
21
+ limit?: string;
22
+ project?: string;
23
+ }
17
24
  /** Map warehouse rows shaped like hook.fire into the existing HookProfileRow UI. */
18
25
  export declare function asHookRows(rows: PerfAggregateRow[]): HookProfileRow[];
19
26
  export declare function renderHookTable(rows: HookProfileRow[], warnMs: number): void;
@@ -24,4 +31,13 @@ export declare function renderHookTable(rows: HookProfileRow[], warnMs: number):
24
31
  * hit ignores it (a caller filtering by project has no legacy rows to miss).
25
32
  */
26
33
  export declare function loadHookProfile(days: number, project?: string): HookProfileRow[];
34
+ /**
35
+ * Sessions stuck repeatedly hitting the SAME guard block (git-guard,
36
+ * rm-guard, git-require-clean-tree, …) instead of adapting after the first
37
+ * denial. Reads the `friction` event sink (emitFriction in events.ts) that
38
+ * guard hooks self-report into via `agents _internal friction` before they
39
+ * exit 2 — see lib/friction-heuristics.ts for the grouping.
40
+ */
41
+ export declare function frictionAction(opts: PerfGlobalOpts): void;
27
42
  export declare function registerPerfCommand(program: Command): void;
43
+ export {};
@@ -175,7 +175,17 @@ function runAction(opts) {
175
175
  * guard hooks self-report into via `agents _internal friction` before they
176
176
  * exit 2 — see lib/friction-heuristics.ts for the grouping.
177
177
  */
178
- function frictionAction(opts) {
178
+ export function frictionAction(opts) {
179
+ // --project is declared on the shared `perf` parent for hooks/commands/run,
180
+ // where every sample carries a cwd. friction events (emitFriction in
181
+ // events.ts) don't carry one today — agents _internal friction has no
182
+ // --cwd flag — so silently accepting the flag here would look like it
183
+ // filtered when it did nothing. Fail loud instead of no-op.
184
+ if (opts.project) {
185
+ console.error(chalk.red("agents perf friction does not support --project yet — friction events carry no cwd to filter on."));
186
+ process.exitCode = 1;
187
+ return;
188
+ }
179
189
  const days = parseDays(opts.days);
180
190
  const startDate = new Date(Date.now() - days * 86_400_000);
181
191
  const events = query({ eventTypes: ['friction'], startDate });
@@ -20,7 +20,17 @@ export declare const PROJECTS_NO_FANOUT_ENV = "AGENTS_PROJECTS_LOCAL";
20
20
  * string when everything answered.
21
21
  */
22
22
  export declare function formatFleetSkippedNote(skipped: string[]): string;
23
- /** Best-effort `owner/repo` from a repo's origin remote. */
23
+ /**
24
+ * Best-effort `owner/repo` from a repo's origin remote.
25
+ *
26
+ * `stderr: 'ignore'` is load-bearing, not tidiness. A checkout with no origin
27
+ * makes git print `error: No such remote 'origin'` on ITS stderr, which is the
28
+ * terminal's — the catch below never sees it. That was invisible while this was
29
+ * called once from `add` inside a real repo, and became noise the moment
30
+ * `import --from-factory` started calling it per row: importing 12 registry
31
+ * rows printed two raw git errors between the progress lines. Absence of a
32
+ * remote is an expected answer here (`undefined`), not something to report.
33
+ */
24
34
  export declare function originSlug(cwd: string): string | undefined;
25
35
  /** One `projects list` row, pre-render. */
26
36
  export interface ProjectListRow {
@@ -29,6 +29,8 @@ import { rollupSessionsByProject, liveDeadSplit, enrichProjectSignals, formatPro
29
29
  import { fetchLinearProjectCounts } from '../lib/linear-project-counts.js';
30
30
  import { listLinearProjects, pickLinearProject } from '../lib/linear-projects.js';
31
31
  import { checkRepoSlug } from '../lib/project-doctor.js';
32
+ import { readFocusAreas } from '../lib/project-focus.js';
33
+ import { formatVerdict, scheduleVerdict } from '../lib/project-schedule.js';
32
34
  import { buildFactoryImportCandidates, buildLinearImportCandidates, validateImportOpts, } from '../lib/project-import.js';
33
35
  /** Recursion guard: a peer answering a probe fan-out never re-fans-out itself. */
34
36
  export const PROJECTS_NO_FANOUT_ENV = 'AGENTS_PROJECTS_LOCAL';
@@ -57,10 +59,24 @@ function parseContextFlag(raw) {
57
59
  return { path: raw.trim(), purpose: '' };
58
60
  return { path: raw.slice(0, i).trim(), purpose: raw.slice(i + 1).trim() };
59
61
  }
60
- /** Best-effort `owner/repo` from a repo's origin remote. */
62
+ /**
63
+ * Best-effort `owner/repo` from a repo's origin remote.
64
+ *
65
+ * `stderr: 'ignore'` is load-bearing, not tidiness. A checkout with no origin
66
+ * makes git print `error: No such remote 'origin'` on ITS stderr, which is the
67
+ * terminal's — the catch below never sees it. That was invisible while this was
68
+ * called once from `add` inside a real repo, and became noise the moment
69
+ * `import --from-factory` started calling it per row: importing 12 registry
70
+ * rows printed two raw git errors between the progress lines. Absence of a
71
+ * remote is an expected answer here (`undefined`), not something to report.
72
+ */
61
73
  export function originSlug(cwd) {
62
74
  try {
63
- const url = execFileSync('git', ['remote', 'get-url', 'origin'], { cwd, encoding: 'utf8' }).trim();
75
+ const url = execFileSync('git', ['remote', 'get-url', 'origin'], {
76
+ cwd,
77
+ encoding: 'utf8',
78
+ stdio: ['ignore', 'pipe', 'ignore'],
79
+ }).trim();
64
80
  return parseOwnerRepoFromRemote(url) ?? undefined;
65
81
  }
66
82
  catch {
@@ -254,7 +270,9 @@ function statusBar(r) {
254
270
  }
255
271
  function renderCard(def, r, remote, fleet, linear, nowMs = Date.now(),
256
272
  /** How many milestones to print. `status` shows the next one; `view` shows all. */
257
- milestoneLimit = 1) {
273
+ milestoneLimit = 1,
274
+ /** Directories the window's work landed in, from local git. */
275
+ focus = []) {
258
276
  // The headline counts LIVE agents. It used to be every matched session, which
259
277
  // read `39 agents` on a project where 19 had crashed. `planPct` used to sit
260
278
  // here too and is gone: it summed each session's latest checklist snapshot,
@@ -290,6 +308,14 @@ milestoneLimit = 1) {
290
308
  for (const line of formatMilestoneLines(linear?.milestones ?? [], linear?.nextMilestone, nowMs, milestoneLimit)) {
291
309
  console.log(line);
292
310
  }
311
+ // What the dates prove — never an invented "on track". See project-schedule.ts.
312
+ const verdict = linear?.milestones?.length ? formatVerdict(scheduleVerdict(linear.milestones, nowMs)) : undefined;
313
+ if (verdict) {
314
+ console.log(` ${chalk.dim('schedule')} ${verdict.warn ? chalk.yellow(verdict.text) : verdict.text}`);
315
+ }
316
+ if (focus.length) {
317
+ console.log(` ${chalk.dim('focus')} ${focus.map((f) => `${f.path} ${chalk.dim(String(f.touches))}`).join(chalk.dim(' · '))}`);
318
+ }
293
319
  if (r && r.tickets.length) {
294
320
  console.log(` ${chalk.dim('tickets')} ${r.tickets.slice(0, 8).join(' · ')}${r.tickets.length > 8 ? ' …' : ''}`);
295
321
  }
@@ -569,6 +595,8 @@ export function registerProjectsCommands(program) {
569
595
  // log but skips the gh calls + Linear counts (both are network).
570
596
  const remote = new Map();
571
597
  const linear = new Map();
598
+ // Local git, no API, no rate limit — measured 0.23s over a 897-commit week.
599
+ const focus = new Map();
572
600
  await Promise.all(defs.map(async (d) => {
573
601
  const skipRemote = opts.remote === false;
574
602
  const [sig, counts] = await Promise.all([
@@ -580,6 +608,8 @@ export function registerProjectsCommands(program) {
580
608
  remote.set(d.name, sig);
581
609
  if (counts)
582
610
  linear.set(d.name, counts);
611
+ if (d.root)
612
+ focus.set(d.name, await readFocusAreas(expandLocalHome(d.root), windowDays));
583
613
  }));
584
614
  /** This def's slice of the fleet probe, in its own target order. */
585
615
  const fleetFor = (d) => {
@@ -598,6 +628,10 @@ export function registerProjectsCommands(program) {
598
628
  byStatus: r?.byStatus ?? {},
599
629
  members: r?.members ?? [],
600
630
  plan: r?.plan ?? { done: 0, total: 0 },
631
+ schedule: linear.get(d.name)?.milestones?.length
632
+ ? scheduleVerdict(linear.get(d.name).milestones, nowMs)
633
+ : null,
634
+ focus: focus.get(d.name) ?? [],
601
635
  live: r ? liveDeadSplit(r.byStatus).live : 0,
602
636
  dead: r ? liveDeadSplit(r.byStatus).dead : 0,
603
637
  openPrs: r?.openPrs ?? [],
@@ -616,7 +650,7 @@ export function registerProjectsCommands(program) {
616
650
  return;
617
651
  }
618
652
  for (const d of defs) {
619
- renderCard(d, roll.get(d.name), remote.get(d.name), opts.fleet ? fleetFor(d) : undefined, linear.get(d.name), nowMs);
653
+ renderCard(d, roll.get(d.name), remote.get(d.name), opts.fleet ? fleetFor(d) : undefined, linear.get(d.name), nowMs, 1, focus.get(d.name) ?? []);
620
654
  }
621
655
  if (fleetSkipped.length > 0)
622
656
  process.stdout.write(formatFleetSkippedNote(fleetSkipped));
@@ -0,0 +1,32 @@
1
+ import type { Command } from 'commander';
2
+ import { type ToolIndexCoverage } from '../lib/session/tool-index.js';
3
+ export interface ToolBackfillMachineResult {
4
+ machine: string;
5
+ indexedFiles: number;
6
+ indexedCalls: number;
7
+ coverage: ToolIndexCoverage;
8
+ }
9
+ export interface ToolBackfillEnvelope {
10
+ schemaVersion: 1;
11
+ kind: 'tools-backfill';
12
+ generatedAt: string;
13
+ complete: boolean;
14
+ machines: ToolBackfillMachineResult[];
15
+ }
16
+ interface ToolBackfillOptions {
17
+ agent?: string;
18
+ project?: string;
19
+ since?: string;
20
+ until?: string;
21
+ unmanaged?: boolean;
22
+ teams?: boolean;
23
+ json?: boolean;
24
+ local?: boolean;
25
+ fleet?: boolean;
26
+ host?: string[] | string;
27
+ device?: string[] | string;
28
+ }
29
+ export declare function backfillToolsLocal(options: ToolBackfillOptions, oneBatch?: boolean): Promise<ToolBackfillMachineResult>;
30
+ export declare function runToolsBackfill(options: ToolBackfillOptions): Promise<ToolBackfillEnvelope>;
31
+ export declare function registerSessionsBackfillCommand(sessionsCmd: Command): void;
32
+ export {};
@@ -0,0 +1,186 @@
1
+ import chalk from 'chalk';
2
+ import { setHelpSections } from '../lib/help.js';
3
+ import { gatherRemoteAgentsJson } from '../lib/remote-agents-json.js';
4
+ import { discoverSessions } from '../lib/session/discover.js';
5
+ import { machineId } from '../lib/session/sync/config.js';
6
+ import { SESSION_AGENTS } from '../lib/session/types.js';
7
+ import { ensureToolIndex, readToolIndexCoverage } from '../lib/session/tool-index.js';
8
+ import { NO_FANOUT_ENV } from '../lib/session/remote-active.js';
9
+ const BACKFILL_REMOTE_TIMEOUT_MS = 10 * 60_000;
10
+ function parseAgent(value) {
11
+ if (!value)
12
+ return {};
13
+ const separator = value.indexOf('@');
14
+ const name = (separator < 0 ? value : value.slice(0, separator));
15
+ if (!SESSION_AGENTS.includes(name))
16
+ throw new Error(`Unknown session agent: ${name}`);
17
+ return { agent: name, version: separator < 0 ? undefined : value.slice(separator + 1) || undefined };
18
+ }
19
+ function mergeHosts(options) {
20
+ const values = [options.host, options.device].flatMap((value) => value === undefined ? [] : Array.isArray(value) ? value : [value]);
21
+ return [...new Set(values)];
22
+ }
23
+ export async function backfillToolsLocal(options, oneBatch = false) {
24
+ const { agent, version } = parseAgent(options.agent);
25
+ const sessions = await discoverSessions({
26
+ agent,
27
+ version,
28
+ project: options.project,
29
+ since: options.since,
30
+ until: options.until,
31
+ all: true,
32
+ unbounded: true,
33
+ skipExistenceCheck: true,
34
+ includeUnmanaged: options.unmanaged,
35
+ excludeTeamOrigin: !options.teams,
36
+ });
37
+ let indexedFiles = 0;
38
+ let indexedCalls = 0;
39
+ for (;;) {
40
+ const batch = await ensureToolIndex(sessions);
41
+ indexedFiles += batch.indexedFiles;
42
+ indexedCalls += batch.indexedCalls;
43
+ if (oneBatch || batch.remainingFiles === 0 || batch.indexedFiles === 0)
44
+ break;
45
+ }
46
+ return {
47
+ machine: machineId(),
48
+ indexedFiles,
49
+ indexedCalls,
50
+ coverage: readToolIndexCoverage(sessions),
51
+ };
52
+ }
53
+ function parseRemoteBackfill(stdout, machine) {
54
+ try {
55
+ const envelope = JSON.parse(stdout);
56
+ if (envelope.schemaVersion !== 1 || envelope.kind !== 'tools-backfill'
57
+ || !Array.isArray(envelope.machines) || envelope.machines.length !== 1) {
58
+ return { items: [], valid: false };
59
+ }
60
+ const item = envelope.machines[0];
61
+ const coverage = item.coverage;
62
+ const coverageKeys = ['indexedFiles', 'indexedCalls', 'skippedFiles', 'limitedFiles', 'remainingFiles'];
63
+ if (!coverage || typeof coverage.complete !== 'boolean'
64
+ || !Number.isSafeInteger(item.indexedFiles) || item.indexedFiles < 0
65
+ || !Number.isSafeInteger(item.indexedCalls) || item.indexedCalls < 0
66
+ || coverageKeys.some((key) => !Number.isSafeInteger(coverage[key]) || coverage[key] < 0)) {
67
+ return { items: [], valid: false };
68
+ }
69
+ return { items: [{ ...item, machine }], valid: true };
70
+ }
71
+ catch {
72
+ return { items: [], valid: false };
73
+ }
74
+ }
75
+ function peerArgs(options) {
76
+ const args = ['sessions', 'backfill', 'tools', '--json', '--local'];
77
+ if (options.agent)
78
+ args.push('--agent', options.agent);
79
+ if (options.project)
80
+ args.push('--project', options.project);
81
+ if (options.since)
82
+ args.push('--since', options.since);
83
+ if (options.until)
84
+ args.push('--until', options.until);
85
+ if (options.unmanaged)
86
+ args.push('--unmanaged');
87
+ if (options.teams)
88
+ args.push('--teams');
89
+ return args;
90
+ }
91
+ export async function runToolsBackfill(options) {
92
+ const hosts = mergeHosts(options);
93
+ const includeLocal = options.local === true || hosts.length === 0;
94
+ const fanOut = options.local !== true && (options.fleet === true || hosts.length > 0);
95
+ if (!fanOut) {
96
+ const machines = includeLocal
97
+ ? [await backfillToolsLocal(options, process.env[NO_FANOUT_ENV] === '1')]
98
+ : [];
99
+ return {
100
+ schemaVersion: 1,
101
+ kind: 'tools-backfill',
102
+ generatedAt: new Date().toISOString(),
103
+ complete: machines.every((machine) => machine.coverage.complete),
104
+ machines,
105
+ };
106
+ }
107
+ const machineTotals = new Map();
108
+ let unreachable = false;
109
+ for (;;) {
110
+ const localPromise = includeLocal ? backfillToolsLocal(options, true) : Promise.resolve(undefined);
111
+ const [local, remote] = await Promise.all([localPromise, gatherRemoteAgentsJson({
112
+ args: peerArgs(options),
113
+ noFanoutEnv: NO_FANOUT_ENV,
114
+ hosts: hosts.length > 0 ? hosts : undefined,
115
+ timeoutMs: BACKFILL_REMOTE_TIMEOUT_MS,
116
+ parse: parseRemoteBackfill,
117
+ })]);
118
+ const round = [local, ...remote.items].filter((item) => item !== undefined);
119
+ for (const item of round) {
120
+ const prior = machineTotals.get(item.machine);
121
+ machineTotals.set(item.machine, {
122
+ ...item,
123
+ indexedFiles: (prior?.indexedFiles ?? 0) + item.indexedFiles,
124
+ indexedCalls: (prior?.indexedCalls ?? 0) + item.indexedCalls,
125
+ });
126
+ }
127
+ unreachable = remote.discoveryFailed || remote.skipped.length > 0 || remote.parseFailed.length > 0;
128
+ const progressed = round.reduce((sum, item) => sum + item.indexedFiles, 0);
129
+ if (unreachable || progressed === 0 || (round.length > 0 && round.every((item) => item.coverage.complete)))
130
+ break;
131
+ }
132
+ const machines = [...machineTotals.values()];
133
+ return {
134
+ schemaVersion: 1,
135
+ kind: 'tools-backfill',
136
+ generatedAt: new Date().toISOString(),
137
+ complete: !unreachable && machines.every((machine) => machine.coverage.complete),
138
+ machines,
139
+ };
140
+ }
141
+ export function registerSessionsBackfillCommand(sessionsCmd) {
142
+ const backfill = sessionsCmd.command('backfill').description('Populate derived session data explicitly.');
143
+ const tools = backfill.command('tools').description('Parse historical tool calls once into the local SQLite index.');
144
+ setHelpSections(tools, {
145
+ examples: `
146
+ # Backfill this machine once; reruns resume and skip completed transcripts
147
+ agents sessions backfill tools
148
+
149
+ # Backfill every online compute device, keeping each index local
150
+ agents sessions backfill tools --fleet --json
151
+
152
+ # Narrow the historical work
153
+ agents sessions backfill tools --agent codex --since 7d
154
+ `,
155
+ notes: `
156
+ - This command is the only historical transcript parse for tool indexing. Tool queries never trigger it.
157
+ - New and changed sessions are indexed during their normal incremental scan.
158
+ - No embeddings, vector database, network model, or semantic processing is used.
159
+ `,
160
+ });
161
+ tools.action(async (_options, command) => {
162
+ const options = command.optsWithGlobals();
163
+ if (options.local && (options.fleet || mergeHosts(options).length > 0)) {
164
+ console.error(chalk.red('--local cannot be combined with --fleet, --host, or --device.'));
165
+ process.exitCode = 1;
166
+ return;
167
+ }
168
+ try {
169
+ const envelope = await runToolsBackfill(options);
170
+ if (options.json)
171
+ process.stdout.write(JSON.stringify(envelope, null, 2) + '\n');
172
+ else {
173
+ for (const machine of envelope.machines) {
174
+ console.log(`${machine.machine}: indexed ${machine.indexedFiles.toLocaleString()} transcript${machine.indexedFiles === 1 ? '' : 's'} and ${machine.indexedCalls.toLocaleString()} tool call${machine.indexedCalls === 1 ? '' : 's'}.`);
175
+ }
176
+ if (!envelope.complete) {
177
+ console.log(chalk.yellow('Backfill is partial; rerun the command after resolving skipped or limited transcripts.'));
178
+ }
179
+ }
180
+ }
181
+ catch (error) {
182
+ console.error(chalk.red(error instanceof Error ? error.message : String(error)));
183
+ process.exitCode = 1;
184
+ }
185
+ });
186
+ }
@@ -374,6 +374,13 @@ function formatCompactPreview(events, session) {
374
374
  let latestTodos;
375
375
  let subAgentCount = 0;
376
376
  const toolTags = new Set();
377
+ // usedBrowser/usedComputer are computed at scan time from a sessionId-scoped
378
+ // events-log read (session/db.ts detectToolUsage), NOT a transcript regex —
379
+ // undefined means a legacy row this scanner hasn't computed the field for
380
+ // yet, so only THEN does classifySessionTool's transcript-derived guess run
381
+ // below (mirrors directoriesTouched's prefer-persisted/fall-back-to-derived
382
+ // pattern for recentDirectoriesTouched).
383
+ const knownToolUsage = session.usedBrowser !== undefined;
377
384
  for (const event of events) {
378
385
  if (event.type === 'message') {
379
386
  if (event.role === 'user' && !firstUser && event.content) {
@@ -388,8 +395,10 @@ function formatCompactPreview(events, session) {
388
395
  else if (event.type === 'tool_use' && !event._local) {
389
396
  const tool = event.tool || '';
390
397
  const command = event.command || '';
391
- for (const tag of classifySessionTool(tool, command))
392
- toolTags.add(tag);
398
+ if (!knownToolUsage) {
399
+ for (const tag of classifySessionTool(tool, command))
400
+ toolTags.add(tag);
401
+ }
393
402
  if (isSubAgentTool(tool, command))
394
403
  subAgentCount++;
395
404
  const p = event.path || event.args?.file_path || event.args?.path || '';
@@ -412,6 +421,12 @@ function formatCompactPreview(events, session) {
412
421
  toolCalls++;
413
422
  }
414
423
  }
424
+ // Persisted field wins when computed — it comes from real browser.navigate/
425
+ // browser.screenshot/computer.action events, not a fuzzy tool-name regex.
426
+ if (session.usedBrowser)
427
+ toolTags.add('browser');
428
+ if (session.usedComputer)
429
+ toolTags.add('computer');
415
430
  // Prefer the transcript-derived list when we just re-parsed the file (freshest
416
431
  // checklist write); fall back to SessionMeta.todos for rows where the scan/
417
432
  // fan-out attached progress but the event stream has no TodoWrite yet.
@@ -4,6 +4,7 @@ import type { SessionAgentId, SessionMeta, ViewMode } from '../lib/session/types
4
4
  import { type ActiveSession } from '../lib/session/active.js';
5
5
  import { gatherRemoteList, runOnPeer } from '../lib/session/remote-list.js';
6
6
  import { type PickedSession } from './sessions-picker.js';
7
+ import { type ToolSearchEnvelope, type ToolProgramCountEnvelope } from '../lib/session/tool-index.js';
7
8
  interface SessionFilterOptions {
8
9
  agent?: string;
9
10
  project?: string;
@@ -17,7 +18,7 @@ interface SessionFilterOptions {
17
18
  interface SessionsOptions extends SessionFilterOptions {
18
19
  /** Also list sessions from the user's own unmanaged ~/.<agent> installs. */
19
20
  unmanaged?: boolean;
20
- query?: string;
21
+ query?: string[];
21
22
  /** Resolve one historical selector to metadata only (requires --json). */
22
23
  resolve?: string;
23
24
  /** Versioned internal peer protocol; old/unsafe peers must reject it. */
@@ -55,6 +56,10 @@ interface SessionsOptions extends SessionFilterOptions {
55
56
  local?: boolean;
56
57
  /** --device <target...> — alias for --host; resolves against the device registry. */
57
58
  device?: string[];
59
+ /** Query every registered online compute device and merge tool evidence. */
60
+ fleet?: boolean;
61
+ /** Aggregate static program sites instead of returning matching call evidence. */
62
+ count?: boolean;
58
63
  /** Per-agent shorthands: aliases for `--agent <name>` (prioritized harnesses). */
59
64
  claude?: boolean;
60
65
  codex?: boolean;
@@ -70,6 +75,11 @@ interface SessionsOptions extends SessionFilterOptions {
70
75
  printCmd?: boolean;
71
76
  /** Print a compact preview of the matched session and exit (no pager). */
72
77
  preview?: boolean;
78
+ /** Only sessions that invoked this skill (#12) — matches a bare name or a
79
+ * namespaced plugin skill's short name (`--skill design` finds `rush:design`). */
80
+ skill?: string;
81
+ /** Only sessions that used a skill/command owned by this plugin (#12). */
82
+ plugin?: string;
73
83
  }
74
84
  /**
75
85
  * Strip terminal/harness noise from a preview so the column stays a single line
@@ -333,6 +343,15 @@ export declare function renderSessionPreview(query: string, scope: {
333
343
  * attached", so those two spell it out.
334
344
  */
335
345
  export declare function formatLiveStatusHeadline(live: ActiveSession | undefined, favorite?: boolean): string;
346
+ /** Merge local and peer envelopes without changing the versioned JSON shape. */
347
+ export declare function mergeToolSearchEnvelopes(local: ToolSearchEnvelope, remotes: ToolSearchEnvelope[]): ToolSearchEnvelope;
348
+ export declare function mergeToolProgramCountEnvelopes(local: ToolProgramCountEnvelope, remotes: ToolProgramCountEnvelope[]): ToolProgramCountEnvelope;
349
+ /** Partition a fleet query by transcript origin so synced mirrors cannot duplicate results. */
350
+ export declare function toolOriginSessions(sessions: SessionMeta[], machine: string, originOnly: boolean): SessionMeta[];
351
+ export declare function printToolProgramCount(envelope: ToolProgramCountEnvelope): void;
352
+ export declare function toolSearchFleetSortError(sort: string | undefined, spansDevices: boolean): string | undefined;
353
+ /** Compact grouped evidence for humans; JSON retains every bounded field. */
354
+ export declare function printToolSearch(envelope: ToolSearchEnvelope): void;
336
355
  /**
337
356
  * Whether a session belongs to `team`, from either end: it spawned the team, or
338
357
  * it is one of the team's teammates. Case-insensitive, matching the SQL
@@ -600,6 +619,8 @@ export declare function fleetCandidatesByQuery(rows: SessionMeta[], query: strin
600
619
  /** Fixed peer argv for the metadata resolver. Scope flags compose identically on
601
620
  * every host; `--all` removes the SSH login cwd/time window, not agent/project filters. */
602
621
  export declare function metadataResolveForwardedArgs(selector: string, scope: Pick<SessionFilterOptions, 'agent' | 'project'>): string[];
622
+ /** Build the local-only peer argv for a distributed tool search. */
623
+ export declare function toolSearchForwardedArgs(argv: string[], hosts: string[]): string[];
603
624
  /** Resolution must fail closed when any selected peer did not answer. Choosing
604
625
  * from a partial fleet can turn an unseen candidate into a false unique match. */
605
626
  export declare function metadataResolveOutcome(localMatches: SessionMeta[], remote: {