@alexkroman1/aai-cli 9.2.0 → 10.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 (81) hide show
  1. package/dist/{_bundler-CDuYl5Gb.mjs → _bundler-B31MqYaD.mjs} +7 -33
  2. package/dist/_bundler.d.ts +3 -41
  3. package/dist/{_dev-server-BAR8Ya43.mjs → _dev-server-Bo5dSBzQ.mjs} +140 -44
  4. package/dist/_dev-server.d.ts +1 -8
  5. package/dist/_dev-vite-config.d.ts +21 -0
  6. package/dist/{_init-BNotRgV1.mjs → _init-BURbdHNM.mjs} +1 -1
  7. package/dist/{_slug-api--ZBmIGH-.mjs → _slug-api-CfGQq1Tt.mjs} +1 -1
  8. package/dist/{_templates-4WcKOjS5.mjs → _templates-0Zr2z6yA.mjs} +37 -2
  9. package/dist/_templates.d.ts +29 -0
  10. package/dist/{build-DvDdSwAS.mjs → build-DKC2e9o_.mjs} +1 -2
  11. package/dist/cli.mjs +46 -22
  12. package/dist/{delete-8pjH8n8n.mjs → delete-X_CRgCf6.mjs} +1 -1
  13. package/dist/{deploy-CjU3e-3P.mjs → deploy-CchW5pmw.mjs} +2 -2
  14. package/dist/{dev-dFzdvwa7.mjs → dev-BfVl1uBA.mjs} +1 -1
  15. package/dist/{init-DK7GpIF9.mjs → init-BRfoc2EK.mjs} +2 -2
  16. package/dist/{logs-D2Yhqe2w.mjs → logs-hWnJ66Bl.mjs} +2 -2
  17. package/dist/scaffold/CLAUDE.md +162 -54
  18. package/dist/scaffold/package.json +4 -6
  19. package/dist/scaffold/server.mjs +12 -23
  20. package/dist/{secret-CwgDLpSy.mjs → secret-B-3T5tvr.mjs} +2 -2
  21. package/dist/{studio-D3-HWIfk.mjs → studio-wVWNLREn.mjs} +4 -3
  22. package/dist/templates/call-audit/agent.eval.test.ts +1 -1
  23. package/dist/templates/call-audit/agent.test.ts +80 -8
  24. package/dist/templates/call-audit/client.tsx +87 -3
  25. package/dist/templates/call-audit/workflows/audit.ts +52 -32
  26. package/dist/templates/call-audit/workflows/ingest.ts +15 -15
  27. package/dist/templates/call-audit/workflows/media.ts +1 -1
  28. package/dist/templates/call-audit/workflows/summarize.ts +0 -4
  29. package/dist/templates/link-digest/agent.eval.test.ts +4 -2
  30. package/dist/templates/link-digest/agent.test.ts +25 -5
  31. package/dist/templates/link-digest/client.tsx +65 -7
  32. package/dist/templates/link-digest/workflows/digest.ts +39 -29
  33. package/dist/templates/podcast-digest/agent.eval.test.ts +5 -5
  34. package/dist/templates/podcast-digest/agent.test.ts +108 -2
  35. package/dist/templates/podcast-digest/client.tsx +89 -8
  36. package/dist/templates/podcast-digest/workflows/digest.ts +67 -48
  37. package/dist/templates/podcast-digest/workflows/feeds.ts +1 -4
  38. package/dist/templates/podcast-digest/workflows/slack.ts +5 -6
  39. package/dist/templates/recap-workflow/agent.eval.test.ts +72 -8
  40. package/dist/templates/recap-workflow/agent.test.ts +323 -83
  41. package/dist/templates/recap-workflow/shared.ts +4 -3
  42. package/dist/templates/recap-workflow/workflows/recap.ts +322 -86
  43. package/dist/templates/recap-workflow/workflows/tokens.ts +21 -0
  44. package/dist/templates/redline/agent.eval.test.ts +1 -1
  45. package/dist/templates/redline/agent.test.ts +1 -1
  46. package/dist/templates/redline/client.tsx +83 -3
  47. package/dist/templates/redline/workflows/redline.ts +26 -23
  48. package/dist/templates/research-workflow/agent.eval.test.ts +9 -8
  49. package/dist/templates/research-workflow/agent.test.ts +32 -6
  50. package/dist/templates/research-workflow/agent.ts +4 -4
  51. package/dist/templates/research-workflow/shared.ts +4 -3
  52. package/dist/templates/research-workflow/workflows/prompts.ts +1 -1
  53. package/dist/templates/research-workflow/workflows/research.ts +35 -38
  54. package/dist/templates/spoken-summary/agent.eval.test.ts +3 -3
  55. package/dist/templates/spoken-summary/agent.test.ts +10 -8
  56. package/dist/templates/spoken-summary/client.tsx +72 -3
  57. package/dist/templates/spoken-summary/workflows/summarize.ts +25 -21
  58. package/dist/templates/spoken-summary/workflows/transcribe.ts +15 -18
  59. package/dist/templates/transcription-workflow/agent.eval.test.ts +1 -1
  60. package/dist/templates/transcription-workflow/agent.test.ts +626 -14
  61. package/dist/templates/transcription-workflow/agent.ts +6 -4
  62. package/dist/templates/transcription-workflow/client.tsx +106 -308
  63. package/dist/templates/transcription-workflow/recover.test.ts +141 -0
  64. package/dist/templates/transcription-workflow/recover.ts +130 -0
  65. package/dist/templates/transcription-workflow/run-panel.tsx +203 -0
  66. package/dist/templates/transcription-workflow/total-latency.tsx +144 -0
  67. package/dist/templates/transcription-workflow/workflows/batch.ts +21 -24
  68. package/dist/templates/transcription-workflow/workflows/downsample.ts +273 -0
  69. package/dist/templates/transcription-workflow/workflows/normalize.ts +17 -40
  70. package/dist/templates/transcription-workflow/workflows/stream.ts +177 -24
  71. package/dist/templates/transcription-workflow/workflows/transcribe.ts +69 -32
  72. package/dist/templates/transcription-workflow/workflows/wav.ts +15 -4
  73. package/dist/worker-bundler.d.ts +0 -14
  74. package/dist/worker-bundler.mjs +149 -1
  75. package/dist/{workflow-BflATMmU.mjs → workflow-BBu5oEDw.mjs} +84 -10
  76. package/dist/workflow.d.ts +1 -0
  77. package/package.json +4 -6
  78. package/dist/_workflow-scan.d.ts +0 -108
  79. package/dist/worker-bundler-CCVEDjm1.mjs +0 -650
  80. package/dist/workflow-bundler.d.ts +0 -138
  81. package/dist/{_studio-CKrsixd0.mjs → _studio-B1waXMuR.mjs} +1 -1
@@ -1,10 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { f as validateAgentExport } from "./_utils-B8QmtFhK.mjs";
3
3
  import { t as buildClient } from "./client-bundler-CWnG42cU.mjs";
4
- import { n as buildWorkflows, t as buildWorker } from "./worker-bundler-CCVEDjm1.mjs";
4
+ import { buildWorker } from "./worker-bundler.mjs";
5
5
  import path from "node:path";
6
6
  import { pathToFileURL } from "node:url";
7
- import { omitUndefined } from "@alexkroman1/aai/utils";
8
7
  import { mkdtemp, rm, writeFile } from "node:fs/promises";
9
8
  import { tmpdir } from "node:os";
10
9
  import { hash } from "node:crypto";
@@ -18,14 +17,10 @@ import { hash } from "node:crypto";
18
17
  * config and evaluates nothing, so every read of one happens here.
19
18
  */
20
19
  async function buildAgentBundle(cwd, opts = {}) {
21
- const [workflows, clientFiles] = await Promise.all([buildWorkflows(cwd), buildClient(cwd)]);
20
+ const [clientFiles, worker] = await Promise.all([buildClient(cwd), buildWorker(cwd, opts)]);
22
21
  return {
23
- worker: await buildWorker(cwd, {
24
- ...opts,
25
- workflows
26
- }),
27
- clientFiles,
28
- ...omitUndefined({ workflows })
22
+ worker,
23
+ clientFiles
29
24
  };
30
25
  }
31
26
  /**
@@ -47,27 +42,10 @@ async function buildAgentBundle(cwd, opts = {}) {
47
42
  * experimental.
48
43
  */
49
44
  async function evalWorkerBundle(code) {
50
- return (await evalWorkerWithWorkflows(code)).agent;
51
- }
52
- /**
53
- * {@link evalWorkerBundle}, keeping the workflow exports the AgentDef cannot
54
- * carry.
55
- *
56
- * A separate function rather than a wider return from `evalWorkerBundle`,
57
- * because every other caller wants the agent and nothing else — and the two
58
- * strings are ~76 KB that a deploy path has no use for.
59
- */
60
- async function evalWorkerWithWorkflows(code) {
61
45
  const mod = await importWorkerModule(code);
62
46
  const agentDef = mod.default ?? mod;
63
47
  validateAgentExport(agentDef);
64
- const workflowCode = mod.__aaiWorkflowCode;
65
- const stepCode = mod.__aaiStepCode;
66
- return {
67
- agent: agentDef,
68
- workflowCode: typeof workflowCode === "string" ? workflowCode : void 0,
69
- stepCode: typeof stepCode === "string" ? stepCode : void 0
70
- };
48
+ return agentDef;
71
49
  }
72
50
  /** Import a built worker from a temp file. See {@link evalWorkerBundle}. */
73
51
  async function importWorkerModule(code) {
@@ -100,16 +78,12 @@ async function evalWorkerConfig(code) {
100
78
  return (await importWorkerModule(code)).__aaiConfig;
101
79
  }
102
80
  /**
103
- * Memoizing wrapper around `evalWorkerWithWorkflows` for long-lived callers
81
+ * Memoizing wrapper around {@link evalWorkerBundle} for long-lived callers
104
82
  * (the dev server): byte-identical worker code returns the previously
105
83
  * evaluated result without touching the ESM registry. No-op saves and formatter
106
84
  * churn are the common watcher events, so this caps the registry leak (see
107
85
  * `evalWorkerBundle`) to genuinely-new bundles — the residual one-module-per-
108
86
  * distinct-build leak is accepted for the reasons documented there.
109
- *
110
- * It evaluates the WORKFLOW-carrying variant because its one caller is
111
- * `aai dev`, which has no guest to re-read the bundle: dropping the two strings
112
- * here is what left the dev server unable to serve a workflow at all.
113
87
  */
114
88
  function createWorkerEvaluator() {
115
89
  let lastHash;
@@ -117,7 +91,7 @@ function createWorkerEvaluator() {
117
91
  return async (code) => {
118
92
  const codeHash = hash("sha256", code);
119
93
  if (lastWorker && codeHash === lastHash) return lastWorker;
120
- const worker = await evalWorkerWithWorkflows(code);
94
+ const worker = await evalWorkerBundle(code);
121
95
  lastHash = codeHash;
122
96
  lastWorker = worker;
123
97
  return worker;
@@ -1,21 +1,11 @@
1
1
  import type { AgentDef } from "@alexkroman1/aai";
2
2
  import { type BuildWorkerOptions } from "./worker-bundler.ts";
3
- import { type WorkflowBundleOutput } from "./workflow-bundler.ts";
4
- /** Output from the bundler: worker ESM + client files + workflow artifacts. */
3
+ /** Output from the bundler: worker ESM + client files. */
5
4
  export type DirectoryBundleOutput = {
6
5
  /** ESM bundle of agent.ts (tool execute functions + hook handlers). */
7
6
  worker: string;
8
7
  /** Static client files from Vite build. Empty if no client.tsx. */
9
8
  clientFiles: Record<string, string>;
10
- /**
11
- * The project's durable workflows, or undefined when it declares none.
12
- *
13
- * Built here rather than in the guest because the transform is per TENANT: the
14
- * guest image is baked once and serves many agents, so there is no
15
- * `workflows/` directory in existence when it is built. See
16
- * `workflow-bundler.ts`.
17
- */
18
- workflows?: WorkflowBundleOutput;
19
9
  };
20
10
  /**
21
11
  * Bundle an agent directory: build agent.ts into worker ESM + client files.
@@ -45,30 +35,6 @@ export declare function buildAgentBundle(cwd: string, opts?: BuildWorkerOptions)
45
35
  * experimental.
46
36
  */
47
37
  export declare function evalWorkerBundle(code: string): Promise<AgentDef>;
48
- /**
49
- * The agent plus the compiled workflow surface a worker carries.
50
- *
51
- * `evalWorkerBundle` returns only the `AgentDef`, which is all a deploy needs —
52
- * the guest re-reads the bundle itself. `aai dev` never hands the bundle to a
53
- * guest, so this is the only path by which the workflow code reaches a running
54
- * server locally, and dropping it is why `aai dev` served no workflows at first.
55
- */
56
- export type EvaluatedWorker = {
57
- agent: AgentDef;
58
- /** `__aaiWorkflowCode` — the flow bundle, for `workflowEntrypoint`. */
59
- workflowCode: string | undefined;
60
- /** `__aaiStepCode` — evaluated to register the project's step functions. */
61
- stepCode: string | undefined;
62
- };
63
- /**
64
- * {@link evalWorkerBundle}, keeping the workflow exports the AgentDef cannot
65
- * carry.
66
- *
67
- * A separate function rather than a wider return from `evalWorkerBundle`,
68
- * because every other caller wants the agent and nothing else — and the two
69
- * strings are ~76 KB that a deploy path has no use for.
70
- */
71
- export declare function evalWorkerWithWorkflows(code: string): Promise<EvaluatedWorker>;
72
38
  /**
73
39
  * The config a built worker describes about itself — its `__aaiConfig`
74
40
  * export, generated by `buildWorker`'s wrapper entry (`toAgentConfig(def)`
@@ -84,15 +50,11 @@ export declare function evalWorkerWithWorkflows(code: string): Promise<Evaluated
84
50
  */
85
51
  export declare function evalWorkerConfig(code: string): Promise<unknown>;
86
52
  /**
87
- * Memoizing wrapper around `evalWorkerWithWorkflows` for long-lived callers
53
+ * Memoizing wrapper around {@link evalWorkerBundle} for long-lived callers
88
54
  * (the dev server): byte-identical worker code returns the previously
89
55
  * evaluated result without touching the ESM registry. No-op saves and formatter
90
56
  * churn are the common watcher events, so this caps the registry leak (see
91
57
  * `evalWorkerBundle`) to genuinely-new bundles — the residual one-module-per-
92
58
  * distinct-build leak is accepted for the reasons documented there.
93
- *
94
- * It evaluates the WORKFLOW-carrying variant because its one caller is
95
- * `aai dev`, which has no guest to re-read the bundle: dropping the two strings
96
- * here is what left the dev server unable to serve a workflow at all.
97
59
  */
98
- export declare function createWorkerEvaluator(): (code: string) => Promise<EvaluatedWorker>;
60
+ export declare function createWorkerEvaluator(): (code: string) => Promise<AgentDef>;
@@ -3,18 +3,18 @@ import { i as outputSilenced, r as notify } from "./_ui-u7T4YooX.mjs";
3
3
  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
- import { n as buildWorkflows, t as buildWorker } from "./worker-bundler-CCVEDjm1.mjs";
6
+ import { buildWorker } from "./worker-bundler.mjs";
7
7
  import { typecheckProject } from "./typecheck.mjs";
8
8
  import { n as ensureApiKey } from "./_config-CmJOFsAP.mjs";
9
- import { n as createWorkerEvaluator } from "./_bundler-CDuYl5Gb.mjs";
9
+ import { n as createWorkerEvaluator } from "./_bundler-B31MqYaD.mjs";
10
10
  import { t as resolveServerEnv } from "./_server-common-6e0QI4mq.mjs";
11
- import { existsSync } from "node:fs";
11
+ import { existsSync, statSync } from "node:fs";
12
12
  import path from "node:path";
13
13
  import { omitUndefined } from "@alexkroman1/aai/utils";
14
- import { createCoalescingRunner, sleep } from "@alexkroman1/aai/internal";
14
+ import { createCoalescingRunner, requestPath, sleep } from "@alexkroman1/aai/internal";
15
15
  import { agentConfigWarnings } from "@alexkroman1/aai/manifest";
16
- import { WORKFLOW_API_PREFIX, createRuntime, createServer, ensureSessionStateSchema, requiredProviderEnvVars, withHostCredentialFallback } from "@alexkroman1/aai-runtime";
17
- import { configureWorkflowWorld, consoleLogger, createWorkflowSurface, handleWorkflowRequest, publishStepEnv, startWorkflowWorldIfDeclared } from "@alexkroman1/aai-runtime/internal";
16
+ import { DEFAULT_LISTEN_HOST, WORKFLOW_API_PREFIX, createRuntime, createServer, ensureSessionStateSchema, ensureWorkflowJournalSchema, requiredProviderEnvVars, withHostCredentialFallback } from "@alexkroman1/aai-runtime";
17
+ import { WORKFLOW_DATA_DIR_ENV, consoleLogger, createMemoryJournal, handleWorkflowRequest, publishStepEnv } from "@alexkroman1/aai-runtime/internal";
18
18
  import { defaultClientDir } from "@alexkroman1/aai-ui/client-dir";
19
19
  import { watch } from "chokidar";
20
20
  import getPort, { portNumbers } from "get-port";
@@ -286,6 +286,77 @@ function createDevTypecheck(cwd, report = notify) {
286
286
  * file past the length cap.
287
287
  */
288
288
  /**
289
+ * The request under {@link WORKFLOW_API_PREFIX} that VITE must answer, not the
290
+ * proxy — or `undefined` for every request the workflow API owns.
291
+ *
292
+ * `/workflows` is a proxy prefix key AND the directory the SDK tells authors to
293
+ * put workflow bodies in, so the two claim the same URL space. A string key
294
+ * prefix-matches, which is what makes one entry cover `/runs/:id/events` — and
295
+ * it also swallowed `transcription-workflow`'s `client.tsx:173`, a value import
296
+ * of `./workflows/stitch.ts`. Vite rewrites that specifier to the absolute
297
+ * `/workflows/stitch.ts` during import analysis, the proxy claimed it, and the
298
+ * agent server answered the `404 {"error":"Not found"}` its workflow router
299
+ * gives any unmatched path under the prefix. The browser refuses a module
300
+ * served as `application/json`, so the page rendered BLANK — and this lands on
301
+ * the naming convention every workflow template follows, not on one template's
302
+ * bad luck.
303
+ *
304
+ * ## Why the filesystem decides, and not a route table
305
+ *
306
+ * The obvious fix is to narrow the proxy to the API's real shape: the fourteen
307
+ * routes are enumerable (`/runs`, `/runs/:id`, `…/events`, `…/stream`,
308
+ * `…/wake`, `/uploads`, `…/parts`, `…/info`). That restates `aai-runtime`'s
309
+ * router here, in a package that cannot see it — and a route added there but
310
+ * missing from the copy 404s under `aai dev` while working deployed, which is
311
+ * the exact silent failure this proxy table exists to prevent and the reason
312
+ * the entry is keyed off the SDK's own constant rather than a literal. The
313
+ * drift runs the wrong way.
314
+ *
315
+ * Enumerating what VITE owns inverts it: a new API route has no file behind it
316
+ * and is proxied with nothing to update. The only thing that can shadow the API
317
+ * is a real file at a real route's exact path — `workflows/runs` with no
318
+ * extension, or a `workflows/runs/` directory — where an unreachable API is the
319
+ * lesser of the two failures and the author can see the file that caused it.
320
+ * Note `workflows/runs.ts` is NOT such a file: `/workflows/runs.ts` is neither
321
+ * `/workflows/runs` nor under `/workflows/runs/`.
322
+ *
323
+ * Moving the API off `/workflows` was the third option and is the expensive one
324
+ * — it is a wire change reaching the SDK client, the platform's broker and
325
+ * every deployed agent, to buy what a file check buys locally.
326
+ *
327
+ * ## Only an EXACT file counts
328
+ *
329
+ * No extension resolution, deliberately: `/workflows/runs` must not find
330
+ * `workflows/runs.ts`, and a workflow body named `runs.ts` is entirely
331
+ * plausible. Nothing is lost by it — Vite resolves specifiers SERVER-side and
332
+ * rewrites them to paths that exist, so an extensionless `./workflows/stitch`
333
+ * reaches the browser as `/workflows/stitch.ts` (verified: both the explicit
334
+ * and the bare import in one module come back with `.ts` appended). The browser
335
+ * only ever asks for files Vite already found.
336
+ *
337
+ * A path escaping the root is refused rather than served — a raw client can
338
+ * send `..` where a browser would normalize it — and a malformed percent-escape
339
+ * is left to the API, which is where a path we cannot resolve belongs.
340
+ */
341
+ function workflowPathServedByVite(root, rawUrl) {
342
+ if (rawUrl === void 0) return void 0;
343
+ let decoded;
344
+ try {
345
+ decoded = decodeURIComponent(requestPath(rawUrl));
346
+ } catch {
347
+ return;
348
+ }
349
+ const base = path.resolve(root);
350
+ const resolved = path.resolve(base, `.${decoded}`);
351
+ if (resolved !== base && !resolved.startsWith(base + path.sep)) return void 0;
352
+ try {
353
+ if (!statSync(resolved).isFile()) return void 0;
354
+ } catch {
355
+ return;
356
+ }
357
+ return rawUrl;
358
+ }
359
+ /**
289
360
  * Vite dev-server config for the client SPA. Extracted so the proxy wiring
290
361
  * is unit-testable: `/websocket` MUST proxy with `ws: true` or `aai dev`
291
362
  * with a `client.tsx` serves a page whose WebSocket never connects.
@@ -314,6 +385,27 @@ function createDevTypecheck(cwd, report = notify) {
314
385
  * the port the user is told to open — on loopback, i.e. failing exactly the
315
386
  * case that variable exists for (`aai dev` in a container, reached from the host).
316
387
  *
388
+ * ## With no `AAI_DEV_HOST` the bind host is the BACKEND's, not Vite's default
389
+ *
390
+ * Vite's default `server.host` is the hostname `localhost`, so Node binds
391
+ * whatever `getaddrinfo` returns first — `::1` on macOS, usually `127.0.0.1` on
392
+ * Linux. Measured here: `vite.httpServer.address()` reported
393
+ * `{ address: "::1", family: "IPv6" }` and `http://127.0.0.1:<port>` was
394
+ * ECONNREFUSED while `http://localhost:<port>` worked. So the same command
395
+ * produced different reachability per machine, and `aai dev` reports
396
+ * `http://localhost:<port>` either way — a caller that resolves IPv4-only, or
397
+ * dials the literal, gets a connection refused against a server that is up.
398
+ *
399
+ * The two halves of `aai dev` also disagreed: `createServer` binds
400
+ * {@link DEFAULT_LISTEN_HOST} explicitly, Vite took its own default, and only a
401
+ * set `AAI_DEV_HOST` brought them back together. Taking the same constant is
402
+ * what makes them agree by construction rather than by two matching literals.
403
+ *
404
+ * IPv4 loopback is the SAFE side of that choice rather than a coin flip:
405
+ * browsers, curl and undici all try every address `localhost` resolves to, so a
406
+ * `127.0.0.1` bind stays reachable as `localhost`, while a `::1` bind is not
407
+ * reachable at all from a client holding the IPv4 literal.
408
+ *
317
409
  * ## The target is an IP LITERAL, and that is a fix rather than a style choice
318
410
  *
319
411
  * `127.0.0.1`, never `localhost`. Vite opens a FRESH upstream connection for
@@ -359,7 +451,7 @@ function viteDevConfig(cwd, vitePort, backendPort) {
359
451
  server: {
360
452
  port: vitePort,
361
453
  strictPort: true,
362
- ...omitUndefined({ host: devBindHost() }),
454
+ host: devBindHost() ?? DEFAULT_LISTEN_HOST,
363
455
  proxy: {
364
456
  "/health": target,
365
457
  "/client-config": target,
@@ -367,7 +459,10 @@ function viteDevConfig(cwd, vitePort, backendPort) {
367
459
  target,
368
460
  ws: true
369
461
  },
370
- [WORKFLOW_API_PREFIX]: target
462
+ [WORKFLOW_API_PREFIX]: {
463
+ target,
464
+ bypass: (req) => workflowPathServedByVite(cwd, req.url)
465
+ }
371
466
  }
372
467
  }
373
468
  };
@@ -430,20 +525,9 @@ async function resolveAgentEnv(root, agentDef) {
430
525
  * keeps the old server. Evaluation goes through the memoizing evaluator so
431
526
  * a no-op save doesn't leak another module into the ESM registry.
432
527
  *
433
- * The project's `workflows/` directory is compiled by the same pass deploy
434
- * runs (`buildWorkflows`) and rides the bundle as two string exports, which is
435
- * how `aai dev` serves a workflow at all: nothing here hands the bundle to a
436
- * guest, so the CLI is both ends of that contract locally. A project with no
437
- * `workflows/` directory pays nothing — `buildWorkflows` resolves `undefined`
438
- * without starting a builder.
439
528
  */
440
529
  async function loadWorker(cwd, evaluate) {
441
- const workflows = await buildWorkflows(cwd);
442
- for (const warning of workflows?.warnings ?? []) notify("warn", warning);
443
- return evaluate(await buildWorker(cwd, {
444
- runtime: false,
445
- workflows
446
- }));
530
+ return evaluate(await buildWorker(cwd, { runtime: false }));
447
531
  }
448
532
  /**
449
533
  * True for paths that should never trigger a restart: anything inside
@@ -496,25 +580,38 @@ function watchDirectory(dir, onChange) {
496
580
  */
497
581
  async function startDevServer(opts) {
498
582
  const { cwd, port } = opts;
583
+ if (!process.env[WORKFLOW_DATA_DIR_ENV]?.trim()) process.env[WORKFLOW_DATA_DIR_ENV] = path.join(cwd, ".workflow-data");
499
584
  const hasClient = existsSync(path.join(cwd, "client.tsx"));
500
585
  const backendPort = hasClient ? await getPort({ port: portNumbers(port + 1, port + 100) }) : port;
501
586
  const vitePort = port;
502
587
  const clientDirOpt = hasClient ? {} : { clientDir: defaultClientDir() };
503
588
  const evaluateWorker = createWorkerEvaluator();
504
589
  const devLogger = createDevLogger(outputSilenced());
505
- let workflowWorldStarted = false;
506
590
  /**
507
591
  * Whether the session-state tables have been ensured this process.
508
592
  *
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.
593
+ * Once per dev server rather than per restart: a rebuild replaces the routes,
594
+ * not the storage behind them, and re-running the DDL on every file save would
595
+ * be two round trips per keystroke burst.
512
596
  */
513
597
  let sessionSchemaEnsured = false;
598
+ /**
599
+ * Where a databaseless project's durable runs live for this whole `aai dev`.
600
+ *
601
+ * STORAGE per PROCESS, CODE per BUILD — the split the deleted DevKit world
602
+ * made on our behalf. Every save rebuilds the runtime (that is what reloads a
603
+ * workflow BODY) and a rebuild handed no journal builds a fresh one inside the
604
+ * engine, so a run started before a save was gone after it and
605
+ * `GET /workflows/runs/:id` 404'd for a run the page was still polling.
606
+ * Deliberately NOT the whole client: reusing build 1's would freeze every body
607
+ * at build 1. `RuntimeOptions.journal` carries the rest, including what this
608
+ * does NOT restore (a `ctx.sleep` parked across the save still needs a
609
+ * delivery) and why it cannot demote a postgres or platform journal.
610
+ */
611
+ const journal = createMemoryJournal();
514
612
  /** Full build sequence, shared by initial startup and every restart. */
515
613
  async function buildServer() {
516
- const worker = await loadWorker(cwd, evaluateWorker);
517
- const agentDef = worker.agent;
614
+ const agentDef = await loadWorker(cwd, evaluateWorker);
518
615
  const env = await resolveAgentEnv(cwd, agentDef);
519
616
  if (env.DATABASE_URL && !sessionSchemaEnsured) {
520
617
  sessionSchemaEnsured = true;
@@ -522,33 +619,32 @@ async function startDevServer(opts) {
522
619
  url: env.DATABASE_URL,
523
620
  logger: devLogger
524
621
  });
622
+ await ensureWorkflowJournalSchema({
623
+ url: env.DATABASE_URL,
624
+ logger: devLogger
625
+ });
525
626
  }
526
- const world = configureWorkflowWorld({
527
- databaseUrl: env.DATABASE_URL,
528
- port: backendPort,
529
- dataDir: path.join(cwd, ".workflow-data")
530
- });
531
627
  publishStepEnv(env);
532
- const workflows = await createWorkflowSurface(worker.workflowCode, worker.stepCode);
533
- if (workflows && !workflowWorldStarted) {
534
- workflowWorldStarted = true;
535
- await startWorkflowWorldIfDeclared(true, world);
536
- }
537
628
  const providerEnv = withHostCredentialFallback(env);
629
+ const runtime = createRuntime({
630
+ agent: agentDef,
631
+ env,
632
+ providerEnv,
633
+ logger: devLogger,
634
+ journal,
635
+ publicUrl: process.env.PUBLIC_URL?.trim() || `http://localhost:${backendPort}`
636
+ });
538
637
  return createServer({
539
- runtime: createRuntime({
540
- agent: agentDef,
541
- env,
542
- providerEnv,
543
- logger: devLogger,
544
- publicUrl: process.env.PUBLIC_URL?.trim() || `http://localhost:${backendPort}`
545
- }),
638
+ runtime,
546
639
  name: agentDef.name,
547
640
  env: hostModeEnv(providerEnv),
548
641
  hostBaseAgent: agentDef,
549
642
  greeting: agentDef.greeting,
550
643
  ...omitUndefined({ page: agentDef.page }),
551
- request: (req, res, url, method) => handleWorkflowRequest(workflows, req, res, url, method),
644
+ request: (req, res, url, method) => handleWorkflowRequest(req, res, url, method, {
645
+ deliver: () => runtime.deliverWorkflow,
646
+ logger: devLogger
647
+ }),
552
648
  ...clientDirOpt
553
649
  });
554
650
  }
@@ -8,7 +8,6 @@
8
8
  */
9
9
  import type { AgentDef } from "@alexkroman1/aai";
10
10
  import { type FSWatcher } from "chokidar";
11
- import { type EvaluatedWorker } from "./_bundler.ts";
12
11
  /**
13
12
  * Warnings about the agent's credentials, computed against the `.env`-derived
14
13
  * env and the shell. Pure so it is directly testable; `resolveAgentEnv` logs
@@ -36,14 +35,8 @@ export declare function agentEnvWarnings(agentDef: Pick<AgentDef, "stt" | "llm"
36
35
  * keeps the old server. Evaluation goes through the memoizing evaluator so
37
36
  * a no-op save doesn't leak another module into the ESM registry.
38
37
  *
39
- * The project's `workflows/` directory is compiled by the same pass deploy
40
- * runs (`buildWorkflows`) and rides the bundle as two string exports, which is
41
- * how `aai dev` serves a workflow at all: nothing here hands the bundle to a
42
- * guest, so the CLI is both ends of that contract locally. A project with no
43
- * `workflows/` directory pays nothing — `buildWorkflows` resolves `undefined`
44
- * without starting a builder.
45
38
  */
46
- export declare function loadWorker(cwd: string, evaluate: (code: string) => Promise<EvaluatedWorker>): Promise<EvaluatedWorker>;
39
+ export declare function loadWorker(cwd: string, evaluate: (code: string) => Promise<AgentDef>): Promise<AgentDef>;
47
40
  /**
48
41
  * True for paths that should never trigger a restart: anything inside
49
42
  * `node_modules/` and any dot-entry (`.git/`, `.aai/`, `.DS_Store`, …).
@@ -35,6 +35,27 @@
35
35
  * the port the user is told to open — on loopback, i.e. failing exactly the
36
36
  * case that variable exists for (`aai dev` in a container, reached from the host).
37
37
  *
38
+ * ## With no `AAI_DEV_HOST` the bind host is the BACKEND's, not Vite's default
39
+ *
40
+ * Vite's default `server.host` is the hostname `localhost`, so Node binds
41
+ * whatever `getaddrinfo` returns first — `::1` on macOS, usually `127.0.0.1` on
42
+ * Linux. Measured here: `vite.httpServer.address()` reported
43
+ * `{ address: "::1", family: "IPv6" }` and `http://127.0.0.1:<port>` was
44
+ * ECONNREFUSED while `http://localhost:<port>` worked. So the same command
45
+ * produced different reachability per machine, and `aai dev` reports
46
+ * `http://localhost:<port>` either way — a caller that resolves IPv4-only, or
47
+ * dials the literal, gets a connection refused against a server that is up.
48
+ *
49
+ * The two halves of `aai dev` also disagreed: `createServer` binds
50
+ * {@link DEFAULT_LISTEN_HOST} explicitly, Vite took its own default, and only a
51
+ * set `AAI_DEV_HOST` brought them back together. Taking the same constant is
52
+ * what makes them agree by construction rather than by two matching literals.
53
+ *
54
+ * IPv4 loopback is the SAFE side of that choice rather than a coin flip:
55
+ * browsers, curl and undici all try every address `localhost` resolves to, so a
56
+ * `127.0.0.1` bind stays reachable as `localhost`, while a `::1` bind is not
57
+ * reachable at all from a client holding the IPv4 literal.
58
+ *
38
59
  * ## The target is an IP LITERAL, and that is a fix rather than a style choice
39
60
  *
40
61
  * `127.0.0.1`, never `localhost`. Vite opens a FRESH upstream connection for
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { c as isEexist, l as readJson, p as writeJson } from "./_utils-B8QmtFhK.mjs";
3
3
  import { r as isDevMode, t as getMonorepoRoot } from "./_agent-BTwZJu4P.mjs";
4
- import { REPO_URL, downloadAndMergeTemplate } from "./_templates-4WcKOjS5.mjs";
4
+ import { REPO_URL, downloadAndMergeTemplate } from "./_templates-0Zr2z6yA.mjs";
5
5
  import path from "node:path";
6
6
  import fs from "node:fs/promises";
7
7
  //#region _init.ts
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { n as getServerInfo } from "./_agent-BTwZJu4P.mjs";
3
- import { l as HINT_NOT_DEPLOYED, s as studioProjectApiUrl, u as apiRequest } from "./_studio-CKrsixd0.mjs";
3
+ import { l as HINT_NOT_DEPLOYED, s as studioProjectApiUrl, u as apiRequest } from "./_studio-B1waXMuR.mjs";
4
4
  //#region _slug-api.ts
5
5
  /**
6
6
  * Authenticated request against a deployed agent's slug-scoped resource
@@ -5,6 +5,7 @@ import { existsSync } from "node:fs";
5
5
  import path from "node:path";
6
6
  import { isRecord } from "@alexkroman1/aai/utils";
7
7
  import fs from "node:fs/promises";
8
+ import { IGNORED_WORKSPACE_DIRS, isLocalOnlyFile } from "@alexkroman1/aai/workspace-files";
8
9
  const REPO_URL = `https://github.com/alexkroman/agent`;
9
10
  /**
10
11
  * Templates as shipped inside the published tarball, copied into `dist/` by
@@ -144,11 +145,44 @@ async function layerScaffold(targetDir) {
144
145
  await fs.cp(dir, targetDir, {
145
146
  recursive: true,
146
147
  force: false,
147
- errorOnExist: false
148
+ errorOnExist: false,
149
+ filter: templateCopyFilter
148
150
  });
149
151
  await layerScaffoldManifest(dir, targetDir);
150
152
  }
151
153
  /**
154
+ * `fs.cp` filter for every copy OUT of a template or the scaffold — the runtime
155
+ * ones here and the build-time one in `bundle-templates.mjs`.
156
+ *
157
+ * A template directory is also a runnable project, so a developer who runs
158
+ * `aai dev`, `aai build` or `aai publish` inside one leaves build output and
159
+ * machine state in it: `.aai/` (which holds `project.json` — a SLUG and a
160
+ * `serverUrl` — plus a built client), `.workflow-data/`, `node_modules/`, a
161
+ * `.env`. None of it is git-tracked, and an unfiltered `fs.cp` copied all of it
162
+ * anyway, to both destinations:
163
+ *
164
+ * - into every scaffolded project, so `aai init foo --template bar` produced a
165
+ * directory already LINKED to `bar`'s last local deploy. `aai init` publishes
166
+ * by default, so the first publish either targeted a slug the user never
167
+ * chose or — for the `http://localhost:8080` a dev checkout leaves behind —
168
+ * failed outright with "Refusing to send your API key to …", the project
169
+ * staying mis-linked for every later `push`/`publish`/`secret`.
170
+ * - into `packages/aai-cli/dist/templates`, i.e. into the PUBLISHED tarball
171
+ * (`files: ["bin.mjs", "dist"]`). Measured on a real build: 26 stray
172
+ * `.aai/project.json` files and 9.4 MB of one developer's `.aai/client`
173
+ * bundles out of a 12 MB `templates/`.
174
+ *
175
+ * The vocabulary is the SDK's, not a fourth list: {@link IGNORED_WORKSPACE_DIRS}
176
+ * is already "never walk this", and {@link isLocalOnlyFile} already means "this
177
+ * exists only on a developer's machine" — including a `.env` that must not ship
178
+ * to npm, and deliberately EXCLUDING `.env.example`, which the scaffold ships as
179
+ * source and a scaffolded project cannot do without.
180
+ */
181
+ function templateCopyFilter(src) {
182
+ const name = path.basename(src);
183
+ return !(IGNORED_WORKSPACE_DIRS.has(name) || isLocalOnlyFile(name));
184
+ }
185
+ /**
152
186
  * Copy a template into targetDir, merging scaffold files underneath.
153
187
  */
154
188
  async function downloadAndMergeTemplate(template, targetDir) {
@@ -158,7 +192,8 @@ async function downloadAndMergeTemplate(template, targetDir) {
158
192
  if (!names.includes(template)) throw new Error(`Unknown template "${template}". Available templates: ${names.join(", ")}`);
159
193
  await fs.cp(path.join(templatesDir, template), targetDir, {
160
194
  recursive: true,
161
- force: true
195
+ force: true,
196
+ filter: templateCopyFilter
162
197
  });
163
198
  await layerScaffold(targetDir);
164
199
  }
@@ -56,6 +56,35 @@ export declare function scaffoldDir(): string;
56
56
  * {@link layerScaffoldManifest}.
57
57
  */
58
58
  export declare function layerScaffold(targetDir: string): Promise<void>;
59
+ /**
60
+ * `fs.cp` filter for every copy OUT of a template or the scaffold — the runtime
61
+ * ones here and the build-time one in `bundle-templates.mjs`.
62
+ *
63
+ * A template directory is also a runnable project, so a developer who runs
64
+ * `aai dev`, `aai build` or `aai publish` inside one leaves build output and
65
+ * machine state in it: `.aai/` (which holds `project.json` — a SLUG and a
66
+ * `serverUrl` — plus a built client), `.workflow-data/`, `node_modules/`, a
67
+ * `.env`. None of it is git-tracked, and an unfiltered `fs.cp` copied all of it
68
+ * anyway, to both destinations:
69
+ *
70
+ * - into every scaffolded project, so `aai init foo --template bar` produced a
71
+ * directory already LINKED to `bar`'s last local deploy. `aai init` publishes
72
+ * by default, so the first publish either targeted a slug the user never
73
+ * chose or — for the `http://localhost:8080` a dev checkout leaves behind —
74
+ * failed outright with "Refusing to send your API key to …", the project
75
+ * staying mis-linked for every later `push`/`publish`/`secret`.
76
+ * - into `packages/aai-cli/dist/templates`, i.e. into the PUBLISHED tarball
77
+ * (`files: ["bin.mjs", "dist"]`). Measured on a real build: 26 stray
78
+ * `.aai/project.json` files and 9.4 MB of one developer's `.aai/client`
79
+ * bundles out of a 12 MB `templates/`.
80
+ *
81
+ * The vocabulary is the SDK's, not a fourth list: {@link IGNORED_WORKSPACE_DIRS}
82
+ * is already "never walk this", and {@link isLocalOnlyFile} already means "this
83
+ * exists only on a developer's machine" — including a `.env` that must not ship
84
+ * to npm, and deliberately EXCLUDING `.env.example`, which the scaffold ships as
85
+ * source and a scaffolded project cannot do without.
86
+ */
87
+ export declare function templateCopyFilter(src: string): boolean;
59
88
  /**
60
89
  * Copy a template into targetDir, merging scaffold files underneath.
61
90
  */
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { a as ok, t as CliError } from "./_output-CKkmWs7i.mjs";
3
3
  import { n as log, r as notify } from "./_ui-u7T4YooX.mjs";
4
- import { r as evalWorkerBundle, t as buildAgentBundle } from "./_bundler-CDuYl5Gb.mjs";
4
+ import { r as evalWorkerBundle, t as buildAgentBundle } from "./_bundler-B31MqYaD.mjs";
5
5
  import { assertTypechecks } from "./_typecheck-gate-BXvUNnfs.mjs";
6
6
  import { classifyVitestError, runVitest } from "./test-CvwgeVSQ.mjs";
7
7
  import path from "node:path";
@@ -48,7 +48,6 @@ async function executeBuild(opts) {
48
48
  }
49
49
  if (!opts.skipTypecheck) await assertTypechecks(cwd);
50
50
  const bundle = await buildAgentBundle(cwd, { minify: true });
51
- for (const warning of bundle.workflows?.warnings ?? []) notify("warn", warning);
52
51
  const agentDef = await evalWorkerBundle(bundle.worker);
53
52
  for (const warning of agentConfigWarnings(agentDef)) notify("warn", warning);
54
53
  const worker = path.join(cwd, WORKER_ARTIFACT_REL);