@alexkroman1/aai-cli 13.3.0 → 14.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 (134) hide show
  1. package/dist/{_agent-Clbq3a4h.mjs → _agent-BzUeqOdj.mjs} +32 -9
  2. package/dist/_agent.d.ts +23 -0
  3. package/dist/{_bundler-DEkjO1d8.mjs → _bundler-DolUCMxu.mjs} +8 -7
  4. package/dist/_cli-common.d.ts +45 -6
  5. package/dist/{_config-ZY2euhdh.mjs → _config-DYzC6WMD.mjs} +2 -2
  6. package/dist/_dev-env.d.ts +1 -1
  7. package/dist/{_dev-server-C13xuGB1.mjs → _dev-server-CSMqF8PN.mjs} +20 -22
  8. package/dist/_dev-vite-config.d.ts +3 -3
  9. package/dist/{_init-Ci4QGjp8.mjs → _init-CQ8idAwo.mjs} +9 -23
  10. package/dist/{_output-DbJt9gsl.mjs → _output-DBP9Op_d.mjs} +22 -13
  11. package/dist/_output.d.ts +10 -9
  12. package/dist/{_preflight-B2laWPdU.mjs → _preflight-CcAR577V.mjs} +2 -2
  13. package/dist/_resource-commands.d.ts +9 -9
  14. package/dist/{_server-common-BV4_CrP3.mjs → _server-common-vILJp3it.mjs} +1 -1
  15. package/dist/{_slug-api-DlKpj91X.mjs → _slug-api-CGvc2YSQ.mjs} +18 -9
  16. package/dist/_slug-api.d.ts +19 -0
  17. package/dist/{_studio-DTi3EdXQ.mjs → _studio-DnR_BqFp.mjs} +7 -3
  18. package/dist/_studio-commands.d.ts +13 -13
  19. package/dist/{_templates-V4l5sZJy.mjs → _templates-CK4oKoeX.mjs} +3 -3
  20. package/dist/{_typecheck-gate-sV4N5F93.mjs → _typecheck-gate-U3jcil8L.mjs} +3 -2
  21. package/dist/_typecheck-gate.d.ts +3 -1
  22. package/dist/{_utils-Dhd058D9.mjs → _utils-D5JGcjiW.mjs} +43 -1
  23. package/dist/_utils.d.ts +35 -0
  24. package/dist/{_workflow-determinism-C9l8uC2R.mjs → _workflow-determinism-CvpqMpdh.mjs} +18 -26
  25. package/dist/_workflow-determinism.d.ts +3 -3
  26. package/dist/{build-CWyFGe-W.mjs → build-Mxk8gWvX.mjs} +46 -7
  27. package/dist/build.d.ts +5 -0
  28. package/dist/cli.d.ts +1 -2
  29. package/dist/cli.mjs +117 -135
  30. package/dist/{client-bundler-CT-_26c5.mjs → client-bundler-BJgREAh6.mjs} +1 -1
  31. package/dist/client-bundler.mjs +1 -1
  32. package/dist/{delete-RqlQZ6Y_.mjs → delete-BdFnbnVR.mjs} +4 -4
  33. package/dist/{deploy-B7f3CK7v.mjs → deploy-uAJ4NukN.mjs} +11 -11
  34. package/dist/{dev-DamXieW_.mjs → dev-DApPSaE_.mjs} +3 -3
  35. package/dist/{eval-h3DTVfRw.mjs → eval-BK47A_K5.mjs} +3 -3
  36. package/dist/{init-D-vFrV8h.mjs → init-DukDxECd.mjs} +11 -19
  37. package/dist/{login-CDiemNmC.mjs → login-DaLrPOFg.mjs} +4 -6
  38. package/dist/{logs-DGwPNCzB.mjs → logs-7BRwq0cG.mjs} +21 -14
  39. package/dist/project-config.mjs +1 -1
  40. package/dist/scaffold/CLAUDE.md +84 -56
  41. package/dist/scaffold/package.json +4 -4
  42. package/dist/scaffold/server.mjs +129 -25
  43. package/dist/{secret-V1w7y9-j.mjs → secret-Cjp912yF.mjs} +5 -5
  44. package/dist/secret.d.ts +8 -6
  45. package/dist/{studio-BpMCfUNx.mjs → studio-CpHlNHUZ.mjs} +13 -15
  46. package/dist/templates/call-audit/agent.test.ts +11 -6
  47. package/dist/templates/call-audit/agent.ts +2 -2
  48. package/dist/templates/call-audit/client.tsx +27 -42
  49. package/dist/templates/call-audit/workflows/audit.ts +7 -5
  50. package/dist/templates/call-audit/workflows/ingest.ts +7 -7
  51. package/dist/templates/call-audit/workflows/media.ts +12 -37
  52. package/dist/templates/call-audit/workflows/summarize.ts +8 -8
  53. package/dist/templates/call-audit/workflows/sync-api.ts +3 -3
  54. package/dist/templates/dispatch-center/agent.test.ts +7 -7
  55. package/dist/templates/dispatch-center/client.tsx +53 -61
  56. package/dist/templates/dispatch-center/shared.ts +0 -8
  57. package/dist/templates/dispatch-center/tools/ops_dashboard.ts +4 -1
  58. package/dist/templates/embedded-assets/agent.eval.test.ts +4 -2
  59. package/dist/templates/health-assistant/agent.eval.test.ts +4 -2
  60. package/dist/templates/infocom-adventure/client.tsx +40 -36
  61. package/dist/templates/link-digest/agent.test.ts +3 -3
  62. package/dist/templates/link-digest/agent.ts +2 -2
  63. package/dist/templates/link-digest/client.tsx +27 -17
  64. package/dist/templates/link-digest/workflows/digest.ts +13 -16
  65. package/dist/templates/night-owl/agent.eval.test.ts +4 -2
  66. package/dist/templates/night-owl/client.tsx +8 -2
  67. package/dist/templates/pipeline-simple/agent.test.ts +2 -2
  68. package/dist/templates/pizza-ordering/client.tsx +5 -4
  69. package/dist/templates/pizza-ordering/shared.ts +3 -8
  70. package/dist/templates/pizza-ordering/tools/add_pizza.ts +3 -2
  71. package/dist/templates/pizza-ordering/tools/place_order.ts +3 -2
  72. package/dist/templates/pizza-ordering/tools/remove_pizza.ts +3 -2
  73. package/dist/templates/pizza-ordering/tools/update_pizza.ts +3 -2
  74. package/dist/templates/pizza-ordering/tools/view_order.ts +3 -2
  75. package/dist/templates/plan-and-execute/agent.test.ts +10 -5
  76. package/dist/templates/plan-and-execute/client.tsx +2 -2
  77. package/dist/templates/podcast-digest/agent.test.ts +20 -11
  78. package/dist/templates/podcast-digest/client.tsx +16 -22
  79. package/dist/templates/podcast-digest/workflows/digest.ts +21 -19
  80. package/dist/templates/podcast-digest/workflows/feeds.ts +61 -33
  81. package/dist/templates/podcast-digest/workflows/slack.ts +7 -7
  82. package/dist/templates/recap-workflow/agent.eval.test.ts +21 -13
  83. package/dist/templates/recap-workflow/agent.test.ts +27 -23
  84. package/dist/templates/recap-workflow/workflows/recap.ts +24 -28
  85. package/dist/templates/redline/agent.eval.test.ts +5 -8
  86. package/dist/templates/redline/agent.test.ts +1 -1
  87. package/dist/templates/redline/client.tsx +36 -34
  88. package/dist/templates/redline/workflows/redline.ts +17 -17
  89. package/dist/templates/research-workflow/agent.eval.test.ts +5 -5
  90. package/dist/templates/research-workflow/agent.test.ts +10 -7
  91. package/dist/templates/research-workflow/workflows/research.ts +28 -30
  92. package/dist/templates/retail/agent.test.ts +17 -15
  93. package/dist/templates/retail/cancel.ts +7 -6
  94. package/dist/templates/retail/client.tsx +39 -58
  95. package/dist/templates/retail/payment.ts +4 -3
  96. package/dist/templates/retail/pending.test.ts +19 -19
  97. package/dist/templates/retail/store.test.ts +5 -5
  98. package/dist/templates/retail/swap.ts +8 -11
  99. package/dist/templates/solo-rpg/agent.eval.test.ts +2 -16
  100. package/dist/templates/solo-rpg/agent.test.ts +12 -12
  101. package/dist/templates/solo-rpg/client.tsx +3 -3
  102. package/dist/templates/spoken-summary/agent.test.ts +9 -9
  103. package/dist/templates/spoken-summary/agent.ts +3 -3
  104. package/dist/templates/spoken-summary/client.tsx +23 -30
  105. package/dist/templates/spoken-summary/workflows/summarize.ts +11 -11
  106. package/dist/templates/spoken-summary/workflows/transcribe.ts +14 -14
  107. package/dist/templates/support-line/client.tsx +4 -3
  108. package/dist/templates/support-line/prompts.ts +0 -2
  109. package/dist/templates/transcription-workflow/agent.test.ts +20 -12
  110. package/dist/templates/transcription-workflow/agent.ts +2 -2
  111. package/dist/templates/transcription-workflow/client.tsx +15 -14
  112. package/dist/templates/transcription-workflow/recover.ts +1 -1
  113. package/dist/templates/transcription-workflow/run-panel.tsx +34 -14
  114. package/dist/templates/transcription-workflow/total-latency.tsx +46 -20
  115. package/dist/templates/transcription-workflow/workflows/batch.ts +15 -15
  116. package/dist/templates/transcription-workflow/workflows/downsample.ts +1 -1
  117. package/dist/templates/transcription-workflow/workflows/normalize.ts +15 -19
  118. package/dist/templates/transcription-workflow/workflows/stream.ts +12 -12
  119. package/dist/templates/transcription-workflow/workflows/sync-api.ts +3 -3
  120. package/dist/templates/transcription-workflow/workflows/transcribe.ts +23 -34
  121. package/dist/templates/travel-concierge/agent.eval.test.ts +2 -2
  122. package/dist/templates/travel-concierge/agent.test.ts +15 -7
  123. package/dist/templates/travel-concierge/client.tsx +2 -2
  124. package/dist/templates/travel-concierge/shared.ts +10 -14
  125. package/dist/templates/travel-concierge/tools/lookup_booking.ts +4 -3
  126. package/dist/templates/travel-concierge/tools/search_car_rentals.ts +3 -2
  127. package/dist/templates/travel-concierge/tools/search_excursions.ts +3 -2
  128. package/dist/templates/travel-concierge/tools/search_flights.ts +3 -2
  129. package/dist/templates/travel-concierge/tools/search_hotels.ts +3 -2
  130. package/dist/{test-guenJUxi.mjs → test-D032V2cA.mjs} +4 -17
  131. package/dist/typecheck.mjs +1 -1
  132. package/dist/worker-bundler.mjs +1 -1
  133. package/dist/{workflow-BZyKLkhV.mjs → workflow-CVioyO3V.mjs} +6 -5
  134. package/package.json +4 -4
@@ -122,6 +122,71 @@ function resolveClientDir() {
122
122
  return defaultClientDir();
123
123
  }
124
124
 
125
+ /**
126
+ * The error classes that mean a DEFECT in this code rather than a mistake in
127
+ * the configuration.
128
+ *
129
+ * Everything below turns a boot failure into two lines and a non-zero exit,
130
+ * which is right for "ASSEMBLYAI_API_KEY is not set" and wrong for a
131
+ * `TypeError`, where the traceback is the only thing that can locate the bug.
132
+ * Those are re-thrown untouched.
133
+ */
134
+ const BUG_ERRORS = [TypeError, ReferenceError, RangeError, SyntaxError];
135
+
136
+ /**
137
+ * Every distinct message on an error and its `cause` chain, outermost first.
138
+ *
139
+ * A driver failure states the useful half one hop down — `connect to database
140
+ * failed: getaddrinfo ENOTFOUND db` — so printing only the top message is how a
141
+ * tidy envelope ends up less informative than the stack it replaced.
142
+ *
143
+ * @param {unknown} err
144
+ * @returns {string}
145
+ */
146
+ function errorText(err) {
147
+ /** @type {string[]} */
148
+ const messages = [];
149
+ /** @type {unknown} */
150
+ let cursor = err;
151
+ while (cursor instanceof Error) {
152
+ if (cursor.message !== "" && !messages.includes(cursor.message)) messages.push(cursor.message);
153
+ cursor = cursor.cause;
154
+ }
155
+ return messages.length > 0 ? messages.join(": ") : String(err);
156
+ }
157
+
158
+ /**
159
+ * Run one step of BOOT, and answer a failure the way this file already answers
160
+ * a missing build artifact: what is wrong, then what to do, then exit 1.
161
+ *
162
+ * Without this, a missing provider key — the commonest way a first `npm start`
163
+ * fails — killed the process with a ten-frame traceback pointing into
164
+ * `node_modules/@alexkroman1/aai-runtime/dist/host-env-*.js`. The MESSAGE was
165
+ * already good ("AssemblyAI LLM: missing API key. Set ASSEMBLYAI_API_KEY in the
166
+ * agent env."); what it arrived wrapped in was a crash report about somebody
167
+ * else's bundle, in a container that then restarted and did it again.
168
+ *
169
+ * Exiting is deliberate rather than binding anyway and serving an unhealthy
170
+ * `/health`: a process that stays up tells an orchestrator it started, and a
171
+ * misconfigured deployment that reports itself healthy is worse than one that
172
+ * refuses to run. The non-zero exit is what a supervisor, a `docker run`, and
173
+ * CI all already read.
174
+ *
175
+ * @template T
176
+ * @param {string} fix - What the operator should change, in one sentence.
177
+ * @param {() => T | Promise<T>} work
178
+ * @returns {Promise<T>}
179
+ */
180
+ async function bootOrExit(fix, work) {
181
+ try {
182
+ return await work();
183
+ } catch (err) {
184
+ if (BUG_ERRORS.some((kind) => err instanceof kind)) throw err;
185
+ console.error(`Cannot start the agent: ${errorText(err)}\n${fix}`);
186
+ process.exit(1);
187
+ }
188
+ }
189
+
125
190
  const env = await resolveAgentEnv();
126
191
 
127
192
  /**
@@ -152,38 +217,77 @@ const publicUrl = process.env.PUBLIC_URL?.trim();
152
217
  * Best-effort: if a real migration already created them and this role may not
153
218
  * CREATE, it warns and the server starts anyway.
154
219
  */
155
- if (env.DATABASE_URL) {
156
- await ensureSessionStateSchema({ url: env.DATABASE_URL, logger: console });
157
- // And the durable-run journal's, which is a separate set of tables owned by
158
- // the same deployment. Without it a project with a `DATABASE_URL` boots
159
- // claiming durable runs and fails on the first one.
160
- await ensureWorkflowJournalSchema({ url: env.DATABASE_URL, logger: console });
220
+ // Read into a const: `env.DATABASE_URL` is a record lookup, so its narrowing
221
+ // does not survive into the callback below.
222
+ const databaseUrl = env.DATABASE_URL;
223
+ if (databaseUrl) {
224
+ await bootOrExit(
225
+ "Check DATABASE_URL: this server has to reach that database at boot to create the tables it owns.",
226
+ async () => {
227
+ await ensureSessionStateSchema({ url: databaseUrl, logger: console });
228
+ // And the durable-run journal's, which is a separate set of tables owned
229
+ // by the same deployment. Without it a project with a `DATABASE_URL`
230
+ // boots claiming durable runs and fails on the first one.
231
+ await ensureWorkflowJournalSchema({ url: databaseUrl, logger: console });
232
+ },
233
+ );
234
+ } else {
235
+ /**
236
+ * Say that this process is the only place the state lives, because the next
237
+ * thing an operator does with a container is run two of them.
238
+ *
239
+ * Session state (slots, the event log) is keyed by session id and held in
240
+ * memory here — the boot line below reports it as `sessionState: { backend:
241
+ * 'memory', durable: false }`, which is true and easy to read as being about
242
+ * restarts alone. It is also about REPLICAS: the browser reconnects with
243
+ * `?sessionId=<id>`, so a reconnect that lands on a different process resumes
244
+ * a session that process has never heard of and the agent's context is gone
245
+ * mid-call. One replica has no such problem, which is exactly why nothing
246
+ * catches this until the deployment grows a second one.
247
+ */
248
+ console.warn(
249
+ "No DATABASE_URL: session state and durable runs live in THIS process's memory.\n" +
250
+ "One replica is fine. Behind a load balancer, enable sticky sessions so a reconnect " +
251
+ "(the client re-dials with ?sessionId=) reaches the same process — or set DATABASE_URL " +
252
+ "and let every replica share the state.",
253
+ );
161
254
  }
162
255
 
163
- const server = createAgentServer({
164
- agent,
165
- env,
166
- // Provider credentials may ALSO arrive straight from the environment without
167
- // being declared, and without becoming ctx.env the ordinary way to hand
168
- // ASSEMBLYAI_API_KEY to a container. Anything in `env` still wins.
169
- providerEnv: withHostCredentialFallback(env),
170
- clientDir: resolveClientDir(),
171
- ...(publicUrl ? { publicUrl } : {}),
172
- // Durable workflows need nothing passed here. A `DATABASE_URL` in `env` puts
173
- // the runs in Postgres and they survive a restart; without one they live in a
174
- // per-process directory and do not, which is the same trade `aai dev` makes.
175
- //
176
- // Two options used to sit here — the compiled workflow surface, carried on the
177
- // bundle as `__aaiWorkflowCode`/`__aaiStepCode` because a `"use workflow"` body
178
- // had to go through a compiler at BUILD time. The engine reads the agent's own
179
- // `workflows` declaration instead, so there is no artifact to hand over.
180
- });
256
+ const server = await bootOrExit(
257
+ // The commonest first-run failure, and the one whose stack this replaces: a
258
+ // provider credential that is not there. `.env` is what `aai dev` reads too,
259
+ // so the fix is the same one in both places.
260
+ "Set the missing value in .env, or pass it as a real environment variable (`docker run -e NAME=value`), then start again.",
261
+ () =>
262
+ createAgentServer({
263
+ agent,
264
+ env,
265
+ // Provider credentials may ALSO arrive straight from the environment without
266
+ // being declared, and without becoming ctx.env the ordinary way to hand
267
+ // ASSEMBLYAI_API_KEY to a container. Anything in `env` still wins.
268
+ providerEnv: withHostCredentialFallback(env),
269
+ clientDir: resolveClientDir(),
270
+ ...(publicUrl ? { publicUrl } : {}),
271
+ // Durable workflows need nothing passed here. A `DATABASE_URL` in `env` puts
272
+ // the runs in Postgres and they survive a restart; without one they live in a
273
+ // per-process directory and do not, which is the same trade `aai dev` makes.
274
+ //
275
+ // Two options used to sit here — the compiled workflow surface, carried on the
276
+ // bundle as `__aaiWorkflowCode`/`__aaiStepCode` because a `"use workflow"` body
277
+ // had to go through a compiler at BUILD time. The engine reads the agent's own
278
+ // `workflows` declaration instead, so there is no artifact to hand over.
279
+ }),
280
+ );
181
281
 
182
282
  // Loopback by default: this server has no request authentication of its own,
183
283
  // so exposing it is a deliberate act. Set HOST=0.0.0.0 to bind every interface
184
284
  // behind your own proxy or auth. An empty HOST means unset, not "everything".
185
285
  const host = process.env.HOST?.trim() || undefined;
186
- await server.listen(Number(process.env.PORT ?? 3000), host);
286
+ const port = Number(process.env.PORT ?? 3000);
287
+ await bootOrExit(
288
+ `Nothing is listening yet — port ${port} is in use, or this process may not bind it. Set PORT to a free one.`,
289
+ () => server.listen(port, host),
290
+ );
187
291
  console.log(`${agent.name} listening on http://${host ?? "127.0.0.1"}:${server.port}`);
188
292
 
189
293
  for (const signal of ["SIGINT", "SIGTERM"]) {
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import { a as ok, n as fail, t as CliError } from "./_output-DbJt9gsl.mjs";
2
+ import { a as ok, n as fail, t as CliError } from "./_output-DBP9Op_d.mjs";
3
3
  import { n as log, s as unwrapCancel } from "./_ui-DCt4qZrk.mjs";
4
- import { f as checkedResponse, p as isStringArray } from "./_studio-DTi3EdXQ.mjs";
5
- import { t as secretRequest } from "./_slug-api-DlKpj91X.mjs";
6
- import { isRecord } from "@alexkroman1/aai/utils";
4
+ import { f as checkedResponse, p as isStringArray } from "./_studio-DnR_BqFp.mjs";
5
+ import { t as secretRequest } from "./_slug-api-CGvc2YSQ.mjs";
6
+ import { isRecord, plural } from "@alexkroman1/aai/utils";
7
7
  import * as p from "@clack/prompts";
8
8
  import pTimeout from "p-timeout";
9
9
  //#region src/secret.ts
@@ -153,7 +153,7 @@ async function executeSecretList(cwd, server) {
153
153
  const { vars } = checkedResponse(data, (value) => isRecord(value) && isStringArray(value.vars), `the secret list for ${target}`);
154
154
  if (vars.length === 0) log.info("No secrets set. Use `aai secret put <name>` to add one.");
155
155
  else {
156
- log.message(`${vars.length} secret${vars.length === 1 ? "" : "s"}:`);
156
+ log.message(`${vars.length} ${plural(vars.length, "secret")}:`);
157
157
  for (const v of vars) log.message(` ${v}`);
158
158
  }
159
159
  return ok({ secrets: vars });
package/dist/secret.d.ts CHANGED
@@ -38,10 +38,12 @@ export type SecretValueIo = {
38
38
  * sitting at a terminal has no reason to send.
39
39
  */
40
40
  export declare function resolveSecretValue(name: string, mode: OutputMode, io?: SecretValueIo): Promise<string | undefined>;
41
- type SecretPutData = {
42
- name: string;
43
- };
44
- type SecretDeleteData = {
41
+ /**
42
+ * Both the put and the delete result — one shape, so one name. They were two
43
+ * distinct aliases for `{ name: string }`, which implied a difference that does
44
+ * not exist and left "which alias meant the secret result" to be re-decided.
45
+ */
46
+ type SecretNameData = {
45
47
  name: string;
46
48
  };
47
49
  type SecretListData = {
@@ -52,7 +54,7 @@ type SecretListData = {
52
54
  * path). If not, prompt for it — masked — which is what
53
55
  * {@link resolveSecretValue} returning `undefined` asks for.
54
56
  */
55
- export declare function executeSecretPut(cwd: string, name: string, value: string | undefined, server: string | undefined): Promise<CommandResult<SecretPutData>>;
56
- export declare function executeSecretDelete(cwd: string, name: string, server: string | undefined): Promise<CommandResult<SecretDeleteData>>;
57
+ export declare function executeSecretPut(cwd: string, name: string, value: string | undefined, server: string | undefined): Promise<CommandResult<SecretNameData>>;
58
+ export declare function executeSecretDelete(cwd: string, name: string, server: string | undefined): Promise<CommandResult<SecretNameData>>;
57
59
  export declare function executeSecretList(cwd: string, server: string | undefined): Promise<CommandResult<SecretListData>>;
58
60
  export {};
@@ -1,14 +1,15 @@
1
1
  #!/usr/bin/env node
2
- import { a as ok, t as CliError } from "./_output-DbJt9gsl.mjs";
2
+ import { a as ok, t as CliError } from "./_output-DBP9Op_d.mjs";
3
3
  import { n as log, t as fmtUrl } from "./_ui-DCt4qZrk.mjs";
4
- import { s as updateProjectConfig } from "./_config-ZY2euhdh.mjs";
5
- import { t as resolveServerEnv } from "./_server-common-BV4_CrP3.mjs";
6
- import { a as resolveDeployTarget } from "./_agent-Clbq3a4h.mjs";
7
- import { layerScaffold } from "./_templates-V4l5sZJy.mjs";
8
- import { a as publishStudioProject, c as studioProjectUrl, f as checkedResponse, i as projectNameFromDir, n as fetchStudioProject, o as pushStudioSource, r as listStudioProjects, s as studioProjectApiUrl, t as collectSourceFiles, u as apiRequest } from "./_studio-DTi3EdXQ.mjs";
4
+ import { l as formatCappedList } from "./_utils-D5JGcjiW.mjs";
5
+ import { s as updateProjectConfig } from "./_config-DYzC6WMD.mjs";
6
+ import { t as resolveServerEnv } from "./_server-common-vILJp3it.mjs";
7
+ import { o as resolveDeployTarget } from "./_agent-BzUeqOdj.mjs";
8
+ import { layerScaffold } from "./_templates-CK4oKoeX.mjs";
9
+ import { a as publishStudioProject, c as studioProjectUrl, f as checkedResponse, i as projectNameFromDir, n as fetchStudioProject, o as pushStudioSource, r as listStudioProjects, s as studioProjectApiUrl, t as collectSourceFiles, u as apiRequest } from "./_studio-DnR_BqFp.mjs";
9
10
  import { existsSync } from "node:fs";
10
11
  import path from "node:path";
11
- import { isRecord, omitUndefined } from "@alexkroman1/aai/utils";
12
+ import { isRecord, omitUndefined, plural } from "@alexkroman1/aai/utils";
12
13
  import { mkdir, readdir, writeFile } from "node:fs/promises";
13
14
  import { isPathInside } from "@alexkroman1/aai-runtime/internal";
14
15
  //#region src/studio.ts
@@ -62,7 +63,7 @@ async function notFoundHint(serverUrl, apiKey) {
62
63
  const projects = await listStudioProjects(serverUrl, apiKey).catch(() => null);
63
64
  if (projects === null) return "Run `aai list` to see your projects.";
64
65
  if (projects.length === 0) return "This login has no studio projects at all. If yours are in the studio, the CLI is linked to a different account — run `aai login` again, approve it in a browser signed in to the account that owns the project, then `aai list`.";
65
- return `Your projects: ${projects.slice(0, 10).join(", ")}${projects.length > 10 ? `, and ${projects.length - 10} more` : ""}.`;
66
+ return `Your projects: ${formatCappedList(projects)}.`;
66
67
  }
67
68
  async function executePull(opts) {
68
69
  const { serverUrl, apiKey } = await resolveDeployTarget(opts.cwd, opts.server);
@@ -159,17 +160,14 @@ async function syncEnvSecrets(cwd, serverUrl, apiKey, project) {
159
160
  method: "PUT",
160
161
  body: env
161
162
  });
162
- log.info(`Synced ${names.length} secret${names.length === 1 ? "" : "s"} from .env`);
163
+ log.info(`Synced ${names.length} ${plural(names.length, "secret")} from .env`);
163
164
  return names;
164
165
  }
165
166
  async function executePublish(opts) {
166
- if (!opts.skipTypecheck) {
167
- const { assertTypechecks } = await import("./_typecheck-gate-sV4N5F93.mjs");
168
- await assertTypechecks(opts.cwd);
169
- }
167
+ const { assertTypechecks } = await import("./_typecheck-gate-U3jcil8L.mjs");
168
+ await assertTypechecks(opts.cwd, { skip: opts.skipTypecheck });
170
169
  const pushed = await pushProject(opts);
171
170
  const { project, serverUrl, apiKey } = pushed;
172
- const hadSlug = pushed.slug !== void 0;
173
171
  if (pushed.slug) await syncEnvSecrets(opts.cwd, serverUrl, apiKey, project);
174
172
  log.step(`Publishing ${project} (builds in the project's sandbox)…`);
175
173
  const result = checkedResponse(await publishStudioProject(serverUrl, apiKey, project, { skipTypecheck: opts.skipTypecheck }), (value) => isRecord(value) && typeof value.slug === "string" && typeof value.output === "string", `the publish route at ${serverUrl}`);
@@ -178,7 +176,7 @@ async function executePublish(opts) {
178
176
  serverUrl,
179
177
  slug: result.slug
180
178
  });
181
- if (!hadSlug) {
179
+ if (!pushed.slug) {
182
180
  if ((await syncEnvSecrets(opts.cwd, serverUrl, apiKey, result.slug)).length > 0) log.info("They apply on the next `aai publish`.");
183
181
  }
184
182
  const agentUrl = `${serverUrl}/${result.slug}`;
@@ -32,7 +32,7 @@
32
32
  import { readdir } from "node:fs/promises";
33
33
  import { tmpdir } from "node:os";
34
34
  import { FatalError, RetryableError } from "@alexkroman1/aai/step-errors";
35
- import { createWorkflowCtx, stubSpeech, WORKFLOW_CTX_NOW } from "@alexkroman1/aai/testing";
35
+ import { createWorkflowContext, stubSpeech, WORKFLOW_CONTEXT_NOW } from "@alexkroman1/aai/testing";
36
36
  import {
37
37
  installStubGateway,
38
38
  installStubReporter,
@@ -516,7 +516,7 @@ describe("planning where to cut", () => {
516
516
  test("the last segment never addresses a byte the file does not have", () => {
517
517
  // The twelve-byte bug this API shape exists to prevent: planning from a
518
518
  // duration rounded to whole milliseconds put `endByte` at 640,512 for a
519
- // 640,500-byte file. `readUpload` clamps a window to the stored size, so
519
+ // 640,500-byte file. `stepReadUpload` clamps a window to the stored size, so
520
520
  // nothing threw — the plan was simply describing audio that does not exist.
521
521
  // Found by running the real argv against a real ffmpeg, which is the only
522
522
  // place a twelve-byte error was ever going to surface.
@@ -867,14 +867,14 @@ describe("the body's step policy", () => {
867
867
  * only reads `ctx.steps` afterwards, so it is built once.
868
868
  */
869
869
  const walkedCtx = () =>
870
- createWorkflowCtx({
870
+ createWorkflowContext({
871
871
  runSteps: false,
872
872
  // The body reads the clock at both ends with `ctx.now()`, which the engine
873
873
  // journals under `now!0` and `now!1`. A PRODUCER here rather than a fixed
874
874
  // number, so the two reaches differ and `elapsedMs` is assertable — based
875
- // on `WORKFLOW_CTX_NOW`, the instant the fake otherwise freezes at, rather
875
+ // on `WORKFLOW_CONTEXT_NOW`, the instant the fake otherwise freezes at, rather
876
876
  // than on a second arbitrary epoch nobody can relate to the first.
877
- now: clockReads(WORKFLOW_CTX_NOW, WORKFLOW_CTX_NOW + 3000),
877
+ now: clockReads(WORKFLOW_CONTEXT_NOW, WORKFLOW_CONTEXT_NOW + 3000),
878
878
  results: {
879
879
  ingestRecording: {
880
880
  audio: "upl_pcm",
@@ -937,7 +937,7 @@ describe("the body's step policy", () => {
937
937
  /**
938
938
  * `auditFlow` on the real replay engine — as far as this tier can take it.
939
939
  *
940
- * The block above drives the body through `createWorkflowCtx` with
940
+ * The block above drives the body through `createWorkflowContext` with
941
941
  * `runSteps: false` and a journaled result per step, which is what makes it
942
942
  * affordable: `ingestRecording` shells out to the ffmpeg toolchain, which this
943
943
  * tier cannot usefully feed — the `ingestRecording` and `narrate` specs above
@@ -970,6 +970,11 @@ describe("the run is DURABLE, as far as ffmpeg allows", () => {
970
970
  });
971
971
  installStubReporter();
972
972
  vi.stubEnv("ASSEMBLYAI_API_KEY", "test-key");
973
+ // Neither binary is this block's subject (see its doc), and a real
974
+ // conversion here would make a developer with ffmpeg installed run a
975
+ // slower, different test from CI, which has none.
976
+ vi.stubEnv("AAI_FFMPEG_PATH", "/nonexistent/ffmpeg");
977
+ vi.stubEnv("AAI_FFPROBE_PATH", "/nonexistent/ffprobe");
973
978
  });
974
979
 
975
980
  test("a FatalError in the first step fails the run on ONE attempt, not six", async () => {
@@ -20,7 +20,7 @@
20
20
  * - `transcription-workflow` owns the FAN-OUT — why the sync transcription
21
21
  * endpoint's 120-second cap forces one, and how a run survives dying on segment
22
22
  * 27 of 60.
23
- * - `spoken-summary` owns the audio ROUND TRIP — `stepSpeak`, `writeUpload`, and
23
+ * - `spoken-summary` owns the audio ROUND TRIP — `stepSpeak`, `stepWriteUpload`, and
24
24
  * why a page needs `api.download` rather than a URL.
25
25
  *
26
26
  * **What this template adds is a decoder**, and everything downstream changes
@@ -153,7 +153,7 @@ export const audit: WorkflowDef<typeof auditInput, CallAudit> = workflow({
153
153
  input: auditInput,
154
154
  // The one line that makes the form take a file: `<WorkflowFields>` renders a
155
155
  // picker for this property, `useWorkflowSubmit` stores the chosen file, and the
156
- // ingest step reads it back with `readUpload`.
156
+ // ingest step reads it back with `stepReadUpload`.
157
157
  uploads: ["recording"],
158
158
  run: auditFlow,
159
159
  });
@@ -76,9 +76,11 @@ import "@alexkroman1/aai-ui/styles.css";
76
76
  // already declares.
77
77
  import { formatBytes, formatDuration } from "@alexkroman1/aai/utils";
78
78
  import {
79
+ BulletList,
79
80
  createWorkflowApi,
81
+ Facts,
80
82
  Form,
81
- page,
83
+ mountPage,
82
84
  SubmitButton,
83
85
  UploadProgressBar,
84
86
  useDownloadUrl,
@@ -86,7 +88,6 @@ import {
86
88
  WorkflowFields,
87
89
  WorkflowProgress,
88
90
  } from "@alexkroman1/aai-ui";
89
- import { useState } from "react";
90
91
  import type { audit } from "./agent.ts";
91
92
 
92
93
  /**
@@ -130,33 +131,13 @@ function Stat({ label, value }: { label: string; value: string }) {
130
131
  );
131
132
  }
132
133
 
133
- /** A list that renders nothing rather than an empty box — see `risks` in the schema. */
134
- function Findings({ title, items }: { title: string; items: string[] }) {
135
- if (items.length === 0) return null;
136
- return (
137
- <section className="flex flex-col gap-1">
138
- <h3 className="text-sm font-medium opacity-70">{title}</h3>
139
- <ul className="flex list-disc flex-col gap-1 pl-5">
140
- {items.map((item) => (
141
- <li key={item}>{item}</li>
142
- ))}
143
- </ul>
144
- </section>
145
- );
146
- }
147
-
148
134
  export function App() {
149
- // Did THIS load start the run? A reload cannot have, and that is the only way
150
- // the page can tell "auditing what you just uploaded" from "picking up where
151
- // you left off" — the hook reports the run, not who asked for it.
152
- const [startedHere, setStartedHere] = useState(false);
153
135
  // The generic is what makes `run.status === "completed"` narrow to a TYPED
154
136
  // `run.output` instead of `unknown`. The reload — both halves of it — is the
155
137
  // hook's own doing; see the module doc for why the key it mints is the one
156
138
  // this desk wants.
157
- const { submitForm, run, pending, upload, pauseUpload, resumeUpload, error } = useWorkflowSubmit<
158
- typeof audit
159
- >(WORKFLOW, { api });
139
+ const { submitForm, run, pending, upload, pauseUpload, resumeUpload, error, startedHere } =
140
+ useWorkflowSubmit<typeof audit>(WORKFLOW, { api });
160
141
  const output = run?.status === "completed" ? run.output : undefined;
161
142
  // `useDownloadUrl` is the SDK's: the byte route takes the agent's bearer, so the
162
143
  // bytes have to be FETCHED and handed to the element as an object URL — and the
@@ -173,14 +154,7 @@ export function App() {
173
154
  </p>
174
155
  </header>
175
156
 
176
- <Form
177
- onSubmit={(values) => {
178
- setStartedHere(true);
179
- return submitForm(values);
180
- }}
181
- error={error}
182
- className="flex flex-col gap-4"
183
- >
157
+ <Form onSubmit={(values) => submitForm(values)} error={error} className="flex flex-col gap-4">
184
158
  {/* Every control, from the workflow's own input schema. See the module doc. */}
185
159
  <WorkflowFields workflow={WORKFLOW} />
186
160
  <SubmitButton pending={pending} pendingLabel="Auditing…">
@@ -200,20 +174,26 @@ export function App() {
200
174
  for `<WorkflowProgress>` to read. */}
201
175
  <UploadProgressBar upload={upload} onPause={pauseUpload} onResume={resumeUpload} />
202
176
 
203
- {/* What the run itself says, from `report()` in the workflow's steps — which
177
+ {/* What the run itself says, from `stepReport()` in the workflow's steps — which
204
178
  for this template is the ffmpeg narration: what the file was, what it
205
179
  measured, how many pauses were found. */}
206
180
  <WorkflowProgress runId={run?.runId} api={api} />
207
181
 
208
- {run?.status === "failed" && <p className="text-red-600">That one failed: {run.error}</p>}
182
+ {/* `role="alert"`, the same contract `<Form>` gives the submit error: this
183
+ is the outcome the reader waited minutes for. */}
184
+ {run?.status === "failed" && (
185
+ <p role="alert" className="text-red-600">
186
+ That one failed: {run.error}
187
+ </p>
188
+ )}
209
189
 
210
190
  {output !== undefined && (
211
191
  <article className="flex flex-col gap-6">
212
192
  <div className="flex flex-col gap-1">
213
193
  <h2 className="text-xl">{output.headline}</h2>
214
- <p className="text-sm opacity-70">
215
- {output.source} · {formatDuration(output.durationMs)} · {output.words} words
216
- </p>
194
+ <Facts
195
+ items={[output.source, formatDuration(output.durationMs), `${output.words} words`]}
196
+ />
217
197
  </div>
218
198
 
219
199
  {/* What the pipeline did, which is this template's subject. Rendered rather
@@ -231,8 +211,11 @@ export function App() {
231
211
  <Stat label="Run time" value={formatDuration(output.elapsedMs)} />
232
212
  </dl>
233
213
 
234
- <Findings title="Risks" items={output.risks} />
235
- <Findings title="Actions" items={output.actions} />
214
+ {/* Either list can come back empty — see `risks` in the schema — and
215
+ `<BulletList>` renders nothing at all when it does, heading
216
+ included, rather than a stray heading over an empty box. */}
217
+ <BulletList title="Risks" items={output.risks} />
218
+ <BulletList title="Actions" items={output.actions} />
236
219
 
237
220
  <section className="flex flex-col gap-2">
238
221
  <h3 className="text-sm font-medium opacity-70">
@@ -241,7 +224,9 @@ export function App() {
241
224
  </h3>
242
225
  {audio.pending && <p className="text-sm opacity-70">Fetching the audio…</p>}
243
226
  {audio.error !== undefined && (
244
- <p className="text-red-600">Could not load the audio: {audio.error}</p>
227
+ <p role="alert" className="text-red-600">
228
+ Could not load the audio: {audio.error}
229
+ </p>
245
230
  )}
246
231
  {audio.url !== undefined && (
247
232
  <>
@@ -251,7 +236,7 @@ export function App() {
251
236
  track would carry. `spoken-summary` serves a one-cue WebVTT
252
237
  data URL instead — worth reading for how, if a real track is
253
238
  what a page needs. */}
254
- <audio controls src={audio.url} className="w-full" />
239
+ <audio aria-label="Audit read aloud" controls src={audio.url} className="w-full" />
255
240
  {/* `download` works on an object URL because the bytes are already in
256
241
  the tab; it is the href that could not carry the agent's bearer,
257
242
  not the attribute. */}
@@ -273,4 +258,4 @@ export function App() {
273
258
  );
274
259
  }
275
260
 
276
- page({ name: "Call Audit", component: App });
261
+ mountPage({ name: "Call Audit", component: App });
@@ -45,8 +45,8 @@
45
45
  * ingest result, once as the plan) and buy nothing.
46
46
  */
47
47
 
48
- import type { WorkflowCtx } from "@alexkroman1/aai";
49
- import { encodeWav, mapConcurrent, readUpload, report } from "@alexkroman1/aai/step";
48
+ import type { WorkflowContext } from "@alexkroman1/aai";
49
+ import { encodeWav, mapConcurrent, stepReadUpload, stepReport } from "@alexkroman1/aai/step";
50
50
  import { countWords, formatDuration } from "@alexkroman1/aai/utils";
51
51
  // ERASED at build time, so the body can name the schema's own output type without
52
52
  // a runtime cycle back through `agent.ts` — the same mechanism `client.tsx` uses
@@ -156,7 +156,7 @@ export type CallAudit = {
156
156
  */
157
157
  export async function auditFlow(
158
158
  input: WorkflowInputOf<typeof audit>,
159
- ctx: WorkflowCtx,
159
+ ctx: WorkflowContext,
160
160
  ): Promise<CallAudit> {
161
161
  // Both at once: neither needs the other, and issued together they are one round
162
162
  // trip instead of two before any audio moves. The ORDER is still a pure function
@@ -259,12 +259,14 @@ export async function transcribeSegment(audioId: string, segment: Segment): Prom
259
259
  // ORDER is not guaranteed here and does not need to be — the calls go out
260
260
  // together, so their lines interleave by completion, and `segment.index` is what
261
261
  // puts the TRANSCRIPT back in order.
262
- await report(`Transcribing ${formatDuration(segment.startMs)}–${formatDuration(segment.endMs)}.`);
262
+ await stepReport(
263
+ `Transcribing ${formatDuration(segment.startMs)}–${formatDuration(segment.endMs)}.`,
264
+ );
263
265
 
264
266
  // `[start, end)`, the same half-open pair `planSegments` produced — the store
265
267
  // owns the conversion to HTTP's inclusive range, so there is no `- 1` here to get
266
268
  // wrong.
267
- const audio = await readUpload(audioId, { start: segment.startByte, end: segment.endByte });
269
+ const audio = await stepReadUpload(audioId, { start: segment.startByte, end: segment.endByte });
268
270
  const text = await transcribeSpan(
269
271
  encodeWav(audio.bytes, ANALYSIS_FORMAT),
270
272
  `segment-${segment.index}.wav`,
@@ -63,7 +63,7 @@
63
63
  import { readFile, stat } from "node:fs/promises";
64
64
  import { join } from "node:path";
65
65
  import { probeMedia, runFfmpeg } from "@alexkroman1/aai/ffmpeg";
66
- import { pcmDurationMs, report, requireCompleteUpload } from "@alexkroman1/aai/step";
66
+ import { pcmDurationMs, stepReport, stepRequireCompleteUpload } from "@alexkroman1/aai/step";
67
67
  import { throwFatalStepError, throwFfmpegStepError } from "@alexkroman1/aai/step-errors";
68
68
  import { readUploadToFile, withTempDir, writeUploadFromFile } from "@alexkroman1/aai/step-files";
69
69
  import { formatBytes, formatDuration, plural } from "@alexkroman1/aai/utils";
@@ -132,10 +132,10 @@ export type Ingested = {
132
132
  // nothing. Removing them is what took the file under the floor: a directive
133
133
  // propping a coverage number up is the least useful statement in the tree.
134
134
  export async function ingestRecording(uploadId: string): Promise<Ingested> {
135
- // `requireCompleteUpload`, not `uploadInfo`: `size` is the readable PREFIX, so
135
+ // `stepRequireCompleteUpload`, not `stepUploadInfo`: `size` is the readable PREFIX, so
136
136
  // an upload still arriving would be copied short and levelled as the whole call.
137
- const stored = await requireCompleteUpload(uploadId);
138
- await report(`Reading ${stored.name || uploadId} (${formatBytes(stored.size)}).`);
137
+ const stored = await stepRequireCompleteUpload(uploadId);
138
+ await stepReport(`Reading ${stored.name || uploadId} (${formatBytes(stored.size)}).`);
139
139
 
140
140
  return await withTempDir(
141
141
  async (dir) => {
@@ -148,7 +148,7 @@ export async function ingestRecording(uploadId: string): Promise<Ingested> {
148
148
  // `STEP_FILE_READ_CONCURRENCY` of them. Passing `size` means "I am judging
149
149
  // completeness myself", which is what a body polling a still-arriving
150
150
  // upload needs and is the opposite of what happened above: this step has
151
- // already called `requireCompleteUpload`, so the file IS whole and the
151
+ // already called `stepRequireCompleteUpload`, so the file IS whole and the
152
152
  // windows may land in any order. Omitting it lets `readUploadToFile`
153
153
  // establish that for itself and fan out. The cost is one metadata round
154
154
  // trip, against the dozens of window reads it overlaps.
@@ -162,7 +162,7 @@ export async function ingestRecording(uploadId: string): Promise<Ingested> {
162
162
  throwFfmpegStepError,
163
163
  );
164
164
  const codec = probed.audio?.codec ?? "unknown";
165
- await report(
165
+ await stepReport(
166
166
  `Levelling ${describeSource(codec, probed.durationSec)} to ${ANALYSIS_FORMAT.sampleRate / 1000} kHz mono.`,
167
167
  );
168
168
 
@@ -206,7 +206,7 @@ export async function ingestRecording(uploadId: string): Promise<Ingested> {
206
206
  type: "application/octet-stream",
207
207
  });
208
208
 
209
- await report(
209
+ await stepReport(
210
210
  `Levelled ${formatDuration(durationMs)} from ${loudness.inputLufs} LUFS, ` +
211
211
  `${Math.round(speechFraction(silences, durationMs / 1000) * 100)}% speech across ` +
212
212
  `${silences.length} ${plural(silences.length, "pause")}.`,