@phnx-labs/agents-cli 1.20.34 → 1.20.36
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 +113 -0
- package/LICENSE +185 -21
- package/README.md +13 -5
- 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-actions.d.ts +15 -0
- package/dist/commands/computer-actions.js +120 -22
- package/dist/commands/computer.js +81 -0
- package/dist/commands/cost.js +17 -12
- package/dist/commands/daemon.js +4 -1
- package/dist/commands/doctor.d.ts +1 -0
- package/dist/commands/doctor.js +54 -97
- package/dist/commands/events.d.ts +16 -0
- package/dist/commands/events.js +173 -0
- package/dist/commands/exec.d.ts +19 -0
- package/dist/commands/exec.js +283 -12
- package/dist/commands/hosts.js +37 -29
- package/dist/commands/inspect.d.ts +2 -0
- package/dist/commands/inspect.js +62 -16
- package/dist/commands/lock.d.ts +12 -0
- package/dist/commands/lock.js +70 -0
- package/dist/commands/logs.d.ts +17 -0
- package/dist/commands/logs.js +139 -0
- package/dist/commands/message.d.ts +15 -0
- package/dist/commands/message.js +56 -0
- package/dist/commands/models.d.ts +3 -0
- package/dist/commands/models.js +27 -8
- package/dist/commands/permissions.js +9 -2
- package/dist/commands/repo.d.ts +34 -0
- package/dist/commands/repo.js +243 -65
- package/dist/commands/resource-view.d.ts +20 -0
- package/dist/commands/resource-view.js +90 -28
- 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 +196 -91
- 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.d.ts +2 -0
- package/dist/commands/sessions-resume.js +221 -0
- package/dist/commands/sessions-tail.d.ts +10 -0
- package/dist/commands/sessions-tail.js +11 -0
- package/dist/commands/sessions.d.ts +143 -1
- package/dist/commands/sessions.js +628 -80
- 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/view.d.ts +3 -0
- package/dist/commands/view.js +19 -8
- package/dist/commands/wallet.d.ts +6 -0
- package/dist/commands/wallet.js +22 -5
- package/dist/commands/watchdog.d.ts +18 -0
- package/dist/commands/watchdog.js +238 -0
- package/dist/index.js +82 -32
- package/dist/lib/agents.js +30 -9
- 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/cdp.js +5 -1
- package/dist/lib/browser/chrome.d.ts +10 -0
- package/dist/lib/browser/chrome.js +18 -7
- package/dist/lib/browser/drivers/ssh.d.ts +8 -0
- package/dist/lib/browser/drivers/ssh.js +77 -16
- package/dist/lib/browser/har.d.ts +84 -0
- package/dist/lib/browser/har.js +77 -0
- package/dist/lib/browser/ipc.d.ts +3 -0
- package/dist/lib/browser/ipc.js +37 -12
- 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 +183 -23
- 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 +92 -0
- package/dist/lib/daemon.js +222 -18
- package/dist/lib/devices/connect.d.ts +3 -2
- package/dist/lib/devices/connect.js +5 -3
- package/dist/lib/devices/registry.d.ts +7 -0
- package/dist/lib/devices/registry.js +24 -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 +10 -3
- package/dist/lib/events.js +101 -24
- package/dist/lib/exec.d.ts +17 -0
- package/dist/lib/exec.js +100 -12
- 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 +135 -10
- package/dist/lib/hosts/dispatch.d.ts +15 -0
- package/dist/lib/hosts/dispatch.js +45 -13
- package/dist/lib/hosts/logs.d.ts +16 -0
- package/dist/lib/hosts/logs.js +74 -0
- package/dist/lib/hosts/option.js +1 -1
- package/dist/lib/hosts/passthrough.js +3 -1
- package/dist/lib/hosts/progress.d.ts +66 -0
- package/dist/lib/hosts/progress.js +125 -17
- package/dist/lib/hosts/ready.d.ts +51 -7
- package/dist/lib/hosts/ready.js +98 -25
- package/dist/lib/hosts/reconcile.d.ts +53 -0
- package/dist/lib/hosts/reconcile.js +81 -0
- 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 +22 -0
- package/dist/lib/hosts/tasks.js +29 -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/permissions.d.ts +4 -0
- package/dist/lib/permissions.js +35 -0
- package/dist/lib/picker.d.ts +26 -0
- package/dist/lib/picker.js +127 -0
- 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/plugin-marketplace.d.ts +30 -0
- package/dist/lib/plugin-marketplace.js +215 -2
- package/dist/lib/plugins.d.ts +5 -0
- package/dist/lib/plugins.js +61 -10
- 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 +61 -0
- package/dist/lib/session/active.js +196 -17
- package/dist/lib/session/discover.d.ts +37 -0
- package/dist/lib/session/discover.js +204 -27
- package/dist/lib/session/inject.d.ts +18 -0
- package/dist/lib/session/inject.js +21 -0
- package/dist/lib/session/parse.d.ts +6 -0
- package/dist/lib/session/parse.js +286 -2
- package/dist/lib/session/pid-registry.d.ts +22 -0
- package/dist/lib/session/pid-registry.js +104 -0
- package/dist/lib/session/provenance.d.ts +14 -2
- package/dist/lib/session/provenance.js +39 -8
- package/dist/lib/session/remote-active.d.ts +26 -0
- package/dist/lib/session/remote-active.js +153 -0
- package/dist/lib/session/remote-list.d.ts +51 -0
- package/dist/lib/session/remote-list.js +213 -0
- package/dist/lib/session/remote.d.ts +7 -1
- package/dist/lib/session/remote.js +18 -8
- package/dist/lib/session/render.js +1 -1
- package/dist/lib/session/sync/config.d.ts +1 -7
- package/dist/lib/session/sync/config.js +4 -11
- package/dist/lib/session/types.d.ts +18 -1
- package/dist/lib/session/types.js +1 -1
- package/dist/lib/shims.d.ts +44 -6
- package/dist/lib/shims.js +143 -57
- package/dist/lib/ssh-exec.d.ts +26 -3
- package/dist/lib/ssh-exec.js +47 -3
- package/dist/lib/ssh-tunnel.d.ts +24 -5
- package/dist/lib/ssh-tunnel.js +62 -63
- package/dist/lib/staleness/writers/hooks.js +1 -1
- package/dist/lib/startup/command-registry.d.ts +8 -0
- package/dist/lib/startup/command-registry.js +17 -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 +17 -9
- 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/registry.js +25 -9
- 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/ghostty.d.ts +6 -0
- package/dist/lib/terminal/backends/ghostty.js +69 -0
- package/dist/lib/terminal/backends/index.d.ts +19 -0
- package/dist/lib/terminal/backends/index.js +31 -0
- package/dist/lib/terminal/backends/iterm.d.ts +6 -0
- package/dist/lib/terminal/backends/iterm.js +62 -0
- package/dist/lib/terminal/backends/tmux.d.ts +14 -0
- package/dist/lib/terminal/backends/tmux.js +23 -0
- 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/engine.d.ts +39 -0
- package/dist/lib/terminal/engine.js +54 -0
- package/dist/lib/terminal/index.d.ts +17 -0
- package/dist/lib/terminal/index.js +8 -0
- package/dist/lib/terminal/inject.d.ts +204 -0
- package/dist/lib/terminal/inject.js +247 -0
- package/dist/lib/terminal/policy.d.ts +11 -0
- package/dist/lib/terminal/policy.js +11 -0
- package/dist/lib/terminal/quote.d.ts +11 -0
- package/dist/lib/terminal/quote.js +13 -0
- package/dist/lib/terminal/resolve.d.ts +64 -0
- package/dist/lib/terminal/resolve.js +90 -0
- package/dist/lib/terminal/shell.d.ts +6 -0
- package/dist/lib/terminal/shell.js +23 -0
- package/dist/lib/terminal/transport.d.ts +15 -0
- package/dist/lib/terminal/transport.js +40 -0
- package/dist/lib/terminal/types.d.ts +59 -0
- package/dist/lib/terminal/types.js +13 -0
- package/dist/lib/triggers/webhook.d.ts +85 -0
- package/dist/lib/triggers/webhook.js +141 -0
- package/dist/lib/types.d.ts +7 -0
- package/dist/lib/versions.d.ts +30 -3
- package/dist/lib/versions.js +129 -18
- 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/whats-new.d.ts +9 -0
- package/dist/lib/whats-new.js +35 -0
- package/dist/lib/workflows.d.ts +166 -0
- package/dist/lib/workflows.js +193 -0
- package/package.json +5 -4
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { ALL_AGENT_IDS } from './agents.js';
|
|
2
|
+
import { getGlobalDefault, listInstalledVersions } from './versions.js';
|
|
3
|
+
import { loadManifest, isStale } from './staleness/index.js';
|
|
4
|
+
import { diffVersionResources } from './doctor-diff.js';
|
|
5
|
+
import { diffVersionCommands, iterCommandsCapableVersions } from './commands.js';
|
|
6
|
+
import { diffVersionSkills, iterSkillsCapableVersions } from './skills.js';
|
|
7
|
+
import { iterHooksCapableVersions, listUnmanagedHooksInVersionHome } from './hooks.js';
|
|
8
|
+
// Lines naming exactly what's out of sync for a version, plugins prioritized:
|
|
9
|
+
// each divergent plugin gets its own line with specifics (stale mirror version,
|
|
10
|
+
// invalid manifest, or the bundled skills/commands missing from the mirror —
|
|
11
|
+
// the system-repo plugin content that matters most). Other kinds collapse to
|
|
12
|
+
// compact counts so the readout stays scannable.
|
|
13
|
+
export function divergenceLines(report) {
|
|
14
|
+
const lines = [];
|
|
15
|
+
for (const p of report.kinds.plugins) {
|
|
16
|
+
if (p.status === 'missing')
|
|
17
|
+
lines.push(`plugin ${p.name} — not installed`);
|
|
18
|
+
else if (p.status === 'diff')
|
|
19
|
+
lines.push(`plugin ${p.name} — ${p.detail ?? 'mirror drifted'}`);
|
|
20
|
+
}
|
|
21
|
+
for (const kind of ['commands', 'skills', 'hooks', 'rules', 'mcp', 'permissions', 'subagents']) {
|
|
22
|
+
const rows = report.kinds[kind];
|
|
23
|
+
const miss = rows.filter((r) => r.status === 'missing').length;
|
|
24
|
+
const dif = rows.filter((r) => r.status === 'diff').length;
|
|
25
|
+
const bits = [];
|
|
26
|
+
if (miss)
|
|
27
|
+
bits.push(`${miss} missing`);
|
|
28
|
+
if (dif)
|
|
29
|
+
bits.push(`${dif} drifted`);
|
|
30
|
+
if (bits.length)
|
|
31
|
+
lines.push(`${kind.padEnd(11)} ${bits.join(' · ')}`);
|
|
32
|
+
}
|
|
33
|
+
return lines;
|
|
34
|
+
}
|
|
35
|
+
export function checkSyncStatus(cwd) {
|
|
36
|
+
const rows = [];
|
|
37
|
+
// Every installed version, not just the default — a stale NON-default version
|
|
38
|
+
// (e.g. one you launched from yesterday) is exactly the rot that silently
|
|
39
|
+
// serves outdated/invalid resources and that `--fix` now heals. Hiding it here
|
|
40
|
+
// is why that class of bug went unnoticed.
|
|
41
|
+
for (const agent of ALL_AGENT_IDS) {
|
|
42
|
+
const def = getGlobalDefault(agent);
|
|
43
|
+
for (const version of listInstalledVersions(agent)) {
|
|
44
|
+
const manifest = loadManifest(agent, version);
|
|
45
|
+
const status = !manifest
|
|
46
|
+
? 'never-synced'
|
|
47
|
+
: isStale(manifest, agent, version, cwd) ? 'stale' : 'fresh';
|
|
48
|
+
const row = { agent, version, status, isDefault: version === def };
|
|
49
|
+
if (status === 'stale') {
|
|
50
|
+
// Resolve the specifics against non-project layers (the global home is
|
|
51
|
+
// never reconciled against per-cwd project resources).
|
|
52
|
+
const report = diffVersionResources(agent, version, { cwd, excludeProject: true });
|
|
53
|
+
const lines = divergenceLines(report);
|
|
54
|
+
if (lines.length)
|
|
55
|
+
row.divergence = lines;
|
|
56
|
+
}
|
|
57
|
+
rows.push(row);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return rows;
|
|
61
|
+
}
|
|
62
|
+
export function countOrphans() {
|
|
63
|
+
const byKey = new Map();
|
|
64
|
+
const ensure = (agent, version) => {
|
|
65
|
+
const key = `${agent}@${version}`;
|
|
66
|
+
let row = byKey.get(key);
|
|
67
|
+
if (!row) {
|
|
68
|
+
row = { agent, version, commands: 0, skills: 0, hooks: 0 };
|
|
69
|
+
byKey.set(key, row);
|
|
70
|
+
}
|
|
71
|
+
return row;
|
|
72
|
+
};
|
|
73
|
+
for (const { agent, version } of iterCommandsCapableVersions()) {
|
|
74
|
+
const diff = diffVersionCommands(agent, version);
|
|
75
|
+
if (diff.orphans.length > 0)
|
|
76
|
+
ensure(agent, version).commands = diff.orphans.length;
|
|
77
|
+
}
|
|
78
|
+
for (const { agent, version } of iterSkillsCapableVersions()) {
|
|
79
|
+
const diff = diffVersionSkills(agent, version);
|
|
80
|
+
if (diff.orphans.length > 0)
|
|
81
|
+
ensure(agent, version).skills = diff.orphans.length;
|
|
82
|
+
}
|
|
83
|
+
// Orphan hooks are scripts in the version home that no agents.yaml/hooks.yaml
|
|
84
|
+
// entry registers — so the registrar never wires them to an event and they
|
|
85
|
+
// never fire. (Distinct from the source-diff `diffVersionHooks().orphans`,
|
|
86
|
+
// which false-flags valid system-sourced registered hooks.)
|
|
87
|
+
for (const { agent, version } of iterHooksCapableVersions()) {
|
|
88
|
+
const dead = listUnmanagedHooksInVersionHome(agent, version);
|
|
89
|
+
if (dead.length > 0)
|
|
90
|
+
ensure(agent, version).hooks = dead.length;
|
|
91
|
+
}
|
|
92
|
+
return Array.from(byKey.values()).filter((r) => r.commands + r.skills + r.hooks > 0);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Compute the same drift/divergence diagnostic `agents doctor` prints, reduced
|
|
96
|
+
* to a summary with a single `hasDrift` boolean. The gate `agents check` maps
|
|
97
|
+
* to an exit code; the readout `agents doctor` renders in full.
|
|
98
|
+
*/
|
|
99
|
+
export function computeDrift(cwd) {
|
|
100
|
+
const syncRows = checkSyncStatus(cwd);
|
|
101
|
+
const orphanRows = countOrphans();
|
|
102
|
+
const staleCount = syncRows.filter((r) => r.status === 'stale').length;
|
|
103
|
+
const neverSyncedCount = syncRows.filter((r) => r.status === 'never-synced').length;
|
|
104
|
+
return {
|
|
105
|
+
syncRows,
|
|
106
|
+
orphanRows,
|
|
107
|
+
staleCount,
|
|
108
|
+
neverSyncedCount,
|
|
109
|
+
orphanVersionCount: orphanRows.length,
|
|
110
|
+
hasDrift: syncRows.some((r) => r.status !== 'fresh'),
|
|
111
|
+
};
|
|
112
|
+
}
|
package/dist/lib/events.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Centralized event logging for agents-cli.
|
|
3
3
|
*
|
|
4
|
-
* Structured JSONL logs at ~/.agents/logs/events-YYYY-MM-DD.jsonl
|
|
4
|
+
* Structured JSONL logs at ~/.agents/.cache/logs/events-YYYY-MM-DD.jsonl
|
|
5
5
|
* with automatic daily rotation and rich metadata for debugging/auditing.
|
|
6
6
|
*
|
|
7
7
|
* Features:
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* - Permissions: logs dir is 0700, files are 0600 (owner-only)
|
|
12
12
|
* - Performance tracking: withTiming() wrapper for any async function
|
|
13
13
|
*/
|
|
14
|
-
export type EventType = 'agent.run.start' | 'agent.run.end' | 'agent.spawn.start' | 'agent.spawn.end' | 'version.install' | 'version.switch' | 'version.remove' | 'skill.install' | 'skill.remove' | 'browser.launch' | 'browser.close' | 'browser.navigate' | 'browser.screenshot' | 'secrets.get' | 'secrets.set' | 'secrets.delete' | 'secrets.rename' | 'cloud.dispatch' | 'cloud.complete' | 'teams.create' | 'teams.add' | 'teams.start' | 'teams.complete' | 'hook.fire' | 'hook.complete' | 'hook.error' | 'resource.sync' | 'command.start' | 'command.end' | 'perf.timing' | 'session.start' | 'session.end' | 'error' | 'warn' | 'info' | 'debug';
|
|
14
|
+
export type EventType = 'agent.run.start' | 'agent.run.end' | 'agent.spawn.start' | 'agent.spawn.end' | 'version.install' | 'version.switch' | 'version.remove' | 'skill.install' | 'skill.remove' | 'browser.launch' | 'browser.close' | 'browser.navigate' | 'browser.screenshot' | 'secrets.get' | 'secrets.set' | 'secrets.delete' | 'secrets.rename' | 'cloud.dispatch' | 'cloud.complete' | 'teams.create' | 'teams.add' | 'teams.start' | 'teams.complete' | 'teams.disband' | 'hook.fire' | 'hook.complete' | 'hook.error' | 'resource.sync' | 'command.start' | 'command.end' | 'perf.timing' | 'session.start' | 'session.end' | 'error' | 'warn' | 'info' | 'debug';
|
|
15
15
|
export interface EventMeta {
|
|
16
16
|
ts: string;
|
|
17
17
|
tz: string;
|
|
@@ -22,12 +22,18 @@ export interface EventMeta {
|
|
|
22
22
|
pid: number;
|
|
23
23
|
ppid: number;
|
|
24
24
|
event: EventType;
|
|
25
|
+
osUser: string;
|
|
26
|
+
transport: 'local' | 'ssh';
|
|
27
|
+
sshClientIp?: string;
|
|
25
28
|
}
|
|
26
29
|
export interface EventPayload {
|
|
27
30
|
agent?: string;
|
|
28
31
|
version?: string;
|
|
29
32
|
sessionId?: string;
|
|
30
33
|
cwd?: string;
|
|
34
|
+
/** Top-level command group, e.g. 'teams', 'secrets' — the audit filter key. */
|
|
35
|
+
module?: string;
|
|
36
|
+
/** Full command path, e.g. 'teams create', 'secrets get'. */
|
|
31
37
|
command?: string;
|
|
32
38
|
args?: string[];
|
|
33
39
|
input?: string;
|
|
@@ -155,6 +161,7 @@ export declare function query(options: {
|
|
|
155
161
|
eventTypes?: EventType[];
|
|
156
162
|
agent?: string;
|
|
157
163
|
command?: string;
|
|
164
|
+
module?: string;
|
|
158
165
|
limit?: number;
|
|
159
166
|
}): EventRecord[];
|
|
160
167
|
/**
|
|
@@ -170,4 +177,4 @@ export declare function getTimingStats(label: string, options?: {
|
|
|
170
177
|
p50Ms: number;
|
|
171
178
|
p95Ms: number;
|
|
172
179
|
} | null;
|
|
173
|
-
export declare
|
|
180
|
+
export declare function getLogsPath(): string;
|
package/dist/lib/events.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Centralized event logging for agents-cli.
|
|
3
3
|
*
|
|
4
|
-
* Structured JSONL logs at ~/.agents/logs/events-YYYY-MM-DD.jsonl
|
|
4
|
+
* Structured JSONL logs at ~/.agents/.cache/logs/events-YYYY-MM-DD.jsonl
|
|
5
5
|
* with automatic daily rotation and rich metadata for debugging/auditing.
|
|
6
6
|
*
|
|
7
7
|
* Features:
|
|
@@ -15,9 +15,26 @@ import * as fs from 'fs';
|
|
|
15
15
|
import * as path from 'path';
|
|
16
16
|
import * as os from 'os';
|
|
17
17
|
import { createHash } from 'node:crypto';
|
|
18
|
+
import { parseSshConnection } from './session/provenance.js';
|
|
19
|
+
import { getLogsDir } from './state.js';
|
|
18
20
|
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
19
|
-
// Logs live under the cache bucket — they're regenerable telemetry.
|
|
20
|
-
|
|
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
|
+
}
|
|
21
38
|
/** Default retention period in days. */
|
|
22
39
|
const DEFAULT_RETENTION_DAYS = 7;
|
|
23
40
|
/** Default max length for truncated strings. */
|
|
@@ -53,16 +70,16 @@ function getLogFilePath(date = new Date()) {
|
|
|
53
70
|
const yyyy = date.getFullYear();
|
|
54
71
|
const mm = String(date.getMonth() + 1).padStart(2, '0');
|
|
55
72
|
const dd = String(date.getDate()).padStart(2, '0');
|
|
56
|
-
return path.join(
|
|
73
|
+
return path.join(logsDir(), `events-${yyyy}-${mm}-${dd}.jsonl`);
|
|
57
74
|
}
|
|
58
75
|
function ensureLogsDir() {
|
|
59
|
-
if (!fs.existsSync(
|
|
60
|
-
fs.mkdirSync(
|
|
76
|
+
if (!fs.existsSync(logsDir())) {
|
|
77
|
+
fs.mkdirSync(logsDir(), { recursive: true, mode: DIR_MODE });
|
|
61
78
|
}
|
|
62
79
|
else {
|
|
63
80
|
// Ensure permissions are correct on existing dir
|
|
64
81
|
try {
|
|
65
|
-
fs.chmodSync(
|
|
82
|
+
fs.chmodSync(logsDir(), DIR_MODE);
|
|
66
83
|
}
|
|
67
84
|
catch {
|
|
68
85
|
// May fail if not owner
|
|
@@ -130,6 +147,30 @@ function truncatePayload(payload, maxLength = DEFAULT_TRUNCATE_LENGTH) {
|
|
|
130
147
|
}
|
|
131
148
|
return result;
|
|
132
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* Who is running this process and from where. Derived once per process from the
|
|
152
|
+
* OS user and $SSH_CONNECTION (via the same parser the sessions layer uses), then
|
|
153
|
+
* cached — provenance can't change mid-process, so every emit() pays for it once.
|
|
154
|
+
*/
|
|
155
|
+
let _origin;
|
|
156
|
+
function auditOrigin() {
|
|
157
|
+
if (_origin)
|
|
158
|
+
return _origin;
|
|
159
|
+
let osUser = 'unknown';
|
|
160
|
+
try {
|
|
161
|
+
osUser = os.userInfo().username;
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
// Container/edge cases where the uid has no passwd entry.
|
|
165
|
+
}
|
|
166
|
+
const ssh = process.env.SSH_CONNECTION ? parseSshConnection(process.env.SSH_CONNECTION) : undefined;
|
|
167
|
+
_origin = {
|
|
168
|
+
osUser,
|
|
169
|
+
transport: ssh ? 'ssh' : 'local',
|
|
170
|
+
...(ssh ? { sshClientIp: ssh.clientIp } : {}),
|
|
171
|
+
};
|
|
172
|
+
return _origin;
|
|
173
|
+
}
|
|
133
174
|
// ─── Core API ─────────────────────────────────────────────────────────────────
|
|
134
175
|
/**
|
|
135
176
|
* Emit a structured event to the daily log file.
|
|
@@ -152,23 +193,33 @@ export function emit(event, payload = {}) {
|
|
|
152
193
|
pid: process.pid,
|
|
153
194
|
ppid: process.ppid,
|
|
154
195
|
event,
|
|
196
|
+
...auditOrigin(),
|
|
155
197
|
...truncatePayload(payload),
|
|
156
198
|
};
|
|
157
199
|
const line = JSON.stringify(record) + '\n';
|
|
158
200
|
const logPath = getLogFilePath();
|
|
201
|
+
const isNew = !fs.existsSync(logPath);
|
|
159
202
|
fs.appendFileSync(logPath, line, { mode: FILE_MODE });
|
|
160
|
-
//
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
203
|
+
// appendFileSync's mode only applies when it CREATES the file, so we chmod
|
|
204
|
+
// to guarantee 0600 — but only on first write to a given path this process.
|
|
205
|
+
// command.start/end fire on every invocation; chmod-per-append would double
|
|
206
|
+
// the syscalls on the hot path for no gain (perms don't drift mid-run).
|
|
207
|
+
if (isNew || logPath !== _chmoddedPath) {
|
|
208
|
+
_chmoddedPath = logPath;
|
|
209
|
+
try {
|
|
210
|
+
fs.chmodSync(logPath, FILE_MODE);
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
// May fail if not owner
|
|
214
|
+
}
|
|
166
215
|
}
|
|
167
216
|
}
|
|
168
217
|
catch {
|
|
169
218
|
// Silent failure - logging should never break the CLI
|
|
170
219
|
}
|
|
171
220
|
}
|
|
221
|
+
/** Last log path this process chmod'd — avoids a redundant chmod per append. */
|
|
222
|
+
let _chmoddedPath;
|
|
172
223
|
/**
|
|
173
224
|
* Convenience wrapper for timed operations.
|
|
174
225
|
* Returns a function to call when the operation completes.
|
|
@@ -360,10 +411,10 @@ export function emitError(err, payload = {}) {
|
|
|
360
411
|
*/
|
|
361
412
|
export function rotate(retentionDays = DEFAULT_RETENTION_DAYS) {
|
|
362
413
|
try {
|
|
363
|
-
if (!fs.existsSync(
|
|
414
|
+
if (!fs.existsSync(logsDir()))
|
|
364
415
|
return 0;
|
|
365
416
|
const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1000;
|
|
366
|
-
const files = fs.readdirSync(
|
|
417
|
+
const files = fs.readdirSync(logsDir()).filter(f => f.startsWith('events-') && f.endsWith('.jsonl'));
|
|
367
418
|
let removed = 0;
|
|
368
419
|
for (const file of files) {
|
|
369
420
|
const match = file.match(/^events-(\d{4})-(\d{2})-(\d{2})\.jsonl$/);
|
|
@@ -372,7 +423,7 @@ export function rotate(retentionDays = DEFAULT_RETENTION_DAYS) {
|
|
|
372
423
|
const [, yyyy, mm, dd] = match;
|
|
373
424
|
const fileDate = new Date(parseInt(yyyy), parseInt(mm) - 1, parseInt(dd));
|
|
374
425
|
if (fileDate.getTime() < cutoff) {
|
|
375
|
-
fs.unlinkSync(path.join(
|
|
426
|
+
fs.unlinkSync(path.join(logsDir(), file));
|
|
376
427
|
removed++;
|
|
377
428
|
}
|
|
378
429
|
}
|
|
@@ -402,34 +453,58 @@ export function maybeRotate() {
|
|
|
402
453
|
* @returns Array of event records
|
|
403
454
|
*/
|
|
404
455
|
export function query(options) {
|
|
405
|
-
const { startDate, endDate = new Date(), eventTypes, agent, command, limit } = options;
|
|
456
|
+
const { startDate, endDate = new Date(), eventTypes, agent, command, module, limit } = options;
|
|
406
457
|
const results = [];
|
|
407
|
-
if (!fs.existsSync(
|
|
458
|
+
if (!fs.existsSync(logsDir()))
|
|
408
459
|
return results;
|
|
409
|
-
const files = fs.readdirSync(
|
|
460
|
+
const files = fs.readdirSync(logsDir())
|
|
410
461
|
.filter(f => f.startsWith('events-') && f.endsWith('.jsonl'))
|
|
411
462
|
.sort()
|
|
412
463
|
.reverse();
|
|
464
|
+
// Coarse file skip works on whole days, so floor the bounds to midnight —
|
|
465
|
+
// otherwise a sub-day window (`--since 2h` at 15:00 → startDate 13:00) would
|
|
466
|
+
// drop *today's* file, whose date stamps to 00:00. Precise filtering below is
|
|
467
|
+
// per-record on `ts`.
|
|
468
|
+
const startDay = startDate
|
|
469
|
+
? new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate())
|
|
470
|
+
: undefined;
|
|
471
|
+
const endDay = endDate
|
|
472
|
+
? new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate())
|
|
473
|
+
: undefined;
|
|
474
|
+
const startMs = startDate?.getTime();
|
|
475
|
+
const endMs = endDate?.getTime();
|
|
413
476
|
for (const file of files) {
|
|
414
477
|
const match = file.match(/^events-(\d{4})-(\d{2})-(\d{2})\.jsonl$/);
|
|
415
478
|
if (!match)
|
|
416
479
|
continue;
|
|
417
480
|
const [, yyyy, mm, dd] = match;
|
|
418
481
|
const fileDate = new Date(parseInt(yyyy), parseInt(mm) - 1, parseInt(dd));
|
|
419
|
-
if (
|
|
482
|
+
if (startDay && fileDate < startDay)
|
|
420
483
|
continue;
|
|
421
|
-
if (
|
|
484
|
+
if (endDay && fileDate > endDay)
|
|
422
485
|
continue;
|
|
423
|
-
const content = fs.readFileSync(path.join(
|
|
486
|
+
const content = fs.readFileSync(path.join(logsDir(), file), 'utf-8');
|
|
424
487
|
const lines = content.trim().split('\n').filter(Boolean);
|
|
425
488
|
for (const line of lines.reverse()) {
|
|
426
489
|
try {
|
|
427
490
|
const record = JSON.parse(line);
|
|
491
|
+
// Precise per-record window — the file skip above is day-granular only.
|
|
492
|
+
const recMs = Date.parse(record.ts);
|
|
493
|
+
if (startMs !== undefined && !isNaN(recMs) && recMs < startMs)
|
|
494
|
+
continue;
|
|
495
|
+
if (endMs !== undefined && !isNaN(recMs) && recMs > endMs)
|
|
496
|
+
continue;
|
|
428
497
|
if (eventTypes && !eventTypes.includes(record.event))
|
|
429
498
|
continue;
|
|
430
499
|
if (agent && record.agent !== agent)
|
|
431
500
|
continue;
|
|
432
|
-
|
|
501
|
+
// `--command` matches by path prefix on a word boundary, so a coarse
|
|
502
|
+
// "teams" catches "teams create"/"teams remove" while an exact
|
|
503
|
+
// "teams create" stays exact.
|
|
504
|
+
if (command && record.command !== command &&
|
|
505
|
+
!(typeof record.command === 'string' && record.command.startsWith(command + ' ')))
|
|
506
|
+
continue;
|
|
507
|
+
if (module && record.module !== module)
|
|
433
508
|
continue;
|
|
434
509
|
results.push(record);
|
|
435
510
|
if (limit && results.length >= limit) {
|
|
@@ -469,4 +544,6 @@ export function getTimingStats(label, options = {}) {
|
|
|
469
544
|
};
|
|
470
545
|
}
|
|
471
546
|
// ─── Exports ──────────────────────────────────────────────────────────────────
|
|
472
|
-
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,6 +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, extractSessionIdArg } from './session/pid-registry.js';
|
|
20
|
+
import { mailboxDir, isValidMailboxId } from './mailbox.js';
|
|
21
|
+
import { composeWin32CommandLine } from './platform/index.js';
|
|
19
22
|
/**
|
|
20
23
|
* Map a raw mode string (CLI flag, YAML field, env var) to the canonical Mode.
|
|
21
24
|
*
|
|
@@ -181,6 +184,14 @@ export function buildExecEnv(options) {
|
|
|
181
184
|
: (resolvedVersion && isVersionInstalled('claude', resolvedVersion) ? resolvedVersion : null);
|
|
182
185
|
if (version) {
|
|
183
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
|
+
}
|
|
184
195
|
}
|
|
185
196
|
delete result.CODEX_HOME;
|
|
186
197
|
delete result.COPILOT_HOME;
|
|
@@ -236,6 +247,14 @@ export function buildExecEnv(options) {
|
|
|
236
247
|
delete result.COPILOT_HOME;
|
|
237
248
|
delete result.KIMI_CODE_HOME;
|
|
238
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
|
+
}
|
|
239
258
|
return {
|
|
240
259
|
...result,
|
|
241
260
|
...options.env,
|
|
@@ -471,11 +490,12 @@ export function buildExecCommand(options) {
|
|
|
471
490
|
// on Linux installs where the shims dir isn't on PATH, spawning the bare
|
|
472
491
|
// versioned name fails with ENOENT even though `agents view` shows the agent.
|
|
473
492
|
//
|
|
474
|
-
// On Windows the
|
|
475
|
-
//
|
|
476
|
-
//
|
|
477
|
-
//
|
|
478
|
-
//
|
|
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).
|
|
479
499
|
if (options.version && cmd.length > 0) {
|
|
480
500
|
const versionedName = `${cmd[0]}@${options.version}`;
|
|
481
501
|
const absPath = path.join(getShimsDir(), versionedName);
|
|
@@ -671,7 +691,14 @@ export function resolveShimSpawn(platform, binary, extraArgs) {
|
|
|
671
691
|
// Use win32 path semantics regardless of the host running this (the platform
|
|
672
692
|
// is the parameter, not process.platform) so `C:\...` reads as absolute.
|
|
673
693
|
const useShell = !path.win32.isAbsolute(binary) || binary.endsWith('.cmd');
|
|
674
|
-
|
|
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 };
|
|
675
702
|
}
|
|
676
703
|
return { command: binary, args: extraArgs, shell: false };
|
|
677
704
|
}
|
|
@@ -704,6 +731,21 @@ export async function execShimPassthrough(agent, rawArgs, cwd, pinnedVersion) {
|
|
|
704
731
|
const { command, args, shell } = resolveShimSpawn(process.platform, binary, [...launchArgs, ...rawArgs]);
|
|
705
732
|
return new Promise((resolve) => {
|
|
706
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
|
+
}
|
|
707
749
|
child.on('exit', (code, signal) => resolve(code ?? (signal ? 1 : 0)));
|
|
708
750
|
child.on('error', (err) => {
|
|
709
751
|
process.stderr.write(`agents: failed to launch ${agent}: ${err.message}\n`);
|
|
@@ -722,6 +764,15 @@ export async function execShimPassthrough(agent, rawArgs, cwd, pinnedVersion) {
|
|
|
722
764
|
* unbuffered.
|
|
723
765
|
*/
|
|
724
766
|
async function spawnAgent(options) {
|
|
767
|
+
// Assign a known session id up front for agents that accept one, so the
|
|
768
|
+
// launcher can record an EXACT pid -> session mapping (see pid-registry) —
|
|
769
|
+
// otherwise the headless `ag sessions --active` path can only guess
|
|
770
|
+
// "newest .jsonl in the cwd" and collapses co-located agents onto one row.
|
|
771
|
+
// Claude: `--session-id <uuid>` CREATES the session with that id (wired in
|
|
772
|
+
// buildExecCommand). Skip on resume — the id is the one being resumed.
|
|
773
|
+
if (options.agent === 'claude' && !options.resume && !options.sessionId) {
|
|
774
|
+
options = { ...options, sessionId: randomUUID() };
|
|
775
|
+
}
|
|
725
776
|
const cmd = buildExecCommand(options);
|
|
726
777
|
const [executable, ...args] = cmd;
|
|
727
778
|
const timeoutMs = options.timeout ? parseTimeout(options.timeout) : undefined;
|
|
@@ -762,14 +813,32 @@ async function spawnAgent(options) {
|
|
|
762
813
|
? ['inherit', 'inherit', 'inherit']
|
|
763
814
|
: ['inherit', tapStdout ? 'pipe' : 'inherit', 'pipe'];
|
|
764
815
|
// On Windows, .cmd batch wrappers (npm-installed CLIs) require shell:true
|
|
765
|
-
// 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).
|
|
766
821
|
const useShell = process.platform === 'win32' && (!path.isAbsolute(executable) || executable.endsWith('.cmd'));
|
|
767
|
-
const
|
|
822
|
+
const spawnCommand = useShell ? composeWin32CommandLine(executable, args) : executable;
|
|
823
|
+
const spawnArgs = useShell ? [] : args;
|
|
824
|
+
const child = spawn(spawnCommand, spawnArgs, {
|
|
768
825
|
cwd: options.cwd || process.cwd(),
|
|
769
826
|
stdio,
|
|
770
827
|
env: buildExecEnv(options),
|
|
771
828
|
shell: useShell,
|
|
772
829
|
});
|
|
830
|
+
// Record this launch so `ag sessions --active` can map the pid to its exact
|
|
831
|
+
// session (sessionId is set for Claude above) instead of guessing the newest
|
|
832
|
+
// .jsonl in the cwd — the collapse that made co-located agents indistinguishable.
|
|
833
|
+
// Best-effort: pruned when the pid dies; a failed write just degrades to the heuristic.
|
|
834
|
+
writePidSessionEntry({
|
|
835
|
+
pid: child.pid ?? 0,
|
|
836
|
+
agent: options.agent,
|
|
837
|
+
sessionId: options.sessionId,
|
|
838
|
+
cwd: options.cwd || process.cwd(),
|
|
839
|
+
tmuxPane: process.env.TMUX_PANE,
|
|
840
|
+
startedAtMs: Date.now(),
|
|
841
|
+
});
|
|
773
842
|
// Mark startup time (time from function call to process spawn)
|
|
774
843
|
timer.mark('startup');
|
|
775
844
|
let budgetKilled = false;
|
|
@@ -994,9 +1063,20 @@ export async function runWithFallback(options) {
|
|
|
994
1063
|
}
|
|
995
1064
|
}
|
|
996
1065
|
for (let i = 0; i < chain.length; i++) {
|
|
997
|
-
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
|
+
}
|
|
998
1073
|
const pinnedSessionId = agent === 'claude' ? randomUUID() : undefined;
|
|
999
|
-
|
|
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
|
|
1000
1080
|
? buildFallbackPrompt(prevAgent, prevSessionId, agent, options.prompt)
|
|
1001
1081
|
: options.prompt;
|
|
1002
1082
|
const execOpts = {
|
|
@@ -1004,12 +1084,18 @@ export async function runWithFallback(options) {
|
|
|
1004
1084
|
agent,
|
|
1005
1085
|
version,
|
|
1006
1086
|
prompt,
|
|
1087
|
+
env: envOverride ? { ...(options.env ?? {}), ...envOverride } : options.env,
|
|
1007
1088
|
sessionId: pinnedSessionId ?? (i === 0 ? options.sessionId : undefined),
|
|
1008
1089
|
};
|
|
1009
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
|
+
: '';
|
|
1010
1094
|
const banner = i === 0
|
|
1011
1095
|
? `[agents] running ${label}`
|
|
1012
|
-
:
|
|
1096
|
+
: sameHostRetry
|
|
1097
|
+
? `[agents] retry → ${label}${modelSwapNote}`
|
|
1098
|
+
: `[agents] fallback → ${label}`;
|
|
1013
1099
|
process.stderr.write(`${banner}${pinnedSessionId ? ` (session ${pinnedSessionId.slice(0, 8)})` : ''}\n`);
|
|
1014
1100
|
let result;
|
|
1015
1101
|
try {
|
|
@@ -1032,7 +1118,9 @@ export async function runWithFallback(options) {
|
|
|
1032
1118
|
}
|
|
1033
1119
|
const next = chain[i + 1];
|
|
1034
1120
|
const nextLabel = next.version ? `${next.agent}@${next.version}` : next.agent;
|
|
1035
|
-
|
|
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`);
|
|
1036
1124
|
prevAgent = agent;
|
|
1037
1125
|
prevSessionId = pinnedSessionId;
|
|
1038
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.
|