@alexkroman1/aai-cli 13.3.0 → 15.0.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 (158) hide show
  1. package/dist/{_agent-Clbq3a4h.mjs → _agent-BzUeqOdj.mjs} +32 -9
  2. package/dist/_agent.d.ts +23 -0
  3. package/dist/_artifacts-BJOYGQPp.mjs +21 -0
  4. package/dist/_artifacts.d.ts +16 -0
  5. package/dist/_build-target.d.ts +172 -0
  6. package/dist/{_bundler-DEkjO1d8.mjs → _bundler-DM0d0M7m.mjs} +8 -7
  7. package/dist/_cli-common.d.ts +45 -6
  8. package/dist/{_config-ZY2euhdh.mjs → _config-DYzC6WMD.mjs} +2 -2
  9. package/dist/_dev-env.d.ts +1 -1
  10. package/dist/{_dev-server-C13xuGB1.mjs → _dev-server-BzWB6-4y.mjs} +25 -24
  11. package/dist/_dev-vite-config.d.ts +3 -3
  12. package/dist/_e2e-test-utils.d.ts +1 -1
  13. package/dist/{_init-Ci4QGjp8.mjs → _init-Bsi3DZNJ.mjs} +9 -23
  14. package/dist/{_output-DbJt9gsl.mjs → _output-DBP9Op_d.mjs} +22 -13
  15. package/dist/_output.d.ts +10 -9
  16. package/dist/{_preflight-B2laWPdU.mjs → _preflight-CcAR577V.mjs} +2 -2
  17. package/dist/_resource-commands.d.ts +9 -9
  18. package/dist/_server-common-De0haHr9.mjs +70 -0
  19. package/dist/_server-common.d.ts +20 -1
  20. package/dist/{_slug-api-DlKpj91X.mjs → _slug-api-CGvc2YSQ.mjs} +18 -9
  21. package/dist/_slug-api.d.ts +19 -0
  22. package/dist/{_studio-DTi3EdXQ.mjs → _studio-DnR_BqFp.mjs} +7 -3
  23. package/dist/_studio-commands.d.ts +13 -13
  24. package/dist/{_templates-V4l5sZJy.mjs → _templates-CIlJ3Vay.mjs} +4 -4
  25. package/dist/_templates.d.ts +1 -1
  26. package/dist/{_typecheck-gate-sV4N5F93.mjs → _typecheck-gate-U3jcil8L.mjs} +3 -2
  27. package/dist/_typecheck-gate.d.ts +3 -1
  28. package/dist/{_utils-Dhd058D9.mjs → _utils-D5JGcjiW.mjs} +43 -1
  29. package/dist/_utils.d.ts +35 -0
  30. package/dist/_vercel-output.d.ts +63 -0
  31. package/dist/{_workflow-determinism-C9l8uC2R.mjs → _workflow-determinism-CvpqMpdh.mjs} +18 -26
  32. package/dist/_workflow-determinism.d.ts +3 -3
  33. package/dist/build-BhEaxBPu.mjs +481 -0
  34. package/dist/build.d.ts +23 -10
  35. package/dist/cli.d.ts +1 -2
  36. package/dist/cli.mjs +155 -138
  37. package/dist/{client-bundler-CT-_26c5.mjs → client-bundler-6mTLs6ny.mjs} +5 -5
  38. package/dist/client-bundler.d.ts +1 -1
  39. package/dist/client-bundler.mjs +1 -1
  40. package/dist/{delete-RqlQZ6Y_.mjs → delete-BdFnbnVR.mjs} +4 -4
  41. package/dist/{deploy-B7f3CK7v.mjs → deploy-CGqPU5U-.mjs} +11 -11
  42. package/dist/{dev-DamXieW_.mjs → dev-Bx9gYBHM.mjs} +3 -3
  43. package/dist/{eval-h3DTVfRw.mjs → eval-B3I7FqN9.mjs} +3 -3
  44. package/dist/{init-D-vFrV8h.mjs → init-CFyusRbq.mjs} +48 -20
  45. package/dist/init.d.ts +14 -0
  46. package/dist/{login-CDiemNmC.mjs → login-DaLrPOFg.mjs} +4 -6
  47. package/dist/{logs-DGwPNCzB.mjs → logs-7BRwq0cG.mjs} +21 -14
  48. package/dist/project-config.mjs +1 -1
  49. package/dist/scaffold/CLAUDE.md +155 -76
  50. package/dist/scaffold/package.json +6 -6
  51. package/dist/{secret-V1w7y9-j.mjs → secret-Cjp912yF.mjs} +5 -5
  52. package/dist/secret.d.ts +8 -6
  53. package/dist/start.d.ts +112 -0
  54. package/dist/start.mjs +156 -0
  55. package/dist/{studio-BpMCfUNx.mjs → studio-C_zuRC_z.mjs} +13 -15
  56. package/dist/templates/briefing-desk/agent.eval.test.ts +156 -0
  57. package/dist/templates/call-audit/agent.test.ts +11 -6
  58. package/dist/templates/call-audit/agent.ts +2 -2
  59. package/dist/templates/call-audit/client.tsx +27 -42
  60. package/dist/templates/call-audit/workflows/audit.ts +7 -5
  61. package/dist/templates/call-audit/workflows/ingest.ts +7 -7
  62. package/dist/templates/call-audit/workflows/media.ts +12 -37
  63. package/dist/templates/call-audit/workflows/summarize.ts +8 -8
  64. package/dist/templates/call-audit/workflows/sync-api.ts +3 -3
  65. package/dist/templates/code-interpreter/agent.test.ts +103 -0
  66. package/dist/templates/dispatch-center/agent.test.ts +7 -7
  67. package/dist/templates/dispatch-center/client.tsx +53 -61
  68. package/dist/templates/dispatch-center/shared.ts +0 -8
  69. package/dist/templates/dispatch-center/tools/ops_dashboard.ts +4 -1
  70. package/dist/templates/embedded-assets/agent.eval.test.ts +4 -2
  71. package/dist/templates/health-assistant/agent.eval.test.ts +4 -2
  72. package/dist/templates/infocom-adventure/client.tsx +40 -36
  73. package/dist/templates/link-digest/agent.test.ts +3 -3
  74. package/dist/templates/link-digest/agent.ts +2 -2
  75. package/dist/templates/link-digest/client.tsx +81 -19
  76. package/dist/templates/link-digest/workflows/digest.ts +13 -16
  77. package/dist/templates/math-buddy/agent.test.ts +126 -0
  78. package/dist/templates/night-owl/agent.eval.test.ts +4 -2
  79. package/dist/templates/night-owl/client.tsx +8 -2
  80. package/dist/templates/personal-finance/agent.test.ts +127 -0
  81. package/dist/templates/pipeline-simple/agent.test.ts +2 -2
  82. package/dist/templates/pizza-ordering/client.tsx +5 -4
  83. package/dist/templates/pizza-ordering/shared.ts +3 -8
  84. package/dist/templates/pizza-ordering/tools/add_pizza.ts +3 -2
  85. package/dist/templates/pizza-ordering/tools/place_order.ts +3 -2
  86. package/dist/templates/pizza-ordering/tools/remove_pizza.ts +3 -2
  87. package/dist/templates/pizza-ordering/tools/update_pizza.ts +3 -2
  88. package/dist/templates/pizza-ordering/tools/view_order.ts +3 -2
  89. package/dist/templates/plan-and-execute/agent.test.ts +10 -5
  90. package/dist/templates/plan-and-execute/client.tsx +2 -2
  91. package/dist/templates/podcast-digest/agent.test.ts +20 -11
  92. package/dist/templates/podcast-digest/client.tsx +16 -22
  93. package/dist/templates/podcast-digest/workflows/digest.ts +21 -19
  94. package/dist/templates/podcast-digest/workflows/feeds.ts +61 -33
  95. package/dist/templates/podcast-digest/workflows/slack.ts +7 -7
  96. package/dist/templates/recap-workflow/agent.eval.test.ts +21 -13
  97. package/dist/templates/recap-workflow/agent.test.ts +27 -23
  98. package/dist/templates/recap-workflow/workflows/recap.ts +24 -28
  99. package/dist/templates/redline/agent.eval.test.ts +5 -8
  100. package/dist/templates/redline/agent.test.ts +1 -1
  101. package/dist/templates/redline/client.tsx +36 -34
  102. package/dist/templates/redline/workflows/redline.ts +17 -17
  103. package/dist/templates/research-workflow/agent.eval.test.ts +5 -5
  104. package/dist/templates/research-workflow/agent.test.ts +10 -7
  105. package/dist/templates/research-workflow/workflows/research.ts +28 -30
  106. package/dist/templates/retail/agent.test.ts +17 -15
  107. package/dist/templates/retail/cancel.ts +7 -6
  108. package/dist/templates/retail/client.tsx +39 -58
  109. package/dist/templates/retail/payment.ts +4 -3
  110. package/dist/templates/retail/pending.test.ts +19 -19
  111. package/dist/templates/retail/store.test.ts +5 -5
  112. package/dist/templates/retail/swap.ts +8 -11
  113. package/dist/templates/solo-rpg/agent.eval.test.ts +2 -16
  114. package/dist/templates/solo-rpg/agent.test.ts +12 -12
  115. package/dist/templates/solo-rpg/client.tsx +3 -3
  116. package/dist/templates/spoken-summary/agent.test.ts +9 -9
  117. package/dist/templates/spoken-summary/agent.ts +3 -3
  118. package/dist/templates/spoken-summary/client.tsx +23 -30
  119. package/dist/templates/spoken-summary/workflows/summarize.ts +11 -11
  120. package/dist/templates/spoken-summary/workflows/transcribe.ts +14 -14
  121. package/dist/templates/support-line/agent.ts +8 -0
  122. package/dist/templates/support-line/client.tsx +4 -3
  123. package/dist/templates/support-line/prompts.ts +0 -2
  124. package/dist/templates/transcription-workflow/agent.test.ts +20 -12
  125. package/dist/templates/transcription-workflow/agent.ts +2 -2
  126. package/dist/templates/transcription-workflow/client.tsx +15 -14
  127. package/dist/templates/transcription-workflow/recover.ts +1 -1
  128. package/dist/templates/transcription-workflow/run-panel.tsx +34 -14
  129. package/dist/templates/transcription-workflow/total-latency.tsx +46 -20
  130. package/dist/templates/transcription-workflow/workflows/batch.ts +15 -15
  131. package/dist/templates/transcription-workflow/workflows/downsample.ts +1 -1
  132. package/dist/templates/transcription-workflow/workflows/normalize.ts +15 -19
  133. package/dist/templates/transcription-workflow/workflows/stream.ts +12 -12
  134. package/dist/templates/transcription-workflow/workflows/sync-api.ts +3 -3
  135. package/dist/templates/transcription-workflow/workflows/transcribe.ts +23 -34
  136. package/dist/templates/travel-concierge/agent.eval.test.ts +2 -2
  137. package/dist/templates/travel-concierge/agent.test.ts +15 -7
  138. package/dist/templates/travel-concierge/client.tsx +2 -2
  139. package/dist/templates/travel-concierge/routing.ts +64 -55
  140. package/dist/templates/travel-concierge/shared.ts +10 -14
  141. package/dist/templates/travel-concierge/tools/cancel_action.ts +3 -1
  142. package/dist/templates/travel-concierge/tools/complete_or_escalate.ts +3 -1
  143. package/dist/templates/travel-concierge/tools/confirm_action.ts +3 -1
  144. package/dist/templates/travel-concierge/tools/lookup_booking.ts +4 -3
  145. package/dist/templates/travel-concierge/tools/search_car_rentals.ts +3 -2
  146. package/dist/templates/travel-concierge/tools/search_excursions.ts +3 -2
  147. package/dist/templates/travel-concierge/tools/search_flights.ts +3 -2
  148. package/dist/templates/travel-concierge/tools/search_hotels.ts +3 -2
  149. package/dist/templates/web-researcher/agent.test.ts +130 -0
  150. package/dist/{test-guenJUxi.mjs → test-D032V2cA.mjs} +4 -17
  151. package/dist/typecheck.mjs +1 -1
  152. package/dist/worker-bundler.d.ts +1 -1
  153. package/dist/worker-bundler.mjs +8 -8
  154. package/dist/{workflow-BZyKLkhV.mjs → workflow-CVioyO3V.mjs} +6 -5
  155. package/package.json +9 -4
  156. package/dist/_server-common-BV4_CrP3.mjs +0 -43
  157. package/dist/build-CWyFGe-W.mjs +0 -69
  158. package/dist/scaffold/server.mjs +0 -204
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Assemble a PREBUILT Vercel deployment — `.vercel/output/` — from what
3
+ * `aai build` just produced.
4
+ *
5
+ * The argument for the Build Output API over an `api/` entry is in
6
+ * `_build-target.ts` at {@link VERCEL_OUTPUT_DIR}; what this module adds is the
7
+ * consequence of it. A `.func` directory is a directory WE fill, so every file
8
+ * the server reads at runtime is present because it was copied in, and nothing
9
+ * depends on a static tracer following a path it structurally cannot:
10
+ *
11
+ * - **`.aai/worker.mjs`** is loaded through `import(pathToFileURL(...))`
12
+ * (`start.ts`), which `@vercel/nft` cannot resolve. Under the `api/` shape
13
+ * the build was green and the function 500'd on its first request with
14
+ * "No built agent at .aai/worker.mjs".
15
+ * - **`.env.example`** is not documentation here. `resolveServerEnv` treats it
16
+ * as the DECLARATION of which variables become `ctx.env` (see
17
+ * `DEPLOY_ENV_FILES`), so a function without it hands every tool an empty
18
+ * env while the Vercel project has the values set — a failure that looks
19
+ * like a credential problem and is a packaging one.
20
+ * - **The client** is copied to `static/`, where the CDN serves it, and also
21
+ * beside the worker, so `resolveClientDir` finds a real directory rather
22
+ * than reaching into a `node_modules` that the bundle replaced.
23
+ *
24
+ * The entry is BUNDLED rather than shipped with a `node_modules`. Same call as
25
+ * the worker (`ssr: { noExternal: true }`, `root: cwd`), so it resolves the
26
+ * project's own installed SDK and pulls the runtime, `ws` and `pg` in with it
27
+ * — which is already the arrangement a deployed worker runs under, and it is
28
+ * what makes the function independent of whether the host's install left a
29
+ * usable, hoisted `node_modules` behind.
30
+ */
31
+ /** Options for {@link emitVercelOutput}. */
32
+ export interface EmitVercelOutputOptions {
33
+ /**
34
+ * Produce the function's `index.mjs`. Defaults to {@link bundleEntry}.
35
+ *
36
+ * Injectable so the ASSEMBLY — which file lands where, and which does not —
37
+ * can be asserted without a ~15s rolldown pass over the whole runtime. The
38
+ * bundle has its own contract and its own (scenario-tier) test; what this
39
+ * seam separates is a question about a directory from a question about a
40
+ * bundler.
41
+ */
42
+ bundle?: (cwd: string) => Promise<string>;
43
+ }
44
+ /**
45
+ * Write `.vercel/output/` for this project.
46
+ *
47
+ * The directory is REMOVED first. It is not addressed by content, so a
48
+ * function or a static asset left by an earlier build with a different shape
49
+ * would be deployed alongside this one — and `vercel deploy --prebuilt`
50
+ * uploads whatever is there.
51
+ */
52
+ export declare function emitVercelOutput(cwd: string, options?: EmitVercelOutputOptions): Promise<void>;
53
+ /**
54
+ * Bundle {@link VERCEL_ENTRY_SOURCE} and everything it imports into one ESM
55
+ * file.
56
+ *
57
+ * The entry is written INTO the project rather than a temp directory, because
58
+ * that is what makes `@alexkroman1/aai-cli/start` resolve against the user's
59
+ * install — the deployed server is then the version their lockfile pins, which
60
+ * is the same guarantee `aai publish` gives. Removed in a `finally`: a build
61
+ * that throws must not leave a file that looks authored.
62
+ */
63
+ export declare function bundleEntry(cwd: string): Promise<string>;
@@ -24,12 +24,12 @@ import { readFile, readdir } from "node:fs/promises";
24
24
  * compiling every fence in this repo's shipped docs:
25
25
  *
26
26
  * ```ts
27
- * import type { WorkflowCtx } from "@alexkroman1/aai";
27
+ * import type { WorkflowContext } from "@alexkroman1/aai";
28
28
  *
29
29
  * declare const charge: () => Promise<string>;
30
30
  *
31
- * export async function body(ctx: WorkflowCtx, coin: string): Promise<void> {
32
- * // No error. The banned shape, against the real `WorkflowCtx`.
31
+ * export async function body(ctx: WorkflowContext, coin: string): Promise<void> {
32
+ * // No error. The banned shape, against the real `WorkflowContext`.
33
33
  * await ctx.step(`charge-${coin}`, charge);
34
34
  * }
35
35
  * ```
@@ -81,19 +81,6 @@ import { readFile, readdir } from "node:fs/promises";
81
81
  * @module
82
82
  */
83
83
  /**
84
- * The three `ctx` methods whose first argument is a journal identity.
85
- *
86
- * All three key a journal row by it — `name#occurrence` for a step,
87
- * `sleep!<label>#<n>` and `hook!<token>#<n>` for the waits — so a computed one
88
- * mints a row no earlier walk reached in exactly the same way. Kept as a list
89
- * so the message can name the method that was called.
90
- */
91
- const IDENTITY_METHODS = [
92
- "step",
93
- "sleep",
94
- "waitFor"
95
- ];
96
- /**
97
84
  * A `ctx.<method>(` immediately followed by a BACKTICK.
98
85
  *
99
86
  * The whole check, and its narrowness is the point. A quoted literal is
@@ -104,8 +91,17 @@ const IDENTITY_METHODS = [
104
91
  * The receiver is not pinned to `ctx` — a body may destructure or rename it —
105
92
  * and a `.step(` on anything else taking a template-literal first argument is
106
93
  * not a shape worth excluding.
94
+ *
95
+ * Shared rather than built per call, which is safe ONLY because the single
96
+ * reader is `matchAll`: that method species-constructs its own clone and never
97
+ * advances this object's `lastIndex`. A `.exec` or `.test` added against a `/g`
98
+ * regex would carry state between lines — build a fresh one there.
107
99
  */
108
- const identityPattern = () => new RegExp(`\\.(${IDENTITY_METHODS.join("|")})\\(\``, "g");
100
+ const IDENTITY_PATTERN = new RegExp(`\\.(${[
101
+ "step",
102
+ "sleep",
103
+ "waitFor"
104
+ ].join("|")})\\(\``, "g");
109
105
  /** Whether a template literal actually INTERPOLATES, or is merely quoted oddly. */
110
106
  const INTERPOLATES = /\$\{/;
111
107
  /**
@@ -132,7 +128,7 @@ function findComputedIdentities(source, file) {
132
128
  const findings = [];
133
129
  for (const [n, line] of source.split("\n").entries()) {
134
130
  if (isCommentOnly(line)) continue;
135
- for (const match of line.matchAll(identityPattern())) {
131
+ for (const match of line.matchAll(IDENTITY_PATTERN)) {
136
132
  const method = match[1];
137
133
  if (method === void 0) continue;
138
134
  if (!INTERPOLATES.test(line.slice((match.index ?? 0) + match[0].length))) continue;
@@ -169,17 +165,13 @@ async function workflowFiles(cwd) {
169
165
  * warning pass beside a typecheck that has already run on the same tree.
170
166
  */
171
167
  async function scanWorkflowDeterminism(cwd) {
172
- const findings = [];
173
- for (const rel of await workflowFiles(cwd)) {
174
- let source;
168
+ return (await Promise.all((await workflowFiles(cwd)).map(async (rel) => {
175
169
  try {
176
- source = await readFile(path.join(cwd, rel), "utf-8");
170
+ return findComputedIdentities(await readFile(path.join(cwd, rel), "utf-8"), rel);
177
171
  } catch {
178
- continue;
172
+ return [];
179
173
  }
180
- findings.push(...findComputedIdentities(source, rel));
181
- }
182
- return findings;
174
+ }))).flat();
183
175
  }
184
176
  /**
185
177
  * The remedy, once, however many findings there are.
@@ -20,12 +20,12 @@
20
20
  * compiling every fence in this repo's shipped docs:
21
21
  *
22
22
  * ```ts
23
- * import type { WorkflowCtx } from "@alexkroman1/aai";
23
+ * import type { WorkflowContext } from "@alexkroman1/aai";
24
24
  *
25
25
  * declare const charge: () => Promise<string>;
26
26
  *
27
- * export async function body(ctx: WorkflowCtx, coin: string): Promise<void> {
28
- * // No error. The banned shape, against the real `WorkflowCtx`.
27
+ * export async function body(ctx: WorkflowContext, coin: string): Promise<void> {
28
+ * // No error. The banned shape, against the real `WorkflowContext`.
29
29
  * await ctx.step(`charge-${coin}`, charge);
30
30
  * }
31
31
  * ```
@@ -0,0 +1,481 @@
1
+ #!/usr/bin/env node
2
+ import { a as ok, t as CliError } from "./_output-DBP9Op_d.mjs";
3
+ import { n as log, r as notify } from "./_ui-DCt4qZrk.mjs";
4
+ import { n as withPreservedNodeEnv } from "./_vite-env-kejF0p2L.mjs";
5
+ import { n as WORKER_ARTIFACT_REL, t as CLIENT_ARTIFACT_REL } from "./_artifacts-BJOYGQPp.mjs";
6
+ import { r as evalWorkerBundle, t as buildAgentBundle } from "./_bundler-DM0d0M7m.mjs";
7
+ import { assertTypechecks } from "./_typecheck-gate-U3jcil8L.mjs";
8
+ import { n as scanWorkflowDeterminism, t as determinismWarnings } from "./_workflow-determinism-CvpqMpdh.mjs";
9
+ import { TEST_FILES, classifyVitestError, runVitest } from "./test-D032V2cA.mjs";
10
+ import path from "node:path";
11
+ import fs from "node:fs/promises";
12
+ import { invariant } from "@alexkroman1/aai/internal";
13
+ import { build } from "vite";
14
+ import { defaultClientDir } from "@alexkroman1/aai-ui/client-dir";
15
+ import { agentConfigWarnings } from "@alexkroman1/aai/manifest";
16
+ import { DEFAULT_SYSTEM_PROMPT } from "@alexkroman1/aai";
17
+ //#region src/_build-target.ts
18
+ /**
19
+ * Deployment TARGETS — what `aai build` emits beside the worker so a host can
20
+ * run this project without the project holding anything host-specific.
21
+ *
22
+ * ## Why a target rather than a file in the scaffold
23
+ *
24
+ * Every host wants a different entry shape: Vercel wants a module whose default
25
+ * export is an `http.Server` it binds itself, a container wants a long-lived
26
+ * process, another platform wants something else again. Committing one of those
27
+ * to the scaffold makes every project assert a fact that is load-bearing on
28
+ * exactly one host — the same objection that removed `server.mjs`.
29
+ *
30
+ * Nitro is the worked precedent: one codebase, a preset per provider, and the
31
+ * preset EMITS the host's expected shape into the build directory. `node-server`
32
+ * is its default and Vercel/Netlify/Cloudflare are detected from the CI
33
+ * environment with no configuration. Next does the same narrower thing with
34
+ * `output: "standalone"` — it generates a `server.js` rather than asking anyone
35
+ * to write one. Either way the user's repository contains no host file, which is
36
+ * the property to preserve here.
37
+ *
38
+ * ## Auto-detection, and why it is safe
39
+ *
40
+ * A target is chosen with `--target`, or detected from the environment when the
41
+ * flag is absent. Detection reads the variables the hosts set on their own build
42
+ * containers ({@link TARGET_ENV_MARKERS}), so it only ever fires where the build
43
+ * is genuinely running on that host — a laptop sets none of them and gets
44
+ * {@link DEFAULT_BUILD_TARGET}, which emits nothing extra and is what every
45
+ * existing project already does.
46
+ */
47
+ /** The targets `aai build --target` accepts. */
48
+ const BUILD_TARGETS = ["node", "vercel"];
49
+ /**
50
+ * What a build with no `--target` and no host environment produces: the worker
51
+ * and the client, and nothing else.
52
+ *
53
+ * `node` rather than a `"none"` sentinel because it NAMES the deployment it
54
+ * serves — a long-lived process running `aai start` — which is what every
55
+ * container platform wants and what the scaffold's own `start` script runs.
56
+ */
57
+ const DEFAULT_BUILD_TARGET = "node";
58
+ /**
59
+ * The environment variable each host sets on its own build container.
60
+ *
61
+ * `VERCEL` is set for every Vercel build and deployment. Detection is per host
62
+ * rather than a single "am I in CI" test, because a GitHub Action building a
63
+ * container image is CI too and wants the default.
64
+ */
65
+ const TARGET_ENV_MARKERS = { VERCEL: "vercel" };
66
+ function isBuildTarget(value) {
67
+ return BUILD_TARGETS.includes(value);
68
+ }
69
+ /**
70
+ * Resolve the target for this build: an explicit flag wins, then the
71
+ * environment, then {@link DEFAULT_BUILD_TARGET}.
72
+ *
73
+ * An unrecognised `--target` is REFUSED naming what is accepted, rather than
74
+ * falling back to the default — a typo'd target that silently built the default
75
+ * would deploy a project missing the entry its host needs, and the failure would
76
+ * arrive as a 404 from the platform rather than as an error from the build.
77
+ */
78
+ function resolveBuildTarget(explicit, env = process.env) {
79
+ if (explicit !== void 0) {
80
+ if (!isBuildTarget(explicit)) throw new Error(`Unknown build target "${explicit}". Accepted: ${BUILD_TARGETS.join(", ")}.`);
81
+ return explicit;
82
+ }
83
+ for (const [marker, target] of Object.entries(TARGET_ENV_MARKERS)) if (env[marker]) return target;
84
+ return DEFAULT_BUILD_TARGET;
85
+ }
86
+ /**
87
+ * Where a PREBUILT Vercel deployment lives, relative to the project root.
88
+ *
89
+ * The Build Output API rather than an `api/` entry plus a `vercel.json`, and
90
+ * the reason is ORDERING. Vercel reads `vercel.json` and decides what to build
91
+ * BEFORE it runs the build command, so a `vercel.json` that the build WRITES
92
+ * configures the NEXT deployment and not this one — a clean clone deploys with
93
+ * no rewrite and no function at all. The `api/` shape only ever appeared to
94
+ * work because a previous local `aai build --target vercel` had left both
95
+ * files in the working tree, which is a property of one laptop rather than of
96
+ * the repository. `.vercel/output/` is read AFTER the build command; it is the
97
+ * only place a build can describe its own deployment.
98
+ *
99
+ * Two more things fall out of owning the directory, both of which the `api/`
100
+ * shape got wrong and could not fix. The function bundle is ASSEMBLED here
101
+ * rather than traced, so `.aai/worker.mjs` — loaded through a dynamic
102
+ * `import(pathToFileURL(...))` that no static tracer can follow — and
103
+ * `.env.example` — the file that DECLARES which variables become `ctx.env` —
104
+ * are present because they were copied in. And the built client is served by
105
+ * the CDN out of `static/` instead of through the function.
106
+ *
107
+ * Nitro's vercel preset is the worked precedent and lands in exactly here:
108
+ * `output.dir = {{rootDir}}/.vercel/output`, `serverDir` a `.func` under it.
109
+ *
110
+ * @see https://vercel.com/docs/build-output-api/v3
111
+ */
112
+ const VERCEL_OUTPUT_DIR = path.join(".vercel", "output");
113
+ /**
114
+ * The one function every request that is not a static file reaches.
115
+ *
116
+ * The Build Output API derives a function's ROUTE from its path, so the
117
+ * directory name IS a URL and must not collide with one the static output
118
+ * claims. **`index.func` collides**, which a deployment is the only way to
119
+ * find out: it is served at `/index`, and Vercel's directory index resolves
120
+ * `/` to the extensionless `/index` — so the function won `/`, every other
121
+ * asset came off the CDN correctly, and the home page 500'd on a deployment
122
+ * whose static output was perfect. Measured on a real preview:
123
+ * `/favicon.ico`, `/index.html` and both hashed `/assets/*` returned 200
124
+ * while `/` and `/index` did not.
125
+ *
126
+ * `__server` is Nitro's answer to the same problem (`__server.func`) and the
127
+ * reason is this one: a double-underscore prefix is not a path any bundler
128
+ * emits, so no static file can ever take the name.
129
+ */
130
+ const VERCEL_FUNCTION_DIR = path.join(VERCEL_OUTPUT_DIR, "functions", "__server.func");
131
+ /**
132
+ * The route {@link VERCEL_FUNCTION_DIR} is served at — its directory name
133
+ * without `.func`, which is how the Build Output API names a function.
134
+ */
135
+ const VERCEL_FUNCTION_ROUTE = "/__server";
136
+ /** Static assets the Vercel CDN serves directly, never reaching the function. */
137
+ const VERCEL_STATIC_DIR = path.join(VERCEL_OUTPUT_DIR, "static");
138
+ /**
139
+ * Node versions Vercel offers. A build on anything newer picks the newest of
140
+ * these rather than naming a runtime the platform will reject.
141
+ *
142
+ * @see https://vercel.com/docs/functions/runtimes/node-js/node-js-versions
143
+ */
144
+ const SUPPORTED_NODE_MAJORS = [
145
+ 20,
146
+ 22,
147
+ 24
148
+ ];
149
+ /** `nodejs<major>.x` for the Node running this build, clamped to what Vercel offers. */
150
+ function vercelNodeRuntime(version = process.versions.node) {
151
+ const major = Number.parseInt(version.split(".")[0] ?? "", 10);
152
+ return `nodejs${Number.isNaN(major) ? 22 : SUPPORTED_NODE_MAJORS.findLast((v) => v <= major) ?? SUPPORTED_NODE_MAJORS[0]}.x`;
153
+ }
154
+ /**
155
+ * The Vercel function entry, emitted into {@link VERCEL_FUNCTION_DIR}.
156
+ *
157
+ * ## Why a `(req, res)` handler and not `export default server`
158
+ *
159
+ * `export default <http.Server>` is what Vercel's own `@vercel/node` BUILDER
160
+ * accepts, and it is the shape the previous `api/index.mjs` used. The Build
161
+ * Output API has no builder in the path: `launcherType: "Nodejs"` invokes the
162
+ * module's default export as a request handler, so the server never gets
163
+ * bound and there is nothing to raise an `upgrade` event on it.
164
+ *
165
+ * ## How a WebSocket survives that
166
+ *
167
+ * Vercel hands a Node function the raw upgrade through its PER-REQUEST
168
+ * context — `globalThis[Symbol.for("@vercel/request-context")].get()
169
+ * .upgradeWebSocket()` returns the `{ req, socket, head }` triple — rather
170
+ * than as an event. Nitro reaches it through `crossws/adapters/vercel`; here
171
+ * the adapter is three lines, because {@link AgentServer.node} is a real
172
+ * `http.Server` that already has both an `upgrade` and a `request` listener
173
+ * registered. Re-emitting onto it is the whole translation, and it means the
174
+ * deployed path through `server.ts` is the same one `aai dev` and `aai start`
175
+ * take — no second WebSocket entry point to keep in step.
176
+ *
177
+ * The `204` afterwards is what the launcher needs to consider the invocation
178
+ * finished; the socket the agent is now talking on is not this `res`.
179
+ *
180
+ * ## `import.meta.dirname`, not `process.cwd()`
181
+ *
182
+ * The function's working directory belongs to the platform, but `.aai/` and
183
+ * `.env.example` were copied in BESIDE this file. Resolving from the module
184
+ * keeps that a fact about the bundle rather than about how Vercel happens to
185
+ * invoke it.
186
+ */
187
+ const VERCEL_ENTRY_SOURCE = `// Generated by \`aai build --target vercel\` — do not edit, and do not commit.
188
+ // Vercel invokes this handler per request and delivers a WebSocket upgrade
189
+ // through its request context. See @alexkroman1/aai-cli/start.
190
+ import { createProjectServer } from "@alexkroman1/aai-cli/start";
191
+
192
+ const server = (await createProjectServer({ cwd: import.meta.dirname })).node;
193
+
194
+ const REQUEST_CONTEXT = Symbol.for("@vercel/request-context");
195
+
196
+ export default function handler(req, res) {
197
+ if (req.method === "GET" && req.headers.upgrade?.toLowerCase() === "websocket") {
198
+ const upgrade = globalThis[REQUEST_CONTEXT]?.get?.()?.upgradeWebSocket?.();
199
+ if (upgrade) {
200
+ server.emit("upgrade", upgrade.req, upgrade.socket, upgrade.head);
201
+ if (!res.headersSent && !res.writableEnded) {
202
+ res.statusCode = 204;
203
+ res.end();
204
+ }
205
+ return;
206
+ }
207
+ }
208
+ server.emit("request", req, res);
209
+ }
210
+ `;
211
+ /**
212
+ * `.vc-config.json` — how the platform runs {@link VERCEL_ENTRY_SOURCE}.
213
+ *
214
+ * `supportsResponseStreaming` is not optional here: an agent streams TTS audio
215
+ * and SSE workflow events, and without it the platform buffers a response to
216
+ * completion, which for a stream that ends when the call does means it never
217
+ * arrives. `shouldAddHelpers` stays off — the entry speaks `node:http`, and
218
+ * the helpers exist to bolt Express-shaped sugar onto a handler that does not.
219
+ */
220
+ function vercelFunctionConfigSource(runtime = vercelNodeRuntime()) {
221
+ return `${JSON.stringify({
222
+ runtime,
223
+ handler: "index.mjs",
224
+ launcherType: "Nodejs",
225
+ shouldAddHelpers: false,
226
+ supportsResponseStreaming: true
227
+ }, null, 2)}\n`;
228
+ }
229
+ /**
230
+ * `config.json` — the routing table, and the reason static assets stop paying
231
+ * for a function invocation.
232
+ *
233
+ * `{ "handle": "filesystem" }` serves anything present in
234
+ * {@link VERCEL_STATIC_DIR} from the CDN and only then falls through, so the
235
+ * client bundle, its assets and the worklets are edge-served while
236
+ * `/client-config`, `/websocket`, `/workflows/*` and the webhook route reach
237
+ * the agent. The `api/` shape routed EVERY request through the function, which
238
+ * this file's earlier revision noted as deliberate and "not what makes a
239
+ * deployment work or not" — true of correctness, false of cost, and free here
240
+ * because the Build Output API already separates the two directories.
241
+ */
242
+ const VERCEL_BUILD_CONFIG_SOURCE = `${JSON.stringify({
243
+ version: 3,
244
+ routes: [{ handle: "filesystem" }, {
245
+ src: "/(.*)",
246
+ dest: VERCEL_FUNCTION_ROUTE
247
+ }]
248
+ }, null, 2)}\n`;
249
+ //#endregion
250
+ //#region src/_vercel-output.ts
251
+ /**
252
+ * Assemble a PREBUILT Vercel deployment — `.vercel/output/` — from what
253
+ * `aai build` just produced.
254
+ *
255
+ * The argument for the Build Output API over an `api/` entry is in
256
+ * `_build-target.ts` at {@link VERCEL_OUTPUT_DIR}; what this module adds is the
257
+ * consequence of it. A `.func` directory is a directory WE fill, so every file
258
+ * the server reads at runtime is present because it was copied in, and nothing
259
+ * depends on a static tracer following a path it structurally cannot:
260
+ *
261
+ * - **`.aai/worker.mjs`** is loaded through `import(pathToFileURL(...))`
262
+ * (`start.ts`), which `@vercel/nft` cannot resolve. Under the `api/` shape
263
+ * the build was green and the function 500'd on its first request with
264
+ * "No built agent at .aai/worker.mjs".
265
+ * - **`.env.example`** is not documentation here. `resolveServerEnv` treats it
266
+ * as the DECLARATION of which variables become `ctx.env` (see
267
+ * `DEPLOY_ENV_FILES`), so a function without it hands every tool an empty
268
+ * env while the Vercel project has the values set — a failure that looks
269
+ * like a credential problem and is a packaging one.
270
+ * - **The client** is copied to `static/`, where the CDN serves it, and also
271
+ * beside the worker, so `resolveClientDir` finds a real directory rather
272
+ * than reaching into a `node_modules` that the bundle replaced.
273
+ *
274
+ * The entry is BUNDLED rather than shipped with a `node_modules`. Same call as
275
+ * the worker (`ssr: { noExternal: true }`, `root: cwd`), so it resolves the
276
+ * project's own installed SDK and pulls the runtime, `ws` and `pg` in with it
277
+ * — which is already the arrangement a deployed worker runs under, and it is
278
+ * what makes the function independent of whether the host's install left a
279
+ * usable, hoisted `node_modules` behind.
280
+ */
281
+ /**
282
+ * Files copied verbatim into the function, each one read at RUNTIME by a path
283
+ * no bundler can see. A missing one is skipped rather than fatal.
284
+ *
285
+ * **`.env` is deliberately NOT here.** `resolveServerEnv` reads
286
+ * {@link DEPLOY_ENV_FILES} — `.env.example` then `.env` — but only the first is
287
+ * a DECLARATION; the second holds a developer's own keys, and copying it would
288
+ * bake them into a deployment artifact and let them silently win over the
289
+ * values set in the Vercel project. Declarations ship, values come from the
290
+ * platform environment. Verified by building the `simple` template: a local
291
+ * `.env` with live credentials landed in the function until this list dropped
292
+ * it.
293
+ */
294
+ const RUNTIME_FILES = [WORKER_ARTIFACT_REL, ".env.example"];
295
+ /**
296
+ * Write `.vercel/output/` for this project.
297
+ *
298
+ * The directory is REMOVED first. It is not addressed by content, so a
299
+ * function or a static asset left by an earlier build with a different shape
300
+ * would be deployed alongside this one — and `vercel deploy --prebuilt`
301
+ * uploads whatever is there.
302
+ */
303
+ async function emitVercelOutput(cwd, options = {}) {
304
+ const outputDir = path.join(cwd, VERCEL_OUTPUT_DIR);
305
+ const functionDir = path.join(cwd, VERCEL_FUNCTION_DIR);
306
+ const staticDir = path.join(cwd, VERCEL_STATIC_DIR);
307
+ await fs.rm(outputDir, {
308
+ recursive: true,
309
+ force: true
310
+ });
311
+ await fs.mkdir(functionDir, { recursive: true });
312
+ await fs.writeFile(path.join(outputDir, "config.json"), VERCEL_BUILD_CONFIG_SOURCE, "utf-8");
313
+ await fs.writeFile(path.join(functionDir, ".vc-config.json"), vercelFunctionConfigSource(), "utf-8");
314
+ const bundle = options.bundle ?? bundleEntry;
315
+ await fs.writeFile(path.join(functionDir, "index.mjs"), await bundle(cwd), "utf-8");
316
+ for (const rel of RUNTIME_FILES) {
317
+ const from = path.join(cwd, rel);
318
+ if (!await exists(from)) continue;
319
+ const to = path.join(functionDir, rel);
320
+ await fs.mkdir(path.dirname(to), { recursive: true });
321
+ await fs.copyFile(from, to);
322
+ }
323
+ const clientSource = await exists(path.join(cwd, CLIENT_ARTIFACT_REL, "index.html")) ? path.join(cwd, CLIENT_ARTIFACT_REL) : defaultClientDir();
324
+ await fs.cp(clientSource, staticDir, { recursive: true });
325
+ await fs.cp(clientSource, path.join(functionDir, CLIENT_ARTIFACT_REL), { recursive: true });
326
+ }
327
+ /**
328
+ * Bundle {@link VERCEL_ENTRY_SOURCE} and everything it imports into one ESM
329
+ * file.
330
+ *
331
+ * The entry is written INTO the project rather than a temp directory, because
332
+ * that is what makes `@alexkroman1/aai-cli/start` resolve against the user's
333
+ * install — the deployed server is then the version their lockfile pins, which
334
+ * is the same guarantee `aai publish` gives. Removed in a `finally`: a build
335
+ * that throws must not leave a file that looks authored.
336
+ */
337
+ async function bundleEntry(cwd) {
338
+ const entryPath = path.join(cwd, ".aai", "vercel-entry.mjs");
339
+ await fs.mkdir(path.dirname(entryPath), { recursive: true });
340
+ await fs.writeFile(entryPath, VERCEL_ENTRY_SOURCE, "utf-8");
341
+ let result;
342
+ try {
343
+ result = await withPreservedNodeEnv(() => build({
344
+ root: cwd,
345
+ logLevel: "silent",
346
+ configFile: false,
347
+ ssr: { noExternal: true },
348
+ build: {
349
+ ssr: true,
350
+ lib: {
351
+ entry: entryPath,
352
+ formats: ["es"],
353
+ fileName: "index"
354
+ },
355
+ target: "node20",
356
+ minify: false,
357
+ write: false,
358
+ rollupOptions: { output: {
359
+ entryFileNames: "[name].mjs",
360
+ codeSplitting: false
361
+ } }
362
+ }
363
+ }));
364
+ } finally {
365
+ await fs.rm(entryPath, { force: true }).catch(() => void 0);
366
+ }
367
+ const output = Array.isArray(result) ? result[0] : result;
368
+ invariant(output !== void 0, "vercel.entry.output");
369
+ const chunk = output.output.find((o) => o.type === "chunk" && o.isEntry);
370
+ invariant(chunk !== void 0, "vercel.entry.chunk", () => ({ kinds: output.output.map((o) => o.type) }));
371
+ return chunk.code;
372
+ }
373
+ async function exists(target) {
374
+ return await fs.stat(target).then(() => true, () => false);
375
+ }
376
+ //#endregion
377
+ //#region src/build.ts
378
+ /**
379
+ * `aai build` — bundle the agent without deploying, behind the same gates
380
+ * deploy runs (tests, then typecheck), so the command previews the deploy
381
+ * artifact and its failures alike.
382
+ *
383
+ * It also LEAVES that artifact on disk, at {@link WORKER_ARTIFACT_REL}, which is
384
+ * what makes self-hosting work: `aai start` loads the built worker rather than
385
+ * `agent.ts`, because a tool is discovered by the bundler enumerating `tools/`
386
+ * and no un-bundled loader can see that directory. See `start.ts` and
387
+ * "Self-hosting is the scaffold's default" in `packages/aai-cli/CLAUDE.md`.
388
+ *
389
+ * `--target` decides what else it emits — see `_build-target.ts`.
390
+ */
391
+ /**
392
+ * The prose slot, named by convention. Mirrors `worker-bundler.ts`'s constant of
393
+ * the same name — the two cannot share one without an import cycle
394
+ * (`build.ts` → `_bundler.ts` → `worker-bundler.ts`), so `_build.test.ts` pins
395
+ * the pair end to end instead: a build whose prompt really came from the file
396
+ * has to report the file.
397
+ */
398
+ const SYSTEM_PROMPT_FILE = "system-prompt.md";
399
+ /** What {@link systemPromptSource} reports when nothing declared a prompt. */
400
+ const FRAMEWORK_DEFAULT_PROMPT_SOURCE = "the framework default (DEFAULT_SYSTEM_PROMPT)";
401
+ /**
402
+ * Which of the three possible authors of this agent's system prompt actually
403
+ * wrote the one in the bundle.
404
+ *
405
+ * `aai build` reported `name` and `workerBytes` and nothing about the prompt,
406
+ * and the prompt is the one field that can change COMPLETELY without any
407
+ * config changing: deleting `system-prompt.md` swaps in
408
+ * `DEFAULT_SYSTEM_PROMPT` — a total personality change — with exit 0 and no
409
+ * line anywhere. `withSystemPrompt` refuses the case where a file exists and
410
+ * nothing reads it; it cannot refuse a file that is simply GONE, because that
411
+ * is what an agent with no file legitimately looks like. So this is a report
412
+ * rather than a check.
413
+ *
414
+ * Decided by comparing VALUES against the built agent's resolved prompt, which
415
+ * is the same method `withSystemPrompt` uses and for the same reason — the
416
+ * alternative asks the bundler's module graph a question, and the author may
417
+ * have imported the file and composed it, which is neither "the file" nor
418
+ * "agent.ts" alone.
419
+ */
420
+ async function systemPromptSource(cwd, resolved) {
421
+ if (resolved === DEFAULT_SYSTEM_PROMPT) return FRAMEWORK_DEFAULT_PROMPT_SOURCE;
422
+ const trimmed = (await fs.readFile(path.join(cwd, SYSTEM_PROMPT_FILE), "utf-8").catch(() => void 0))?.trim();
423
+ if (trimmed === void 0 || trimmed === "" || !resolved.includes(trimmed)) return "agent.ts";
424
+ return resolved.trim() === trimmed ? SYSTEM_PROMPT_FILE : `agent.ts (composing ${SYSTEM_PROMPT_FILE})`;
425
+ }
426
+ /**
427
+ * Map a {@link runVitest} failure to a CliError — distinguishing a real test
428
+ * failure (test_failed) from the runner not spawning (spawn_failed) instead
429
+ * of a generic command_failed.
430
+ */
431
+ function testGateError(err) {
432
+ const { code, message } = classifyVitestError(err);
433
+ return new CliError(code, message, "Re-run with --skipTests to build without tests", { cause: err });
434
+ }
435
+ async function executeBuild(opts) {
436
+ const { cwd } = opts;
437
+ const target = resolveBuildTarget(opts.target);
438
+ if (!opts.skipTests) try {
439
+ runVitest(cwd, {
440
+ candidates: TEST_FILES,
441
+ all: true
442
+ });
443
+ } catch (err) {
444
+ throw testGateError(err);
445
+ }
446
+ await assertTypechecks(cwd, { skip: opts.skipTypecheck });
447
+ const bundle = await buildAgentBundle(cwd, { minify: true });
448
+ const agentDef = await evalWorkerBundle(bundle.worker);
449
+ for (const warning of agentConfigWarnings(agentDef)) notify("warn", warning);
450
+ for (const warning of determinismWarnings(await scanWorkflowDeterminism(cwd))) notify("warn", warning);
451
+ const worker = path.join(cwd, WORKER_ARTIFACT_REL);
452
+ await fs.mkdir(path.dirname(worker), { recursive: true });
453
+ await fs.writeFile(worker, bundle.worker, "utf-8");
454
+ await emitTargetFiles(cwd, target);
455
+ const systemPrompt = await systemPromptSource(cwd, agentDef.systemPrompt);
456
+ log.info(`System prompt: ${systemPrompt}`);
457
+ log.success("Build complete");
458
+ return ok({
459
+ name: agentDef.name,
460
+ workerBytes: bundle.worker.length,
461
+ worker,
462
+ systemPrompt,
463
+ target
464
+ });
465
+ }
466
+ /**
467
+ * Write what a target needs, if it needs anything.
468
+ *
469
+ * `node` writes nothing: a long-lived process runs `aai start`, which needs no
470
+ * generated file. `vercel` writes a complete prebuilt deployment under
471
+ * `.vercel/output/` — see `_vercel-output.ts`, and `VERCEL_OUTPUT_DIR` in
472
+ * `_build-target.ts` for why that directory and not an `api/` entry beside a
473
+ * generated `vercel.json`.
474
+ */
475
+ async function emitTargetFiles(cwd, target) {
476
+ if (target === "node") return;
477
+ await emitVercelOutput(cwd);
478
+ log.info(`Target ${target}: wrote ${VERCEL_OUTPUT_DIR}`);
479
+ }
480
+ //#endregion
481
+ export { executeBuild };