@phnx-labs/agents-cli 1.20.35 → 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.
Files changed (225) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/LICENSE +185 -21
  3. package/README.md +8 -4
  4. package/dist/commands/audit.d.ts +14 -0
  5. package/dist/commands/audit.js +68 -0
  6. package/dist/commands/browser.js +82 -8
  7. package/dist/commands/check.d.ts +15 -0
  8. package/dist/commands/check.js +84 -0
  9. package/dist/commands/cloud.js +143 -3
  10. package/dist/commands/computer.js +81 -0
  11. package/dist/commands/daemon.js +4 -1
  12. package/dist/commands/doctor.js +1 -89
  13. package/dist/commands/events.js +3 -3
  14. package/dist/commands/exec.d.ts +19 -0
  15. package/dist/commands/exec.js +277 -11
  16. package/dist/commands/hosts.js +10 -6
  17. package/dist/commands/inspect.js +8 -9
  18. package/dist/commands/lock.d.ts +12 -0
  19. package/dist/commands/lock.js +70 -0
  20. package/dist/commands/message.d.ts +15 -0
  21. package/dist/commands/message.js +56 -0
  22. package/dist/commands/routines.js +101 -5
  23. package/dist/commands/secrets-migrate.js +106 -57
  24. package/dist/commands/secrets.d.ts +31 -18
  25. package/dist/commands/secrets.js +156 -75
  26. package/dist/commands/serve.d.ts +10 -0
  27. package/dist/commands/serve.js +37 -0
  28. package/dist/commands/sessions-inject.d.ts +14 -0
  29. package/dist/commands/sessions-inject.js +111 -0
  30. package/dist/commands/sessions-picker.d.ts +2 -0
  31. package/dist/commands/sessions-picker.js +24 -3
  32. package/dist/commands/sessions-resume.js +20 -8
  33. package/dist/commands/sessions.d.ts +71 -1
  34. package/dist/commands/sessions.js +365 -37
  35. package/dist/commands/setup.js +4 -2
  36. package/dist/commands/sync.d.ts +3 -1
  37. package/dist/commands/sync.js +156 -4
  38. package/dist/commands/teams.js +217 -0
  39. package/dist/commands/versions.js +2 -4
  40. package/dist/commands/watchdog.d.ts +18 -0
  41. package/dist/commands/watchdog.js +238 -0
  42. package/dist/index.js +25 -2
  43. package/dist/lib/audit/log.d.ts +92 -0
  44. package/dist/lib/audit/log.js +177 -0
  45. package/dist/lib/auto-pull.js +2 -1
  46. package/dist/lib/browser/chrome.d.ts +10 -0
  47. package/dist/lib/browser/chrome.js +18 -7
  48. package/dist/lib/browser/drivers/ssh.js +2 -1
  49. package/dist/lib/browser/har.d.ts +84 -0
  50. package/dist/lib/browser/har.js +77 -0
  51. package/dist/lib/browser/ipc.js +24 -3
  52. package/dist/lib/browser/profiles.d.ts +1 -1
  53. package/dist/lib/browser/profiles.js +8 -10
  54. package/dist/lib/browser/refs.d.ts +65 -0
  55. package/dist/lib/browser/refs.js +73 -1
  56. package/dist/lib/browser/runtime-state.js +1 -0
  57. package/dist/lib/browser/service.d.ts +38 -2
  58. package/dist/lib/browser/service.js +112 -8
  59. package/dist/lib/browser/types.d.ts +14 -1
  60. package/dist/lib/budget/live-cloud.d.ts +42 -0
  61. package/dist/lib/budget/live-cloud.js +79 -0
  62. package/dist/lib/budget/live-team.d.ts +31 -0
  63. package/dist/lib/budget/live-team.js +115 -0
  64. package/dist/lib/cloud/codex.js +4 -0
  65. package/dist/lib/cloud/rush.d.ts +12 -1
  66. package/dist/lib/cloud/rush.js +13 -3
  67. package/dist/lib/cloud/types.d.ts +9 -0
  68. package/dist/lib/computer/dispatch.d.ts +8 -0
  69. package/dist/lib/computer/dispatch.js +125 -0
  70. package/dist/lib/computer/loop.d.ts +62 -0
  71. package/dist/lib/computer/loop.js +98 -0
  72. package/dist/lib/computer/model.d.ts +44 -0
  73. package/dist/lib/computer/model.js +157 -0
  74. package/dist/lib/concurrency.d.ts +19 -0
  75. package/dist/lib/concurrency.js +33 -0
  76. package/dist/lib/daemon.d.ts +57 -0
  77. package/dist/lib/daemon.js +192 -16
  78. package/dist/lib/devices/registry.d.ts +7 -0
  79. package/dist/lib/devices/registry.js +24 -0
  80. package/dist/lib/devices/tailscale.js +1 -1
  81. package/dist/lib/drift.d.ts +52 -0
  82. package/dist/lib/drift.js +112 -0
  83. package/dist/lib/events.d.ts +1 -1
  84. package/dist/lib/events.js +31 -13
  85. package/dist/lib/exec.d.ts +17 -0
  86. package/dist/lib/exec.js +79 -13
  87. package/dist/lib/git.d.ts +27 -0
  88. package/dist/lib/git.js +56 -1
  89. package/dist/lib/hooks/cache.d.ts +6 -0
  90. package/dist/lib/hooks/cache.js +54 -12
  91. package/dist/lib/hooks.d.ts +27 -0
  92. package/dist/lib/hooks.js +127 -8
  93. package/dist/lib/hosts/dispatch.d.ts +15 -0
  94. package/dist/lib/hosts/dispatch.js +39 -6
  95. package/dist/lib/hosts/logs.js +30 -1
  96. package/dist/lib/hosts/option.js +1 -1
  97. package/dist/lib/hosts/passthrough.js +3 -1
  98. package/dist/lib/hosts/ready.d.ts +29 -6
  99. package/dist/lib/hosts/ready.js +66 -15
  100. package/dist/lib/hosts/registry.d.ts +19 -2
  101. package/dist/lib/hosts/registry.js +58 -2
  102. package/dist/lib/hosts/remote-cmd.d.ts +62 -1
  103. package/dist/lib/hosts/remote-cmd.js +70 -1
  104. package/dist/lib/hosts/remote-os.d.ts +17 -0
  105. package/dist/lib/hosts/remote-os.js +30 -0
  106. package/dist/lib/hosts/session-index.d.ts +34 -0
  107. package/dist/lib/hosts/session-index.js +56 -0
  108. package/dist/lib/hosts/tasks.d.ts +14 -0
  109. package/dist/lib/hosts/tasks.js +15 -0
  110. package/dist/lib/lock.d.ts +93 -0
  111. package/dist/lib/lock.js +207 -0
  112. package/dist/lib/loop.js +16 -1
  113. package/dist/lib/machine-id.d.ts +21 -0
  114. package/dist/lib/machine-id.js +26 -0
  115. package/dist/lib/mailbox-target.d.ts +36 -0
  116. package/dist/lib/mailbox-target.js +45 -0
  117. package/dist/lib/mailbox.d.ts +47 -0
  118. package/dist/lib/mailbox.js +194 -0
  119. package/dist/lib/mcp.d.ts +5 -0
  120. package/dist/lib/mcp.js +24 -8
  121. package/dist/lib/migrate.d.ts +19 -0
  122. package/dist/lib/migrate.js +134 -26
  123. package/dist/lib/overdue.js +3 -0
  124. package/dist/lib/picker.d.ts +2 -0
  125. package/dist/lib/picker.js +4 -1
  126. package/dist/lib/platform/exec.d.ts +46 -0
  127. package/dist/lib/platform/exec.js +74 -0
  128. package/dist/lib/platform/process.d.ts +31 -0
  129. package/dist/lib/platform/process.js +34 -1
  130. package/dist/lib/platform/winpath.js +2 -0
  131. package/dist/lib/plugins.js +16 -6
  132. package/dist/lib/profiles.d.ts +25 -0
  133. package/dist/lib/profiles.js +22 -6
  134. package/dist/lib/pty-client.js +2 -1
  135. package/dist/lib/rotate.d.ts +61 -0
  136. package/dist/lib/rotate.js +52 -0
  137. package/dist/lib/routines.d.ts +40 -2
  138. package/dist/lib/routines.js +66 -8
  139. package/dist/lib/runner.d.ts +11 -2
  140. package/dist/lib/runner.js +49 -7
  141. package/dist/lib/scheduler.js +6 -1
  142. package/dist/lib/secrets/bundles.d.ts +60 -4
  143. package/dist/lib/secrets/bundles.js +131 -12
  144. package/dist/lib/secrets/filestore.d.ts +3 -0
  145. package/dist/lib/secrets/filestore.js +42 -16
  146. package/dist/lib/secrets/index.d.ts +43 -2
  147. package/dist/lib/secrets/index.js +102 -3
  148. package/dist/lib/secrets/mcp.d.ts +93 -0
  149. package/dist/lib/secrets/mcp.js +205 -0
  150. package/dist/lib/secrets/remote.js +12 -5
  151. package/dist/lib/secrets/sync.js +83 -4
  152. package/dist/lib/secrets/windows.js +14 -3
  153. package/dist/lib/serve/data.d.ts +81 -0
  154. package/dist/lib/serve/data.js +91 -0
  155. package/dist/lib/serve/page.d.ts +7 -0
  156. package/dist/lib/serve/page.js +140 -0
  157. package/dist/lib/serve/server.d.ts +46 -0
  158. package/dist/lib/serve/server.js +115 -0
  159. package/dist/lib/session/active.d.ts +54 -0
  160. package/dist/lib/session/active.js +190 -19
  161. package/dist/lib/session/discover.d.ts +37 -0
  162. package/dist/lib/session/discover.js +111 -28
  163. package/dist/lib/session/inject.d.ts +18 -0
  164. package/dist/lib/session/inject.js +21 -0
  165. package/dist/lib/session/parse.js +23 -20
  166. package/dist/lib/session/pid-registry.d.ts +1 -0
  167. package/dist/lib/session/pid-registry.js +24 -0
  168. package/dist/lib/session/provenance.d.ts +14 -2
  169. package/dist/lib/session/provenance.js +39 -8
  170. package/dist/lib/session/remote-active.js +19 -7
  171. package/dist/lib/session/remote-list.d.ts +51 -0
  172. package/dist/lib/session/remote-list.js +213 -0
  173. package/dist/lib/session/remote.d.ts +7 -1
  174. package/dist/lib/session/remote.js +16 -2
  175. package/dist/lib/session/sync/config.d.ts +1 -15
  176. package/dist/lib/session/sync/config.js +4 -20
  177. package/dist/lib/session/types.d.ts +17 -0
  178. package/dist/lib/shims.d.ts +36 -6
  179. package/dist/lib/shims.js +91 -29
  180. package/dist/lib/ssh-exec.js +2 -0
  181. package/dist/lib/ssh-tunnel.js +2 -1
  182. package/dist/lib/startup/command-registry.d.ts +6 -0
  183. package/dist/lib/startup/command-registry.js +13 -1
  184. package/dist/lib/state.d.ts +13 -0
  185. package/dist/lib/state.js +103 -9
  186. package/dist/lib/sync-umbrella.d.ts +14 -7
  187. package/dist/lib/sync-umbrella.js +17 -9
  188. package/dist/lib/teams/forEach.d.ts +110 -0
  189. package/dist/lib/teams/forEach.js +186 -0
  190. package/dist/lib/teams/index.d.ts +1 -0
  191. package/dist/lib/teams/index.js +1 -0
  192. package/dist/lib/teams/pr-watch.d.ts +226 -0
  193. package/dist/lib/teams/pr-watch.js +371 -0
  194. package/dist/lib/teams/supervisor.d.ts +14 -1
  195. package/dist/lib/teams/supervisor.js +19 -0
  196. package/dist/lib/teams/worktree.d.ts +9 -0
  197. package/dist/lib/teams/worktree.js +32 -0
  198. package/dist/lib/terminal/backends/index.d.ts +2 -1
  199. package/dist/lib/terminal/backends/index.js +3 -1
  200. package/dist/lib/terminal/backends/vscodium-agent.d.ts +31 -0
  201. package/dist/lib/terminal/backends/vscodium-agent.js +72 -0
  202. package/dist/lib/terminal/index.d.ts +4 -1
  203. package/dist/lib/terminal/index.js +4 -1
  204. package/dist/lib/terminal/inject.d.ts +204 -0
  205. package/dist/lib/terminal/inject.js +247 -0
  206. package/dist/lib/terminal/resolve.d.ts +64 -0
  207. package/dist/lib/terminal/resolve.js +90 -0
  208. package/dist/lib/terminal/types.d.ts +1 -1
  209. package/dist/lib/triggers/webhook.d.ts +85 -0
  210. package/dist/lib/triggers/webhook.js +141 -0
  211. package/dist/lib/versions.d.ts +23 -0
  212. package/dist/lib/versions.js +119 -13
  213. package/dist/lib/watchdog/index.d.ts +3 -0
  214. package/dist/lib/watchdog/index.js +5 -0
  215. package/dist/lib/watchdog/read.d.ts +35 -0
  216. package/dist/lib/watchdog/read.js +149 -0
  217. package/dist/lib/watchdog/runner.d.ts +127 -0
  218. package/dist/lib/watchdog/runner.js +322 -0
  219. package/dist/lib/watchdog/watchdog.d.ts +40 -0
  220. package/dist/lib/watchdog/watchdog.js +166 -0
  221. package/dist/lib/watchdog/watchdogTail.d.ts +5 -0
  222. package/dist/lib/watchdog/watchdogTail.js +154 -0
  223. package/dist/lib/workflows.d.ts +166 -0
  224. package/dist/lib/workflows.js +193 -0
  225. package/package.json +5 -4
@@ -0,0 +1,85 @@
1
+ /**
2
+ * GitHub webhook trigger receiver for routines.
3
+ *
4
+ * A routine may declare a `trigger` block instead of (or alongside) a cron
5
+ * `schedule` (see `JobConfig.trigger` in `../routines.ts`). This module turns
6
+ * an incoming GitHub webhook into the set of routines it should fire, and
7
+ * dispatches those routines through the exact same path a cron fire uses
8
+ * (`executeJobDetached`).
9
+ *
10
+ * The matching logic (`matchJobsToWebhook`) is a pure, side-effect-free
11
+ * function so it can be unit-tested without a running daemon or http server.
12
+ * The optional http listener (`startWebhookServer`) is a thin adapter over it.
13
+ */
14
+ import * as http from 'http';
15
+ import type { JobConfig, RunMeta } from '../routines.js';
16
+ /**
17
+ * A parsed GitHub webhook: the event name (from the `X-GitHub-Event` HTTP
18
+ * header) plus the decoded JSON body. Kept deliberately loose (`payload` is an
19
+ * arbitrary object) because callers may hand us any of GitHub's event shapes.
20
+ */
21
+ export interface GithubWebhook {
22
+ /** The GitHub event name, e.g. `pull_request`, `push`, `issue_comment`. */
23
+ event: string;
24
+ /** The decoded JSON request body. */
25
+ payload: Record<string, unknown>;
26
+ }
27
+ /** Read `repository.full_name` (`owner/name`) from a webhook payload, if present. */
28
+ export declare function webhookRepo(payload: Record<string, unknown>): string | null;
29
+ /**
30
+ * Extract every candidate branch a webhook payload references, per event type.
31
+ * A trigger's `branch` matches if it equals any of these. Different events
32
+ * carry the branch in different places:
33
+ * - push: `ref` (refs/heads/<b>)
34
+ * - pull_request: base + head refs of the PR
35
+ * - workflow_run: `workflow_run.head_branch`
36
+ * - issue_comment: no branch (comments aren't branch-scoped)
37
+ */
38
+ export declare function webhookBranches(event: string, payload: Record<string, unknown>): string[];
39
+ /** True when a single job's trigger matches the given webhook. Pure. */
40
+ export declare function jobMatchesWebhook(job: JobConfig, webhook: GithubWebhook): boolean;
41
+ /**
42
+ * Pure matcher: given a set of jobs and an incoming webhook, return the jobs
43
+ * whose `trigger` matches (event + optional repo + optional branch). Jobs
44
+ * without a trigger (schedule-only routines) are never selected — proving
45
+ * time-based jobs are unaffected by webhook delivery.
46
+ */
47
+ export declare function matchJobsToWebhook(jobs: JobConfig[], webhook: GithubWebhook): JobConfig[];
48
+ /** Options for firing webhook-matched jobs (dispatch is injectable for tests). */
49
+ export interface FireWebhookOptions {
50
+ /** Job source. Defaults to all persisted routines (`listJobs()`). */
51
+ jobs?: JobConfig[];
52
+ /**
53
+ * How to dispatch a matched job. Defaults to `executeJobDetached` — the SAME
54
+ * path a cron fire uses (see `daemon.ts`). Injectable so tests can assert
55
+ * matching without spawning real agent processes.
56
+ */
57
+ dispatch?: (config: JobConfig) => Promise<RunMeta>;
58
+ }
59
+ /** Result of firing one matched job. */
60
+ export interface FiredJob {
61
+ jobName: string;
62
+ runId: string;
63
+ }
64
+ /**
65
+ * Match an incoming webhook against the persisted routines and fire each match
66
+ * through the cron dispatch path. Returns one entry per fired job.
67
+ */
68
+ export declare function fireWebhookJobs(webhook: GithubWebhook, options?: FireWebhookOptions): Promise<FiredJob[]>;
69
+ /** Options for the local webhook http listener. */
70
+ export interface WebhookServerOptions {
71
+ port?: number;
72
+ host?: string;
73
+ /** Override the fire options (mainly for tests). */
74
+ fire?: FireWebhookOptions;
75
+ /** Called after each delivery is handled (mainly for tests/observability). */
76
+ onDelivery?: (event: string, fired: FiredJob[]) => void;
77
+ }
78
+ /**
79
+ * Start a minimal local webhook receiver. POSTs are read as JSON, the GitHub
80
+ * event is taken from the `X-GitHub-Event` header, and matching routines are
81
+ * fired via {@link fireWebhookJobs}. Returns the underlying server so callers
82
+ * can `close()` it. The heavy lifting is the pure matcher above; this is only
83
+ * the transport.
84
+ */
85
+ export declare function startWebhookServer(options?: WebhookServerOptions): http.Server;
@@ -0,0 +1,141 @@
1
+ /**
2
+ * GitHub webhook trigger receiver for routines.
3
+ *
4
+ * A routine may declare a `trigger` block instead of (or alongside) a cron
5
+ * `schedule` (see `JobConfig.trigger` in `../routines.ts`). This module turns
6
+ * an incoming GitHub webhook into the set of routines it should fire, and
7
+ * dispatches those routines through the exact same path a cron fire uses
8
+ * (`executeJobDetached`).
9
+ *
10
+ * The matching logic (`matchJobsToWebhook`) is a pure, side-effect-free
11
+ * function so it can be unit-tested without a running daemon or http server.
12
+ * The optional http listener (`startWebhookServer`) is a thin adapter over it.
13
+ */
14
+ import * as http from 'http';
15
+ import { listJobs } from '../routines.js';
16
+ import { executeJobDetached } from '../runner.js';
17
+ /** Read `repository.full_name` (`owner/name`) from a webhook payload, if present. */
18
+ export function webhookRepo(payload) {
19
+ const repo = payload?.repository;
20
+ const fullName = repo?.full_name;
21
+ return typeof fullName === 'string' && fullName.length > 0 ? fullName : null;
22
+ }
23
+ /** Strip a `refs/heads/` (or `refs/tags/`) prefix to the short branch/tag name. */
24
+ function shortRef(ref) {
25
+ return ref.replace(/^refs\/(heads|tags)\//, '');
26
+ }
27
+ /**
28
+ * Extract every candidate branch a webhook payload references, per event type.
29
+ * A trigger's `branch` matches if it equals any of these. Different events
30
+ * carry the branch in different places:
31
+ * - push: `ref` (refs/heads/<b>)
32
+ * - pull_request: base + head refs of the PR
33
+ * - workflow_run: `workflow_run.head_branch`
34
+ * - issue_comment: no branch (comments aren't branch-scoped)
35
+ */
36
+ export function webhookBranches(event, payload) {
37
+ const branches = new Set();
38
+ const add = (v) => {
39
+ if (typeof v === 'string' && v.length > 0)
40
+ branches.add(shortRef(v));
41
+ };
42
+ switch (event) {
43
+ case 'push':
44
+ add(payload.ref);
45
+ break;
46
+ case 'pull_request': {
47
+ const pr = payload.pull_request;
48
+ add(pr?.base?.ref);
49
+ add(pr?.head?.ref);
50
+ break;
51
+ }
52
+ case 'workflow_run': {
53
+ const run = payload.workflow_run;
54
+ add(run?.head_branch);
55
+ break;
56
+ }
57
+ default:
58
+ break;
59
+ }
60
+ return [...branches];
61
+ }
62
+ /** True when a single job's trigger matches the given webhook. Pure. */
63
+ export function jobMatchesWebhook(job, webhook) {
64
+ const trigger = job.trigger;
65
+ if (!trigger || trigger.type !== 'github_event')
66
+ return false;
67
+ if (trigger.event !== webhook.event)
68
+ return false;
69
+ if (trigger.repo) {
70
+ const repo = webhookRepo(webhook.payload);
71
+ if (!repo || repo.toLowerCase() !== trigger.repo.toLowerCase())
72
+ return false;
73
+ }
74
+ if (trigger.branch) {
75
+ const branches = webhookBranches(webhook.event, webhook.payload);
76
+ if (!branches.some((b) => b === trigger.branch))
77
+ return false;
78
+ }
79
+ return true;
80
+ }
81
+ /**
82
+ * Pure matcher: given a set of jobs and an incoming webhook, return the jobs
83
+ * whose `trigger` matches (event + optional repo + optional branch). Jobs
84
+ * without a trigger (schedule-only routines) are never selected — proving
85
+ * time-based jobs are unaffected by webhook delivery.
86
+ */
87
+ export function matchJobsToWebhook(jobs, webhook) {
88
+ return jobs.filter((job) => job.enabled !== false && jobMatchesWebhook(job, webhook));
89
+ }
90
+ /**
91
+ * Match an incoming webhook against the persisted routines and fire each match
92
+ * through the cron dispatch path. Returns one entry per fired job.
93
+ */
94
+ export async function fireWebhookJobs(webhook, options = {}) {
95
+ const jobs = options.jobs ?? listJobs();
96
+ const dispatch = options.dispatch ?? executeJobDetached;
97
+ const matched = matchJobsToWebhook(jobs, webhook);
98
+ const fired = [];
99
+ for (const job of matched) {
100
+ const meta = await dispatch(job);
101
+ fired.push({ jobName: job.name, runId: meta.runId });
102
+ }
103
+ return fired;
104
+ }
105
+ /**
106
+ * Start a minimal local webhook receiver. POSTs are read as JSON, the GitHub
107
+ * event is taken from the `X-GitHub-Event` header, and matching routines are
108
+ * fired via {@link fireWebhookJobs}. Returns the underlying server so callers
109
+ * can `close()` it. The heavy lifting is the pure matcher above; this is only
110
+ * the transport.
111
+ */
112
+ export function startWebhookServer(options = {}) {
113
+ const server = http.createServer((req, res) => {
114
+ if (req.method !== 'POST') {
115
+ res.writeHead(405, { 'content-type': 'text/plain' });
116
+ res.end('method not allowed');
117
+ return;
118
+ }
119
+ const event = req.headers['x-github-event'] ?? '';
120
+ const chunks = [];
121
+ req.on('data', (c) => chunks.push(c));
122
+ req.on('end', () => {
123
+ void (async () => {
124
+ try {
125
+ const raw = Buffer.concat(chunks).toString('utf-8');
126
+ const payload = raw ? JSON.parse(raw) : {};
127
+ const fired = await fireWebhookJobs({ event, payload }, options.fire);
128
+ options.onDelivery?.(event, fired);
129
+ res.writeHead(200, { 'content-type': 'application/json' });
130
+ res.end(JSON.stringify({ ok: true, fired: fired.map((f) => f.jobName) }));
131
+ }
132
+ catch (err) {
133
+ res.writeHead(400, { 'content-type': 'application/json' });
134
+ res.end(JSON.stringify({ ok: false, error: err.message }));
135
+ }
136
+ })();
137
+ });
138
+ });
139
+ server.listen(options.port ?? 0, options.host ?? '127.0.0.1');
140
+ return server;
141
+ }
@@ -121,6 +121,12 @@ export declare function getBinaryPath(agent: AgentId, version: string): string;
121
121
  export declare function getVersionHomePath(agent: AgentId, version: string): string;
122
122
  /**
123
123
  * Check if a specific version is installed.
124
+ *
125
+ * Probes the actual launch binary (the same executable the shims run), not just
126
+ * the version dir or the node_modules/.bin/<cli> wrapper. For npm agents that
127
+ * means statting the package's real `bin` target (getPackageBinaryPath), so a
128
+ * present-but-gutted install (vendor auto-updater destroyed the binary) reports
129
+ * as NOT installed and `agents add` re-runs its install step to repair it.
124
130
  */
125
131
  export declare function isVersionInstalled(agent: AgentId, version: string): boolean;
126
132
  /**
@@ -346,6 +352,23 @@ export declare function listRepoNames(): string[];
346
352
  * exactly as an unscoped full reconcile does.
347
353
  */
348
354
  export declare function buildRepoScopedSelection(repo: string, cwd?: string): ResourceSelection;
355
+ /**
356
+ * Union several ResourceSelections into one, deduping names per kind. Pure — no
357
+ * FS — so the merge logic is unit-tested without a DotAgents layout. `memory` is
358
+ * a whole-file merge of the user+system layers, not a per-repo artifact: pass
359
+ * `includeMemory` to request a full memory write (`'all'`), else the skip
360
+ * sentinel (`[]`) is kept.
361
+ */
362
+ export declare function unionResourceSelections(selections: ResourceSelection[], includeMemory: boolean): ResourceSelection;
363
+ /**
364
+ * Build one selection spanning several DotAgent repos — the source set an
365
+ * interactive `agents sync` picker collects. Each repo is scoped via
366
+ * `buildRepoScopedSelection`, then unioned. Memory is written whole when the
367
+ * `user` or `system` layer is among the selected repos (that's where the memory
368
+ * file's content comes from); a project-only / alias-only pick leaves it
369
+ * untouched.
370
+ */
371
+ export declare function mergeRepoScopedSelections(repos: string[], cwd?: string): ResourceSelection;
349
372
  /**
350
373
  * Sync central resources (~/.agents/) into a specific version's config directory.
351
374
  * Copies selected resources from central storage into {versionHome}/.{agent}/.
@@ -34,6 +34,7 @@ import { parseMcpServerConfig } from './mcp.js';
34
34
  import { createVersionedAlias, removeVersionedAlias, getConfigSymlinkVersion, ensureClaudeInsideSymlink } from './shims.js';
35
35
  import { importInstallScriptBinary } from './import.js';
36
36
  import { IS_WINDOWS } from './platform/index.js';
37
+ import { pruneVersionHomeHookEntriesFromSettings } from './hooks.js';
37
38
  import { supports, explainSkip } from './capabilities.js';
38
39
  import { discoverPlugins } from './plugins.js';
39
40
  import { loadManifest, saveManifest, buildManifest as buildSyncManifest, isStale } from './staleness/index.js';
@@ -840,12 +841,65 @@ export function getBinaryPath(agent, version) {
840
841
  export function getVersionHomePath(agent, version) {
841
842
  return path.join(getVersionDir(agent, version), 'home');
842
843
  }
844
+ /**
845
+ * Resolve the REAL launch binary for an npm-package agent version: the file the
846
+ * installed package's `bin` entry points to — e.g.
847
+ * node_modules/@anthropic-ai/claude-code/bin/claude.exe on Windows. This is the
848
+ * executable that the node_modules/.bin/<cli>.cmd wrapper ultimately execs (and
849
+ * what `agents run` spawns), NOT the wrapper itself.
850
+ *
851
+ * The distinction is load-bearing: npm leaves the tiny node_modules/.bin/<cli>
852
+ * and <cli>.cmd wrappers in place even after a vendor auto-updater destroys the
853
+ * multi-hundred-MB real binary the wrapper points at. Keying "installed" on the
854
+ * wrapper (getBinaryPath) — or on the version dir — therefore reports a gutted
855
+ * install as healthy, and `agents run` then dies at spawn with
856
+ * "'...claude.exe' is not recognized".
857
+ *
858
+ * Returns null for agents without an npm package (grok/droid/installScript,
859
+ * whose getBinaryPath already resolves the real per-host binary) or when the
860
+ * installed package.json can't be read/parsed — callers fall back to the
861
+ * generic getBinaryPath check in those cases.
862
+ */
863
+ function getPackageBinaryPath(agent, version) {
864
+ const agentConfig = AGENTS[agent];
865
+ if (!agentConfig.npmPackage)
866
+ return null;
867
+ const pkgRoot = path.join(getVersionDir(agent, version), 'node_modules', agentConfig.npmPackage);
868
+ let bin;
869
+ try {
870
+ const pkg = JSON.parse(fs.readFileSync(path.join(pkgRoot, 'package.json'), 'utf-8'));
871
+ bin = pkg.bin;
872
+ }
873
+ catch {
874
+ return null;
875
+ }
876
+ let rel;
877
+ if (typeof bin === 'string') {
878
+ rel = bin;
879
+ }
880
+ else if (bin && typeof bin === 'object') {
881
+ const map = bin;
882
+ // Prefer the entry named after our launch command; else the first bin.
883
+ rel = map[agentConfig.cliCommand] ?? Object.values(map)[0];
884
+ }
885
+ if (!rel || typeof rel !== 'string')
886
+ return null;
887
+ return path.join(pkgRoot, rel);
888
+ }
843
889
  /**
844
890
  * Check if a specific version is installed.
891
+ *
892
+ * Probes the actual launch binary (the same executable the shims run), not just
893
+ * the version dir or the node_modules/.bin/<cli> wrapper. For npm agents that
894
+ * means statting the package's real `bin` target (getPackageBinaryPath), so a
895
+ * present-but-gutted install (vendor auto-updater destroyed the binary) reports
896
+ * as NOT installed and `agents add` re-runs its install step to repair it.
845
897
  */
846
898
  export function isVersionInstalled(agent, version) {
847
- const binaryPath = getBinaryPath(agent, version);
848
- return fs.existsSync(binaryPath);
899
+ const packageBinary = getPackageBinaryPath(agent, version);
900
+ if (packageBinary !== null)
901
+ return fs.existsSync(packageBinary);
902
+ return fs.existsSync(getBinaryPath(agent, version));
849
903
  }
850
904
  /**
851
905
  * Get the latest available version from npm for an agent.
@@ -936,8 +990,10 @@ export function listInstalledVersions(agent) {
936
990
  const versions = [];
937
991
  for (const entry of entries) {
938
992
  if (entry.isDirectory()) {
939
- const binaryPath = getBinaryPath(agent, entry.name);
940
- if (fs.existsSync(binaryPath)) {
993
+ // Probe the real launch binary (isVersionInstalled), not just the
994
+ // node_modules/.bin wrapper — a gutted install must not count as healthy
995
+ // in the balanced account/version picker.
996
+ if (isVersionInstalled(agent, entry.name)) {
941
997
  versions.push(entry.name);
942
998
  }
943
999
  }
@@ -966,7 +1022,7 @@ export function listInstalledVersionDirs(agent) {
966
1022
  continue;
967
1023
  out.push({
968
1024
  version: entry.name,
969
- hasBinary: fs.existsSync(getBinaryPath(agent, entry.name)),
1025
+ hasBinary: isVersionInstalled(agent, entry.name),
970
1026
  });
971
1027
  }
972
1028
  return out.sort((a, b) => compareVersions(a.version, b.version));
@@ -1117,7 +1173,7 @@ export async function installVersion(agent, version, onProgress) {
1117
1173
  };
1118
1174
  }
1119
1175
  onProgress?.(`Installing ${packageSpec}...`);
1120
- const { stdout } = await execFileAsync('npm', ['install', packageSpec], { cwd: versionDir, shell: winShell });
1176
+ const { stdout } = await execFileAsync('npm', ['install', packageSpec, '--ignore-scripts'], { cwd: versionDir, shell: winShell });
1121
1177
  // Determine the actual installed version
1122
1178
  let installedVersion = version;
1123
1179
  if (version === 'latest') {
@@ -1311,16 +1367,20 @@ export function removeVersion(agent, version) {
1311
1367
  }
1312
1368
  // Remove versioned alias (e.g., claude@2.0.65)
1313
1369
  removeVersionedAlias(agent, version);
1314
- // Clear default if it was the removed version - user must explicitly pick a new one
1370
+ // If the removed version was the global default, keep launchers working:
1371
+ // reassign the default to the newest remaining version, or clear it outright
1372
+ // only when nothing is left. Leaving a dangling default pointer here is what
1373
+ // broke every launcher after removing the pinned default.
1315
1374
  if (getGlobalDefault(agent) === version) {
1316
- const meta = readMeta();
1317
- if (meta.agents?.[agent]) {
1318
- delete meta.agents[agent];
1319
- writeMeta(meta);
1320
- }
1321
1375
  const remaining = listInstalledVersions(agent);
1322
1376
  if (remaining.length > 0) {
1323
- console.log(chalk.yellow(`Default version removed. Run: agents use ${agent}@<version> to set a new default`));
1377
+ const newestRemaining = remaining[remaining.length - 1];
1378
+ setGlobalDefault(agent, newestRemaining);
1379
+ console.log(chalk.yellow(`Default ${agent} was ${version} (removed); reassigned to ${newestRemaining}. Change it with: agents use ${agent}@<version>`));
1380
+ }
1381
+ else {
1382
+ setGlobalDefault(agent, undefined);
1383
+ console.log(chalk.yellow(`Removed the last installed ${agent} version and cleared its default. Reinstall with: agents add ${agent}, then set one with: agents use ${agent}@<version>`));
1324
1384
  }
1325
1385
  }
1326
1386
  // Clean up dangling config symlink if it pointed to the removed version
@@ -1334,6 +1394,18 @@ export function removeVersion(agent, version) {
1334
1394
  // Ignore if already gone
1335
1395
  }
1336
1396
  }
1397
+ // Clear dead per-version hook entries the removed version left behind in the
1398
+ // remaining versions' settings. Their command paths point under the now-gone
1399
+ // version home, so they error on every tool call until the next sync. Scoped
1400
+ // to the Claude-family settings.json format (claude + droid) — the surface
1401
+ // where per-version guard hooks accumulate; other agents self-heal on sync.
1402
+ if (agent === 'claude' || agent === 'droid') {
1403
+ const configDir = agentConfigDirName(agent);
1404
+ for (const remaining of listInstalledVersions(agent)) {
1405
+ const settingsPath = path.join(getVersionHomePath(agent, remaining), configDir, 'settings.json');
1406
+ pruneVersionHomeHookEntriesFromSettings(settingsPath, agent, version);
1407
+ }
1408
+ }
1337
1409
  emit('version.remove', { agent, version });
1338
1410
  return true;
1339
1411
  }
@@ -1715,6 +1787,40 @@ export function buildRepoScopedSelection(repo, cwd = process.cwd()) {
1715
1787
  selection.memory = 'all';
1716
1788
  return selection;
1717
1789
  }
1790
+ /**
1791
+ * Union several ResourceSelections into one, deduping names per kind. Pure — no
1792
+ * FS — so the merge logic is unit-tested without a DotAgents layout. `memory` is
1793
+ * a whole-file merge of the user+system layers, not a per-repo artifact: pass
1794
+ * `includeMemory` to request a full memory write (`'all'`), else the skip
1795
+ * sentinel (`[]`) is kept.
1796
+ */
1797
+ export function unionResourceSelections(selections, includeMemory) {
1798
+ const merged = {};
1799
+ const kinds = ['commands', 'skills', 'hooks', 'subagents', 'permissions', 'mcp', 'plugins', 'workflows'];
1800
+ for (const sel of selections) {
1801
+ for (const kind of kinds) {
1802
+ const names = sel[kind];
1803
+ if (!Array.isArray(names) || names.length === 0)
1804
+ continue;
1805
+ const existing = merged[kind] ?? [];
1806
+ merged[kind] = Array.from(new Set([...existing, ...names]));
1807
+ }
1808
+ }
1809
+ merged.memory = includeMemory ? 'all' : [];
1810
+ return merged;
1811
+ }
1812
+ /**
1813
+ * Build one selection spanning several DotAgent repos — the source set an
1814
+ * interactive `agents sync` picker collects. Each repo is scoped via
1815
+ * `buildRepoScopedSelection`, then unioned. Memory is written whole when the
1816
+ * `user` or `system` layer is among the selected repos (that's where the memory
1817
+ * file's content comes from); a project-only / alias-only pick leaves it
1818
+ * untouched.
1819
+ */
1820
+ export function mergeRepoScopedSelections(repos, cwd = process.cwd()) {
1821
+ const includeMemory = repos.includes('user') || repos.includes('system');
1822
+ return unionResourceSelections(repos.map((r) => buildRepoScopedSelection(r, cwd)), includeMemory);
1823
+ }
1718
1824
  /**
1719
1825
  * Sync central resources (~/.agents/) into a specific version's config directory.
1720
1826
  * Copies selected resources from central storage into {versionHome}/.{agent}/.
@@ -0,0 +1,3 @@
1
+ export * from './watchdog.js';
2
+ export * from './watchdogTail.js';
3
+ export * from './read.js';
@@ -0,0 +1,5 @@
1
+ // Watchdog stall-detection + nudge-decision core. Pure logic ported from
2
+ // Swarmify; terminal-injection delivery and menu-bar wiring live elsewhere.
3
+ export * from './watchdog.js';
4
+ export * from './watchdogTail.js';
5
+ export * from './read.js';
@@ -0,0 +1,35 @@
1
+ /** Default watchdog thresholds, mirrored from the Swarmify VS Code runtime. */
2
+ export declare const WATCHDOG_TAIL_LINES = 20;
3
+ export declare const WATCHDOG_STALL_MS = 300000;
4
+ export declare const WATCHDOG_COOLDOWN_MS = 1200000;
5
+ export declare const WATCHDOG_DORMANT_MS = 3600000;
6
+ /**
7
+ * Read the last `maxLines` non-empty lines of a JSONL transcript by seeking
8
+ * backward from EOF in 64KB chunks. A tail that begins mid-line yields one
9
+ * malformed leading line, which the callers' per-line JSON try/catch tolerates.
10
+ * Returns `[]` on any read error or empty file.
11
+ */
12
+ export declare function readTailLines(filePath: string, maxLines: number): string[];
13
+ /**
14
+ * Given candidate transcript-root directories, find the transcript file for a
15
+ * session. Handles the flat Claude layout (`<sessionId>.jsonl` inside a
16
+ * per-project `<enc>/` subfolder), Codex-style names that merely embed the uuid
17
+ * (`rollout-…-<sessionId>.jsonl`), and — critically — the DEEP date partitions
18
+ * Codex/Droid use (`sessions/YYYY/MM/DD/rollout-…-<uuid>.jsonl`). The scan is
19
+ * fully recursive via `walkForFiles` (the same walker session/discover.ts uses),
20
+ * so no fixed number of subdir levels is assumed — a hand-rolled one-level scan
21
+ * silently missed every Codex transcript. Newest mtime wins when a uuid appears
22
+ * in more than one root (e.g. across version homes). Pure over its `dirs`
23
+ * argument, so it is testable without touching the real home directory.
24
+ */
25
+ export declare function findSessionJsonlIn(dirs: string[], sessionId: string, ext?: string): string | undefined;
26
+ /**
27
+ * Resolve a session transcript path from its id + agent, reusing the CLI's
28
+ * per-version transcript roots. Returns `undefined` if no transcript is found.
29
+ */
30
+ export declare function resolveWatchdogSessionPath(sessionId: string, agent: string): string | undefined;
31
+ /**
32
+ * Read the last `maxLines` JSONL lines for a session by id + agent. Returns
33
+ * `[]` when the transcript cannot be located or read.
34
+ */
35
+ export declare function readWatchdogTail(sessionId: string, agent: string, maxLines?: number): string[];
@@ -0,0 +1,149 @@
1
+ // Watchdog tail reader: locate a session transcript from its id + agent and
2
+ // pull the last N raw JSONL lines. The raw lines feed the pure detectors in
3
+ // watchdog.ts (isLikelyTrulyBlocked / renderWatchdogPrompt) and the pure
4
+ // summarizer in watchdogTail.ts.
5
+ //
6
+ // Adapted from Swarmify's readTailLines (extension/src/vscode/sessions.vscode.ts)
7
+ // to agents-cli's session layout. Path resolution REUSES getAgentSessionDirs()
8
+ // from src/lib/session/discover.ts — the same resolver the rest of the CLI uses
9
+ // to enumerate per-version transcript roots — instead of hardcoding
10
+ // ~/.agents/.history/versions/<agent>/.../projects/<enc>/<sessionId>.jsonl.
11
+ import * as fs from 'fs';
12
+ import * as path from 'path';
13
+ import { getAgentSessionDirs } from '../session/discover.js';
14
+ import { walkForFiles } from '../fs-walk.js';
15
+ /** Default watchdog thresholds, mirrored from the Swarmify VS Code runtime. */
16
+ export const WATCHDOG_TAIL_LINES = 20;
17
+ export const WATCHDOG_STALL_MS = 300_000; // 5m — stallSeconds default
18
+ export const WATCHDOG_COOLDOWN_MS = 1_200_000; // 20m — cooldownSeconds default
19
+ export const WATCHDOG_DORMANT_MS = 3_600_000; // 1h — DORMANT_MS
20
+ const CHUNK_SIZE = 64 * 1024;
21
+ const UUID_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
22
+ /** Cap on files walked per transcript root — matches session/discover.ts. */
23
+ const WATCHDOG_WALK_CAP = 100_000;
24
+ /**
25
+ * Per-agent transcript layout, mirroring the `getAgentSessionDirs()` call sites
26
+ * in session/discover.ts so the resolver never diverges from the rest of the CLI:
27
+ * Claude keeps per-project folders under `projects/` (discover.ts:486); Codex and
28
+ * Droid date-/work-partition rollout `.jsonl` deep under `sessions/`
29
+ * (discover.ts:665, :1673); Gemini nests chat `.json` under `tmp/<hash>/chats/`
30
+ * (discover.ts:820). Subdir + extension are driven from this table instead of the
31
+ * old hardcoded `codex ? 'sessions' : 'projects'` ternary, which sent every
32
+ * non-Codex agent (Gemini included) to the wrong root.
33
+ */
34
+ const WATCHDOG_SESSION_LAYOUT = {
35
+ claude: { subdir: 'projects', ext: '.jsonl' },
36
+ codex: { subdir: 'sessions', ext: '.jsonl' },
37
+ droid: { subdir: 'sessions', ext: '.jsonl' },
38
+ gemini: { subdir: 'tmp', ext: '.json' },
39
+ };
40
+ const WATCHDOG_SESSION_LAYOUT_DEFAULT = { subdir: 'projects', ext: '.jsonl' };
41
+ /**
42
+ * Read the last `maxLines` non-empty lines of a JSONL transcript by seeking
43
+ * backward from EOF in 64KB chunks. A tail that begins mid-line yields one
44
+ * malformed leading line, which the callers' per-line JSON try/catch tolerates.
45
+ * Returns `[]` on any read error or empty file.
46
+ */
47
+ export function readTailLines(filePath, maxLines) {
48
+ let fd;
49
+ try {
50
+ fd = fs.openSync(filePath, 'r');
51
+ }
52
+ catch {
53
+ return [];
54
+ }
55
+ try {
56
+ const fileSize = fs.fstatSync(fd).size;
57
+ if (fileSize === 0)
58
+ return [];
59
+ let position = fileSize;
60
+ let buffer = '';
61
+ let collected = [];
62
+ while (position > 0 && collected.length <= maxLines) {
63
+ const readSize = Math.min(CHUNK_SIZE, position);
64
+ position -= readSize;
65
+ const chunk = Buffer.alloc(readSize);
66
+ fs.readSync(fd, chunk, 0, readSize, position);
67
+ buffer = chunk.toString('utf-8') + buffer;
68
+ collected = buffer.split(/\r?\n/).filter((l) => l.trim());
69
+ }
70
+ return collected.slice(-maxLines);
71
+ }
72
+ catch {
73
+ return [];
74
+ }
75
+ finally {
76
+ try {
77
+ fs.closeSync(fd);
78
+ }
79
+ catch {
80
+ /* fd already gone */
81
+ }
82
+ }
83
+ }
84
+ /**
85
+ * Given candidate transcript-root directories, find the transcript file for a
86
+ * session. Handles the flat Claude layout (`<sessionId>.jsonl` inside a
87
+ * per-project `<enc>/` subfolder), Codex-style names that merely embed the uuid
88
+ * (`rollout-…-<sessionId>.jsonl`), and — critically — the DEEP date partitions
89
+ * Codex/Droid use (`sessions/YYYY/MM/DD/rollout-…-<uuid>.jsonl`). The scan is
90
+ * fully recursive via `walkForFiles` (the same walker session/discover.ts uses),
91
+ * so no fixed number of subdir levels is assumed — a hand-rolled one-level scan
92
+ * silently missed every Codex transcript. Newest mtime wins when a uuid appears
93
+ * in more than one root (e.g. across version homes). Pure over its `dirs`
94
+ * argument, so it is testable without touching the real home directory.
95
+ */
96
+ export function findSessionJsonlIn(dirs, sessionId, ext = '.jsonl') {
97
+ if (!sessionId)
98
+ return undefined;
99
+ const matches = (name) => {
100
+ if (!name.endsWith(ext))
101
+ return false;
102
+ const stem = name.slice(0, -ext.length);
103
+ if (stem === sessionId)
104
+ return true;
105
+ // Codex embeds the uuid in a longer filename (rollout-<ts>-<uuid>.jsonl).
106
+ return name.includes(sessionId) || (UUID_RE.test(sessionId) && stem.includes(sessionId));
107
+ };
108
+ let best;
109
+ for (const dir of dirs) {
110
+ for (const file of walkForFiles(dir, ext, WATCHDOG_WALK_CAP)) {
111
+ if (!matches(path.basename(file)))
112
+ continue;
113
+ let mtime;
114
+ try {
115
+ mtime = fs.statSync(file).mtimeMs;
116
+ }
117
+ catch {
118
+ continue;
119
+ }
120
+ if (!best || mtime > best.mtime)
121
+ best = { file, mtime };
122
+ }
123
+ }
124
+ return best?.file;
125
+ }
126
+ /**
127
+ * Resolve a session transcript path from its id + agent, reusing the CLI's
128
+ * per-version transcript roots. Returns `undefined` if no transcript is found.
129
+ */
130
+ export function resolveWatchdogSessionPath(sessionId, agent) {
131
+ // Subdir + transcript extension are driven per-agent from WATCHDOG_SESSION_LAYOUT
132
+ // (which mirrors the getAgentSessionDirs() convention in session/discover.ts),
133
+ // not a hardcoded `codex ? 'sessions' : 'projects'` — that ternary sent Codex to
134
+ // a root it only scanned one level deep, and every other agent (Gemini included)
135
+ // to the wrong subdir entirely.
136
+ const layout = WATCHDOG_SESSION_LAYOUT[agent] ?? WATCHDOG_SESSION_LAYOUT_DEFAULT;
137
+ const dirs = getAgentSessionDirs(agent, layout.subdir);
138
+ return findSessionJsonlIn(dirs, sessionId, layout.ext);
139
+ }
140
+ /**
141
+ * Read the last `maxLines` JSONL lines for a session by id + agent. Returns
142
+ * `[]` when the transcript cannot be located or read.
143
+ */
144
+ export function readWatchdogTail(sessionId, agent, maxLines = WATCHDOG_TAIL_LINES) {
145
+ const filePath = resolveWatchdogSessionPath(sessionId, agent);
146
+ if (!filePath)
147
+ return [];
148
+ return readTailLines(filePath, maxLines);
149
+ }