@alexkroman1/aai-cli 6.10.1 → 6.11.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 (29) hide show
  1. package/dist/scaffold/CLAUDE.md +58 -0
  2. package/dist/scaffold/package.json +3 -3
  3. package/dist/scaffold/server.mjs +12 -3
  4. package/dist/scaffold/vite.config.ts +1 -1
  5. package/dist/templates/call-audit/agent.test.ts +965 -0
  6. package/dist/templates/call-audit/agent.ts +158 -0
  7. package/dist/templates/call-audit/client.tsx +235 -0
  8. package/dist/templates/call-audit/workflows/audit.ts +305 -0
  9. package/dist/templates/call-audit/workflows/ingest.ts +259 -0
  10. package/dist/templates/call-audit/workflows/media.ts +647 -0
  11. package/dist/templates/call-audit/workflows/summarize.ts +206 -0
  12. package/dist/templates/call-audit/workflows/sync-api.ts +44 -0
  13. package/dist/templates/call-audit/workflows/temp-media.ts +138 -0
  14. package/dist/templates/recap-workflow/agent.test.ts +11 -3
  15. package/dist/templates/recap-workflow/workflows/recap.ts +19 -8
  16. package/dist/templates/spoken-summary/agent.test.ts +343 -0
  17. package/dist/templates/spoken-summary/agent.ts +142 -0
  18. package/dist/templates/spoken-summary/client.tsx +225 -0
  19. package/dist/templates/spoken-summary/workflows/summarize.ts +242 -0
  20. package/dist/templates/spoken-summary/workflows/transcribe.ts +145 -0
  21. package/dist/templates/transcription-workflow/agent.test.ts +241 -18
  22. package/dist/templates/transcription-workflow/agent.ts +20 -6
  23. package/dist/templates/transcription-workflow/workflows/batch.ts +75 -173
  24. package/dist/templates/transcription-workflow/workflows/normalize.ts +343 -0
  25. package/dist/templates/transcription-workflow/workflows/stream.ts +6 -4
  26. package/dist/templates/transcription-workflow/workflows/sync-api.ts +26 -94
  27. package/dist/templates/transcription-workflow/workflows/transcribe.ts +23 -14
  28. package/dist/templates/transcription-workflow/workflows/wav.ts +31 -0
  29. package/package.json +3 -3
@@ -466,6 +466,64 @@ Three rules come with it:
466
466
  `stepGenerate` already goes through this, so a step that only calls a model gets
467
467
  it for free.
468
468
 
469
+ ### A step can SPEAK, and store the file it made
470
+
471
+ A workflow whose answer is a FILE — a summary read aloud, a rendered image, a
472
+ generated PDF — needs two things a first draft reaches for and does not find.
473
+ Both are on `@alexkroman1/aai/utils`, and `spoken-summary` is the template that
474
+ shows the whole round trip.
475
+
476
+ ```ts no-check
477
+ import { stepSpeak, writeUpload } from "@alexkroman1/aai/utils";
478
+
479
+ export async function narrate(script: string) {
480
+ "use step";
481
+
482
+ const spoken = await stepSpeak(script, { voice: "jane" });
483
+ const stored = await writeUpload(spoken.audio, { name: "summary.wav", type: "audio/wav" });
484
+ return { audio: stored.id, durationMs: spoken.durationMs };
485
+ }
486
+ ```
487
+
488
+ **`stepSpeak` is `stepGenerate` for the voice.** A step is handed no
489
+ `ToolContext`, so the provider stack your `agent()` declares is not in scope —
490
+ and the session TTS surface would not help anyway: it is an event stream wired
491
+ into a live pipeline's playback, and a step has no turn to be part of and has to
492
+ return a value. So this is the smaller thing: text in, the whole utterance out
493
+ as a WAV, on the same `ASSEMBLYAI_API_KEY` everything else uses. Voices come
494
+ from `ASSEMBLYAI_TTS_VOICES` (`@alexkroman1/aai/tts`) — read that list rather
495
+ than typing an id, because a wrong one is refused *after* the socket opens and
496
+ produces silence rather than an error.
497
+
498
+ **`writeUpload` is `readUpload`'s other direction, and you need it.** A run's
499
+ output is read back as JSON, so audio cannot travel in one — the same rule that
500
+ keeps an uploaded recording's bytes out of a run's INPUT, arriving at the other
501
+ end of the run. Store the bytes, return the **id**, and let the page fetch it
502
+ with `api.download(id)`.
503
+
504
+ Three rules come with it:
505
+
506
+ - **Speak and store in ONE step.** A step is journaled by its return value, so
507
+ an id is replayed on a resume and bytes are not. Split in two, the audio
508
+ crosses the queue between them every time the run resumes.
509
+ - **A retried step writes a SECOND upload** and abandons the first — the store
510
+ cannot know two calls meant one file. That is the price of the step being
511
+ retryable at all, and it is the right trade.
512
+ - **Name and TYPE what you store.** The byte route serves the `type` it was
513
+ given, and a browser will not play inline a file it was handed as
514
+ `application/octet-stream`.
515
+
516
+ On the page, `api.download(id)` answers a `Blob`, not a URL — the byte route
517
+ takes the same bearer every other route does, and neither `<audio src>` nor
518
+ `<a href>` can send one, so a page built on a URL works in `aai dev` and 401s
519
+ once the agent has a token. `URL.createObjectURL(blob)` is what those elements
520
+ take; revoke it when the id changes.
521
+
522
+ Test both with `stubSpeech()` and `stubUploads(files, { writable: true })`
523
+ (`@alexkroman1/aai/testing`). The write half is opt-in on purpose: a store that
524
+ silently accepted writes could not fail a spec whose step stored a file nobody
525
+ meant it to.
526
+
469
527
  ### A builtin's failure is its RESULT, so narrow it
470
528
 
471
529
  `webSearch`, `visitWebpage` and `fetchJson` (`@alexkroman1/aai/tools`) answer
@@ -13,8 +13,8 @@
13
13
  "publish:agent": "aai publish"
14
14
  },
15
15
  "dependencies": {
16
- "@alexkroman1/aai": "^6.10.1",
17
- "@alexkroman1/aai-ui": "^6.10.1",
16
+ "@alexkroman1/aai": "^6.11.0",
17
+ "@alexkroman1/aai-ui": "^6.11.0",
18
18
  "@workflow/world-postgres": "4.3.3",
19
19
  "react": "^19.2.8",
20
20
  "react-dom": "^19.2.8",
@@ -23,7 +23,7 @@
23
23
  "zod": "^4.4.3"
24
24
  },
25
25
  "devDependencies": {
26
- "@alexkroman1/aai-cli": "^6.10.1",
26
+ "@alexkroman1/aai-cli": "^6.11.0",
27
27
  "@tailwindcss/vite": "^4.3.3",
28
28
  "@types/node": "^26.2.0",
29
29
  "@types/react": "^19.2.18",
@@ -144,9 +144,18 @@ await server.listen(Number(process.env.PORT ?? 3000), host);
144
144
  console.log(`${agent.name} listening on http://${host ?? "127.0.0.1"}:${server.port}`);
145
145
 
146
146
  for (const signal of ["SIGINT", "SIGTERM"]) {
147
- process.once(signal, async () => {
147
+ // A SYNCHRONOUS listener. An `async` one hands its promise to `process`,
148
+ // which discards what a listener returns — so a `close()` that rejects would
149
+ // surface as an unhandled rejection, i.e. a crash with a stack trace on
150
+ // Ctrl-C, instead of the non-zero exit a failed shutdown should be.
151
+ process.once(signal, () => {
148
152
  // close() shuts the runtime down too — no separate runtime.shutdown().
149
- await server.close();
150
- process.exit(0);
153
+ server.close().then(
154
+ () => process.exit(0),
155
+ (error) => {
156
+ console.error(`shutdown failed: ${error?.message ?? error}`);
157
+ process.exit(1);
158
+ },
159
+ );
151
160
  });
152
161
  }
@@ -1,5 +1,5 @@
1
- import react from "@vitejs/plugin-react";
2
1
  import tailwindcss from "@tailwindcss/vite";
2
+ import react from "@vitejs/plugin-react";
3
3
  import { defineConfig } from "vite";
4
4
 
5
5
  export default defineConfig({