@phnx-labs/agents-cli 1.21.1 → 1.21.2
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 +172 -0
- package/README.md +1 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/doctor.js +5 -2
- package/dist/commands/feed.js +28 -19
- package/dist/commands/hooks.js +9 -45
- package/dist/commands/menubar.js +24 -24
- package/dist/commands/message.js +23 -3
- package/dist/commands/perf.d.ts +13 -0
- package/dist/commands/perf.js +80 -23
- package/dist/commands/projects.d.ts +11 -0
- package/dist/commands/projects.js +153 -21
- package/dist/commands/routines.js +46 -1
- package/dist/commands/ssh.js +69 -0
- package/dist/commands/trends.d.ts +2 -0
- package/dist/commands/trends.js +158 -0
- package/dist/commands/usage.d.ts +4 -4
- package/dist/commands/view.d.ts +6 -0
- package/dist/commands/view.js +90 -45
- package/dist/index.js +14 -1
- package/dist/lib/agents.js +2 -2
- package/dist/lib/analytics/dashboard.d.ts +11 -0
- package/dist/lib/analytics/dashboard.js +31 -0
- package/dist/lib/analytics/recipes.d.ts +32 -0
- package/dist/lib/analytics/recipes.js +316 -0
- package/dist/lib/analytics/usage-db.d.ts +84 -0
- package/dist/lib/analytics/usage-db.js +301 -0
- package/dist/lib/browser/service.js +18 -0
- package/dist/lib/cli-resources.d.ts +20 -0
- package/dist/lib/cli-resources.js +48 -1
- package/dist/lib/daemon.js +51 -14
- package/dist/lib/devices/health-report.d.ts +5 -0
- package/dist/lib/devices/health-report.js +3 -0
- package/dist/lib/feed-broadcast.d.ts +52 -7
- package/dist/lib/feed-broadcast.js +125 -18
- package/dist/lib/fleet-cache.d.ts +37 -0
- package/dist/lib/fleet-cache.js +40 -0
- package/dist/lib/fleet-status.d.ts +53 -0
- package/dist/lib/fleet-status.js +120 -0
- package/dist/lib/friction-heuristics.d.ts +32 -0
- package/dist/lib/friction-heuristics.js +47 -0
- package/dist/lib/hooks/cache.js +28 -6
- package/dist/lib/hooks/profile.d.ts +8 -0
- package/dist/lib/hooks/profile.js +14 -4
- package/dist/lib/hooks.js +72 -17
- package/dist/lib/linear-cache.d.ts +63 -0
- package/dist/lib/linear-cache.js +146 -0
- package/dist/lib/linear-project-counts.d.ts +35 -5
- package/dist/lib/linear-project-counts.js +61 -16
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +3 -1
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +7 -0
- package/dist/lib/menubar/install-menubar.js +36 -6
- package/dist/lib/perf/db.d.ts +6 -1
- package/dist/lib/perf/db.js +35 -5
- package/dist/lib/perf/types.d.ts +10 -0
- package/dist/lib/project-doctor.d.ts +36 -0
- package/dist/lib/project-doctor.js +45 -0
- package/dist/lib/project-import.d.ts +11 -1
- package/dist/lib/project-import.js +17 -3
- package/dist/lib/project-status.d.ts +25 -5
- package/dist/lib/project-status.js +48 -6
- package/dist/lib/rotate.d.ts +27 -0
- package/dist/lib/rotate.js +44 -17
- package/dist/lib/routines.d.ts +16 -0
- package/dist/lib/routines.js +39 -0
- package/dist/lib/runner.js +34 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/usage-db.d.ts +3 -63
- package/dist/lib/secrets/usage-db.js +46 -186
- package/dist/lib/session/db.d.ts +2 -1
- package/dist/lib/session/db.js +14 -3
- package/dist/lib/session/discover.d.ts +3 -0
- package/dist/lib/session/discover.js +8 -0
- package/dist/lib/session/types.d.ts +1 -0
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/state.d.ts +31 -3
- package/dist/lib/state.js +53 -10
- package/dist/lib/types.d.ts +8 -4
- package/dist/lib/usage-refresh.d.ts +106 -0
- package/dist/lib/usage-refresh.js +238 -0
- package/dist/lib/usage.d.ts +152 -17
- package/dist/lib/usage.js +393 -79
- package/package.json +1 -1
package/dist/commands/ssh.js
CHANGED
|
@@ -332,6 +332,20 @@ async function localHealthRow(self, stats) {
|
|
|
332
332
|
inventory: await collectLocalFleetInventory(process.cwd()),
|
|
333
333
|
};
|
|
334
334
|
}
|
|
335
|
+
/** SSH into a host and read its already-computed fleet-status row (a cheap
|
|
336
|
+
* `fleet status --local --json` on the peer — NOT a fresh remote resource probe;
|
|
337
|
+
* the peer's daemon keeps that row warm). Bounded + reaped via sshExecAsync's
|
|
338
|
+
* timeout (RUSH-2114). */
|
|
339
|
+
async function probeRemoteFleetStatus(target) {
|
|
340
|
+
const isWin = /^win/i.test((target.platform ?? '').trim());
|
|
341
|
+
const env = isWin ? undefined : { PATH: '$HOME/.agents/.cache/shims:$HOME/.local/bin:$PATH' };
|
|
342
|
+
const cmd = buildRemoteAgentsInvocation(['devices', 'status', '--local', '--json'], undefined, isWin ? 'windows' : undefined, env);
|
|
343
|
+
const res = await sshExecAsync(target.dialTarget, cmd, { timeoutMs: 15000, multiplex: true });
|
|
344
|
+
if (res.code !== 0) {
|
|
345
|
+
throw new Error(res.timedOut ? 'timed out' : (res.stderr.trim() || `exit ${res.code ?? 'unknown'}`));
|
|
346
|
+
}
|
|
347
|
+
return JSON.parse(res.stdout);
|
|
348
|
+
}
|
|
335
349
|
async function probeRemoteHealth(target) {
|
|
336
350
|
const isWin = /^win/i.test((target.platform ?? '').trim());
|
|
337
351
|
const env = isWin ? undefined : { PATH: '$HOME/.agents/.cache/shims:$HOME/.local/bin:$PATH' };
|
|
@@ -363,6 +377,19 @@ async function runFleetStatus(opts) {
|
|
|
363
377
|
const reg = await loadDevices();
|
|
364
378
|
const self = machineId();
|
|
365
379
|
const forceRefresh = Boolean(opts.refresh || opts.live);
|
|
380
|
+
// `--local`: the publish endpoint the read-union reads over ssh. Probe THIS
|
|
381
|
+
// host only (resource stats + live-agent workload, no ssh) and print its row.
|
|
382
|
+
// Publishes into the local mirror as a side effect so a same-host reader is
|
|
383
|
+
// instantly warm too.
|
|
384
|
+
if (opts.local) {
|
|
385
|
+
const { publishLocalFleetStatus } = await import('../lib/fleet-status.js');
|
|
386
|
+
const row = await publishLocalFleetStatus(self);
|
|
387
|
+
if (opts.json)
|
|
388
|
+
console.log(JSON.stringify(row, null, 2));
|
|
389
|
+
else
|
|
390
|
+
console.log(`${self}: ${row.agents.running} running agent(s), ${row.agents.live} live`);
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
366
393
|
const planned = planFleetTargets(reg);
|
|
367
394
|
const probeable = planned.filter((t) => !t.skip).map((t) => t.device);
|
|
368
395
|
// Cache-first: serve remote stats from the daemon-warmed cache (instant),
|
|
@@ -429,6 +456,47 @@ async function runFleetStatus(opts) {
|
|
|
429
456
|
row.lastSeen = profile.tailscale?.lastSeen ?? profile.reachability?.checkedAt;
|
|
430
457
|
}
|
|
431
458
|
}
|
|
459
|
+
// Live-agent workload (RUSH-2061): publish THIS host's row, then union peers'
|
|
460
|
+
// rows cache-first. The daemon no longer probes the fleet (publish-own /
|
|
461
|
+
// read-union), so cross-host counts are gathered HERE, on demand — a mirror row
|
|
462
|
+
// younger than the freshness window is served without ssh; a missing/stale one
|
|
463
|
+
// is read over ssh via `fleet status --local --json` (bounded + kill-on-timeout
|
|
464
|
+
// through sshExecAsync/fanOutDevices, RUSH-2114). Best-effort: agent counts are
|
|
465
|
+
// additive, so a failed gather never breaks the status render.
|
|
466
|
+
try {
|
|
467
|
+
const { publishLocalFleetStatus, readFleetStatus, writeFleetStatusRows } = await import('../lib/fleet-status.js');
|
|
468
|
+
const selfRow = await publishLocalFleetStatus(self);
|
|
469
|
+
const mirror = readFleetStatus();
|
|
470
|
+
const now = Date.now();
|
|
471
|
+
const AGENT_STATUS_STALE_MS = 3 * 60_000;
|
|
472
|
+
const toRead = remoteTargets.filter((t) => {
|
|
473
|
+
if (t.skip)
|
|
474
|
+
return false;
|
|
475
|
+
if (forceRefresh)
|
|
476
|
+
return true;
|
|
477
|
+
const row = mirror[t.name];
|
|
478
|
+
return !row || now - row.capturedAt > AGENT_STATUS_STALE_MS;
|
|
479
|
+
});
|
|
480
|
+
if (toRead.length > 0) {
|
|
481
|
+
const gathered = await fanOutDevices(toRead, probeRemoteFleetStatus, { perDeviceTimeoutMs: 20_000 });
|
|
482
|
+
const updates = {};
|
|
483
|
+
for (const g of gathered) {
|
|
484
|
+
if (g.status === 'ok' && g.value)
|
|
485
|
+
updates[g.name] = { ...g.value, host: g.name };
|
|
486
|
+
}
|
|
487
|
+
if (Object.keys(updates).length > 0)
|
|
488
|
+
writeFleetStatusRows(updates);
|
|
489
|
+
}
|
|
490
|
+
const union = readFleetStatus();
|
|
491
|
+
for (const row of rows) {
|
|
492
|
+
const r = row.name === self ? selfRow : union[row.name];
|
|
493
|
+
if (r)
|
|
494
|
+
row.agents = r.agents;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
catch {
|
|
498
|
+
// best-effort — agent counts are additive to the health view
|
|
499
|
+
}
|
|
432
500
|
const report = buildFleetHealthReport(rows, new Date(), { self });
|
|
433
501
|
if (opts.json) {
|
|
434
502
|
console.log(JSON.stringify(report, null, 2));
|
|
@@ -1110,6 +1178,7 @@ Typical workflow:
|
|
|
1110
1178
|
.option('--no-stats', 'skip the live resource probe')
|
|
1111
1179
|
.option('--refresh', 'force a live probe of every device, bypassing the cache')
|
|
1112
1180
|
.option('--live', 'alias of --refresh (shorter to type)')
|
|
1181
|
+
.option('--local', "this machine only: print THIS host's status row (resource stats + live-agent workload). The publish endpoint the fleet-status read-union reads over ssh.")
|
|
1113
1182
|
.option('--verbose', 'show the full per-device auth/CLI/sync/version grid instead of the summary')
|
|
1114
1183
|
.action(async (opts, cmd) => {
|
|
1115
1184
|
// The root program also defines a global `--verbose`; commander binds a
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { setHelpSections } from '../lib/help.js';
|
|
3
|
+
import { buildTrendsDashboard, trendsWindow, runRecipe, RECIPE_IDS } from '../lib/analytics/dashboard.js';
|
|
4
|
+
import { listRecipes } from '../lib/analytics/recipes.js';
|
|
5
|
+
import { queryUsage, usageDbPath, USAGE_KINDS } from '../lib/analytics/usage-db.js';
|
|
6
|
+
function parseDays(raw) {
|
|
7
|
+
const n = parseInt(raw ?? '7', 10);
|
|
8
|
+
return Number.isFinite(n) && n > 0 ? n : 7;
|
|
9
|
+
}
|
|
10
|
+
function parseLimit(raw, fallback) {
|
|
11
|
+
const n = parseInt(raw ?? String(fallback), 10);
|
|
12
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
13
|
+
}
|
|
14
|
+
function printSection(section) {
|
|
15
|
+
console.log(chalk.bold(section.title));
|
|
16
|
+
if (section.rows.length === 0) {
|
|
17
|
+
console.log(chalk.gray(' (no data)'));
|
|
18
|
+
console.log();
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const keys = Object.keys(section.rows[0]);
|
|
22
|
+
const widths = keys.map((k) => Math.max(k.length, ...section.rows.map((r) => String(r[k] ?? '').length), 4));
|
|
23
|
+
const pad = (s, w) => (s.length >= w ? s.slice(0, w) : s + ' '.repeat(w - s.length));
|
|
24
|
+
console.log(chalk.gray(keys.map((k, i) => pad(k.toUpperCase(), widths[i])).join(' ')));
|
|
25
|
+
for (const row of section.rows) {
|
|
26
|
+
console.log(keys.map((k, i) => pad(String(row[k] ?? ''), widths[i])).join(' '));
|
|
27
|
+
}
|
|
28
|
+
console.log();
|
|
29
|
+
}
|
|
30
|
+
function renderDashboard(days, asJson) {
|
|
31
|
+
const dash = buildTrendsDashboard({ days });
|
|
32
|
+
if (asJson) {
|
|
33
|
+
console.log(JSON.stringify(dash, null, 2));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
console.log(chalk.bold(`agents trends — last ${dash.window.days} days`));
|
|
37
|
+
console.log(chalk.gray(`compute ${dash.durationMs}ms · usage ${usageDbPath()}`));
|
|
38
|
+
console.log();
|
|
39
|
+
if (dash.sections.length === 0) {
|
|
40
|
+
console.log(chalk.gray('No session or usage data in this window yet.'));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
for (const section of dash.sections)
|
|
44
|
+
printSection(section);
|
|
45
|
+
}
|
|
46
|
+
export function registerTrendsCommand(program) {
|
|
47
|
+
const trends = program
|
|
48
|
+
.command('trends')
|
|
49
|
+
.description('Usage analytics — harness/model mix, token ratios, resource frequency')
|
|
50
|
+
.option('--days <n>', 'Days of history to include', '7')
|
|
51
|
+
.option('--json', 'Emit JSON instead of tables')
|
|
52
|
+
.action(function summary() {
|
|
53
|
+
const opts = this.opts();
|
|
54
|
+
renderDashboard(parseDays(opts.days), Boolean(opts.json));
|
|
55
|
+
});
|
|
56
|
+
setHelpSections(trends, {
|
|
57
|
+
examples: `
|
|
58
|
+
# Auto recipe dashboard (7d)
|
|
59
|
+
agents trends
|
|
60
|
+
|
|
61
|
+
# Last 30 days
|
|
62
|
+
agents trends --days 30
|
|
63
|
+
|
|
64
|
+
# One recipe as JSON
|
|
65
|
+
agents trends harness-mix --json
|
|
66
|
+
|
|
67
|
+
# Raw usage events
|
|
68
|
+
agents trends query --kind secret --days 7
|
|
69
|
+
|
|
70
|
+
# List baked recipe ids
|
|
71
|
+
agents trends recipes
|
|
72
|
+
`,
|
|
73
|
+
notes: `
|
|
74
|
+
Session recipes read sessions.db; resource recipes read ~/.agents/.history/analytics/usage.db.
|
|
75
|
+
Empty recipes are skipped on the default dashboard.
|
|
76
|
+
Quota / rate-limits remain on \`agents usage\`; latency on \`agents perf\`.
|
|
77
|
+
`,
|
|
78
|
+
});
|
|
79
|
+
trends.command('recipes')
|
|
80
|
+
.description('List baked recipe ids')
|
|
81
|
+
.option('--json', 'Emit JSON')
|
|
82
|
+
.action((opts) => {
|
|
83
|
+
const list = listRecipes();
|
|
84
|
+
if (opts.json) {
|
|
85
|
+
console.log(JSON.stringify(list, null, 2));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
for (const r of list) {
|
|
89
|
+
console.log(`${r.id.padEnd(22)} ${r.store.padEnd(10)} ${r.title}`);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
trends.command('query')
|
|
93
|
+
.description('Raw usage-event query')
|
|
94
|
+
.option('--kind <kind>', `One of: ${USAGE_KINDS.join(', ')}`)
|
|
95
|
+
.option('--name <name>', 'Resource name filter')
|
|
96
|
+
.option('--event <event>', 'Event name filter')
|
|
97
|
+
.option('--days <n>', 'Days of history', '7')
|
|
98
|
+
.option('--limit <n>', 'Max rows', '40')
|
|
99
|
+
.option('--json', 'Emit JSON')
|
|
100
|
+
.action((opts) => {
|
|
101
|
+
const win = trendsWindow(parseDays(opts.days));
|
|
102
|
+
const kind = opts.kind && USAGE_KINDS.includes(opts.kind)
|
|
103
|
+
? opts.kind
|
|
104
|
+
: undefined;
|
|
105
|
+
if (opts.kind && !kind) {
|
|
106
|
+
console.error(`Unknown kind '${opts.kind}'. Expected: ${USAGE_KINDS.join(', ')}`);
|
|
107
|
+
process.exitCode = 1;
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const rows = queryUsage({
|
|
111
|
+
kind,
|
|
112
|
+
name: opts.name,
|
|
113
|
+
event: opts.event,
|
|
114
|
+
sinceIso: win.sinceIso,
|
|
115
|
+
limit: parseLimit(opts.limit, 40),
|
|
116
|
+
});
|
|
117
|
+
if (opts.json) {
|
|
118
|
+
console.log(JSON.stringify({ window: win, rows }, null, 2));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (rows.length === 0) {
|
|
122
|
+
console.log(chalk.gray('No usage events.'));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
printSection({
|
|
126
|
+
id: 'query',
|
|
127
|
+
title: 'Usage events',
|
|
128
|
+
rows: rows.map((r) => ({
|
|
129
|
+
ts: r.ts,
|
|
130
|
+
kind: r.kind,
|
|
131
|
+
name: r.name,
|
|
132
|
+
event: r.event,
|
|
133
|
+
agent: r.agent,
|
|
134
|
+
})),
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
for (const id of RECIPE_IDS) {
|
|
138
|
+
trends.command(id)
|
|
139
|
+
.description(`Recipe: ${id}`)
|
|
140
|
+
.option('--days <n>', 'Days of history', '7')
|
|
141
|
+
.option('--json', 'Emit JSON')
|
|
142
|
+
.action(function recipeAction() {
|
|
143
|
+
const parent = this.parent?.opts?.();
|
|
144
|
+
const opts = { ...parent, ...this.opts() };
|
|
145
|
+
const win = trendsWindow(parseDays(opts.days));
|
|
146
|
+
const section = runRecipe(id, win);
|
|
147
|
+
if (opts.json) {
|
|
148
|
+
console.log(JSON.stringify({ window: win, section }, null, 2));
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (section.empty) {
|
|
152
|
+
console.log(chalk.gray(`No data for recipe '${id}' in the last ${win.days} days.`));
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
printSection(section);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
package/dist/commands/usage.d.ts
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
* Usage command -- show rate-limit / quota status for each installed agent.
|
|
3
3
|
*
|
|
4
4
|
* Lists every installed agent with the best available usage snapshot:
|
|
5
|
-
* - claude: live OAuth API call (cached for
|
|
5
|
+
* - claude: live OAuth API call (cached for 5 minutes)
|
|
6
6
|
* - codex: parsed from latest session log's rate_limits event
|
|
7
|
-
* - kimi: live Kimi Code /usages API call (cached for
|
|
8
|
-
* - droid: live Factory billing/limits API call (cached for
|
|
7
|
+
* - kimi: live Kimi Code /usages API call (cached for 5 minutes)
|
|
8
|
+
* - droid: live Factory billing/limits API call (cached for 5 minutes)
|
|
9
9
|
* - grok: parsed from the latest local usage event
|
|
10
|
-
* - cursor: live Cursor usage API call (cached for
|
|
10
|
+
* - cursor: live Cursor usage API call (cached for 5 minutes)
|
|
11
11
|
* - others: marked as "not exposed by CLI"
|
|
12
12
|
*/
|
|
13
13
|
import type { Command } from 'commander';
|
package/dist/commands/view.d.ts
CHANGED
|
@@ -14,6 +14,12 @@ import { type ConfiguredModelSource } from '../lib/models.js';
|
|
|
14
14
|
import { type ProfileSummary } from '../lib/profiles.js';
|
|
15
15
|
/** Shared account identity formatter, re-exported for the view-specific tests. */
|
|
16
16
|
export declare const accountColumnLabel: typeof accountDisplayLabel;
|
|
17
|
+
/**
|
|
18
|
+
* Join fixed view columns with a consistent two-space gutter. Empty trailing
|
|
19
|
+
* columns are dropped so a row without an auth chip does not grow a dangling
|
|
20
|
+
* gutter, but interior empties stay padded so later columns stay aligned.
|
|
21
|
+
*/
|
|
22
|
+
export declare function joinViewColumns(cols: string[]): string;
|
|
17
23
|
type SyncState = 'synced' | 'new' | 'modified' | 'deleted';
|
|
18
24
|
/** Per-section filter flags. When any are true, only those sections render. */
|
|
19
25
|
export interface ViewSectionFilter {
|
package/dist/commands/view.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { addHostOption } from '../lib/hosts/option.js';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
-
import {
|
|
3
|
+
import { termLink } from '../lib/format.js';
|
|
4
4
|
import ora from 'ora';
|
|
5
5
|
import * as fs from 'fs';
|
|
6
6
|
import * as path from 'path';
|
|
@@ -31,9 +31,31 @@ import { renderHarnessDetail } from './harness.js';
|
|
|
31
31
|
import { loadManifest, isStale } from '../lib/staleness/index.js';
|
|
32
32
|
import { confirm } from '@inquirer/prompts';
|
|
33
33
|
import { formatPath, isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
34
|
-
import { terminalWidth, truncateToWidth, stringWidth } from '../lib/session/width.js';
|
|
34
|
+
import { terminalWidth, truncateToWidth, stringWidth, padToWidth } from '../lib/session/width.js';
|
|
35
35
|
/** Shared account identity formatter, re-exported for the view-specific tests. */
|
|
36
36
|
export const accountColumnLabel = accountDisplayLabel;
|
|
37
|
+
/**
|
|
38
|
+
* Overview (`agents view` with no agent filter) caps compact usage windows so
|
|
39
|
+
* multi-meter agents (Antigravity's four model quotas, Droid's three buckets)
|
|
40
|
+
* cannot force every row to pad past the terminal width and wrap. Single-agent
|
|
41
|
+
* views leave the cap unset and show every blocking window.
|
|
42
|
+
*/
|
|
43
|
+
const OVERVIEW_MAX_USAGE_WINDOWS = 2;
|
|
44
|
+
/** Fixed width for the last-active column ("just now", "8h ago", "2d ago"). */
|
|
45
|
+
const LAST_ACTIVE_COL_WIDTH = 10;
|
|
46
|
+
/**
|
|
47
|
+
* Join fixed view columns with a consistent two-space gutter. Empty trailing
|
|
48
|
+
* columns are dropped so a row without an auth chip does not grow a dangling
|
|
49
|
+
* gutter, but interior empties stay padded so later columns stay aligned.
|
|
50
|
+
*/
|
|
51
|
+
export function joinViewColumns(cols) {
|
|
52
|
+
// Trim only pure-trailing empty strings so auth/status can be absent without
|
|
53
|
+
// shifting earlier columns for rows that do carry them.
|
|
54
|
+
let end = cols.length;
|
|
55
|
+
while (end > 0 && cols[end - 1] === '')
|
|
56
|
+
end--;
|
|
57
|
+
return cols.slice(0, end).join(' ');
|
|
58
|
+
}
|
|
37
59
|
/**
|
|
38
60
|
* Custom harnesses (the `~/.agents/profiles/*.yml` bundles), sorted by name and
|
|
39
61
|
* optionally narrowed to the ones that run on one host agent. YAMLs that fail
|
|
@@ -246,6 +268,8 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
|
|
|
246
268
|
: 'Checking installed agents...';
|
|
247
269
|
const spinner = ora({ text: spinnerText, isSilent: !process.stdout.isTTY }).start();
|
|
248
270
|
const agentsToShow = filterAgentId ? [filterAgentId] : ALL_AGENT_IDS;
|
|
271
|
+
// Overview caps meter count; single-agent view shows every blocking window.
|
|
272
|
+
const usageWindowCap = filterAgentId ? undefined : OVERVIEW_MAX_USAGE_WINDOWS;
|
|
249
273
|
// A globally-installed CLI is superseded only by a NORMAL managed version — that
|
|
250
274
|
// is when agents-cli owns the launcher and a "global" row would just be our own
|
|
251
275
|
// shim reported back. `--isolated` promises the opposite: no default, no bare
|
|
@@ -265,7 +289,6 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
|
|
|
265
289
|
const cliStates = Object.fromEntries(await Promise.all(agentsToShow
|
|
266
290
|
.filter((agentId) => !hasNonIsolatedVersion(agentId))
|
|
267
291
|
.map(async (agentId) => [agentId, await getUnmanagedCliState(agentId)])));
|
|
268
|
-
spinner.stop();
|
|
269
292
|
const showPaths = !!filterAgentId;
|
|
270
293
|
const harnesses = getHarnesses(filterAgentId);
|
|
271
294
|
// Auto-heal stale versioned aliases. Pre-v2 aliases (e.g. pre-CLAUDE_CONFIG_DIR
|
|
@@ -287,11 +310,15 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
|
|
|
287
310
|
}
|
|
288
311
|
}
|
|
289
312
|
// Shim healing is silent — users don't need to know about internal repairs
|
|
290
|
-
console.log(chalk.bold('Installed Agent CLIs\n'));
|
|
291
313
|
const selfHost = machineId();
|
|
292
314
|
// Read the auth-health cache once (not per version row — see the batching note above).
|
|
293
315
|
const authCache = readAuthHealthCache();
|
|
294
|
-
// Pre-fetch account info for all versions in parallel
|
|
316
|
+
// Pre-fetch account info for all versions in parallel. Spinner stays up through
|
|
317
|
+
// account + usage so a multi-account cold path doesn't leave a blank terminal
|
|
318
|
+
// after "Checking…" vanishes (the hang the screenshots caught).
|
|
319
|
+
spinner.text = filterAgentId
|
|
320
|
+
? `Loading ${agentLabel(filterAgentId)} accounts...`
|
|
321
|
+
: 'Loading accounts and usage...';
|
|
295
322
|
const infoFetches = [];
|
|
296
323
|
const globalInfoFetches = [];
|
|
297
324
|
for (const agentId of agentsToShow) {
|
|
@@ -329,6 +356,7 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
|
|
|
329
356
|
// or org scope, not a specific installed version. Version homes cache those
|
|
330
357
|
// values independently, so older installs can show stale values. Reuse the
|
|
331
358
|
// freshest cache entry per stable usage identity and keep lastActive per version.
|
|
359
|
+
// Goes through the unified usage core (SWR cache + concurrency cap + timeout).
|
|
332
360
|
const { canonicalByUsageKey, usageByKey } = await getUsageInfoByIdentity([
|
|
333
361
|
...infoResults.map(({ agentId, home, version, info }) => ({
|
|
334
362
|
agentId,
|
|
@@ -342,6 +370,8 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
|
|
|
342
370
|
info,
|
|
343
371
|
})),
|
|
344
372
|
], { forceRefresh: viewOpts?.forceRefresh });
|
|
373
|
+
spinner.stop();
|
|
374
|
+
console.log(chalk.bold('Installed Agent CLIs\n'));
|
|
345
375
|
const mergeCanonical = (info) => {
|
|
346
376
|
const key = getUsageLookupKey(info);
|
|
347
377
|
if (!key)
|
|
@@ -441,7 +471,8 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
|
|
|
441
471
|
}
|
|
442
472
|
}
|
|
443
473
|
}
|
|
444
|
-
// Second pass: compute max visible usage + status widths (now that maxPlanWidth is settled)
|
|
474
|
+
// Second pass: compute max visible usage + status widths (now that maxPlanWidth is settled).
|
|
475
|
+
// stringWidth (not String.length) so chalk + block-bar glyphs pad correctly.
|
|
445
476
|
for (const agentId of versionManaged) {
|
|
446
477
|
const versions = listInstalledVersions(agentId);
|
|
447
478
|
for (const v of versions) {
|
|
@@ -451,10 +482,14 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
|
|
|
451
482
|
const usageInfo = usageKey ? usageByKey.get(usageKey) : undefined;
|
|
452
483
|
const usageUnavailable = agentReportsUsage(agentId) && !!info?.signedIn && !usageInfo?.snapshot;
|
|
453
484
|
const usageUnverified = !!usageInfo?.snapshot && !!usageInfo.error;
|
|
454
|
-
const usageStr = formatUsageSummary(info?.plan || null, usageInfo?.snapshot || null, maxPlanWidth, {
|
|
455
|
-
|
|
485
|
+
const usageStr = formatUsageSummary(info?.plan || null, usageInfo?.snapshot || null, maxPlanWidth, {
|
|
486
|
+
unavailable: usageUnavailable,
|
|
487
|
+
unverified: usageUnverified,
|
|
488
|
+
maxWindows: usageWindowCap,
|
|
489
|
+
});
|
|
490
|
+
maxUsageWidth = Math.max(maxUsageWidth, stringWidth(usageStr));
|
|
456
491
|
const statusStr = formatUsageStatusBadge(info?.usageStatus);
|
|
457
|
-
maxStatusWidth = Math.max(maxStatusWidth,
|
|
492
|
+
maxStatusWidth = Math.max(maxStatusWidth, stringWidth(statusStr));
|
|
458
493
|
}
|
|
459
494
|
}
|
|
460
495
|
for (const agentId of versionManaged) {
|
|
@@ -496,18 +531,26 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
|
|
|
496
531
|
const vInfo = rawInfo ? mergeCanonical(rawInfo) : undefined;
|
|
497
532
|
const usageKey = getUsageLookupKey(vInfo);
|
|
498
533
|
const usageInfo = usageKey ? usageByKey.get(usageKey) : undefined;
|
|
499
|
-
//
|
|
534
|
+
// Fixed columns for every signed-in row so status / lastActive / auth
|
|
535
|
+
// stay vertically aligned across agents — even when this row has no
|
|
536
|
+
// usage bars or no rate-limit badge. Skipping empty columns mid-table
|
|
537
|
+
// was what made the multi-agent view look unjustified next to
|
|
538
|
+
// `agents view claude`.
|
|
500
539
|
const parts = [` ${label}`];
|
|
501
540
|
// Configured model — same priority as the version, right beside it.
|
|
502
541
|
if (maxModelWidth > 0) {
|
|
503
542
|
const model = modelByKey.get(`${agentId}:${version}`) ?? '';
|
|
504
|
-
parts.push(chalk.yellow(model
|
|
543
|
+
parts.push(chalk.yellow(padToWidth(model, maxModelWidth)));
|
|
505
544
|
}
|
|
506
545
|
const hasEmail = !!vInfo?.email;
|
|
507
546
|
const signedIn = !!vInfo?.signedIn;
|
|
508
547
|
const usageUnavailable = agentReportsUsage(agentId) && signedIn && !usageInfo?.snapshot;
|
|
509
548
|
const usageUnverified = !!usageInfo?.snapshot && !!usageInfo.error;
|
|
510
|
-
const usageStr = formatUsageSummary(vInfo?.plan || null, usageInfo?.snapshot || null, maxPlanWidth, {
|
|
549
|
+
const usageStr = formatUsageSummary(vInfo?.plan || null, usageInfo?.snapshot || null, maxPlanWidth, {
|
|
550
|
+
unavailable: usageUnavailable,
|
|
551
|
+
unverified: usageUnverified,
|
|
552
|
+
maxWindows: usageWindowCap,
|
|
553
|
+
});
|
|
511
554
|
const hasUsage = usageStr.length > 0;
|
|
512
555
|
// Only show lastActive for versions with an actual logged-in account.
|
|
513
556
|
// Otherwise it reflects install time (misleading "just now" for fresh installs).
|
|
@@ -534,25 +577,21 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
|
|
|
534
577
|
: '(logged out — log in with: ' + loginHint(agentId) + ')'));
|
|
535
578
|
}
|
|
536
579
|
else {
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
parts.push(
|
|
544
|
-
}
|
|
545
|
-
if (hasUsage || hasActive) {
|
|
546
|
-
const usagePad = ' '.repeat(Math.max(0, maxUsageWidth - visibleWidth(usageStr)));
|
|
547
|
-
parts.push(usageStr + usagePad);
|
|
580
|
+
// Always emit account / usage / status / lastActive columns once any
|
|
581
|
+
// signed-in row exists in the table (widths are global). Empty cells
|
|
582
|
+
// are space-padded so later columns do not drift left.
|
|
583
|
+
const display = accountColumnLabel(vInfo);
|
|
584
|
+
parts.push(display ? chalk.cyan(padToWidth(display, maxEmail)) : ' '.repeat(maxEmail));
|
|
585
|
+
if (maxUsageWidth > 0) {
|
|
586
|
+
parts.push(padToWidth(usageStr, maxUsageWidth));
|
|
548
587
|
}
|
|
549
|
-
const statusStr = formatUsageStatusBadge(vInfo?.usageStatus);
|
|
550
588
|
if (maxStatusWidth > 0) {
|
|
551
|
-
const
|
|
552
|
-
parts.push(statusStr
|
|
589
|
+
const statusStr = formatUsageStatusBadge(vInfo?.usageStatus);
|
|
590
|
+
parts.push(padToWidth(statusStr, maxStatusWidth));
|
|
553
591
|
}
|
|
554
|
-
|
|
555
|
-
|
|
592
|
+
// Fixed-width lastActive so the auth chip (●/○/◐) lines up even when
|
|
593
|
+
// some rows have no email-derived lastActive.
|
|
594
|
+
parts.push(hasActive ? padToWidth(activeStr, LAST_ACTIVE_COL_WIDTH) : ' '.repeat(LAST_ACTIVE_COL_WIDTH));
|
|
556
595
|
}
|
|
557
596
|
if (runDefaultBits.length > 0) {
|
|
558
597
|
parts.push(chalk.gray(`run ${runDefaultBits.join(' ')}`));
|
|
@@ -560,7 +599,7 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
|
|
|
560
599
|
const authChip = liveAuthChip(authCache, selfHost, agentId, version);
|
|
561
600
|
if (authChip)
|
|
562
601
|
parts.push(authChip);
|
|
563
|
-
console.log(parts
|
|
602
|
+
console.log(joinViewColumns(parts));
|
|
564
603
|
if (showPaths) {
|
|
565
604
|
const versionDir = getVersionDir(agentId, version);
|
|
566
605
|
console.log(chalk.gray(` ${versionDir}`));
|
|
@@ -586,18 +625,25 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
|
|
|
586
625
|
const cliState = cliStates[agentId];
|
|
587
626
|
return `${cliState?.version || 'installed'} (global)`.length;
|
|
588
627
|
}));
|
|
589
|
-
// Pre-pass: max badge
|
|
590
|
-
//
|
|
591
|
-
// have "out of credits" shifts every other row's `lastActive` left by
|
|
592
|
-
// ~16 chars, exactly what the version-managed block at maxStatusWidth
|
|
593
|
-
// already solves above.
|
|
628
|
+
// Pre-pass: max badge/usage/email widths so columns line up the same way
|
|
629
|
+
// the version-managed block does (stringWidth for chalk-aware padding).
|
|
594
630
|
let gMaxStatusWidth = 0;
|
|
631
|
+
let gMaxUsageWidth = 0;
|
|
632
|
+
let gMaxEmail = 0;
|
|
595
633
|
for (const agentId of globallyInstalled) {
|
|
596
634
|
const gInfoRaw = globalInfoMap.get(agentId);
|
|
597
635
|
const gInfo = gInfoRaw ? mergeCanonical(gInfoRaw) : undefined;
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
636
|
+
gMaxStatusWidth = Math.max(gMaxStatusWidth, stringWidth(formatUsageStatusBadge(gInfo?.usageStatus)));
|
|
637
|
+
const gUsageKey = getUsageLookupKey(gInfo);
|
|
638
|
+
const gUsage = gUsageKey ? usageByKey.get(gUsageKey) : undefined;
|
|
639
|
+
const gUsageStr = formatUsageSummary(gInfo?.plan || null, gUsage?.snapshot || null, 3, {
|
|
640
|
+
unverified: !!gUsage?.snapshot && !!gUsage.error,
|
|
641
|
+
maxWindows: usageWindowCap,
|
|
642
|
+
});
|
|
643
|
+
gMaxUsageWidth = Math.max(gMaxUsageWidth, stringWidth(gUsageStr));
|
|
644
|
+
const gDisplay = accountColumnLabel(gInfo);
|
|
645
|
+
if (gDisplay)
|
|
646
|
+
gMaxEmail = Math.max(gMaxEmail, gDisplay.length);
|
|
601
647
|
}
|
|
602
648
|
for (const agentId of globallyInstalled) {
|
|
603
649
|
const agent = AGENTS[agentId];
|
|
@@ -613,22 +659,21 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
|
|
|
613
659
|
const gUsage = gUsageKey ? usageByKey.get(gUsageKey) : undefined;
|
|
614
660
|
const gUsageStr = formatUsageSummary(gInfo?.plan || null, gUsage?.snapshot || null, 3, {
|
|
615
661
|
unverified: !!gUsage?.snapshot && !!gUsage.error,
|
|
662
|
+
maxWindows: usageWindowCap,
|
|
616
663
|
});
|
|
617
664
|
const gActiveStr = gInfo ? formatLastActive(gInfo.lastActive) : '';
|
|
618
665
|
if (gInfo?.email || gUsageStr || gActiveStr || gInfo?.signedIn) {
|
|
619
666
|
const gDisplay = accountColumnLabel(gInfo);
|
|
620
|
-
parts.push(gDisplay ? chalk.cyan(gDisplay) : '');
|
|
667
|
+
parts.push(gDisplay ? chalk.cyan(padToWidth(gDisplay, gMaxEmail)) : ' '.repeat(gMaxEmail));
|
|
621
668
|
}
|
|
622
|
-
if (
|
|
623
|
-
parts.push(gUsageStr);
|
|
624
|
-
const gStatusStr = formatUsageStatusBadge(gInfo?.usageStatus);
|
|
669
|
+
if (gMaxUsageWidth > 0)
|
|
670
|
+
parts.push(padToWidth(gUsageStr, gMaxUsageWidth));
|
|
625
671
|
if (gMaxStatusWidth > 0) {
|
|
626
|
-
|
|
627
|
-
parts.push(gStatusStr + statusPad);
|
|
672
|
+
parts.push(padToWidth(formatUsageStatusBadge(gInfo?.usageStatus), gMaxStatusWidth));
|
|
628
673
|
}
|
|
629
674
|
if (gActiveStr)
|
|
630
|
-
parts.push(gActiveStr);
|
|
631
|
-
console.log(parts
|
|
675
|
+
parts.push(padToWidth(gActiveStr, LAST_ACTIVE_COL_WIDTH));
|
|
676
|
+
console.log(joinViewColumns(parts));
|
|
632
677
|
if (showPaths && cliState?.path) {
|
|
633
678
|
console.log(chalk.gray(` ${cliState.path}`));
|
|
634
679
|
}
|
package/dist/index.js
CHANGED
|
@@ -94,7 +94,7 @@ if (IS_DEV_BUILD) {
|
|
|
94
94
|
// module on each invocation (which loaded the whole ~50-module tree before the
|
|
95
95
|
// first byte of output), the registry maps a command name to a thunk that
|
|
96
96
|
// imports only what that command needs. See src/lib/startup/command-registry.ts.
|
|
97
|
-
import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadFeedback, loadCommands, loadHooks, loadSkills, loadRules, loadMemory, loadPermissions, loadMcp, loadCli, loadSubagents, loadPlugins, loadWorkflows, loadWorktree, loadVersions, loadImport, loadExport, loadPackages, loadRoutines, loadMonitors, loadProjects, loadRun, loadFork, loadDefaults, loadSet, loadModels, loadPrune, loadTrash, loadRestore, loadDoctor, loadApply, loadStatus, loadProfiles, loadHarness, loadSecrets, loadLogin, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadLock, loadRefreshRules, loadFactory, loadUsage, loadCost, loadPerf, loadOutput, loadBudget, loadAlias, loadMine, loadPty, loadTmux, loadWatchdog, loadBrowser, loadComputer, loadHosts, loadLogs, loadEvents, loadAudit, loadWebhook, loadFunnel, loadSsh, loadPull, loadPush, loadRepo, loadSetup, loadUninstall, loadShare, loadSend, loadFeed, loadActivity, loadMailboxes, } from './lib/startup/command-registry.js';
|
|
97
|
+
import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadFeedback, loadCommands, loadHooks, loadSkills, loadRules, loadMemory, loadPermissions, loadMcp, loadCli, loadSubagents, loadPlugins, loadWorkflows, loadWorktree, loadVersions, loadImport, loadExport, loadPackages, loadRoutines, loadMonitors, loadProjects, loadRun, loadFork, loadDefaults, loadSet, loadModels, loadPrune, loadTrash, loadRestore, loadDoctor, loadApply, loadStatus, loadProfiles, loadHarness, loadSecrets, loadLogin, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadLock, loadRefreshRules, loadFactory, loadUsage, loadCost, loadPerf, loadTrends, loadOutput, loadBudget, loadAlias, loadMine, loadPty, loadTmux, loadWatchdog, loadBrowser, loadComputer, loadHosts, loadLogs, loadEvents, loadAudit, loadWebhook, loadFunnel, loadSsh, loadPull, loadPush, loadRepo, loadSetup, loadUninstall, loadShare, loadSend, loadFeed, loadActivity, loadMailboxes, } from './lib/startup/command-registry.js';
|
|
98
98
|
import { applyGlobalHelpConventions } from './lib/help.js';
|
|
99
99
|
import { renderWhatsNew } from './lib/whats-new.js';
|
|
100
100
|
import { getCliLaunch } from './lib/cli-entry.js';
|
|
@@ -197,6 +197,18 @@ program.hook('postAction', (_thisCommand, actionCommand) => {
|
|
|
197
197
|
command,
|
|
198
198
|
...(durationMs !== undefined ? { durationMs } : {}),
|
|
199
199
|
});
|
|
200
|
+
if (parts[0] === 'run') {
|
|
201
|
+
const agentName = actionCommand.args?.[0] ? String(actionCommand.args[0]).split('@')[0] : 'run';
|
|
202
|
+
void import('./lib/analytics/usage-db.js').then(({ recordUsage }) => {
|
|
203
|
+
recordUsage({
|
|
204
|
+
kind: 'agent',
|
|
205
|
+
name: agentName || 'run',
|
|
206
|
+
event: 'invoke',
|
|
207
|
+
source: 'cli',
|
|
208
|
+
meta: durationMs !== undefined ? { durationMs } : undefined,
|
|
209
|
+
});
|
|
210
|
+
}).catch(() => { });
|
|
211
|
+
}
|
|
200
212
|
// Disposable perf warehouse — fail-soft spool append (no SQLite on this path).
|
|
201
213
|
if (durationMs !== undefined && parts[0] !== 'perf') {
|
|
202
214
|
void import('./lib/perf/spool.js').then(({ recordSample }) => {
|
|
@@ -933,6 +945,7 @@ async function registerAllEagerCommands() {
|
|
|
933
945
|
await reg(loadUsage);
|
|
934
946
|
await reg(loadCost);
|
|
935
947
|
await reg(loadPerf);
|
|
948
|
+
await reg(loadTrends);
|
|
936
949
|
await reg(loadOutput);
|
|
937
950
|
await reg(loadBudget);
|
|
938
951
|
await reg(loadAlias);
|
package/dist/lib/agents.js
CHANGED
|
@@ -1754,8 +1754,8 @@ export async function getAccountInfo(agentId, home) {
|
|
|
1754
1754
|
}
|
|
1755
1755
|
}
|
|
1756
1756
|
// Fresh window for the cached session walk. Matches USAGE_CACHE_FRESH_MS in
|
|
1757
|
-
// usage.ts so a launch storm reuses both probes for the same period.
|
|
1758
|
-
const LAST_ACTIVE_CACHE_FRESH_MS =
|
|
1757
|
+
// usage.ts (5 minutes) so a launch storm reuses both probes for the same period.
|
|
1758
|
+
const LAST_ACTIVE_CACHE_FRESH_MS = 5 * 60 * 1000;
|
|
1759
1759
|
const getLastActiveCachePath = () => path.join(getCacheDir(), 'last-active.json');
|
|
1760
1760
|
/**
|
|
1761
1761
|
* Determine when the agent was last used by checking session file mtimes,
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { RECIPE_IDS, runRecipe, trendsWindow, type RecipeId, type RecipeSection, type TrendsWindow } from './recipes.js';
|
|
2
|
+
export interface TrendsDashboard {
|
|
3
|
+
window: TrendsWindow;
|
|
4
|
+
durationMs: number;
|
|
5
|
+
sections: RecipeSection[];
|
|
6
|
+
}
|
|
7
|
+
export declare function buildTrendsDashboard(opts?: {
|
|
8
|
+
days?: number;
|
|
9
|
+
ids?: RecipeId[];
|
|
10
|
+
}): TrendsDashboard;
|
|
11
|
+
export { trendsWindow, runRecipe, RECIPE_IDS, type RecipeId, type RecipeSection };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { RECIPE_IDS, runRecipe, trendsWindow, } from './recipes.js';
|
|
2
|
+
const DEFAULT_ORDER = [
|
|
3
|
+
'harness-mix',
|
|
4
|
+
'model-mix',
|
|
5
|
+
'session-volume',
|
|
6
|
+
'token-ratio',
|
|
7
|
+
'tools-per-session',
|
|
8
|
+
'secrets-hot',
|
|
9
|
+
'browser-activity',
|
|
10
|
+
'resource-mix',
|
|
11
|
+
];
|
|
12
|
+
export function buildTrendsDashboard(opts = {}) {
|
|
13
|
+
const t0 = Date.now();
|
|
14
|
+
const win = trendsWindow(opts.days ?? 7);
|
|
15
|
+
const ids = opts.ids ?? DEFAULT_ORDER;
|
|
16
|
+
const sections = [];
|
|
17
|
+
for (const id of ids) {
|
|
18
|
+
if (!RECIPE_IDS.includes(id))
|
|
19
|
+
continue;
|
|
20
|
+
const section = runRecipe(id, win);
|
|
21
|
+
if (section.empty)
|
|
22
|
+
continue;
|
|
23
|
+
sections.push(section);
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
window: win,
|
|
27
|
+
durationMs: Date.now() - t0,
|
|
28
|
+
sections,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export { trendsWindow, runRecipe, RECIPE_IDS };
|