@phnx-labs/agents-cli 1.21.3 → 1.22.1
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 +136 -0
- package/README.md +32 -3
- package/dist/bin/agents +0 -0
- package/dist/commands/computer-actions.js +1 -0
- package/dist/commands/doctor.js +34 -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/projects.js +120 -62
- 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/app-bundle-install.d.ts +17 -0
- package/dist/lib/app-bundle-install.js +94 -0
- package/dist/lib/daemon.d.ts +5 -1
- package/dist/lib/daemon.js +63 -14
- package/dist/lib/devices/doctor-findings.js +12 -4
- package/dist/lib/devices/doctor-overview-cache.d.ts +45 -0
- package/dist/lib/devices/doctor-overview-cache.js +168 -0
- package/dist/lib/devices/fleet.js +7 -2
- package/dist/lib/devices/resolve-target.d.ts +6 -0
- package/dist/lib/devices/resolve-target.js +9 -3
- package/dist/lib/devices/self-host.d.ts +9 -0
- package/dist/lib/devices/self-host.js +61 -0
- 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/passthrough.js +8 -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/menubar/install-menubar.js +27 -23
- 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/project-status.d.ts +7 -0
- package/dist/lib/project-status.js +9 -0
- package/dist/lib/projects.d.ts +11 -2
- package/dist/lib/projects.js +57 -9
- 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/secrets/install-helper.js +28 -31
- 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/dist/lib/versions.js +69 -22
- 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(), '~');
|
|
@@ -25,10 +25,12 @@ import { gatherRemoteAgentsJson } from '../lib/remote-agents-json.js';
|
|
|
25
25
|
import { factoryProjectsPath } from '../lib/auto-dispatch.js';
|
|
26
26
|
import { formatFleetWorkspaces, parseRemoteProbe, probeProjectWorkspaces, workspaceTargetsForDef, } from '../lib/project-probe.js';
|
|
27
27
|
import { listProjectDefs, loadProjectDef, writeProjectDef, removeProjectDef, projectDefPath, isSafeProjectName, } from '../lib/projects.js';
|
|
28
|
-
import { rollupSessionsByProject, liveDeadSplit, enrichProjectSignals, formatProjectMembers, } from '../lib/project-status.js';
|
|
28
|
+
import { rollupSessionsByProject, isDeadStatus, liveDeadSplit, enrichProjectSignals, formatProjectMembers, } from '../lib/project-status.js';
|
|
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';
|
|
@@ -266,9 +268,45 @@ function statusBar(r) {
|
|
|
266
268
|
parts.push(chalk.gray(`+${live - shown} other`));
|
|
267
269
|
return parts.join(' · ') || chalk.gray('no live agents');
|
|
268
270
|
}
|
|
271
|
+
/**
|
|
272
|
+
* Gather the live/remote/Linear/focus signals for the projects about to be
|
|
273
|
+
* rendered. `status` and `view` share this deliberately: `view` used to build
|
|
274
|
+
* its own thinner picture, so the command you open to learn everything about
|
|
275
|
+
* ONE project showed strictly less than the roll-up across all of them — no
|
|
276
|
+
* agents, no ships, no focus, no schedule verdict. One gatherer means a signal
|
|
277
|
+
* added for either surface appears on both.
|
|
278
|
+
*
|
|
279
|
+
* Only the shown projects are enriched. `skipRemote` still reads the local
|
|
280
|
+
* artifact log and git focus, and skips just the network calls (`gh`, Linear).
|
|
281
|
+
*/
|
|
282
|
+
async function enrichProjectsForRender(defs, all, opts) {
|
|
283
|
+
const roll = rollupSessionsByProject(all, [...(await getActiveSessions()), ...(opts.extraSessions ?? [])]);
|
|
284
|
+
const remote = new Map();
|
|
285
|
+
const linear = new Map();
|
|
286
|
+
// Local git, no API, no rate limit — measured 0.23s over a 897-commit week.
|
|
287
|
+
const focus = new Map();
|
|
288
|
+
await Promise.all(defs.map(async (d) => {
|
|
289
|
+
const [sig, counts] = await Promise.all([
|
|
290
|
+
enrichProjectSignals(d, opts.windowDays, opts.nowMs, { skipRemote: opts.skipRemote }),
|
|
291
|
+
!opts.skipRemote && d.linear?.projectId
|
|
292
|
+
? fetchLinearProjectCounts(d.linear.projectId)
|
|
293
|
+
: Promise.resolve(undefined),
|
|
294
|
+
]);
|
|
295
|
+
remote.set(d.name, sig);
|
|
296
|
+
if (counts)
|
|
297
|
+
linear.set(d.name, counts);
|
|
298
|
+
if (d.root)
|
|
299
|
+
focus.set(d.name, await readFocusAreas(expandLocalHome(d.root), opts.windowDays));
|
|
300
|
+
}));
|
|
301
|
+
return { roll, remote, linear, focus };
|
|
302
|
+
}
|
|
269
303
|
function renderCard(def, r, remote, fleet, linear, nowMs = Date.now(),
|
|
270
304
|
/** How many milestones to print. `status` shows the next one; `view` shows all. */
|
|
271
|
-
milestoneLimit = 1
|
|
305
|
+
milestoneLimit = 1,
|
|
306
|
+
/** Directories the window's work landed in, from local git. */
|
|
307
|
+
focus = [],
|
|
308
|
+
/** `view` mode: the caller prints the stored definition in full afterwards. */
|
|
309
|
+
detail = false) {
|
|
272
310
|
// The headline counts LIVE agents. It used to be every matched session, which
|
|
273
311
|
// read `39 agents` on a project where 19 had crashed. `planPct` used to sit
|
|
274
312
|
// here too and is gone: it summed each session's latest checklist snapshot,
|
|
@@ -284,8 +322,12 @@ milestoneLimit = 1) {
|
|
|
284
322
|
const detail = split.deadByStatus.map((d) => `${d.n} ${d.status}`).join(', ');
|
|
285
323
|
console.log(` ${chalk.dim('dead')} ${chalk.yellow(`${split.dead} finished or lost`)} ${chalk.dim(`(${detail})`)}`);
|
|
286
324
|
}
|
|
287
|
-
|
|
288
|
-
|
|
325
|
+
// Live only. The roster used to list every matched session, so a card headed
|
|
326
|
+
// `23 live` went on to show `crashed ×25` — the corpses the `dead` row above
|
|
327
|
+
// already accounts for, counted twice and contradicting the headline.
|
|
328
|
+
const liveMembers = r?.members.filter((m) => !isDeadStatus(m.status)) ?? [];
|
|
329
|
+
if (liveMembers.length)
|
|
330
|
+
console.log(` ${chalk.dim('agents')} ${formatProjectMembers(liveMembers)}`);
|
|
289
331
|
const ships = [];
|
|
290
332
|
if (remote?.mergedPrs) {
|
|
291
333
|
ships.push(chalk.green(`${remote.mergedPrs}${remote.mergedPrsTruncated ? '+' : ''} merged (${remote.windowDays}d)`));
|
|
@@ -304,6 +346,14 @@ milestoneLimit = 1) {
|
|
|
304
346
|
for (const line of formatMilestoneLines(linear?.milestones ?? [], linear?.nextMilestone, nowMs, milestoneLimit)) {
|
|
305
347
|
console.log(line);
|
|
306
348
|
}
|
|
349
|
+
// What the dates prove — never an invented "on track". See project-schedule.ts.
|
|
350
|
+
const verdict = linear?.milestones?.length ? formatVerdict(scheduleVerdict(linear.milestones, nowMs)) : undefined;
|
|
351
|
+
if (verdict) {
|
|
352
|
+
console.log(` ${chalk.dim('schedule')} ${verdict.warn ? chalk.yellow(verdict.text) : verdict.text}`);
|
|
353
|
+
}
|
|
354
|
+
if (focus.length) {
|
|
355
|
+
console.log(` ${chalk.dim('focus')} ${focus.map((f) => `${f.path} ${chalk.dim(String(f.touches))}`).join(chalk.dim(' · '))}`);
|
|
356
|
+
}
|
|
307
357
|
if (r && r.tickets.length) {
|
|
308
358
|
console.log(` ${chalk.dim('tickets')} ${r.tickets.slice(0, 8).join(' · ')}${r.tickets.length > 8 ? ' …' : ''}`);
|
|
309
359
|
}
|
|
@@ -331,13 +381,16 @@ milestoneLimit = 1) {
|
|
|
331
381
|
console.log(` ${chalk.yellow('!')} ${chalk.yellow(mismatch.message)}`);
|
|
332
382
|
console.log(` ${' '.repeat(8)}${chalk.gray(mismatch.remediation)}`);
|
|
333
383
|
}
|
|
334
|
-
|
|
384
|
+
// `view` prints these in full (path + purpose, label + URL) right below, so
|
|
385
|
+
// the compact one-line summaries would just say the same thing twice.
|
|
386
|
+
if (!detail && def.contexts?.length) {
|
|
335
387
|
console.log(` ${chalk.dim('context')} ${def.contexts.map((c) => c.path).join(' · ')}`);
|
|
336
388
|
}
|
|
337
|
-
if (def.integrations?.length) {
|
|
389
|
+
if (!detail && def.integrations?.length) {
|
|
338
390
|
console.log(` ${chalk.dim('links')} ${def.integrations.map((i) => i.label ?? i.kind).join(' · ')}`);
|
|
339
391
|
}
|
|
340
|
-
|
|
392
|
+
if (!detail)
|
|
393
|
+
console.log('');
|
|
341
394
|
}
|
|
342
395
|
export function registerProjectsCommands(program) {
|
|
343
396
|
const enabled = isBetaEnabled('projects');
|
|
@@ -456,58 +509,70 @@ export function registerProjectsCommands(program) {
|
|
|
456
509
|
projects
|
|
457
510
|
.command('view <name>')
|
|
458
511
|
.alias('show')
|
|
459
|
-
.description('
|
|
512
|
+
.description('Everything about one project: the full status card, every milestone, and the stored definition.')
|
|
460
513
|
.option('--json', 'Machine-readable output')
|
|
461
|
-
.option('--
|
|
514
|
+
.option('--window <days>', 'Window for merged PRs, artifacts, and focus areas', '7')
|
|
515
|
+
.option('--no-remote', 'Skip the GitHub and Linear lookups; definition only')
|
|
462
516
|
.action(async (name, opts) => {
|
|
463
517
|
const def = loadProjectDef(name);
|
|
464
518
|
if (!def) {
|
|
465
519
|
console.error(chalk.red(`No project named "${name}". List them: agents projects list`));
|
|
466
520
|
process.exit(1);
|
|
467
521
|
}
|
|
468
|
-
//
|
|
469
|
-
//
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
522
|
+
// `view` is `status` for one project PLUS the stored definition and every
|
|
523
|
+
// milestone. It used to render its own short list — root, repos, a raw
|
|
524
|
+
// Linear id, issues — so opening a single project told you LESS than the
|
|
525
|
+
// roll-up did: no agents, no ships, no focus, no schedule verdict.
|
|
526
|
+
const windowDays = Math.max(1, Number.parseInt(opts.window ?? '7', 10) || 7);
|
|
527
|
+
const nowMs = Date.now();
|
|
528
|
+
const all = listProjectDefs();
|
|
529
|
+
const { roll, remote, linear, focus } = await enrichProjectsForRender([def], all, {
|
|
530
|
+
windowDays,
|
|
531
|
+
nowMs,
|
|
532
|
+
skipRemote: opts.remote === false,
|
|
533
|
+
});
|
|
534
|
+
const r = roll.get(def.name);
|
|
535
|
+
const sig = remote.get(def.name);
|
|
536
|
+
const counts = linear.get(def.name);
|
|
473
537
|
if (opts.json) {
|
|
474
|
-
console.log(JSON.stringify({
|
|
538
|
+
console.log(JSON.stringify({
|
|
539
|
+
...def,
|
|
540
|
+
linear: { ...def.linear, ...(counts ?? {}) },
|
|
541
|
+
schedule: counts?.milestones?.length ? scheduleVerdict(counts.milestones, nowMs) : null,
|
|
542
|
+
live: r ? liveDeadSplit(r.byStatus).live : 0,
|
|
543
|
+
dead: r ? liveDeadSplit(r.byStatus).dead : 0,
|
|
544
|
+
byStatus: r?.byStatus ?? {},
|
|
545
|
+
members: r?.members ?? [],
|
|
546
|
+
openPrs: r?.openPrs ?? [],
|
|
547
|
+
tickets: r?.tickets ?? [],
|
|
548
|
+
mergedPrs: sig?.mergedPrs ?? 0,
|
|
549
|
+
latestRelease: sig?.latestRelease ?? null,
|
|
550
|
+
artifacts: sig?.artifacts ?? 0,
|
|
551
|
+
focus: focus.get(def.name) ?? [],
|
|
552
|
+
}, null, 2));
|
|
475
553
|
return;
|
|
476
554
|
}
|
|
477
|
-
|
|
555
|
+
// The same card `status` prints, with every milestone instead of the next.
|
|
556
|
+
renderCard(def, r, sig, undefined, counts, nowMs, Number.POSITIVE_INFINITY, focus.get(def.name) ?? [], true);
|
|
557
|
+
// Then what only `view` shows: the stored definition, in full.
|
|
558
|
+
console.log();
|
|
478
559
|
if (def.root)
|
|
479
|
-
console.log(` root
|
|
560
|
+
console.log(` ${chalk.dim('root')} ${def.root}`);
|
|
480
561
|
if (def.defaultPath)
|
|
481
|
-
console.log(`
|
|
482
|
-
const
|
|
483
|
-
|
|
484
|
-
console.log(`
|
|
562
|
+
console.log(` ${chalk.dim('path')} ${def.defaultPath}`);
|
|
563
|
+
for (const rp of def.repos ?? []) {
|
|
564
|
+
const where = [rp.subpath ? `subpath ${rp.subpath}` : undefined, rp.path].filter(Boolean).join(' · ');
|
|
565
|
+
console.log(` ${chalk.dim('repo')} ${rp.slug}${where ? chalk.dim(` (${where})`) : ''}`);
|
|
566
|
+
}
|
|
485
567
|
for (const c of def.contexts ?? [])
|
|
486
|
-
console.log(` context
|
|
487
|
-
for (const
|
|
488
|
-
console.log(` ${
|
|
568
|
+
console.log(` ${chalk.dim('context')} ${chalk.cyan(c.path)} ${chalk.dim('—')} ${c.purpose}`);
|
|
569
|
+
for (const ig of def.integrations ?? []) {
|
|
570
|
+
console.log(` ${chalk.dim(ig.kind.padEnd(8))} ${ig.url}${ig.label ? chalk.dim(` (${ig.label})`) : ''}`);
|
|
571
|
+
}
|
|
489
572
|
if (def.linear?.url || def.linear?.projectId)
|
|
490
|
-
console.log(` linear
|
|
573
|
+
console.log(` ${chalk.dim('linear')} ${def.linear.url ?? def.linear.projectId}`);
|
|
491
574
|
for (const d of def.docs ?? [])
|
|
492
|
-
console.log(` doc
|
|
493
|
-
if (counts) {
|
|
494
|
-
const staleNote = counts.stale ? chalk.dim(' (cached)') : '';
|
|
495
|
-
console.log(` issues ${counts.done}/${counts.total}${counts.truncated ? '+' : ''} done · ${counts.inProgress} in progress${staleNote}`);
|
|
496
|
-
}
|
|
497
|
-
const ms = counts?.milestones ?? [];
|
|
498
|
-
if (ms.length) {
|
|
499
|
-
console.log(` ${chalk.bold('milestones')}`);
|
|
500
|
-
for (const m of ms) {
|
|
501
|
-
const mark = m.name === counts?.nextMilestone?.name ? chalk.cyan('next') : '';
|
|
502
|
-
console.log(` ${mark.padEnd(6)} ${formatNextMilestone(m, Date.now())}`);
|
|
503
|
-
}
|
|
504
|
-
// A milestone nothing is filed against cannot report progress. Say that
|
|
505
|
-
// once, plainly, rather than printing a row of silent 0%s.
|
|
506
|
-
const unfiled = ms.filter((m) => m.total === 0).length;
|
|
507
|
-
if (unfiled === ms.length) {
|
|
508
|
-
console.log(` ${chalk.yellow('!')} ${chalk.yellow(`no issues are assigned to any milestone — progress against them cannot be measured`)}`);
|
|
509
|
-
}
|
|
510
|
-
}
|
|
575
|
+
console.log(` ${chalk.dim('doc')} ${d}`);
|
|
511
576
|
console.log(chalk.gray(` ${projectDefPath(name)}`));
|
|
512
577
|
});
|
|
513
578
|
// ---- edit ----
|
|
@@ -578,23 +643,12 @@ export function registerProjectsCommands(program) {
|
|
|
578
643
|
fleetSkipped = probeRes.skipped;
|
|
579
644
|
fleetSessions = activeRes.sessions;
|
|
580
645
|
}
|
|
581
|
-
const roll
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
const skipRemote = opts.remote === false;
|
|
588
|
-
const [sig, counts] = await Promise.all([
|
|
589
|
-
enrichProjectSignals(d, windowDays, nowMs, { skipRemote }),
|
|
590
|
-
!skipRemote && d.linear?.projectId
|
|
591
|
-
? fetchLinearProjectCounts(d.linear.projectId)
|
|
592
|
-
: Promise.resolve(undefined),
|
|
593
|
-
]);
|
|
594
|
-
remote.set(d.name, sig);
|
|
595
|
-
if (counts)
|
|
596
|
-
linear.set(d.name, counts);
|
|
597
|
-
}));
|
|
646
|
+
const { roll, remote, linear, focus } = await enrichProjectsForRender(defs, all, {
|
|
647
|
+
windowDays,
|
|
648
|
+
nowMs,
|
|
649
|
+
skipRemote: opts.remote === false,
|
|
650
|
+
extraSessions: fleetSessions,
|
|
651
|
+
});
|
|
598
652
|
/** This def's slice of the fleet probe, in its own target order. */
|
|
599
653
|
const fleetFor = (d) => {
|
|
600
654
|
const targets = new Set(workspaceTargetsForDef(d));
|
|
@@ -612,6 +666,10 @@ export function registerProjectsCommands(program) {
|
|
|
612
666
|
byStatus: r?.byStatus ?? {},
|
|
613
667
|
members: r?.members ?? [],
|
|
614
668
|
plan: r?.plan ?? { done: 0, total: 0 },
|
|
669
|
+
schedule: linear.get(d.name)?.milestones?.length
|
|
670
|
+
? scheduleVerdict(linear.get(d.name).milestones, nowMs)
|
|
671
|
+
: null,
|
|
672
|
+
focus: focus.get(d.name) ?? [],
|
|
615
673
|
live: r ? liveDeadSplit(r.byStatus).live : 0,
|
|
616
674
|
dead: r ? liveDeadSplit(r.byStatus).dead : 0,
|
|
617
675
|
openPrs: r?.openPrs ?? [],
|
|
@@ -630,7 +688,7 @@ export function registerProjectsCommands(program) {
|
|
|
630
688
|
return;
|
|
631
689
|
}
|
|
632
690
|
for (const d of defs) {
|
|
633
|
-
renderCard(d, roll.get(d.name), remote.get(d.name), opts.fleet ? fleetFor(d) : undefined, linear.get(d.name), nowMs);
|
|
691
|
+
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) ?? []);
|
|
634
692
|
}
|
|
635
693
|
if (fleetSkipped.length > 0)
|
|
636
694
|
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
|
+
}
|
|
@@ -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;
|
|
@@ -338,6 +343,15 @@ export declare function renderSessionPreview(query: string, scope: {
|
|
|
338
343
|
* attached", so those two spell it out.
|
|
339
344
|
*/
|
|
340
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;
|
|
341
355
|
/**
|
|
342
356
|
* Whether a session belongs to `team`, from either end: it spawned the team, or
|
|
343
357
|
* it is one of the team's teammates. Case-insensitive, matching the SQL
|
|
@@ -605,6 +619,8 @@ export declare function fleetCandidatesByQuery(rows: SessionMeta[], query: strin
|
|
|
605
619
|
/** Fixed peer argv for the metadata resolver. Scope flags compose identically on
|
|
606
620
|
* every host; `--all` removes the SSH login cwd/time window, not agent/project filters. */
|
|
607
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[];
|
|
608
624
|
/** Resolution must fail closed when any selected peer did not answer. Choosing
|
|
609
625
|
* from a partial fleet can turn an unseen candidate into a false unique match. */
|
|
610
626
|
export declare function metadataResolveOutcome(localMatches: SessionMeta[], remote: {
|