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.
- package/CHANGELOG.md +18 -0
- package/dist/docs/public/auth-and-route-protection.md +27 -7
- package/dist/docs/public/channels/README.md +7 -3
- package/dist/docs/public/channels/slack.md +10 -4
- package/dist/docs/public/sandbox.md +42 -19
- package/dist/docs/public/session-context.md +1 -1
- package/dist/src/cli/commands/channel-add-conflicts.d.ts +21 -0
- package/dist/src/cli/commands/channel-add-conflicts.js +1 -0
- package/dist/src/cli/commands/channels.d.ts +9 -1
- package/dist/src/cli/commands/channels.js +1 -3
- package/dist/src/cli/dev/repl.js +1 -1
- package/dist/src/cli/run.js +1 -1
- package/dist/src/compiled/.vendor-stamp.json +2 -2
- package/dist/src/compiled/@vercel/sandbox/index.d.ts +11 -2
- package/dist/src/compiled/@vercel/sandbox/index.js +3 -3
- package/dist/src/compiled/@vercel/sandbox/package.json +1 -1
- package/dist/src/compiled/_chunks/node/{auth-ZhCJAHxl.js → auth-CVVvWjaK.js} +1 -1
- package/dist/src/compiled/_chunks/node/{version-D4IYmfaS.js → version-nR4RSpFw.js} +1 -1
- package/dist/src/compiler/compile-agent.js +1 -1
- package/dist/src/compiler/normalize-manifest.js +1 -1
- package/dist/src/execution/sandbox/bindings/vercel.d.ts +1 -1
- package/dist/src/execution/sandbox/session.js +1 -1
- package/dist/src/internal/application/package.js +1 -1
- package/dist/src/internal/logging.js +1 -1
- package/dist/src/internal/nitro/host/start-production-server.js +1 -1
- package/dist/src/node_modules/.pnpm/@clack_core@1.3.1/node_modules/@clack/core/dist/index.js +10 -0
- package/dist/src/node_modules/.pnpm/fast-string-truncated-width@3.0.3/node_modules/fast-string-truncated-width/dist/index.js +1 -0
- package/dist/src/node_modules/.pnpm/fast-string-truncated-width@3.0.3/node_modules/fast-string-truncated-width/dist/utils.js +1 -0
- package/dist/src/node_modules/.pnpm/fast-string-width@3.0.2/node_modules/fast-string-width/dist/index.js +1 -0
- package/dist/src/node_modules/.pnpm/fast-wrap-ansi@0.2.2/node_modules/fast-wrap-ansi/lib/main.js +5 -0
- package/dist/src/node_modules/.pnpm/picocolors@1.1.1/node_modules/picocolors/picocolors.js +1 -0
- package/dist/src/node_modules/.pnpm/sisteransi@1.0.5/node_modules/sisteransi/src/index.js +1 -0
- package/dist/src/packages/ash-scaffold/src/channels.js +12 -2
- package/dist/src/packages/ash-scaffold/src/cli/channel-add-prompter.js +1 -0
- package/dist/src/packages/ash-scaffold/src/cli/channel-setup-prompter.js +1 -0
- package/dist/src/packages/ash-scaffold/src/cli/command-output.js +1 -0
- package/dist/src/packages/ash-scaffold/src/cli/index.js +1 -0
- package/dist/src/packages/ash-scaffold/src/cli/prompt-ui.js +3 -0
- package/dist/src/packages/ash-scaffold/src/cli/rail-log.js +2 -0
- package/dist/src/packages/ash-scaffold/src/primitives/detect-deployment.js +1 -0
- package/dist/src/packages/ash-scaffold/src/primitives/index.js +1 -0
- package/dist/src/packages/ash-scaffold/src/primitives/pnpm-invocation.js +1 -0
- package/dist/src/packages/ash-scaffold/src/primitives/process-output.js +1 -0
- package/dist/src/packages/ash-scaffold/src/primitives/run-pnpm.js +1 -0
- package/dist/src/packages/ash-scaffold/src/primitives/run-vercel.js +1 -0
- package/dist/src/packages/ash-scaffold/src/primitives/update-slack-channel.js +1 -0
- package/dist/src/packages/ash-scaffold/src/project.js +1 -1
- package/dist/src/packages/ash-scaffold/src/steps/deploy-to-vercel.js +1 -0
- package/dist/src/packages/ash-scaffold/src/steps/index.js +1 -0
- package/dist/src/packages/ash-scaffold/src/steps/run-add-to-agent.js +2 -0
- package/dist/src/packages/ash-scaffold/src/steps/setup-slackbot.js +1 -0
- package/dist/src/packages/ash-scaffold/src/web-template.js +4713 -0
- package/dist/src/public/channels/auth.d.ts +22 -11
- package/dist/src/public/channels/auth.js +1 -1
- package/dist/src/public/definitions/sandbox.d.ts +1 -1
- package/dist/src/public/next/server.js +1 -1
- package/dist/src/public/sandbox/index.d.ts +1 -1
- package/dist/src/public/sandbox/vercel-sandbox.d.ts +4 -4
- package/dist/src/runtime/governance/auth/oidc.js +1 -1
- package/dist/src/runtime/governance/auth/token-claims.d.ts +2 -0
- package/dist/src/runtime/governance/auth/token-claims.js +1 -1
- package/dist/src/runtime/governance/auth/types.d.ts +6 -0
- package/dist/src/shared/sandbox-session.d.ts +0 -17
- 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
|
-
##
|
|
18
|
+
## Generated Web Chat Auth
|
|
19
19
|
|
|
20
|
-
`pnpm create experimental-ash-agent` scaffolds `agent/channels/ash.ts`
|
|
21
|
-
|
|
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(),
|
|
39
|
+
auth: [vercelOidc(), localDev(), exampleProductionAuth()],
|
|
30
40
|
});
|
|
31
41
|
```
|
|
32
42
|
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
115
|
-
|
|
116
|
-
|
|
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
|
|
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.
|
|
89
|
-
|
|
90
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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 `
|
|
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.
|
|
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 `
|
|
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
|
|
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.
|
|
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
|
|
286
|
-
`Sandbox.
|
|
287
|
-
|
|
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
|
-
|
|
365
|
-
|
|
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.
|
|
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
|
|
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
|
|
@@ -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{
|
|
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};
|
package/dist/src/cli/dev/repl.js
CHANGED
|
@@ -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{
|
|
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};
|
package/dist/src/cli/run.js
CHANGED
|
@@ -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(`
|
|
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.
|
|
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": "
|
|
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:
|
|
106
|
+
static get(options: SandboxGetOptions): Promise<Sandbox>;
|
|
99
107
|
domain(port: number): string;
|
|
100
|
-
|
|
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 }>;
|