@alexkroman1/aai-cli 8.2.1 → 9.0.1

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 (90) hide show
  1. package/dist/_dev-env.d.ts +15 -2
  2. package/dist/{_dev-server-Dt3TCjhj.mjs → _dev-server-BAR8Ya43.mjs} +182 -55
  3. package/dist/_dev-server.d.ts +5 -30
  4. package/dist/_dev-typecheck.d.ts +38 -0
  5. package/dist/_dev-vite-config.d.ts +74 -0
  6. package/dist/_e2e-test-utils.d.ts +33 -1
  7. package/dist/_init-BNotRgV1.mjs +163 -0
  8. package/dist/_resource-commands.d.ts +0 -1
  9. package/dist/{build-BBxf8vom.mjs → build-DvDdSwAS.mjs} +1 -1
  10. package/dist/cli.mjs +22 -93
  11. package/dist/{delete-CPf347lo.mjs → delete-8pjH8n8n.mjs} +2 -1
  12. package/dist/{deploy-BqV6ShIe.mjs → deploy-CjU3e-3P.mjs} +3 -3
  13. package/dist/{dev-QuuOOUtv.mjs → dev-dFzdvwa7.mjs} +3 -2
  14. package/dist/dev.d.ts +2 -0
  15. package/dist/{eval-DfQ8cm5p.mjs → eval-DwNwdqmS.mjs} +1 -1
  16. package/dist/{init-Cp5GZg0B.mjs → init-DK7GpIF9.mjs} +7 -6
  17. package/dist/scaffold/.env.example +7 -4
  18. package/dist/scaffold/CLAUDE.md +152 -97
  19. package/dist/scaffold/global.d.ts +12 -0
  20. package/dist/scaffold/package.json +4 -4
  21. package/dist/scaffold/server.mjs +49 -2
  22. package/dist/scaffold/tsconfig.json +1 -0
  23. package/dist/scaffold/vitest.config.ts +24 -0
  24. package/dist/{studio-DUfCH9I8.mjs → studio-D3-HWIfk.mjs} +3 -3
  25. package/dist/templates/briefing-desk/agent.test.ts +3 -15
  26. package/dist/templates/briefing-desk/shared.ts +3 -3
  27. package/dist/templates/call-audit/agent.eval.test.ts +8 -1
  28. package/dist/templates/call-audit/agent.test.ts +0 -1
  29. package/dist/templates/call-audit/agent.ts +4 -3
  30. package/dist/templates/call-audit/client.tsx +4 -7
  31. package/dist/templates/call-audit/workflows/audit.ts +10 -1
  32. package/dist/templates/code-interpreter/agent.eval.test.ts +14 -43
  33. package/dist/templates/dispatch-center/agent.eval.test.ts +16 -26
  34. package/dist/templates/embedded-assets/agent.eval.test.ts +6 -16
  35. package/dist/templates/embedded-assets/agent.test.ts +3 -16
  36. package/dist/templates/health-assistant/agent.eval.test.ts +5 -15
  37. package/dist/templates/health-assistant/agent.test.ts +3 -14
  38. package/dist/templates/infocom-adventure/agent.eval.test.ts +6 -16
  39. package/dist/templates/infocom-adventure/agent.test.ts +3 -17
  40. package/dist/templates/link-digest/agent.test.ts +0 -14
  41. package/dist/templates/link-digest/agent.ts +9 -5
  42. package/dist/templates/link-digest/client.tsx +8 -6
  43. package/dist/templates/math-buddy/agent.eval.test.ts +11 -40
  44. package/dist/templates/night-owl/agent.eval.test.ts +15 -44
  45. package/dist/templates/night-owl/agent.test.ts +2 -15
  46. package/dist/templates/personal-finance/agent.eval.test.ts +10 -39
  47. package/dist/templates/pipeline-simple/agent.test.ts +73 -0
  48. package/dist/templates/pizza-ordering/agent.eval.test.ts +2 -17
  49. package/dist/templates/pizza-ordering/agent.test.ts +2 -17
  50. package/dist/templates/plan-and-execute/agent.eval.test.ts +2 -12
  51. package/dist/templates/plan-and-execute/agent.test.ts +3 -23
  52. package/dist/templates/plan-and-execute/shared.ts +1 -1
  53. package/dist/templates/podcast-digest/agent.test.ts +0 -1
  54. package/dist/templates/podcast-digest/client.tsx +3 -6
  55. package/dist/templates/podcast-digest/workflows/digest.ts +16 -8
  56. package/dist/templates/recap-workflow/agent.eval.test.ts +7 -14
  57. package/dist/templates/recap-workflow/agent.test.ts +3 -18
  58. package/dist/templates/recap-workflow/agent.ts +7 -5
  59. package/dist/templates/redline/agent.test.ts +0 -13
  60. package/dist/templates/redline/agent.ts +3 -2
  61. package/dist/templates/redline/client.tsx +9 -4
  62. package/dist/templates/research-workflow/agent.eval.test.ts +7 -14
  63. package/dist/templates/research-workflow/agent.test.ts +3 -15
  64. package/dist/templates/research-workflow/agent.ts +7 -5
  65. package/dist/templates/research-workflow/workflows/research.ts +8 -2
  66. package/dist/templates/retail/agent.eval.test.ts +10 -25
  67. package/dist/templates/retail/registry.test.ts +16 -13
  68. package/dist/templates/solo-rpg/agent.eval.test.ts +5 -15
  69. package/dist/templates/solo-rpg/agent.test.ts +17 -124
  70. package/dist/templates/solo-rpg/shared.ts +13 -61
  71. package/dist/templates/spoken-summary/agent.test.ts +0 -1
  72. package/dist/templates/spoken-summary/agent.ts +5 -4
  73. package/dist/templates/spoken-summary/client.tsx +4 -7
  74. package/dist/templates/support-line/agent.eval.test.ts +2 -12
  75. package/dist/templates/support-line/agent.test.ts +3 -22
  76. package/dist/templates/transcription-workflow/agent.ts +4 -4
  77. package/dist/templates/transcription-workflow/client.tsx +5 -5
  78. package/dist/templates/transcription-workflow/workflows/transcribe.ts +10 -1
  79. package/dist/templates/travel-concierge/agent.eval.test.ts +5 -15
  80. package/dist/templates/travel-concierge/agent.test.ts +3 -23
  81. package/dist/templates/web-researcher/agent.eval.test.ts +2 -5
  82. package/dist/test-CvwgeVSQ.mjs +181 -0
  83. package/dist/test.d.ts +33 -2
  84. package/package.json +4 -4
  85. package/dist/_init-BhEfQ6Yi.mjs +0 -93
  86. package/dist/storage-Bfs7QQWq.mjs +0 -66
  87. package/dist/storage.d.ts +0 -22
  88. package/dist/templates/solo-rpg/tools/load_game.ts +0 -37
  89. package/dist/templates/solo-rpg/tools/save_game.ts +0 -32
  90. package/dist/test-BvyT6sAc.mjs +0 -104
@@ -30,7 +30,7 @@ export declare function hostModeEnv(providerEnv: Record<string, string>): Record
30
30
  */
31
31
  export declare function devBindHost(): string | undefined;
32
32
  /**
33
- * File watching is OPT-IN — `AAI_DEV_WATCH=1` turns it on.
33
+ * File watching is OPT-IN — `aai dev --watch`, or `AAI_DEV_WATCH=1`.
34
34
  *
35
35
  * A restart rebuilds the bundle and replaces the server, which drops nothing
36
36
  * mid-request but does end in-flight voice sessions. That is the right default
@@ -38,8 +38,21 @@ export declare function devBindHost(): string | undefined;
38
38
  * host for twenty minutes: a stray formatter save, a `.env` touch, or a git
39
39
  * operation restarts the server underneath the run, and the harness reports it
40
40
  * as a provider failure several records deep.
41
+ *
42
+ * **The FLAG exists because the variable was undiscoverable**, which made the
43
+ * default a defect rather than a decision: `aai dev --help` listed nothing about
44
+ * watching, `AAI_DEV_WATCH` appeared in no document a user reads, and the guide
45
+ * shipped into every scaffolded project opened with "Iterate in `pnpm dev` —
46
+ * hot reload". So the promise was false and there was no way to find the switch
47
+ * that makes it true. The variable stays for a process supervisor, which has an
48
+ * environment and no argv.
49
+ *
50
+ * The flag WINS when passed, in both directions: `--watch` turns it on where the
51
+ * variable is unset or off, and `--watch=false` turns it off where the variable
52
+ * says on — an explicit argument that a stale exported variable could override
53
+ * would be the same discoverability bug wearing the fix's clothes.
41
54
  */
42
- export declare function devWatchEnabled(): boolean;
55
+ export declare function devWatchEnabled(flag?: boolean | undefined): boolean;
43
56
  /**
44
57
  * The logger the dev server's runtime writes through.
45
58
  *
@@ -4,6 +4,7 @@ import { i as errorCode, o as errorMessage$1 } from "./_utils-B8QmtFhK.mjs";
4
4
  import { n as fallbackHtmlPlugin } from "./client-bundler-CWnG42cU.mjs";
5
5
  import { t as DEDUPED_PEERS } from "./_vite-env-BNveawd1.mjs";
6
6
  import { n as buildWorkflows, t as buildWorker } from "./worker-bundler-CCVEDjm1.mjs";
7
+ import { typecheckProject } from "./typecheck.mjs";
7
8
  import { n as ensureApiKey } from "./_config-CmJOFsAP.mjs";
8
9
  import { n as createWorkerEvaluator } from "./_bundler-CDuYl5Gb.mjs";
9
10
  import { t as resolveServerEnv } from "./_server-common-6e0QI4mq.mjs";
@@ -12,7 +13,7 @@ import path from "node:path";
12
13
  import { omitUndefined } from "@alexkroman1/aai/utils";
13
14
  import { createCoalescingRunner, sleep } from "@alexkroman1/aai/internal";
14
15
  import { agentConfigWarnings } from "@alexkroman1/aai/manifest";
15
- import { WORKFLOW_API_PREFIX, createRuntime, createServer, requiredProviderEnvVars, withHostCredentialFallback } from "@alexkroman1/aai-runtime";
16
+ import { WORKFLOW_API_PREFIX, createRuntime, createServer, ensureSessionStateSchema, requiredProviderEnvVars, withHostCredentialFallback } from "@alexkroman1/aai-runtime";
16
17
  import { configureWorkflowWorld, consoleLogger, createWorkflowSurface, handleWorkflowRequest, publishStepEnv, startWorkflowWorldIfDeclared } from "@alexkroman1/aai-runtime/internal";
17
18
  import { defaultClientDir } from "@alexkroman1/aai-ui/client-dir";
18
19
  import { watch } from "chokidar";
@@ -42,7 +43,7 @@ function devBindHost() {
42
43
  return host ? host : void 0;
43
44
  }
44
45
  /**
45
- * File watching is OPT-IN — `AAI_DEV_WATCH=1` turns it on.
46
+ * File watching is OPT-IN — `aai dev --watch`, or `AAI_DEV_WATCH=1`.
46
47
  *
47
48
  * A restart rebuilds the bundle and replaces the server, which drops nothing
48
49
  * mid-request but does end in-flight voice sessions. That is the right default
@@ -50,8 +51,22 @@ function devBindHost() {
50
51
  * host for twenty minutes: a stray formatter save, a `.env` touch, or a git
51
52
  * operation restarts the server underneath the run, and the harness reports it
52
53
  * as a provider failure several records deep.
54
+ *
55
+ * **The FLAG exists because the variable was undiscoverable**, which made the
56
+ * default a defect rather than a decision: `aai dev --help` listed nothing about
57
+ * watching, `AAI_DEV_WATCH` appeared in no document a user reads, and the guide
58
+ * shipped into every scaffolded project opened with "Iterate in `pnpm dev` —
59
+ * hot reload". So the promise was false and there was no way to find the switch
60
+ * that makes it true. The variable stays for a process supervisor, which has an
61
+ * environment and no argv.
62
+ *
63
+ * The flag WINS when passed, in both directions: `--watch` turns it on where the
64
+ * variable is unset or off, and `--watch=false` turns it off where the variable
65
+ * says on — an explicit argument that a stale exported variable could override
66
+ * would be the same discoverability bug wearing the fix's clothes.
53
67
  */
54
- function devWatchEnabled() {
68
+ function devWatchEnabled(flag) {
69
+ if (flag !== void 0) return flag;
55
70
  return /^(1|true|yes|on)$/i.test(process.env.AAI_DEV_WATCH?.trim() ?? "");
56
71
  }
57
72
  /**
@@ -215,6 +230,149 @@ function createRestartSupervisor(ops) {
215
230
  };
216
231
  }
217
232
  //#endregion
233
+ //#region _dev-typecheck.ts
234
+ /**
235
+ * The background typecheck under `aai dev`.
236
+ *
237
+ * `assertTypechecks` is wired into `build`, `deploy` and `studio` — and not
238
+ * into `dev`, the loop an author actually lives in. That made the whole
239
+ * compile-message system in `agent-params.ts` (five families of hand-written
240
+ * diagnostic sentences, the most careful work in the SDK) conditional on the
241
+ * author having an editor open on the right file. Demonstrated: a project whose
242
+ * `agent.ts` fails `tsc --noEmit` starts cleanly under `aai dev` and serves,
243
+ * and the typo is found later by `aai build` — or by a caller.
244
+ *
245
+ * Three properties, and each is the reason this is not just a call to
246
+ * `assertTypechecks`:
247
+ *
248
+ * - **It never blocks and never fails the server.** `aai dev` must start with a
249
+ * type error in an unrelated file; refusing to serve would be a worse tool
250
+ * than one that says nothing. The report is a `notify`, not a throw.
251
+ * - **It coalesces.** A save touches several files and the watcher debounces to
252
+ * a restart; without coalescing a burst spawns a `tsc` per restart, and `tsc`
253
+ * on a real project is seconds. A request during a run schedules exactly one
254
+ * more, which is `createCoalescingRunner`'s contract.
255
+ * - **It is quiet when clean.** A dev server that prints "types OK" on every
256
+ * save trains the author to stop reading it, which is how the failure this
257
+ * exists to surface gets missed again.
258
+ */
259
+ /**
260
+ * A coalescing background typechecker for the project at `cwd`.
261
+ *
262
+ * `report` is injected so the dev-server specs can read what an author would
263
+ * have seen without capturing stdout — the same seam every other reporter in
264
+ * this package takes.
265
+ */
266
+ function createDevTypecheck(cwd, report = notify) {
267
+ const runner = createCoalescingRunner(async () => {
268
+ const result = await typecheckProject(cwd);
269
+ if (result.ok) return;
270
+ report("warn", `Type errors — \`aai dev\` keeps serving, but \`aai build\` will refuse:\n${result.output}`);
271
+ });
272
+ return { request() {
273
+ runner.trigger().catch((err) => {
274
+ report("error", `Background typecheck failed to run: ${String(err)}`);
275
+ });
276
+ } };
277
+ }
278
+ //#endregion
279
+ //#region _dev-vite-config.ts
280
+ /**
281
+ * The Vite dev server's config for a project with a `client.tsx`.
282
+ *
283
+ * Its own module because it is the whole agent API as the BROWSER can see it —
284
+ * a table worth reading without the 400 lines of watcher, restart and env
285
+ * plumbing that surround it in `_dev-server.ts`, which is also what pushed that
286
+ * file past the length cap.
287
+ */
288
+ /**
289
+ * Vite dev-server config for the client SPA. Extracted so the proxy wiring
290
+ * is unit-testable: `/websocket` MUST proxy with `ws: true` or `aai dev`
291
+ * with a `client.tsx` serves a page whose WebSocket never connects.
292
+ *
293
+ * **This table is the whole agent API as the browser can see it**, which is
294
+ * the thing to hold in mind before adding a route to `createServer`. Vite owns
295
+ * the port the user is told to open and answers everything not listed here
296
+ * itself — with a bare 404 carrying none of the agent server's headers, so the
297
+ * failure looks like a missing route rather than a missing proxy entry.
298
+ *
299
+ * `/workflows` is why that matters beyond voice. A WORKFLOW APP
300
+ * (`workflowApp()`) has no session and no socket: `page()` mounts a
301
+ * form and every single thing it does — listing workflows, starting a run,
302
+ * polling it, streaming its events — is a same-origin `fetch` under that
303
+ * prefix. Unproxied, the two workflow-app templates were dead on arrival under
304
+ * `aai dev` (`404 POST /workflows/runs` the instant the form is submitted)
305
+ * while the backend served the API correctly one port over. A string key
306
+ * prefix-matches, so this one entry covers `/runs`, `/runs/:id` and the
307
+ * `/runs/:id/events` SSE stream.
308
+ *
309
+ * `strictPort` because the reported URL is `http://localhost:<port>` —
310
+ * without it, Vite silently binds port+N when the port is busy and the
311
+ * printed/JSON-returned URL points at whatever else was listening.
312
+ *
313
+ * `AAI_DEV_HOST` reaches BOTH servers. Binding only the backend left Vite —
314
+ * the port the user is told to open — on loopback, i.e. failing exactly the
315
+ * case that variable exists for (`aai dev` in a container, reached from the host).
316
+ *
317
+ * ## The target is an IP LITERAL, and that is a fix rather than a style choice
318
+ *
319
+ * `127.0.0.1`, never `localhost`. Vite opens a FRESH upstream connection for
320
+ * every WebSocket upgrade — an HTTP request reuses a pooled keep-alive socket and
321
+ * so resolves rarely — which means a hostname here is one `getaddrinfo` per
322
+ * session handshake. That lookup runs on libuv's threadpool (four threads,
323
+ * shared with every other fs and DNS call in a process that is ALSO serving the
324
+ * agent), and under load it intermittently stalls for almost exactly two
325
+ * seconds.
326
+ *
327
+ * Measured on the `retail` template, session handshakes to `session.configured`:
328
+ *
329
+ * | Target | conc | rps | p50 | p99 |
330
+ * | --- | --- | --- | --- | --- |
331
+ * | `localhost` | 1 | 12-18 | 8-11 ms | 2.0 s |
332
+ * | `localhost` | 10 | 0.6 | 16.7 s | 16.7 s |
333
+ * | `127.0.0.1` | 1 | 89-207 | 4-9 ms | 23-49 ms |
334
+ * | `127.0.0.1` | 20 | 260 | 73 ms | 166 ms |
335
+ *
336
+ * The `localhost` row is not a slow proxy, it is a queue: one handshake in thirty
337
+ * stalls two seconds on its own, and at concurrency 10 the stalls pile onto four
338
+ * threads until a sustained burst left the proxy refusing upgrades entirely until
339
+ * the dev server was restarted. With the literal it recovers from a burst and
340
+ * sits within ~1.5x of the backend port.
341
+ *
342
+ * Localized by timing the phases separately — TCP connect and the first frame
343
+ * were always fast, the 101 was not — and then by comparing the instant the
344
+ * client sent its upgrade against the backend's own log line for it: 23.808 out,
345
+ * 25.796 in, answered in 5 ms. The two seconds were spent before Vite dialled,
346
+ * which is what pointed at resolution rather than at either server.
347
+ *
348
+ * Not a behaviour change: `localhost` resolved to loopback anyway, so this
349
+ * removes the lookup and nothing else. An `AAI_DEV_HOST` that binds the backend
350
+ * to ONE non-loopback interface was unreachable through the proxy before this and
351
+ * still is.
352
+ */
353
+ function viteDevConfig(cwd, vitePort, backendPort) {
354
+ const target = `http://127.0.0.1:${backendPort}`;
355
+ return {
356
+ root: cwd,
357
+ plugins: [fallbackHtmlPlugin(cwd)],
358
+ resolve: { dedupe: DEDUPED_PEERS },
359
+ server: {
360
+ port: vitePort,
361
+ strictPort: true,
362
+ ...omitUndefined({ host: devBindHost() }),
363
+ proxy: {
364
+ "/health": target,
365
+ "/client-config": target,
366
+ "/websocket": {
367
+ target,
368
+ ws: true
369
+ },
370
+ [WORKFLOW_API_PREFIX]: target
371
+ }
372
+ }
373
+ };
374
+ }
375
+ //#endregion
218
376
  //#region _dev-server.ts
219
377
  /**
220
378
  * Dev server for directory-based agents.
@@ -332,57 +490,6 @@ function watchDirectory(dir, onChange) {
332
490
  return watcher;
333
491
  }
334
492
  /**
335
- * Vite dev-server config for the client SPA. Extracted so the proxy wiring
336
- * is unit-testable: `/websocket` MUST proxy with `ws: true` or `aai dev`
337
- * with a `client.tsx` serves a page whose WebSocket never connects.
338
- *
339
- * **This table is the whole agent API as the browser can see it**, which is
340
- * the thing to hold in mind before adding a route to `createServer`. Vite owns
341
- * the port the user is told to open and answers everything not listed here
342
- * itself — with a bare 404 carrying none of the agent server's headers, so the
343
- * failure looks like a missing route rather than a missing proxy entry.
344
- *
345
- * `/workflows` is why that matters beyond voice. A WORKFLOW APP
346
- * (`workflowApp()`) has no session and no socket: `page()` mounts a
347
- * form and every single thing it does — listing workflows, starting a run,
348
- * polling it, streaming its events — is a same-origin `fetch` under that
349
- * prefix. Unproxied, the two workflow-app templates were dead on arrival under
350
- * `aai dev` (`404 POST /workflows/runs` the instant the form is submitted)
351
- * while the backend served the API correctly one port over. A string key
352
- * prefix-matches, so this one entry covers `/runs`, `/runs/:id` and the
353
- * `/runs/:id/events` SSE stream.
354
- *
355
- * `strictPort` because the reported URL is `http://localhost:<port>` —
356
- * without it, Vite silently binds port+N when the port is busy and the
357
- * printed/JSON-returned URL points at whatever else was listening.
358
- *
359
- * `AAI_DEV_HOST` reaches BOTH servers. Binding only the backend left Vite —
360
- * the port the user is told to open — on loopback, i.e. failing exactly the
361
- * case that variable exists for (`aai dev` in a container, reached from the host).
362
- */
363
- function viteDevConfig(cwd, vitePort, backendPort) {
364
- const target = `http://localhost:${backendPort}`;
365
- return {
366
- root: cwd,
367
- plugins: [fallbackHtmlPlugin(cwd)],
368
- resolve: { dedupe: DEDUPED_PEERS },
369
- server: {
370
- port: vitePort,
371
- strictPort: true,
372
- ...omitUndefined({ host: devBindHost() }),
373
- proxy: {
374
- "/health": target,
375
- "/client-config": target,
376
- "/websocket": {
377
- target,
378
- ws: true
379
- },
380
- [WORKFLOW_API_PREFIX]: target
381
- }
382
- }
383
- };
384
- }
385
- /**
386
493
  * Start the dev server for a directory-based agent.
387
494
  *
388
495
  * Returns a cleanup function to shut down the server and watchers.
@@ -396,11 +503,26 @@ async function startDevServer(opts) {
396
503
  const evaluateWorker = createWorkerEvaluator();
397
504
  const devLogger = createDevLogger(outputSilenced());
398
505
  let workflowWorldStarted = false;
506
+ /**
507
+ * Whether the session-state tables have been ensured this process.
508
+ *
509
+ * Once, like the workflow world above and for the same reason: a rebuild
510
+ * replaces the routes, not the storage behind them, and re-running the DDL on
511
+ * every file save would be two round trips per keystroke burst.
512
+ */
513
+ let sessionSchemaEnsured = false;
399
514
  /** Full build sequence, shared by initial startup and every restart. */
400
515
  async function buildServer() {
401
516
  const worker = await loadWorker(cwd, evaluateWorker);
402
517
  const agentDef = worker.agent;
403
518
  const env = await resolveAgentEnv(cwd, agentDef);
519
+ if (env.DATABASE_URL && !sessionSchemaEnsured) {
520
+ sessionSchemaEnsured = true;
521
+ await ensureSessionStateSchema({
522
+ url: env.DATABASE_URL,
523
+ logger: devLogger
524
+ });
525
+ }
404
526
  const world = configureWorkflowWorld({
405
527
  databaseUrl: env.DATABASE_URL,
406
528
  port: backendPort,
@@ -442,7 +564,12 @@ async function startDevServer(opts) {
442
564
  await viteServer?.close().catch(() => void 0);
443
565
  }
444
566
  });
445
- watcher = devWatchEnabled() ? watchDirectory(cwd, supervisor.request) : void 0;
567
+ const devTypecheck = createDevTypecheck(cwd);
568
+ devTypecheck.request();
569
+ watcher = devWatchEnabled(opts.watch) ? watchDirectory(cwd, () => {
570
+ devTypecheck.request();
571
+ supervisor.request();
572
+ }) : void 0;
446
573
  let boundServer;
447
574
  try {
448
575
  const initialServer = await buildServer();
@@ -63,37 +63,12 @@ export declare function watchDirectory(dir: string, onChange: () => void): FSWat
63
63
  export type DevServerOptions = {
64
64
  cwd: string;
65
65
  port: number;
66
+ /**
67
+ * Restart on a file change. Omitted, `AAI_DEV_WATCH` decides — see
68
+ * {@link devWatchEnabled} for why watching is opt-in at all.
69
+ */
70
+ watch?: boolean | undefined;
66
71
  };
67
- /**
68
- * Vite dev-server config for the client SPA. Extracted so the proxy wiring
69
- * is unit-testable: `/websocket` MUST proxy with `ws: true` or `aai dev`
70
- * with a `client.tsx` serves a page whose WebSocket never connects.
71
- *
72
- * **This table is the whole agent API as the browser can see it**, which is
73
- * the thing to hold in mind before adding a route to `createServer`. Vite owns
74
- * the port the user is told to open and answers everything not listed here
75
- * itself — with a bare 404 carrying none of the agent server's headers, so the
76
- * failure looks like a missing route rather than a missing proxy entry.
77
- *
78
- * `/workflows` is why that matters beyond voice. A WORKFLOW APP
79
- * (`workflowApp()`) has no session and no socket: `page()` mounts a
80
- * form and every single thing it does — listing workflows, starting a run,
81
- * polling it, streaming its events — is a same-origin `fetch` under that
82
- * prefix. Unproxied, the two workflow-app templates were dead on arrival under
83
- * `aai dev` (`404 POST /workflows/runs` the instant the form is submitted)
84
- * while the backend served the API correctly one port over. A string key
85
- * prefix-matches, so this one entry covers `/runs`, `/runs/:id` and the
86
- * `/runs/:id/events` SSE stream.
87
- *
88
- * `strictPort` because the reported URL is `http://localhost:<port>` —
89
- * without it, Vite silently binds port+N when the port is busy and the
90
- * printed/JSON-returned URL points at whatever else was listening.
91
- *
92
- * `AAI_DEV_HOST` reaches BOTH servers. Binding only the backend left Vite —
93
- * the port the user is told to open — on loopback, i.e. failing exactly the
94
- * case that variable exists for (`aai dev` in a container, reached from the host).
95
- */
96
- export declare function viteDevConfig(cwd: string, vitePort: number, backendPort: number): import("vite").InlineConfig;
97
72
  /**
98
73
  * Start the dev server for a directory-based agent.
99
74
  *
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The background typecheck under `aai dev`.
3
+ *
4
+ * `assertTypechecks` is wired into `build`, `deploy` and `studio` — and not
5
+ * into `dev`, the loop an author actually lives in. That made the whole
6
+ * compile-message system in `agent-params.ts` (five families of hand-written
7
+ * diagnostic sentences, the most careful work in the SDK) conditional on the
8
+ * author having an editor open on the right file. Demonstrated: a project whose
9
+ * `agent.ts` fails `tsc --noEmit` starts cleanly under `aai dev` and serves,
10
+ * and the typo is found later by `aai build` — or by a caller.
11
+ *
12
+ * Three properties, and each is the reason this is not just a call to
13
+ * `assertTypechecks`:
14
+ *
15
+ * - **It never blocks and never fails the server.** `aai dev` must start with a
16
+ * type error in an unrelated file; refusing to serve would be a worse tool
17
+ * than one that says nothing. The report is a `notify`, not a throw.
18
+ * - **It coalesces.** A save touches several files and the watcher debounces to
19
+ * a restart; without coalescing a burst spawns a `tsc` per restart, and `tsc`
20
+ * on a real project is seconds. A request during a run schedules exactly one
21
+ * more, which is `createCoalescingRunner`'s contract.
22
+ * - **It is quiet when clean.** A dev server that prints "types OK" on every
23
+ * save trains the author to stop reading it, which is how the failure this
24
+ * exists to surface gets missed again.
25
+ */
26
+ /** What {@link createDevTypecheck} hands back: fire-and-forget, already coalesced. */
27
+ export type DevTypecheck = {
28
+ /** Schedule a run. Returns immediately; never rejects. */
29
+ request(): void;
30
+ };
31
+ /**
32
+ * A coalescing background typechecker for the project at `cwd`.
33
+ *
34
+ * `report` is injected so the dev-server specs can read what an author would
35
+ * have seen without capturing stdout — the same seam every other reporter in
36
+ * this package takes.
37
+ */
38
+ export declare function createDevTypecheck(cwd: string, report?: (level: "warn" | "error", message: string) => void): DevTypecheck;
@@ -0,0 +1,74 @@
1
+ /**
2
+ * The Vite dev server's config for a project with a `client.tsx`.
3
+ *
4
+ * Its own module because it is the whole agent API as the BROWSER can see it —
5
+ * a table worth reading without the 400 lines of watcher, restart and env
6
+ * plumbing that surround it in `_dev-server.ts`, which is also what pushed that
7
+ * file past the length cap.
8
+ */
9
+ /**
10
+ * Vite dev-server config for the client SPA. Extracted so the proxy wiring
11
+ * is unit-testable: `/websocket` MUST proxy with `ws: true` or `aai dev`
12
+ * with a `client.tsx` serves a page whose WebSocket never connects.
13
+ *
14
+ * **This table is the whole agent API as the browser can see it**, which is
15
+ * the thing to hold in mind before adding a route to `createServer`. Vite owns
16
+ * the port the user is told to open and answers everything not listed here
17
+ * itself — with a bare 404 carrying none of the agent server's headers, so the
18
+ * failure looks like a missing route rather than a missing proxy entry.
19
+ *
20
+ * `/workflows` is why that matters beyond voice. A WORKFLOW APP
21
+ * (`workflowApp()`) has no session and no socket: `page()` mounts a
22
+ * form and every single thing it does — listing workflows, starting a run,
23
+ * polling it, streaming its events — is a same-origin `fetch` under that
24
+ * prefix. Unproxied, the two workflow-app templates were dead on arrival under
25
+ * `aai dev` (`404 POST /workflows/runs` the instant the form is submitted)
26
+ * while the backend served the API correctly one port over. A string key
27
+ * prefix-matches, so this one entry covers `/runs`, `/runs/:id` and the
28
+ * `/runs/:id/events` SSE stream.
29
+ *
30
+ * `strictPort` because the reported URL is `http://localhost:<port>` —
31
+ * without it, Vite silently binds port+N when the port is busy and the
32
+ * printed/JSON-returned URL points at whatever else was listening.
33
+ *
34
+ * `AAI_DEV_HOST` reaches BOTH servers. Binding only the backend left Vite —
35
+ * the port the user is told to open — on loopback, i.e. failing exactly the
36
+ * case that variable exists for (`aai dev` in a container, reached from the host).
37
+ *
38
+ * ## The target is an IP LITERAL, and that is a fix rather than a style choice
39
+ *
40
+ * `127.0.0.1`, never `localhost`. Vite opens a FRESH upstream connection for
41
+ * every WebSocket upgrade — an HTTP request reuses a pooled keep-alive socket and
42
+ * so resolves rarely — which means a hostname here is one `getaddrinfo` per
43
+ * session handshake. That lookup runs on libuv's threadpool (four threads,
44
+ * shared with every other fs and DNS call in a process that is ALSO serving the
45
+ * agent), and under load it intermittently stalls for almost exactly two
46
+ * seconds.
47
+ *
48
+ * Measured on the `retail` template, session handshakes to `session.configured`:
49
+ *
50
+ * | Target | conc | rps | p50 | p99 |
51
+ * | --- | --- | --- | --- | --- |
52
+ * | `localhost` | 1 | 12-18 | 8-11 ms | 2.0 s |
53
+ * | `localhost` | 10 | 0.6 | 16.7 s | 16.7 s |
54
+ * | `127.0.0.1` | 1 | 89-207 | 4-9 ms | 23-49 ms |
55
+ * | `127.0.0.1` | 20 | 260 | 73 ms | 166 ms |
56
+ *
57
+ * The `localhost` row is not a slow proxy, it is a queue: one handshake in thirty
58
+ * stalls two seconds on its own, and at concurrency 10 the stalls pile onto four
59
+ * threads until a sustained burst left the proxy refusing upgrades entirely until
60
+ * the dev server was restarted. With the literal it recovers from a burst and
61
+ * sits within ~1.5x of the backend port.
62
+ *
63
+ * Localized by timing the phases separately — TCP connect and the first frame
64
+ * were always fast, the 101 was not — and then by comparing the instant the
65
+ * client sent its upgrade against the backend's own log line for it: 23.808 out,
66
+ * 25.796 in, answered in 5 ms. The two seconds were spent before Vite dialled,
67
+ * which is what pointed at resolution rather than at either server.
68
+ *
69
+ * Not a behaviour change: `localhost` resolved to loopback anyway, so this
70
+ * removes the lookup and nothing else. An `AAI_DEV_HOST` that binds the backend
71
+ * to ONE non-loopback interface was unreachable through the proxy before this and
72
+ * still is.
73
+ */
74
+ export declare function viteDevConfig(cwd: string, vitePort: number, backendPort: number): import("vite").InlineConfig;
@@ -3,13 +3,25 @@
3
3
  * registry setup, dependency installation, and process/server utilities.
4
4
  * Each e2e suite performs its own setup/teardown using these helpers.
5
5
  */
6
- import type { ChildProcess } from "node:child_process";
6
+ import { type ChildProcess } from "node:child_process";
7
7
  import type { MockRegistry } from "./_mock-registry.ts";
8
8
  export declare const dir: string;
9
9
  export declare const packagesDir: string;
10
10
  export declare const pm: "pnpm" | "npm" | "yarn";
11
11
  export declare function aaiEnv(): NodeJS.ProcessEnv;
12
12
  export declare function aai(aaiBin: string, args: string[], cwd: string, timeoutMs?: number): void;
13
+ /**
14
+ * {@link aai}, but CAPTURING — for the cases whose subject is what the CLI
15
+ * SAID rather than that it exited 0.
16
+ *
17
+ * Separate rather than a flag, because `stdio: "inherit"` is load-bearing for
18
+ * every other call: an e2e failure's only diagnosis trail is the child's output
19
+ * in the run log, and capturing it by default would take that away.
20
+ */
21
+ export declare function aaiOutput(aaiBin: string, args: string[], cwd: string, timeoutMs?: number): {
22
+ stdout: string;
23
+ stderr: string;
24
+ };
13
25
  /**
14
26
  * Build the CLI and return the path to the built binary.
15
27
  *
@@ -39,6 +51,26 @@ export declare function startRegistry(): Promise<MockRegistry>;
39
51
  export declare function waitForHealth(url: string, child?: ChildProcess, timeoutMs?: number): Promise<void>;
40
52
  /** Wait for a child process to exit (for clean teardown). */
41
53
  export declare function waitForExit(child: ChildProcess, timeoutMs?: number): Promise<void>;
54
+ /**
55
+ * Run a scaffolded project the way a self-hoster does — `npm start`, which runs
56
+ * the project's own `prestart` (`aai build`) and then `server.mjs`.
57
+ *
58
+ * Extracted because there are two legs now and the spawn is the fiddly half: the
59
+ * port has to be read off stdout, and every way this can fail — a build error, a
60
+ * missing artifact, a throwing agent — reports on STDERR, so both streams are
61
+ * buffered and both go into the failure. Discarding stderr leaves a bare
62
+ * "exited with code 1" naming none of them, which cost a full diagnosis cycle
63
+ * once already.
64
+ *
65
+ * `PORT=0` lets the OS assign one: this suite runs servers concurrently and a
66
+ * fixed port is an EADDRINUSE flake waiting to happen.
67
+ */
68
+ export declare function startSelfHostedServer(projectDir: string, extraEnv?: NodeJS.ProcessEnv): Promise<{
69
+ url: string;
70
+ port: number;
71
+ stop: () => Promise<void>;
72
+ output: () => string;
73
+ }>;
42
74
  /**
43
75
  * Whether a failed install may be excused as the mock registry's npmjs
44
76
  * passthrough failing rather than as a real dependency-resolution break.