@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.
- package/CHANGELOG.md +36 -0
- package/README.md +14 -3
- package/dist/bin/agents +0 -0
- package/dist/bootstrap.js +0 -1
- package/dist/cli/command-registry.d.ts +2 -0
- package/dist/cli/command-registry.js +5 -2
- package/dist/commands/accounts.d.ts +29 -0
- package/dist/commands/accounts.js +220 -29
- package/dist/commands/alias.js +1 -1
- package/dist/commands/auth.d.ts +9 -0
- package/dist/commands/auth.js +104 -0
- package/dist/commands/browser-sessions-picker.js +42 -62
- package/dist/commands/browser.d.ts +15 -0
- package/dist/commands/browser.js +145 -78
- package/dist/commands/computer-actions.d.ts +1 -1
- package/dist/commands/computer-actions.js +1 -1
- package/dist/commands/computer-sessions-picker.js +32 -55
- package/dist/commands/computer.d.ts +1 -1
- package/dist/commands/computer.js +2 -2
- package/dist/commands/doctor.d.ts +1 -1
- package/dist/commands/doctor.js +3 -3
- package/dist/commands/feed.js +4 -12
- package/dist/commands/hooks.js +1 -1
- package/dist/commands/import.js +1 -1
- package/dist/commands/insights.js +163 -85
- package/dist/commands/inspect.js +2 -2
- package/dist/commands/mine.js +1 -1
- package/dist/commands/org.d.ts +6 -0
- package/dist/commands/org.js +174 -0
- package/dist/commands/packages.js +1 -1
- package/dist/commands/prune.js +1 -1
- package/dist/commands/routines.js +1 -1
- package/dist/commands/run-account-picker.d.ts +19 -0
- package/dist/commands/run-account-picker.js +81 -0
- package/dist/commands/secrets.js +87 -97
- package/dist/commands/sessions-picker-factory.d.ts +40 -0
- package/dist/commands/sessions-picker-factory.js +67 -0
- package/dist/commands/sessions-picker.d.ts +1 -0
- package/dist/commands/sessions-picker.js +27 -5
- package/dist/commands/sessions-share.d.ts +25 -0
- package/dist/commands/sessions-share.js +166 -0
- package/dist/commands/sessions.js +2 -0
- package/dist/commands/setup-browser.js +5 -5
- package/dist/commands/setup-mine.js +1 -1
- package/dist/commands/setup-preferences.js +4 -4
- package/dist/commands/setup.js +5 -3
- package/dist/commands/sync.js +1 -1
- package/dist/commands/versions.js +1 -1
- package/dist/commands/view.js +1 -1
- package/dist/commands/webhook.js +14 -9
- package/dist/lib/account-capabilities.d.ts +10 -0
- package/dist/lib/account-capabilities.js +34 -0
- package/dist/lib/agent-spec/agents.d.ts +584 -0
- package/dist/lib/agent-spec/agents.js +3127 -0
- package/dist/lib/agent-spec/provider.js +1 -1
- package/dist/lib/agents.d.ts +2 -584
- package/dist/lib/agents.js +2 -3127
- package/dist/lib/browser/cdp.js +4 -0
- package/dist/lib/browser/chrome.js +12 -1
- package/dist/lib/browser/drivers/local.d.ts +8 -2
- package/dist/lib/browser/drivers/local.js +8 -2
- package/dist/lib/browser/drivers/ssh.d.ts +8 -2
- package/dist/lib/browser/drivers/ssh.js +11 -4
- package/dist/lib/browser/hygiene.d.ts +1 -1
- package/dist/lib/browser/hygiene.js +5 -1
- package/dist/lib/browser/profiles.d.ts +77 -4
- package/dist/lib/browser/profiles.js +121 -12
- package/dist/lib/browser/runtime-state.d.ts +11 -5
- package/dist/lib/browser/runtime-state.js +13 -9
- package/dist/lib/browser/service.d.ts +91 -30
- package/dist/lib/browser/service.js +342 -207
- package/dist/lib/browser/types.d.ts +96 -4
- package/dist/lib/browser/types.js +51 -0
- package/dist/lib/catchup.js +1 -1
- package/dist/lib/computer/actions.d.ts +1 -1
- package/dist/lib/computer/actions.js +1 -1
- package/dist/lib/{computer-rpc.js → computer/computer-rpc.js} +5 -5
- package/dist/lib/computer/dispatch.d.ts +1 -1
- package/dist/lib/computer/download.d.ts +1 -1
- package/dist/lib/computer/download.js +3 -3
- package/dist/lib/{ssh-tunnel.d.ts → computer/ssh-tunnel.d.ts} +1 -1
- package/dist/lib/{ssh-tunnel.js → computer/ssh-tunnel.js} +12 -12
- package/dist/lib/daemon/daemon.js +1 -1
- package/dist/lib/{runner.d.ts → daemon/runner.d.ts} +10 -10
- package/dist/lib/{runner.js → daemon/runner.js} +59 -118
- package/dist/lib/daemon-webhooks.d.ts +3 -1
- package/dist/lib/daemon-webhooks.js +12 -7
- package/dist/lib/device-config.js +1 -1
- package/dist/lib/devices/doctor-findings.d.ts +1 -1
- package/dist/lib/devices/doctor-findings.js +3 -3
- package/dist/lib/devices/fleet-inventory.js +1 -1
- package/dist/lib/devices/harness-inventory.js +1 -1
- package/dist/lib/devices/registry.js +4 -22
- package/dist/lib/doctor-diff.d.ts +1 -1
- package/dist/lib/doctor-diff.js +1 -1
- package/dist/lib/drift.js +1 -1
- package/dist/lib/entitlement.d.ts +31 -0
- package/dist/lib/entitlement.js +137 -0
- package/dist/lib/exec.js +39 -202
- package/dist/lib/feed/feed.js +2 -8
- package/dist/lib/fs-atomic.d.ts +7 -0
- package/dist/lib/fs-atomic.js +9 -0
- package/dist/lib/harness/adapter.d.ts +160 -0
- package/dist/lib/harness/adapter.js +47 -0
- package/dist/lib/harness/adapters/claude.d.ts +2 -0
- package/dist/lib/harness/adapters/claude.js +110 -0
- package/dist/lib/harness/adapters/codex.d.ts +2 -0
- package/dist/lib/harness/adapters/codex.js +83 -0
- package/dist/lib/harness/adapters/copilot.d.ts +2 -0
- package/dist/lib/harness/adapters/copilot.js +23 -0
- package/dist/lib/harness/adapters/cursor.d.ts +2 -0
- package/dist/lib/harness/adapters/cursor.js +38 -0
- package/dist/lib/harness/adapters/droid.d.ts +2 -0
- package/dist/lib/harness/adapters/droid.js +17 -0
- package/dist/lib/harness/adapters/grok.d.ts +2 -0
- package/dist/lib/harness/adapters/grok.js +14 -0
- package/dist/lib/harness/adapters/kimi.d.ts +2 -0
- package/dist/lib/harness/adapters/kimi.js +49 -0
- package/dist/lib/harness/adapters/muse.d.ts +2 -0
- package/dist/lib/harness/adapters/muse.js +44 -0
- package/dist/lib/harness/adapters/opencode.d.ts +2 -0
- package/dist/lib/harness/adapters/opencode.js +14 -0
- package/dist/lib/harness/exec-config-version.d.ts +8 -0
- package/dist/lib/harness/exec-config-version.js +22 -0
- package/dist/lib/harness/index.d.ts +1 -0
- package/dist/lib/harness/index.js +34 -0
- package/dist/lib/{hooks.d.ts → hooks/install.d.ts} +1 -1
- package/dist/lib/{hooks.js → hooks/install.js} +11 -11
- package/dist/lib/import.js +1 -1
- package/dist/lib/installations/index.d.ts +4 -4
- package/dist/lib/installations/index.js +3 -4
- package/dist/lib/installations/migrate.js +6 -6
- package/dist/lib/{shims.d.ts → installations/shims.d.ts} +2 -2
- package/dist/lib/{shims.js → installations/shims.js} +12 -114
- package/dist/lib/installations/store.d.ts +141 -3
- package/dist/lib/installations/store.js +475 -5
- package/dist/lib/installations/versions.d.ts +1 -126
- package/dist/lib/installations/versions.js +5 -465
- package/dist/lib/isolation-boundary-report.d.ts +1 -1
- package/dist/lib/isolation-boundary-report.js +1 -1
- package/dist/lib/mailbox-gc.js +2 -3
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/monitors/dispatch.d.ts +1 -1
- package/dist/lib/monitors/dispatch.js +2 -2
- package/dist/lib/monitors/state.d.ts +1 -1
- package/dist/lib/observe-aliases.d.ts +2 -2
- package/dist/lib/observe-aliases.js +2 -11
- package/dist/lib/plugins/plugins.js +1 -1
- package/dist/lib/plugins/skills.js +1 -1
- package/dist/lib/prix-account.d.ts +158 -0
- package/dist/lib/prix-account.js +214 -0
- package/dist/lib/project-key.js +7 -0
- package/dist/lib/project-launch.js +1 -1
- package/dist/lib/refresh.js +2 -2
- package/dist/lib/resource-inventory.d.ts +1 -1
- package/dist/lib/resource-inventory.js +1 -1
- package/dist/lib/rules/rules.js +1 -1
- package/dist/lib/scheduling/routines.js +1 -1
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/bundles.d.ts +1 -1
- package/dist/lib/secrets/bundles.js +1 -1
- package/dist/lib/secrets/headless.d.ts +16 -0
- package/dist/lib/secrets/headless.js +21 -0
- package/dist/lib/secrets/remote.d.ts +9 -7
- package/dist/lib/secrets/remote.js +18 -9
- package/dist/lib/self-heal/checks/hook-runtime.js +1 -1
- package/dist/lib/self-heal/checks/path.js +1 -1
- package/dist/lib/self-heal/checks/shadowing.js +1 -1
- package/dist/lib/self-heal/checks/shims.js +1 -1
- package/dist/lib/session/discover.d.ts +39 -0
- package/dist/lib/session/discover.js +3 -3
- package/dist/lib/session/parse.js +24 -42
- package/dist/lib/session/recovery.js +1 -1
- package/dist/lib/session/share-html.d.ts +55 -0
- package/dist/lib/session/share-html.js +319 -0
- package/dist/lib/settings-manifest.d.ts +2 -0
- package/dist/lib/settings-manifest.js +81 -3
- package/dist/lib/share/publish.d.ts +38 -0
- package/dist/lib/share/publish.js +81 -8
- package/dist/lib/staleness/prune.d.ts +1 -1
- package/dist/lib/staleness/prune.js +1 -1
- package/dist/lib/staleness/writers/hooks.js +1 -1
- package/dist/lib/startup/command-registry.d.ts +2 -1
- package/dist/lib/startup/command-registry.js +5 -3
- package/dist/lib/teams/agents.js +2 -19
- package/dist/lib/teams/parsers.js +20 -26
- package/dist/lib/teams/registry.js +2 -21
- package/dist/lib/triggers/handlers.d.ts +37 -2
- package/dist/lib/triggers/handlers.js +57 -6
- package/dist/lib/triggers/webhook.d.ts +80 -6
- package/dist/lib/triggers/webhook.js +128 -4
- package/dist/lib/types.d.ts +1 -1
- package/dist/lib/uninstall.js +1 -1
- package/dist/lib/version.d.ts +9 -2
- package/dist/lib/version.js +11 -9
- package/dist/lib/wrap.d.ts +33 -0
- package/dist/lib/wrap.js +70 -0
- package/package.json +1 -1
- /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
|
-
...(
|
|
363
|
-
...(
|
|
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
|
-
...(
|
|
389
|
-
...(
|
|
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
|
|
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
|
|
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
|
|
146
|
-
* dispatch (see `startWebhookServer`),
|
|
147
|
-
* has been written, and is the only
|
|
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
|
-
:
|
|
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 });
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -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
|
/**
|
package/dist/lib/uninstall.js
CHANGED
|
@@ -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. */
|
package/dist/lib/version.d.ts
CHANGED
|
@@ -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;
|
package/dist/lib/version.js
CHANGED
|
@@ -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
|
|
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[];
|
package/dist/lib/wrap.js
ADDED
|
@@ -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.
|
|
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",
|
|
File without changes
|