@phnx-labs/agents-cli 1.20.35 → 1.20.37
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 +91 -0
- package/LICENSE +185 -21
- package/README.md +8 -4
- package/dist/commands/audit.d.ts +14 -0
- package/dist/commands/audit.js +68 -0
- package/dist/commands/browser.js +82 -8
- package/dist/commands/check.d.ts +15 -0
- package/dist/commands/check.js +84 -0
- package/dist/commands/cloud.js +143 -3
- package/dist/commands/computer.js +81 -0
- package/dist/commands/daemon.js +4 -1
- package/dist/commands/doctor.js +1 -89
- package/dist/commands/events.js +3 -3
- package/dist/commands/exec.d.ts +19 -0
- package/dist/commands/exec.js +277 -11
- package/dist/commands/hosts.js +10 -6
- package/dist/commands/inspect.js +8 -9
- package/dist/commands/lock.d.ts +12 -0
- package/dist/commands/lock.js +70 -0
- package/dist/commands/message.d.ts +15 -0
- package/dist/commands/message.js +56 -0
- package/dist/commands/routines.js +101 -5
- package/dist/commands/secrets-migrate.js +106 -57
- package/dist/commands/secrets.d.ts +31 -18
- package/dist/commands/secrets.js +156 -75
- package/dist/commands/serve.d.ts +10 -0
- package/dist/commands/serve.js +37 -0
- package/dist/commands/sessions-inject.d.ts +14 -0
- package/dist/commands/sessions-inject.js +111 -0
- package/dist/commands/sessions-picker.d.ts +2 -0
- package/dist/commands/sessions-picker.js +24 -3
- package/dist/commands/sessions-resume.js +20 -8
- package/dist/commands/sessions-sync.d.ts +3 -0
- package/dist/commands/sessions-sync.js +44 -4
- package/dist/commands/sessions.d.ts +78 -1
- package/dist/commands/sessions.js +506 -66
- package/dist/commands/setup.js +4 -2
- package/dist/commands/sync.d.ts +3 -1
- package/dist/commands/sync.js +156 -4
- package/dist/commands/teams.js +217 -0
- package/dist/commands/versions.js +2 -4
- package/dist/commands/watchdog.d.ts +18 -0
- package/dist/commands/watchdog.js +238 -0
- package/dist/index.js +25 -2
- package/dist/lib/audit/log.d.ts +92 -0
- package/dist/lib/audit/log.js +177 -0
- package/dist/lib/auto-pull.js +2 -1
- package/dist/lib/browser/chrome.d.ts +10 -0
- package/dist/lib/browser/chrome.js +18 -7
- package/dist/lib/browser/drivers/ssh.js +2 -1
- package/dist/lib/browser/har.d.ts +84 -0
- package/dist/lib/browser/har.js +77 -0
- package/dist/lib/browser/ipc.js +24 -3
- package/dist/lib/browser/profiles.d.ts +1 -1
- package/dist/lib/browser/profiles.js +8 -10
- package/dist/lib/browser/refs.d.ts +65 -0
- package/dist/lib/browser/refs.js +73 -1
- package/dist/lib/browser/runtime-state.js +1 -0
- package/dist/lib/browser/service.d.ts +38 -2
- package/dist/lib/browser/service.js +112 -8
- package/dist/lib/browser/types.d.ts +14 -1
- package/dist/lib/budget/live-cloud.d.ts +42 -0
- package/dist/lib/budget/live-cloud.js +79 -0
- package/dist/lib/budget/live-team.d.ts +31 -0
- package/dist/lib/budget/live-team.js +115 -0
- package/dist/lib/cloud/codex.js +4 -0
- package/dist/lib/cloud/rush.d.ts +12 -1
- package/dist/lib/cloud/rush.js +13 -3
- package/dist/lib/cloud/types.d.ts +9 -0
- package/dist/lib/computer/dispatch.d.ts +8 -0
- package/dist/lib/computer/dispatch.js +125 -0
- package/dist/lib/computer/loop.d.ts +62 -0
- package/dist/lib/computer/loop.js +98 -0
- package/dist/lib/computer/model.d.ts +44 -0
- package/dist/lib/computer/model.js +157 -0
- package/dist/lib/concurrency.d.ts +19 -0
- package/dist/lib/concurrency.js +33 -0
- package/dist/lib/daemon.d.ts +57 -0
- package/dist/lib/daemon.js +196 -18
- package/dist/lib/devices/registry.d.ts +7 -0
- package/dist/lib/devices/registry.js +24 -0
- package/dist/lib/devices/resolve-target.d.ts +24 -0
- package/dist/lib/devices/resolve-target.js +80 -0
- package/dist/lib/devices/tailscale.js +1 -1
- package/dist/lib/drift.d.ts +52 -0
- package/dist/lib/drift.js +112 -0
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/events.js +31 -13
- package/dist/lib/exec.d.ts +17 -0
- package/dist/lib/exec.js +79 -13
- package/dist/lib/git.d.ts +27 -0
- package/dist/lib/git.js +56 -1
- package/dist/lib/hooks/cache.d.ts +6 -0
- package/dist/lib/hooks/cache.js +54 -12
- package/dist/lib/hooks.d.ts +27 -0
- package/dist/lib/hooks.js +127 -8
- package/dist/lib/hosts/dispatch.d.ts +15 -0
- package/dist/lib/hosts/dispatch.js +39 -6
- package/dist/lib/hosts/logs.js +30 -1
- package/dist/lib/hosts/option.js +1 -1
- package/dist/lib/hosts/passthrough.js +3 -1
- package/dist/lib/hosts/ready.d.ts +29 -6
- package/dist/lib/hosts/ready.js +66 -15
- package/dist/lib/hosts/registry.d.ts +19 -2
- package/dist/lib/hosts/registry.js +58 -2
- package/dist/lib/hosts/remote-cmd.d.ts +62 -1
- package/dist/lib/hosts/remote-cmd.js +70 -1
- package/dist/lib/hosts/remote-os.d.ts +17 -0
- package/dist/lib/hosts/remote-os.js +30 -0
- package/dist/lib/hosts/session-index.d.ts +34 -0
- package/dist/lib/hosts/session-index.js +56 -0
- package/dist/lib/hosts/tasks.d.ts +14 -0
- package/dist/lib/hosts/tasks.js +15 -0
- package/dist/lib/lock.d.ts +93 -0
- package/dist/lib/lock.js +207 -0
- package/dist/lib/loop.js +16 -1
- package/dist/lib/machine-id.d.ts +21 -0
- package/dist/lib/machine-id.js +26 -0
- package/dist/lib/mailbox-target.d.ts +36 -0
- package/dist/lib/mailbox-target.js +45 -0
- package/dist/lib/mailbox.d.ts +47 -0
- package/dist/lib/mailbox.js +194 -0
- package/dist/lib/mcp.d.ts +5 -0
- package/dist/lib/mcp.js +24 -8
- package/dist/lib/migrate.d.ts +19 -0
- package/dist/lib/migrate.js +134 -26
- package/dist/lib/overdue.js +3 -0
- package/dist/lib/picker.d.ts +2 -0
- package/dist/lib/picker.js +4 -1
- package/dist/lib/platform/exec.d.ts +46 -0
- package/dist/lib/platform/exec.js +74 -0
- package/dist/lib/platform/process.d.ts +31 -0
- package/dist/lib/platform/process.js +34 -1
- package/dist/lib/platform/winpath.js +2 -0
- package/dist/lib/plugins.js +16 -6
- package/dist/lib/profiles.d.ts +25 -0
- package/dist/lib/profiles.js +22 -6
- package/dist/lib/pty-client.js +2 -1
- package/dist/lib/rotate.d.ts +61 -0
- package/dist/lib/rotate.js +52 -0
- package/dist/lib/routines.d.ts +40 -2
- package/dist/lib/routines.js +66 -8
- package/dist/lib/runner.d.ts +11 -2
- package/dist/lib/runner.js +49 -7
- package/dist/lib/scheduler.js +6 -1
- package/dist/lib/secrets/bundles.d.ts +60 -4
- package/dist/lib/secrets/bundles.js +131 -12
- package/dist/lib/secrets/filestore.d.ts +3 -0
- package/dist/lib/secrets/filestore.js +42 -16
- package/dist/lib/secrets/index.d.ts +43 -2
- package/dist/lib/secrets/index.js +102 -3
- package/dist/lib/secrets/mcp.d.ts +93 -0
- package/dist/lib/secrets/mcp.js +205 -0
- package/dist/lib/secrets/remote.js +12 -5
- package/dist/lib/secrets/sync.js +83 -4
- package/dist/lib/secrets/windows.js +14 -3
- package/dist/lib/serve/data.d.ts +81 -0
- package/dist/lib/serve/data.js +91 -0
- package/dist/lib/serve/page.d.ts +7 -0
- package/dist/lib/serve/page.js +140 -0
- package/dist/lib/serve/server.d.ts +46 -0
- package/dist/lib/serve/server.js +115 -0
- package/dist/lib/session/active.d.ts +73 -0
- package/dist/lib/session/active.js +199 -22
- package/dist/lib/session/db.d.ts +2 -1
- package/dist/lib/session/db.js +41 -5
- package/dist/lib/session/discover.d.ts +39 -0
- package/dist/lib/session/discover.js +127 -29
- package/dist/lib/session/ghostty-tabs.d.ts +33 -0
- package/dist/lib/session/ghostty-tabs.js +126 -0
- package/dist/lib/session/inject.d.ts +18 -0
- package/dist/lib/session/inject.js +21 -0
- package/dist/lib/session/parse.js +23 -20
- package/dist/lib/session/pid-registry.d.ts +1 -0
- package/dist/lib/session/pid-registry.js +24 -0
- package/dist/lib/session/provenance.d.ts +14 -2
- package/dist/lib/session/provenance.js +39 -8
- package/dist/lib/session/relative-time.js +6 -2
- package/dist/lib/session/remote-active.js +19 -17
- package/dist/lib/session/remote-list.d.ts +51 -0
- package/dist/lib/session/remote-list.js +205 -0
- package/dist/lib/session/remote.d.ts +7 -1
- package/dist/lib/session/remote.js +20 -4
- package/dist/lib/session/sync/config.d.ts +14 -15
- package/dist/lib/session/sync/config.js +60 -20
- package/dist/lib/session/types.d.ts +23 -0
- package/dist/lib/shims.d.ts +36 -6
- package/dist/lib/shims.js +91 -29
- package/dist/lib/ssh-exec.js +2 -0
- package/dist/lib/ssh-tunnel.js +2 -1
- package/dist/lib/startup/command-registry.d.ts +6 -0
- package/dist/lib/startup/command-registry.js +13 -1
- package/dist/lib/state.d.ts +13 -0
- package/dist/lib/state.js +103 -9
- package/dist/lib/sync-umbrella.d.ts +14 -7
- package/dist/lib/sync-umbrella.js +21 -13
- package/dist/lib/teams/forEach.d.ts +110 -0
- package/dist/lib/teams/forEach.js +186 -0
- package/dist/lib/teams/index.d.ts +1 -0
- package/dist/lib/teams/index.js +1 -0
- package/dist/lib/teams/pr-watch.d.ts +226 -0
- package/dist/lib/teams/pr-watch.js +371 -0
- package/dist/lib/teams/supervisor.d.ts +14 -1
- package/dist/lib/teams/supervisor.js +19 -0
- package/dist/lib/teams/worktree.d.ts +9 -0
- package/dist/lib/teams/worktree.js +32 -0
- package/dist/lib/terminal/backends/index.d.ts +2 -1
- package/dist/lib/terminal/backends/index.js +3 -1
- package/dist/lib/terminal/backends/vscodium-agent.d.ts +31 -0
- package/dist/lib/terminal/backends/vscodium-agent.js +72 -0
- package/dist/lib/terminal/index.d.ts +4 -1
- package/dist/lib/terminal/index.js +4 -1
- package/dist/lib/terminal/inject.d.ts +204 -0
- package/dist/lib/terminal/inject.js +247 -0
- package/dist/lib/terminal/resolve.d.ts +64 -0
- package/dist/lib/terminal/resolve.js +90 -0
- package/dist/lib/terminal/types.d.ts +1 -1
- package/dist/lib/tmux/session.d.ts +10 -0
- package/dist/lib/tmux/session.js +31 -0
- package/dist/lib/triggers/webhook.d.ts +85 -0
- package/dist/lib/triggers/webhook.js +141 -0
- package/dist/lib/versions.d.ts +23 -0
- package/dist/lib/versions.js +119 -13
- package/dist/lib/watchdog/index.d.ts +3 -0
- package/dist/lib/watchdog/index.js +5 -0
- package/dist/lib/watchdog/read.d.ts +35 -0
- package/dist/lib/watchdog/read.js +149 -0
- package/dist/lib/watchdog/runner.d.ts +127 -0
- package/dist/lib/watchdog/runner.js +322 -0
- package/dist/lib/watchdog/watchdog.d.ts +40 -0
- package/dist/lib/watchdog/watchdog.js +166 -0
- package/dist/lib/watchdog/watchdogTail.d.ts +5 -0
- package/dist/lib/watchdog/watchdogTail.js +154 -0
- package/dist/lib/workflows.d.ts +166 -0
- package/dist/lib/workflows.js +193 -0
- package/package.json +5 -4
package/dist/lib/events.js
CHANGED
|
@@ -16,9 +16,25 @@ import * as path from 'path';
|
|
|
16
16
|
import * as os from 'os';
|
|
17
17
|
import { createHash } from 'node:crypto';
|
|
18
18
|
import { parseSshConnection } from './session/provenance.js';
|
|
19
|
+
import { getLogsDir } from './state.js';
|
|
19
20
|
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
20
|
-
// Logs live under the cache bucket — they're regenerable telemetry.
|
|
21
|
-
|
|
21
|
+
// Logs live under the cache bucket — they're regenerable telemetry. Route
|
|
22
|
+
// through state's canonical home anchor (HOME override → os.homedir()) rather
|
|
23
|
+
// than a bare os.homedir(): on Windows os.homedir() reads USERPROFILE and
|
|
24
|
+
// ignores a HOME override, so a test (or any caller) that redirects HOME would
|
|
25
|
+
// have its events silently written to the real profile instead. state.getLogsDir()
|
|
26
|
+
// honors the HOME override on every platform while falling back to os.homedir()
|
|
27
|
+
// (== USERPROFILE on Windows) in production where HOME is unset.
|
|
28
|
+
//
|
|
29
|
+
// Resolved lazily + memoized: importing this module must NOT call getLogsDir()
|
|
30
|
+
// at eval time. events.ts is pulled in transitively (skills/versions/exec/
|
|
31
|
+
// runner), and several tests mock './state.js' with partial factories that omit
|
|
32
|
+
// getLogsDir — an eager call would crash those on import. Deferring to first use
|
|
33
|
+
// keeps a bare import side-effect-free while staying a one-time resolution.
|
|
34
|
+
let _logsDir;
|
|
35
|
+
function logsDir() {
|
|
36
|
+
return (_logsDir ??= getLogsDir());
|
|
37
|
+
}
|
|
22
38
|
/** Default retention period in days. */
|
|
23
39
|
const DEFAULT_RETENTION_DAYS = 7;
|
|
24
40
|
/** Default max length for truncated strings. */
|
|
@@ -54,16 +70,16 @@ function getLogFilePath(date = new Date()) {
|
|
|
54
70
|
const yyyy = date.getFullYear();
|
|
55
71
|
const mm = String(date.getMonth() + 1).padStart(2, '0');
|
|
56
72
|
const dd = String(date.getDate()).padStart(2, '0');
|
|
57
|
-
return path.join(
|
|
73
|
+
return path.join(logsDir(), `events-${yyyy}-${mm}-${dd}.jsonl`);
|
|
58
74
|
}
|
|
59
75
|
function ensureLogsDir() {
|
|
60
|
-
if (!fs.existsSync(
|
|
61
|
-
fs.mkdirSync(
|
|
76
|
+
if (!fs.existsSync(logsDir())) {
|
|
77
|
+
fs.mkdirSync(logsDir(), { recursive: true, mode: DIR_MODE });
|
|
62
78
|
}
|
|
63
79
|
else {
|
|
64
80
|
// Ensure permissions are correct on existing dir
|
|
65
81
|
try {
|
|
66
|
-
fs.chmodSync(
|
|
82
|
+
fs.chmodSync(logsDir(), DIR_MODE);
|
|
67
83
|
}
|
|
68
84
|
catch {
|
|
69
85
|
// May fail if not owner
|
|
@@ -395,10 +411,10 @@ export function emitError(err, payload = {}) {
|
|
|
395
411
|
*/
|
|
396
412
|
export function rotate(retentionDays = DEFAULT_RETENTION_DAYS) {
|
|
397
413
|
try {
|
|
398
|
-
if (!fs.existsSync(
|
|
414
|
+
if (!fs.existsSync(logsDir()))
|
|
399
415
|
return 0;
|
|
400
416
|
const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1000;
|
|
401
|
-
const files = fs.readdirSync(
|
|
417
|
+
const files = fs.readdirSync(logsDir()).filter(f => f.startsWith('events-') && f.endsWith('.jsonl'));
|
|
402
418
|
let removed = 0;
|
|
403
419
|
for (const file of files) {
|
|
404
420
|
const match = file.match(/^events-(\d{4})-(\d{2})-(\d{2})\.jsonl$/);
|
|
@@ -407,7 +423,7 @@ export function rotate(retentionDays = DEFAULT_RETENTION_DAYS) {
|
|
|
407
423
|
const [, yyyy, mm, dd] = match;
|
|
408
424
|
const fileDate = new Date(parseInt(yyyy), parseInt(mm) - 1, parseInt(dd));
|
|
409
425
|
if (fileDate.getTime() < cutoff) {
|
|
410
|
-
fs.unlinkSync(path.join(
|
|
426
|
+
fs.unlinkSync(path.join(logsDir(), file));
|
|
411
427
|
removed++;
|
|
412
428
|
}
|
|
413
429
|
}
|
|
@@ -439,9 +455,9 @@ export function maybeRotate() {
|
|
|
439
455
|
export function query(options) {
|
|
440
456
|
const { startDate, endDate = new Date(), eventTypes, agent, command, module, limit } = options;
|
|
441
457
|
const results = [];
|
|
442
|
-
if (!fs.existsSync(
|
|
458
|
+
if (!fs.existsSync(logsDir()))
|
|
443
459
|
return results;
|
|
444
|
-
const files = fs.readdirSync(
|
|
460
|
+
const files = fs.readdirSync(logsDir())
|
|
445
461
|
.filter(f => f.startsWith('events-') && f.endsWith('.jsonl'))
|
|
446
462
|
.sort()
|
|
447
463
|
.reverse();
|
|
@@ -467,7 +483,7 @@ export function query(options) {
|
|
|
467
483
|
continue;
|
|
468
484
|
if (endDay && fileDate > endDay)
|
|
469
485
|
continue;
|
|
470
|
-
const content = fs.readFileSync(path.join(
|
|
486
|
+
const content = fs.readFileSync(path.join(logsDir(), file), 'utf-8');
|
|
471
487
|
const lines = content.trim().split('\n').filter(Boolean);
|
|
472
488
|
for (const line of lines.reverse()) {
|
|
473
489
|
try {
|
|
@@ -528,4 +544,6 @@ export function getTimingStats(label, options = {}) {
|
|
|
528
544
|
};
|
|
529
545
|
}
|
|
530
546
|
// ─── Exports ──────────────────────────────────────────────────────────────────
|
|
531
|
-
export
|
|
547
|
+
export function getLogsPath() {
|
|
548
|
+
return logsDir();
|
|
549
|
+
}
|
package/dist/lib/exec.d.ts
CHANGED
|
@@ -230,6 +230,12 @@ export interface FallbackEntry {
|
|
|
230
230
|
agent: AgentId;
|
|
231
231
|
/** Optional pinned version (e.g. '0.116.0'). When set, takes precedence over the active default. */
|
|
232
232
|
version?: string;
|
|
233
|
+
/**
|
|
234
|
+
* Env vars merged over options.env for THIS attempt only. Used by profiles
|
|
235
|
+
* with `fallback_model` to swap the model env key (e.g. ANTHROPIC_MODEL) on
|
|
236
|
+
* a same-agent retry without touching auth or base URL.
|
|
237
|
+
*/
|
|
238
|
+
envOverride?: Record<string, string>;
|
|
233
239
|
}
|
|
234
240
|
/** ExecOptions extended with a fallback chain for rate-limit cascading. */
|
|
235
241
|
export interface FallbackOptions extends ExecOptions {
|
|
@@ -237,6 +243,17 @@ export interface FallbackOptions extends ExecOptions {
|
|
|
237
243
|
fallback: FallbackEntry[];
|
|
238
244
|
/** Fallback requires a prompt -- chain handoff doesn't apply to interactive sessions. */
|
|
239
245
|
prompt: string;
|
|
246
|
+
/**
|
|
247
|
+
* Optional out-param the caller reads AFTER the call to learn which chain
|
|
248
|
+
* entry actually executed — updated to each entry as it is attempted, so on
|
|
249
|
+
* return it holds the agent+version whose exit code is returned. Lets the
|
|
250
|
+
* audit log record the fallback that really ran, not always the primary
|
|
251
|
+
* (issue #347).
|
|
252
|
+
*/
|
|
253
|
+
dispatchSink?: {
|
|
254
|
+
agent?: AgentId;
|
|
255
|
+
version?: string;
|
|
256
|
+
};
|
|
240
257
|
}
|
|
241
258
|
/**
|
|
242
259
|
* Build the prompt handed to the fallback agent when the primary was stopped
|
package/dist/lib/exec.js
CHANGED
|
@@ -16,7 +16,9 @@ import { resolveModel, buildReasoningFlags } from './models.js';
|
|
|
16
16
|
import { maybeRotate, createTimer, redactPrompt, redactArgs } from './events.js';
|
|
17
17
|
import { sanitizeProcessEnv } from './secrets/bundles.js';
|
|
18
18
|
import { getShimsDir } from './state.js';
|
|
19
|
-
import { writePidSessionEntry } from './session/pid-registry.js';
|
|
19
|
+
import { writePidSessionEntry, extractSessionIdArg } from './session/pid-registry.js';
|
|
20
|
+
import { mailboxDir, isValidMailboxId } from './mailbox.js';
|
|
21
|
+
import { composeWin32CommandLine } from './platform/index.js';
|
|
20
22
|
/**
|
|
21
23
|
* Map a raw mode string (CLI flag, YAML field, env var) to the canonical Mode.
|
|
22
24
|
*
|
|
@@ -182,6 +184,14 @@ export function buildExecEnv(options) {
|
|
|
182
184
|
: (resolvedVersion && isVersionInstalled('claude', resolvedVersion) ? resolvedVersion : null);
|
|
183
185
|
if (version) {
|
|
184
186
|
result.CLAUDE_CONFIG_DIR = path.join(getVersionHomePath('claude', version), '.claude');
|
|
187
|
+
// A managed pin lives in a per-version dir; Claude Code's own background
|
|
188
|
+
// auto-updater would rewrite that pinned binary in place (and has left it
|
|
189
|
+
// half-swapped and broken). Disable it so a pin stays a pin. Honor an
|
|
190
|
+
// explicit user value — from process.env (already in result) or from
|
|
191
|
+
// options.env (spread over result below).
|
|
192
|
+
if (result.DISABLE_AUTOUPDATER === undefined) {
|
|
193
|
+
result.DISABLE_AUTOUPDATER = '1';
|
|
194
|
+
}
|
|
185
195
|
}
|
|
186
196
|
delete result.CODEX_HOME;
|
|
187
197
|
delete result.COPILOT_HOME;
|
|
@@ -237,6 +247,14 @@ export function buildExecEnv(options) {
|
|
|
237
247
|
delete result.COPILOT_HOME;
|
|
238
248
|
delete result.KIMI_CODE_HOME;
|
|
239
249
|
}
|
|
250
|
+
// Point the agent at its own mailbox so the PreToolUse `mailbox-inject` hook
|
|
251
|
+
// knows which box to drain and inject mid-run. Keyed by the session id — the
|
|
252
|
+
// same id the writer resolves via mailboxIdForActiveSession(). A loop run
|
|
253
|
+
// overrides this to its run-level box via options.env (spread below), so all
|
|
254
|
+
// iterations share one inbox.
|
|
255
|
+
if (options.sessionId && isValidMailboxId(options.sessionId)) {
|
|
256
|
+
result.AGENTS_MAILBOX_DIR = mailboxDir(options.sessionId);
|
|
257
|
+
}
|
|
240
258
|
return {
|
|
241
259
|
...result,
|
|
242
260
|
...options.env,
|
|
@@ -472,11 +490,12 @@ export function buildExecCommand(options) {
|
|
|
472
490
|
// on Linux installs where the shims dir isn't on PATH, spawning the bare
|
|
473
491
|
// versioned name fails with ENOENT even though `agents view` shows the agent.
|
|
474
492
|
//
|
|
475
|
-
// On Windows the
|
|
476
|
-
//
|
|
477
|
-
//
|
|
478
|
-
//
|
|
479
|
-
//
|
|
493
|
+
// On Windows the alias is materialized as a `.cmd` only (see
|
|
494
|
+
// createVersionedAlias — a bash alias next to it would shadow the `.cmd` in
|
|
495
|
+
// cmd.exe/PowerShell name resolution); the extensionless existsSync branch
|
|
496
|
+
// below still matches a legacy install's bash alias. When no shim exists on
|
|
497
|
+
// disk we fall back to the bare versioned name, which spawnAgent() resolves
|
|
498
|
+
// via PATH (+ PATHEXT/shell on Windows).
|
|
480
499
|
if (options.version && cmd.length > 0) {
|
|
481
500
|
const versionedName = `${cmd[0]}@${options.version}`;
|
|
482
501
|
const absPath = path.join(getShimsDir(), versionedName);
|
|
@@ -672,7 +691,14 @@ export function resolveShimSpawn(platform, binary, extraArgs) {
|
|
|
672
691
|
// Use win32 path semantics regardless of the host running this (the platform
|
|
673
692
|
// is the parameter, not process.platform) so `C:\...` reads as absolute.
|
|
674
693
|
const useShell = !path.win32.isAbsolute(binary) || binary.endsWith('.cmd');
|
|
675
|
-
|
|
694
|
+
if (useShell) {
|
|
695
|
+
// DEP0190-safe: hand cmd.exe ONE fully-quoted command line with an EMPTY
|
|
696
|
+
// args array, so Node never concatenates `extraArgs` (which carry the
|
|
697
|
+
// user's raw prompt/flags) into the shell line unescaped — that concat is
|
|
698
|
+
// both the deprecation and a command-injection surface.
|
|
699
|
+
return { command: composeWin32CommandLine(binary, extraArgs), args: [], shell: true };
|
|
700
|
+
}
|
|
701
|
+
return { command: binary, args: extraArgs, shell: false };
|
|
676
702
|
}
|
|
677
703
|
return { command: binary, args: extraArgs, shell: false };
|
|
678
704
|
}
|
|
@@ -705,6 +731,21 @@ export async function execShimPassthrough(agent, rawArgs, cwd, pinnedVersion) {
|
|
|
705
731
|
const { command, args, shell } = resolveShimSpawn(process.platform, binary, [...launchArgs, ...rawArgs]);
|
|
706
732
|
return new Promise((resolve) => {
|
|
707
733
|
const child = spawn(command, args, { cwd, stdio: 'inherit', env, shell });
|
|
734
|
+
// Record the launch so `ag sessions --active` can attribute the agent
|
|
735
|
+
// process to its cwd (and exact session when the caller passed
|
|
736
|
+
// --session-id). Vital on Windows, where there is no lsof to recover a
|
|
737
|
+
// foreign process's cwd. On the shell path this pid is the cmd.exe
|
|
738
|
+
// wrapper, not the agent binary — the active scan resolves that by
|
|
739
|
+
// walking the candidate's ancestors (readAncestorSessionEntry).
|
|
740
|
+
if (child.pid) {
|
|
741
|
+
writePidSessionEntry({
|
|
742
|
+
pid: child.pid,
|
|
743
|
+
agent,
|
|
744
|
+
sessionId: extractSessionIdArg(rawArgs),
|
|
745
|
+
cwd,
|
|
746
|
+
startedAtMs: Date.now(),
|
|
747
|
+
});
|
|
748
|
+
}
|
|
708
749
|
child.on('exit', (code, signal) => resolve(code ?? (signal ? 1 : 0)));
|
|
709
750
|
child.on('error', (err) => {
|
|
710
751
|
process.stderr.write(`agents: failed to launch ${agent}: ${err.message}\n`);
|
|
@@ -772,9 +813,15 @@ async function spawnAgent(options) {
|
|
|
772
813
|
? ['inherit', 'inherit', 'inherit']
|
|
773
814
|
: ['inherit', tapStdout ? 'pipe' : 'inherit', 'pipe'];
|
|
774
815
|
// On Windows, .cmd batch wrappers (npm-installed CLIs) require shell:true
|
|
775
|
-
// whether addressed by name or absolute path.
|
|
816
|
+
// whether addressed by name or absolute path. On that shell path, compose a
|
|
817
|
+
// single fully-quoted command line and pass an EMPTY args array (see
|
|
818
|
+
// composeWin32CommandLine) so Node never concatenates the args array — which
|
|
819
|
+
// carries the user's prompt — into the cmd.exe line unescaped (DEP0190 +
|
|
820
|
+
// command injection).
|
|
776
821
|
const useShell = process.platform === 'win32' && (!path.isAbsolute(executable) || executable.endsWith('.cmd'));
|
|
777
|
-
const
|
|
822
|
+
const spawnCommand = useShell ? composeWin32CommandLine(executable, args) : executable;
|
|
823
|
+
const spawnArgs = useShell ? [] : args;
|
|
824
|
+
const child = spawn(spawnCommand, spawnArgs, {
|
|
778
825
|
cwd: options.cwd || process.cwd(),
|
|
779
826
|
stdio,
|
|
780
827
|
env: buildExecEnv(options),
|
|
@@ -1016,9 +1063,20 @@ export async function runWithFallback(options) {
|
|
|
1016
1063
|
}
|
|
1017
1064
|
}
|
|
1018
1065
|
for (let i = 0; i < chain.length; i++) {
|
|
1019
|
-
const { agent, version } = chain[i];
|
|
1066
|
+
const { agent, version, envOverride } = chain[i];
|
|
1067
|
+
// Record the entry we're about to attempt so the caller (audit log) sees the
|
|
1068
|
+
// agent+version that actually ran, even after a rate-limit handoff.
|
|
1069
|
+
if (options.dispatchSink) {
|
|
1070
|
+
options.dispatchSink.agent = agent;
|
|
1071
|
+
options.dispatchSink.version = version;
|
|
1072
|
+
}
|
|
1020
1073
|
const pinnedSessionId = agent === 'claude' ? randomUUID() : undefined;
|
|
1021
|
-
|
|
1074
|
+
// Same-host retry (same agent+version as previous entry — used by profile
|
|
1075
|
+
// `fallback_model` swaps) keeps the original prompt: the model changed,
|
|
1076
|
+
// not the CLI, so a `/continue` handoff prompt would be misleading.
|
|
1077
|
+
const prev = i > 0 ? chain[i - 1] : undefined;
|
|
1078
|
+
const sameHostRetry = !!prev && prev.agent === agent && prev.version === version;
|
|
1079
|
+
const prompt = prevAgent && !sameHostRetry
|
|
1022
1080
|
? buildFallbackPrompt(prevAgent, prevSessionId, agent, options.prompt)
|
|
1023
1081
|
: options.prompt;
|
|
1024
1082
|
const execOpts = {
|
|
@@ -1026,12 +1084,18 @@ export async function runWithFallback(options) {
|
|
|
1026
1084
|
agent,
|
|
1027
1085
|
version,
|
|
1028
1086
|
prompt,
|
|
1087
|
+
env: envOverride ? { ...(options.env ?? {}), ...envOverride } : options.env,
|
|
1029
1088
|
sessionId: pinnedSessionId ?? (i === 0 ? options.sessionId : undefined),
|
|
1030
1089
|
};
|
|
1031
1090
|
const label = version ? `${agent}@${version}` : agent;
|
|
1091
|
+
const modelSwapNote = sameHostRetry && envOverride
|
|
1092
|
+
? ` (retry with ${Object.entries(envOverride).map(([k, v]) => `${k}=${v}`).join(', ')})`
|
|
1093
|
+
: '';
|
|
1032
1094
|
const banner = i === 0
|
|
1033
1095
|
? `[agents] running ${label}`
|
|
1034
|
-
:
|
|
1096
|
+
: sameHostRetry
|
|
1097
|
+
? `[agents] retry → ${label}${modelSwapNote}`
|
|
1098
|
+
: `[agents] fallback → ${label}`;
|
|
1035
1099
|
process.stderr.write(`${banner}${pinnedSessionId ? ` (session ${pinnedSessionId.slice(0, 8)})` : ''}\n`);
|
|
1036
1100
|
let result;
|
|
1037
1101
|
try {
|
|
@@ -1054,7 +1118,9 @@ export async function runWithFallback(options) {
|
|
|
1054
1118
|
}
|
|
1055
1119
|
const next = chain[i + 1];
|
|
1056
1120
|
const nextLabel = next.version ? `${next.agent}@${next.version}` : next.agent;
|
|
1057
|
-
|
|
1121
|
+
const nextSameHost = next.agent === agent && next.version === version;
|
|
1122
|
+
const handoffVerb = nextSameHost ? 'Retrying on same host' : 'Handing off';
|
|
1123
|
+
process.stderr.write(`[agents] ${label} hit rate limit. ${handoffVerb} to ${nextLabel}...\n`);
|
|
1058
1124
|
prevAgent = agent;
|
|
1059
1125
|
prevSessionId = pinnedSessionId;
|
|
1060
1126
|
}
|
package/dist/lib/git.d.ts
CHANGED
|
@@ -112,6 +112,11 @@ export declare function isSystemRepoOrigin(dir: string): Promise<boolean>;
|
|
|
112
112
|
* Check if repo has uncommitted changes (including untracked files).
|
|
113
113
|
*/
|
|
114
114
|
export declare function hasLocalChanges(dir: string): Promise<boolean>;
|
|
115
|
+
/**
|
|
116
|
+
* Render an absolute path in ~-relative form with forward slashes, matching the
|
|
117
|
+
* way the rest of the CLI prints home-anchored paths (e.g. `~/.agents/.system`).
|
|
118
|
+
*/
|
|
119
|
+
export declare function displayHomePath(dir: string): string;
|
|
115
120
|
/**
|
|
116
121
|
* Pull changes in an existing repo.
|
|
117
122
|
* Refuses to pull if the working tree is dirty -- user must commit or discard changes first.
|
|
@@ -121,6 +126,28 @@ export declare function pullRepo(dir: string): Promise<{
|
|
|
121
126
|
commit: string;
|
|
122
127
|
error?: string;
|
|
123
128
|
}>;
|
|
129
|
+
/**
|
|
130
|
+
* Rebase a repo onto its remote, optionally pushing local commits back up.
|
|
131
|
+
*
|
|
132
|
+
* The one-repo counterpart to `pullRepo` used by `agents sync <repo>`:
|
|
133
|
+
* 1. Refuse if the working tree is dirty (commit or discard first).
|
|
134
|
+
* 2. `git fetch origin` then `git pull --rebase origin <branch>` — rebase, not
|
|
135
|
+
* merge, so a local commit lands cleanly on top of upstream with no merge
|
|
136
|
+
* bubble.
|
|
137
|
+
* 3. When `push` is set, `git push origin <branch>` to send local commits up.
|
|
138
|
+
*
|
|
139
|
+
* The branch is read from the repo's current HEAD (falls back to `main`) rather
|
|
140
|
+
* than hardcoded. System repos pass `push: false` — they are pull-only mirrors
|
|
141
|
+
* of the npm-shipped upstream.
|
|
142
|
+
*/
|
|
143
|
+
export declare function syncRepoGit(dir: string, opts: {
|
|
144
|
+
push: boolean;
|
|
145
|
+
}): Promise<{
|
|
146
|
+
success: boolean;
|
|
147
|
+
commit: string;
|
|
148
|
+
pushed: boolean;
|
|
149
|
+
error?: string;
|
|
150
|
+
}>;
|
|
124
151
|
/**
|
|
125
152
|
* Get git status for sync display.
|
|
126
153
|
* Returns files categorized by their status relative to HEAD.
|
package/dist/lib/git.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import simpleGit from 'simple-git';
|
|
9
9
|
import * as fs from 'fs';
|
|
10
|
+
import * as os from 'os';
|
|
10
11
|
import * as path from 'path';
|
|
11
12
|
import { IS_WINDOWS, isWindowsAbsolutePath } from './platform/index.js';
|
|
12
13
|
import { getPackageLocalPath } from './state.js';
|
|
@@ -494,6 +495,15 @@ export async function hasLocalChanges(dir) {
|
|
|
494
495
|
return false;
|
|
495
496
|
}
|
|
496
497
|
}
|
|
498
|
+
/**
|
|
499
|
+
* Render an absolute path in ~-relative form with forward slashes, matching the
|
|
500
|
+
* way the rest of the CLI prints home-anchored paths (e.g. `~/.agents/.system`).
|
|
501
|
+
*/
|
|
502
|
+
export function displayHomePath(dir) {
|
|
503
|
+
const home = os.homedir();
|
|
504
|
+
const rel = dir.startsWith(home) ? '~' + dir.slice(home.length) : dir;
|
|
505
|
+
return rel.replace(/\\/g, '/');
|
|
506
|
+
}
|
|
497
507
|
/**
|
|
498
508
|
* Pull changes in an existing repo.
|
|
499
509
|
* Refuses to pull if the working tree is dirty -- user must commit or discard changes first.
|
|
@@ -506,7 +516,7 @@ export async function pullRepo(dir) {
|
|
|
506
516
|
return {
|
|
507
517
|
success: false,
|
|
508
518
|
commit: '',
|
|
509
|
-
error:
|
|
519
|
+
error: `Working tree has uncommitted changes. Commit or discard them before pulling.\n\n cd ${displayHomePath(dir)} && git status`,
|
|
510
520
|
};
|
|
511
521
|
}
|
|
512
522
|
await git.fetch();
|
|
@@ -522,6 +532,51 @@ export async function pullRepo(dir) {
|
|
|
522
532
|
return { success: false, commit: '', error: err.message };
|
|
523
533
|
}
|
|
524
534
|
}
|
|
535
|
+
/**
|
|
536
|
+
* Rebase a repo onto its remote, optionally pushing local commits back up.
|
|
537
|
+
*
|
|
538
|
+
* The one-repo counterpart to `pullRepo` used by `agents sync <repo>`:
|
|
539
|
+
* 1. Refuse if the working tree is dirty (commit or discard first).
|
|
540
|
+
* 2. `git fetch origin` then `git pull --rebase origin <branch>` — rebase, not
|
|
541
|
+
* merge, so a local commit lands cleanly on top of upstream with no merge
|
|
542
|
+
* bubble.
|
|
543
|
+
* 3. When `push` is set, `git push origin <branch>` to send local commits up.
|
|
544
|
+
*
|
|
545
|
+
* The branch is read from the repo's current HEAD (falls back to `main`) rather
|
|
546
|
+
* than hardcoded. System repos pass `push: false` — they are pull-only mirrors
|
|
547
|
+
* of the npm-shipped upstream.
|
|
548
|
+
*/
|
|
549
|
+
export async function syncRepoGit(dir, opts) {
|
|
550
|
+
try {
|
|
551
|
+
if (!isGitRepo(dir)) {
|
|
552
|
+
return { success: false, commit: '', pushed: false, error: `Not a git repo: ${dir}` };
|
|
553
|
+
}
|
|
554
|
+
const git = simpleGit(dir);
|
|
555
|
+
const status = await git.status();
|
|
556
|
+
if (!status.isClean()) {
|
|
557
|
+
return {
|
|
558
|
+
success: false,
|
|
559
|
+
commit: '',
|
|
560
|
+
pushed: false,
|
|
561
|
+
error: `Working tree has uncommitted changes. Commit or discard them first.\n\n cd ${dir} && git status`,
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
const branch = status.current || 'main';
|
|
565
|
+
await git.fetch('origin');
|
|
566
|
+
await git.pull('origin', branch, { '--rebase': 'true' });
|
|
567
|
+
installGithooksSymlinks(dir);
|
|
568
|
+
let pushed = false;
|
|
569
|
+
if (opts.push) {
|
|
570
|
+
await git.push('origin', branch);
|
|
571
|
+
pushed = true;
|
|
572
|
+
}
|
|
573
|
+
const log = await git.log({ maxCount: 1 });
|
|
574
|
+
return { success: true, commit: log.latest?.hash.slice(0, 8) || 'unknown', pushed };
|
|
575
|
+
}
|
|
576
|
+
catch (err) {
|
|
577
|
+
return { success: false, commit: '', pushed: false, error: err.message };
|
|
578
|
+
}
|
|
579
|
+
}
|
|
525
580
|
/** Compute the sync status of a git repo, optionally scoped to a subdirectory. */
|
|
526
581
|
export async function getGitSyncStatus(dir, subdir) {
|
|
527
582
|
if (!isGitRepo(dir)) {
|
|
@@ -7,6 +7,12 @@ import type { HookCache, HookCacheConfig } from '../types.js';
|
|
|
7
7
|
export declare function parseCacheConfig(raw: HookCache | undefined): HookCacheConfig | null;
|
|
8
8
|
/** Parse "30s" | "5m" | "1h" | plain seconds. Returns seconds, or null on failure. */
|
|
9
9
|
export declare function parseDuration(d: number | string | undefined): number | null;
|
|
10
|
+
/**
|
|
11
|
+
* Reject hook names that could escape the shims directory when interpolated
|
|
12
|
+
* into a filename. Mirrors the containment gate on hook script resolution in
|
|
13
|
+
* hooks.ts (`resolveContainedHookPath`).
|
|
14
|
+
*/
|
|
15
|
+
export declare function isValidHookShimName(name: string): boolean;
|
|
10
16
|
/** Absolute path of the generated shim for a hook name. */
|
|
11
17
|
export declare function getHookShimPath(name: string): string;
|
|
12
18
|
/**
|
package/dist/lib/hooks/cache.js
CHANGED
|
@@ -70,9 +70,35 @@ export function parseDuration(d) {
|
|
|
70
70
|
return value * 60;
|
|
71
71
|
return value;
|
|
72
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Reject hook names that could escape the shims directory when interpolated
|
|
75
|
+
* into a filename. Mirrors the containment gate on hook script resolution in
|
|
76
|
+
* hooks.ts (`resolveContainedHookPath`).
|
|
77
|
+
*/
|
|
78
|
+
export function isValidHookShimName(name) {
|
|
79
|
+
return (!!name &&
|
|
80
|
+
name !== '.' &&
|
|
81
|
+
name !== '..' &&
|
|
82
|
+
!name.startsWith('-') &&
|
|
83
|
+
!/[\/\\\x00]/.test(name) &&
|
|
84
|
+
name.length <= 255);
|
|
85
|
+
}
|
|
86
|
+
/** Resolve shimsDir + `${name}.sh` and assert the result stays inside shimsDir. */
|
|
87
|
+
function resolveContainedHookShimPath(shimsDir, name) {
|
|
88
|
+
if (!isValidHookShimName(name)) {
|
|
89
|
+
throw new Error(`Invalid hook shim name: ${name}`);
|
|
90
|
+
}
|
|
91
|
+
const resolvedRoot = path.resolve(shimsDir);
|
|
92
|
+
const candidate = path.join(shimsDir, `${name}.sh`);
|
|
93
|
+
const resolved = path.resolve(candidate);
|
|
94
|
+
if (!resolved.startsWith(resolvedRoot + path.sep)) {
|
|
95
|
+
throw new Error(`Invalid hook shim name: ${name}`);
|
|
96
|
+
}
|
|
97
|
+
return resolved;
|
|
98
|
+
}
|
|
73
99
|
/** Absolute path of the generated shim for a hook name. */
|
|
74
100
|
export function getHookShimPath(name) {
|
|
75
|
-
return
|
|
101
|
+
return resolveContainedHookShimPath(getHookShimsDir(), name);
|
|
76
102
|
}
|
|
77
103
|
/**
|
|
78
104
|
* Generate (or refresh) the shim script for a hook. Idempotent — only writes
|
|
@@ -82,7 +108,7 @@ export function generateHookShim(args) {
|
|
|
82
108
|
const shimsDir = args.paths?.shimsDir ?? getHookShimsDir();
|
|
83
109
|
const cacheDir = args.paths?.cacheDir ?? getHookCacheDir();
|
|
84
110
|
const logsDir = args.paths?.logsDir ?? getLogsDir();
|
|
85
|
-
const shimPath =
|
|
111
|
+
const shimPath = resolveContainedHookShimPath(shimsDir, args.name);
|
|
86
112
|
const content = renderShim(args.name, args.scriptPath, args.cache, { cacheDir, logsDir });
|
|
87
113
|
fs.mkdirSync(shimsDir, { recursive: true });
|
|
88
114
|
let existing = null;
|
|
@@ -105,9 +131,10 @@ export function generateHookShim(args) {
|
|
|
105
131
|
return shimPath;
|
|
106
132
|
}
|
|
107
133
|
/**
|
|
108
|
-
* Render the bash shim. Bash 3.2-compatible (macOS default). Uses
|
|
109
|
-
* monotonic-ish nanosecond timing
|
|
110
|
-
*
|
|
134
|
+
* Render the bash shim. Bash 3.2-compatible (macOS default). Uses Python for
|
|
135
|
+
* hashing + monotonic-ish nanosecond timing + portable mtime, resolved at
|
|
136
|
+
* runtime (python3, then python) so a Windows Microsoft Store `python3` alias
|
|
137
|
+
* stub — which exits non-zero without running — doesn't silently break caching.
|
|
111
138
|
*/
|
|
112
139
|
function renderShim(name, scriptPath, cache, paths) {
|
|
113
140
|
const ttl = typeof cache.ttl === 'number' ? cache.ttl : (parseDuration(cache.ttl) ?? 0);
|
|
@@ -133,12 +160,25 @@ KEY_MODE=${q(key)}
|
|
|
133
160
|
|
|
134
161
|
mkdir -p "$CACHE_DIR" "$LOGS_DIR"
|
|
135
162
|
|
|
163
|
+
# Resolve a real Python. On Windows, bare python3 is often a Microsoft Store
|
|
164
|
+
# app-execution alias stub that prints to stderr and exits non-zero (0 bytes on
|
|
165
|
+
# stdout) -- command -v finds it but it cannot run, which silently empties the
|
|
166
|
+
# hash + mtime primitives below and makes EVERY call a cache miss (the hook
|
|
167
|
+
# re-runs every time). Probe by executing, not by lookup, and fall back to python.
|
|
168
|
+
PY=""
|
|
169
|
+
for _cand in python3 python; do
|
|
170
|
+
if command -v "$_cand" >/dev/null 2>&1 && "$_cand" -c 'import sys' >/dev/null 2>&1; then
|
|
171
|
+
PY="$_cand"; break
|
|
172
|
+
fi
|
|
173
|
+
done
|
|
174
|
+
[ -z "$PY" ] && PY=python3
|
|
175
|
+
|
|
136
176
|
# Read stdin once (Claude/Codex/Gemini pass JSON on stdin to every hook).
|
|
137
177
|
STDIN_PAYLOAD="$(cat || true)"
|
|
138
178
|
|
|
139
179
|
# Portable sha1 — \`shasum\` is Perl, missing on minimal Linux images;
|
|
140
180
|
# \`sha1sum\` is coreutils, missing on macOS. Truncate to 12 hex chars.
|
|
141
|
-
sha1_12() {
|
|
181
|
+
sha1_12() { "$PY" -c 'import hashlib,sys; print(hashlib.sha1(sys.stdin.read().encode()).hexdigest()[:12])'; }
|
|
142
182
|
|
|
143
183
|
# Derive cache key suffix from KEY_MODE. All untrusted inputs (cwd, session_id,
|
|
144
184
|
# project path) are hashed before going into the filename so a malicious stdin
|
|
@@ -146,14 +186,14 @@ sha1_12() { python3 -c 'import hashlib,sys; print(hashlib.sha1(sys.stdin.read().
|
|
|
146
186
|
cache_suffix=""
|
|
147
187
|
case "$KEY_MODE" in
|
|
148
188
|
per-cwd)
|
|
149
|
-
cwd_val="$(printf '%s' "$STDIN_PAYLOAD" |
|
|
189
|
+
cwd_val="$(printf '%s' "$STDIN_PAYLOAD" | "$PY" -c 'import json,sys
|
|
150
190
|
try: print(json.load(sys.stdin).get("cwd","") or "")
|
|
151
191
|
except Exception: pass' 2>/dev/null || true)"
|
|
152
192
|
[ -z "$cwd_val" ] && cwd_val="$PWD"
|
|
153
193
|
cache_suffix=".$(printf '%s' "$cwd_val" | sha1_12)"
|
|
154
194
|
;;
|
|
155
195
|
per-session)
|
|
156
|
-
sid_val="$(printf '%s' "$STDIN_PAYLOAD" |
|
|
196
|
+
sid_val="$(printf '%s' "$STDIN_PAYLOAD" | "$PY" -c 'import json,sys
|
|
157
197
|
try: print(json.load(sys.stdin).get("session_id","") or "")
|
|
158
198
|
except Exception: pass' 2>/dev/null || true)"
|
|
159
199
|
# Hash + fall back to a sentinel so missing-session doesn't silently
|
|
@@ -173,7 +213,7 @@ esac
|
|
|
173
213
|
CACHE_FILE="$CACHE_DIR/$HOOK_NAME$cache_suffix.out"
|
|
174
214
|
|
|
175
215
|
# Monotonic-ish nanosecond timer (macOS \`date\` has no %N).
|
|
176
|
-
now_ns() {
|
|
216
|
+
now_ns() { "$PY" -c 'import time; print(int(time.time()*1e9))'; }
|
|
177
217
|
START_NS=$(now_ns)
|
|
178
218
|
|
|
179
219
|
CACHE_STATUS=miss
|
|
@@ -181,10 +221,10 @@ CACHE_AGE=-1
|
|
|
181
221
|
EXIT=0
|
|
182
222
|
|
|
183
223
|
if [ -f "$CACHE_FILE" ]; then
|
|
184
|
-
#
|
|
224
|
+
# $PY is already resolved (used for now_ns) and gives portable mtime
|
|
185
225
|
# without the macOS-vs-Linux \`stat\` flag divergence (-f %m vs -c %Y) that
|
|
186
226
|
# blew up under \`set -u\` when the wrong flag produced literal "%m".
|
|
187
|
-
mtime=$(
|
|
227
|
+
mtime=$("$PY" -c 'import os,sys; print(int(os.path.getmtime(sys.argv[1])))' "$CACHE_FILE" 2>/dev/null)
|
|
188
228
|
mtime=\${mtime:-0}
|
|
189
229
|
now_s=$(date +%s)
|
|
190
230
|
CACHE_AGE=$((now_s - mtime))
|
|
@@ -231,8 +271,10 @@ exit "$EXIT"
|
|
|
231
271
|
* hook is renamed/deleted or has its `cache:` field removed.
|
|
232
272
|
*/
|
|
233
273
|
export function removeHookShim(name, shimsDir) {
|
|
274
|
+
if (!isValidHookShimName(name))
|
|
275
|
+
return;
|
|
234
276
|
const dir = shimsDir ?? getHookShimsDir();
|
|
235
|
-
const shimPath =
|
|
277
|
+
const shimPath = resolveContainedHookShimPath(dir, name);
|
|
236
278
|
if (fs.existsSync(shimPath)) {
|
|
237
279
|
try {
|
|
238
280
|
fs.unlinkSync(shimPath);
|
package/dist/lib/hooks.d.ts
CHANGED
|
@@ -8,6 +8,19 @@
|
|
|
8
8
|
* and syncing them across version switches.
|
|
9
9
|
*/
|
|
10
10
|
export declare function resolveHookScriptPath(script: string): string | null;
|
|
11
|
+
/**
|
|
12
|
+
* Convert an absolute path under HOME to a portable ~/... form with forward
|
|
13
|
+
* slashes. Hook commands stored this way work on both macOS and Windows:
|
|
14
|
+
* absolute Windows paths break in bash because backslashes are stripped as
|
|
15
|
+
* escape characters, whereas ~/... paths expand correctly via the ~/.claude
|
|
16
|
+
* symlink/junction on both platforms.
|
|
17
|
+
*
|
|
18
|
+
* `home` and `sep` are injectable so the Windows behavior (backslash sep,
|
|
19
|
+
* drive-letter home) is unit-testable on a POSIX CI host — pass sep='\\' to
|
|
20
|
+
* simulate Windows. With the defaults this is byte-identical to reading
|
|
21
|
+
* os.homedir()/path.sep at the call site.
|
|
22
|
+
*/
|
|
23
|
+
export declare function toPortableCommand(absPath: string, home?: string, sep?: string): string;
|
|
11
24
|
import type { AgentId, InstalledHook, ManifestHook } from './types.js';
|
|
12
25
|
export type HookEntry = {
|
|
13
26
|
name: string;
|
|
@@ -165,3 +178,17 @@ export declare function registerHooksToSettings(agentId: AgentId, versionHome: s
|
|
|
165
178
|
registered: string[];
|
|
166
179
|
errors: string[];
|
|
167
180
|
};
|
|
181
|
+
/**
|
|
182
|
+
* Prune every Claude-family (`settings.json`) hook entry whose command lives
|
|
183
|
+
* under a removed version's home
|
|
184
|
+
* (`~/.agents/.history/versions/<agent>/<removedVersion>/home/…`).
|
|
185
|
+
*
|
|
186
|
+
* `agents remove <agent>@<version>` soft-deletes the version's files but leaves
|
|
187
|
+
* the hook entries other version homes registered against it — dead hooks that
|
|
188
|
+
* error on every tool call ("No such file or directory") until the next sync.
|
|
189
|
+
* This clears them from a remaining version's settings immediately. Only the
|
|
190
|
+
* removed version's entries are touched; the current version's entries, system
|
|
191
|
+
* hooks, and the user's own custom hooks are left intact. Returns the number of
|
|
192
|
+
* entries removed.
|
|
193
|
+
*/
|
|
194
|
+
export declare function pruneVersionHomeHookEntriesFromSettings(settingsPath: string, agent: AgentId, removedVersion: string): number;
|