@phnx-labs/agents-cli 1.21.0 → 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 +198 -0
- package/README.md +2 -1
- package/dist/bin/agents +0 -0
- package/dist/commands/doctor.js +5 -2
- package/dist/commands/feed.js +63 -40
- package/dist/commands/hooks.js +9 -45
- package/dist/commands/menubar.js +24 -24
- package/dist/commands/message.js +23 -3
- package/dist/commands/monitors.js +8 -6
- 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/activity.d.ts +6 -0
- package/dist/lib/activity.js +3 -0
- 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/channels/providers/openclaw-telegram.js +1 -1
- package/dist/lib/channels/resolve.d.ts +18 -1
- package/dist/lib/channels/resolve.js +15 -5
- 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 +93 -12
- package/dist/lib/feed-broadcast.js +235 -30
- package/dist/lib/feed-post.d.ts +16 -2
- package/dist/lib/feed-post.js +35 -7
- package/dist/lib/feed.js +1 -1
- 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.d.ts +16 -1
- package/dist/lib/hooks.js +133 -18
- 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/monitors/config.d.ts +1 -1
- package/dist/lib/monitors/dispatch.d.ts +6 -2
- package/dist/lib/monitors/dispatch.js +10 -15
- package/dist/lib/notify.d.ts +42 -6
- package/dist/lib/notify.js +41 -32
- 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 +14 -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/menubar.js
CHANGED
|
@@ -11,7 +11,7 @@ import { setHelpSections } from '../lib/help.js';
|
|
|
11
11
|
import { enableMenubarService, disableMenubarService, getMenubarStatus, runMenubarSetup, } from '../lib/menubar/install-menubar.js';
|
|
12
12
|
function notMac() {
|
|
13
13
|
if (process.platform !== 'darwin') {
|
|
14
|
-
console.log(chalk.yellow('
|
|
14
|
+
console.log(chalk.yellow('AGI Menu is macOS only.'));
|
|
15
15
|
return true;
|
|
16
16
|
}
|
|
17
17
|
return false;
|
|
@@ -19,7 +19,7 @@ function notMac() {
|
|
|
19
19
|
/** Shared status readout — `status`, bare `menubar`, and `setup --check` all end here. */
|
|
20
20
|
function printStatus(s, opts = {}) {
|
|
21
21
|
const yn = (b) => (b ? chalk.green('yes') : chalk.gray('no'));
|
|
22
|
-
console.log(chalk.bold('Menu
|
|
22
|
+
console.log(chalk.bold('AGI Menu\n'));
|
|
23
23
|
console.log(` running ${yn(s.running)}`);
|
|
24
24
|
console.log(` service installed ${yn(s.serviceInstalled)}`);
|
|
25
25
|
if (opts.brief) {
|
|
@@ -34,7 +34,7 @@ function printStatus(s, opts = {}) {
|
|
|
34
34
|
// Two copies of the INSTALLED bundle is the duplicate the user sees as two
|
|
35
35
|
// agents marks in the menu bar. It used to read as a healthy `running: yes`.
|
|
36
36
|
if (s.instances.length > 1) {
|
|
37
|
-
console.log(chalk.yellow(`\n ${s.instances.length} copies of
|
|
37
|
+
console.log(chalk.yellow(`\n ${s.instances.length} copies of AGI Menu are running — that is the duplicate menu-bar icon:`));
|
|
38
38
|
for (const p of s.instances)
|
|
39
39
|
console.log(chalk.gray(` ${p.pid} ${p.executable}`));
|
|
40
40
|
console.log(chalk.gray(' Fix it with `agents menubar setup`.'));
|
|
@@ -45,20 +45,20 @@ function printStatus(s, opts = {}) {
|
|
|
45
45
|
// was — only that a rival exists — so report the conflict, not a winner.
|
|
46
46
|
// The loser has no other symptom: its chords simply never fire.
|
|
47
47
|
const n = s.foreignInstances.length;
|
|
48
|
-
console.log(chalk.yellow(`\n ${n} other
|
|
48
|
+
console.log(chalk.yellow(`\n ${n} other AGI Menu process${n === 1 ? '' : 'es'} running — ${n === 1 ? 'it' : 'they'} may hold Cmd-Shift-V/O instead of the installed one:`));
|
|
49
49
|
for (const p of s.foreignInstances)
|
|
50
50
|
console.log(chalk.gray(` ${p.pid} ${p.executable}`));
|
|
51
51
|
console.log(chalk.gray(' End them with `agents menubar setup`.'));
|
|
52
52
|
}
|
|
53
53
|
if (s.stale) {
|
|
54
|
-
console.log(chalk.yellow('\n Installed
|
|
54
|
+
console.log(chalk.yellow('\n Installed AGI Menu is stale — runs on next `agents` startup, or `agents menubar setup` now.'));
|
|
55
55
|
}
|
|
56
56
|
else if (!s.serviceInstalled && !s.disabledByUser) {
|
|
57
57
|
console.log(chalk.gray('\n Set it up with `agents menubar setup`.'));
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
function printSetupResult(r) {
|
|
61
|
-
console.log(chalk.bold('Menu
|
|
61
|
+
console.log(chalk.bold('AGI Menu setup\n'));
|
|
62
62
|
for (const step of r.steps) {
|
|
63
63
|
const mark = step.outcome === 'failed' ? chalk.red('✗')
|
|
64
64
|
: step.outcome === 'changed' ? chalk.green('+') : chalk.green('✓');
|
|
@@ -66,22 +66,22 @@ function printSetupResult(r) {
|
|
|
66
66
|
}
|
|
67
67
|
console.log();
|
|
68
68
|
if (r.configured) {
|
|
69
|
-
console.log(chalk.green('Menu
|
|
69
|
+
console.log(chalk.green('AGI Menu configured.') + chalk.gray(' One agents mark, started at login.'));
|
|
70
70
|
}
|
|
71
71
|
else {
|
|
72
|
-
console.log(chalk.red('Menu
|
|
72
|
+
console.log(chalk.red('AGI Menu not fully configured.') + chalk.gray(' See the failed step above.'));
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
export function registerMenubarCommands(program) {
|
|
76
76
|
const menubar = program
|
|
77
77
|
.command('menubar')
|
|
78
|
-
.description('Manage
|
|
78
|
+
.description('Manage AGI Menu (running sessions, agents awaiting input, routines)');
|
|
79
79
|
// `setup` is the one command that gets a machine to the intended state:
|
|
80
80
|
// exactly one status item, started at login. `enable` stays the narrow
|
|
81
81
|
// install+start; setup adds duplicate cleanup and verifies the end state.
|
|
82
82
|
const setup = menubar
|
|
83
83
|
.command('setup')
|
|
84
|
-
.description('Configure
|
|
84
|
+
.description('Configure AGI Menu end-to-end: one instance, started at login')
|
|
85
85
|
.option('--check', 'Report the current state, change nothing')
|
|
86
86
|
.option('--json', 'Emit machine-readable JSON')
|
|
87
87
|
.action((options) => {
|
|
@@ -110,7 +110,7 @@ export function registerMenubarCommands(program) {
|
|
|
110
110
|
});
|
|
111
111
|
setHelpSections(setup, {
|
|
112
112
|
examples: `
|
|
113
|
-
# Configure
|
|
113
|
+
# Configure AGI Menu end-to-end (idempotent — safe to re-run)
|
|
114
114
|
agents menubar setup
|
|
115
115
|
|
|
116
116
|
# Two agents marks in the menu bar? This ends the duplicate.
|
|
@@ -120,44 +120,44 @@ export function registerMenubarCommands(program) {
|
|
|
120
120
|
agents menubar setup --check
|
|
121
121
|
`,
|
|
122
122
|
notes: `
|
|
123
|
-
Configures, in order: every running helper ended,
|
|
124
|
-
~/Library/Application Support/agents-cli, its code
|
|
125
|
-
login item (com.phnx-labs.agents-menubar —
|
|
126
|
-
verifies exactly one helper came back up.
|
|
123
|
+
Configures, in order: every running helper ended, AGI Menu at
|
|
124
|
+
~/Library/Application Support/agents-cli/MenubarHelper.app, its code
|
|
125
|
+
signature, the launchd login item (com.phnx-labs.agents-menubar —
|
|
126
|
+
RunAtLoad + KeepAlive), then verifies exactly one helper came back up.
|
|
127
127
|
|
|
128
128
|
Every running helper is ended and launchd restarts one, so the survivor is
|
|
129
129
|
always the login-managed copy. Exits nonzero if it cannot reach that state.
|
|
130
130
|
|
|
131
|
-
Setup clears a previous \`agents menubar disable\`. To turn
|
|
131
|
+
Setup clears a previous \`agents menubar disable\`. To turn AGI Menu off
|
|
132
132
|
again, run \`agents menubar disable\`.
|
|
133
133
|
`,
|
|
134
134
|
});
|
|
135
135
|
menubar
|
|
136
136
|
.command('enable')
|
|
137
|
-
.description('Install and start
|
|
137
|
+
.description('Install and start AGI Menu (launches at login)')
|
|
138
138
|
.action(() => {
|
|
139
139
|
if (notMac())
|
|
140
140
|
return;
|
|
141
141
|
const ok = enableMenubarService({ clearOptOut: true });
|
|
142
142
|
if (!ok) {
|
|
143
|
-
console.log(chalk.red('Could not enable: no
|
|
143
|
+
console.log(chalk.red('Could not enable: no AGI Menu bundle ships with this install.'));
|
|
144
144
|
console.log(chalk.gray(' This build may predate the helper, or be a non-macOS package.'));
|
|
145
145
|
return;
|
|
146
146
|
}
|
|
147
|
-
console.log(chalk.green('Menu
|
|
147
|
+
console.log(chalk.green('AGI Menu enabled.') + chalk.gray(' Look for the agents mark in your menu bar.'));
|
|
148
148
|
});
|
|
149
149
|
menubar
|
|
150
150
|
.command('disable')
|
|
151
|
-
.description('Stop and remove
|
|
151
|
+
.description('Stop and remove AGI Menu (stays off across upgrades)')
|
|
152
152
|
.action(() => {
|
|
153
153
|
if (notMac())
|
|
154
154
|
return;
|
|
155
155
|
disableMenubarService();
|
|
156
|
-
console.log(chalk.green('Menu
|
|
156
|
+
console.log(chalk.green('AGI Menu disabled.') + chalk.gray(' Re-enable any time with `agents menubar setup`.'));
|
|
157
157
|
});
|
|
158
158
|
menubar
|
|
159
159
|
.command('status')
|
|
160
|
-
.description('Show whether
|
|
160
|
+
.description('Show whether AGI Menu is installed and running')
|
|
161
161
|
.option('--json', 'Emit machine-readable JSON')
|
|
162
162
|
.action((options) => {
|
|
163
163
|
const s = getMenubarStatus();
|
|
@@ -166,7 +166,7 @@ export function registerMenubarCommands(program) {
|
|
|
166
166
|
return;
|
|
167
167
|
}
|
|
168
168
|
if (s.platform !== 'darwin') {
|
|
169
|
-
console.log(chalk.yellow('
|
|
169
|
+
console.log(chalk.yellow('AGI Menu is macOS only.'));
|
|
170
170
|
return;
|
|
171
171
|
}
|
|
172
172
|
printStatus(s);
|
|
@@ -175,7 +175,7 @@ export function registerMenubarCommands(program) {
|
|
|
175
175
|
menubar.action(() => {
|
|
176
176
|
const s = getMenubarStatus();
|
|
177
177
|
if (s.platform !== 'darwin') {
|
|
178
|
-
console.log(chalk.yellow('
|
|
178
|
+
console.log(chalk.yellow('AGI Menu is macOS only.'));
|
|
179
179
|
return;
|
|
180
180
|
}
|
|
181
181
|
printStatus(s, { brief: true });
|
package/dist/commands/message.js
CHANGED
|
@@ -12,6 +12,7 @@ import { blockIdForSession, listBlocks, readBlock, recordAnswer, recordMessageRe
|
|
|
12
12
|
import { verifyOperatorIdentity } from '../lib/operator.js';
|
|
13
13
|
import { resolveAnswerRoute, resumeArgv, } from '../lib/answer-router.js';
|
|
14
14
|
import { injectIntoTerminal } from '../lib/terminal/inject.js';
|
|
15
|
+
import { setHelpSections } from '../lib/help.js';
|
|
15
16
|
/** Find the still-open block addressed to `mailboxId`, if any. */
|
|
16
17
|
function findOpenBlockForMailbox(mailboxId) {
|
|
17
18
|
// Fast path: the mailbox id is usually the session id, so the block id is
|
|
@@ -111,15 +112,34 @@ async function deliverViaResume(route, mailboxId) {
|
|
|
111
112
|
console.log(chalk.green(`Resumed ${mailboxId} with answer. `) +
|
|
112
113
|
chalk.dim(route.reason));
|
|
113
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* `message` is the agent-control plane (RUSH-2123): the answer/keystroke/
|
|
117
|
+
* injected input a running agent consumes, never a notification a human reads.
|
|
118
|
+
* Mirrors SHARED_NOTES in commands/send.ts so an agent reading either --help
|
|
119
|
+
* sees the same three-plane map and doesn't reach for `message` when it means
|
|
120
|
+
* `send`/`notify`.
|
|
121
|
+
*/
|
|
122
|
+
const CONTROL_PLANE_NOTES = `
|
|
123
|
+
Planes (do not mix them up):
|
|
124
|
+
message / sessions inject - CONTROL a running agent (mailbox answer, PTY keystroke, or resume by runtime)
|
|
125
|
+
send / notify - DELIVER a message to a human recipient over a channel provider
|
|
126
|
+
feed post - RECORD progress / milestones (optional broadcast may call send/notify)
|
|
127
|
+
|
|
128
|
+
<text> here is consumed BY THE TARGET AGENT (an answer, a keystroke, or the
|
|
129
|
+
argument to a resume) -- it is not a notification a person reads on their
|
|
130
|
+
phone. To reach the operator instead, use \`agents send\` / \`agents notify\`
|
|
131
|
+
(or a feed.broadcast \`channel:\` sink).
|
|
132
|
+
`;
|
|
114
133
|
export function registerMessageCommand(program) {
|
|
115
|
-
program
|
|
134
|
+
const messageCmd = program
|
|
116
135
|
.command('message <target> <text>')
|
|
117
136
|
.description('Send a message to a running or parked agent (mailbox / PTY-select / resume by runtime).')
|
|
118
137
|
.option('--from <who>', 'Label recorded as the sender of this message')
|
|
119
138
|
.option('--as <operator>', 'Verified operator id answering a high-consequence block')
|
|
120
139
|
.option('--surface <surface>', 'Surface that is sending this answer (feed, terminal, etc.)', 'cli')
|
|
121
|
-
.option('--ttl <dur>', 'Delivery TTL if the message is not consumed (e.g. 30m, 1h, 24h); 0 disables expiry')
|
|
122
|
-
|
|
140
|
+
.option('--ttl <dur>', 'Delivery TTL if the message is not consumed (e.g. 30m, 1h, 24h); 0 disables expiry');
|
|
141
|
+
setHelpSections(messageCmd, { notes: CONTROL_PLANE_NOTES });
|
|
142
|
+
messageCmd.action(async (target, text, opts) => {
|
|
123
143
|
if (!target.trim()) {
|
|
124
144
|
die('Target must be a session/agent id or cloud task id. Run `agents sessions --active` to list running agents.');
|
|
125
145
|
}
|
|
@@ -57,7 +57,7 @@ function actionLabel(action) {
|
|
|
57
57
|
case 'routine':
|
|
58
58
|
return `routine ${action.routine ?? ''}`;
|
|
59
59
|
case 'notify':
|
|
60
|
-
return `notify ${action.notifyChannel ?? '
|
|
60
|
+
return `notify ${action.notifyChannel ?? 'owner'}`;
|
|
61
61
|
case 'webhook-out':
|
|
62
62
|
return `webhook-out ${action.url ?? ''}`;
|
|
63
63
|
default:
|
|
@@ -195,9 +195,11 @@ function buildAction(options) {
|
|
|
195
195
|
if (options.routine)
|
|
196
196
|
chosen.push({ type: 'routine', routine: options.routine });
|
|
197
197
|
if (options.notify !== undefined) {
|
|
198
|
-
// --notify may be a bare flag (
|
|
199
|
-
|
|
200
|
-
|
|
198
|
+
// --notify may be a bare flag (notify the owner) or carry a channel that
|
|
199
|
+
// overrides notify.owner.channel. Left unset, the send resolves the owner
|
|
200
|
+
// channel + target from notify.owner in agents.yaml (one source of truth).
|
|
201
|
+
const channel = typeof options.notify === 'string' ? options.notify : undefined;
|
|
202
|
+
chosen.push({ type: 'notify', ...(channel ? { notifyChannel: channel } : {}) });
|
|
201
203
|
}
|
|
202
204
|
if (options.webhookOut)
|
|
203
205
|
chosen.push({ type: 'webhook-out', url: options.webhookOut });
|
|
@@ -310,7 +312,7 @@ export function registerMonitorsCommands(program) {
|
|
|
310
312
|
.option('--effort <effort>', 'Reasoning effort for --run: low | medium | high | xhigh | max | auto')
|
|
311
313
|
.option('--action-timeout <t>', 'Kill the --run action if it runs longer than this (e.g. 10m)')
|
|
312
314
|
.option('--routine <name>', 'Fire an existing routine on change')
|
|
313
|
-
.option('--notify [channel]', '
|
|
315
|
+
.option('--notify [channel]', 'Notify the owner (notify.owner); [channel] overrides the owner channel')
|
|
314
316
|
.option('--webhook-out <url>', 'POST the event to this URL')
|
|
315
317
|
// PLACEMENT / hygiene
|
|
316
318
|
.option('--device <name>', 'OWNER device — the single machine that evaluates + fires (exactly-once)')
|
|
@@ -573,7 +575,7 @@ export function registerMonitorsCommands(program) {
|
|
|
573
575
|
name,
|
|
574
576
|
source: { type: 'poll', command: 'echo hello', interval: '1m' },
|
|
575
577
|
condition: { mode: 'on-change' },
|
|
576
|
-
action: { type: 'notify'
|
|
578
|
+
action: { type: 'notify' },
|
|
577
579
|
});
|
|
578
580
|
fs.writeFileSync(monitorPath, template, 'utf-8');
|
|
579
581
|
console.log(chalk.gray(`Created new monitor file: ${monitorPath}`));
|
package/dist/commands/perf.d.ts
CHANGED
|
@@ -6,9 +6,22 @@
|
|
|
6
6
|
* agents perf hooks per-hook p50/p99 + cache hit rates
|
|
7
7
|
* agents perf commands slowest CLI command paths (from command.end)
|
|
8
8
|
* agents perf run agent.run / perf.timing labels
|
|
9
|
+
* agents perf friction sessions stuck repeatedly hitting the same guard
|
|
9
10
|
*
|
|
10
11
|
* Soft-joins sessions.db via shared string keys (session_id, agent, machine) —
|
|
11
12
|
* no foreign keys. Warehouse lives at ~/.agents/.cache/perf/perf.db (safe to wipe).
|
|
12
13
|
*/
|
|
13
14
|
import type { Command } from 'commander';
|
|
15
|
+
import { type PerfAggregateRow } from '../lib/perf/db.js';
|
|
16
|
+
import { type HookProfileRow } from '../lib/hooks/profile.js';
|
|
17
|
+
/** Map warehouse rows shaped like hook.fire into the existing HookProfileRow UI. */
|
|
18
|
+
export declare function asHookRows(rows: PerfAggregateRow[]): HookProfileRow[];
|
|
19
|
+
export declare function renderHookTable(rows: HookProfileRow[], warnMs: number): void;
|
|
20
|
+
/**
|
|
21
|
+
* Prefer SQLite samples; fall back to the legacy daily JSONL so existing
|
|
22
|
+
* instrumentation still surfaces until shims are resynced. `project` only
|
|
23
|
+
* narrows the SQLite path — the legacy JSONL log has no cwd, so a fallback
|
|
24
|
+
* hit ignores it (a caller filtering by project has no legacy rows to miss).
|
|
25
|
+
*/
|
|
26
|
+
export declare function loadHookProfile(days: number, project?: string): HookProfileRow[];
|
|
14
27
|
export declare function registerPerfCommand(program: Command): void;
|
package/dist/commands/perf.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* agents perf hooks per-hook p50/p99 + cache hit rates
|
|
7
7
|
* agents perf commands slowest CLI command paths (from command.end)
|
|
8
8
|
* agents perf run agent.run / perf.timing labels
|
|
9
|
+
* agents perf friction sessions stuck repeatedly hitting the same guard
|
|
9
10
|
*
|
|
10
11
|
* Soft-joins sessions.db via shared string keys (session_id, agent, machine) —
|
|
11
12
|
* no foreign keys. Warehouse lives at ~/.agents/.cache/perf/perf.db (safe to wipe).
|
|
@@ -13,6 +14,8 @@
|
|
|
13
14
|
import chalk from 'chalk';
|
|
14
15
|
import { aggregateSamples, perfDbPath, } from '../lib/perf/db.js';
|
|
15
16
|
import { formatMs, formatCacheColumn, DEFAULT_SLOW_HOOK_WARN_MS, loadHookFireEvents, aggregateHookProfile, } from '../lib/hooks/profile.js';
|
|
17
|
+
import { query } from '../lib/events.js';
|
|
18
|
+
import { detectRepeatedGuardBlocks } from '../lib/friction-heuristics.js';
|
|
16
19
|
function parseDays(raw) {
|
|
17
20
|
const n = parseInt(raw ?? '7', 10);
|
|
18
21
|
return Number.isFinite(n) && n > 0 ? n : 7;
|
|
@@ -26,11 +29,12 @@ function parseLimit(raw, fallback) {
|
|
|
26
29
|
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
27
30
|
}
|
|
28
31
|
/** Map warehouse rows shaped like hook.fire into the existing HookProfileRow UI. */
|
|
29
|
-
function asHookRows(rows) {
|
|
32
|
+
export function asHookRows(rows) {
|
|
30
33
|
return rows.map((r) => ({
|
|
31
34
|
hook: r.label,
|
|
32
35
|
n: r.n,
|
|
33
36
|
p50Ms: r.p50Ms,
|
|
37
|
+
p95Ms: r.p95Ms,
|
|
34
38
|
p99Ms: r.p99Ms,
|
|
35
39
|
meanMs: r.meanMs,
|
|
36
40
|
maxMs: r.maxMs,
|
|
@@ -38,6 +42,9 @@ function asHookRows(rows) {
|
|
|
38
42
|
cacheStalePct: r.cacheStalePct ?? 0,
|
|
39
43
|
cacheMissPct: r.cacheMissPct ?? 0,
|
|
40
44
|
errorCount: r.errorCount ?? 0,
|
|
45
|
+
errorRate: r.errorRate,
|
|
46
|
+
timeoutRate: r.timeoutRate,
|
|
47
|
+
project: r.project,
|
|
41
48
|
}));
|
|
42
49
|
}
|
|
43
50
|
function printTable(headers, widths, lines, slowFlags) {
|
|
@@ -50,23 +57,34 @@ function printTable(headers, widths, lines, slowFlags) {
|
|
|
50
57
|
console.log(slowFlags[i] ? chalk.yellow(line) : line);
|
|
51
58
|
}
|
|
52
59
|
}
|
|
53
|
-
|
|
60
|
+
/** `err:12% to:4%` when either rate is present, else ''. */
|
|
61
|
+
function formatRateColumn(r) {
|
|
62
|
+
const parts = [];
|
|
63
|
+
if (r.errorRate)
|
|
64
|
+
parts.push(`err:${Math.round(r.errorRate * 100)}%`);
|
|
65
|
+
if (r.timeoutRate)
|
|
66
|
+
parts.push(`to:${Math.round(r.timeoutRate * 100)}%`);
|
|
67
|
+
return parts.join(' ');
|
|
68
|
+
}
|
|
69
|
+
export function renderHookTable(rows, warnMs) {
|
|
54
70
|
if (rows.length === 0) {
|
|
55
71
|
console.log(chalk.gray('No hook timing samples yet.'));
|
|
56
72
|
console.log(chalk.gray(`Warehouse: ${perfDbPath()}`));
|
|
57
73
|
console.log(chalk.gray('Hooks write via cache/matches shims into the spool; run a session or resync hooks.'));
|
|
58
74
|
return;
|
|
59
75
|
}
|
|
60
|
-
const widths = { hook: 36, n: 5, p50: 7, p99: 7, mean: 7, max: 7, cache:
|
|
76
|
+
const widths = { hook: 36, n: 5, p50: 7, p95: 7, p99: 7, mean: 7, max: 7, cache: 22, rate: 14 };
|
|
61
77
|
const pad = (s, w) => (s.length >= w ? s.slice(0, w) : s + ' '.repeat(w - s.length));
|
|
62
78
|
const header = [
|
|
63
79
|
pad('HOOK', widths.hook),
|
|
64
80
|
pad('N', widths.n),
|
|
65
81
|
pad('P50', widths.p50),
|
|
82
|
+
pad('P95', widths.p95),
|
|
66
83
|
pad('P99', widths.p99),
|
|
67
84
|
pad('MEAN', widths.mean),
|
|
68
85
|
pad('MAX', widths.max),
|
|
69
86
|
pad('CACHE', widths.cache),
|
|
87
|
+
pad('ERR/TIMEOUT', widths.rate),
|
|
70
88
|
].join(' ');
|
|
71
89
|
console.log(chalk.bold(header));
|
|
72
90
|
console.log(chalk.gray('─'.repeat(header.length)));
|
|
@@ -78,10 +96,12 @@ function renderHookTable(rows, warnMs) {
|
|
|
78
96
|
pad(r.hook, widths.hook),
|
|
79
97
|
pad(String(r.n), widths.n),
|
|
80
98
|
pad(formatMs(r.p50Ms), widths.p50),
|
|
99
|
+
pad(formatMs(r.p95Ms), widths.p95),
|
|
81
100
|
pad(formatMs(r.p99Ms), widths.p99),
|
|
82
101
|
pad(formatMs(r.meanMs), widths.mean),
|
|
83
102
|
pad(formatMs(r.maxMs), widths.max),
|
|
84
103
|
pad(cacheCol, widths.cache),
|
|
104
|
+
pad(formatRateColumn(r), widths.rate),
|
|
85
105
|
].join(' ') + warning;
|
|
86
106
|
console.log(slow ? chalk.yellow(line) : line);
|
|
87
107
|
}
|
|
@@ -92,30 +112,34 @@ function renderLabelTable(title, rows, warnMs, limit) {
|
|
|
92
112
|
console.log(chalk.gray(`No ${title} samples yet.`));
|
|
93
113
|
return;
|
|
94
114
|
}
|
|
95
|
-
const widths = [40, 5, 7, 7, 7, 7];
|
|
96
|
-
printTable(['LABEL', 'N', 'P50', 'P99', 'MEAN', 'MAX'], widths, sliced.map((r) => [
|
|
115
|
+
const widths = [40, 5, 7, 7, 7, 7, 7, 14];
|
|
116
|
+
printTable(['LABEL', 'N', 'P50', 'P95', 'P99', 'MEAN', 'MAX', 'ERR/TIMEOUT'], widths, sliced.map((r) => [
|
|
97
117
|
r.label,
|
|
98
118
|
String(r.n),
|
|
99
119
|
formatMs(r.p50Ms),
|
|
120
|
+
formatMs(r.p95Ms),
|
|
100
121
|
formatMs(r.p99Ms),
|
|
101
122
|
formatMs(r.meanMs),
|
|
102
123
|
formatMs(r.maxMs),
|
|
124
|
+
formatRateColumn(r),
|
|
103
125
|
]), sliced.map((r) => r.p99Ms > warnMs));
|
|
104
126
|
}
|
|
105
127
|
/**
|
|
106
128
|
* Prefer SQLite samples; fall back to the legacy daily JSONL so existing
|
|
107
|
-
* instrumentation still surfaces until shims are resynced.
|
|
129
|
+
* instrumentation still surfaces until shims are resynced. `project` only
|
|
130
|
+
* narrows the SQLite path — the legacy JSONL log has no cwd, so a fallback
|
|
131
|
+
* hit ignores it (a caller filtering by project has no legacy rows to miss).
|
|
108
132
|
*/
|
|
109
|
-
function loadHookProfile(days) {
|
|
110
|
-
const fromDb = asHookRows(aggregateSamples({ days, kinds: ['hook.fire'] }));
|
|
111
|
-
if (fromDb.length > 0)
|
|
133
|
+
export function loadHookProfile(days, project) {
|
|
134
|
+
const fromDb = asHookRows(aggregateSamples({ days, kinds: ['hook.fire'], project }));
|
|
135
|
+
if (fromDb.length > 0 || project)
|
|
112
136
|
return fromDb;
|
|
113
137
|
return aggregateHookProfile(loadHookFireEvents(days));
|
|
114
138
|
}
|
|
115
139
|
function hooksAction(opts) {
|
|
116
140
|
const days = parseDays(opts.days);
|
|
117
141
|
const warnMs = parseWarnMs(opts.warnMs, DEFAULT_SLOW_HOOK_WARN_MS);
|
|
118
|
-
const rows = loadHookProfile(days);
|
|
142
|
+
const rows = loadHookProfile(days, opts.project);
|
|
119
143
|
if (opts.json) {
|
|
120
144
|
console.log(JSON.stringify(rows, null, 2));
|
|
121
145
|
return;
|
|
@@ -126,7 +150,7 @@ function commandsAction(opts) {
|
|
|
126
150
|
const days = parseDays(opts.days);
|
|
127
151
|
const warnMs = parseWarnMs(opts.warnMs, 500);
|
|
128
152
|
const limit = parseLimit(opts.limit, 40);
|
|
129
|
-
const rows = aggregateSamples({ days, kinds: ['command.end'] });
|
|
153
|
+
const rows = aggregateSamples({ days, kinds: ['command.end'], project: opts.project });
|
|
130
154
|
if (opts.json) {
|
|
131
155
|
console.log(JSON.stringify(rows.slice(0, limit), null, 2));
|
|
132
156
|
return;
|
|
@@ -137,42 +161,69 @@ function runAction(opts) {
|
|
|
137
161
|
const days = parseDays(opts.days);
|
|
138
162
|
const warnMs = parseWarnMs(opts.warnMs, 60_000);
|
|
139
163
|
const limit = parseLimit(opts.limit, 40);
|
|
140
|
-
const rows = aggregateSamples({ days, kinds: ['perf.timing'] });
|
|
164
|
+
const rows = aggregateSamples({ days, kinds: ['perf.timing'], project: opts.project });
|
|
141
165
|
if (opts.json) {
|
|
142
166
|
console.log(JSON.stringify(rows.slice(0, limit), null, 2));
|
|
143
167
|
return;
|
|
144
168
|
}
|
|
145
169
|
renderLabelTable('run/timing', rows, warnMs, limit);
|
|
146
170
|
}
|
|
171
|
+
/**
|
|
172
|
+
* Sessions stuck repeatedly hitting the SAME guard block (git-guard,
|
|
173
|
+
* rm-guard, git-require-clean-tree, …) instead of adapting after the first
|
|
174
|
+
* denial. Reads the `friction` event sink (emitFriction in events.ts) that
|
|
175
|
+
* guard hooks self-report into via `agents _internal friction` before they
|
|
176
|
+
* exit 2 — see lib/friction-heuristics.ts for the grouping.
|
|
177
|
+
*/
|
|
178
|
+
function frictionAction(opts) {
|
|
179
|
+
const days = parseDays(opts.days);
|
|
180
|
+
const startDate = new Date(Date.now() - days * 86_400_000);
|
|
181
|
+
const events = query({ eventTypes: ['friction'], startDate });
|
|
182
|
+
const findings = detectRepeatedGuardBlocks(events);
|
|
183
|
+
if (opts.json) {
|
|
184
|
+
console.log(JSON.stringify(findings, null, 2));
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (findings.length === 0) {
|
|
188
|
+
console.log(chalk.gray(`No repeated guard blocks in the last ${days} day${days === 1 ? '' : 's'}.`));
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
console.log(chalk.bold(`Repeated guard blocks — last ${days} day${days === 1 ? '' : 's'}\n`));
|
|
192
|
+
const widths = [14, 10, 24, 5, 22, 22];
|
|
193
|
+
printTable(['SESSION', 'SURFACE', 'FAILURE ID', 'N', 'FIRST', 'LAST'], widths, findings.map((f) => [f.session, f.surface, f.failureId, String(f.count), f.firstTs, f.lastTs]), findings.map(() => false));
|
|
194
|
+
}
|
|
147
195
|
function summaryAction(opts) {
|
|
148
196
|
const days = parseDays(opts.days);
|
|
197
|
+
const project = opts.project;
|
|
149
198
|
if (opts.json) {
|
|
150
199
|
console.log(JSON.stringify({
|
|
151
200
|
days,
|
|
201
|
+
project: project ?? null,
|
|
152
202
|
warehouse: perfDbPath(),
|
|
153
|
-
hooks: loadHookProfile(days),
|
|
154
|
-
commands: aggregateSamples({ days, kinds: ['command.end'] }).slice(0, 20),
|
|
155
|
-
run: aggregateSamples({ days, kinds: ['perf.timing'] }).slice(0, 20),
|
|
203
|
+
hooks: loadHookProfile(days, project),
|
|
204
|
+
commands: aggregateSamples({ days, kinds: ['command.end'], project }).slice(0, 20),
|
|
205
|
+
run: aggregateSamples({ days, kinds: ['perf.timing'], project }).slice(0, 20),
|
|
156
206
|
}, null, 2));
|
|
157
207
|
return;
|
|
158
208
|
}
|
|
159
|
-
console.log(chalk.bold(`agents perf — last ${days} day${days === 1 ? '' : 's'}`));
|
|
209
|
+
console.log(chalk.bold(`agents perf — last ${days} day${days === 1 ? '' : 's'}${project ? ` — project ${project}` : ''}`));
|
|
160
210
|
console.log(chalk.gray(`warehouse: ${perfDbPath()} (disposable; soft-join sessions via session_id/agent/machine)`));
|
|
161
211
|
console.log('');
|
|
162
212
|
console.log(chalk.bold('Commands (slowest by p99)'));
|
|
163
|
-
renderLabelTable('command', aggregateSamples({ days, kinds: ['command.end'] }), parseWarnMs(opts.warnMs, 500), 12);
|
|
213
|
+
renderLabelTable('command', aggregateSamples({ days, kinds: ['command.end'], project }), parseWarnMs(opts.warnMs, 500), 12);
|
|
164
214
|
console.log('');
|
|
165
215
|
console.log(chalk.bold('Hooks'));
|
|
166
|
-
renderHookTable(loadHookProfile(days), parseWarnMs(opts.warnMs, DEFAULT_SLOW_HOOK_WARN_MS));
|
|
216
|
+
renderHookTable(loadHookProfile(days, project), parseWarnMs(opts.warnMs, DEFAULT_SLOW_HOOK_WARN_MS));
|
|
167
217
|
console.log('');
|
|
168
218
|
console.log(chalk.bold('Runs (perf.timing)'));
|
|
169
|
-
renderLabelTable('run/timing', aggregateSamples({ days, kinds: ['perf.timing'] }), parseWarnMs(opts.warnMs, 60_000), 12);
|
|
219
|
+
renderLabelTable('run/timing', aggregateSamples({ days, kinds: ['perf.timing'], project }), parseWarnMs(opts.warnMs, 60_000), 12);
|
|
170
220
|
}
|
|
171
221
|
function attachSharedOptions(cmd) {
|
|
172
222
|
return cmd
|
|
173
223
|
.option('--days <n>', 'Days of samples to include', '7')
|
|
174
224
|
.option('--warn-ms <n>', 'p99 above this is highlighted')
|
|
175
225
|
.option('--limit <n>', 'Max rows in the table', '40')
|
|
226
|
+
.option('--project <key>', 'Scope to one project (the repo directory name a sample\'s cwd resolves to — see project-key.ts)')
|
|
176
227
|
.option('--json', 'Emit JSON instead of a table');
|
|
177
228
|
}
|
|
178
229
|
/**
|
|
@@ -195,11 +246,13 @@ Identity columns reuse sessions/events string shapes (session_id, agent, machine
|
|
|
195
246
|
for soft cross-reference; there are no foreign keys.
|
|
196
247
|
|
|
197
248
|
Examples:
|
|
198
|
-
agents perf
|
|
199
|
-
agents perf hooks
|
|
200
|
-
agents perf commands --days 30
|
|
201
|
-
agents perf run --json
|
|
249
|
+
agents perf # summary: commands + hooks + runs
|
|
250
|
+
agents perf hooks # per-hook p50/p95/p99 + cache hit rate
|
|
251
|
+
agents perf commands --days 30 # slowest CLI entrypoints
|
|
252
|
+
agents perf run --json # agent.run timings as JSON
|
|
202
253
|
agents perf hooks --warn-ms 500
|
|
254
|
+
agents perf hooks --project agents-cli # scope to one repo's samples
|
|
255
|
+
agents perf friction # sessions stuck retrying the same guard block
|
|
203
256
|
`);
|
|
204
257
|
// Options live on the parent so `agents perf --json` and
|
|
205
258
|
// `agents perf commands --json` both work (see leafOpts).
|
|
@@ -218,4 +271,8 @@ Examples:
|
|
|
218
271
|
.action(function run() {
|
|
219
272
|
runAction(leafOpts(this));
|
|
220
273
|
});
|
|
274
|
+
perf.command('friction').description('Sessions stuck repeatedly hitting the same guard block')
|
|
275
|
+
.action(function friction() {
|
|
276
|
+
frictionAction(leafOpts(this));
|
|
277
|
+
});
|
|
221
278
|
}
|
|
@@ -20,6 +20,8 @@ export declare const PROJECTS_NO_FANOUT_ENV = "AGENTS_PROJECTS_LOCAL";
|
|
|
20
20
|
* string when everything answered.
|
|
21
21
|
*/
|
|
22
22
|
export declare function formatFleetSkippedNote(skipped: string[]): string;
|
|
23
|
+
/** Best-effort `owner/repo` from a repo's origin remote. */
|
|
24
|
+
export declare function originSlug(cwd: string): string | undefined;
|
|
23
25
|
/** One `projects list` row, pre-render. */
|
|
24
26
|
export interface ProjectListRow {
|
|
25
27
|
name: string;
|
|
@@ -45,6 +47,15 @@ export declare function computeProjectListWidths(rows: ProjectListRow[]): {
|
|
|
45
47
|
* read it as UTC and shift the answer by a day for anyone west of Greenwich.
|
|
46
48
|
*/
|
|
47
49
|
export declare function formatMilestoneDue(targetDate: string, nowMs: number): string | undefined;
|
|
50
|
+
/**
|
|
51
|
+
* The milestone block. `status` prints one line — the next checkpoint — because
|
|
52
|
+
* a roll-up across projects has to stay scannable. `view` prints every declared
|
|
53
|
+
* milestone, because "how many are there and when are they due" is the shape of
|
|
54
|
+
* the plan and the reason to open one project.
|
|
55
|
+
*
|
|
56
|
+
* Pure (chalk only) so the layout is testable without a Linear account.
|
|
57
|
+
*/
|
|
58
|
+
export declare function formatMilestoneLines(milestones: LinearMilestone[], next: LinearMilestone | undefined, nowMs: number, limit: number): string[];
|
|
48
59
|
/** The `next` card line: what this project is due to hit, and how far along it is. */
|
|
49
60
|
export declare function formatNextMilestone(ms: LinearMilestone, nowMs: number): string;
|
|
50
61
|
export declare function registerProjectsCommands(program: Command): void;
|