@phnx-labs/agents-cli 1.19.2 → 1.20.3
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 +140 -0
- package/README.md +72 -12
- package/dist/browser.js +0 -0
- package/dist/commands/browser.js +88 -16
- package/dist/commands/cli.d.ts +14 -0
- package/dist/commands/cli.js +244 -0
- package/dist/commands/cloud.js +1 -1
- package/dist/commands/commands.js +27 -10
- package/dist/commands/computer.js +18 -1
- package/dist/commands/doctor.d.ts +1 -1
- package/dist/commands/doctor.js +2 -2
- package/dist/commands/exec.js +38 -18
- package/dist/commands/factory.d.ts +3 -14
- package/dist/commands/factory.js +3 -3
- package/dist/commands/feedback.d.ts +7 -0
- package/dist/commands/feedback.js +89 -0
- package/dist/commands/helper.d.ts +12 -0
- package/dist/commands/helper.js +87 -0
- package/dist/commands/hooks.js +89 -10
- package/dist/commands/mcp.js +166 -10
- package/dist/commands/packages.js +196 -27
- package/dist/commands/permissions.js +21 -6
- package/dist/commands/plugins.js +11 -4
- package/dist/commands/profiles.d.ts +8 -0
- package/dist/commands/profiles.js +118 -5
- package/dist/commands/prune.js +39 -160
- package/dist/commands/pull.js +58 -5
- package/dist/commands/routines.js +107 -14
- package/dist/commands/rules.js +8 -4
- package/dist/commands/secrets-migrate.d.ts +24 -0
- package/dist/commands/secrets-migrate.js +198 -0
- package/dist/commands/secrets-sync.d.ts +11 -0
- package/dist/commands/secrets-sync.js +155 -0
- package/dist/commands/secrets.js +79 -46
- package/dist/commands/sessions.d.ts +28 -0
- package/dist/commands/sessions.js +98 -33
- package/dist/commands/setup.d.ts +1 -0
- package/dist/commands/setup.js +37 -28
- package/dist/commands/skills.js +25 -8
- package/dist/commands/subagents.js +69 -49
- package/dist/commands/teams.js +61 -10
- package/dist/commands/utils.d.ts +33 -0
- package/dist/commands/utils.js +139 -0
- package/dist/commands/versions.d.ts +4 -3
- package/dist/commands/versions.js +134 -130
- package/dist/commands/view.d.ts +6 -0
- package/dist/commands/view.js +175 -19
- package/dist/commands/workflows.js +29 -6
- package/dist/computer.js +0 -0
- package/dist/index.js +38 -6
- package/dist/lib/acp/client.js +6 -1
- package/dist/lib/acp/harnesses.js +8 -0
- package/dist/lib/agents.d.ts +4 -0
- package/dist/lib/agents.js +125 -34
- package/dist/lib/auto-pull-worker.js +18 -1
- package/dist/lib/browser/cdp.d.ts +8 -1
- package/dist/lib/browser/cdp.js +40 -3
- package/dist/lib/browser/chrome.d.ts +13 -0
- package/dist/lib/browser/chrome.js +46 -3
- package/dist/lib/browser/domain-skills.d.ts +51 -0
- package/dist/lib/browser/domain-skills.js +157 -0
- package/dist/lib/browser/drivers/local.js +45 -4
- package/dist/lib/browser/drivers/ssh.js +2 -2
- package/dist/lib/browser/ipc.d.ts +8 -1
- package/dist/lib/browser/ipc.js +37 -28
- package/dist/lib/browser/profiles.d.ts +16 -3
- package/dist/lib/browser/profiles.js +44 -4
- package/dist/lib/browser/service.d.ts +3 -0
- package/dist/lib/browser/service.js +40 -5
- package/dist/lib/browser/types.d.ts +11 -4
- package/dist/lib/cli-resources.d.ts +137 -0
- package/dist/lib/cli-resources.js +477 -0
- package/dist/lib/cloud/factory.d.ts +1 -1
- package/dist/lib/cloud/factory.js +1 -1
- package/dist/lib/cloud/rush.js +5 -5
- package/dist/lib/command-skills.js +0 -2
- package/dist/lib/computer-rpc.d.ts +3 -0
- package/dist/lib/computer-rpc.js +53 -0
- package/dist/lib/daemon.js +20 -0
- package/dist/lib/events.d.ts +16 -2
- package/dist/lib/events.js +33 -2
- package/dist/lib/exec.d.ts +42 -13
- package/dist/lib/exec.js +127 -33
- package/dist/lib/help.js +11 -5
- package/dist/lib/hooks/cache.d.ts +38 -0
- package/dist/lib/hooks/cache.js +242 -0
- package/dist/lib/hooks/profile.d.ts +33 -0
- package/dist/lib/hooks/profile.js +129 -0
- package/dist/lib/hooks.d.ts +0 -10
- package/dist/lib/hooks.js +246 -11
- package/dist/lib/mcp.d.ts +15 -0
- package/dist/lib/mcp.js +46 -0
- package/dist/lib/migrate.js +1 -1
- package/dist/lib/overdue.d.ts +26 -0
- package/dist/lib/overdue.js +101 -0
- package/dist/lib/permissions.d.ts +13 -0
- package/dist/lib/permissions.js +55 -1
- package/dist/lib/plugin-marketplace.js +1 -1
- package/dist/lib/plugins.js +15 -1
- package/dist/lib/profiles-presets.d.ts +26 -0
- package/dist/lib/profiles-presets.js +216 -0
- package/dist/lib/profiles.d.ts +34 -0
- package/dist/lib/profiles.js +112 -1
- package/dist/lib/resources/mcp.js +37 -0
- package/dist/lib/resources.d.ts +1 -1
- package/dist/lib/rotate.js +10 -4
- package/dist/lib/routines-format.d.ts +47 -0
- package/dist/lib/routines-format.js +194 -0
- package/dist/lib/routines.d.ts +8 -2
- package/dist/lib/routines.js +34 -14
- package/dist/lib/runner.js +83 -15
- package/dist/lib/scheduler.js +8 -1
- 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/Agents CLI.app/Contents/_CodeSignature/CodeResources +1 -9
- package/dist/lib/secrets/bundles.d.ts +34 -17
- package/dist/lib/secrets/bundles.js +210 -36
- package/dist/lib/secrets/index.d.ts +49 -30
- package/dist/lib/secrets/index.js +126 -115
- package/dist/lib/secrets/install-helper.d.ts +45 -0
- package/dist/lib/secrets/install-helper.js +165 -0
- package/dist/lib/secrets/linux.js +4 -4
- package/dist/lib/secrets/sync.d.ts +56 -0
- package/dist/lib/secrets/sync.js +180 -0
- package/dist/lib/session/active.d.ts +8 -0
- package/dist/lib/session/active.js +3 -2
- package/dist/lib/session/db.d.ts +0 -4
- package/dist/lib/session/db.js +0 -26
- package/dist/lib/session/parse.d.ts +1 -0
- package/dist/lib/session/parse.js +44 -0
- package/dist/lib/session/render.js +4 -4
- package/dist/lib/session/types.d.ts +2 -2
- package/dist/lib/session/types.js +1 -1
- package/dist/lib/shims.d.ts +5 -2
- package/dist/lib/shims.js +70 -38
- package/dist/lib/state.d.ts +14 -2
- package/dist/lib/state.js +51 -20
- package/dist/lib/teams/agents.d.ts +5 -4
- package/dist/lib/teams/agents.js +48 -22
- package/dist/lib/teams/api.d.ts +2 -1
- package/dist/lib/teams/api.js +4 -3
- package/dist/lib/teams/parsers.d.ts +1 -1
- package/dist/lib/teams/parsers.js +153 -3
- package/dist/lib/teams/summarizer.js +18 -2
- package/dist/lib/teams/worktree.js +14 -3
- package/dist/lib/types.d.ts +63 -4
- package/dist/lib/types.js +8 -3
- package/dist/lib/usage.d.ts +27 -2
- package/dist/lib/usage.js +100 -17
- package/dist/lib/versions.d.ts +45 -3
- package/dist/lib/versions.js +455 -60
- package/package.json +15 -14
- package/scripts/install-helper.js +97 -0
- package/scripts/postinstall.js +16 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/embedded.provisionprofile +0 -0
- package/npm-shrinkwrap.json +0 -3162
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declarative hook caching + timing.
|
|
3
|
+
*
|
|
4
|
+
* Hooks that opt in via `cache:` in hooks.yaml get a generated bash shim
|
|
5
|
+
* (~/.agents/.cache/shims/hooks/<name>.sh) registered with the agent instead
|
|
6
|
+
* of the raw script path. The shim handles:
|
|
7
|
+
*
|
|
8
|
+
* 1. cache lookup — reads ~/.agents/.cache/state/hooks/<name>.<key>.out
|
|
9
|
+
* and serves it if newer than ttl.
|
|
10
|
+
* 2. stale-while-revalidate — when prefetch=background, serves stale cache
|
|
11
|
+
* and refreshes the cache file in a detached child.
|
|
12
|
+
* 3. timing — appends one JSONL line per fire to events-YYYY-MM-DD.jsonl.
|
|
13
|
+
*
|
|
14
|
+
* The shim is regenerated whenever the registrar runs; if its content doesn't
|
|
15
|
+
* change (idempotent), mtime is preserved. Stale shims for removed hooks are
|
|
16
|
+
* cleaned by the registrar's garbage collection (shims dir is in
|
|
17
|
+
* managedPrefixes).
|
|
18
|
+
*/
|
|
19
|
+
import * as fs from 'fs';
|
|
20
|
+
import * as path from 'path';
|
|
21
|
+
import { getHookCacheDir, getHookShimsDir, getLogsDir } from '../state.js';
|
|
22
|
+
/**
|
|
23
|
+
* Parse a `cache:` value from hooks.yaml into the canonical config form.
|
|
24
|
+
* Accepts the shorthand string ("5m", "30s-bg") or the full object form.
|
|
25
|
+
* Returns null if the value is missing or unparseable.
|
|
26
|
+
*/
|
|
27
|
+
export function parseCacheConfig(raw) {
|
|
28
|
+
if (raw == null)
|
|
29
|
+
return null;
|
|
30
|
+
if (typeof raw === 'string')
|
|
31
|
+
return parseShorthand(raw);
|
|
32
|
+
const ttlSec = parseDuration(raw.ttl);
|
|
33
|
+
if (ttlSec == null)
|
|
34
|
+
return null;
|
|
35
|
+
return {
|
|
36
|
+
ttl: ttlSec,
|
|
37
|
+
key: raw.key ?? 'global',
|
|
38
|
+
prefetch: raw.prefetch ?? 'none',
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function parseShorthand(s) {
|
|
42
|
+
const trimmed = s.trim();
|
|
43
|
+
let prefetch = 'none';
|
|
44
|
+
let durationPart = trimmed;
|
|
45
|
+
if (trimmed.endsWith('-bg')) {
|
|
46
|
+
prefetch = 'background';
|
|
47
|
+
durationPart = trimmed.slice(0, -3);
|
|
48
|
+
}
|
|
49
|
+
const ttlSec = parseDuration(durationPart);
|
|
50
|
+
if (ttlSec == null)
|
|
51
|
+
return null;
|
|
52
|
+
return { ttl: ttlSec, key: 'global', prefetch };
|
|
53
|
+
}
|
|
54
|
+
/** Parse "30s" | "5m" | "1h" | plain seconds. Returns seconds, or null on failure. */
|
|
55
|
+
export function parseDuration(d) {
|
|
56
|
+
if (d == null)
|
|
57
|
+
return null;
|
|
58
|
+
if (typeof d === 'number')
|
|
59
|
+
return Number.isFinite(d) && d > 0 ? Math.floor(d) : null;
|
|
60
|
+
const m = d.trim().match(/^(\d+)\s*(s|sec|secs|m|min|mins|h|hr|hrs)?$/i);
|
|
61
|
+
if (!m)
|
|
62
|
+
return null;
|
|
63
|
+
const value = parseInt(m[1], 10);
|
|
64
|
+
if (!Number.isFinite(value) || value <= 0)
|
|
65
|
+
return null;
|
|
66
|
+
const unit = (m[2] || 's').toLowerCase();
|
|
67
|
+
if (unit.startsWith('h'))
|
|
68
|
+
return value * 3600;
|
|
69
|
+
if (unit.startsWith('m'))
|
|
70
|
+
return value * 60;
|
|
71
|
+
return value;
|
|
72
|
+
}
|
|
73
|
+
/** Absolute path of the generated shim for a hook name. */
|
|
74
|
+
export function getHookShimPath(name) {
|
|
75
|
+
return path.join(getHookShimsDir(), `${name}.sh`);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Generate (or refresh) the shim script for a hook. Idempotent — only writes
|
|
79
|
+
* when the content differs from what's on disk. Returns the absolute shim path.
|
|
80
|
+
*/
|
|
81
|
+
export function generateHookShim(args) {
|
|
82
|
+
const shimsDir = args.paths?.shimsDir ?? getHookShimsDir();
|
|
83
|
+
const cacheDir = args.paths?.cacheDir ?? getHookCacheDir();
|
|
84
|
+
const logsDir = args.paths?.logsDir ?? getLogsDir();
|
|
85
|
+
const shimPath = path.join(shimsDir, `${args.name}.sh`);
|
|
86
|
+
const content = renderShim(args.name, args.scriptPath, args.cache, { cacheDir, logsDir });
|
|
87
|
+
fs.mkdirSync(shimsDir, { recursive: true });
|
|
88
|
+
let existing = null;
|
|
89
|
+
if (fs.existsSync(shimPath)) {
|
|
90
|
+
try {
|
|
91
|
+
existing = fs.readFileSync(shimPath, 'utf-8');
|
|
92
|
+
}
|
|
93
|
+
catch { /* rewrite */ }
|
|
94
|
+
}
|
|
95
|
+
if (existing !== content) {
|
|
96
|
+
fs.writeFileSync(shimPath, content, { mode: 0o755 });
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
// Ensure exec bit even when content unchanged (file mode can drift).
|
|
100
|
+
try {
|
|
101
|
+
fs.chmodSync(shimPath, 0o755);
|
|
102
|
+
}
|
|
103
|
+
catch { /* best effort */ }
|
|
104
|
+
}
|
|
105
|
+
return shimPath;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Render the bash shim. Bash 3.2-compatible (macOS default). Uses python3 for
|
|
109
|
+
* monotonic-ish nanosecond timing — already a hard dependency of other hooks
|
|
110
|
+
* in this repo (04-capture-session-start-metadata.sh does the same).
|
|
111
|
+
*/
|
|
112
|
+
function renderShim(name, scriptPath, cache, paths) {
|
|
113
|
+
const ttl = typeof cache.ttl === 'number' ? cache.ttl : (parseDuration(cache.ttl) ?? 0);
|
|
114
|
+
const key = cache.key ?? 'global';
|
|
115
|
+
const prefetch = cache.prefetch ?? 'none';
|
|
116
|
+
const { cacheDir, logsDir } = paths;
|
|
117
|
+
// sh-escape: wrap in single quotes, escape any embedded single quotes.
|
|
118
|
+
const q = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
|
|
119
|
+
return `#!/usr/bin/env bash
|
|
120
|
+
# GENERATED by agents-cli. Do not edit — re-run \`agents hooks sync\` to refresh.
|
|
121
|
+
# Hook: ${name}
|
|
122
|
+
# Source: ${scriptPath}
|
|
123
|
+
# Cache: key=${key} ttl=${ttl}s prefetch=${prefetch}
|
|
124
|
+
set -u
|
|
125
|
+
|
|
126
|
+
HOOK_NAME=${q(name)}
|
|
127
|
+
SOURCE=${q(scriptPath)}
|
|
128
|
+
CACHE_DIR=${q(cacheDir)}
|
|
129
|
+
LOGS_DIR=${q(logsDir)}
|
|
130
|
+
TTL=${ttl}
|
|
131
|
+
PREFETCH=${q(prefetch)}
|
|
132
|
+
KEY_MODE=${q(key)}
|
|
133
|
+
|
|
134
|
+
mkdir -p "$CACHE_DIR" "$LOGS_DIR"
|
|
135
|
+
|
|
136
|
+
# Read stdin once (Claude/Codex/Gemini pass JSON on stdin to every hook).
|
|
137
|
+
STDIN_PAYLOAD="$(cat || true)"
|
|
138
|
+
|
|
139
|
+
# Portable sha1 — \`shasum\` is Perl, missing on minimal Linux images;
|
|
140
|
+
# \`sha1sum\` is coreutils, missing on macOS. Truncate to 12 hex chars.
|
|
141
|
+
sha1_12() { python3 -c 'import hashlib,sys; print(hashlib.sha1(sys.stdin.read().encode()).hexdigest()[:12])'; }
|
|
142
|
+
|
|
143
|
+
# Derive cache key suffix from KEY_MODE. All untrusted inputs (cwd, session_id,
|
|
144
|
+
# project path) are hashed before going into the filename so a malicious stdin
|
|
145
|
+
# payload can't write outside $CACHE_DIR via path traversal.
|
|
146
|
+
cache_suffix=""
|
|
147
|
+
case "$KEY_MODE" in
|
|
148
|
+
per-cwd)
|
|
149
|
+
cwd_val="$(printf '%s' "$STDIN_PAYLOAD" | python3 -c 'import json,sys
|
|
150
|
+
try: print(json.load(sys.stdin).get("cwd","") or "")
|
|
151
|
+
except Exception: pass' 2>/dev/null || true)"
|
|
152
|
+
[ -z "$cwd_val" ] && cwd_val="$PWD"
|
|
153
|
+
cache_suffix=".$(printf '%s' "$cwd_val" | sha1_12)"
|
|
154
|
+
;;
|
|
155
|
+
per-session)
|
|
156
|
+
sid_val="$(printf '%s' "$STDIN_PAYLOAD" | python3 -c 'import json,sys
|
|
157
|
+
try: print(json.load(sys.stdin).get("session_id","") or "")
|
|
158
|
+
except Exception: pass' 2>/dev/null || true)"
|
|
159
|
+
# Hash + fall back to a sentinel so missing-session doesn't silently
|
|
160
|
+
# collapse to the same file as KEY_MODE=global.
|
|
161
|
+
[ -z "$sid_val" ] && sid_val="__nosession__"
|
|
162
|
+
cache_suffix=".$(printf '%s' "$sid_val" | sha1_12)"
|
|
163
|
+
;;
|
|
164
|
+
per-project)
|
|
165
|
+
proj_val="$(git -C "$PWD" rev-parse --show-toplevel 2>/dev/null || echo "")"
|
|
166
|
+
[ -z "$proj_val" ] && proj_val="$PWD"
|
|
167
|
+
cache_suffix=".$(printf '%s' "$proj_val" | sha1_12)"
|
|
168
|
+
;;
|
|
169
|
+
global|*)
|
|
170
|
+
cache_suffix=""
|
|
171
|
+
;;
|
|
172
|
+
esac
|
|
173
|
+
CACHE_FILE="$CACHE_DIR/$HOOK_NAME$cache_suffix.out"
|
|
174
|
+
|
|
175
|
+
# Monotonic-ish nanosecond timer (macOS \`date\` has no %N).
|
|
176
|
+
now_ns() { python3 -c 'import time; print(int(time.time()*1e9))'; }
|
|
177
|
+
START_NS=$(now_ns)
|
|
178
|
+
|
|
179
|
+
CACHE_STATUS=miss
|
|
180
|
+
CACHE_AGE=-1
|
|
181
|
+
EXIT=0
|
|
182
|
+
|
|
183
|
+
if [ -f "$CACHE_FILE" ]; then
|
|
184
|
+
# python3 is already a hard dep (used for now_ns) and gives portable mtime
|
|
185
|
+
# without the macOS-vs-Linux \`stat\` flag divergence (-f %m vs -c %Y) that
|
|
186
|
+
# blew up under \`set -u\` when the wrong flag produced literal "%m".
|
|
187
|
+
mtime=$(python3 -c 'import os,sys; print(int(os.path.getmtime(sys.argv[1])))' "$CACHE_FILE" 2>/dev/null)
|
|
188
|
+
mtime=\${mtime:-0}
|
|
189
|
+
now_s=$(date +%s)
|
|
190
|
+
CACHE_AGE=$((now_s - mtime))
|
|
191
|
+
if [ "$CACHE_AGE" -ge 0 ] && [ "$CACHE_AGE" -lt "$TTL" ]; then
|
|
192
|
+
cat "$CACHE_FILE"
|
|
193
|
+
CACHE_STATUS=hit
|
|
194
|
+
fi
|
|
195
|
+
fi
|
|
196
|
+
|
|
197
|
+
if [ "$CACHE_STATUS" = miss ]; then
|
|
198
|
+
if [ -f "$CACHE_FILE" ] && [ "$PREFETCH" = background ]; then
|
|
199
|
+
# Stale-while-revalidate: serve stale immediately, refresh in detached child.
|
|
200
|
+
cat "$CACHE_FILE"
|
|
201
|
+
CACHE_STATUS=stale-prefetch
|
|
202
|
+
tmp="$CACHE_FILE.new.$$"
|
|
203
|
+
( printf '%s' "$STDIN_PAYLOAD" | "$SOURCE" >"$tmp" 2>/dev/null && mv -f "$tmp" "$CACHE_FILE" || rm -f "$tmp" ) >/dev/null 2>&1 &
|
|
204
|
+
disown 2>/dev/null || true
|
|
205
|
+
else
|
|
206
|
+
# Synchronous fetch + cache.
|
|
207
|
+
tmp="$CACHE_FILE.new.$$"
|
|
208
|
+
if printf '%s' "$STDIN_PAYLOAD" | "$SOURCE" >"$tmp"; then
|
|
209
|
+
EXIT=0
|
|
210
|
+
cat "$tmp"
|
|
211
|
+
mv -f "$tmp" "$CACHE_FILE"
|
|
212
|
+
else
|
|
213
|
+
EXIT=$?
|
|
214
|
+
rm -f "$tmp"
|
|
215
|
+
fi
|
|
216
|
+
fi
|
|
217
|
+
fi
|
|
218
|
+
|
|
219
|
+
END_NS=$(now_ns)
|
|
220
|
+
MS=$(( (END_NS - START_NS) / 1000000 ))
|
|
221
|
+
TS=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
222
|
+
LOG_FILE="$LOGS_DIR/events-$(date -u +%Y-%m-%d).jsonl"
|
|
223
|
+
printf '{"ts":"%s","event":"hook.fire","hook":"%s","ms":%d,"cache":"%s","exit":%d}\\n' \\
|
|
224
|
+
"$TS" "$HOOK_NAME" "$MS" "$CACHE_STATUS" "$EXIT" >>"$LOG_FILE" 2>/dev/null || true
|
|
225
|
+
|
|
226
|
+
exit "$EXIT"
|
|
227
|
+
`;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Remove a hook's shim. Called by the registrar's garbage collection when a
|
|
231
|
+
* hook is renamed/deleted or has its `cache:` field removed.
|
|
232
|
+
*/
|
|
233
|
+
export function removeHookShim(name, shimsDir) {
|
|
234
|
+
const dir = shimsDir ?? getHookShimsDir();
|
|
235
|
+
const shimPath = path.join(dir, `${name}.sh`);
|
|
236
|
+
if (fs.existsSync(shimPath)) {
|
|
237
|
+
try {
|
|
238
|
+
fs.unlinkSync(shimPath);
|
|
239
|
+
}
|
|
240
|
+
catch { /* best effort */ }
|
|
241
|
+
}
|
|
242
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface HookProfileRow {
|
|
2
|
+
hook: string;
|
|
3
|
+
n: number;
|
|
4
|
+
p50Ms: number;
|
|
5
|
+
p99Ms: number;
|
|
6
|
+
meanMs: number;
|
|
7
|
+
maxMs: number;
|
|
8
|
+
cacheHitPct: number;
|
|
9
|
+
cacheStalePct: number;
|
|
10
|
+
cacheMissPct: number;
|
|
11
|
+
errorCount: number;
|
|
12
|
+
}
|
|
13
|
+
interface RawFireEvent {
|
|
14
|
+
event?: string;
|
|
15
|
+
hook?: string;
|
|
16
|
+
ms?: number;
|
|
17
|
+
cache?: 'hit' | 'miss' | 'stale-prefetch' | string;
|
|
18
|
+
exit?: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Load every `hook.fire` event from the last `days` daily log files.
|
|
22
|
+
* Lines that aren't JSON or aren't `hook.fire` events are silently skipped —
|
|
23
|
+
* the events log is multiplexed (version.switch, secrets.get, …).
|
|
24
|
+
*/
|
|
25
|
+
export declare function loadHookFireEvents(days?: number, logsDir?: string): RawFireEvent[];
|
|
26
|
+
/** Aggregate fire events into a per-hook profile, sorted by p99 desc. */
|
|
27
|
+
export declare function aggregateHookProfile(events: RawFireEvent[]): HookProfileRow[];
|
|
28
|
+
/** Human-friendly duration: "42ms" / "1.2s" / "12s" / "2m". */
|
|
29
|
+
export declare function formatMs(ms: number): string;
|
|
30
|
+
/** Format a row's cache column: `hit:97% miss:3%` or `n/a` when nothing cached. */
|
|
31
|
+
export declare function formatCacheColumn(row: HookProfileRow): string;
|
|
32
|
+
export declare const DEFAULT_SLOW_HOOK_WARN_MS = 2000;
|
|
33
|
+
export {};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook profiling — reads `hook.fire` events from the daily JSONL logs that
|
|
3
|
+
* generated shims (see `cache.ts`) emit on every invocation, and aggregates
|
|
4
|
+
* per-hook timing + cache stats.
|
|
5
|
+
*
|
|
6
|
+
* Only hooks declared with `cache:` get instrumented today, because only those
|
|
7
|
+
* are wrapped by a generated shim. Hooks without `cache:` are not in the
|
|
8
|
+
* profile output — that's deliberate: opting into the primitive is what
|
|
9
|
+
* surfaces the data.
|
|
10
|
+
*/
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
import { getLogsDir } from '../state.js';
|
|
14
|
+
/**
|
|
15
|
+
* Load every `hook.fire` event from the last `days` daily log files.
|
|
16
|
+
* Lines that aren't JSON or aren't `hook.fire` events are silently skipped —
|
|
17
|
+
* the events log is multiplexed (version.switch, secrets.get, …).
|
|
18
|
+
*/
|
|
19
|
+
export function loadHookFireEvents(days = 7, logsDir = getLogsDir()) {
|
|
20
|
+
if (!fs.existsSync(logsDir))
|
|
21
|
+
return [];
|
|
22
|
+
const today = new Date();
|
|
23
|
+
const events = [];
|
|
24
|
+
for (let i = 0; i < days; i++) {
|
|
25
|
+
const d = new Date(today);
|
|
26
|
+
d.setUTCDate(d.getUTCDate() - i);
|
|
27
|
+
const yyyy = d.getUTCFullYear();
|
|
28
|
+
const mm = String(d.getUTCMonth() + 1).padStart(2, '0');
|
|
29
|
+
const dd = String(d.getUTCDate()).padStart(2, '0');
|
|
30
|
+
const file = path.join(logsDir, `events-${yyyy}-${mm}-${dd}.jsonl`);
|
|
31
|
+
if (!fs.existsSync(file))
|
|
32
|
+
continue;
|
|
33
|
+
const raw = fs.readFileSync(file, 'utf-8');
|
|
34
|
+
for (const line of raw.split('\n')) {
|
|
35
|
+
if (!line)
|
|
36
|
+
continue;
|
|
37
|
+
let parsed;
|
|
38
|
+
try {
|
|
39
|
+
parsed = JSON.parse(line);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (parsed.event !== 'hook.fire')
|
|
45
|
+
continue;
|
|
46
|
+
if (typeof parsed.hook !== 'string')
|
|
47
|
+
continue;
|
|
48
|
+
if (typeof parsed.ms !== 'number')
|
|
49
|
+
continue;
|
|
50
|
+
events.push(parsed);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return events;
|
|
54
|
+
}
|
|
55
|
+
/** Percentile of a sorted-ascending array. p in [0,100]. Linear interpolation. */
|
|
56
|
+
function percentile(sorted, p) {
|
|
57
|
+
if (sorted.length === 0)
|
|
58
|
+
return 0;
|
|
59
|
+
if (sorted.length === 1)
|
|
60
|
+
return sorted[0];
|
|
61
|
+
const rank = (p / 100) * (sorted.length - 1);
|
|
62
|
+
const lo = Math.floor(rank);
|
|
63
|
+
const hi = Math.ceil(rank);
|
|
64
|
+
if (lo === hi)
|
|
65
|
+
return sorted[lo];
|
|
66
|
+
const frac = rank - lo;
|
|
67
|
+
return sorted[lo] * (1 - frac) + sorted[hi] * frac;
|
|
68
|
+
}
|
|
69
|
+
/** Aggregate fire events into a per-hook profile, sorted by p99 desc. */
|
|
70
|
+
export function aggregateHookProfile(events) {
|
|
71
|
+
const byHook = new Map();
|
|
72
|
+
for (const e of events) {
|
|
73
|
+
if (!e.hook)
|
|
74
|
+
continue;
|
|
75
|
+
if (!byHook.has(e.hook))
|
|
76
|
+
byHook.set(e.hook, []);
|
|
77
|
+
byHook.get(e.hook).push(e);
|
|
78
|
+
}
|
|
79
|
+
const rows = [];
|
|
80
|
+
for (const [hook, evs] of byHook) {
|
|
81
|
+
const sortedMs = evs.map(e => e.ms).sort((a, b) => a - b);
|
|
82
|
+
const n = evs.length;
|
|
83
|
+
const sum = sortedMs.reduce((a, b) => a + b, 0);
|
|
84
|
+
const hits = evs.filter(e => e.cache === 'hit').length;
|
|
85
|
+
const stale = evs.filter(e => e.cache === 'stale-prefetch').length;
|
|
86
|
+
const misses = evs.filter(e => e.cache === 'miss').length;
|
|
87
|
+
const errors = evs.filter(e => typeof e.exit === 'number' && e.exit !== 0).length;
|
|
88
|
+
rows.push({
|
|
89
|
+
hook,
|
|
90
|
+
n,
|
|
91
|
+
p50Ms: Math.round(percentile(sortedMs, 50)),
|
|
92
|
+
p99Ms: Math.round(percentile(sortedMs, 99)),
|
|
93
|
+
meanMs: Math.round(sum / n),
|
|
94
|
+
maxMs: sortedMs[sortedMs.length - 1],
|
|
95
|
+
cacheHitPct: Math.round((hits / n) * 100),
|
|
96
|
+
cacheStalePct: Math.round((stale / n) * 100),
|
|
97
|
+
cacheMissPct: Math.round((misses / n) * 100),
|
|
98
|
+
errorCount: errors,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
rows.sort((a, b) => b.p99Ms - a.p99Ms);
|
|
102
|
+
return rows;
|
|
103
|
+
}
|
|
104
|
+
/** Human-friendly duration: "42ms" / "1.2s" / "12s" / "2m". */
|
|
105
|
+
export function formatMs(ms) {
|
|
106
|
+
if (ms < 1000)
|
|
107
|
+
return `${ms}ms`;
|
|
108
|
+
if (ms < 10_000)
|
|
109
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
110
|
+
if (ms < 60_000)
|
|
111
|
+
return `${Math.round(ms / 1000)}s`;
|
|
112
|
+
const mins = Math.floor(ms / 60_000);
|
|
113
|
+
const secs = Math.round((ms % 60_000) / 1000);
|
|
114
|
+
return secs > 0 ? `${mins}m${secs}s` : `${mins}m`;
|
|
115
|
+
}
|
|
116
|
+
/** Format a row's cache column: `hit:97% miss:3%` or `n/a` when nothing cached. */
|
|
117
|
+
export function formatCacheColumn(row) {
|
|
118
|
+
if (row.cacheHitPct + row.cacheStalePct + row.cacheMissPct === 0)
|
|
119
|
+
return 'n/a';
|
|
120
|
+
const parts = [];
|
|
121
|
+
if (row.cacheHitPct > 0)
|
|
122
|
+
parts.push(`hit:${row.cacheHitPct}%`);
|
|
123
|
+
if (row.cacheStalePct > 0)
|
|
124
|
+
parts.push(`stale:${row.cacheStalePct}%`);
|
|
125
|
+
if (row.cacheMissPct > 0)
|
|
126
|
+
parts.push(`miss:${row.cacheMissPct}%`);
|
|
127
|
+
return parts.join(' ');
|
|
128
|
+
}
|
|
129
|
+
export const DEFAULT_SLOW_HOOK_WARN_MS = 2000;
|
package/dist/lib/hooks.d.ts
CHANGED
|
@@ -124,16 +124,6 @@ export declare function listCentralHooks(): HookEntry[];
|
|
|
124
124
|
* Hooks marked `enabled: false` are dropped from the returned map.
|
|
125
125
|
*/
|
|
126
126
|
export declare function parseHookManifest(): Record<string, ManifestHook>;
|
|
127
|
-
/**
|
|
128
|
-
* Register hooks as lifecycle events in an agent's config.
|
|
129
|
-
* Reads hooks.yaml manifest, merges into the agent's config file(s).
|
|
130
|
-
* Only manages hooks whose command paths are under ~/.agents/hooks/ or
|
|
131
|
-
* ~/.agents-system/hooks/. Does not remove user-added hooks.
|
|
132
|
-
*
|
|
133
|
-
* @param agentsDirOverride - When provided, treats this single dir as the
|
|
134
|
-
* only managed hook root. Used by tests to inject a temp path. In normal
|
|
135
|
-
* operation, both user and system roots are consulted with user precedence.
|
|
136
|
-
*/
|
|
137
127
|
export declare function registerHooksToSettings(agentId: AgentId, versionHome: string, hookManifest?: Record<string, ManifestHook>, agentsDirOverride?: string): {
|
|
138
128
|
registered: string[];
|
|
139
129
|
errors: string[];
|