@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.
- package/CHANGELOG.md +101 -0
- package/README.md +32 -3
- package/dist/bin/agents +0 -0
- package/dist/commands/computer-actions.d.ts +4 -0
- package/dist/commands/computer-actions.js +35 -0
- package/dist/commands/computer.js +4 -2
- 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/perf.d.ts +16 -0
- package/dist/commands/perf.js +11 -1
- package/dist/commands/projects.d.ts +11 -1
- package/dist/commands/projects.js +38 -4
- package/dist/commands/sessions-backfill.d.ts +32 -0
- package/dist/commands/sessions-backfill.js +186 -0
- package/dist/commands/sessions-picker.js +17 -2
- package/dist/commands/sessions.d.ts +22 -1
- package/dist/commands/sessions.js +331 -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/index.js +8 -0
- package/dist/lib/browser/service.js +13 -0
- package/dist/lib/computer/dispatch.d.ts +3 -1
- package/dist/lib/computer/dispatch.js +10 -2
- 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/event-stream.d.ts +2 -0
- package/dist/lib/event-stream.js +3 -0
- package/dist/lib/events.d.ts +3 -1
- package/dist/lib/events.js +4 -2
- package/dist/lib/exec.js +39 -8
- package/dist/lib/git.d.ts +14 -0
- package/dist/lib/git.js +36 -0
- package/dist/lib/hooks/profile.js +1 -14
- 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/percentile.d.ts +12 -0
- package/dist/lib/percentile.js +24 -0
- package/dist/lib/perf/db.d.ts +1 -2
- package/dist/lib/perf/db.js +2 -14
- package/dist/lib/plugins.js +12 -1
- 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/resources.d.ts +16 -0
- package/dist/lib/resources.js +25 -14
- package/dist/lib/rotate.d.ts +84 -1
- package/dist/lib/rotate.js +155 -5
- package/dist/lib/routines.js +1 -14
- package/dist/lib/runner.d.ts +4 -2
- package/dist/lib/runner.js +21 -5
- 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 +22 -1
- package/dist/lib/session/db.js +516 -24
- package/dist/lib/session/discover.d.ts +68 -7
- package/dist/lib/session/discover.js +186 -84
- package/dist/lib/session/highlights.d.ts +24 -4
- package/dist/lib/session/highlights.js +52 -7
- package/dist/lib/session/parse.d.ts +8 -1
- package/dist/lib/session/parse.js +102 -35
- package/dist/lib/session/prompt.d.ts +19 -0
- package/dist/lib/session/prompt.js +43 -0
- 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 +49 -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 +31 -0
- package/dist/lib/types.js +17 -0
- package/package.json +3 -1
package/dist/commands/models.js
CHANGED
|
@@ -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(), '~');
|
package/dist/commands/perf.d.ts
CHANGED
|
@@ -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 {};
|
package/dist/commands/perf.js
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
/**
|
|
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'], {
|
|
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
|
-
|
|
392
|
-
|
|
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: {
|