@alexkroman1/aai-cli 8.2.1 → 9.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 (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
@@ -1,5 +1,3 @@
1
- /// <reference types="vite/client" />
2
-
3
1
  // An EVAL: does the line really refuse to say what it cannot ground? Run it
4
2
  // with `aai eval`.
5
3
  //
@@ -27,20 +25,12 @@
27
25
  // product questions from its own knowledge and skips the tools the prompt exists
28
26
  // to route it through, so a case run that way measures nothing it claims to.
29
27
 
30
- import { withSystemPrompt } from "@alexkroman1/aai/manifest";
31
- import { withDiscoveredTools } from "@alexkroman1/aai/testing";
28
+ /** The def a DEPLOYED agent runs — see `agent.test.ts` on why the glob is here. */
29
+ import agentDef from "virtual:aai/agent";
32
30
  import { type EvalSession, toolResultIn } from "@alexkroman1/aai-runtime/eval";
33
31
  import { describeEval } from "@alexkroman1/aai-runtime/eval/vitest";
34
32
  import { expect } from "vitest";
35
33
  import { z } from "zod";
36
- import authoredAgent from "./agent.ts";
37
- import systemPrompt from "./system-prompt.md?raw";
38
-
39
- /** The def a DEPLOYED agent runs — see `agent.test.ts` on why the glob is here. */
40
- const agentDef = withSystemPrompt(
41
- withDiscoveredTools(authoredAgent, import.meta.glob("./tools/*.ts", { eager: true })),
42
- systemPrompt,
43
- );
44
34
 
45
35
  /**
46
36
  * What the BROWSER is sent, as this eval reads it.
@@ -1,26 +1,7 @@
1
- /// <reference types="vite/client" />
2
-
3
- import {
4
- createToolContext,
5
- stubGenerate,
6
- toolRunner,
7
- withDiscoveredTools,
8
- } from "@alexkroman1/aai/testing";
1
+ /** The def a DEPLOYED agent runs: authored, plus what `tools/` declares. */
2
+ import agentDef from "virtual:aai/agent";
3
+ import { createToolContext, stubGenerate, toolRunner } from "@alexkroman1/aai/testing";
9
4
  import { describe, expect, test } from "vitest";
10
- import authoredAgent from "./agent.ts";
11
-
12
- /**
13
- * The def a DEPLOYED agent runs: authored, plus what `tools/` declares.
14
- *
15
- * The glob is written HERE rather than reached for from a shared helper because
16
- * this file SHIPS: it is what a scaffolded project runs, so it may not import
17
- * anything outside its own template, and `import.meta.glob` is expanded against
18
- * the file containing it either way. This is the pattern a user writes.
19
- */
20
- const agentDef = withDiscoveredTools(
21
- authoredAgent,
22
- import.meta.glob("./tools/*.ts", { eager: true }),
23
- );
24
5
 
25
6
  import { MAX_ATTEMPTS, runCorrectiveRag } from "./procedure.ts";
26
7
  import {
@@ -17,10 +17,10 @@
17
17
  * `aai secret put ASSEMBLYAI_API_KEY` once deployed. `requiredEnv` below is
18
18
  * what makes a deploy check for it rather than letting the first run find out.
19
19
  * A step reads it with `requireStepEnv`; see `@alexkroman1/aai/step`.
20
- * - **Storage** (`aai storage enable`, Settings Database in the studio, or
21
- * `DATABASE_URL` under `aai dev`). REQUIRED here, unlike most workflow apps:
22
- * a run survives without it, but an UPLOAD's record is a row, so the form
23
- * below refuses by name until storage is on.
20
+ * - **A `DATABASE_URL`** a secret when deployed, `.env` under `aai dev` — and
21
+ * the platform provisions none, so it is a Postgres you bring. REQUIRED here,
22
+ * unlike most workflow apps: runs are durable on the platform either way, but
23
+ * an UPLOAD's record is a row, so the form below refuses by name without one.
24
24
  * - **ffmpeg, under `aai dev` only.** A deployed guest's image installs it; on a
25
25
  * laptop it is whatever is on `PATH` (or `AAI_FFMPEG_PATH`). The `transcribe`
26
26
  * flow needs it for anything that is not already a linear-PCM WAV, because the
@@ -359,14 +359,14 @@ function TranscriptionDesk() {
359
359
  // ALL THREE hooks are called every render, because a hook may not be conditional —
360
360
  // and that costs nothing here: none of them does anything until its `submit` is
361
361
  // called, and `useWorkflowRun` underneath them holds no id until then either.
362
- const streamed = useWorkflowStream<Transcript>(WORKFLOWS.streaming, { parallel });
363
- const stored = useWorkflowSubmit<Transcript>(WORKFLOWS.classic, { parallel });
364
- const batched = useWorkflowSubmit<Transcript>(WORKFLOWS.batch, { parallel });
362
+ const streamed = useWorkflowStream<typeof transcribe>(WORKFLOWS.streaming, { parallel });
363
+ const stored = useWorkflowSubmit<typeof transcribe>(WORKFLOWS.classic, { parallel });
364
+ const batched = useWorkflowSubmit<typeof transcribe>(WORKFLOWS.batch, { parallel });
365
365
  // The batch flow uploads the same way the classic one does — the id comes from the
366
366
  // store — so it is the SAME hook against a different workflow. Only the streaming
367
367
  // mode needs the other one, because only it needs the id before the bytes.
368
368
  const active = mode === "streaming" ? streamed : mode === "batch" ? batched : stored;
369
- const { submit, run, upload, pending, error, reset, pauseUpload, resumeUpload } = active;
369
+ const { submitForm, run, upload, pending, error, reset, pauseUpload, resumeUpload } = active;
370
370
  // History is per WORKFLOW, so the list follows the mode: two flows that produce
371
371
  // the same output are still two different things to have run, and merging them
372
372
  // would put a run under a heading that cannot explain it.
@@ -422,7 +422,7 @@ function TranscriptionDesk() {
422
422
  <Form
423
423
  onSubmit={(values) => {
424
424
  total.start();
425
- return submit(values);
425
+ return submitForm(values);
426
426
  }}
427
427
  error={error}
428
428
  >
@@ -167,7 +167,16 @@ export const BYTES_IN_FLIGHT = 640 * 1024 * 1024;
167
167
  *
168
168
  * The table above was measured under the old per-round barrier. Re-measuring it is
169
169
  * worth doing before this number moves again: the window makes a wide fan-out
170
- * cheaper at the tail, which if anything argues for a HIGHER knee.
170
+ * cheaper at the tail, which if anything argues for a HIGHER knee. *
171
+ * **What EXECUTES at this width is the world's call, not this number's.**
172
+ * `mapConcurrent` bounds how many step calls the body has in flight; how many
173
+ * run at once is the workflow world's worker concurrency, which on the
174
+ * `DATABASE_URL` path defaults to three — so on a default deployment a width
175
+ * above three is inert while still costing a queued job per item. That makes
176
+ * this the FAR SIDE's knee and the width to use once an operator has raised
177
+ * the ceiling, not a promise about a stock deployment. See "The WINDOW is not
178
+ * the concurrency" in `@alexkroman1/aai/step`'s `mapConcurrent`; the numbers
179
+ * above were measured against the endpoint and say nothing about that layer.
171
180
  */
172
181
  export const MAX_SEGMENT_CONCURRENCY = 32;
173
182
 
@@ -1,5 +1,3 @@
1
- /// <reference types="vite/client" />
2
-
3
1
  // An EVAL: does the confirmation gate actually gate? Run it with `aai eval`.
4
2
  //
5
3
  // `agent.test.ts` drives each tool directly and asserts about the state it
@@ -22,15 +20,6 @@
22
20
  // product questions from its own knowledge and skips the tools the prompt exists
23
21
  // to route it through, so a case run that way measures nothing it claims to.
24
22
 
25
- import { withSystemPrompt } from "@alexkroman1/aai/manifest";
26
- import { withDiscoveredTools } from "@alexkroman1/aai/testing";
27
- import { type EvalSession, type EvalTurn, lastStateIn } from "@alexkroman1/aai-runtime/eval";
28
- import { describeEval } from "@alexkroman1/aai-runtime/eval/vitest";
29
- import { expect } from "vitest";
30
- import { z } from "zod";
31
- import authoredAgent from "./agent.ts";
32
- import systemPrompt from "./system-prompt.md?raw";
33
-
34
23
  /**
35
24
  * The def a DEPLOYED agent runs: authored, plus what `tools/` declares.
36
25
  *
@@ -39,10 +28,11 @@ import systemPrompt from "./system-prompt.md?raw";
39
28
  * outside its own template. An eval that forgot it would run an agent with NO
40
29
  * tools and read as a model that refuses to act.
41
30
  */
42
- const agentDef = withSystemPrompt(
43
- withDiscoveredTools(authoredAgent, import.meta.glob("./tools/*.ts", { eager: true })),
44
- systemPrompt,
45
- );
31
+ import agentDef from "virtual:aai/agent";
32
+ import { type EvalSession, type EvalTurn, lastStateIn } from "@alexkroman1/aai-runtime/eval";
33
+ import { describeEval } from "@alexkroman1/aai-runtime/eval/vitest";
34
+ import { expect } from "vitest";
35
+ import { z } from "zod";
46
36
 
47
37
  /**
48
38
  * What the BROWSER is sent, as this eval reads it.
@@ -1,28 +1,8 @@
1
- /// <reference types="vite/client" />
2
-
1
+ /** The def a DEPLOYED agent runs: authored, plus what `tools/` declares. */
2
+ import agentDef from "virtual:aai/agent";
3
3
  import type { ToolContext } from "@alexkroman1/aai";
4
- import {
5
- createToolContext,
6
- ok,
7
- okPosition,
8
- toolRunner,
9
- withDiscoveredTools,
10
- } from "@alexkroman1/aai/testing";
4
+ import { createToolContext, ok, okPosition, toolRunner } from "@alexkroman1/aai/testing";
11
5
  import { describe, expect, test } from "vitest";
12
- import authoredAgent from "./agent.ts";
13
-
14
- /**
15
- * The def a DEPLOYED agent runs: authored, plus what `tools/` declares.
16
- *
17
- * The glob is written HERE rather than reached for from a shared helper because
18
- * this file SHIPS: it is what a scaffolded project runs, so it may not import
19
- * anything outside its own template, and `import.meta.glob` is expanded against
20
- * the file containing it either way. This is the pattern a user writes.
21
- */
22
- const agentDef = withDiscoveredTools(
23
- authoredAgent,
24
- import.meta.glob("./tools/*.ts", { eager: true }),
25
- );
26
6
 
27
7
  import {
28
8
  activeAssistant,
@@ -12,13 +12,10 @@
12
12
  // `agent.ts` alone would measure Scout with none of its own rules — and its
13
13
  // rules are the entire subject of this file. The reasoning is spelled out in
14
14
  // `../code-interpreter/agent.eval.test.ts`.
15
- import { withSystemPrompt } from "@alexkroman1/aai/manifest";
15
+
16
+ import agentDef from "virtual:aai/agent";
16
17
  import { describeEval } from "@alexkroman1/aai-runtime/eval/vitest";
17
18
  import { expect } from "vitest";
18
- import authored from "./agent.ts";
19
- import systemPrompt from "./system-prompt.md?raw";
20
-
21
- const agentDef = withSystemPrompt(authored, systemPrompt);
22
19
 
23
20
  /**
24
21
  * The registrable label of every host this turn's tool results mentioned —
@@ -0,0 +1,181 @@
1
+ #!/usr/bin/env node
2
+ import { a as ok, n as fail } from "./_output-CKkmWs7i.mjs";
3
+ import { n as log, r as notify } from "./_ui-u7T4YooX.mjs";
4
+ import { i as errorCode, o as errorMessage$1, r as binFromPackageJson } from "./_utils-B8QmtFhK.mjs";
5
+ import { createRequire } from "node:module";
6
+ import { existsSync, readdirSync } from "node:fs";
7
+ import path from "node:path";
8
+ import { omitUndefined } from "@alexkroman1/aai/utils";
9
+ import { execaSync } from "execa";
10
+ //#region test.ts
11
+ /**
12
+ * `aai test` — run agent tests via vitest.
13
+ */
14
+ /**
15
+ * Resolve the agent project's own vitest binary so tests run without the
16
+ * npx resolution overhead (and its potential network fetch of vitest).
17
+ *
18
+ * Resolves `vitest/package.json` from the agent directory, derives the bin
19
+ * script, and runs it with the current Node executable. Falls back to
20
+ * `npx vitest` only when no local install is resolvable.
21
+ */
22
+ function resolveVitestCommand(cwd, resolve = createRequire(path.join(cwd, "package.json")).resolve) {
23
+ try {
24
+ const bin = binFromPackageJson(resolve("vitest/package.json"), "vitest");
25
+ if (bin) return {
26
+ cmd: process.execPath,
27
+ args: [bin]
28
+ };
29
+ } catch {}
30
+ return {
31
+ cmd: "npx",
32
+ args: ["vitest"]
33
+ };
34
+ }
35
+ /** The files `aai test` runs, in preference order. */
36
+ const TEST_FILES = ["agent.test.ts", "agent.test.js"];
37
+ /**
38
+ * Run vitest over one of `candidates` in the given project directory.
39
+ *
40
+ * Returns the FILE it ran, or `false` if none of the candidate files exists.
41
+ * Throws on failure.
42
+ *
43
+ * A vitest FILTER, not an include glob: the argument is matched as a substring
44
+ * against the paths vitest's own include globs already found, which is why the
45
+ * candidates are named `*.test.ts` — `agent.test.ts` cannot match
46
+ * `agent.eval.test.ts` and vice versa, so the two commands stay disjoint
47
+ * without either one having to exclude the other's file.
48
+ */
49
+ function runVitest(cwd, opts = { candidates: TEST_FILES }) {
50
+ const testFile = opts.candidates.find((name) => existsSync(path.join(cwd, name)));
51
+ if (!testFile) return false;
52
+ const { cmd, args } = resolveVitestCommand(cwd);
53
+ execaSync(cmd, [
54
+ ...args,
55
+ "run",
56
+ "--root",
57
+ ".",
58
+ ...opts.extraArgs ?? [],
59
+ testFile
60
+ ], {
61
+ cwd,
62
+ stdio: "inherit",
63
+ ...omitUndefined({ env: opts.env ? {
64
+ ...process.env,
65
+ ...opts.env
66
+ } : void 0 })
67
+ });
68
+ return testFile;
69
+ }
70
+ /**
71
+ * Classify a {@link runVitest} failure. execaSync throws an ENOENT-coded
72
+ * error when the binary itself couldn't be spawned (infrastructure problem)
73
+ * and an exit-code error when vitest ran and the tests failed.
74
+ */
75
+ function classifyVitestError(err, label = "Tests") {
76
+ if (errorCode(err) === "ENOENT") return {
77
+ code: "spawn_failed",
78
+ message: `Could not launch the test runner: ${errorMessage$1(err)} — is the binary on your PATH?`
79
+ };
80
+ return {
81
+ code: "test_failed",
82
+ message: `${label} failed: ${errorMessage$1(err)}`
83
+ };
84
+ }
85
+ /** Directories a project's own specs never live in. */
86
+ const UNSCANNED_DIRS = /* @__PURE__ */ new Set([
87
+ "node_modules",
88
+ ".aai",
89
+ ".git",
90
+ "dist",
91
+ ".workflow-data"
92
+ ]);
93
+ /** What counts as a spec file. */
94
+ const SPEC_FILE_RE = /\.test\.(ts|js|tsx|mts|cts)$/;
95
+ /**
96
+ * Spec files in the project that `aai test` did NOT run.
97
+ *
98
+ * `runVitest` passes ONE filename as a vitest FILTER, which is what keeps `test`
99
+ * and `eval` disjoint without either excluding the other's file — see its doc.
100
+ * The cost is that every other `*.test.ts` in the project is skipped, and the
101
+ * skip was SILENT: the shipped `retail` template carries seven of them, so
102
+ * `aai test` there ran 1 file / 67 tests, printed "Tests passed", and left
103
+ * 211 of the project's 278 tests unrun with nothing saying so.
104
+ *
105
+ * A silent skip is the worst outcome available, so the skip is announced rather
106
+ * than the filter widened: which files `aai test` runs is a documented contract
107
+ * (the scaffold guide says "Run agent.test.ts via vitest"), and running a
108
+ * project's other specs by default could reach ones that are slow or want
109
+ * credentials. Naming them costs nothing and is what a reader needs.
110
+ *
111
+ * Eval files are excluded because they have their OWN command, named in the
112
+ * message.
113
+ */
114
+ function unrunSpecFiles(cwd, ran) {
115
+ const found = [];
116
+ collectSpecs(cwd, "", ran, found);
117
+ return found.sort(compareCodeUnits);
118
+ }
119
+ /** Code-unit comparison — see {@link unrunSpecFiles} for why not `localeCompare`. */
120
+ function compareCodeUnits(a, b) {
121
+ if (a < b) return -1;
122
+ return a > b ? 1 : 0;
123
+ }
124
+ /** One directory of {@link unrunSpecFiles}, recursing into the ones that count. */
125
+ function collectSpecs(dir, prefix, ran, out) {
126
+ let entries;
127
+ try {
128
+ entries = readdirSync(dir, { withFileTypes: true });
129
+ } catch {
130
+ return;
131
+ }
132
+ for (const e of entries) {
133
+ const rel = prefix ? `${prefix}/${e.name}` : e.name;
134
+ if (e.isDirectory()) {
135
+ if (!(UNSCANNED_DIRS.has(e.name) || e.name.startsWith("."))) collectSpecs(path.join(dir, e.name), rel, ran, out);
136
+ } else if (isUnrunSpec(e.name, rel, ran)) out.push(rel);
137
+ }
138
+ }
139
+ /** A spec file this run did not cover. The `.eval.` INFIX is the tier convention. */
140
+ function isUnrunSpec(name, rel, ran) {
141
+ if (!SPEC_FILE_RE.test(name)) return false;
142
+ return rel !== ran && !name.includes(".eval.test.");
143
+ }
144
+ /**
145
+ * Warn, once, naming the spec files this run did not cover.
146
+ *
147
+ * `ran` is `false` when there was no `agent.test.ts` to run, and that case
148
+ * needs the warning MORE rather than less: `aai test` then prints "No test file
149
+ * found" while the project's spec files sit right there unrun, which reads as
150
+ * "this project has no tests". Measured on a project whose only spec was
151
+ * `tools/echo_back.test.ts` — `{"passed":true,"skipped":true}` and not a word
152
+ * about it. It stays silent when there is nothing to name, in both arms.
153
+ */
154
+ function warnUnrunSpecs(cwd, ran) {
155
+ const skipped = unrunSpecFiles(cwd, ran === false ? "" : ran);
156
+ if (skipped.length === 0) return;
157
+ notify("warn", `${ran === false ? `\`aai test\` found no agent.test.ts, so it ran nothing. ${skipped.length} spec file(s) exist and were NOT run:` : `\`aai test\` ran ${ran} only. ${skipped.length} other spec file(s) were NOT run:`} ${skipped.join(", ")}. Run them with your own vitest (\`npx vitest run\`); behaviour evals have their own command (\`aai eval\`).`);
158
+ }
159
+ /** Execute agent tests and return structured result. */
160
+ async function executeTest(cwd) {
161
+ log.step("Running agent tests");
162
+ try {
163
+ const ran = runVitest(cwd);
164
+ if (!ran) {
165
+ log.info("No test file found. Create agent.test.ts to add tests.");
166
+ warnUnrunSpecs(cwd, ran);
167
+ return ok({
168
+ passed: true,
169
+ skipped: true
170
+ });
171
+ }
172
+ log.success("Tests passed");
173
+ warnUnrunSpecs(cwd, ran);
174
+ return ok({ passed: true });
175
+ } catch (err) {
176
+ const { code, message } = classifyVitestError(err);
177
+ return fail(code, message);
178
+ }
179
+ }
180
+ //#endregion
181
+ export { classifyVitestError, executeTest, runVitest };
package/dist/test.d.ts CHANGED
@@ -37,7 +37,7 @@ export type VitestRunOptions = {
37
37
  /**
38
38
  * Run vitest over one of `candidates` in the given project directory.
39
39
  *
40
- * Returns `true` if it ran, `false` if none of the candidate files exists.
40
+ * Returns the FILE it ran, or `false` if none of the candidate files exists.
41
41
  * Throws on failure.
42
42
  *
43
43
  * A vitest FILTER, not an include glob: the argument is matched as a substring
@@ -46,7 +46,7 @@ export type VitestRunOptions = {
46
46
  * `agent.eval.test.ts` and vice versa, so the two commands stay disjoint
47
47
  * without either one having to exclude the other's file.
48
48
  */
49
- export declare function runVitest(cwd: string, opts?: VitestRunOptions): boolean;
49
+ export declare function runVitest(cwd: string, opts?: VitestRunOptions): string | false;
50
50
  /**
51
51
  * Classify a {@link runVitest} failure. execaSync throws an ENOENT-coded
52
52
  * error when the binary itself couldn't be spawned (infrastructure problem)
@@ -58,6 +58,37 @@ label?: string): {
58
58
  code: "spawn_failed" | "test_failed";
59
59
  message: string;
60
60
  };
61
+ /**
62
+ * Spec files in the project that `aai test` did NOT run.
63
+ *
64
+ * `runVitest` passes ONE filename as a vitest FILTER, which is what keeps `test`
65
+ * and `eval` disjoint without either excluding the other's file — see its doc.
66
+ * The cost is that every other `*.test.ts` in the project is skipped, and the
67
+ * skip was SILENT: the shipped `retail` template carries seven of them, so
68
+ * `aai test` there ran 1 file / 67 tests, printed "Tests passed", and left
69
+ * 211 of the project's 278 tests unrun with nothing saying so.
70
+ *
71
+ * A silent skip is the worst outcome available, so the skip is announced rather
72
+ * than the filter widened: which files `aai test` runs is a documented contract
73
+ * (the scaffold guide says "Run agent.test.ts via vitest"), and running a
74
+ * project's other specs by default could reach ones that are slow or want
75
+ * credentials. Naming them costs nothing and is what a reader needs.
76
+ *
77
+ * Eval files are excluded because they have their OWN command, named in the
78
+ * message.
79
+ */
80
+ export declare function unrunSpecFiles(cwd: string, ran: string): string[];
81
+ /**
82
+ * Warn, once, naming the spec files this run did not cover.
83
+ *
84
+ * `ran` is `false` when there was no `agent.test.ts` to run, and that case
85
+ * needs the warning MORE rather than less: `aai test` then prints "No test file
86
+ * found" while the project's spec files sit right there unrun, which reads as
87
+ * "this project has no tests". Measured on a project whose only spec was
88
+ * `tools/echo_back.test.ts` — `{"passed":true,"skipped":true}` and not a word
89
+ * about it. It stays silent when there is nothing to name, in both arms.
90
+ */
91
+ export declare function warnUnrunSpecs(cwd: string, ran: string | false): void;
61
92
  /** Execute agent tests and return structured result. */
62
93
  export declare function executeTest(cwd: string): Promise<CommandResult<TestData>>;
63
94
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alexkroman1/aai-cli",
3
- "version": "8.2.1",
3
+ "version": "9.0.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "aai": "bin.mjs"
@@ -44,9 +44,9 @@
44
44
  "p-timeout": "^7.0.1",
45
45
  "vite": "^8.2.1",
46
46
  "zod": "^4.4.3",
47
- "@alexkroman1/aai": "8.2.1",
48
- "@alexkroman1/aai-runtime": "8.2.1",
49
- "@alexkroman1/aai-ui": "8.2.1"
47
+ "@alexkroman1/aai": "9.0.0",
48
+ "@alexkroman1/aai-runtime": "9.0.0",
49
+ "@alexkroman1/aai-ui": "9.0.0"
50
50
  },
51
51
  "devDependencies": {
52
52
  "playwright": "^1.62.1",
@@ -1,93 +0,0 @@
1
- #!/usr/bin/env node
2
- import { c as isEexist, l as readJson, p as writeJson } from "./_utils-B8QmtFhK.mjs";
3
- import { r as isDevMode, t as getMonorepoRoot } from "./_agent-BTwZJu4P.mjs";
4
- import { REPO_URL, downloadAndMergeTemplate } from "./_templates-4WcKOjS5.mjs";
5
- import path from "node:path";
6
- import fs from "node:fs/promises";
7
- //#region _init.ts
8
- function readmeContent(slug) {
9
- return `# ${slug}
10
-
11
- A voice agent built with [aai](${REPO_URL}).
12
-
13
- ## Getting started
14
-
15
- \`\`\`sh
16
- npm install # Install dependencies
17
- aai dev # Run locally (opens browser)
18
- aai publish # Publish to production (and sync to the studio)
19
- \`\`\`
20
-
21
- ## Secrets
22
-
23
- Access secrets in your agent via \`ctx.env.MY_KEY\`.
24
-
25
- **Local development** — add secrets to \`.env\` (auto-loaded by \`aai dev\`):
26
-
27
- \`\`\`sh
28
- ALPHA_VANTAGE_KEY=sk-abc123
29
- MY_API_KEY=secret-value
30
- \`\`\`
31
-
32
- **Production** — set secrets on the server:
33
-
34
- \`\`\`sh
35
- aai secret put MY_KEY # Set a secret (prompts for value)
36
- aai secret list # List secret names
37
- aai secret delete MY_KEY # Remove a secret
38
- \`\`\`
39
-
40
- `;
41
- }
42
- /**
43
- * Map from npm package name to directory name under packages/.
44
- * Used to rewrite published version ranges to link: paths in dev mode.
45
- */
46
- const WORKSPACE_PKG_DIRS = {
47
- "@alexkroman1/aai": "aai",
48
- "@alexkroman1/aai-cli": "aai-cli",
49
- "@alexkroman1/aai-runtime": "aai-runtime",
50
- "@alexkroman1/aai-ui": "aai-ui"
51
- };
52
- /** Rewrite workspace deps to link: paths so pnpm links to local source. */
53
- async function patchPackageJsonForWorkspace(targetDir) {
54
- const pkgPath = path.join(targetDir, "package.json");
55
- const pkgJson = await readJson(pkgPath);
56
- if (!pkgJson) return;
57
- pkgJson.name = path.basename(targetDir);
58
- delete pkgJson.packageManager;
59
- const root = getMonorepoRoot();
60
- if (!root) return;
61
- const packagesDir = path.join(root, "packages");
62
- for (const field of ["dependencies", "devDependencies"]) {
63
- const deps = pkgJson[field];
64
- if (!deps) continue;
65
- for (const key of Object.keys(deps)) {
66
- const dir = WORKSPACE_PKG_DIRS[key];
67
- if (dir) deps[key] = `link:${path.relative(targetDir, path.join(packagesDir, dir))}`;
68
- }
69
- }
70
- await writeJson(pkgPath, pkgJson);
71
- }
72
- async function runInit(opts) {
73
- const { targetDir, template } = opts;
74
- await downloadAndMergeTemplate(template, targetDir);
75
- if (isDevMode()) {
76
- await patchPackageJsonForWorkspace(targetDir);
77
- try {
78
- await fs.unlink(path.join(targetDir, ".npmrc"));
79
- } catch {}
80
- }
81
- try {
82
- await fs.copyFile(path.join(targetDir, ".env.example"), path.join(targetDir, ".env"), fs.constants.COPYFILE_EXCL);
83
- } catch {}
84
- const readmePath = path.join(targetDir, "README.md");
85
- const slug = path.basename(path.resolve(targetDir));
86
- try {
87
- await fs.writeFile(readmePath, readmeContent(slug), { flag: "wx" });
88
- } catch (err) {
89
- if (!isEexist(err)) throw err;
90
- }
91
- }
92
- //#endregion
93
- export { runInit };
@@ -1,66 +0,0 @@
1
- #!/usr/bin/env node
2
- import { a as ok, n as fail } from "./_output-CKkmWs7i.mjs";
3
- import { n as log } from "./_ui-u7T4YooX.mjs";
4
- import { f as checkedResponse } from "./_studio-CKrsixd0.mjs";
5
- import { n as slugRequest } from "./_slug-api--ZBmIGH-.mjs";
6
- import { isRecord } from "@alexkroman1/aai/utils";
7
- import * as p from "@clack/prompts";
8
- //#region storage.ts
9
- /**
10
- * The storage route's answer, CHECKED — every caller here reads `enabled` and
11
- * a body without it reported "Storage is disabled for <slug>" (and returned
12
- * `enabled: undefined` to a script) for a server that never said so. See
13
- * `checkedResponse`.
14
- */
15
- async function storageRequest(cwd, init, server) {
16
- const { data, slug } = await slugRequest(cwd, "/storage", {
17
- ...init,
18
- action: "storage"
19
- }, server);
20
- return {
21
- enabled: checkedResponse(data, (value) => isRecord(value) && typeof value.enabled === "boolean", `the storage route for ${slug}`).enabled,
22
- slug
23
- };
24
- }
25
- async function executeStorageStatus(cwd, server) {
26
- const { enabled, slug } = await storageRequest(cwd, void 0, server);
27
- if (enabled) log.info(`Storage is enabled for ${slug}`);
28
- else log.info(`Storage is disabled for ${slug}. Use \`aai storage enable\` to turn it on.`);
29
- return ok({
30
- slug,
31
- enabled
32
- });
33
- }
34
- async function executeStorageEnable(cwd, server) {
35
- const { enabled, slug } = await storageRequest(cwd, { method: "POST" }, server);
36
- log.success(`Storage enabled for ${slug}`);
37
- log.info("Tool code can now use ctx.db.query(sql, params).");
38
- return ok({
39
- slug,
40
- enabled
41
- });
42
- }
43
- /**
44
- * Disable storage — destructive: the server DROPS the app's database schema
45
- * and all its data. On a TTY this requires interactive confirmation; without
46
- * a TTY it refuses unless `--force` is passed, so a script can never drop
47
- * data by accident.
48
- */
49
- async function executeStorageDisable(cwd, opts = {}) {
50
- if (!opts.force) {
51
- if (!(opts.isTTY ?? Boolean(process.stdin.isTTY && process.stdout.isTTY))) return fail("confirmation_required", "Disabling storage drops the app's database schema and ALL its data.", "Re-run with --force to disable storage without confirmation.");
52
- const confirmed = await p.confirm({ message: "Disable storage? This DROPS the app's database schema and all its data." });
53
- if (p.isCancel(confirmed) || confirmed !== true) {
54
- log.info("Cancelled. Storage was left unchanged.");
55
- return fail("cancelled", "Disable cancelled");
56
- }
57
- }
58
- const { enabled, slug } = await storageRequest(cwd, { method: "DELETE" }, opts.server);
59
- log.success(`Storage disabled for ${slug} — database schema and data dropped`);
60
- return ok({
61
- slug,
62
- enabled
63
- });
64
- }
65
- //#endregion
66
- export { executeStorageDisable, executeStorageEnable, executeStorageStatus };
package/dist/storage.d.ts DELETED
@@ -1,22 +0,0 @@
1
- import { type CommandResult } from "./_output.ts";
2
- type StorageStatusData = {
3
- slug: string;
4
- enabled: boolean;
5
- };
6
- export declare function executeStorageStatus(cwd: string, server: string | undefined): Promise<CommandResult<StorageStatusData>>;
7
- export declare function executeStorageEnable(cwd: string, server: string | undefined): Promise<CommandResult<StorageStatusData>>;
8
- export type StorageDisableOpts = {
9
- server?: string | undefined;
10
- /** Skip the confirmation prompt (required in non-interactive runs). */
11
- force?: boolean | undefined;
12
- /** TTY override for testing. Defaults to real stdin+stdout TTY state. */
13
- isTTY?: boolean | undefined;
14
- };
15
- /**
16
- * Disable storage — destructive: the server DROPS the app's database schema
17
- * and all its data. On a TTY this requires interactive confirmation; without
18
- * a TTY it refuses unless `--force` is passed, so a script can never drop
19
- * data by accident.
20
- */
21
- export declare function executeStorageDisable(cwd: string, opts?: StorageDisableOpts): Promise<CommandResult<StorageStatusData>>;
22
- export {};