@phnx-labs/agents-cli 1.22.40 → 1.22.42

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 (201) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +14 -3
  3. package/dist/bin/agents +0 -0
  4. package/dist/bootstrap.js +0 -1
  5. package/dist/cli/command-registry.d.ts +2 -0
  6. package/dist/cli/command-registry.js +5 -2
  7. package/dist/commands/accounts.d.ts +29 -0
  8. package/dist/commands/accounts.js +220 -29
  9. package/dist/commands/alias.js +1 -1
  10. package/dist/commands/auth.d.ts +9 -0
  11. package/dist/commands/auth.js +104 -0
  12. package/dist/commands/browser-sessions-picker.js +42 -62
  13. package/dist/commands/browser.d.ts +15 -0
  14. package/dist/commands/browser.js +145 -78
  15. package/dist/commands/computer-actions.d.ts +1 -1
  16. package/dist/commands/computer-actions.js +1 -1
  17. package/dist/commands/computer-sessions-picker.js +32 -55
  18. package/dist/commands/computer.d.ts +1 -1
  19. package/dist/commands/computer.js +2 -2
  20. package/dist/commands/doctor.d.ts +1 -1
  21. package/dist/commands/doctor.js +3 -3
  22. package/dist/commands/feed.js +4 -12
  23. package/dist/commands/hooks.js +1 -1
  24. package/dist/commands/import.js +1 -1
  25. package/dist/commands/insights.js +163 -85
  26. package/dist/commands/inspect.js +2 -2
  27. package/dist/commands/mine.js +1 -1
  28. package/dist/commands/org.d.ts +6 -0
  29. package/dist/commands/org.js +174 -0
  30. package/dist/commands/packages.js +1 -1
  31. package/dist/commands/prune.js +1 -1
  32. package/dist/commands/routines.js +1 -1
  33. package/dist/commands/run-account-picker.d.ts +19 -0
  34. package/dist/commands/run-account-picker.js +81 -0
  35. package/dist/commands/secrets.js +87 -97
  36. package/dist/commands/sessions-picker-factory.d.ts +40 -0
  37. package/dist/commands/sessions-picker-factory.js +67 -0
  38. package/dist/commands/sessions-picker.d.ts +1 -0
  39. package/dist/commands/sessions-picker.js +27 -5
  40. package/dist/commands/sessions-share.d.ts +25 -0
  41. package/dist/commands/sessions-share.js +166 -0
  42. package/dist/commands/sessions.js +2 -0
  43. package/dist/commands/setup-browser.js +5 -5
  44. package/dist/commands/setup-mine.js +1 -1
  45. package/dist/commands/setup-preferences.js +4 -4
  46. package/dist/commands/setup.js +5 -3
  47. package/dist/commands/sync.js +1 -1
  48. package/dist/commands/versions.js +1 -1
  49. package/dist/commands/view.js +1 -1
  50. package/dist/commands/webhook.js +14 -9
  51. package/dist/lib/account-capabilities.d.ts +10 -0
  52. package/dist/lib/account-capabilities.js +34 -0
  53. package/dist/lib/agent-spec/agents.d.ts +584 -0
  54. package/dist/lib/agent-spec/agents.js +3127 -0
  55. package/dist/lib/agent-spec/provider.js +1 -1
  56. package/dist/lib/agents.d.ts +2 -584
  57. package/dist/lib/agents.js +2 -3127
  58. package/dist/lib/browser/cdp.js +4 -0
  59. package/dist/lib/browser/chrome.js +12 -1
  60. package/dist/lib/browser/drivers/local.d.ts +8 -2
  61. package/dist/lib/browser/drivers/local.js +8 -2
  62. package/dist/lib/browser/drivers/ssh.d.ts +8 -2
  63. package/dist/lib/browser/drivers/ssh.js +11 -4
  64. package/dist/lib/browser/hygiene.d.ts +1 -1
  65. package/dist/lib/browser/hygiene.js +5 -1
  66. package/dist/lib/browser/profiles.d.ts +77 -4
  67. package/dist/lib/browser/profiles.js +121 -12
  68. package/dist/lib/browser/runtime-state.d.ts +11 -5
  69. package/dist/lib/browser/runtime-state.js +13 -9
  70. package/dist/lib/browser/service.d.ts +91 -30
  71. package/dist/lib/browser/service.js +342 -207
  72. package/dist/lib/browser/types.d.ts +96 -4
  73. package/dist/lib/browser/types.js +51 -0
  74. package/dist/lib/catchup.js +1 -1
  75. package/dist/lib/computer/actions.d.ts +1 -1
  76. package/dist/lib/computer/actions.js +1 -1
  77. package/dist/lib/{computer-rpc.js → computer/computer-rpc.js} +5 -5
  78. package/dist/lib/computer/dispatch.d.ts +1 -1
  79. package/dist/lib/computer/download.d.ts +1 -1
  80. package/dist/lib/computer/download.js +3 -3
  81. package/dist/lib/{ssh-tunnel.d.ts → computer/ssh-tunnel.d.ts} +1 -1
  82. package/dist/lib/{ssh-tunnel.js → computer/ssh-tunnel.js} +12 -12
  83. package/dist/lib/daemon/daemon.js +1 -1
  84. package/dist/lib/{runner.d.ts → daemon/runner.d.ts} +10 -10
  85. package/dist/lib/{runner.js → daemon/runner.js} +59 -118
  86. package/dist/lib/daemon-webhooks.d.ts +3 -1
  87. package/dist/lib/daemon-webhooks.js +12 -7
  88. package/dist/lib/device-config.js +1 -1
  89. package/dist/lib/devices/doctor-findings.d.ts +1 -1
  90. package/dist/lib/devices/doctor-findings.js +3 -3
  91. package/dist/lib/devices/fleet-inventory.js +1 -1
  92. package/dist/lib/devices/harness-inventory.js +1 -1
  93. package/dist/lib/devices/registry.js +4 -22
  94. package/dist/lib/doctor-diff.d.ts +1 -1
  95. package/dist/lib/doctor-diff.js +1 -1
  96. package/dist/lib/drift.js +1 -1
  97. package/dist/lib/entitlement.d.ts +31 -0
  98. package/dist/lib/entitlement.js +137 -0
  99. package/dist/lib/exec.js +39 -202
  100. package/dist/lib/feed/feed.js +2 -8
  101. package/dist/lib/fs-atomic.d.ts +7 -0
  102. package/dist/lib/fs-atomic.js +9 -0
  103. package/dist/lib/harness/adapter.d.ts +160 -0
  104. package/dist/lib/harness/adapter.js +47 -0
  105. package/dist/lib/harness/adapters/claude.d.ts +2 -0
  106. package/dist/lib/harness/adapters/claude.js +110 -0
  107. package/dist/lib/harness/adapters/codex.d.ts +2 -0
  108. package/dist/lib/harness/adapters/codex.js +83 -0
  109. package/dist/lib/harness/adapters/copilot.d.ts +2 -0
  110. package/dist/lib/harness/adapters/copilot.js +23 -0
  111. package/dist/lib/harness/adapters/cursor.d.ts +2 -0
  112. package/dist/lib/harness/adapters/cursor.js +38 -0
  113. package/dist/lib/harness/adapters/droid.d.ts +2 -0
  114. package/dist/lib/harness/adapters/droid.js +17 -0
  115. package/dist/lib/harness/adapters/grok.d.ts +2 -0
  116. package/dist/lib/harness/adapters/grok.js +14 -0
  117. package/dist/lib/harness/adapters/kimi.d.ts +2 -0
  118. package/dist/lib/harness/adapters/kimi.js +49 -0
  119. package/dist/lib/harness/adapters/muse.d.ts +2 -0
  120. package/dist/lib/harness/adapters/muse.js +44 -0
  121. package/dist/lib/harness/adapters/opencode.d.ts +2 -0
  122. package/dist/lib/harness/adapters/opencode.js +14 -0
  123. package/dist/lib/harness/exec-config-version.d.ts +8 -0
  124. package/dist/lib/harness/exec-config-version.js +22 -0
  125. package/dist/lib/harness/index.d.ts +1 -0
  126. package/dist/lib/harness/index.js +34 -0
  127. package/dist/lib/{hooks.d.ts → hooks/install.d.ts} +1 -1
  128. package/dist/lib/{hooks.js → hooks/install.js} +11 -11
  129. package/dist/lib/import.js +1 -1
  130. package/dist/lib/installations/index.d.ts +4 -4
  131. package/dist/lib/installations/index.js +3 -4
  132. package/dist/lib/installations/migrate.js +6 -6
  133. package/dist/lib/{shims.d.ts → installations/shims.d.ts} +2 -2
  134. package/dist/lib/{shims.js → installations/shims.js} +12 -114
  135. package/dist/lib/installations/store.d.ts +141 -3
  136. package/dist/lib/installations/store.js +475 -5
  137. package/dist/lib/installations/versions.d.ts +1 -126
  138. package/dist/lib/installations/versions.js +5 -465
  139. package/dist/lib/isolation-boundary-report.d.ts +1 -1
  140. package/dist/lib/isolation-boundary-report.js +1 -1
  141. package/dist/lib/mailbox-gc.js +2 -3
  142. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  143. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  144. package/dist/lib/monitors/dispatch.d.ts +1 -1
  145. package/dist/lib/monitors/dispatch.js +2 -2
  146. package/dist/lib/monitors/state.d.ts +1 -1
  147. package/dist/lib/observe-aliases.d.ts +2 -2
  148. package/dist/lib/observe-aliases.js +2 -11
  149. package/dist/lib/plugins/plugins.js +1 -1
  150. package/dist/lib/plugins/skills.js +1 -1
  151. package/dist/lib/prix-account.d.ts +158 -0
  152. package/dist/lib/prix-account.js +214 -0
  153. package/dist/lib/project-key.js +7 -0
  154. package/dist/lib/project-launch.js +1 -1
  155. package/dist/lib/refresh.js +2 -2
  156. package/dist/lib/resource-inventory.d.ts +1 -1
  157. package/dist/lib/resource-inventory.js +1 -1
  158. package/dist/lib/rules/rules.js +1 -1
  159. package/dist/lib/scheduling/routines.js +1 -1
  160. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  161. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  162. package/dist/lib/secrets/bundles.d.ts +1 -1
  163. package/dist/lib/secrets/bundles.js +1 -1
  164. package/dist/lib/secrets/headless.d.ts +16 -0
  165. package/dist/lib/secrets/headless.js +21 -0
  166. package/dist/lib/secrets/remote.d.ts +9 -7
  167. package/dist/lib/secrets/remote.js +18 -9
  168. package/dist/lib/self-heal/checks/hook-runtime.js +1 -1
  169. package/dist/lib/self-heal/checks/path.js +1 -1
  170. package/dist/lib/self-heal/checks/shadowing.js +1 -1
  171. package/dist/lib/self-heal/checks/shims.js +1 -1
  172. package/dist/lib/session/discover.d.ts +39 -0
  173. package/dist/lib/session/discover.js +3 -3
  174. package/dist/lib/session/parse.js +24 -42
  175. package/dist/lib/session/recovery.js +1 -1
  176. package/dist/lib/session/share-html.d.ts +55 -0
  177. package/dist/lib/session/share-html.js +319 -0
  178. package/dist/lib/settings-manifest.d.ts +2 -0
  179. package/dist/lib/settings-manifest.js +81 -3
  180. package/dist/lib/share/publish.d.ts +38 -0
  181. package/dist/lib/share/publish.js +81 -8
  182. package/dist/lib/staleness/prune.d.ts +1 -1
  183. package/dist/lib/staleness/prune.js +1 -1
  184. package/dist/lib/staleness/writers/hooks.js +1 -1
  185. package/dist/lib/startup/command-registry.d.ts +2 -1
  186. package/dist/lib/startup/command-registry.js +5 -3
  187. package/dist/lib/teams/agents.js +2 -19
  188. package/dist/lib/teams/parsers.js +20 -26
  189. package/dist/lib/teams/registry.js +2 -21
  190. package/dist/lib/triggers/handlers.d.ts +37 -2
  191. package/dist/lib/triggers/handlers.js +57 -6
  192. package/dist/lib/triggers/webhook.d.ts +80 -6
  193. package/dist/lib/triggers/webhook.js +128 -4
  194. package/dist/lib/types.d.ts +1 -1
  195. package/dist/lib/uninstall.js +1 -1
  196. package/dist/lib/version.d.ts +9 -2
  197. package/dist/lib/version.js +11 -9
  198. package/dist/lib/wrap.d.ts +33 -0
  199. package/dist/lib/wrap.js +70 -0
  200. package/package.json +1 -1
  201. /package/dist/lib/{computer-rpc.d.ts → computer/computer-rpc.d.ts} +0 -0
@@ -208,6 +208,17 @@ export function handlerMatchesWebhook(handler, webhook) {
208
208
  return false;
209
209
  if (!handlerRunsOnThisDevice(handler))
210
210
  return false;
211
+ // Slack has no GitHub/Linear action/label/branch vocabulary; it matches on the
212
+ // slash command and (optionally) the channel. `event` already pinned the
213
+ // subtype (`app_mention`) or the slash command name above.
214
+ if (webhook.source === 'slack') {
215
+ const slack = webhook.payload;
216
+ if (handler.command && slack.command !== handler.command)
217
+ return false;
218
+ if (handler.channel && slack.channel !== handler.channel)
219
+ return false;
220
+ return true;
221
+ }
211
222
  if (handler.action) {
212
223
  const action = webhook.source === 'github' ? githubAction(webhook.payload) : linearAction(webhook.payload);
213
224
  if (action !== handler.action)
@@ -262,12 +273,47 @@ export function handlerMatchesWebhook(handler, webhook) {
262
273
  }
263
274
  return true;
264
275
  }
276
+ const asString = (v) => (typeof v === 'string' ? v : '');
277
+ /**
278
+ * Split a Slack message into a `{{slack.*}}` context. Strips a leading bot
279
+ * mention (`<@U0BOT> …`), then reads an optional `PROJECT:` prefix — a single
280
+ * bare token followed by a colon and a space — as the project, with the
281
+ * remainder the prompt. `AGI: rebase my PR` → project `AGI`, prompt
282
+ * `rebase my PR`; a message with no such prefix leaves `project` empty and the
283
+ * whole text as the prompt. The token pattern forbids slashes, so a project
284
+ * value can never carry a path into a `cwd`/`project` substitution.
285
+ */
286
+ export function parseSlackMessage(payload) {
287
+ const cleaned = asString(payload.text).replace(/^\s*<@[^>]+>\s*/, '').trim();
288
+ const m = /^([A-Za-z0-9][\w.-]*)\s*:\s+([\s\S]+)$/.exec(cleaned);
289
+ return {
290
+ text: cleaned,
291
+ project: m ? m[1] : '',
292
+ prompt: m ? m[2].trim() : cleaned,
293
+ channel: asString(payload.channel),
294
+ thread_ts: asString(payload.thread_ts),
295
+ user: asString(payload.user),
296
+ command: asString(payload.command),
297
+ };
298
+ }
265
299
  /**
266
300
  * Build the variable-substitution context for a webhook. Linear events expose
267
301
  * `issue` and `updatedFrom`; GitHub events expose `repository`, `pull_request`,
268
- * and `issue`.
302
+ * and `issue`; Slack events expose the `{{slack.*}}` namespace above.
269
303
  */
270
304
  export function buildWebhookContext(webhook) {
305
+ if (webhook.source === 'slack') {
306
+ // The `{{slack.*}}` namespace rides an intersection over WebhookContext (a
307
+ // subtype of it) rather than a field on the shared type, so a Slack-only
308
+ // addition never touches the scheduling hub every routine test depends on.
309
+ // `substituteWebhookPrompt`/`getPath` read it dynamically at runtime.
310
+ const ctx = {
311
+ source: webhook.source,
312
+ event: webhook.event,
313
+ slack: parseSlackMessage(webhook.payload),
314
+ };
315
+ return ctx;
316
+ }
271
317
  const action = webhook.source === 'github'
272
318
  ? githubAction(webhook.payload) ?? undefined
273
319
  : linearAction(webhook.payload) ?? undefined;
@@ -306,7 +352,7 @@ function dispatchDefault(config) {
306
352
  // Import lazily so the runner module (heavy) is only loaded when a handler
307
353
  // actually needs to spawn. Tests inject dispatch functions, so this path is
308
354
  // not exercised in unit tests.
309
- return import('../runner.js').then((m) => m.executeJobDetached(config));
355
+ return import('../daemon/runner.js').then((m) => m.executeJobDetached(config));
310
356
  }
311
357
  /**
312
358
  * Execute a handler's action. Runs the configured agent/workflow/command or
@@ -346,6 +392,11 @@ export async function executeHandler(handler, webhook, opts = {}) {
346
392
  }
347
393
  async function executeHandlerAction(handler, webhook, context, opts) {
348
394
  const substitutedPrompt = handler.run?.prompt ? substituteWebhookPrompt(handler.run.prompt, context) : '';
395
+ // Slack routes the project per-message (`AGI: …`), so a handler may template
396
+ // its project/cwd — e.g. `project: "{{slack.project}}"`. Substitute + trim; an
397
+ // empty result omits the field (falls back to the run's default cwd / $HOME).
398
+ const substitutedProject = handler.project ? substituteWebhookPrompt(handler.project, context).trim() : '';
399
+ const substitutedCwd = handler.cwd ? substituteWebhookPrompt(handler.cwd, context).trim() : '';
349
400
  // Resolved once so a fleet pick can't differ between the two dispatch paths.
350
401
  const hostFields = resolveHandlerHost(handler.host);
351
402
  if (handler.run?.agent || handler.run?.workflow) {
@@ -359,8 +410,8 @@ async function executeHandlerAction(handler, webhook, context, opts) {
359
410
  ...(handler.run.agent ? { agent: handler.run.agent } : { workflow: handler.run.workflow }),
360
411
  ...(handler.devices ? { devices: handler.devices } : {}),
361
412
  ...(handler.run.env ? { env: handler.run.env } : {}),
362
- ...(handler.project ? { project: handler.project } : {}),
363
- ...(handler.cwd ? { cwd: handler.cwd } : {}),
413
+ ...(substitutedProject ? { project: substitutedProject } : {}),
414
+ ...(substitutedCwd ? { cwd: substitutedCwd } : {}),
364
415
  ...(hostFields.host ? { host: hostFields.host } : {}),
365
416
  ...(hostFields.hostStrategy ? { hostStrategy: hostFields.hostStrategy } : {}),
366
417
  };
@@ -385,8 +436,8 @@ async function executeHandlerAction(handler, webhook, context, opts) {
385
436
  ...routine,
386
437
  prompt: substituteWebhookPrompt(routine.prompt, context),
387
438
  ...(handler.devices ? { devices: handler.devices } : {}),
388
- ...(handler.project ? { project: handler.project } : {}),
389
- ...(handler.cwd ? { cwd: handler.cwd } : {}),
439
+ ...(substitutedProject ? { project: substitutedProject } : {}),
440
+ ...(substitutedCwd ? { cwd: substitutedCwd } : {}),
390
441
  ...(handler.mode ? { mode: handler.mode } : {}),
391
442
  ...(hostFields.host ? { host: hostFields.host } : {}),
392
443
  ...(hostFields.hostStrategy ? { hostStrategy: hostFields.hostStrategy } : {}),
@@ -15,13 +15,15 @@ import * as http from 'http';
15
15
  import type { IncomingHttpHeaders } from 'http';
16
16
  import type { JobConfig, RunMeta, WebhookContext } from '../scheduling/routines.js';
17
17
  import { type FiredHandler } from './handlers.js';
18
- export type WebhookSource = 'github' | 'linear';
18
+ export type WebhookSource = 'github' | 'linear' | 'slack';
19
19
  export interface IncomingWebhook {
20
20
  /** Delivery source, derived from `/hooks/<source>` or one-shot command flags. */
21
21
  source: WebhookSource;
22
- /** Source event name: GitHub header event or Linear payload `type`. */
22
+ /** Source event name: GitHub header event, Linear payload `type`, or the Slack
23
+ * event subtype (`app_mention`) / slash command (`/agents`). */
23
24
  event: string;
24
- /** Decoded JSON request body. */
25
+ /** Decoded request body (JSON for GitHub/Linear/Slack-events; the normalized
26
+ * {@link SlackPayload} for a Slack slash command's form body). */
25
27
  payload: Record<string, unknown>;
26
28
  }
27
29
  export type GithubWebhook = IncomingWebhook & {
@@ -30,6 +32,9 @@ export type GithubWebhook = IncomingWebhook & {
30
32
  export type LinearWebhook = IncomingWebhook & {
31
33
  source: 'linear';
32
34
  };
35
+ export type SlackWebhook = IncomingWebhook & {
36
+ source: 'slack';
37
+ };
33
38
  /** Read `repository.full_name` (`owner/name`) from a webhook payload, if present. */
34
39
  export declare function webhookRepo(payload: Record<string, unknown>): string | null;
35
40
  /**
@@ -96,9 +101,59 @@ export declare function fireWebhookJobs(webhook: IncomingWebhook, options?: Fire
96
101
  export declare function verifyGithubSignature(headers: IncomingHttpHeaders, rawBody: Buffer, secret: string): boolean;
97
102
  export declare function verifyLinearSignature(headers: IncomingHttpHeaders, rawBody: Buffer, secret: string): boolean;
98
103
  export declare function verifyLinearTimestamp(payload: Record<string, unknown>, now?: number, toleranceMs?: number): boolean;
104
+ /**
105
+ * Verify a Slack request signature (the `v0` scheme). Slack signs the base
106
+ * string `v0:${timestamp}:${rawBody}` with the app's signing secret and sends
107
+ * the hex digest as `X-Slack-Signature: v0=<hex>`, alongside the unix
108
+ * `X-Slack-Request-Timestamp`. The timestamp is BOTH part of the signed base
109
+ * string AND checked for freshness here — a request older than `toleranceSec`
110
+ * (default 5 min) is rejected, so a captured, correctly-signed body cannot be
111
+ * replayed later. Fails closed on any missing/malformed header.
112
+ */
113
+ export declare function verifySlackSignature(headers: IncomingHttpHeaders, rawBody: Buffer, secret: string, now?: number, toleranceSec?: number): boolean;
114
+ /**
115
+ * Normalized fields extracted from a Slack slash-command or Events API delivery.
116
+ * This is the `payload` of a Slack {@link IncomingWebhook}: transport-level
117
+ * parsing lives here, message *semantics* (splitting the mention text into an
118
+ * agent / project / prompt) live in `handlers.ts` `buildWebhookContext`.
119
+ */
120
+ export interface SlackPayload extends Record<string, unknown> {
121
+ /** `url_verification` | `event_callback` | `slash_command`. */
122
+ type: string;
123
+ /** Echoed back for the one-time Events API URL-verification handshake. */
124
+ challenge?: string;
125
+ /** Slack event id (Events API) — the dedup key when present. */
126
+ event_id?: string;
127
+ /** Event subtype, e.g. `app_mention` (Events API). */
128
+ event_type?: string;
129
+ /** Raw message text (the mention body, or the slash-command text). */
130
+ text?: string;
131
+ /** Channel id (`C0…`) the message arrived in. */
132
+ channel?: string;
133
+ /** Thread to reply into: an existing thread's parent ts, else the message ts. */
134
+ thread_ts?: string;
135
+ /** Invoking user id (`U0…`). */
136
+ user?: string;
137
+ /** Slash command name, e.g. `/agents`. */
138
+ command?: string;
139
+ /** Slash-command `response_url` (valid ~30 min, 5 uses). */
140
+ response_url?: string;
141
+ /** Slack team / workspace id. */
142
+ team?: string;
143
+ }
144
+ /**
145
+ * Parse a Slack delivery body into a normalized {@link SlackPayload}. Slack
146
+ * sends slash commands as `application/x-www-form-urlencoded` and Events API
147
+ * deliveries (including the `url_verification` handshake) as
148
+ * `application/json` — this is the one place that content-type branch lives.
149
+ */
150
+ export declare function parseSlackBody(contentType: string | undefined, rawBody: Buffer): SlackPayload;
151
+ /** The `event` name a Slack delivery fires under: the slash command, or the event subtype. */
152
+ export declare function slackEventName(payload: SlackPayload): string;
99
153
  export interface WebhookSecrets {
100
154
  github?: string;
101
155
  linear?: string;
156
+ slack?: string;
102
157
  }
103
158
  export interface DeliveryStore {
104
159
  seen(id: string): boolean;
@@ -140,11 +195,25 @@ export interface WebhookServerOptions {
140
195
  secrets: WebhookSecrets;
141
196
  /** Override the fire options (mainly for tests). */
142
197
  fire?: FireWebhookOptions;
198
+ /**
199
+ * Called the moment a delivery's matches are known — BEFORE any routine or
200
+ * handler is dispatched. Matching is a pure, synchronous lookup (no I/O), so
201
+ * this fires immediately after the ack, regardless of how long the matched
202
+ * work then takes to run. This is the right hook for a "webhook X fired Y"
203
+ * log line: a `run.command` handler that shells out a long agent session
204
+ * (`exec()`, which only resolves on process exit) used to hold that log back
205
+ * for as long as the session ran — `ps` would show the agent already running
206
+ * while the log still implied nothing had matched (RUSH-2722). Names only
207
+ * (no runId/exitCode yet — those aren't known until dispatch settles).
208
+ */
209
+ onMatch?: (webhook: IncomingWebhook, matchedJobNames: string[], matchedHandlerNames: string[]) => void;
143
210
  /**
144
211
  * Called after a delivery has fully settled — every matched routine and
145
- * handler dispatched. Because the receiver acks the HTTP response BEFORE
146
- * dispatch (see `startWebhookServer`), this fires strictly after the response
147
- * has been written, and is the only way a caller observes the outcome.
212
+ * handler dispatched (and, for `run.command` handlers, exited). Because the
213
+ * receiver acks the HTTP response BEFORE dispatch (see `startWebhookServer`),
214
+ * this fires strictly after the response has been written, and is the only
215
+ * way a caller observes the dispatch outcome (runId/exitCode/output). It is
216
+ * NOT the right hook for "did this webhook match" logging — use `onMatch`.
148
217
  */
149
218
  onDelivery?: (webhook: IncomingWebhook, fired: FiredJob[], handlers: FiredHandler[]) => void;
150
219
  /**
@@ -183,6 +252,11 @@ export interface WebhookServerOptions {
183
252
  * duplicate from an in-flight set, since `deliveryStore.seen` only reports
184
253
  * completed deliveries and would otherwise let a mid-flight retry double-fire.
185
254
  *
255
+ * `onMatch` fires as soon as matching is known, before any dispatch; `onDelivery`
256
+ * fires only once every matched routine/handler has settled — see their docs on
257
+ * `WebhookServerOptions`. A caller that wants a prompt "this fired" log uses
258
+ * `onMatch`, not `onDelivery` (RUSH-2722).
259
+ *
186
260
  * Returns the underlying server so callers can `close()` it.
187
261
  */
188
262
  export declare function startWebhookServer(options: WebhookServerOptions): http.Server;
@@ -16,7 +16,7 @@ import * as fs from 'fs';
16
16
  import * as http from 'http';
17
17
  import * as path from 'path';
18
18
  import { jobRunsOnThisDevice, listJobs, substituteWebhookPrompt } from '../scheduling/routines.js';
19
- import { executeJobDetached } from '../runner.js';
19
+ import { executeJobDetached } from '../daemon/runner.js';
20
20
  import { emit } from '../feed/events.js';
21
21
  import { listHandlers, handlerMatchesWebhook, executeHandler, buildWebhookContext, } from './handlers.js';
22
22
  /** Read `repository.full_name` (`owner/name`) from a webhook payload, if present. */
@@ -264,6 +264,76 @@ export function verifyLinearTimestamp(payload, now = Date.now(), toleranceMs = 6
264
264
  const ts = payload.webhookTimestamp;
265
265
  return typeof ts === 'number' && Math.abs(now - ts) <= toleranceMs;
266
266
  }
267
+ /**
268
+ * Verify a Slack request signature (the `v0` scheme). Slack signs the base
269
+ * string `v0:${timestamp}:${rawBody}` with the app's signing secret and sends
270
+ * the hex digest as `X-Slack-Signature: v0=<hex>`, alongside the unix
271
+ * `X-Slack-Request-Timestamp`. The timestamp is BOTH part of the signed base
272
+ * string AND checked for freshness here — a request older than `toleranceSec`
273
+ * (default 5 min) is rejected, so a captured, correctly-signed body cannot be
274
+ * replayed later. Fails closed on any missing/malformed header.
275
+ */
276
+ export function verifySlackSignature(headers, rawBody, secret, now = Date.now(), toleranceSec = 300) {
277
+ const ts = header(headers, 'x-slack-request-timestamp');
278
+ if (!ts || !/^\d+$/.test(ts))
279
+ return false;
280
+ if (Math.abs(Math.floor(now / 1000) - Number(ts)) > toleranceSec)
281
+ return false;
282
+ const received = header(headers, 'x-slack-signature');
283
+ const signature = received?.startsWith('v0=') ? received.slice('v0='.length) : undefined;
284
+ // Concatenate the base-string prefix with the raw body bytes so a non-ASCII
285
+ // payload hashes identically to Slack's own `v0:${ts}:${body}` string.
286
+ const base = Buffer.concat([Buffer.from(`v0:${ts}:`, 'utf-8'), rawBody]);
287
+ const expected = crypto.createHmac('sha256', secret).update(base).digest('hex');
288
+ return timingSafeHexEqual(signature, expected);
289
+ }
290
+ /**
291
+ * Parse a Slack delivery body into a normalized {@link SlackPayload}. Slack
292
+ * sends slash commands as `application/x-www-form-urlencoded` and Events API
293
+ * deliveries (including the `url_verification` handshake) as
294
+ * `application/json` — this is the one place that content-type branch lives.
295
+ */
296
+ export function parseSlackBody(contentType, rawBody) {
297
+ if ((contentType ?? '').includes('application/x-www-form-urlencoded')) {
298
+ const form = new URLSearchParams(rawBody.toString('utf-8'));
299
+ // A slash command carries no thread; its reply posts to the channel.
300
+ return {
301
+ type: 'slash_command',
302
+ command: form.get('command') ?? undefined,
303
+ text: form.get('text') ?? undefined,
304
+ channel: form.get('channel_id') ?? undefined,
305
+ user: form.get('user_id') ?? undefined,
306
+ response_url: form.get('response_url') ?? undefined,
307
+ team: form.get('team_id') ?? undefined,
308
+ };
309
+ }
310
+ const json = rawBody.length > 0 ? JSON.parse(rawBody.toString('utf-8')) : {};
311
+ const type = String(json.type ?? '');
312
+ if (type === 'url_verification') {
313
+ return { type, challenge: typeof json.challenge === 'string' ? json.challenge : '' };
314
+ }
315
+ const event = (json.event ?? {});
316
+ return {
317
+ type: type || 'event_callback',
318
+ event_id: typeof json.event_id === 'string' ? json.event_id : undefined,
319
+ event_type: typeof event.type === 'string' ? event.type : undefined,
320
+ text: typeof event.text === 'string' ? event.text : undefined,
321
+ channel: typeof event.channel === 'string' ? event.channel : undefined,
322
+ thread_ts: typeof event.thread_ts === 'string'
323
+ ? event.thread_ts
324
+ : typeof event.ts === 'string'
325
+ ? event.ts
326
+ : undefined,
327
+ user: typeof event.user === 'string' ? event.user : undefined,
328
+ team: typeof json.team_id === 'string' ? json.team_id : undefined,
329
+ };
330
+ }
331
+ /** The `event` name a Slack delivery fires under: the slash command, or the event subtype. */
332
+ export function slackEventName(payload) {
333
+ if (payload.type === 'slash_command')
334
+ return payload.command ?? 'slash_command';
335
+ return payload.event_type ?? payload.type;
336
+ }
267
337
  export function createMemoryDeliveryStore(maxEntries = 1000) {
268
338
  const seen = new Map();
269
339
  const touch = (id) => {
@@ -405,7 +475,7 @@ function deliveryId(source, headers, rawBody) {
405
475
  return `${source}:${named ?? crypto.createHash('sha256').update(rawBody).digest('hex')}`;
406
476
  }
407
477
  function sourceFromPath(pathname) {
408
- const match = /^\/hooks\/(github|linear)\/?$/.exec(pathname ?? '');
478
+ const match = /^\/hooks\/(github|linear|slack)\/?$/.exec(pathname ?? '');
409
479
  return match ? match[1] : null;
410
480
  }
411
481
  async function readRawBody(req, maxBytes) {
@@ -461,6 +531,11 @@ export function waitForListening(server) {
461
531
  * duplicate from an in-flight set, since `deliveryStore.seen` only reports
462
532
  * completed deliveries and would otherwise let a mid-flight retry double-fire.
463
533
  *
534
+ * `onMatch` fires as soon as matching is known, before any dispatch; `onDelivery`
535
+ * fires only once every matched routine/handler has settled — see their docs on
536
+ * `WebhookServerOptions`. A caller that wants a prompt "this fired" log uses
537
+ * `onMatch`, not `onDelivery` (RUSH-2722).
538
+ *
464
539
  * Returns the underlying server so callers can `close()` it.
465
540
  */
466
541
  export function startWebhookServer(options) {
@@ -482,8 +557,17 @@ export function startWebhookServer(options) {
482
557
  try {
483
558
  const context = buildWebhookContext(webhook);
484
559
  const fireOptions = options.fire ?? {};
560
+ // Match FIRST, before any dispatch — matching is a pure in-memory lookup,
561
+ // so this is the earliest point the receiver knows what fired, and the
562
+ // right time to log it (RUSH-2722). Dispatching a `run.command` handler
563
+ // can then block on the shelled-out process for minutes; that must never
564
+ // hold the "fired" log back with it.
565
+ const matchedJobs = matchJobsToWebhook(fireOptions.jobs ?? listJobs(), webhook);
566
+ const matchedHandlers = listHandlers().filter((handler) => handlerMatchesWebhook(handler, webhook));
567
+ options.onMatch?.(webhook, matchedJobs.map((job) => job.name), matchedHandlers.map((handler) => handler.name));
485
568
  const firedJobs = await fireWebhookJobs(webhook, {
486
569
  ...fireOptions,
570
+ jobs: matchedJobs,
487
571
  context,
488
572
  skipJobNames: deliveryStore.completedJobs(id),
489
573
  onJobFired: (job, firedJob) => {
@@ -492,7 +576,6 @@ export function startWebhookServer(options) {
492
576
  fireOptions.onJobFired?.(job, firedJob);
493
577
  },
494
578
  });
495
- const matchedHandlers = listHandlers().filter((handler) => handlerMatchesWebhook(handler, webhook));
496
579
  const firedHandlers = [];
497
580
  const handlerErrors = [];
498
581
  await Promise.allSettled(matchedHandlers.map(async (handler) => {
@@ -563,13 +646,54 @@ export function startWebhookServer(options) {
563
646
  const rawBody = await readRawBody(req, maxBodyBytes);
564
647
  const valid = source === 'github'
565
648
  ? verifyGithubSignature(req.headers, rawBody, secret)
566
- : verifyLinearSignature(req.headers, rawBody, secret);
649
+ : source === 'linear'
650
+ ? verifyLinearSignature(req.headers, rawBody, secret)
651
+ : verifySlackSignature(req.headers, rawBody, secret);
567
652
  if (!valid) {
568
653
  emit('webhook.rejected', { source, reason: 'invalid signature' });
569
654
  res.writeHead(401, { 'content-type': 'application/json' });
570
655
  res.end(JSON.stringify({ ok: false, error: 'invalid signature' }));
571
656
  return;
572
657
  }
658
+ // Slack diverges from the GitHub/Linear JSON path: two content-types
659
+ // (slash = form, events = JSON), a one-time url_verification handshake,
660
+ // a body-carried event id, and a 200 ack Slack shows to the caller.
661
+ if (source === 'slack') {
662
+ const slack = parseSlackBody(header(req.headers, 'content-type'), rawBody);
663
+ // One-time Events API URL handshake: echo the challenge, dispatch nothing.
664
+ if (slack.type === 'url_verification') {
665
+ res.writeHead(200, { 'content-type': 'application/json' });
666
+ res.end(JSON.stringify({ challenge: slack.challenge ?? '' }));
667
+ return;
668
+ }
669
+ const slackEvent = slackEventName(slack);
670
+ // Slash commands carry no event_id, so fall back to a body hash for dedup.
671
+ const slackId = `slack:${slack.event_id ?? crypto.createHash('sha256').update(rawBody).digest('hex')}`;
672
+ emit('webhook.received', { source, event: slackEvent, deliveryId: slackId });
673
+ if (deliveryStore.seen(slackId) || inFlight.has(slackId)) {
674
+ res.writeHead(200, { 'content-type': 'application/json' });
675
+ res.end(JSON.stringify({ ok: true, duplicate: true }));
676
+ return;
677
+ }
678
+ if (!rateLimiter.take(source)) {
679
+ emit('webhook.rejected', { source, event: slackEvent, deliveryId: slackId, reason: 'rate limit exceeded' });
680
+ res.writeHead(429, { 'content-type': 'application/json' });
681
+ res.end(JSON.stringify({ ok: false, error: 'rate limit exceeded' }));
682
+ return;
683
+ }
684
+ const slackWebhook = { source, event: slackEvent, payload: slack };
685
+ emit('webhook.authorized', { source, event: slackEvent, deliveryId: slackId });
686
+ // ACK FIRST (RUSH-2548): a 200 inside Slack's 3s window. A slash
687
+ // command renders the ack text to the caller; an event delivery
688
+ // ignores the body. Dispatch outlives the ack, same as GitHub/Linear.
689
+ inFlight.add(slackId);
690
+ res.writeHead(200, { 'content-type': 'application/json' });
691
+ res.end(slack.type === 'slash_command'
692
+ ? JSON.stringify({ response_type: 'ephemeral', text: 'On it — replying in this thread.' })
693
+ : '');
694
+ void settleDelivery(slackWebhook, slackId).finally(() => inFlight.delete(slackId));
695
+ return;
696
+ }
573
697
  const id = deliveryId(source, req.headers, rawBody);
574
698
  const event = source === 'github' ? (header(req.headers, 'x-github-event') ?? '') : '';
575
699
  emit('webhook.received', { source, event, deliveryId: id });
@@ -1110,7 +1110,7 @@ export interface HostEntry {
1110
1110
  /** Browser profile definition stored in agents.yaml. */
1111
1111
  export interface BrowserProfileConfig {
1112
1112
  description?: string;
1113
- browser: 'chrome' | 'comet' | 'chromium' | 'brave' | 'edge' | 'custom';
1113
+ browser: 'chrome' | 'comet' | 'chromium' | 'brave' | 'edge' | 'arc' | 'custom';
1114
1114
  binary?: string;
1115
1115
  electron?: boolean;
1116
1116
  /**
@@ -19,7 +19,7 @@ import * as fs from 'fs';
19
19
  import * as os from 'os';
20
20
  import * as path from 'path';
21
21
  import { AGENTS, ALL_AGENT_IDS } from './agents.js';
22
- import { getAgentConfigPath, getConfigSymlinkVersion, stripShimPathLines, releaseAdoptedLauncher, } from './shims.js';
22
+ import { getAgentConfigPath, getConfigSymlinkVersion, stripShimPathLines, releaseAdoptedLauncher, } from './installations/shims.js';
23
23
  import { moveDirCrossDevice, copyDirStrippingAgentsSymlinks } from './config-transfer.js';
24
24
  import { getUserAgentsDir, getBackupsDir, getHistoryDir, getShimsDir, getLegacySystemAgentsDir, } from './state.js';
25
25
  /** Home dir honoring the AGENTS_REAL_HOME test/override, mirroring shims.ts. */
@@ -43,8 +43,15 @@ export declare function resolveInstalledLayout(): InstallLayout | null;
43
43
  * binary can't read its own bundled package.json), fall back to the on-disk
44
44
  * install found via the launcher symlink so callers like the menu bar don't see
45
45
  * a bogus `unknown`.
46
+ *
47
+ * `pkgJsonPath` is an optional override of the shipping package.json path,
48
+ * accepted by both this function and {@link getCliVersionFresh}. Every
49
+ * production call site uses the zero-argument form and gets the real on-disk
50
+ * path below; the parameter exists only so a unit test can point both
51
+ * functions at a fixture package.json instead of the real one shared by every
52
+ * parallel test fork.
46
53
  */
47
- export declare function getCliVersion(): string;
54
+ export declare function getCliVersion(pkgJsonPath?: string): string;
48
55
  /**
49
56
  * Read the version from package.json on disk every call, bypassing the cache.
50
57
  *
@@ -55,4 +62,4 @@ export declare function getCliVersion(): string;
55
62
  * is now stale and should reload onto the new code (self-healing). Returns
56
63
  * 'unknown' on any error.
57
64
  */
58
- export declare function getCliVersionFresh(): string;
65
+ export declare function getCliVersionFresh(pkgJsonPath?: string): string;
@@ -91,14 +91,18 @@ export function resolveInstalledLayout() {
91
91
  * binary can't read its own bundled package.json), fall back to the on-disk
92
92
  * install found via the launcher symlink so callers like the menu bar don't see
93
93
  * a bogus `unknown`.
94
+ *
95
+ * `pkgJsonPath` is an optional override of the shipping package.json path,
96
+ * accepted by both this function and {@link getCliVersionFresh}. Every
97
+ * production call site uses the zero-argument form and gets the real on-disk
98
+ * path below; the parameter exists only so a unit test can point both
99
+ * functions at a fixture package.json instead of the real one shared by every
100
+ * parallel test fork.
94
101
  */
95
- export function getCliVersion() {
102
+ export function getCliVersion(pkgJsonPath = path.join(__dirname, '..', '..', 'package.json')) {
96
103
  if (cached)
97
104
  return cached;
98
- cached =
99
- readVersionAt(path.join(__dirname, '..', '..', 'package.json')) ??
100
- readInstalledPackageVersion() ??
101
- 'unknown';
105
+ cached = readVersionAt(pkgJsonPath) ?? readInstalledPackageVersion() ?? 'unknown';
102
106
  return cached;
103
107
  }
104
108
  /** Version from the on-disk install (Bun-binary fallback). */
@@ -116,8 +120,6 @@ function readInstalledPackageVersion() {
116
120
  * is now stale and should reload onto the new code (self-healing). Returns
117
121
  * 'unknown' on any error.
118
122
  */
119
- export function getCliVersionFresh() {
120
- return (readVersionAt(path.join(__dirname, '..', '..', 'package.json')) ??
121
- readInstalledPackageVersion() ??
122
- 'unknown');
123
+ export function getCliVersionFresh(pkgJsonPath = path.join(__dirname, '..', '..', 'package.json')) {
124
+ return readVersionAt(pkgJsonPath) ?? readInstalledPackageVersion() ?? 'unknown';
123
125
  }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Terminal word-wrap — one shared helper for the whole CLI.
3
+ *
4
+ * Multi-line word wrapping is the gap the single-line helpers in
5
+ * `session/width.ts` (`truncateToWidth`, `padToWidth`) do not fill. Anywhere that
6
+ * renders variable-length text into a fixed-width terminal wraps through THIS,
7
+ * never a per-field slice.
8
+ *
9
+ * Width is measured with `stringWidth` (from the canonical `session/width.ts`),
10
+ * NOT `String.length`: the text this wraps is routinely ANSI-coloured (chalk /
11
+ * marked-terminal output) and may carry OSC-8 hyperlinks, where `.length`
12
+ * over-counts the invisible escape bytes. Words are split on whitespace, so a
13
+ * wrap boundary always falls between escape sequences, never inside one.
14
+ */
15
+ /**
16
+ * Hard-wrap `text` to `cols` visible columns.
17
+ *
18
+ * - Splits on existing newlines first (each paragraph wraps independently).
19
+ * - Wraps on whitespace; a single word wider than the available width is emitted
20
+ * on its own line rather than split mid-word (and never mid-escape).
21
+ * - Width is the visible display width via `stringWidth`, so ANSI colour and
22
+ * OSC-8 hyperlink escapes do not inflate the count.
23
+ * - `hangingIndent` pads every line AFTER the first with that many spaces, so a
24
+ * labelled value ("Latest <text>") keeps its continuation lines aligned under
25
+ * the value. The wrapping width is `cols - indent`.
26
+ * - Floor: when `hangingIndent` is set, the width is floored at 8 so a
27
+ * pathologically large indent still makes forward progress. With no indent the
28
+ * floor is 1, so a legitimately small `cols` is respected, never overridden. A
29
+ * non-finite `cols` degrades to the floor rather than silently disabling wrap.
30
+ *
31
+ * Returns the wrapped lines (never mutates input). An empty string yields [''].
32
+ */
33
+ export declare function wrapToWidth(text: string, cols: number, hangingIndent?: number): string[];
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Terminal word-wrap — one shared helper for the whole CLI.
3
+ *
4
+ * Multi-line word wrapping is the gap the single-line helpers in
5
+ * `session/width.ts` (`truncateToWidth`, `padToWidth`) do not fill. Anywhere that
6
+ * renders variable-length text into a fixed-width terminal wraps through THIS,
7
+ * never a per-field slice.
8
+ *
9
+ * Width is measured with `stringWidth` (from the canonical `session/width.ts`),
10
+ * NOT `String.length`: the text this wraps is routinely ANSI-coloured (chalk /
11
+ * marked-terminal output) and may carry OSC-8 hyperlinks, where `.length`
12
+ * over-counts the invisible escape bytes. Words are split on whitespace, so a
13
+ * wrap boundary always falls between escape sequences, never inside one.
14
+ */
15
+ import { stringWidth } from './session/width.js';
16
+ /**
17
+ * Hard-wrap `text` to `cols` visible columns.
18
+ *
19
+ * - Splits on existing newlines first (each paragraph wraps independently).
20
+ * - Wraps on whitespace; a single word wider than the available width is emitted
21
+ * on its own line rather than split mid-word (and never mid-escape).
22
+ * - Width is the visible display width via `stringWidth`, so ANSI colour and
23
+ * OSC-8 hyperlink escapes do not inflate the count.
24
+ * - `hangingIndent` pads every line AFTER the first with that many spaces, so a
25
+ * labelled value ("Latest <text>") keeps its continuation lines aligned under
26
+ * the value. The wrapping width is `cols - indent`.
27
+ * - Floor: when `hangingIndent` is set, the width is floored at 8 so a
28
+ * pathologically large indent still makes forward progress. With no indent the
29
+ * floor is 1, so a legitimately small `cols` is respected, never overridden. A
30
+ * non-finite `cols` degrades to the floor rather than silently disabling wrap.
31
+ *
32
+ * Returns the wrapped lines (never mutates input). An empty string yields [''].
33
+ */
34
+ export function wrapToWidth(text, cols, hangingIndent = 0) {
35
+ const indent = Math.max(0, Math.trunc(hangingIndent));
36
+ const floor = indent > 0 ? 8 : 1;
37
+ const width = Number.isFinite(cols) ? Math.max(floor, cols - indent) : floor;
38
+ const pad = ' '.repeat(indent);
39
+ const out = [];
40
+ for (const para of String(text).split('\n')) {
41
+ const words = para.split(/\s+/).filter(w => w.length > 0);
42
+ if (words.length === 0) {
43
+ out.push('');
44
+ continue;
45
+ }
46
+ let line = '';
47
+ let lineWidth = 0;
48
+ for (const word of words) {
49
+ const wordWidth = stringWidth(word);
50
+ if (line === '') {
51
+ line = word;
52
+ lineWidth = wordWidth;
53
+ }
54
+ else if (lineWidth + 1 + wordWidth > width) {
55
+ out.push(line);
56
+ line = word;
57
+ lineWidth = wordWidth;
58
+ }
59
+ else {
60
+ line = `${line} ${word}`;
61
+ lineWidth += 1 + wordWidth;
62
+ }
63
+ }
64
+ if (line !== '')
65
+ out.push(line);
66
+ }
67
+ if (out.length === 0)
68
+ out.push('');
69
+ return out.map((l, i) => (i === 0 ? l : pad + l));
70
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phnx-labs/agents-cli",
3
- "version": "1.22.40",
3
+ "version": "1.22.42",
4
4
  "description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",