@phnx-labs/agents-cli 1.20.50 → 1.20.51
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 +24 -0
- package/dist/commands/browser-picker.js +1 -18
- package/dist/commands/cloud.js +1 -25
- package/dist/commands/computer.d.ts +1 -0
- package/dist/commands/computer.js +129 -8
- package/dist/commands/exec.js +49 -6
- package/dist/commands/factory.js +1 -4
- package/dist/commands/inspect.js +1 -11
- package/dist/commands/mcp.js +2 -6
- package/dist/commands/message.js +1 -4
- package/dist/commands/profiles.js +1 -18
- package/dist/commands/repo.js +33 -14
- package/dist/commands/resource-view.d.ts +1 -0
- package/dist/commands/resource-view.js +5 -17
- package/dist/commands/secrets.d.ts +1 -0
- package/dist/commands/secrets.js +1 -28
- package/dist/commands/sessions-picker.js +1 -18
- package/dist/commands/sessions.js +6 -8
- package/dist/commands/teams-picker.js +1 -32
- package/dist/commands/teams.js +1 -27
- package/dist/commands/tmux.js +1 -3
- package/dist/commands/view.js +1 -9
- package/dist/commands/worktree.js +1 -4
- package/dist/lib/agents.d.ts +0 -4
- package/dist/lib/agents.js +20 -33
- package/dist/lib/auto-dispatch-linear.d.ts +18 -0
- package/dist/lib/auto-dispatch-linear.js +107 -0
- package/dist/lib/auto-dispatch-provider.d.ts +10 -0
- package/dist/lib/auto-dispatch-provider.js +25 -0
- package/dist/lib/auto-dispatch.d.ts +87 -0
- package/dist/lib/auto-dispatch.js +142 -0
- package/dist/lib/browser/cdp.js +11 -2
- package/dist/lib/browser/drivers/ssh.d.ts +28 -10
- package/dist/lib/browser/drivers/ssh.js +57 -18
- package/dist/lib/browser/refs.js +1 -5
- package/dist/lib/cli-resources.d.ts +0 -2
- package/dist/lib/cli-resources.js +30 -13
- package/dist/lib/cloud/rush.d.ts +0 -24
- package/dist/lib/cloud/rush.js +0 -31
- package/dist/lib/crabbox/cli.js +4 -1
- package/dist/lib/crabbox/lease.js +29 -1
- package/dist/lib/daemon.js +41 -0
- package/dist/lib/exec.js +31 -14
- package/dist/lib/format.d.ts +38 -0
- package/dist/lib/format.js +108 -0
- package/dist/lib/git.d.ts +21 -0
- package/dist/lib/git.js +92 -0
- package/dist/lib/hooks/cache.d.ts +9 -2
- package/dist/lib/hooks/cache.js +220 -8
- package/dist/lib/hooks.js +17 -8
- package/dist/lib/platform/exec.d.ts +4 -1
- package/dist/lib/platform/exec.js +8 -2
- package/dist/lib/resources.d.ts +0 -8
- package/dist/lib/resources.js +0 -10
- package/dist/lib/runner.js +10 -2
- 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/session/active.d.ts +11 -1
- package/dist/lib/session/active.js +3 -0
- package/dist/lib/session/db.d.ts +1 -4
- package/dist/lib/session/db.js +20 -25
- package/dist/lib/session/discover.d.ts +2 -2
- package/dist/lib/session/discover.js +61 -48
- package/dist/lib/session/parse.js +35 -34
- package/dist/lib/session/render.d.ts +7 -3
- package/dist/lib/session/render.js +15 -9
- package/dist/lib/session/state.d.ts +55 -0
- package/dist/lib/session/state.js +87 -10
- package/dist/lib/session/types.d.ts +9 -0
- package/dist/lib/shims.d.ts +9 -3
- package/dist/lib/shims.js +44 -8
- package/dist/lib/ssh-tunnel.d.ts +33 -2
- package/dist/lib/ssh-tunnel.js +94 -7
- package/dist/lib/staleness/types.d.ts +0 -1
- package/dist/lib/types.d.ts +14 -1
- package/dist/lib/versions.d.ts +0 -26
- package/dist/lib/versions.js +2 -145
- package/dist/lib/warn-unpushed.d.ts +40 -0
- package/dist/lib/warn-unpushed.js +128 -0
- package/package.json +3 -1
- package/dist/lib/resources/index.d.ts +0 -53
- package/dist/lib/resources/index.js +0 -76
package/dist/lib/hooks/cache.js
CHANGED
|
@@ -105,13 +105,19 @@ export function getHookShimPath(name) {
|
|
|
105
105
|
/**
|
|
106
106
|
* Generate (or refresh) the shim script for a hook. Idempotent — only writes
|
|
107
107
|
* when the content differs from what's on disk. Returns the absolute shim path.
|
|
108
|
+
*
|
|
109
|
+
* A shim is generated when the hook opts into caching (`cache`) and/or declares
|
|
110
|
+
* `matches:` predicates. When `matches` is present the shim gates execution on
|
|
111
|
+
* those predicates before running the underlying script (see `renderShim`);
|
|
112
|
+
* when `cache` is absent the shim is a thin pass-through wrapper that only
|
|
113
|
+
* applies the gate and forwards stdin/stdout unchanged.
|
|
108
114
|
*/
|
|
109
115
|
export function generateHookShim(args) {
|
|
110
116
|
const shimsDir = args.paths?.shimsDir ?? getHookShimsDir();
|
|
111
117
|
const cacheDir = args.paths?.cacheDir ?? getHookCacheDir();
|
|
112
118
|
const logsDir = args.paths?.logsDir ?? getLogsDir();
|
|
113
119
|
const shimPath = resolveContainedHookShimPath(shimsDir, args.name);
|
|
114
|
-
const content = renderShim(args.name, args.scriptPath, args.cache, { cacheDir, logsDir });
|
|
120
|
+
const content = renderShim(args.name, args.scriptPath, args.cache ?? null, args.matches, { cacheDir, logsDir });
|
|
115
121
|
fs.mkdirSync(shimsDir, { recursive: true });
|
|
116
122
|
let existing = null;
|
|
117
123
|
if (fs.existsSync(shimPath)) {
|
|
@@ -132,24 +138,204 @@ export function generateHookShim(args) {
|
|
|
132
138
|
}
|
|
133
139
|
return shimPath;
|
|
134
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* The matches: gate, as a self-contained Python program run once per fire.
|
|
143
|
+
*
|
|
144
|
+
* Reads the hook's `matches:` block from $MATCHES_JSON and the event JSON from
|
|
145
|
+
* stdin, then prints `FIRE` or `SKIP`. A faithful port of `shouldFire()` in
|
|
146
|
+
* src/lib/hooks/match.ts — all declared predicates AND together, an empty block
|
|
147
|
+
* always fires, and the same ReDoS guard (`isSafeHookRegex`) rejects unsafe
|
|
148
|
+
* regexes to `SKIP`. Kept in double-quotes/apostrophe-free so it survives being
|
|
149
|
+
* embedded in a single-quoted `python -c '...'` argument in the shim. Behavioural
|
|
150
|
+
* parity with shouldFire() is pinned by a conformance test (match-parity.test.ts).
|
|
151
|
+
*
|
|
152
|
+
* On any exception it does NOT print SKIP — the shim treats a missing/garbled
|
|
153
|
+
* verdict as FIRE (fail-open), so a broken gate never silently disables a hook.
|
|
154
|
+
*/
|
|
155
|
+
const GATE_PY = `import json, os, re, subprocess, sys
|
|
156
|
+
|
|
157
|
+
def arr(v):
|
|
158
|
+
if v is None:
|
|
159
|
+
return []
|
|
160
|
+
return v if isinstance(v, list) else [v]
|
|
161
|
+
|
|
162
|
+
def max_group_depth(src):
|
|
163
|
+
depth = 0
|
|
164
|
+
mx = 0
|
|
165
|
+
escaped = False
|
|
166
|
+
in_class = False
|
|
167
|
+
for ch in src:
|
|
168
|
+
if escaped:
|
|
169
|
+
escaped = False
|
|
170
|
+
continue
|
|
171
|
+
if ch == chr(92):
|
|
172
|
+
escaped = True
|
|
173
|
+
continue
|
|
174
|
+
if ch == "[":
|
|
175
|
+
in_class = True
|
|
176
|
+
continue
|
|
177
|
+
if ch == "]":
|
|
178
|
+
in_class = False
|
|
179
|
+
continue
|
|
180
|
+
if in_class:
|
|
181
|
+
continue
|
|
182
|
+
if ch == "(":
|
|
183
|
+
depth += 1
|
|
184
|
+
if depth > mx:
|
|
185
|
+
mx = depth
|
|
186
|
+
elif ch == ")" and depth > 0:
|
|
187
|
+
depth -= 1
|
|
188
|
+
return mx
|
|
189
|
+
|
|
190
|
+
_NESTED = re.compile(r"\\((?:\\?:)?[^)]*[*+][?+*{,\\d}]*[^)]*\\)\\s*(?:[+*]|\\{\\d*,?\\d*\\})")
|
|
191
|
+
|
|
192
|
+
def is_safe(src):
|
|
193
|
+
if len(src) > 200:
|
|
194
|
+
return False
|
|
195
|
+
if max_group_depth(src) > 3:
|
|
196
|
+
return False
|
|
197
|
+
if _NESTED.search(src):
|
|
198
|
+
return False
|
|
199
|
+
return True
|
|
200
|
+
|
|
201
|
+
def compile_rx(src):
|
|
202
|
+
if not is_safe(src):
|
|
203
|
+
return None
|
|
204
|
+
try:
|
|
205
|
+
return re.compile(src)
|
|
206
|
+
except re.error:
|
|
207
|
+
return None
|
|
208
|
+
|
|
209
|
+
def find_root(start):
|
|
210
|
+
d = os.path.abspath(start)
|
|
211
|
+
while True:
|
|
212
|
+
if os.path.exists(os.path.join(d, ".git")):
|
|
213
|
+
return d
|
|
214
|
+
parent = os.path.dirname(d)
|
|
215
|
+
if parent == d:
|
|
216
|
+
return None
|
|
217
|
+
d = parent
|
|
218
|
+
|
|
219
|
+
def git_dirty(cwd):
|
|
220
|
+
try:
|
|
221
|
+
out = subprocess.run(
|
|
222
|
+
["git", "status", "--porcelain"],
|
|
223
|
+
cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
|
|
224
|
+
)
|
|
225
|
+
return len(out.stdout.decode().strip()) > 0
|
|
226
|
+
except Exception:
|
|
227
|
+
return False
|
|
228
|
+
|
|
229
|
+
def should_fire():
|
|
230
|
+
m = json.loads(os.environ.get("MATCHES_JSON") or "{}")
|
|
231
|
+
if not m:
|
|
232
|
+
return True
|
|
233
|
+
try:
|
|
234
|
+
inp = json.load(sys.stdin)
|
|
235
|
+
except Exception:
|
|
236
|
+
inp = {}
|
|
237
|
+
cwd = inp.get("cwd") or os.getcwd()
|
|
238
|
+
|
|
239
|
+
v = m.get("prompt_contains")
|
|
240
|
+
if v is not None:
|
|
241
|
+
if v not in (inp.get("prompt") or ""):
|
|
242
|
+
return False
|
|
243
|
+
|
|
244
|
+
v = m.get("prompt_matches")
|
|
245
|
+
if v is not None:
|
|
246
|
+
rx = compile_rx(v)
|
|
247
|
+
if rx is None or not rx.search(inp.get("prompt") or ""):
|
|
248
|
+
return False
|
|
249
|
+
|
|
250
|
+
v = m.get("tool_name")
|
|
251
|
+
if v is not None:
|
|
252
|
+
allowed = arr(v)
|
|
253
|
+
if allowed:
|
|
254
|
+
tn = inp.get("tool_name")
|
|
255
|
+
if not tn or tn not in allowed:
|
|
256
|
+
return False
|
|
257
|
+
|
|
258
|
+
v = m.get("tool_args_match")
|
|
259
|
+
if v is not None:
|
|
260
|
+
ta = inp.get("tool_args")
|
|
261
|
+
ser = ta if isinstance(ta, str) else json.dumps(ta if ta is not None else "", separators=(",", ":"))
|
|
262
|
+
rx = compile_rx(v)
|
|
263
|
+
if rx is None or not rx.search(ser):
|
|
264
|
+
return False
|
|
265
|
+
|
|
266
|
+
v = m.get("cwd_includes")
|
|
267
|
+
if v is not None:
|
|
268
|
+
needles = arr(v)
|
|
269
|
+
if needles and not any(n in cwd for n in needles):
|
|
270
|
+
return False
|
|
271
|
+
|
|
272
|
+
v = m.get("project_has")
|
|
273
|
+
if v is not None:
|
|
274
|
+
root = find_root(cwd)
|
|
275
|
+
if not root or not os.path.exists(os.path.join(root, v)):
|
|
276
|
+
return False
|
|
277
|
+
|
|
278
|
+
v = m.get("git_dirty")
|
|
279
|
+
if v is not None:
|
|
280
|
+
if bool(v) != git_dirty(cwd):
|
|
281
|
+
return False
|
|
282
|
+
|
|
283
|
+
return True
|
|
284
|
+
|
|
285
|
+
print("FIRE" if should_fire() else "SKIP")
|
|
286
|
+
`;
|
|
287
|
+
/**
|
|
288
|
+
* Gate-only pass-through tail: no caching, just run the underlying script with
|
|
289
|
+
* stdin forwarded and stdout/exit code propagated, plus one timing log line.
|
|
290
|
+
* Used when a hook declares \`matches:\` but no \`cache:\`.
|
|
291
|
+
*/
|
|
292
|
+
const PASSTHROUGH_TAIL = `now_ns() { "$PY" -c 'import time; print(int(time.time()*1e9))'; }
|
|
293
|
+
START_NS=$(now_ns)
|
|
294
|
+
EXIT=0
|
|
295
|
+
if printf '%s' "$STDIN_PAYLOAD" | "$SOURCE"; then
|
|
296
|
+
EXIT=0
|
|
297
|
+
else
|
|
298
|
+
EXIT=$?
|
|
299
|
+
fi
|
|
300
|
+
END_NS=$(now_ns)
|
|
301
|
+
MS=$(( (END_NS - START_NS) / 1000000 ))
|
|
302
|
+
TS=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
303
|
+
LOG_FILE="$LOGS_DIR/events-$(date -u +%Y-%m-%d).jsonl"
|
|
304
|
+
printf '{"ts":"%s","event":"hook.fire","hook":"%s","ms":%d,"cache":"%s","exit":%d}\\n' \\
|
|
305
|
+
"$TS" "$HOOK_NAME" "$MS" "none" "$EXIT" >>"$LOG_FILE" 2>/dev/null || true
|
|
306
|
+
|
|
307
|
+
exit "$EXIT"`;
|
|
135
308
|
/**
|
|
136
309
|
* Render the bash shim. Bash 3.2-compatible (macOS default). Uses Python for
|
|
137
310
|
* hashing + monotonic-ish nanosecond timing + portable mtime, resolved at
|
|
138
311
|
* runtime (python3, then python) so a Windows Microsoft Store `python3` alias
|
|
139
312
|
* stub — which exits non-zero without running — doesn't silently break caching.
|
|
313
|
+
*
|
|
314
|
+
* When `matches` is set, an early gate block evaluates the `matches:` predicates
|
|
315
|
+
* against the event JSON on stdin and exits 0 without running the script when
|
|
316
|
+
* they don't hold — this is the runtime enforcement of the documented `matches:`
|
|
317
|
+
* gating (mirrors `shouldFire()` in match.ts). When `cache` is null the shim is
|
|
318
|
+
* a gate-only pass-through: it forwards stdin to the script and its stdout back,
|
|
319
|
+
* with no cache read/write.
|
|
140
320
|
*/
|
|
141
|
-
function renderShim(name, scriptPath, cache, paths) {
|
|
142
|
-
const ttl = typeof cache.ttl === 'number' ? cache.ttl : (parseDuration(cache.ttl) ?? 0);
|
|
143
|
-
const key = cache
|
|
144
|
-
const prefetch = cache
|
|
321
|
+
function renderShim(name, scriptPath, cache, matches, paths) {
|
|
322
|
+
const ttl = cache ? (typeof cache.ttl === 'number' ? cache.ttl : (parseDuration(cache.ttl) ?? 0)) : 0;
|
|
323
|
+
const key = cache?.key ?? 'global';
|
|
324
|
+
const prefetch = cache?.prefetch ?? 'none';
|
|
145
325
|
const { cacheDir, logsDir } = paths;
|
|
326
|
+
const hasMatches = matches != null && Object.keys(matches).length > 0;
|
|
327
|
+
const matchesJson = hasMatches ? JSON.stringify(matches) : '';
|
|
146
328
|
// sh-escape: wrap in single quotes, escape any embedded single quotes.
|
|
147
329
|
const q = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
|
|
330
|
+
const cacheHeader = cache
|
|
331
|
+
? `# Cache: key=${key} ttl=${ttl}s prefetch=${prefetch}`
|
|
332
|
+
: `# Cache: none (gate-only pass-through)`;
|
|
333
|
+
const matchesHeader = hasMatches ? `\n# Matches: ${matchesJson}` : '';
|
|
148
334
|
return `#!/usr/bin/env bash
|
|
149
335
|
# GENERATED by agents-cli. Do not edit — re-run \`agents hooks sync\` to refresh.
|
|
150
336
|
# Hook: ${name}
|
|
151
337
|
# Source: ${scriptPath}
|
|
152
|
-
|
|
338
|
+
${cacheHeader}${matchesHeader}
|
|
153
339
|
set -u
|
|
154
340
|
|
|
155
341
|
HOOK_NAME=${q(name)}
|
|
@@ -159,6 +345,7 @@ LOGS_DIR=${q(logsDir)}
|
|
|
159
345
|
TTL=${ttl}
|
|
160
346
|
PREFETCH=${q(prefetch)}
|
|
161
347
|
KEY_MODE=${q(key)}
|
|
348
|
+
MATCHES_JSON=${q(matchesJson)}
|
|
162
349
|
|
|
163
350
|
mkdir -p "$CACHE_DIR" "$LOGS_DIR"
|
|
164
351
|
|
|
@@ -178,7 +365,33 @@ done
|
|
|
178
365
|
# Read stdin once (Claude/Codex/Gemini pass JSON on stdin to every hook).
|
|
179
366
|
STDIN_PAYLOAD="$(cat || true)"
|
|
180
367
|
|
|
181
|
-
#
|
|
368
|
+
# --- matches: gate (issue #744 / RUSH-1506) -------------------------------
|
|
369
|
+
# Enforce the hook's declared \`matches:\` predicates at fire time. Mirrors
|
|
370
|
+
# shouldFire() in src/lib/hooks/match.ts: all declared predicates AND together;
|
|
371
|
+
# an empty/absent block always fires. When the predicates don't hold we exit 0
|
|
372
|
+
# WITHOUT running the script (a skipped hook is not an error). Fail-open: any
|
|
373
|
+
# gate-eval error runs the script, so a broken predicate can never silently
|
|
374
|
+
# disable a safety hook (e.g. git-guard).
|
|
375
|
+
if [ -n "$MATCHES_JSON" ]; then
|
|
376
|
+
_GATE="$(printf '%s' "$STDIN_PAYLOAD" | MATCHES_JSON="$MATCHES_JSON" "$PY" -c ${q(GATE_PY)} 2>/dev/null || printf FIRE)"
|
|
377
|
+
[ -z "$_GATE" ] && _GATE=FIRE
|
|
378
|
+
if [ "$_GATE" = SKIP ]; then
|
|
379
|
+
_TS=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
380
|
+
_LOG_FILE="$LOGS_DIR/events-$(date -u +%Y-%m-%d).jsonl"
|
|
381
|
+
printf '{"ts":"%s","event":"hook.fire","hook":"%s","ms":0,"cache":"skip","exit":0}\\n' \\
|
|
382
|
+
"$_TS" "$HOOK_NAME" >>"$_LOG_FILE" 2>/dev/null || true
|
|
383
|
+
exit 0
|
|
384
|
+
fi
|
|
385
|
+
fi
|
|
386
|
+
${cache ? CACHE_TAIL : PASSTHROUGH_TAIL}
|
|
387
|
+
`;
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Cache tail: the full cache lookup / stale-while-revalidate / timing machinery.
|
|
391
|
+
* Emitted only when the hook opts into \`cache:\`. (When only \`matches:\` is set,
|
|
392
|
+
* PASSTHROUGH_TAIL runs instead — no cache read/write.)
|
|
393
|
+
*/
|
|
394
|
+
const CACHE_TAIL = `# Portable sha1 — \`shasum\` is Perl, missing on minimal Linux images;
|
|
182
395
|
# \`sha1sum\` is coreutils, missing on macOS. Truncate to 12 hex chars.
|
|
183
396
|
sha1_12() { "$PY" -c 'import hashlib,sys; print(hashlib.sha1(sys.stdin.read().encode()).hexdigest()[:12])'; }
|
|
184
397
|
|
|
@@ -267,7 +480,6 @@ printf '{"ts":"%s","event":"hook.fire","hook":"%s","ms":%d,"cache":"%s","exit":%
|
|
|
267
480
|
|
|
268
481
|
exit "$EXIT"
|
|
269
482
|
`;
|
|
270
|
-
}
|
|
271
483
|
/**
|
|
272
484
|
* Remove a hook's shim. Called by the registrar's garbage collection when a
|
|
273
485
|
* hook is renamed/deleted or has its `cache:` field removed.
|
package/dist/lib/hooks.js
CHANGED
|
@@ -153,10 +153,12 @@ import { getHookShimsDir } from './state.js';
|
|
|
153
153
|
/**
|
|
154
154
|
* Resolve the command path to register for a hook.
|
|
155
155
|
*
|
|
156
|
-
* Returns either the raw script path (
|
|
157
|
-
* the path to a generated
|
|
158
|
-
* side effect when `cache:` is configured
|
|
159
|
-
*
|
|
156
|
+
* Returns either the raw script path (neither `cache:` nor `matches:` set,
|
|
157
|
+
* legacy behavior) or the path to a generated wrapper shim. The shim is written
|
|
158
|
+
* as a side effect when `cache:` and/or `matches:` is configured — it enforces
|
|
159
|
+
* the `matches:` gate at fire time and layers the caching/timing machinery when
|
|
160
|
+
* `cache:` is set. The agent-native settings file gets the same shape either
|
|
161
|
+
* way — just a different command path.
|
|
160
162
|
*/
|
|
161
163
|
function resolveHookCommand(name, hookDef, resolveScript) {
|
|
162
164
|
const scriptPath = resolveScript(hookDef.script);
|
|
@@ -165,13 +167,20 @@ function resolveHookCommand(name, hookDef, resolveScript) {
|
|
|
165
167
|
if (!isValidHookShimName(name))
|
|
166
168
|
return null;
|
|
167
169
|
const cache = parseCacheConfig(hookDef.cache);
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
170
|
+
const matches = hookDef.matches;
|
|
171
|
+
const hasMatches = matches != null && Object.keys(matches).length > 0;
|
|
172
|
+
if (!cache && !hasMatches) {
|
|
173
|
+
// No caching and no matches: gate opted in — make sure a previously
|
|
174
|
+
// generated shim from an earlier `cache:`/`matches:` config is gone so the
|
|
175
|
+
// JSONL doesn't keep claiming hits.
|
|
171
176
|
removeHookShim(name);
|
|
172
177
|
return toPortableCommand(scriptPath);
|
|
173
178
|
}
|
|
174
|
-
|
|
179
|
+
// A shim is generated when the hook opts into caching and/or declares
|
|
180
|
+
// `matches:` predicates. The shim enforces the `matches:` gate at fire time
|
|
181
|
+
// (skipping the script when predicates don't hold) and, when `cache:` is set,
|
|
182
|
+
// layers the cache/timing machinery on top.
|
|
183
|
+
return toPortableCommand(generateHookShim({ name, scriptPath, cache, matches }));
|
|
175
184
|
}
|
|
176
185
|
/**
|
|
177
186
|
* Extensions that are NEVER hooks — docs, configuration, plain data. A file
|
|
@@ -14,7 +14,10 @@ export declare function needsWindowsShell(binary: string, platform?: NodeJS.Plat
|
|
|
14
14
|
* Resolve an executable name to its absolute path via the OS PATH search, or
|
|
15
15
|
* `null` if not found. On Windows `where` can return several lines (one per
|
|
16
16
|
* PATHEXT match, e.g. `agents.cmd` and `agents.ps1`) — the first is the one the
|
|
17
|
-
* shell would actually run, matching `which` semantics on POSIX.
|
|
17
|
+
* shell would actually run, matching `which` semantics on POSIX. stderr is
|
|
18
|
+
* dropped: a miss is an expected outcome here, and `where.exe` announces every
|
|
19
|
+
* miss on stderr ("INFO: Could not find files..."), which would otherwise leak
|
|
20
|
+
* into the caller's terminal once per probe.
|
|
18
21
|
*/
|
|
19
22
|
export declare function findExecutable(name: string, platform?: NodeJS.Platform): string | null;
|
|
20
23
|
/**
|
|
@@ -27,11 +27,17 @@ export function needsWindowsShell(binary, platform = process.platform) {
|
|
|
27
27
|
* Resolve an executable name to its absolute path via the OS PATH search, or
|
|
28
28
|
* `null` if not found. On Windows `where` can return several lines (one per
|
|
29
29
|
* PATHEXT match, e.g. `agents.cmd` and `agents.ps1`) — the first is the one the
|
|
30
|
-
* shell would actually run, matching `which` semantics on POSIX.
|
|
30
|
+
* shell would actually run, matching `which` semantics on POSIX. stderr is
|
|
31
|
+
* dropped: a miss is an expected outcome here, and `where.exe` announces every
|
|
32
|
+
* miss on stderr ("INFO: Could not find files..."), which would otherwise leak
|
|
33
|
+
* into the caller's terminal once per probe.
|
|
31
34
|
*/
|
|
32
35
|
export function findExecutable(name, platform = process.platform) {
|
|
33
36
|
try {
|
|
34
|
-
const out = execFileSync(whichCommand(platform), [name], {
|
|
37
|
+
const out = execFileSync(whichCommand(platform), [name], {
|
|
38
|
+
encoding: 'utf-8',
|
|
39
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
40
|
+
});
|
|
35
41
|
const first = out.trim().split(/\r?\n/)[0]?.trim();
|
|
36
42
|
return first || null;
|
|
37
43
|
}
|
package/dist/lib/resources.d.ts
CHANGED
|
@@ -74,11 +74,3 @@ export interface GetAgentResourcesOptions {
|
|
|
74
74
|
* This is the source of truth - not the tracking data in agents.yaml.
|
|
75
75
|
*/
|
|
76
76
|
export declare function getAgentResources(agentId: AgentId, options?: GetAgentResourcesOptions): AgentResources;
|
|
77
|
-
/**
|
|
78
|
-
* Get resources for all agents.
|
|
79
|
-
*/
|
|
80
|
-
export declare function getAllAgentResources(agentIds: AgentId[], options?: GetAgentResourcesOptions & {
|
|
81
|
-
cliStates?: Record<AgentId, {
|
|
82
|
-
installed: boolean;
|
|
83
|
-
}>;
|
|
84
|
-
}): AgentResources[];
|
package/dist/lib/resources.js
CHANGED
|
@@ -180,13 +180,3 @@ export function getAgentResources(agentId, options = {}) {
|
|
|
180
180
|
workflows,
|
|
181
181
|
};
|
|
182
182
|
}
|
|
183
|
-
/**
|
|
184
|
-
* Get resources for all agents.
|
|
185
|
-
*/
|
|
186
|
-
export function getAllAgentResources(agentIds, options = {}) {
|
|
187
|
-
const { cliStates, ...restOptions } = options;
|
|
188
|
-
return agentIds.map((agentId) => {
|
|
189
|
-
const cliInstalled = cliStates?.[agentId]?.installed ?? true;
|
|
190
|
-
return getAgentResources(agentId, { ...restOptions, cliInstalled });
|
|
191
|
-
});
|
|
192
|
-
}
|
package/dist/lib/runner.js
CHANGED
|
@@ -74,8 +74,16 @@ export function buildJobCommand(config, resolvedPrompt) {
|
|
|
74
74
|
appendModelAndReasoning(cmd, config);
|
|
75
75
|
}
|
|
76
76
|
if (config.agent === 'codex') {
|
|
77
|
-
if (mode === '
|
|
78
|
-
|
|
77
|
+
if (mode === 'plan') {
|
|
78
|
+
// The template defaults to workspace-write; plan means read-only.
|
|
79
|
+
const sbIndex = cmd.indexOf('--sandbox');
|
|
80
|
+
if (sbIndex !== -1)
|
|
81
|
+
cmd[sbIndex + 1] = 'read-only';
|
|
82
|
+
}
|
|
83
|
+
else if (mode === 'edit' || mode === 'auto') {
|
|
84
|
+
// Keep the workspace-write sandbox — no approval bypass; only skip drops
|
|
85
|
+
// the guardrails. Re-enable network, which workspace-write turns off.
|
|
86
|
+
cmd.push('-c', 'sandbox_workspace_write.network_access=true');
|
|
79
87
|
}
|
|
80
88
|
else if (mode === 'skip') {
|
|
81
89
|
// Remove sandbox restriction, just --dangerously-bypass-approvals-and-sandbox
|
|
Binary file
|
|
Binary file
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type PidSessionEntry } from './pid-registry.js';
|
|
2
|
-
import { type SessionActivity, type AwaitingReason, type DetectedPr, type DetectedWorktree, type DetectedTicket } from './state.js';
|
|
2
|
+
import { type SessionActivity, type AwaitingReason, type StructuredQuestion, type DetectedPr, type DetectedWorktree, type DetectedTicket } from './state.js';
|
|
3
3
|
import { type SessionProvenance } from './provenance.js';
|
|
4
4
|
/**
|
|
5
5
|
* Per-PID `lsof` probes run bounded and staggered rather than as one parallel
|
|
@@ -30,6 +30,16 @@ export interface ActiveSession {
|
|
|
30
30
|
activity?: SessionActivity;
|
|
31
31
|
/** Why the agent is waiting, when activity is waiting_input. */
|
|
32
32
|
awaitingReason?: AwaitingReason;
|
|
33
|
+
/** The structured decision (question/plan/permission + options) the agent is waiting on. */
|
|
34
|
+
question?: StructuredQuestion;
|
|
35
|
+
/**
|
|
36
|
+
* Plan markdown from the last `ExitPlanMode` tool call. Present when the
|
|
37
|
+
* transcript ever entered plan-review; `awaitingReason === 'plan_review'`
|
|
38
|
+
* says whether it is still pending.
|
|
39
|
+
*/
|
|
40
|
+
plan?: string;
|
|
41
|
+
/** Last few assistant turns (most-recent last), for at-a-glance context in the UI. */
|
|
42
|
+
tail?: string[];
|
|
33
43
|
/** PR opened during the session. */
|
|
34
44
|
pr?: DetectedPr;
|
|
35
45
|
/** Worktree the session runs in. */
|
|
@@ -236,6 +236,9 @@ function applyState(base, state, fallbackFile) {
|
|
|
236
236
|
status: statusFromActivity(state.activity),
|
|
237
237
|
activity: state.activity,
|
|
238
238
|
awaitingReason: state.awaitingReason,
|
|
239
|
+
question: state.question,
|
|
240
|
+
plan: state.plan,
|
|
241
|
+
tail: state.tail,
|
|
239
242
|
// Prefer the live preview (latest turn); keep the first-prompt topic as a fallback.
|
|
240
243
|
preview: state.preview ?? base.preview,
|
|
241
244
|
pr: state.pr,
|
package/dist/lib/session/db.d.ts
CHANGED
|
@@ -35,6 +35,7 @@ export interface SessionRow {
|
|
|
35
35
|
pr_number: number | null;
|
|
36
36
|
worktree_slug: string | null;
|
|
37
37
|
ticket_id: string | null;
|
|
38
|
+
plan: string | null;
|
|
38
39
|
}
|
|
39
40
|
/** File stat snapshot used to detect changes between scan runs. */
|
|
40
41
|
export interface ScanStamp {
|
|
@@ -199,10 +200,6 @@ export interface TopCostSession {
|
|
|
199
200
|
* vanished, mirroring querySessions' liveness filter.
|
|
200
201
|
*/
|
|
201
202
|
export declare function topSessionsByCost(n: number, options?: QueryOptions): TopCostSession[];
|
|
202
|
-
/** Return the set of all file paths currently tracked in the sessions table. */
|
|
203
|
-
export declare function getAllFilePaths(): Set<string>;
|
|
204
|
-
/** Look up sessions by their source file paths. */
|
|
205
|
-
export declare function getSessionsByFilePaths(paths: string[]): Map<string, SessionMeta>;
|
|
206
203
|
/** Look up a single session by its unique ID. */
|
|
207
204
|
export declare function getSessionById(id: string): SessionMeta | null;
|
|
208
205
|
/**
|
package/dist/lib/session/db.js
CHANGED
|
@@ -13,7 +13,7 @@ import { getSessionsDir, getSessionsDbPath } from '../state.js';
|
|
|
13
13
|
const SESSIONS_DIR = getSessionsDir();
|
|
14
14
|
const DB_PATH = getSessionsDbPath();
|
|
15
15
|
/** Current schema version; bumped when migrations are added. */
|
|
16
|
-
const SCHEMA_VERSION =
|
|
16
|
+
const SCHEMA_VERSION = 11;
|
|
17
17
|
/**
|
|
18
18
|
* Canonicalize a file path for use as a scan_ledger key. The same physical
|
|
19
19
|
* session file is reachable via multiple aliases — `~/.claude/projects/x.jsonl`
|
|
@@ -64,7 +64,8 @@ CREATE TABLE IF NOT EXISTS sessions (
|
|
|
64
64
|
pr_url TEXT,
|
|
65
65
|
pr_number INTEGER,
|
|
66
66
|
worktree_slug TEXT,
|
|
67
|
-
ticket_id TEXT
|
|
67
|
+
ticket_id TEXT,
|
|
68
|
+
plan TEXT
|
|
68
69
|
);
|
|
69
70
|
CREATE INDEX IF NOT EXISTS idx_sessions_timestamp ON sessions(timestamp DESC);
|
|
70
71
|
CREATE INDEX IF NOT EXISTS idx_sessions_cwd ON sessions(cwd);
|
|
@@ -213,6 +214,16 @@ function migrateSchema(db, fromVersion) {
|
|
|
213
214
|
db.exec(`ALTER TABLE sessions DROP COLUMN name`);
|
|
214
215
|
}
|
|
215
216
|
}
|
|
217
|
+
if (fromVersion < 11) {
|
|
218
|
+
// v10 → v11: the Claude scanner now captures the ExitPlanMode plan markdown
|
|
219
|
+
// at scan time so `agents sessions --json` can surface it without forcing
|
|
220
|
+
// consumers (the Factory NEEDS-YOU panel) to re-read raw JSONL. Additive
|
|
221
|
+
// column; rescan to backfill.
|
|
222
|
+
const cols = db.prepare(`PRAGMA table_info(sessions)`).all();
|
|
223
|
+
if (!cols.some(c => c.name === 'plan'))
|
|
224
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN plan TEXT`);
|
|
225
|
+
db.exec(`DELETE FROM scan_ledger;`);
|
|
226
|
+
}
|
|
216
227
|
}
|
|
217
228
|
/** Open (or return the cached) sessions database, applying migrations as needed. */
|
|
218
229
|
export function getDB() {
|
|
@@ -429,13 +440,13 @@ const upsertSessionStmt = (db) => db.prepare(`
|
|
|
429
440
|
project, cwd, git_branch, topic, label, message_count, token_count,
|
|
430
441
|
cost_usd, duration_ms,
|
|
431
442
|
file_path, file_mtime_ms, file_size, scanned_at, is_team_origin,
|
|
432
|
-
pr_url, pr_number, worktree_slug, ticket_id
|
|
443
|
+
pr_url, pr_number, worktree_slug, ticket_id, plan
|
|
433
444
|
) VALUES (
|
|
434
445
|
@id, @short_id, @agent, @version, @account, @timestamp, @last_activity,
|
|
435
446
|
@project, @cwd, @git_branch, @topic, @label, @message_count, @token_count,
|
|
436
447
|
@cost_usd, @duration_ms,
|
|
437
448
|
@file_path, @file_mtime_ms, @file_size, @scanned_at, @is_team_origin,
|
|
438
|
-
@pr_url, @pr_number, @worktree_slug, @ticket_id
|
|
449
|
+
@pr_url, @pr_number, @worktree_slug, @ticket_id, @plan
|
|
439
450
|
)
|
|
440
451
|
ON CONFLICT(id) DO UPDATE SET
|
|
441
452
|
short_id = excluded.short_id,
|
|
@@ -461,7 +472,8 @@ const upsertSessionStmt = (db) => db.prepare(`
|
|
|
461
472
|
pr_url = excluded.pr_url,
|
|
462
473
|
pr_number = excluded.pr_number,
|
|
463
474
|
worktree_slug = excluded.worktree_slug,
|
|
464
|
-
ticket_id = excluded.ticket_id
|
|
475
|
+
ticket_id = excluded.ticket_id,
|
|
476
|
+
plan = excluded.plan
|
|
465
477
|
`);
|
|
466
478
|
const deleteTextStmt = (db) => db.prepare(`DELETE FROM session_text WHERE session_id = ?`);
|
|
467
479
|
const insertTextStmt = (db) => db.prepare(`INSERT INTO session_text (session_id, label, topic, project, content) VALUES (?, ?, ?, ?, ?)`);
|
|
@@ -509,6 +521,7 @@ export function upsertSession(meta, content, scan) {
|
|
|
509
521
|
pr_number: meta.prNumber ?? null,
|
|
510
522
|
worktree_slug: meta.worktreeSlug ?? null,
|
|
511
523
|
ticket_id: meta.ticketId ?? null,
|
|
524
|
+
plan: meta.plan ?? null,
|
|
512
525
|
};
|
|
513
526
|
const txn = db.transaction(() => {
|
|
514
527
|
upsert.run(row);
|
|
@@ -597,6 +610,7 @@ export function upsertSessionsBatch(entries) {
|
|
|
597
610
|
pr_number: meta.prNumber ?? null,
|
|
598
611
|
worktree_slug: meta.worktreeSlug ?? null,
|
|
599
612
|
ticket_id: meta.ticketId ?? null,
|
|
613
|
+
plan: meta.plan ?? null,
|
|
600
614
|
});
|
|
601
615
|
delText.run(meta.id);
|
|
602
616
|
insText.run(meta.id, meta.label ?? '', meta.topic ?? '', meta.project ?? '', content ?? '');
|
|
@@ -769,6 +783,7 @@ function rowToMeta(row) {
|
|
|
769
783
|
prNumber: row.pr_number ?? undefined,
|
|
770
784
|
worktreeSlug: row.worktree_slug ?? undefined,
|
|
771
785
|
ticketId: row.ticket_id ?? undefined,
|
|
786
|
+
plan: row.plan ?? undefined,
|
|
772
787
|
};
|
|
773
788
|
}
|
|
774
789
|
/**
|
|
@@ -954,26 +969,6 @@ export function topSessionsByCost(n, options = {}) {
|
|
|
954
969
|
durationMs: r.duration_ms ?? 0,
|
|
955
970
|
}));
|
|
956
971
|
}
|
|
957
|
-
/** Return the set of all file paths currently tracked in the sessions table. */
|
|
958
|
-
export function getAllFilePaths() {
|
|
959
|
-
const db = getDB();
|
|
960
|
-
const rows = db.prepare(`SELECT file_path FROM sessions`).all();
|
|
961
|
-
return new Set(rows.map(r => r.file_path));
|
|
962
|
-
}
|
|
963
|
-
/** Look up sessions by their source file paths. */
|
|
964
|
-
export function getSessionsByFilePaths(paths) {
|
|
965
|
-
if (paths.length === 0)
|
|
966
|
-
return new Map();
|
|
967
|
-
const db = getDB();
|
|
968
|
-
const placeholders = paths.map(() => '?').join(',');
|
|
969
|
-
const rows = db
|
|
970
|
-
.prepare(`SELECT * FROM sessions WHERE file_path IN (${placeholders})`)
|
|
971
|
-
.all(...paths);
|
|
972
|
-
const result = new Map();
|
|
973
|
-
for (const row of rows)
|
|
974
|
-
result.set(row.file_path, rowToMeta(row));
|
|
975
|
-
return result;
|
|
976
|
-
}
|
|
977
972
|
/** Look up a single session by its unique ID. */
|
|
978
973
|
export function getSessionById(id) {
|
|
979
974
|
const db = getDB();
|
|
@@ -68,6 +68,8 @@ interface ClaudeSessionScan {
|
|
|
68
68
|
createdTickets?: string[];
|
|
69
69
|
/** Team name this session SPAWNED via `agents teams create/add` (not team-of-origin). */
|
|
70
70
|
spawnedTeam?: string;
|
|
71
|
+
/** Plan markdown from the last ExitPlanMode tool call (Claude sessions only). */
|
|
72
|
+
plan?: string;
|
|
71
73
|
}
|
|
72
74
|
/**
|
|
73
75
|
* Discover sessions. Scans only files whose (mtime, size) have changed since
|
|
@@ -152,8 +154,6 @@ export declare function readCodexMeta(filePath: string, resolveAccount?: () => s
|
|
|
152
154
|
} | null>;
|
|
153
155
|
/** Stream a Claude JSONL file and extract scan-level metadata (timestamp, cwd, topic, tokens). */
|
|
154
156
|
export declare function scanClaudeSession(filePath: string): Promise<ClaudeSessionScan>;
|
|
155
|
-
/** Read up to maxLines non-empty lines from the beginning of a file. */
|
|
156
|
-
export declare function readFirstLines(filePath: string, maxLines: number): Promise<string[]>;
|
|
157
157
|
/** Parse a single Kimi session state.json file to extract session metadata. */
|
|
158
158
|
export declare function readKimiMeta(filePath: string): {
|
|
159
159
|
meta: SessionMeta;
|