@phnx-labs/agents-cli 1.20.89 → 1.20.91
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 +361 -0
- package/README.md +6 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/exec.js +7 -1
- package/dist/commands/feed.js +77 -4
- package/dist/commands/harness.d.ts +27 -0
- package/dist/commands/harness.js +120 -13
- package/dist/commands/hooks.js +22 -6
- package/dist/commands/perf.d.ts +14 -0
- package/dist/commands/perf.js +221 -0
- package/dist/commands/profiles.d.ts +3 -0
- package/dist/commands/profiles.js +1 -1
- package/dist/commands/routines.d.ts +19 -0
- package/dist/commands/routines.js +58 -30
- package/dist/commands/secrets.d.ts +52 -4
- package/dist/commands/secrets.js +234 -37
- package/dist/commands/send.d.ts +5 -1
- package/dist/commands/send.js +1 -1
- package/dist/commands/sessions-browser.d.ts +4 -0
- package/dist/commands/sessions-browser.js +51 -9
- package/dist/commands/sessions-favorite.d.ts +20 -0
- package/dist/commands/sessions-favorite.js +120 -0
- package/dist/commands/sessions-picker.js +70 -1
- package/dist/commands/sessions.d.ts +103 -20
- package/dist/commands/sessions.js +356 -62
- package/dist/commands/setup-secrets.d.ts +7 -0
- package/dist/commands/setup-secrets.js +12 -9
- package/dist/commands/versions.js +12 -4
- package/dist/commands/view.d.ts +14 -1
- package/dist/commands/view.js +103 -128
- package/dist/index.js +18 -3
- package/dist/lib/activity.d.ts +11 -1
- package/dist/lib/activity.js +1 -0
- package/dist/lib/agents.d.ts +4 -2
- package/dist/lib/agents.js +21 -6
- package/dist/lib/catchup.d.ts +105 -0
- package/dist/lib/catchup.js +160 -0
- package/dist/lib/channels/providers/desktop.d.ts +49 -0
- package/dist/lib/channels/providers/desktop.js +132 -0
- package/dist/lib/channels/providers/index.js +2 -0
- package/dist/lib/daemon.js +74 -13
- package/dist/lib/events.d.ts +12 -0
- package/dist/lib/events.js +122 -9
- package/dist/lib/exec.js +10 -0
- package/dist/lib/feed-broadcast.d.ts +47 -0
- package/dist/lib/feed-broadcast.js +65 -1
- package/dist/lib/feed-post.d.ts +10 -0
- package/dist/lib/feed-post.js +1 -1
- package/dist/lib/feed.d.ts +47 -1
- package/dist/lib/feed.js +38 -0
- package/dist/lib/hooks/cache.d.ts +2 -0
- package/dist/lib/hooks/cache.js +24 -4
- package/dist/lib/hosts/dispatch.js +19 -1
- package/dist/lib/hq/floor.js +12 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/overdue.d.ts +14 -0
- package/dist/lib/overdue.js +37 -1
- package/dist/lib/perf/db.d.ts +25 -0
- package/dist/lib/perf/db.js +290 -0
- package/dist/lib/perf/spool.d.ts +18 -0
- package/dist/lib/perf/spool.js +79 -0
- package/dist/lib/perf/types.d.ts +45 -0
- package/dist/lib/perf/types.js +2 -0
- package/dist/lib/picker.d.ts +27 -2
- package/dist/lib/picker.js +71 -7
- package/dist/lib/profiles.d.ts +48 -0
- package/dist/lib/profiles.js +67 -0
- package/dist/lib/rotate.d.ts +24 -2
- package/dist/lib/rotate.js +63 -6
- package/dist/lib/routines-project.js +6 -0
- package/dist/lib/routines.d.ts +30 -1
- package/dist/lib/routines.js +11 -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/list-filter.d.ts +94 -0
- package/dist/lib/secrets/list-filter.js +245 -0
- package/dist/lib/session/active.d.ts +109 -3
- package/dist/lib/session/active.js +269 -13
- package/dist/lib/session/db.d.ts +14 -0
- package/dist/lib/session/db.js +35 -0
- package/dist/lib/session/digest.d.ts +7 -0
- package/dist/lib/session/digest.js +29 -1
- package/dist/lib/session/discover.d.ts +1 -2
- package/dist/lib/session/discover.js +7 -24
- package/dist/lib/session/favorites.d.ts +39 -0
- package/dist/lib/session/favorites.js +101 -0
- package/dist/lib/session/highlights.d.ts +82 -0
- package/dist/lib/session/highlights.js +251 -0
- package/dist/lib/session/host-link.d.ts +68 -0
- package/dist/lib/session/host-link.js +64 -0
- package/dist/lib/session/parse.js +23 -1
- package/dist/lib/session/presence.d.ts +85 -0
- package/dist/lib/session/presence.js +150 -0
- package/dist/lib/session/relative-time.d.ts +14 -0
- package/dist/lib/session/relative-time.js +36 -0
- package/dist/lib/session/remote-list.d.ts +10 -0
- package/dist/lib/session/remote-list.js +47 -9
- package/dist/lib/session/render.d.ts +7 -0
- package/dist/lib/session/render.js +87 -17
- package/dist/lib/session/types.d.ts +4 -1
- 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 +9 -0
- package/dist/lib/state.js +11 -0
- package/dist/lib/tmux/binary.d.ts +7 -0
- package/dist/lib/tmux/binary.js +11 -1
- package/dist/lib/types.d.ts +4 -3
- package/dist/lib/usage-backoff.d.ts +29 -0
- package/dist/lib/usage-backoff.js +165 -0
- package/dist/lib/usage.d.ts +112 -5
- package/dist/lib/usage.js +464 -46
- package/dist/lib/watchdog/runner.d.ts +13 -0
- package/dist/lib/watchdog/runner.js +16 -1
- package/package.json +3 -1
package/dist/commands/harness.js
CHANGED
|
@@ -11,15 +11,82 @@
|
|
|
11
11
|
* native harness registry. The `agents profiles` tree stays unchanged.
|
|
12
12
|
*/
|
|
13
13
|
import chalk from 'chalk';
|
|
14
|
-
import { addProfile } from './profiles.js';
|
|
15
|
-
import { listProfiles, readProfile, deleteProfile, profileHostLabel, profileProviderLabel, profileModelLabel, profileAuthLabel, getProfilePath, } from '../lib/profiles.js';
|
|
14
|
+
import { addProfile, ensureProviderToken } from './profiles.js';
|
|
15
|
+
import { listProfiles, readProfile, writeProfile, deleteProfile, profileExists, profileHostLabel, profileProviderLabel, profileModelLabel, profileAuthLabel, profileLabel, forkProfile, profileFromHostModel, authEnvKeyForHost, getProfilePath, validateProfileName, } from '../lib/profiles.js';
|
|
16
16
|
import { listPresets } from '../lib/profiles-presets.js';
|
|
17
|
-
import { AGENTS, ALL_AGENT_IDS } from '../lib/agents.js';
|
|
17
|
+
import { AGENTS, ALL_AGENT_IDS, resolveAgentName } from '../lib/agents.js';
|
|
18
18
|
/** Short capability summary for a native harness — its supported run modes. */
|
|
19
19
|
function nativeModes(id) {
|
|
20
20
|
const modes = AGENTS[id]?.capabilities?.modes ?? [];
|
|
21
21
|
return modes.length ? modes.join('/') : '-';
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Print one custom harness. Shared by `agents harness view <name>` and by
|
|
25
|
+
* `agents view <name>` — a custom harness resolves as an agent type there, so
|
|
26
|
+
* both entry points must describe it identically.
|
|
27
|
+
*/
|
|
28
|
+
export function renderHarnessDetail(name) {
|
|
29
|
+
const p = readProfile(name);
|
|
30
|
+
console.log(chalk.bold(profileLabel(p)) + chalk.gray(' (custom harness)'));
|
|
31
|
+
if (p.description)
|
|
32
|
+
console.log(chalk.gray(p.description));
|
|
33
|
+
console.log('');
|
|
34
|
+
console.log(`Host: ${profileHostLabel(p)}`);
|
|
35
|
+
console.log(`Model: ${profileModelLabel(p)}`);
|
|
36
|
+
if (p.fallback_model)
|
|
37
|
+
console.log(`Fallback: ${p.fallback_model}`);
|
|
38
|
+
console.log(`Provider: ${profileProviderLabel(p)}`);
|
|
39
|
+
console.log(`Auth: ${profileAuthLabel(p)}`);
|
|
40
|
+
if (p.forkedFrom)
|
|
41
|
+
console.log(`Forked: from ${p.forkedFrom}`);
|
|
42
|
+
console.log(chalk.gray(getProfilePath(p.name)));
|
|
43
|
+
console.log('');
|
|
44
|
+
console.log(chalk.gray(`Run: agents run ${p.name} "hello"`));
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Build the new harness for `agents harness fork <source> <name>`.
|
|
48
|
+
*
|
|
49
|
+
* Two sources, one verb: an existing custom harness is copied and overridden;
|
|
50
|
+
* a native agent id is turned into a harness pinned to `--model` on that host.
|
|
51
|
+
* Forking a native harness therefore requires `--model` — there is nothing to
|
|
52
|
+
* copy a model from.
|
|
53
|
+
*/
|
|
54
|
+
export function buildFork(source, name, opts) {
|
|
55
|
+
if (profileExists(source)) {
|
|
56
|
+
return forkProfile(readProfile(source), name, {
|
|
57
|
+
model: opts.model,
|
|
58
|
+
baseUrl: opts.baseUrl,
|
|
59
|
+
provider: opts.authProvider,
|
|
60
|
+
version: opts.version,
|
|
61
|
+
label: opts.label,
|
|
62
|
+
description: opts.description,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
const host = resolveAgentName(source);
|
|
66
|
+
if (!host) {
|
|
67
|
+
throw new Error(`No harness or agent named '${source}'.\n` +
|
|
68
|
+
`Fork from a custom harness (agents harness list) or a native one: ${ALL_AGENT_IDS.join(', ')}.`);
|
|
69
|
+
}
|
|
70
|
+
if (!opts.model) {
|
|
71
|
+
throw new Error(`--model <id> is required when forking the native '${host}' harness (there is no model to inherit).`);
|
|
72
|
+
}
|
|
73
|
+
return profileFromHostModel(name, host, opts.model, {
|
|
74
|
+
version: opts.version,
|
|
75
|
+
baseUrl: opts.baseUrl,
|
|
76
|
+
provider: opts.authProvider,
|
|
77
|
+
authEnvVar: opts.authProvider ? authEnvKeyForHostOrThrow(host) : undefined,
|
|
78
|
+
label: opts.label,
|
|
79
|
+
description: opts.description ?? `Forked from ${host}: ${opts.model}`,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
/** Auth env var for a host, as a hard error when the host declares none. */
|
|
83
|
+
function authEnvKeyForHostOrThrow(host) {
|
|
84
|
+
const key = authEnvKeyForHost(host);
|
|
85
|
+
if (!key) {
|
|
86
|
+
throw new Error(`--auth-provider is set but host '${host}' has no known auth env var; it manages its own login.`);
|
|
87
|
+
}
|
|
88
|
+
return key;
|
|
89
|
+
}
|
|
23
90
|
export function registerHarnessCommands(program) {
|
|
24
91
|
const cmd = program
|
|
25
92
|
.command('harness')
|
|
@@ -30,11 +97,18 @@ A custom harness pins a host CLI (opencode, claude, codex, grok, antigravity, ..
|
|
|
30
97
|
model and gives it a name. 'agents run <name>' then behaves like a native agent
|
|
31
98
|
type, and 'agents repo push user' syncs it to every device.
|
|
32
99
|
|
|
100
|
+
A custom harness is its own agent type in 'agents view' — its own block beside Claude
|
|
101
|
+
and Codex, not a row indented under the host CLI that executes it.
|
|
102
|
+
|
|
33
103
|
Examples:
|
|
34
104
|
# Meta Muse Spark 1.1 through OpenCode, called 'spark'
|
|
35
105
|
agents harness add spark --host opencode --model meta/muse-spark-1.1
|
|
36
106
|
agents run spark "refactor api/handlers/checkout.py"
|
|
37
107
|
|
|
108
|
+
# Fork a native harness, or copy one of your own and swap the model
|
|
109
|
+
agents harness fork opencode deepseek --model deepseek/deepseek-v4-flash-0731 --auth-provider openrouter
|
|
110
|
+
agents harness fork deepseek deepseek-chat --model deepseek/deepseek-chat-v3
|
|
111
|
+
|
|
38
112
|
# Per-run model override still wins
|
|
39
113
|
agents run spark --model opencode/big-pickle "quick pass"
|
|
40
114
|
|
|
@@ -66,6 +140,48 @@ Examples:
|
|
|
66
140
|
process.exit(1);
|
|
67
141
|
}
|
|
68
142
|
});
|
|
143
|
+
cmd
|
|
144
|
+
.command('fork <source> <name>')
|
|
145
|
+
.description('Fork a native harness (claude, opencode, ...) or an existing custom one into a new named harness.')
|
|
146
|
+
.option('--model <id>', 'Model to pin on the fork (required when forking a native harness)')
|
|
147
|
+
.option('--base-url <url>', 'Custom endpoint base URL (claude/codex hosts)')
|
|
148
|
+
.option('--auth-provider <provider>', 'Attach a keychain-backed API key under this provider')
|
|
149
|
+
.option('--version <version>', 'Pin the host CLI version (e.g., 1.16.0)')
|
|
150
|
+
.option('--label <text>', 'Human-facing name shown by `agents view` (defaults to <name>)')
|
|
151
|
+
.option('--description <text>', 'One-line description')
|
|
152
|
+
.option('--key-stdin', 'Read the API key from stdin instead of prompting (for scripts/CI)')
|
|
153
|
+
.option('--force', 'Overwrite an existing harness with the same name')
|
|
154
|
+
.addHelpText('after', `
|
|
155
|
+
Examples:
|
|
156
|
+
# Fork OpenCode into a harness pinned to a DeepSeek model on OpenRouter
|
|
157
|
+
agents harness fork opencode deepseek --model deepseek/deepseek-v4-flash-0731 --auth-provider openrouter
|
|
158
|
+
|
|
159
|
+
# Fork Claude Code onto a private gateway
|
|
160
|
+
agents harness fork claude corp --model gpt-x --base-url https://gw.corp/v1 --auth-provider corp
|
|
161
|
+
|
|
162
|
+
# Copy an existing harness and swap only the model
|
|
163
|
+
agents harness fork deepseek deepseek-chat --model deepseek/deepseek-chat-v3
|
|
164
|
+
`)
|
|
165
|
+
.action(async (source, name, opts) => {
|
|
166
|
+
try {
|
|
167
|
+
validateProfileName(name);
|
|
168
|
+
if (profileExists(name) && !opts.force) {
|
|
169
|
+
throw new Error(`Harness '${name}' already exists. Use --force to overwrite.`);
|
|
170
|
+
}
|
|
171
|
+
// Build first so a bad source/flag combination fails before prompting
|
|
172
|
+
// for a key the user would then have stored for nothing.
|
|
173
|
+
const forked = buildFork(source, name, opts);
|
|
174
|
+
if (opts.authProvider)
|
|
175
|
+
await ensureProviderToken(opts.authProvider, undefined, opts.keyStdin);
|
|
176
|
+
writeProfile(forked);
|
|
177
|
+
console.log(chalk.green(`Harness '${name}' forked from ${source}.`));
|
|
178
|
+
console.log(chalk.gray(`Try: agents run ${name} "hello"`));
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
console.error(chalk.red(err.message));
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
69
185
|
cmd
|
|
70
186
|
.command('list')
|
|
71
187
|
.alias('ls')
|
|
@@ -114,16 +230,7 @@ Examples:
|
|
|
114
230
|
.description('Show one custom harness (host, model, provider, auth, path).')
|
|
115
231
|
.action((name) => {
|
|
116
232
|
try {
|
|
117
|
-
|
|
118
|
-
console.log(chalk.bold(p.name));
|
|
119
|
-
if (p.description)
|
|
120
|
-
console.log(chalk.gray(p.description));
|
|
121
|
-
console.log('');
|
|
122
|
-
console.log(`Host: ${profileHostLabel(p)}`);
|
|
123
|
-
console.log(`Model: ${profileModelLabel(p)}`);
|
|
124
|
-
console.log(`Provider: ${profileProviderLabel(p)}`);
|
|
125
|
-
console.log(`Auth: ${profileAuthLabel(p)}`);
|
|
126
|
-
console.log(chalk.gray(getProfilePath(p.name)));
|
|
233
|
+
renderHarnessDetail(name);
|
|
127
234
|
}
|
|
128
235
|
catch (err) {
|
|
129
236
|
console.error(chalk.red(err.message));
|
package/dist/commands/hooks.js
CHANGED
|
@@ -603,30 +603,46 @@ Examples:
|
|
|
603
603
|
.option('--warn-ms <n>', 'p99 threshold above which a hook is flagged as slow', '2000')
|
|
604
604
|
.option('--json', 'Emit raw JSON rows instead of the table')
|
|
605
605
|
.addHelpText('after', `
|
|
606
|
-
Shows aggregated stats for every hook that
|
|
607
|
-
~/.agents/.cache/
|
|
608
|
-
|
|
606
|
+
Shows aggregated stats for every hook that fired through a generated shim.
|
|
607
|
+
Primary source: disposable SQLite warehouse ~/.agents/.cache/perf/perf.db
|
|
608
|
+
(same data as \`agents perf hooks\`). Falls back to the legacy daily JSONL under
|
|
609
|
+
~/.agents/.cache/logs/ when the warehouse is empty.
|
|
609
610
|
|
|
610
611
|
Examples:
|
|
611
612
|
agents hooks profile # last 7 days, table form
|
|
612
613
|
agents hooks profile --days 30 # roll up the full month
|
|
613
614
|
agents hooks profile --json | jq # pipe somewhere
|
|
615
|
+
agents perf hooks # same rollup under the perf surface
|
|
614
616
|
|
|
615
617
|
A hook whose p99 exceeds --warn-ms gets flagged in the cache column. Add
|
|
616
618
|
'cache: 5m' or 'cache: 5m-bg' to its hooks.yaml entry to fix it.
|
|
617
619
|
`)
|
|
618
620
|
.action(async (options) => {
|
|
619
621
|
const { aggregateHookProfile, loadHookFireEvents, formatMs, formatCacheColumn, DEFAULT_SLOW_HOOK_WARN_MS } = await import('../lib/hooks/profile.js');
|
|
622
|
+
const { aggregateSamples } = await import('../lib/perf/db.js');
|
|
620
623
|
const days = Math.max(1, parseInt(options.days, 10) || 7);
|
|
621
624
|
const warnMs = Math.max(0, parseInt(options.warnMs, 10) || DEFAULT_SLOW_HOOK_WARN_MS);
|
|
622
|
-
|
|
625
|
+
// Prefer the indexed warehouse; fall back to legacy JSONL for pre-warehouse shims.
|
|
626
|
+
const fromDb = aggregateSamples({ days, kinds: ['hook.fire'] }).map((r) => ({
|
|
627
|
+
hook: r.label,
|
|
628
|
+
n: r.n,
|
|
629
|
+
p50Ms: r.p50Ms,
|
|
630
|
+
p99Ms: r.p99Ms,
|
|
631
|
+
meanMs: r.meanMs,
|
|
632
|
+
maxMs: r.maxMs,
|
|
633
|
+
cacheHitPct: r.cacheHitPct ?? 0,
|
|
634
|
+
cacheStalePct: r.cacheStalePct ?? 0,
|
|
635
|
+
cacheMissPct: r.cacheMissPct ?? 0,
|
|
636
|
+
errorCount: r.errorCount ?? 0,
|
|
637
|
+
}));
|
|
638
|
+
const rows = fromDb.length > 0 ? fromDb : aggregateHookProfile(loadHookFireEvents(days));
|
|
623
639
|
if (options.json) {
|
|
624
640
|
console.log(JSON.stringify(rows, null, 2));
|
|
625
641
|
return;
|
|
626
642
|
}
|
|
627
643
|
if (rows.length === 0) {
|
|
628
|
-
console.log(chalk.gray(`No hook.fire
|
|
629
|
-
console.log(chalk.gray('Add \'cache: 5m\' to a hook
|
|
644
|
+
console.log(chalk.gray(`No hook.fire samples in the last ${days} day${days === 1 ? '' : 's'}.`));
|
|
645
|
+
console.log(chalk.gray('Add \'cache: 5m\' to a hook (or matches:) so a shim instruments it, then resync.'));
|
|
630
646
|
return;
|
|
631
647
|
}
|
|
632
648
|
const widths = { hook: 36, n: 5, p50: 7, p99: 7, mean: 7, max: 7, cache: 30 };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents perf` — latency rollups over the disposable perf SQLite warehouse.
|
|
3
|
+
*
|
|
4
|
+
* Subcommands:
|
|
5
|
+
* agents perf multi-section summary (commands + hooks + runs)
|
|
6
|
+
* agents perf hooks per-hook p50/p99 + cache hit rates
|
|
7
|
+
* agents perf commands slowest CLI command paths (from command.end)
|
|
8
|
+
* agents perf run agent.run / perf.timing labels
|
|
9
|
+
*
|
|
10
|
+
* Soft-joins sessions.db via shared string keys (session_id, agent, machine) —
|
|
11
|
+
* no foreign keys. Warehouse lives at ~/.agents/.cache/perf/perf.db (safe to wipe).
|
|
12
|
+
*/
|
|
13
|
+
import type { Command } from 'commander';
|
|
14
|
+
export declare function registerPerfCommand(program: Command): void;
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents perf` — latency rollups over the disposable perf SQLite warehouse.
|
|
3
|
+
*
|
|
4
|
+
* Subcommands:
|
|
5
|
+
* agents perf multi-section summary (commands + hooks + runs)
|
|
6
|
+
* agents perf hooks per-hook p50/p99 + cache hit rates
|
|
7
|
+
* agents perf commands slowest CLI command paths (from command.end)
|
|
8
|
+
* agents perf run agent.run / perf.timing labels
|
|
9
|
+
*
|
|
10
|
+
* Soft-joins sessions.db via shared string keys (session_id, agent, machine) —
|
|
11
|
+
* no foreign keys. Warehouse lives at ~/.agents/.cache/perf/perf.db (safe to wipe).
|
|
12
|
+
*/
|
|
13
|
+
import chalk from 'chalk';
|
|
14
|
+
import { aggregateSamples, perfDbPath, } from '../lib/perf/db.js';
|
|
15
|
+
import { formatMs, formatCacheColumn, DEFAULT_SLOW_HOOK_WARN_MS, loadHookFireEvents, aggregateHookProfile, } from '../lib/hooks/profile.js';
|
|
16
|
+
function parseDays(raw) {
|
|
17
|
+
const n = parseInt(raw ?? '7', 10);
|
|
18
|
+
return Number.isFinite(n) && n > 0 ? n : 7;
|
|
19
|
+
}
|
|
20
|
+
function parseWarnMs(raw, fallback) {
|
|
21
|
+
const n = parseInt(raw ?? String(fallback), 10);
|
|
22
|
+
return Number.isFinite(n) && n >= 0 ? n : fallback;
|
|
23
|
+
}
|
|
24
|
+
function parseLimit(raw, fallback) {
|
|
25
|
+
const n = parseInt(raw ?? String(fallback), 10);
|
|
26
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
27
|
+
}
|
|
28
|
+
/** Map warehouse rows shaped like hook.fire into the existing HookProfileRow UI. */
|
|
29
|
+
function asHookRows(rows) {
|
|
30
|
+
return rows.map((r) => ({
|
|
31
|
+
hook: r.label,
|
|
32
|
+
n: r.n,
|
|
33
|
+
p50Ms: r.p50Ms,
|
|
34
|
+
p99Ms: r.p99Ms,
|
|
35
|
+
meanMs: r.meanMs,
|
|
36
|
+
maxMs: r.maxMs,
|
|
37
|
+
cacheHitPct: r.cacheHitPct ?? 0,
|
|
38
|
+
cacheStalePct: r.cacheStalePct ?? 0,
|
|
39
|
+
cacheMissPct: r.cacheMissPct ?? 0,
|
|
40
|
+
errorCount: r.errorCount ?? 0,
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
function printTable(headers, widths, lines, slowFlags) {
|
|
44
|
+
const pad = (s, w) => (s.length >= w ? s.slice(0, w) : s + ' '.repeat(w - s.length));
|
|
45
|
+
const header = headers.map((h, i) => pad(h, widths[i])).join(' ');
|
|
46
|
+
console.log(chalk.bold(header));
|
|
47
|
+
console.log(chalk.gray('─'.repeat(header.length)));
|
|
48
|
+
for (let i = 0; i < lines.length; i++) {
|
|
49
|
+
const line = lines[i].map((c, j) => pad(c, widths[j])).join(' ');
|
|
50
|
+
console.log(slowFlags[i] ? chalk.yellow(line) : line);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function renderHookTable(rows, warnMs) {
|
|
54
|
+
if (rows.length === 0) {
|
|
55
|
+
console.log(chalk.gray('No hook timing samples yet.'));
|
|
56
|
+
console.log(chalk.gray(`Warehouse: ${perfDbPath()}`));
|
|
57
|
+
console.log(chalk.gray('Hooks write via cache/matches shims into the spool; run a session or resync hooks.'));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const widths = { hook: 36, n: 5, p50: 7, p99: 7, mean: 7, max: 7, cache: 28 };
|
|
61
|
+
const pad = (s, w) => (s.length >= w ? s.slice(0, w) : s + ' '.repeat(w - s.length));
|
|
62
|
+
const header = [
|
|
63
|
+
pad('HOOK', widths.hook),
|
|
64
|
+
pad('N', widths.n),
|
|
65
|
+
pad('P50', widths.p50),
|
|
66
|
+
pad('P99', widths.p99),
|
|
67
|
+
pad('MEAN', widths.mean),
|
|
68
|
+
pad('MAX', widths.max),
|
|
69
|
+
pad('CACHE', widths.cache),
|
|
70
|
+
].join(' ');
|
|
71
|
+
console.log(chalk.bold(header));
|
|
72
|
+
console.log(chalk.gray('─'.repeat(header.length)));
|
|
73
|
+
for (const r of rows) {
|
|
74
|
+
const slow = r.p99Ms > warnMs;
|
|
75
|
+
const cacheCol = formatCacheColumn(r);
|
|
76
|
+
const warning = slow && r.cacheHitPct + r.cacheStalePct === 0 ? ' ← add cache: 5m' : '';
|
|
77
|
+
const line = [
|
|
78
|
+
pad(r.hook, widths.hook),
|
|
79
|
+
pad(String(r.n), widths.n),
|
|
80
|
+
pad(formatMs(r.p50Ms), widths.p50),
|
|
81
|
+
pad(formatMs(r.p99Ms), widths.p99),
|
|
82
|
+
pad(formatMs(r.meanMs), widths.mean),
|
|
83
|
+
pad(formatMs(r.maxMs), widths.max),
|
|
84
|
+
pad(cacheCol, widths.cache),
|
|
85
|
+
].join(' ') + warning;
|
|
86
|
+
console.log(slow ? chalk.yellow(line) : line);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function renderLabelTable(title, rows, warnMs, limit) {
|
|
90
|
+
const sliced = rows.slice(0, limit);
|
|
91
|
+
if (sliced.length === 0) {
|
|
92
|
+
console.log(chalk.gray(`No ${title} samples yet.`));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const widths = [40, 5, 7, 7, 7, 7];
|
|
96
|
+
printTable(['LABEL', 'N', 'P50', 'P99', 'MEAN', 'MAX'], widths, sliced.map((r) => [
|
|
97
|
+
r.label,
|
|
98
|
+
String(r.n),
|
|
99
|
+
formatMs(r.p50Ms),
|
|
100
|
+
formatMs(r.p99Ms),
|
|
101
|
+
formatMs(r.meanMs),
|
|
102
|
+
formatMs(r.maxMs),
|
|
103
|
+
]), sliced.map((r) => r.p99Ms > warnMs));
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Prefer SQLite samples; fall back to the legacy daily JSONL so existing
|
|
107
|
+
* instrumentation still surfaces until shims are resynced.
|
|
108
|
+
*/
|
|
109
|
+
function loadHookProfile(days) {
|
|
110
|
+
const fromDb = asHookRows(aggregateSamples({ days, kinds: ['hook.fire'] }));
|
|
111
|
+
if (fromDb.length > 0)
|
|
112
|
+
return fromDb;
|
|
113
|
+
return aggregateHookProfile(loadHookFireEvents(days));
|
|
114
|
+
}
|
|
115
|
+
function hooksAction(opts) {
|
|
116
|
+
const days = parseDays(opts.days);
|
|
117
|
+
const warnMs = parseWarnMs(opts.warnMs, DEFAULT_SLOW_HOOK_WARN_MS);
|
|
118
|
+
const rows = loadHookProfile(days);
|
|
119
|
+
if (opts.json) {
|
|
120
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
renderHookTable(rows, warnMs);
|
|
124
|
+
}
|
|
125
|
+
function commandsAction(opts) {
|
|
126
|
+
const days = parseDays(opts.days);
|
|
127
|
+
const warnMs = parseWarnMs(opts.warnMs, 500);
|
|
128
|
+
const limit = parseLimit(opts.limit, 40);
|
|
129
|
+
const rows = aggregateSamples({ days, kinds: ['command.end'] });
|
|
130
|
+
if (opts.json) {
|
|
131
|
+
console.log(JSON.stringify(rows.slice(0, limit), null, 2));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
renderLabelTable('command', rows, warnMs, limit);
|
|
135
|
+
}
|
|
136
|
+
function runAction(opts) {
|
|
137
|
+
const days = parseDays(opts.days);
|
|
138
|
+
const warnMs = parseWarnMs(opts.warnMs, 60_000);
|
|
139
|
+
const limit = parseLimit(opts.limit, 40);
|
|
140
|
+
const rows = aggregateSamples({ days, kinds: ['perf.timing'] });
|
|
141
|
+
if (opts.json) {
|
|
142
|
+
console.log(JSON.stringify(rows.slice(0, limit), null, 2));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
renderLabelTable('run/timing', rows, warnMs, limit);
|
|
146
|
+
}
|
|
147
|
+
function summaryAction(opts) {
|
|
148
|
+
const days = parseDays(opts.days);
|
|
149
|
+
if (opts.json) {
|
|
150
|
+
console.log(JSON.stringify({
|
|
151
|
+
days,
|
|
152
|
+
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),
|
|
156
|
+
}, null, 2));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
console.log(chalk.bold(`agents perf — last ${days} day${days === 1 ? '' : 's'}`));
|
|
160
|
+
console.log(chalk.gray(`warehouse: ${perfDbPath()} (disposable; soft-join sessions via session_id/agent/machine)`));
|
|
161
|
+
console.log('');
|
|
162
|
+
console.log(chalk.bold('Commands (slowest by p99)'));
|
|
163
|
+
renderLabelTable('command', aggregateSamples({ days, kinds: ['command.end'] }), parseWarnMs(opts.warnMs, 500), 12);
|
|
164
|
+
console.log('');
|
|
165
|
+
console.log(chalk.bold('Hooks'));
|
|
166
|
+
renderHookTable(loadHookProfile(days), parseWarnMs(opts.warnMs, DEFAULT_SLOW_HOOK_WARN_MS));
|
|
167
|
+
console.log('');
|
|
168
|
+
console.log(chalk.bold('Runs (perf.timing)'));
|
|
169
|
+
renderLabelTable('run/timing', aggregateSamples({ days, kinds: ['perf.timing'] }), parseWarnMs(opts.warnMs, 60_000), 12);
|
|
170
|
+
}
|
|
171
|
+
function attachSharedOptions(cmd) {
|
|
172
|
+
return cmd
|
|
173
|
+
.option('--days <n>', 'Days of samples to include', '7')
|
|
174
|
+
.option('--warn-ms <n>', 'p99 above this is highlighted')
|
|
175
|
+
.option('--limit <n>', 'Max rows in the table', '40')
|
|
176
|
+
.option('--json', 'Emit JSON instead of a table');
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Commander binds a flag declared on both parent and child to the *parent*.
|
|
180
|
+
* Merge so `agents perf commands --json` still sees json:true on the leaf.
|
|
181
|
+
*/
|
|
182
|
+
function leafOpts(cmd) {
|
|
183
|
+
const parent = cmd.parent && typeof cmd.parent.opts === 'function'
|
|
184
|
+
? cmd.parent.opts()
|
|
185
|
+
: {};
|
|
186
|
+
return { ...parent, ...cmd.opts() };
|
|
187
|
+
}
|
|
188
|
+
export function registerPerfCommand(program) {
|
|
189
|
+
const perf = program
|
|
190
|
+
.command('perf')
|
|
191
|
+
.description('Latency rollups from the disposable perf warehouse (hooks, commands, runs)')
|
|
192
|
+
.addHelpText('after', `
|
|
193
|
+
The warehouse is SQLite at ~/.agents/.cache/perf/perf.db — safe to delete.
|
|
194
|
+
Identity columns reuse sessions/events string shapes (session_id, agent, machine)
|
|
195
|
+
for soft cross-reference; there are no foreign keys.
|
|
196
|
+
|
|
197
|
+
Examples:
|
|
198
|
+
agents perf # summary: commands + hooks + runs
|
|
199
|
+
agents perf hooks # per-hook p50/p99 + cache hit rate
|
|
200
|
+
agents perf commands --days 30 # slowest CLI entrypoints
|
|
201
|
+
agents perf run --json # agent.run timings as JSON
|
|
202
|
+
agents perf hooks --warn-ms 500
|
|
203
|
+
`);
|
|
204
|
+
// Options live on the parent so `agents perf --json` and
|
|
205
|
+
// `agents perf commands --json` both work (see leafOpts).
|
|
206
|
+
attachSharedOptions(perf).action(function summary() {
|
|
207
|
+
summaryAction(this.opts());
|
|
208
|
+
});
|
|
209
|
+
perf.command('hooks').description('Per-hook timing + cache stats')
|
|
210
|
+
.action(function hooks() {
|
|
211
|
+
hooksAction(leafOpts(this));
|
|
212
|
+
});
|
|
213
|
+
perf.command('commands').description('Slowest CLI command paths (command.end samples)')
|
|
214
|
+
.action(function commands() {
|
|
215
|
+
commandsAction(leafOpts(this));
|
|
216
|
+
});
|
|
217
|
+
perf.command('run').description('agent.run / perf.timing label rollups')
|
|
218
|
+
.action(function run() {
|
|
219
|
+
runAction(leafOpts(this));
|
|
220
|
+
});
|
|
221
|
+
}
|
|
@@ -15,6 +15,9 @@ import { type Preset } from '../lib/profiles-presets.js';
|
|
|
15
15
|
* without mocking @inquirer/prompts.
|
|
16
16
|
*/
|
|
17
17
|
export declare function buildProfileFromCollection(name: string, preset: Preset, collected: Record<string, string>, version?: string): Profile;
|
|
18
|
+
/** Read all available data from stdin synchronously, trimmed. */
|
|
19
|
+
/** Ensure a provider API key exists in keychain, prompting or reading stdin if missing. */
|
|
20
|
+
export declare function ensureProviderToken(provider: string, signupUrl?: string, fromStdin?: boolean): Promise<void>;
|
|
18
21
|
/** Options accepted by {@link addProfile} — shared by `agents profiles add` and `agents harness add`. */
|
|
19
22
|
export interface AddProfileOptions {
|
|
20
23
|
preset?: string;
|
|
@@ -47,7 +47,7 @@ async function promptForSecret(message) {
|
|
|
47
47
|
}
|
|
48
48
|
/** Read all available data from stdin synchronously, trimmed. */
|
|
49
49
|
/** Ensure a provider API key exists in keychain, prompting or reading stdin if missing. */
|
|
50
|
-
async function ensureProviderToken(provider, signupUrl, fromStdin) {
|
|
50
|
+
export async function ensureProviderToken(provider, signupUrl, fromStdin) {
|
|
51
51
|
const item = keychainItemName(provider);
|
|
52
52
|
if (hasKeychainToken(item)) {
|
|
53
53
|
return;
|
|
@@ -14,10 +14,29 @@ import type { DeviceRegistry } from '../lib/devices/registry.js';
|
|
|
14
14
|
* callers drop it straight into a status line.
|
|
15
15
|
*/
|
|
16
16
|
export declare function formatRunDuration(startedAt: string, completedAt: string | null): string;
|
|
17
|
+
/**
|
|
18
|
+
* The last run THIS device can speak for.
|
|
19
|
+
*
|
|
20
|
+
* A run record is written by whichever daemon fired the routine, into that
|
|
21
|
+
* machine's own runs dir — records carry no device attribution, so a record
|
|
22
|
+
* found here only ever describes this device's history. When a routine is
|
|
23
|
+
* pinned away from this machine (`devices:` excludes it) any local record is a
|
|
24
|
+
* leftover from before the pin, and reporting it as the routine's status paints
|
|
25
|
+
* another device's healthy routine red. Report nothing instead; the local
|
|
26
|
+
* history stays readable via `agents routines runs <name>`, and the owning
|
|
27
|
+
* device's status via `agents routines list --device <name>`.
|
|
28
|
+
*/
|
|
29
|
+
export declare function localLatestRun(job: JobConfig): RunMeta | null;
|
|
17
30
|
export interface RoutineListGroup {
|
|
18
31
|
key: string;
|
|
19
32
|
title: string;
|
|
20
33
|
jobs: JobConfig[];
|
|
34
|
+
/**
|
|
35
|
+
* Whether this device's run records describe the group. False for a
|
|
36
|
+
* `Device: <peer>` group — the rows are the same routine seen from a machine
|
|
37
|
+
* that does not fire it there, so Last Status is not ours to report.
|
|
38
|
+
*/
|
|
39
|
+
local: boolean;
|
|
21
40
|
}
|
|
22
41
|
export declare function groupRoutineJobsByDevice(jobs: JobConfig[], registry: DeviceRegistry, self?: string): RoutineListGroup[];
|
|
23
42
|
export declare function buildRunsJson(runs: RunMeta[]): Record<string, unknown>[];
|