experimental-ash 0.33.1 → 0.35.0

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 (64) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/docs/public/auth-and-route-protection.md +27 -7
  3. package/dist/docs/public/channels/README.md +7 -3
  4. package/dist/docs/public/channels/slack.md +10 -4
  5. package/dist/docs/public/sandbox.md +42 -19
  6. package/dist/docs/public/session-context.md +1 -1
  7. package/dist/src/cli/commands/channel-add-conflicts.d.ts +21 -0
  8. package/dist/src/cli/commands/channel-add-conflicts.js +1 -0
  9. package/dist/src/cli/commands/channels.d.ts +9 -1
  10. package/dist/src/cli/commands/channels.js +1 -3
  11. package/dist/src/cli/dev/repl.js +1 -1
  12. package/dist/src/cli/run.js +1 -1
  13. package/dist/src/compiled/.vendor-stamp.json +2 -2
  14. package/dist/src/compiled/@vercel/sandbox/index.d.ts +11 -2
  15. package/dist/src/compiled/@vercel/sandbox/index.js +3 -3
  16. package/dist/src/compiled/@vercel/sandbox/package.json +1 -1
  17. package/dist/src/compiled/_chunks/node/{auth-ZhCJAHxl.js → auth-CVVvWjaK.js} +1 -1
  18. package/dist/src/compiled/_chunks/node/{version-D4IYmfaS.js → version-nR4RSpFw.js} +1 -1
  19. package/dist/src/compiler/compile-agent.js +1 -1
  20. package/dist/src/compiler/normalize-manifest.js +1 -1
  21. package/dist/src/execution/sandbox/bindings/vercel.d.ts +1 -1
  22. package/dist/src/execution/sandbox/session.js +1 -1
  23. package/dist/src/internal/application/package.js +1 -1
  24. package/dist/src/internal/logging.js +1 -1
  25. package/dist/src/internal/nitro/host/start-production-server.js +1 -1
  26. package/dist/src/node_modules/.pnpm/@clack_core@1.3.1/node_modules/@clack/core/dist/index.js +10 -0
  27. package/dist/src/node_modules/.pnpm/fast-string-truncated-width@3.0.3/node_modules/fast-string-truncated-width/dist/index.js +1 -0
  28. package/dist/src/node_modules/.pnpm/fast-string-truncated-width@3.0.3/node_modules/fast-string-truncated-width/dist/utils.js +1 -0
  29. package/dist/src/node_modules/.pnpm/fast-string-width@3.0.2/node_modules/fast-string-width/dist/index.js +1 -0
  30. package/dist/src/node_modules/.pnpm/fast-wrap-ansi@0.2.2/node_modules/fast-wrap-ansi/lib/main.js +5 -0
  31. package/dist/src/node_modules/.pnpm/picocolors@1.1.1/node_modules/picocolors/picocolors.js +1 -0
  32. package/dist/src/node_modules/.pnpm/sisteransi@1.0.5/node_modules/sisteransi/src/index.js +1 -0
  33. package/dist/src/packages/ash-scaffold/src/channels.js +12 -2
  34. package/dist/src/packages/ash-scaffold/src/cli/channel-add-prompter.js +1 -0
  35. package/dist/src/packages/ash-scaffold/src/cli/channel-setup-prompter.js +1 -0
  36. package/dist/src/packages/ash-scaffold/src/cli/command-output.js +1 -0
  37. package/dist/src/packages/ash-scaffold/src/cli/index.js +1 -0
  38. package/dist/src/packages/ash-scaffold/src/cli/prompt-ui.js +3 -0
  39. package/dist/src/packages/ash-scaffold/src/cli/rail-log.js +2 -0
  40. package/dist/src/packages/ash-scaffold/src/primitives/detect-deployment.js +1 -0
  41. package/dist/src/packages/ash-scaffold/src/primitives/index.js +1 -0
  42. package/dist/src/packages/ash-scaffold/src/primitives/pnpm-invocation.js +1 -0
  43. package/dist/src/packages/ash-scaffold/src/primitives/process-output.js +1 -0
  44. package/dist/src/packages/ash-scaffold/src/primitives/run-pnpm.js +1 -0
  45. package/dist/src/packages/ash-scaffold/src/primitives/run-vercel.js +1 -0
  46. package/dist/src/packages/ash-scaffold/src/primitives/update-slack-channel.js +1 -0
  47. package/dist/src/packages/ash-scaffold/src/project.js +1 -1
  48. package/dist/src/packages/ash-scaffold/src/steps/deploy-to-vercel.js +1 -0
  49. package/dist/src/packages/ash-scaffold/src/steps/index.js +1 -0
  50. package/dist/src/packages/ash-scaffold/src/steps/run-add-to-agent.js +2 -0
  51. package/dist/src/packages/ash-scaffold/src/steps/setup-slackbot.js +1 -0
  52. package/dist/src/packages/ash-scaffold/src/web-template.js +4713 -0
  53. package/dist/src/public/channels/auth.d.ts +22 -11
  54. package/dist/src/public/channels/auth.js +1 -1
  55. package/dist/src/public/definitions/sandbox.d.ts +1 -1
  56. package/dist/src/public/next/server.js +1 -1
  57. package/dist/src/public/sandbox/index.d.ts +1 -1
  58. package/dist/src/public/sandbox/vercel-sandbox.d.ts +4 -4
  59. package/dist/src/runtime/governance/auth/oidc.js +1 -1
  60. package/dist/src/runtime/governance/auth/token-claims.d.ts +2 -0
  61. package/dist/src/runtime/governance/auth/token-claims.js +1 -1
  62. package/dist/src/runtime/governance/auth/types.d.ts +6 -0
  63. package/dist/src/shared/sandbox-session.d.ts +0 -17
  64. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # experimental-ash
2
2
 
3
+ ## 0.35.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 24aa0cd: `localDev()` now also grants the local-dev session when running under `vercel dev` (`VERCEL=1` and `VERCEL_ENV=development`), not just loopback requests, so the local Vercel dev server can reach the agent without an OIDC token. `ASH_LOG_LEVEL` now defaults to `info` in every environment (previously `debug` outside production), making `debug` opt-in so dev output is no longer flooded with best-effort lines.
8
+ - a59d91c: Remove deprecated `SandboxSession.runCommand` alias and `SandboxRunCommandOptions` type. Use `sandbox.run({ command })` and `SandboxRunOptions` instead.
9
+ - 083c20a: `vercelOidc()` now accepts Vercel OIDC tokens with `external_sub` as user principals when the token matches the configured Vercel project and deployment environment. The resulting session auth uses `external_sub` as the subject, prefers `external_iss` / `connector_id` as the issuer, and exposes string OIDC profile claims such as `name`, `picture`, and `email` as attributes.
10
+
11
+ ### Patch Changes
12
+
13
+ - 321bae2: Upgrade `@vercel/sandbox` from 2.0.0 to 2.0.1. Sessions are now persistent by default and auto-resume on `Sandbox.get()`. Vendored types updated with `resume`, `onResume`, and `readFile`.
14
+
15
+ ## 0.34.0
16
+
17
+ ### Minor Changes
18
+
19
+ - 6b6e55e: Add Web/Slack channel setup backed by the Next AI Elements Web Chat scaffold, with Vercel/pnpm metadata and a skip for existing Next.js apps.
20
+
3
21
  ## 0.33.1
4
22
 
5
23
  ### Patch Changes
@@ -15,23 +15,34 @@ These settings apply to:
15
15
  - `POST /ash/v1/session/:sessionId`
16
16
  - `GET /ash/v1/session/:sessionId/stream`
17
17
 
18
- ## The Default
18
+ ## Generated Web Chat Auth
19
19
 
20
- `pnpm create experimental-ash-agent` scaffolds `agent/channels/ash.ts` with the same default chain
21
- the framework applies when no file exists:
20
+ `pnpm create experimental-ash-agent` scaffolds `agent/channels/ash.ts` from the Web Chat example.
21
+ It permits Vercel OIDC and localhost requests and leaves end-user production auth as an explicit
22
+ placeholder:
22
23
 
23
24
  ```ts
24
25
  // agent/channels/ash.ts
25
26
  import { ashChannel } from "experimental-ash/channels/ash";
26
- import { localDev, vercelOidc } from "experimental-ash/channels/auth";
27
+ import { type AuthFn, localDev, vercelOidc } from "experimental-ash/channels/auth";
28
+
29
+ function exampleProductionAuth(): AuthFn<Request> {
30
+ return () => {
31
+ if (process.env.VERCEL_ENV === "production") {
32
+ throw new Error("Configure production auth in agent/channels/ash.ts.");
33
+ }
34
+ return null;
35
+ };
36
+ }
27
37
 
28
38
  export default ashChannel({
29
- auth: [localDev(), vercelOidc()],
39
+ auth: [vercelOidc(), localDev(), exampleProductionAuth()],
30
40
  });
31
41
  ```
32
42
 
33
- Deleting the file restores the framework default the array above is the framework default, just
34
- written out so you can see and edit it.
43
+ Replace `exampleProductionAuth()` before a browser user submits a production request. If you
44
+ delete the authored file, Ash falls back to its framework default `[localDev(), vercelOidc()]`;
45
+ that default also does not accept browser-user traffic in production.
35
46
 
36
47
  ## Walking The Auth Array
37
48
 
@@ -145,6 +156,15 @@ Use this for the common Vercel deployment path. Verifies a bearer JWT against th
145
156
  issuer; tokens minted for the current `VERCEL_PROJECT_ID` are always accepted (so internal
146
157
  subagent / runtime callers authenticate without configuration).
147
158
 
159
+ It accepts Vercel OIDC bearer tokens after signature, issuer, audience, and time-claim verification.
160
+ Tokens for the current `VERCEL_PROJECT_ID` authenticate as runtime/service callers. Tokens with
161
+ `external_sub` authenticate as user callers when their `project_id` matches `VERCEL_PROJECT_ID` if
162
+ configured, and their `environment` matches `VERCEL_TARGET_ENV` or `VERCEL_ENV` if configured. For
163
+ those user tokens, `external_sub` becomes the session subject, `external_iss` becomes the session
164
+ issuer when present, and `connector_id` is used as the issuer fallback before the Vercel OIDC issuer.
165
+ String OIDC profile claims such as `name`, `picture`, and `email` are exposed in
166
+ `getSession().auth.current.attributes`.
167
+
148
168
  ### `none`
149
169
 
150
170
  Returns a synthetic anonymous `SessionAuthContext`. Use as the final entry in `auth` to accept
@@ -111,9 +111,13 @@ export default ashChannel({
111
111
  });
112
112
  ```
113
113
 
114
- `pnpm create experimental-ash-agent` scaffolds exactly this file at `agent/channels/ash.ts`
115
- deleting it restores the same default. See [Auth and Route Protection](../auth-and-route-protection.md)
116
- for the full walking semantics and helper reference.
114
+ `pnpm create experimental-ash-agent` scaffolds the Web Chat example channel at
115
+ `agent/channels/ash.ts`. It permits Vercel OIDC and localhost requests and includes an
116
+ `exampleProductionAuth()` placeholder that throws in production until you replace it
117
+ with end-user auth. If you delete the authored file, Ash falls back to
118
+ `[localDev(), vercelOidc()]`; that default does not admit browser users in production.
119
+ See [Auth and Route Protection](../auth-and-route-protection.md) for the full walking
120
+ semantics and helper reference.
117
121
 
118
122
  ## Slack Channels
119
123
 
@@ -72,7 +72,7 @@ npx skills add https://github.com/vercel/connect --skill vercel-connect
72
72
  ```
73
73
 
74
74
  Then create the Slack client from the project or agent folder that will use it, and attach this
75
- project as the trigger destination so Slack events are forwarded to your deployment:
75
+ project as the trigger destination at the route Ash serves:
76
76
 
77
77
  ```bash
78
78
  vercel connect create slack --triggers
@@ -85,9 +85,9 @@ but Slack never delivers `app_mention` or `message.im` events to the Connect web
85
85
  step also auto-attaches the linked project at Connect's default trigger path, so detach it before
86
86
  attaching the Ash path. `attach --triggers` then registers the currently-linked Vercel project as
87
87
  the destination Connect forwards verified webhooks to. The `--trigger-path /ash/v1/slack` value
88
- must match Ash's Slack channel route; the default Connect trigger path is not served by Ash. These
89
- commands target your project's production deployment, so make sure you have run
90
- `vercel deploy --prod` before attaching.
88
+ must match Ash's Slack channel route; the default Connect trigger path is not served by Ash. You
89
+ can register this destination before the first production deploy. Deploy after the channel file
90
+ and dependencies below are ready.
91
91
 
92
92
  Run Connect commands from the directory containing the agent's `package.json` or `vercel.json`.
93
93
  Connect uses that project context to configure project access, webhooks, and triggers. The command
@@ -122,6 +122,12 @@ export default slackChannel({
122
122
 
123
123
  Replace `slack/my-agent` with the UID from the Connect client.
124
124
 
125
+ Deploy the completed project after the Connect trigger destination and channel code are ready:
126
+
127
+ ```bash
128
+ VERCEL_USE_EXPERIMENTAL_FRAMEWORKS=1 vercel deploy --prod
129
+ ```
130
+
125
131
  The helper returns a complete Slack credentials object:
126
132
 
127
133
  - `botToken` resolves an app-scoped Slack token through Connect for outbound posts.
@@ -31,7 +31,7 @@ import { defineSandbox } from "experimental-ash/sandbox";
31
31
  export default defineSandbox({
32
32
  async bootstrap({ use }) {
33
33
  const sandbox = await use();
34
- await sandbox.runCommand("apt-get install -y jq");
34
+ await sandbox.run({ command: "apt-get install -y jq" });
35
35
  },
36
36
  });
37
37
  ```
@@ -58,7 +58,7 @@ The public lifecycle surface is intentionally small:
58
58
  sandbox — creation happens at prewarm time and at first-time session-create, both driven by the
59
59
  backend factory's options.
60
60
  - `sandbox.resolvePath(path)` — translate a logical `/workspace/...` path into the live filesystem
61
- - `sandbox.runCommand(command)` — run a shell command inside the sandbox
61
+ - `sandbox.run({ command })` — run a shell command inside the sandbox
62
62
 
63
63
  `defineSandbox` lives on `experimental-ash/sandbox`.
64
64
 
@@ -126,9 +126,9 @@ export default defineTool({
126
126
  inputSchema: z.object({ kind: z.string(), payload: z.string() }),
127
127
  async execute({ kind, payload }) {
128
128
  const sandbox = await getSandbox();
129
- await sandbox.runCommand(
130
- `echo ${JSON.stringify(JSON.stringify({ kind, payload }))} >> /var/lib/analytics/events.log`,
131
- );
129
+ await sandbox.run({
130
+ command: `echo ${JSON.stringify(JSON.stringify({ kind, payload }))} >> /var/lib/analytics/events.log`,
131
+ });
132
132
  return { ok: true };
133
133
  },
134
134
  });
@@ -144,7 +144,7 @@ Important rules:
144
144
  ### Workspace Paths
145
145
 
146
146
  Every backend runs `bash` with `/workspace` as the working directory. `readFile(...)`,
147
- `writeFile(...)`, and `runCommand(...)` all share that single namespace — `/workspace/foo` refers
147
+ `writeFile(...)`, and `run(...)` all share that single namespace — `/workspace/foo` refers
148
148
  to the same file whether the backend is local or Vercel.
149
149
 
150
150
  `sandbox.resolvePath(...)` anchors a sandbox-relative path to `/workspace` and returns the
@@ -158,11 +158,11 @@ const sandbox = await getSandbox();
158
158
  const analysisRoot = sandbox.resolvePath("python-analysis");
159
159
 
160
160
  await sandbox.writeFile("python-analysis/run.py", "print('ok')\n");
161
- await sandbox.runCommand(`python ${JSON.stringify(`${analysisRoot}/run.py`)}`);
161
+ await sandbox.run({ command: `python ${JSON.stringify(`${analysisRoot}/run.py`)}` });
162
162
  ```
163
163
 
164
164
  `readFile(...)` and `writeFile(...)` apply the same anchoring internally, so you only need to
165
- call `resolvePath(...)` explicitly when building paths for `runCommand(...)` or returning them
165
+ call `resolvePath(...)` explicitly when building paths for `run(...)` or returning them
166
166
  from authored helpers.
167
167
 
168
168
  ## Subagents Get Their Own Sandbox
@@ -236,7 +236,8 @@ export default defineSandbox({
236
236
 
237
237
  Ash ships two built-in backends, exposed as factory functions from `experimental-ash/sandbox`:
238
238
 
239
- - `vercelBackend(opts?)` — runs the sandbox on Vercel Sandbox via `@vercel/sandbox`.
239
+ - `vercelBackend(opts?)` — runs the sandbox on [Vercel Sandbox](https://vercel.com/docs/sandbox) via
240
+ [`@vercel/sandbox`](https://vercel.com/docs/sandbox/sdk-reference).
240
241
  - `localBackend(opts?)` — runs the sandbox locally via `just-bash`. Default for `pnpm ash dev`.
241
242
 
242
243
  Attach a backend via `defineSandbox({ backend })`:
@@ -249,7 +250,7 @@ export default defineSandbox({
249
250
  backend: vercelBackend({ runtime: "node24", resources: { vcpus: 2 } }),
250
251
  async bootstrap({ use }) {
251
252
  const sandbox = await use();
252
- await sandbox.runCommand("git clone https://example.com/repo.git repo");
253
+ await sandbox.run({ command: "git clone https://example.com/repo.git repo" });
253
254
  },
254
255
  async onSession({ use }) {
255
256
  await use({ networkPolicy: "deny-all" });
@@ -282,9 +283,10 @@ The factory `opts` parameter is forwarded to the underlying SDK's `Sandbox.creat
282
283
  every fresh sandbox the framework creates — both the template at prewarm time and the session at
283
284
  first-time session-create.
284
285
 
285
- On resume (when the framework reattaches to an existing persistent session via `Sandbox.get`), no
286
- `Sandbox.create` call happens, so the factory opts are not re-applied. The existing sandbox keeps
287
- whatever configuration it had from its prior creation.
286
+ On resume (when the framework reattaches to an existing
287
+ [persistent](#session-persistence-and-resume) session via `Sandbox.get`), no `Sandbox.create` call
288
+ happens, so the factory opts are not re-applied. The existing sandbox keeps whatever configuration
289
+ it had from its prior creation.
288
290
 
289
291
  Lifecycle hooks remain update-time and override post-create:
290
292
 
@@ -361,9 +363,8 @@ lock down via `onSession`.
361
363
 
362
364
  ### Timeout
363
365
 
364
- The `@vercel/sandbox` SDK shuts down idle VMs after a configurable timeout (default 5 minutes). Ash
365
- raises this default to **30 minutes** so the sandbox survives across workflow step boundaries. Set
366
- a different default on the factory, or override per-session in `onSession`:
366
+ Sandbox VMs shut down after a configurable timeout. Ash defaults to **30 minutes**. Set a different
367
+ value on the factory, or override per-session in `onSession`:
367
368
 
368
369
  ```ts
369
370
  // factory default — applies to every fresh sandbox
@@ -376,7 +377,28 @@ async onSession({ use }) {
376
377
  ```
377
378
 
378
379
  The maximum timeout depends on your Vercel plan: **5 hours** for Pro/Enterprise, **45 minutes** for
379
- Hobby. If a sandbox expires between steps, the next step will fail with a `410 Gone` error.
380
+ Hobby. When the timeout fires, the VM shuts down but because sessions are
381
+ [persistent](#session-persistence-and-resume), the filesystem state is preserved and the sandbox
382
+ resumes automatically on the next request.
383
+
384
+ ### Session Persistence and Resume
385
+
386
+ Sandbox sessions are **persistent by default**: when the VM shuts down (timeout or inactivity), the
387
+ filesystem state is preserved. The next time a message arrives — even days or weeks later — the
388
+ sandbox automatically resumes from that state.
389
+
390
+ This means:
391
+
392
+ - A user can send a message days after the last interaction and pick up where they left off.
393
+ Files created during earlier turns, installed dependencies, and workspace state are all preserved.
394
+ - The resume is transparent to your code — no configuration is required.
395
+ - If a sandbox has been deleted (by cleanup policies or manual deletion), Ash creates a fresh
396
+ session from the prewarmed template snapshot. The user gets a clean sandbox seeded with framework
397
+ defaults, bootstrap output, and workspace files — but any session-specific state from prior turns
398
+ is lost.
399
+
400
+ See the [Vercel Sandbox documentation](https://vercel.com/docs/sandbox) for details on persistence
401
+ behavior and plan-specific retention limits.
380
402
 
381
403
  ### Tags
382
404
 
@@ -386,8 +408,7 @@ Ash attaches Vercel Sandbox tags for runtime attribution:
386
408
  - `channel` — the active channel adapter kind
387
409
  - `sessionId` — the Ash session id
388
410
 
389
- Custom tags can be set on the factory (applied at every fresh `Sandbox.create`) or via
390
- `onSession`'s `use()` (applied via `sandbox.update`):
411
+ Custom tags can be set on the factory or via `onSession`'s `use()`:
391
412
 
392
413
  ```ts
393
414
  backend: vercelBackend({ tags: { team: "infra" } });
@@ -425,3 +446,5 @@ Important behavior:
425
446
  - [Tools](./tools.md)
426
447
  - [Workspace](./workspace.md)
427
448
  - [Session Context](./session-context.md)
449
+ - [Vercel Sandbox](https://vercel.com/docs/sandbox) — platform documentation for Vercel Sandbox
450
+ - [Vercel Sandbox SDK Reference](https://vercel.com/docs/sandbox/sdk-reference) — `@vercel/sandbox` API reference
@@ -65,7 +65,7 @@ Important behavior:
65
65
 
66
66
  ```ts
67
67
  const sandbox = await getSandbox();
68
- const result = await sandbox.runCommand("pnpm test");
68
+ const result = await sandbox.run({ command: "pnpm test" });
69
69
  ```
70
70
 
71
71
  Important behavior:
@@ -0,0 +1,21 @@
1
+ import type { ChannelKind } from "@vercel/ash-scaffold";
2
+ import type { DisabledChannelReasons } from "@vercel/ash-scaffold/cli";
3
+ /**
4
+ * Existing authored registrations that affect the scaffolded channel picker
5
+ * or would conflict with a generated channel module.
6
+ */
7
+ export interface ExistingChannelRegistrations {
8
+ readonly disabledChannelReasons: DisabledChannelReasons;
9
+ readonly webRouteOwners: readonly string[];
10
+ readonly slackOwners: readonly string[];
11
+ }
12
+ /**
13
+ * Inspects compiled authored channels so custom filenames still disable the
14
+ * scaffold option for the channel behavior they register.
15
+ */
16
+ export declare function inspectExistingChannelRegistrations(projectRoot: string): Promise<ExistingChannelRegistrations>;
17
+ /**
18
+ * Rejects scaffolding when another authored module already owns behavior
19
+ * emitted by the generated channel module.
20
+ */
21
+ export declare function assertCanAddSelectedChannels(selectedChannels: readonly ChannelKind[], registrations: ExistingChannelRegistrations): void;
@@ -0,0 +1 @@
1
+ import{ASH_CREATE_SESSION_ROUTE_PATH}from"#protocol/routes.js";import{join}from"node:path";import{compileChannelDefinition}from"#compiler/normalize-channel.js";import{discoverAgent}from"#discover/discover-agent.js";const SCAFFOLDED_WEB_CHANNEL_LOGICAL_PATH=`channels/ash.ts`;async function inspectExistingChannelRegistrations(a){let o=join(a,`agent`),{manifest:s}=await discoverAgent({agentRoot:o,appRoot:a}),c=new Set,l=new Set;for(let t of s.channels){let r=await compileChannelDefinition(o,t),i=Array.isArray(r)?r:[r];for(let n of i)n.kind===`channel`&&(n.method===`POST`&&n.urlPath===ASH_CREATE_SESSION_ROUTE_PATH&&c.add(t.logicalPath),n.adapterKind===`slack`&&l.add(t.logicalPath))}let u={};return[...c].some(e=>e!==SCAFFOLDED_WEB_CHANNEL_LOGICAL_PATH)&&(u.web=`POST ${ASH_CREATE_SESSION_ROUTE_PATH} already registered`),l.size>0&&(u.slack=`Slack channel already registered`),{disabledChannelReasons:u,webRouteOwners:[...c],slackOwners:[...l]}}function assertCanAddSelectedChannels(t,n){if(t.includes(`web`)){let t=n.webRouteOwners.find(e=>e!==SCAFFOLDED_WEB_CHANNEL_LOGICAL_PATH);if(t!==void 0)throw Error(`Cannot scaffold Web Chat because agent/${t} already defines POST ${ASH_CREATE_SESSION_ROUTE_PATH}. Web Chat scaffolds the same Ash session routes.`)}if(t.includes(`slack`)){let e=n.slackOwners.find(e=>e!==`channels/slack.ts`);if(e!==void 0)throw Error(`Cannot scaffold Slack because agent/${e} already defines a Slack channel. Slack scaffolding would register the channel again.`)}}export{assertCanAddSelectedChannels,inspectExistingChannelRegistrations};
@@ -1,3 +1,6 @@
1
+ import { type RunAddToAgentOptions } from "@vercel/ash-scaffold/steps";
2
+ import { type ChannelAddPrompter } from "@vercel/ash-scaffold/cli";
3
+ import { type DeploymentInfo } from "@vercel/ash-scaffold/primitives";
1
4
  export interface CliLogger {
2
5
  error(message: string): void;
3
6
  log(message: string): void;
@@ -5,10 +8,15 @@ export interface CliLogger {
5
8
  export interface AddChannelCommandOptions {
6
9
  force?: boolean;
7
10
  }
11
+ export interface ChannelsAddDependencies {
12
+ createPrompter?: () => ChannelAddPrompter;
13
+ detectDeployment(projectPath: string): Promise<DeploymentInfo>;
14
+ runAddToAgent(options: RunAddToAgentOptions): Promise<void>;
15
+ }
8
16
  export declare function runChannelsAddCommand(logger: CliLogger, appRoot: string, args: {
9
17
  kind?: string;
10
18
  options: AddChannelCommandOptions;
11
- }): Promise<void>;
19
+ }, dependencies?: ChannelsAddDependencies): Promise<void>;
12
20
  export interface ListChannelsCommandOptions {
13
21
  json?: boolean;
14
22
  }
@@ -1,3 +1 @@
1
- import{isAshProject}from"../../packages/ash-scaffold/src/project.js";import{SLACK_CHANNEL_DEFAULT_ROUTE,ensureChannel,listAuthoredChannels}from"../../packages/ash-scaffold/src/channels.js";import"../../packages/ash-scaffold/src/index.js";import{relative}from"node:path";import{createInterface}from"node:readline/promises";const NOT_AN_AGENT_MESSAGE="No Ash agent in this directory. Run `pnpm create experimental-ash-agent`, then run this command from inside the new project.",KNOWN_CHANNEL_KINDS=[`slack`];function isChannelKind(e){return KNOWN_CHANNEL_KINDS.includes(e)}function parseChannelKind(e){if(!isChannelKind(e))throw Error(`Unknown channel kind "${e}". Known: ${KNOWN_CHANNEL_KINDS.join(`, `)}.`);return e}async function promptChannelKind(){if(!process.stdin.isTTY||!process.stdout.isTTY)throw Error("Pass a channel kind: `ash channels add slack`.");let e=createInterface({input:process.stdin,output:process.stdout});try{let t=(await e.question(`Channel to add (slack): `)).trim();return t.length===0?`slack`:parseChannelKind(t)}finally{e.close()}}function renderSlackNextSteps(e){return[`Next steps:`,` pnpm i`,` vercel deploy --prod`,` # If Connect returns a different UID, use it below and in agent/channels/slack.ts.`,` vercel connect create slack --name ${e} --triggers`,` vercel connect detach slack/${e} --yes`,` vercel connect attach slack/${e} --triggers --trigger-path ${SLACK_CHANNEL_DEFAULT_ROUTE} --yes`].join(`
2
- `)}function renderExistingSlackChannelNextSteps(e){return[`Next steps:`,` Review ${e} for the configured Slack connector UID.`,` Run Vercel Connect commands only for the UID already used by that file.`].join(`
3
- `)}function formatProjectPath(e,t){let n=relative(e,t);return n.length===0||n.startsWith(`..`)?t:n}function logPackageJsonMutations(e,t,n){for(let r of n.packageJsonUpdated)e.log(`Updated ${formatProjectPath(t,r.path)} with ${r.dependencies.join(`, `)}.`)}function assertUnhandledChannelMutation(e){throw Error(`Unhandled channel mutation result: ${JSON.stringify(e)}`)}async function runChannelsAddCommand(t,r,i){if(!await isAshProject(r)){t.error(NOT_AN_AGENT_MESSAGE),process.exitCode=1;return}try{let e=i.kind===void 0?await promptChannelKind():parseChannelKind(i.kind),a=await ensureChannel({projectRoot:r,kind:e,force:i.options.force});switch(logPackageJsonMutations(t,r,a),a.action){case`created`:t.log(`Scaffolded channel: ${e}`),t.log(renderSlackNextSteps(a.slackConnectorSlug));break;case`overwritten`:t.log(`Overwrote channel: ${e}`),t.log(renderSlackNextSteps(a.slackConnectorSlug));break;case`skipped`:{let n=a.filesSkipped[0];t.log(`Channel "${e}" already exists; left existing files unchanged.`),t.log(renderExistingSlackChannelNextSteps(formatProjectPath(r,n)));break}default:assertUnhandledChannelMutation(a)}}catch(e){t.error(e instanceof Error?e.message:String(e)),process.exitCode=1}}async function runChannelsListCommand(t,n,i){if(!await isAshProject(n)){t.error(NOT_AN_AGENT_MESSAGE),process.exitCode=1;return}let a=await listAuthoredChannels(n);if(i.json){t.log(JSON.stringify({channels:a},null,2));return}if(a.length===0){t.log("No channels defined. Run `ash channels add slack` to add one.");return}for(let e of a)t.log(e)}export{runChannelsAddCommand,runChannelsListCommand};
1
+ import{assertCanAddSelectedChannels,inspectExistingChannelRegistrations}from"./channel-add-conflicts.js";import{isAshProject}from"../../packages/ash-scaffold/src/project.js";import{listAuthoredChannels}from"../../packages/ash-scaffold/src/channels.js";import"../../packages/ash-scaffold/src/index.js";import{detectDeployment}from"../../packages/ash-scaffold/src/primitives/detect-deployment.js";import{createAddToAgentState,runAddToAgent}from"../../packages/ash-scaffold/src/steps/run-add-to-agent.js";import"../../packages/ash-scaffold/src/steps/index.js";import{ChannelAddCancelledError,createChannelAddPrompter}from"../../packages/ash-scaffold/src/cli/channel-add-prompter.js";import"../../packages/ash-scaffold/src/cli/index.js";import"../../packages/ash-scaffold/src/primitives/index.js";const NOT_AN_AGENT_MESSAGE="No Ash agent in this directory. Run `pnpm create experimental-ash-agent`, then run this command from inside the new project.",KNOWN_CHANNEL_KINDS=[`slack`,`web`];function isChannelKind(e){return KNOWN_CHANNEL_KINDS.includes(e)}function parseChannelKind(e){if(!isChannelKind(e))throw Error(`Unknown channel kind "${e}". Known: ${KNOWN_CHANNEL_KINDS.join(`, `)}.`);return e}const defaultChannelsAddDependencies={detectDeployment,runAddToAgent};async function runAddChannelsFlow(n,r,i,o){if(r===void 0&&(!process.stdin.isTTY||!process.stdout.isTTY))throw Error(`Pass a channel kind: \`ash channels add <${KNOWN_CHANNEL_KINDS.join(`|`)}>\`.`);let s=o.createPrompter?.()??createChannelAddPrompter();s.intro(`Add channels to your Ash agent`),s.log.message(`Checking the current Vercel project...`);let c=createAddToAgentState(await o.detectDeployment(n)),l;function inspectRegistrations(){return l===void 0&&(s.log.message(`Inspecting existing channel registrations...`),l=inspectExistingChannelRegistrations(n)),l}let u=r===void 0?(await inspectRegistrations()).disabledChannelReasons:void 0;await o.runAddToAgent({prompter:s,projectPath:n,state:c,presetChannels:r===void 0?void 0:[r],disabledChannelReasons:u,force:i.force,validateSelectedChannels:async t=>{!t.includes(`web`)&&!t.includes(`slack`)||assertCanAddSelectedChannels(t,await inspectRegistrations())}}),s.outro(c.channels.length===0?`No channels added.`:`Channels added.`)}async function runChannelsAddCommand(e,t,r,i=defaultChannelsAddDependencies){if(!await isAshProject(t)){e.error(NOT_AN_AGENT_MESSAGE),process.exitCode=1;return}try{await runAddChannelsFlow(t,r.kind===void 0?void 0:parseChannelKind(r.kind),r.options,i)}catch(t){if(t instanceof ChannelAddCancelledError)return;e.error(t instanceof Error?t.message:String(t)),process.exitCode=1}}async function runChannelsListCommand(e,t,i){if(!await isAshProject(t)){e.error(NOT_AN_AGENT_MESSAGE),process.exitCode=1;return}let a=await listAuthoredChannels(t);if(i.json){e.log(JSON.stringify({channels:a},null,2));return}if(a.length===0){e.log("No channels defined. Run `ash channels add` to add one.");return}for(let t of a)e.log(t)}export{runChannelsAddCommand,runChannelsListCommand};
@@ -1,2 +1,2 @@
1
- import{ASH_CONTINUE_SESSION_ROUTE_PATTERN,ASH_CREATE_SESSION_ROUTE_PATH,ASH_MESSAGE_STREAM_ROUTE_PATTERN,createAshMessageStreamRoutePath}from"#protocol/routes.js";import{createCliTheme,renderCliBanner,renderCliSection,renderCliSpeakerLine,renderCliTaggedLine}from"#cli/ui/output.js";import{createInterface,emitKeypressEvents}from"node:readline";import{openStreamIterable}from"#client/open-stream.js";import{isCurrentTurnBoundaryEvent}from"#protocol/message.js";import{toErrorMessage}from"#shared/errors.js";import{createDevelopmentRequestHeadersAsync}from"#services/dev-client/request-headers.js";import{extractCurrentTurnBoundaryEvent}from"#services/dev-client/stream.js";import{resolveDevelopmentServerResourceUrl}from"#services/dev-client/url.js";import{formatVercelAuthChallengeMessage,isVercelAuthChallenge}from"#services/dev-client/vercel-auth-error.js";import{createDevClient}from"#services/dev-client.js";import{parseDevReplInput}from"#cli/dev/input.js";import{ESCAPED_RUNTIME_INPUT_PROMPT,extractPendingRuntimeInputRequests,promptForRuntimeInputRequests}from"#cli/dev/input-requests.js";import{createDevelopmentTerminal}from"#cli/dev/terminal.js";function renderConnectionRows(r){let i=[{label:`Server`,tone:`info`,value:r.serverUrl},{label:`Create`,tone:`info`,value:`POST ${ASH_CREATE_SESSION_ROUTE_PATH}`},{label:`Continue`,tone:`info`,value:`POST ${ASH_CONTINUE_SESSION_ROUTE_PATTERN}`},{label:`Stream`,tone:`info`,value:`GET ${ASH_MESSAGE_STREAM_ROUTE_PATTERN}`}];return i.push({label:`Session`,value:`Follow-up messages reuse the active continuation token.`}),i}function renderCommandRows(){return[{label:`/help`,value:`Print the connection contract and available commands.`},{label:`/new`,value:`Clear the current durable session cursor.`},{label:`/exit`,value:`Exit the REPL.`}]}function renderIntro(e,t){return[renderCliBanner(e,{subtitle:`Interactive development REPL for the active Ash server.`,title:`Ash Dev`}),``,renderCliSection(e,{rows:renderConnectionRows(t),title:`Connection`}),``,renderCliSection(e,{rows:renderCommandRows(),title:`Commands`})].join(`
1
+ import{ASH_CONTINUE_SESSION_ROUTE_PATTERN,ASH_CREATE_SESSION_ROUTE_PATH,ASH_MESSAGE_STREAM_ROUTE_PATTERN,createAshMessageStreamRoutePath}from"#protocol/routes.js";import{createInterface,emitKeypressEvents}from"node:readline";import{createCliTheme,renderCliBanner,renderCliSection,renderCliSpeakerLine,renderCliTaggedLine}from"#cli/ui/output.js";import{openStreamIterable}from"#client/open-stream.js";import{isCurrentTurnBoundaryEvent}from"#protocol/message.js";import{toErrorMessage}from"#shared/errors.js";import{createDevelopmentRequestHeadersAsync}from"#services/dev-client/request-headers.js";import{extractCurrentTurnBoundaryEvent}from"#services/dev-client/stream.js";import{resolveDevelopmentServerResourceUrl}from"#services/dev-client/url.js";import{formatVercelAuthChallengeMessage,isVercelAuthChallenge}from"#services/dev-client/vercel-auth-error.js";import{createDevClient}from"#services/dev-client.js";import{parseDevReplInput}from"#cli/dev/input.js";import{ESCAPED_RUNTIME_INPUT_PROMPT,extractPendingRuntimeInputRequests,promptForRuntimeInputRequests}from"#cli/dev/input-requests.js";import{createDevelopmentTerminal}from"#cli/dev/terminal.js";function renderConnectionRows(r){let i=[{label:`Server`,tone:`info`,value:r.serverUrl},{label:`Create`,tone:`info`,value:`POST ${ASH_CREATE_SESSION_ROUTE_PATH}`},{label:`Continue`,tone:`info`,value:`POST ${ASH_CONTINUE_SESSION_ROUTE_PATTERN}`},{label:`Stream`,tone:`info`,value:`GET ${ASH_MESSAGE_STREAM_ROUTE_PATTERN}`}];return i.push({label:`Session`,value:`Follow-up messages reuse the active continuation token.`}),i}function renderCommandRows(){return[{label:`/help`,value:`Print the connection contract and available commands.`},{label:`/new`,value:`Clear the current durable session cursor.`},{label:`/exit`,value:`Exit the REPL.`}]}function renderIntro(e,t){return[renderCliBanner(e,{subtitle:`Interactive development REPL for the active Ash server.`,title:`Ash Dev`}),``,renderCliSection(e,{rows:renderConnectionRows(t),title:`Connection`}),``,renderCliSection(e,{rows:renderCommandRows(),title:`Commands`})].join(`
2
2
  `)}function normalizeStepMessage(e){let t=e.trim();return t.length>0?t:null}function getRenderTag(e){return e.options?.isSubagent===!0?`subagent`:e.fallback}function getRenderTone(e){return e.options?.isSubagent===!0?`subagent`:e.fallback}function prefixSourceLabel(e){return e.options?.sourceLabel===void 0?e.message:`${e.options.sourceLabel}${e.separator??` `}${e.message}`}function formatContentEvent(e,t,n){switch(t.type){case`message.appended`:return{finalized:!1,kind:`message`,line:renderCliSpeakerLine(e,{message:t.data.messageSoFar,speaker:n?.sourceLabel??`agent`,tone:getRenderTone({fallback:`accent`,options:n})})};case`message.completed`:if(t.data.message===null)return;if(t.data.finishReason===`tool-calls`){let r=normalizeStepMessage(t.data.message);return r===null?void 0:{finalized:!0,kind:`message`,line:renderCliTaggedLine(e,{message:prefixSourceLabel({message:r,options:n,separator:`: `}),tag:getRenderTag({fallback:`step`,options:n}),tone:getRenderTone({fallback:`accent`,options:n})})}}return{finalized:!0,kind:`message`,line:renderCliSpeakerLine(e,{message:t.data.message,speaker:n?.sourceLabel??`agent`,tone:getRenderTone({fallback:`accent`,options:n})})};case`reasoning.appended`:return{finalized:!1,kind:`reasoning`,line:renderCliTaggedLine(e,{message:prefixSourceLabel({message:t.data.reasoningSoFar,options:n,separator:`: `}),tag:getRenderTag({fallback:`reasoning`,options:n}),tone:getRenderTone({fallback:`info`,options:n})})};case`reasoning.completed`:return{finalized:!0,kind:`reasoning`,line:renderCliTaggedLine(e,{message:prefixSourceLabel({message:t.data.reasoning,options:n,separator:`: `}),tag:getRenderTag({fallback:`reasoning`,options:n}),tone:getRenderTone({fallback:`info`,options:n})})};default:return}}function formatEvent(e,t,n){let r=formatContentEvent(e,t,n);if(r!==void 0)return r.line;switch(t.type){case`message.received`:return;case`actions.requested`:return renderCliTaggedLine(e,{message:prefixSourceLabel({message:`${t.type} (${t.data.actions.length} action${t.data.actions.length===1?``:`s`})`,options:n}),tag:getRenderTag({fallback:`event`,options:n}),tone:getRenderTone({fallback:`muted`,options:n})});case`input.requested`:return renderCliTaggedLine(e,{message:prefixSourceLabel({message:`${t.type} (${t.data.requests.length} request${t.data.requests.length===1?``:`s`})`,options:n}),tag:getRenderTag({fallback:`event`,options:n}),tone:getRenderTone({fallback:`info`,options:n})});case`action.result`:return renderCliTaggedLine(e,{message:prefixSourceLabel({message:`${t.type} (${formatActionResultLabel(t.data.result)})`,options:n}),tag:getRenderTag({fallback:`event`,options:n}),tone:getRenderTone({fallback:`muted`,options:n})});case`session.waiting`:case`session.completed`:return;case`authorization.required`:return renderCliTaggedLine(e,{message:prefixSourceLabel({message:`${t.type} (${t.data.name}: ${t.data.description}${formatAuthorizationChallengeSuffix(t.data.authorization)})`,options:n}),tag:getRenderTag({fallback:`event`,options:n}),tone:getRenderTone({fallback:`warning`,options:n})});case`compaction.requested`:return renderCliTaggedLine(e,{message:prefixSourceLabel({message:`compacting conversation history`,options:n}),tag:getRenderTag({fallback:`event`,options:n}),tone:getRenderTone({fallback:`muted`,options:n})});case`compaction.completed`:return renderCliTaggedLine(e,{message:prefixSourceLabel({message:`conversation history compacted`,options:n}),tag:getRenderTag({fallback:`event`,options:n}),tone:getRenderTone({fallback:`muted`,options:n})});case`step.failed`:case`turn.failed`:case`session.failed`:return;case`subagent.called`:return renderCliTaggedLine(e,{message:prefixSourceLabel({message:`${t.type} (${t.data.name} -> ${t.data.childSessionId})`,options:n}),tag:getRenderTag({fallback:`event`,options:n}),tone:getRenderTone({fallback:`info`,options:n})});case`subagent.started`:return renderCliTaggedLine(e,{message:prefixSourceLabel({message:`${t.type} (${t.data.subagentName})`,options:n}),tag:getRenderTag({fallback:`event`,options:n}),tone:getRenderTone({fallback:`info`,options:n})});case`subagent.event`:return renderCliTaggedLine(e,{message:prefixSourceLabel({message:`${t.type} (${t.data.subagentName}: ${formatNestedSubagentEventLabel(t.data.event)})`,options:n}),tag:getRenderTag({fallback:`event`,options:n}),tone:getRenderTone({fallback:`muted`,options:n})});case`subagent.completed`:return renderCliTaggedLine(e,{message:prefixSourceLabel({message:`${t.type} (${t.data.subagentName})`,options:n}),tag:getRenderTag({fallback:`event`,options:n}),tone:getRenderTone({fallback:`info`,options:n})});default:return}}function getEventDisplayBlockKind(e){switch(e.type){case`message.appended`:case`message.completed`:case`reasoning.appended`:case`reasoning.completed`:return`content`;default:return`meta`}}function createTurnDisplayState(){return{activeLiveContentKind:null,lastPrintedBlockKind:null}}function printDisplayLine(e){let t=e.state;return t.activeLiveContentKind!==null&&(e.terminal.commitLive(),t={activeLiveContentKind:null,lastPrintedBlockKind:`content`}),t.lastPrintedBlockKind!==null&&t.lastPrintedBlockKind!==e.kind&&e.terminal.print(``),e.terminal.print(e.line),{activeLiveContentKind:null,lastPrintedBlockKind:e.kind}}function renderTurnEvent(e){let t=formatContentEvent(e.theme,e.event,e.options);if(t!==void 0){let n=e.state;return n.activeLiveContentKind!==null&&n.activeLiveContentKind!==t.kind&&(e.terminal.commitLive(),n={activeLiveContentKind:null,lastPrintedBlockKind:`content`}),n.lastPrintedBlockKind!==null&&n.lastPrintedBlockKind!==`content`&&e.terminal.print(``),e.terminal.updateLive(t.line),t.finalized?(e.terminal.commitLive(),{activeLiveContentKind:null,lastPrintedBlockKind:`content`}):{activeLiveContentKind:t.kind,lastPrintedBlockKind:`content`}}let n=formatEvent(e.theme,e.event,e.options);return n===void 0?e.state:printDisplayLine({kind:getEventDisplayBlockKind(e.event),line:n,state:e.state,terminal:e.terminal})}function isAbortLikeError(e){return(e instanceof DOMException||e instanceof Error)&&e.name===`AbortError`}function shouldDrainSubagentStreamsOnBoundary(e){return e.length===0}var ReplSubagentStreamManager=class{#e=new Map;#t;#n;#r;#i;constructor(e){this.#t=e.displayStateRef,this.#n=e.serverUrl,this.#r=e.terminal,this.#i=e.theme}subscribe(e){if(this.#e.has(e.sessionId))return;let t=new AbortController,n=this.#a({controller:t,sessionId:e.sessionId,subagentName:e.subagentName}).finally(()=>{this.#e.delete(e.sessionId)});this.#e.set(e.sessionId,{controller:t,done:n,label:e.subagentName})}async waitForIdle(){for(;this.#e.size>0;)await Promise.all([...this.#e.values()].map(e=>e.done))}async close(){let e=[...this.#e.values()];for(let t of e)t.controller.abort();await Promise.allSettled(e.map(e=>e.done))}async#a(e){let t=resolveDevelopmentServerResourceUrl({resource:createAshMessageStreamRoutePath(e.sessionId),serverUrl:this.#n});try{for await(let n of openStreamIterable({host:this.#n,maxReconnectAttempts:3,resolveHeaders:async()=>await createDevelopmentRequestHeadersAsync({resourceUrl:t}),sessionId:e.sessionId,signal:e.controller.signal,startIndex:0}))if(this.#t.current=renderTurnEvent({event:n,options:{isSubagent:!0,sourceLabel:e.subagentName},state:this.#t.current,terminal:this.#r,theme:this.#i}),n.type===`subagent.called`&&this.subscribe({sessionId:n.data.childSessionId,subagentName:n.data.name}),isCurrentTurnBoundaryEvent(n))return}catch(t){if(isAbortLikeError(t))return;let n=toErrorMessage(t);this.#t.current=printDisplayLine({kind:`meta`,line:renderCliTaggedLine(this.#i,{message:`${e.subagentName} stream failed: ${n}`,tag:`subagent`,tone:`danger`}),state:this.#t.current,terminal:this.#r})}finally{e.controller.abort()}}};function formatActionResultLabel(e){switch(e.kind){case`load-skill-result`:return e.kind;case`subagent-result`:return`${e.kind}:${e.subagentName}`;case`tool-result`:return`${e.kind}:${e.toolName}`}}function formatAuthorizationChallengeSuffix(e){if(e===void 0)return``;let t=[];return e.url!==void 0&&t.push(e.url),e.userCode!==void 0&&t.push(`code ${e.userCode}`),e.instructions!==void 0&&t.push(e.instructions),t.length===0?``:` — ${t.join(`, `)}`}function formatNestedSubagentEventLabel(e){switch(e.type){case`actions.requested`:{let t=e.data.actions,n=t.map(e=>e.kind===`tool-call`?e.toolName:e.kind).join(`,`);return`${e.type} (${t.length} action${t.length===1?``:`s`}${n.length>0?`: ${n}`:``})`}case`action.result`:return`${e.type} (${formatActionResultLabel(e.data.result)})`;case`input.requested`:return`${e.type} (${e.data.requests.length} request${e.data.requests.length===1?``:`s`})`;default:return e.type}}function formatDispatch(e,t){return t.continuationToken?renderCliTaggedLine(e,{message:`resuming session ${t.continuationToken}`,tag:`session`,tone:`info`}):renderCliTaggedLine(e,{message:`starting a new session`,tag:`session`,tone:`info`})}function formatTurnDispatch(e,t){return t.turn.inputResponses!==void 0&&t.turn.message===void 0?renderCliTaggedLine(e,{message:`responding to pending input request${t.turn.inputResponses.length===1?``:`s`}`,tag:`session`,tone:`info`}):formatDispatch(e,t.session)}function formatSessionBoundary(e,t){let n=extractCurrentTurnBoundaryEvent(t),r=extractPendingRuntimeInputRequests(t);switch(n?.type){case`session.waiting`:return[renderCliTaggedLine(e,{message:r.length>0?`waiting for input approval/answer or the next message`:`waiting for the next message`,tag:`session`,tone:`success`})];case`session.completed`:return[renderCliTaggedLine(e,{message:`session completed; the next input starts a new session`,tag:`session`,tone:`success`})];case`session.failed`:{let t=n.data.details&&typeof n.data.details.name==`string`?n.data.details.name:void 0;return[renderCliTaggedLine(e,{message:t?`session failed (${t}): ${n.data.message}`:`session failed: ${n.data.message}`,tag:`session`,tone:`danger`}),renderCliTaggedLine(e,{message:`cleared; the next input starts a new session`,tag:`session`,tone:`warning`})]}default:return[]}}async function waitForInputLine(e,t,n={}){return await new Promise(r=>{let i=e.input,cleanup=()=>{e.off(`close`,handleClose),e.off(`line`,handleLine),i.off(`keypress`,handleKeypress)},handleClose=()=>{cleanup(),r(void 0)},handleLine=e=>{cleanup(),r(e)},handleKeypress=(t,i)=>{n.allowEscape!==!0||i.name!==`escape`||(cleanup(),e.write(null,{ctrl:!0,name:`u`}),r(ESCAPED_RUNTIME_INPUT_PROMPT))};e.setPrompt(t),e.once(`close`,handleClose),e.once(`line`,handleLine),n.allowEscape&&(emitKeypressEvents(i,e),i.on(`keypress`,handleKeypress)),e.prompt()})}async function runTurn(e){let t=e.turn,n={current:createTurnDisplayState()},i=new ReplSubagentStreamManager({displayStateRef:n,serverUrl:e.serverUrl,terminal:e.terminal,theme:e.theme}),ask=async t=>{e.terminal.startPrompt(e.rl,t);let n=await waitForInputLine(e.rl,t,{allowEscape:!0});return e.terminal.stopPrompt(),n};try{for(;;){let a=e.client.getSession();n.current=printDisplayLine({kind:`meta`,line:formatTurnDispatch(e.theme,{session:a,turn:t}),state:n.current,terminal:e.terminal});let o=await e.client.send({inputResponses:t.inputResponses,message:t.message,onEvent(t){n.current=renderTurnEvent({event:t,state:n.current,terminal:e.terminal,theme:e.theme}),t.type===`subagent.called`&&i.subscribe({sessionId:t.data.childSessionId,subagentName:t.data.name})},onResponseStart(t){t.sessionId&&a.sessionId!==t.sessionId&&(n.current=printDisplayLine({kind:`meta`,line:renderCliTaggedLine(e.theme,{message:t.sessionId,tag:`session`,tone:`accent`}),state:n.current,terminal:e.terminal}),n.current=printDisplayLine({kind:`meta`,line:renderCliTaggedLine(e.theme,{message:resolveDevelopmentServerResourceUrl({resource:createAshMessageStreamRoutePath(t.sessionId),serverUrl:e.serverUrl}).toString(),tag:`stream`,tone:`info`}),state:n.current,terminal:e.terminal}))}}),s=extractPendingRuntimeInputRequests(o.events);if(shouldDrainSubagentStreamsOnBoundary(s)){await i.waitForIdle();for(let t of formatSessionBoundary(e.theme,o.events))n.current=printDisplayLine({kind:`meta`,line:t,state:n.current,terminal:e.terminal});return`continue`}let c=await promptForRuntimeInputRequests({ask,print(t){n.current=printDisplayLine({kind:`meta`,line:t,state:n.current,terminal:e.terminal})},requests:s,theme:e.theme});if(c.kind===`aborted`)return`exit`;if(c.kind===`deferred`)return n.current=printDisplayLine({kind:`meta`,line:renderCliTaggedLine(e.theme,{message:`left pending input requests unresolved; send a new message to ignore them`,tag:`session`,tone:`warning`}),state:n.current,terminal:e.terminal}),`continue`;t={inputResponses:c.inputResponses}}}finally{await i.close()}}async function runDevelopmentRepl(e){let t=createDevelopmentTerminal(),n=createCliTheme({color:!0}),r=createInterface({input:process.stdin,output:t.output,terminal:!0});r.on(`SIGINT`,()=>{r.close()});let a=createDevClient({serverUrl:e.serverUrl});try{for(t.print(renderIntro(n,e)),t.print(``);;){t.print(``),t.startPrompt(r,`you> `);let i=await waitForInputLine(r,`you> `);if(t.stopPrompt(),i===void 0)return;let o=parseDevReplInput(i);switch(o.kind!==`empty`&&o.kind!==`exit`&&t.print(``),o.kind){case`empty`:continue;case`help`:t.print(renderIntro(n,e)),t.print(``);continue;case`exit`:return;case`new`:await a.clear(),t.print(renderCliTaggedLine(n,{message:`cleared; the next input starts a new session`,tag:`session`,tone:`warning`})),t.print(``);continue;case`message`:try{if(await runTurn({client:a,rl:r,serverUrl:e.serverUrl,terminal:t,theme:n,turn:{message:o.message}})===`exit`)return}catch(r){isVercelAuthChallenge(r)?t.printError(renderCliTaggedLine(n,{message:formatVercelAuthChallengeMessage({serverUrl:e.serverUrl}),tag:`auth`,tone:`warning`})):t.printError(renderCliTaggedLine(n,{message:toErrorMessage(r),tag:`error`,tone:`danger`}))}t.print(``)}}}finally{await a.close(),r.close(),t.dispose()}}export{createTurnDisplayState,formatContentEvent,formatEvent,renderTurnEvent,runDevelopmentRepl,shouldDrainSubagentStreamsOnBoundary};
@@ -1,3 +1,3 @@
1
- import{createCliTheme,renderCliTaggedLine}from"#cli/ui/output.js";import{Command,CommanderError,InvalidArgumentError}from"#compiled/commander/index.js";import{resolveApplicationRoot}from"#internal/application/paths.js";import{resolveInstalledPackageInfo}from"#internal/application/package.js";import{parseDevelopmentServerUrl}from"#cli/dev/url.js";async function loadBuildHost(){return(await import(`#internal/nitro/host.js`)).buildApplication}async function loadPrintApplicationInfo(){return(await import(`#cli/commands/info.js`)).printApplicationInfo}async function loadRunDevelopmentRepl(){return(await import(`#cli/dev/repl.js`)).runDevelopmentRepl}async function loadRunEvalCommand(){return(await import(`#evals/cli/eval.js`)).runEvalCommand}async function loadStartHost(){return(await import(`#internal/nitro/host.js`)).startDevelopmentServer}async function loadStartProductionHost(){return(await import(`#internal/nitro/host.js`)).startProductionServer}function shouldPrintCliBootBanner(e){return e.name()===`info`||e.name()===`dev`}async function waitForShutdownSignal(e){await new Promise((t,n)=>{let r=!1,cleanup=()=>{process.off(`SIGINT`,handleSignal),process.off(`SIGTERM`,handleSignal)},handleSignal=()=>{r||(r=!0,cleanup(),e.close().then(t,n))};process.once(`SIGINT`,handleSignal),process.once(`SIGTERM`,handleSignal)})}async function waitForProductionServer(e){await Promise.race([e.wait(),waitForShutdownSignal({close:()=>e.close()})])}function parsePortOption(e){if(!/^-?\d+$/.test(e))throw new InvalidArgumentError(`Expected a numeric port, received "${e}".`);let t=Number(e);if(t<0||t>65535)throw new InvalidArgumentError(`Expected a port between 0 and 65535, received "${e}".`);return t}function hasInteractiveTerminal(){return!!(process.stdin.isTTY&&process.stdout.isTTY)}function rewriteDevelopmentUrlShorthand(e){let t=e[1];return e[0]!==`dev`||e.length!==2||t===void 0||t.startsWith(`-`)?[...e]:[`dev`,`--url`,t]}function resolveRemoteDevelopmentServerUrl(e){if(e.url){if(e.host!==void 0)throw new InvalidArgumentError(`The --host option cannot be used with --url.`);if(e.port!==void 0)throw new InvalidArgumentError(`The --port option cannot be used with --url.`);if(e.repl===!1)throw new InvalidArgumentError(`The --no-repl option cannot be used with --url.`);return e.url}}function createCliProgram(r,i){let c=resolveApplicationRoot(),l=resolveInstalledPackageInfo().version,u=new Command,d=createCliTheme();u.name(`ash`).description(`Build and run an Ash application.`).version(l).showHelpAfterError().exitOverride().hook(`preAction`,(e,t)=>{shouldPrintCliBootBanner(t)&&r.log(`Ash (v${l})`)}).configureOutput({writeErr:e=>{r.error(e.trimEnd())},writeOut:e=>{r.log(e.trimEnd())}});let f=u.command(`channels`).description(`Manage user-authored channels in the current project.`);return f.command(`add [kind]`).description(`Scaffold a channel (slack) into the current project.`).option(`-f, --force`,`Overwrite existing channel files`).action(async(e,t)=>{let{runChannelsAddCommand:n}=await import(`#cli/commands/channels.js`);await n(r,c,{kind:e,options:t})}),f.command(`list`).description(`List user-authored channels in the current project.`).option(`--json`,`Output as JSON`).action(async e=>{let{runChannelsListCommand:t}=await import(`#cli/commands/channels.js`);await t(r,c,e)}),u.command(`build`).description(`Build the current Ash application.`).action(async()=>{let{loadDevelopmentEnvironmentFiles:e}=await import(`#cli/dev/environment.js`);e(c);let n=await(i.buildHost??await loadBuildHost())(c);r.log(renderCliTaggedLine(d,{message:`built output at ${n}`,tag:`build`,tone:`success`}))}),u.command(`start`).description(`Start a built Ash application.`).option(`--host <host>`,`Host interface to bind`).option(`--port <port>`,`Port to listen on (defaults to $PORT, then 3000)`,parsePortOption).action(async e=>{let{loadDevelopmentEnvironmentFiles:n}=await import(`#cli/dev/environment.js`);n(c);let a=await(i.startProductionHost??await loadStartProductionHost())(c,{host:e.host,port:e.port});r.log(renderCliTaggedLine(d,{message:`server listening at ${a.url}`,tag:`start`,tone:`success`})),await waitForProductionServer(a)}),u.command(`dev`).description(`Start the Ash development server or connect the REPL to an existing URL.`).option(`--host <host>`,`Host interface to bind`).option(`--no-repl`,`Start the server without the interactive REPL`).option(`--port <port>`,`Port to listen on (defaults to $PORT, then 3000)`,parsePortOption).option(`-u, --url <url>`,`Connect the REPL to an existing server URL`,parseDevelopmentServerUrl).addHelpText(`after`,`
1
+ import{createCliTheme,renderCliTaggedLine}from"#cli/ui/output.js";import{Command,CommanderError,InvalidArgumentError}from"#compiled/commander/index.js";import{resolveApplicationRoot}from"#internal/application/paths.js";import{resolveInstalledPackageInfo}from"#internal/application/package.js";import{parseDevelopmentServerUrl}from"#cli/dev/url.js";async function loadBuildHost(){return(await import(`#internal/nitro/host.js`)).buildApplication}async function loadPrintApplicationInfo(){return(await import(`#cli/commands/info.js`)).printApplicationInfo}async function loadRunDevelopmentRepl(){return(await import(`#cli/dev/repl.js`)).runDevelopmentRepl}async function loadRunEvalCommand(){return(await import(`#evals/cli/eval.js`)).runEvalCommand}async function loadStartHost(){return(await import(`#internal/nitro/host.js`)).startDevelopmentServer}async function loadStartProductionHost(){return(await import(`#internal/nitro/host.js`)).startProductionServer}function shouldPrintCliBootBanner(e){return e.name()===`info`||e.name()===`dev`}async function waitForShutdownSignal(e){await new Promise((t,n)=>{let r=!1,cleanup=()=>{process.off(`SIGINT`,handleSignal),process.off(`SIGTERM`,handleSignal)},handleSignal=()=>{r||(r=!0,cleanup(),e.close().then(t,n))};process.once(`SIGINT`,handleSignal),process.once(`SIGTERM`,handleSignal)})}async function waitForProductionServer(e){await Promise.race([e.wait(),waitForShutdownSignal({close:()=>e.close()})])}function parsePortOption(e){if(!/^-?\d+$/.test(e))throw new InvalidArgumentError(`Expected a numeric port, received "${e}".`);let t=Number(e);if(t<0||t>65535)throw new InvalidArgumentError(`Expected a port between 0 and 65535, received "${e}".`);return t}function hasInteractiveTerminal(){return!!(process.stdin.isTTY&&process.stdout.isTTY)}function rewriteDevelopmentUrlShorthand(e){let t=e[1];return e[0]!==`dev`||e.length!==2||t===void 0||t.startsWith(`-`)?[...e]:[`dev`,`--url`,t]}function resolveRemoteDevelopmentServerUrl(e){if(e.url){if(e.host!==void 0)throw new InvalidArgumentError(`The --host option cannot be used with --url.`);if(e.port!==void 0)throw new InvalidArgumentError(`The --port option cannot be used with --url.`);if(e.repl===!1)throw new InvalidArgumentError(`The --no-repl option cannot be used with --url.`);return e.url}}function createCliProgram(r,i){let c=resolveApplicationRoot(),l=resolveInstalledPackageInfo().version,u=new Command,d=createCliTheme();u.name(`ash`).description(`Build and run an Ash application.`).version(l).showHelpAfterError().exitOverride().hook(`preAction`,(e,t)=>{shouldPrintCliBootBanner(t)&&r.log(`Ash (v${l})`)}).configureOutput({writeErr:e=>{r.error(e.trimEnd())},writeOut:e=>{r.log(e.trimEnd())}});let f=u.command(`channels`).description(`Manage user-authored channels in the current project.`);return f.command(`add [kind]`).description(`Add channels interactively, or scaffold a channel kind (slack | web).`).option(`-f, --force`,`Overwrite existing channel files`).action(async(e,t)=>{let{runChannelsAddCommand:n}=await import(`#cli/commands/channels.js`);await n(r,c,{kind:e,options:t})}),f.command(`list`).description(`List user-authored channels in the current project.`).option(`--json`,`Output as JSON`).action(async e=>{let{runChannelsListCommand:t}=await import(`#cli/commands/channels.js`);await t(r,c,e)}),u.command(`build`).description(`Build the current Ash application.`).action(async()=>{let{loadDevelopmentEnvironmentFiles:e}=await import(`#cli/dev/environment.js`);e(c);let n=await(i.buildHost??await loadBuildHost())(c);r.log(renderCliTaggedLine(d,{message:`built output at ${n}`,tag:`build`,tone:`success`}))}),u.command(`start`).description(`Start a built Ash application.`).option(`--host <host>`,`Host interface to bind`).option(`--port <port>`,`Port to listen on (defaults to $PORT, then 3000)`,parsePortOption).action(async e=>{let{loadDevelopmentEnvironmentFiles:n}=await import(`#cli/dev/environment.js`);n(c);let a=await(i.startProductionHost??await loadStartProductionHost())(c,{host:e.host,port:e.port});r.log(renderCliTaggedLine(d,{message:`server listening at ${a.url}`,tag:`start`,tone:`success`})),await waitForProductionServer(a)}),u.command(`dev`).description(`Start the Ash development server or connect the REPL to an existing URL.`).option(`--host <host>`,`Host interface to bind`).option(`--no-repl`,`Start the server without the interactive REPL`).option(`--port <port>`,`Port to listen on (defaults to $PORT, then 3000)`,parsePortOption).option(`-u, --url <url>`,`Connect the REPL to an existing server URL`,parseDevelopmentServerUrl).addHelpText(`after`,`
2
2
  You can also pass a bare URL as the only argument, for example: ash dev https://example.com
3
3
  `).action(async e=>{let n=resolveRemoteDevelopmentServerUrl(e),{loadDevelopmentEnvironmentFiles:a}=await import(`#cli/dev/environment.js`);if(a(c),n){if(r.log(renderCliTaggedLine(d,{message:`REPL connecting to ${n}`,tag:`dev`,tone:`info`})),!hasInteractiveTerminal()){r.log(renderCliTaggedLine(d,{message:`Interactive REPL disabled because the current terminal is not a TTY.`,tag:`dev`,tone:`warning`}));return}r.log(``),await(i.runDevelopmentRepl??await loadRunDevelopmentRepl())({serverUrl:n});return}let o=await(i.startHost??await loadStartHost())(c,{host:e.host,port:e.port}),s=!1,closeServer=async()=>{s||(s=!0,await o.close())};try{if(r.log(renderCliTaggedLine(d,{message:`server listening at ${o.url}`,tag:`dev`,tone:`success`})),e.repl===!1)return await waitForShutdownSignal({close:closeServer});if(!hasInteractiveTerminal())return r.log(renderCliTaggedLine(d,{message:`Interactive REPL disabled because the current terminal is not a TTY.`,tag:`dev`,tone:`warning`})),await waitForShutdownSignal({close:closeServer});r.log(``),await(i.runDevelopmentRepl??await loadRunDevelopmentRepl())({serverUrl:o.url})}finally{await closeServer()}}),u.command(`info`).description(`Print resolved application information.`).action(async()=>{await(i.printApplicationInfo??await loadPrintApplicationInfo())(r,c)}),u.command(`eval`).description(`Run eval suites against an Ash agent.`).option(`--suite <id...>`,`Suite IDs to run (repeatable)`).option(`--all`,`Run all discovered suites`).option(`--url <url>`,`Remote agent URL (skip local host startup)`).option(`--timeout <ms>`,`Per-case timeout in milliseconds`).option(`--max-concurrency <n>`,`Max concurrent case executions per suite`).option(`--json`,`Output results as JSON`).option(`--skip-report`,`Skip suite-defined reporters (e.g. Braintrust)`).action(async e=>{await(i.runEvalCommand??await loadRunEvalCommand())(e,r)}),u}async function runCli(e=process.argv.slice(2),t=console,n={}){let i=createCliProgram(t,n),a=e.length===0?[`info`]:rewriteDevelopmentUrlShorthand(e);try{await i.parseAsync(a,{from:`user`})}catch(e){if(e instanceof CommanderError){if(e.exitCode===0)return;throw Error(e.message)}throw e}}export{runCli};
@@ -20,11 +20,11 @@
20
20
  "@standard-schema/spec": "1.1.0",
21
21
  "turndown": "7.2.4",
22
22
  "@vercel/oidc": "3.4.1",
23
- "@vercel/sandbox": "2.0.0",
23
+ "@vercel/sandbox": "2.0.1",
24
24
  "@workflow/core": "5.0.0-beta.7",
25
25
  "@workflow/errors": "5.0.0-beta.4",
26
26
  "zod": "4.4.3",
27
27
  "zod-validation-error": "5.0.0"
28
28
  },
29
- "scriptHash": "d4ce5a47e0f6620d682bfcba879a1eabaa5c61cc9c9cfe24e3a54c38aaf1edb0"
29
+ "scriptHash": "de60e990328c8664beec98bc5e81625ecdc0cdace1ba8bd8aa267bf9bea29af4"
30
30
  }
@@ -41,6 +41,7 @@ export interface SandboxCreateOptions {
41
41
  env?: Record<string, string> | undefined;
42
42
  name?: string | undefined;
43
43
  networkPolicy?: NetworkPolicy | undefined;
44
+ onResume?: ((sandbox: Sandbox) => Promise<void>) | undefined;
44
45
  persistent?: boolean | undefined;
45
46
  ports?: number[] | undefined;
46
47
  resources?: { vcpus?: number | undefined } | undefined;
@@ -52,6 +53,13 @@ export interface SandboxCreateOptions {
52
53
  timeout?: number | undefined;
53
54
  }
54
55
 
56
+ export interface SandboxGetOptions {
57
+ name: string;
58
+ onResume?: ((sandbox: Sandbox) => Promise<void>) | undefined;
59
+ resume?: boolean | undefined;
60
+ signal?: AbortSignal | undefined;
61
+ }
62
+
55
63
  export interface SandboxRunCommandParams {
56
64
  args?: readonly string[] | undefined;
57
65
  cmd: string;
@@ -95,9 +103,10 @@ export declare class Sandbox {
95
103
  status: string;
96
104
  tags?: Record<string, string> | undefined;
97
105
  static create(options?: SandboxCreateOptions): Promise<Sandbox>;
98
- static get(options: { name: string } | Record<string, unknown>): Promise<Sandbox>;
106
+ static get(options: SandboxGetOptions): Promise<Sandbox>;
99
107
  domain(port: number): string;
100
- readFileToBuffer(input: { path: string }): Promise<Buffer | Uint8Array | null>;
108
+ readFile(file: { path: string }): Promise<ReadableStream<Uint8Array> | null>;
109
+ readFileToBuffer(file: { path: string }): Promise<Buffer | null>;
101
110
  runCommand(input: SandboxRunCommandParams & { detached: true }): Promise<SandboxCommand>;
102
111
  runCommand(input: SandboxRunCommandParams): Promise<SandboxCommandFinished>;
103
112
  snapshot(options?: unknown): Promise<{ snapshotId: string }>;