@alexkroman1/aai-cli 14.0.0 → 15.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 (45) hide show
  1. package/dist/_artifacts-BJOYGQPp.mjs +21 -0
  2. package/dist/_artifacts.d.ts +16 -0
  3. package/dist/_build-target.d.ts +172 -0
  4. package/dist/{_bundler-DolUCMxu.mjs → _bundler-DM0d0M7m.mjs} +1 -1
  5. package/dist/{_dev-server-CSMqF8PN.mjs → _dev-server-BzWB6-4y.mjs} +9 -6
  6. package/dist/_e2e-test-utils.d.ts +1 -1
  7. package/dist/{_init-CQ8idAwo.mjs → _init-Bsi3DZNJ.mjs} +1 -1
  8. package/dist/_server-common-De0haHr9.mjs +70 -0
  9. package/dist/_server-common.d.ts +20 -1
  10. package/dist/{_templates-CK4oKoeX.mjs → _templates-CIlJ3Vay.mjs} +1 -1
  11. package/dist/_templates.d.ts +1 -1
  12. package/dist/_vercel-output.d.ts +63 -0
  13. package/dist/build-BhEaxBPu.mjs +481 -0
  14. package/dist/build.d.ts +18 -10
  15. package/dist/cli.mjs +49 -14
  16. package/dist/{client-bundler-BJgREAh6.mjs → client-bundler-6mTLs6ny.mjs} +4 -4
  17. package/dist/client-bundler.d.ts +1 -1
  18. package/dist/client-bundler.mjs +1 -1
  19. package/dist/{deploy-uAJ4NukN.mjs → deploy-CGqPU5U-.mjs} +2 -2
  20. package/dist/{dev-DApPSaE_.mjs → dev-Bx9gYBHM.mjs} +1 -1
  21. package/dist/{eval-BK47A_K5.mjs → eval-B3I7FqN9.mjs} +1 -1
  22. package/dist/{init-DukDxECd.mjs → init-CFyusRbq.mjs} +39 -3
  23. package/dist/init.d.ts +14 -0
  24. package/dist/scaffold/CLAUDE.md +73 -22
  25. package/dist/scaffold/package.json +6 -6
  26. package/dist/start.d.ts +112 -0
  27. package/dist/start.mjs +156 -0
  28. package/dist/{studio-CpHlNHUZ.mjs → studio-C_zuRC_z.mjs} +2 -2
  29. package/dist/templates/briefing-desk/agent.eval.test.ts +156 -0
  30. package/dist/templates/code-interpreter/agent.test.ts +103 -0
  31. package/dist/templates/link-digest/client.tsx +55 -3
  32. package/dist/templates/math-buddy/agent.test.ts +126 -0
  33. package/dist/templates/personal-finance/agent.test.ts +127 -0
  34. package/dist/templates/support-line/agent.ts +8 -0
  35. package/dist/templates/travel-concierge/routing.ts +64 -55
  36. package/dist/templates/travel-concierge/tools/cancel_action.ts +3 -1
  37. package/dist/templates/travel-concierge/tools/complete_or_escalate.ts +3 -1
  38. package/dist/templates/travel-concierge/tools/confirm_action.ts +3 -1
  39. package/dist/templates/web-researcher/agent.test.ts +130 -0
  40. package/dist/worker-bundler.d.ts +1 -1
  41. package/dist/worker-bundler.mjs +7 -7
  42. package/package.json +9 -4
  43. package/dist/_server-common-vILJp3it.mjs +0 -43
  44. package/dist/build-Mxk8gWvX.mjs +0 -108
  45. package/dist/scaffold/server.mjs +0 -308
@@ -0,0 +1,127 @@
1
+ /** The def a DEPLOYED agent runs: authored, plus what `system-prompt.md` says. */
2
+ import agentDef from "virtual:aai/agent";
3
+ import { AgentConfigSchema, toAgentConfig } from "@alexkroman1/aai/manifest";
4
+ import { describe, expect, test } from "vitest";
5
+
6
+ /**
7
+ * What a starter's spec may assert.
8
+ *
9
+ * `aai init` scaffolds this template verbatim, and `aai build` runs these tests
10
+ * before it bundles — so an assertion pinning Penny's own identity (her literal
11
+ * name, her voice, her model, the wording of a house rule) turns a user's first
12
+ * customization into a build failure in a file they never wrote. Every test
13
+ * here therefore asserts a property that survives those edits, on the RESOLVED
14
+ * config rather than on the def's empty fields.
15
+ *
16
+ * What is deliberately NOT here: whether Penny actually reaches for `run_code`
17
+ * instead of dividing in her head, whether she looks a rate up rather than
18
+ * quoting a remembered one, and whether she keeps the not-financial-advice
19
+ * caveat. Those are claims about a live model and belong to
20
+ * `agent.eval.test.ts`, which drives them against one. This tier asserts the
21
+ * WIRING those runs depend on — a template whose builtins never reached the
22
+ * config fails an eval as a behaviour problem, in a report nobody reads as
23
+ * "the tool was not there".
24
+ */
25
+ describe("personal-finance template", () => {
26
+ test("config passes manifest validation", () => {
27
+ // Same conversion `aai build`/`aai deploy` run. It is also what checks the
28
+ // two builtin NAMES against the SDK's own enum, so a typo in
29
+ // `builtinTools` fails here rather than shipping an agent whose prompt
30
+ // commands a tool the platform never resolved.
31
+ expect(() => toAgentConfig(agentDef)).not.toThrow();
32
+ });
33
+
34
+ test("exports an agent the platform can name", () => {
35
+ // Not the literal: what has to hold is that there IS a name and the
36
+ // conversion carries it through — the studio lists a deployed agent by
37
+ // exactly this string, and renaming her is the first edit this template
38
+ // invites.
39
+ expect(agentDef.name).toBeTruthy();
40
+ expect(toAgentConfig(agentDef).name).toBe(agentDef.name);
41
+ });
42
+
43
+ test("every stage its mode needs is filled, declared or defaulted", () => {
44
+ // This template declares no provider at all — it is a prompt and two
45
+ // builtins — so the default all-AssemblyAI cascade is what makes it run
46
+ // the moment it is deployed. Asserted per MODE so it stays true after a
47
+ // swap: declare `stt`/`llm`/`tts` and the rest still default; declare
48
+ // `s2s` and there is no cascade to fill, which is the one thing that must
49
+ // never happen by fallthrough.
50
+ const config = toAgentConfig(agentDef);
51
+ if (config.mode === "s2s") {
52
+ expect(config.s2s?.kind).toBeTruthy();
53
+ expect(config.stt).toBeUndefined();
54
+ expect(config.tts).toBeUndefined();
55
+ } else if (config.mode === "text") {
56
+ expect(config.llm?.kind).toBeTruthy();
57
+ } else {
58
+ expect(config.mode).toBe("pipeline");
59
+ expect(config.stt?.kind).toBeTruthy();
60
+ expect(config.llm?.kind).toBeTruthy();
61
+ expect(config.tts?.kind).toBeTruthy();
62
+ }
63
+ });
64
+
65
+ test("both builtins survive into the config a deploy carries", () => {
66
+ const builtins = toAgentConfig(agentDef).builtinTools ?? [];
67
+
68
+ // `run_code` is the arithmetic rule's only mechanism. The prompt forbids
69
+ // Penny working ANY figure out in her head — a tip, a split, a payment, a
70
+ // projection — so without the builtin the rule has nothing to point at and
71
+ // degrades into a model inventing numbers the caller then spends money on.
72
+ expect(builtins).toContain("run_code");
73
+
74
+ // `fetch_json` is the only route to a number that MOVES. A rate or a coin
75
+ // price the model remembers is months stale and carries no source, and it
76
+ // arrives in exactly the confident tone a fetched one would — which is why
77
+ // a finance starter that cannot make a request is worse than one that
78
+ // declines to answer.
79
+ expect(builtins).toContain("fetch_json");
80
+
81
+ // Asserted on the CONFIG rather than the def because that is what a deploy
82
+ // ships, and because `DEFAULT_BUILTIN_TOOLS` is empty: a builtin is
83
+ // something an agent asks for, never something it has to notice and switch
84
+ // off. So a dropped entry is not a quieter Penny, it is the same Penny with
85
+ // no way to be right. Adding builtins beside these two is an ordinary edit;
86
+ // losing one is the regression.
87
+ });
88
+
89
+ test("every builtin the prompt tells Penny to use is one she declares", () => {
90
+ // The pairing this template is made of: the prose holds the endpoints and
91
+ // the formulas, each list headed by the tool that consumes it, and
92
+ // `agent.ts` holds the array that makes those tools exist. The failure is
93
+ // silent in both directions — a prompt commanding `fetch_json` at an agent
94
+ // that never declared it produces a model apologizing for a tool it cannot
95
+ // see, and a builtin dropped from `agent.ts` alone leaves the endpoint list
96
+ // addressed to nothing — and neither shows up in a diff of either file.
97
+ const config = toAgentConfig(agentDef);
98
+ const declared = config.builtinTools ?? [];
99
+
100
+ // Which snake_case tokens in the prose are tool NAMES is a question for the
101
+ // SDK's own schema rather than a catalog restated here: this prompt also
102
+ // names `vs_currencies`, `include_24hr_change`, `per_person` and
103
+ // `annual_rate`, so matching every underscored word would redden on a
104
+ // formula, and a copied list of builtins goes stale the first time the SDK
105
+ // adds one.
106
+ const isBuiltin = (name: string) =>
107
+ AgentConfigSchema.safeParse({ ...config, builtinTools: [name] }).success;
108
+ const commanded = [
109
+ ...new Set(config.systemPrompt.match(/\b[a-z][a-z0-9]*(?:_[a-z0-9]+)+\b/g) ?? []),
110
+ ].filter(isBuiltin);
111
+
112
+ // Non-vacuity, and it earns its keep twice: a prompt naming no builtin at
113
+ // all would leave the loop below asserting nothing, and it is also the state
114
+ // this template lands in when `system-prompt.md` is not applied — the
115
+ // framework default names no builtin, so "I edited the prompt and nothing
116
+ // changed" fails here instead of passing quietly with Penny's rules nowhere
117
+ // in her context.
118
+ expect(commanded.length).toBeGreaterThan(0);
119
+ for (const name of commanded) {
120
+ expect(declared, `the prompt tells Penny to use ${name}`).toContain(name);
121
+ }
122
+
123
+ // The converse is deliberately NOT asserted: declaring a builtin the prompt
124
+ // never mentions is an ordinary edit, and the model learns about it from its
125
+ // own tool schema rather than from the prose.
126
+ });
127
+ });
@@ -24,4 +24,12 @@ export default agent({
24
24
  // caller's callback number, and only its reference crosses to the browser.
25
25
  syncState: supportProjection,
26
26
  greeting: `${PRODUCT} support, you're through to the automated line. What's happened?`,
27
+ // A support line is a PHONE line, so this one declares the carrier its number
28
+ // is with. Nothing serves `WS /phone` without this — the route is an
29
+ // allow-list, and an agent that says nothing about carriers answers none — so
30
+ // point a Twilio number's webhook at the deployed agent's `/phone` and the
31
+ // call lands in an ordinary session. `true` admits every carrier the runtime
32
+ // ships a codec for; a list is the narrower statement, and it is the one to
33
+ // copy.
34
+ telephony: ["twilio"],
27
35
  });
@@ -6,7 +6,10 @@
6
6
  * `activeAssistant`, `applyPending`, `note`. Each is NAMED by a file under
7
7
  * `tools/`, which is what registers it: a tool's name is its file name, so
8
8
  * `tools/to_flight_assistant.ts` is one line handing {@link delegationTool} an
9
- * id, and `tools/confirm_action.ts` re-exports {@link confirmAction}.
9
+ * id, and `tools/confirm_action.ts` one line calling {@link confirmActionTool}.
10
+ * Every file in `tools/` therefore default-exports the RESULT of a call: a
11
+ * factory per tool rather than a shared const, so nothing here is a re-export
12
+ * — which is what `noExportedImports` would otherwise have to be told to allow.
10
13
  *
11
14
  * The delegation four are still generated rather than written out. The notebook
12
15
  * declares `ToFlightBookingAssistant`, `ToHotelBookingAssistant`,
@@ -77,33 +80,35 @@ export function delegationTool(id: SpecialistId): ToolDef {
77
80
  * a `done` tool will keep trying to answer things the desk has no tools for.
78
81
  * The `reason` is what the concierge is handed on the way back up.
79
82
  */
80
- export const completeOrEscalate = tripSlot.updateTool({
81
- description:
82
- "Hand the call back to the main concierge. Use this when the current desk's " +
83
- "work is finished, when the caller changes the subject to something this " +
84
- "desk does not handle, or when they change their mind.",
85
- inputSchema: z.object({
86
- reason: z
87
- .string()
88
- .max(300)
89
- .describe("Why the call is going back — what is done, or what the caller now wants"),
90
- }),
91
- execute(args, trip) {
92
- const left = activeAssistant(trip);
93
- // `primary` stays at the bottom — the slot's `after` hook restores it if a
94
- // pop ever empties the stack.
95
- if (trip.dialogState.length > 1) trip.dialogState.pop();
96
- note(trip, `← back to concierge: ${args.reason}`);
97
- return {
98
- returnedFrom: left === "primary" ? "concierge" : SPECIALISTS[left].title,
99
- nowHandling: "concierge",
100
- reason: args.reason,
101
- instructions:
102
- "You are the main concierge again. Pick up what the caller asked for; " +
103
- "delegate again if it belongs to another desk.",
104
- };
105
- },
106
- });
83
+ export function completeOrEscalateTool(): ToolDef {
84
+ return tripSlot.updateTool({
85
+ description:
86
+ "Hand the call back to the main concierge. Use this when the current desk's " +
87
+ "work is finished, when the caller changes the subject to something this " +
88
+ "desk does not handle, or when they change their mind.",
89
+ inputSchema: z.object({
90
+ reason: z
91
+ .string()
92
+ .max(300)
93
+ .describe("Why the call is going back — what is done, or what the caller now wants"),
94
+ }),
95
+ execute(args, trip) {
96
+ const left = activeAssistant(trip);
97
+ // `primary` stays at the bottom — the slot's `after` hook restores it if a
98
+ // pop ever empties the stack.
99
+ if (trip.dialogState.length > 1) trip.dialogState.pop();
100
+ note(trip, `← back to concierge: ${args.reason}`);
101
+ return {
102
+ returnedFrom: left === "primary" ? "concierge" : SPECIALISTS[left].title,
103
+ nowHandling: "concierge",
104
+ reason: args.reason,
105
+ instructions:
106
+ "You are the main concierge again. Pick up what the caller asked for; " +
107
+ "delegate again if it belongs to another desk.",
108
+ };
109
+ },
110
+ });
111
+ }
107
112
 
108
113
  /**
109
114
  * `confirm_action` — the caller said yes.
@@ -121,14 +126,16 @@ export const completeOrEscalate = tripSlot.updateTool({
121
126
  * the body did NOT answer with a {@link ToolFailure}, so an application that
122
127
  * failed leaves the change staged and the caller still being asked.
123
128
  */
124
- export const confirmAction = gateFlow.tool({
125
- description:
126
- "Apply the change the caller has just confirmed out loud. Only call this " +
127
- "after you have read the change back and heard a clear yes.",
128
- when: "awaitingConfirmation",
129
- send: { type: "SETTLED" },
130
- execute: (_args, ctx) => tripSlot.update(ctx, (trip) => applyPending(trip)),
131
- });
129
+ export function confirmActionTool(): ToolDef {
130
+ return gateFlow.tool({
131
+ description:
132
+ "Apply the change the caller has just confirmed out loud. Only call this " +
133
+ "after you have read the change back and heard a clear yes.",
134
+ when: "awaitingConfirmation",
135
+ send: { type: "SETTLED" },
136
+ execute: (_args, ctx) => tripSlot.update(ctx, (trip) => applyPending(trip)),
137
+ });
138
+ }
132
139
 
133
140
  /**
134
141
  * `cancel_action` — the caller said no. Drops the staged action, changes nothing.
@@ -136,22 +143,24 @@ export const confirmAction = gateFlow.tool({
136
143
  * Gated for the same reason as `confirm_action`, and its own "nothing was
137
144
  * waiting" arm is gone with the same argument.
138
145
  */
139
- export const cancelAction = gateFlow.tool({
140
- description:
141
- "Discard the change the caller just declined. Call this when they say no, " +
142
- "or when they want to change the details before confirming.",
143
- when: "awaitingConfirmation",
144
- send: { type: "SETTLED" },
145
- execute: (_args, ctx) =>
146
- tripSlot.update(ctx, (trip) => {
147
- const action = trip.pending;
148
- // Reachable only if the position and the payload disagree; see
149
- // `applyPending`. Reported rather than thrown, mid-call.
150
- if (!action) return { discarded: null, message: "Nothing was staged after all." };
151
- trip.pending = null;
152
- const described = describeAction(action);
153
- const summary = typeof described === "string" ? described : action.kind;
154
- note(trip, `Declined: ${summary}`);
155
- return { discarded: summary, message: "Nothing was changed." };
156
- }),
157
- });
146
+ export function cancelActionTool(): ToolDef {
147
+ return gateFlow.tool({
148
+ description:
149
+ "Discard the change the caller just declined. Call this when they say no, " +
150
+ "or when they want to change the details before confirming.",
151
+ when: "awaitingConfirmation",
152
+ send: { type: "SETTLED" },
153
+ execute: (_args, ctx) =>
154
+ tripSlot.update(ctx, (trip) => {
155
+ const action = trip.pending;
156
+ // Reachable only if the position and the payload disagree; see
157
+ // `applyPending`. Reported rather than thrown, mid-call.
158
+ if (!action) return { discarded: null, message: "Nothing was staged after all." };
159
+ trip.pending = null;
160
+ const described = describeAction(action);
161
+ const summary = typeof described === "string" ? described : action.kind;
162
+ note(trip, `Declined: ${summary}`);
163
+ return { discarded: summary, message: "Nothing was changed." };
164
+ }),
165
+ });
166
+ }
@@ -4,4 +4,6 @@
4
4
  * gives it its name.
5
5
  */
6
6
 
7
- export { cancelAction as default } from "../routing.ts";
7
+ import { cancelActionTool } from "../routing.ts";
8
+
9
+ export default cancelActionTool();
@@ -4,4 +4,6 @@
4
4
  * gives it its name.
5
5
  */
6
6
 
7
- export { completeOrEscalate as default } from "../routing.ts";
7
+ import { completeOrEscalateTool } from "../routing.ts";
8
+
9
+ export default completeOrEscalateTool();
@@ -4,4 +4,6 @@
4
4
  * gives it its name.
5
5
  */
6
6
 
7
- export { confirmAction as default } from "../routing.ts";
7
+ import { confirmActionTool } from "../routing.ts";
8
+
9
+ export default confirmActionTool();
@@ -0,0 +1,130 @@
1
+ /** The def a DEPLOYED agent runs: authored, plus what `system-prompt.md` says. */
2
+ import agentDef from "virtual:aai/agent";
3
+ import { toAgentConfig } from "@alexkroman1/aai/manifest";
4
+ import { describe, expect, test } from "vitest";
5
+ import promptFile from "./system-prompt.md?raw";
6
+
7
+ /**
8
+ * What a starter's spec may assert.
9
+ *
10
+ * `aai build` runs these tests before it bundles, so an assertion pinning the
11
+ * template's own identity — its literal name, its greeting, its voice, its
12
+ * model — turns a user's first customization into a build failure in a file
13
+ * they never wrote. Every test here therefore asserts a property that survives
14
+ * those edits, on the RESOLVED config rather than on the def's empty fields.
15
+ *
16
+ * What is left to `agent.eval.test.ts` is everything that needs a model: that
17
+ * Scout SEARCHES before answering a fact it is sure of, that the outlet it
18
+ * names appeared in results it actually read, and that the SSRF screen refuses
19
+ * a private address through the agent's own executor. Those are behaviour, and
20
+ * this tier may not reach the network at all — which is also why nothing here
21
+ * runs `web_search`. What a unit test can say is that the capability and the
22
+ * document that instructs it are both still THERE, which is the half that
23
+ * regresses in a diff rather than in a model.
24
+ *
25
+ * The def therefore comes from `virtual:aai/agent` rather than from
26
+ * `./agent.ts`: the prompt file is applied by the BUILD, so the raw default
27
+ * export carries the framework prompt and every claim below would be measuring
28
+ * an agent nobody deploys. This template has no `tools/` directory, so that
29
+ * import is also the file's existence check — `deployedAgent` is handed the
30
+ * prompt and nothing else, and refuses a call with no project files at all.
31
+ */
32
+
33
+ /**
34
+ * The builtins that can reach the web.
35
+ *
36
+ * Typed by USE rather than by an annotation: `builtinTools` takes a
37
+ * `BuiltinTool[]`, so passing this list to `toAgentConfig` below only compiles
38
+ * if every name here is really a builtin — a name the SDK renames fails
39
+ * `pnpm typecheck` in this file instead of dropping silently out of the scan.
40
+ */
41
+ const WEB_BUILTINS = ["web_search", "visit_webpage", "fetch_json", "get_page_design"] as const;
42
+
43
+ describe("web-researcher template", () => {
44
+ test("config passes manifest validation", () => {
45
+ // Same conversion `aai build`/`aai deploy` run.
46
+ expect(() => toAgentConfig(agentDef)).not.toThrow();
47
+ });
48
+
49
+ test("exports an agent the platform can name", () => {
50
+ // Not the literal: what has to hold is that there IS a name, and that the
51
+ // conversion carries it through — `AgentName` refuses a blank one, and the
52
+ // studio lists a deployed agent by exactly this string.
53
+ expect(agentDef.name).toBeTruthy();
54
+ expect(toAgentConfig(agentDef).name).toBe(agentDef.name);
55
+ });
56
+
57
+ test("every stage its mode needs is filled, declared or defaulted", () => {
58
+ // This template declares no provider at all — the whole voice pipeline is
59
+ // the injected all-AssemblyAI default, which is what lets the starter run
60
+ // the moment it is deployed. Asserted per MODE so it survives a swap:
61
+ // declare `stt`/`llm`/`tts` and the rest still default; declare `s2s` and
62
+ // there is no cascade to fill, which is the one thing that must never
63
+ // happen by fallthrough.
64
+ const config = toAgentConfig(agentDef);
65
+ if (config.mode === "s2s") {
66
+ expect(config.s2s?.kind).toBeTruthy();
67
+ expect(config.stt).toBeUndefined();
68
+ expect(config.tts).toBeUndefined();
69
+ } else if (config.mode === "text") {
70
+ expect(config.llm?.kind).toBeTruthy();
71
+ } else {
72
+ expect(config.mode).toBe("pipeline");
73
+ expect(config.stt?.kind).toBeTruthy();
74
+ expect(config.llm?.kind).toBeTruthy();
75
+ expect(config.tts?.kind).toBeTruthy();
76
+ }
77
+ });
78
+
79
+ test("system-prompt.md is the prompt a deploy carries", () => {
80
+ // The build discovers the file; nothing imports it. So the failure this
81
+ // catches is "I edited the prompt and nothing changed" — and here it is the
82
+ // expensive one, because every rule that makes Scout a researcher rather
83
+ // than a talkative model (search first, cite what you read, treat a fetched
84
+ // page as data and not as instructions) lives ONLY in that file. An agent
85
+ // running the framework default answers plausibly, sounds fine, and cites
86
+ // pages it never opened.
87
+ //
88
+ // `toContain` rather than an equality: importing the file into `agent.ts`
89
+ // and composing it (a computed suffix, a date) is a legitimate edit, and
90
+ // `withSystemPrompt` leaves such a def exactly as the author built it.
91
+ const trimmed = promptFile.trim();
92
+ // Not vacuous: `toContain("")` would pass over an empty file, so the file
93
+ // having text in it is asserted before it is used as the needle.
94
+ expect(trimmed).not.toBe("");
95
+ expect(toAgentConfig(agentDef).systemPrompt).toContain(trimmed);
96
+ });
97
+
98
+ test("can search the web, and names no web tool it does not have", () => {
99
+ const config = toAgentConfig(agentDef);
100
+ const declared = new Set<string>(config.builtinTools ?? []);
101
+
102
+ // `web_search` is the template's reason to exist, and dropping it is the
103
+ // silent failure: an agent with no search answers from memory, which reads
104
+ // back exactly like an agent that searched — right up to the fabricated
105
+ // source its eval exists to catch.
106
+ expect([...declared]).toContain("web_search");
107
+
108
+ // The prompt addresses its tools BY NAME ("begins with a web_search call",
109
+ // "Use visit_webpage when the search snippets aren't detailed enough"), so
110
+ // the two halves can drift apart: a builtin dropped from `agent.ts` leaves
111
+ // the prompt ordering a call the model cannot make, and the model then
112
+ // apologizes for a tool it was told it had. Stated as an implication so
113
+ // either edit alone survives — retire the sentence along with the builtin
114
+ // and nothing here objects, and a builtin added without a mention is a
115
+ // free edit too. An empty scan is likewise not a failure: a prompt
116
+ // rewritten to DESCRIBE the tools rather than name them is fine, and the
117
+ // assertion above is what keeps the search itself from going away.
118
+ const named = WEB_BUILTINS.filter((name) => config.systemPrompt.includes(name));
119
+ expect(named.filter((name) => !declared.has(name))).toEqual([]);
120
+ });
121
+
122
+ test("the web builtins this spec scans for are the SDK's own names", () => {
123
+ // The other half of the `as const` above, at run time: a scan whose
124
+ // candidates had gone stale would report "no unbacked names" over a prompt
125
+ // full of them. `toAgentConfig` validates `builtinTools` against the
126
+ // builtin enum, so accepting the whole list is the SDK confirming every
127
+ // name in it.
128
+ expect(() => toAgentConfig({ ...agentDef, builtinTools: WEB_BUILTINS })).not.toThrow();
129
+ });
130
+ });
@@ -34,4 +34,4 @@ export type BuildWorkerOptions = {
34
34
  * @internal — build hook for aai-server/the studio; not a supported public
35
35
  * API and not covered by semver.
36
36
  */
37
- export declare function buildWorker(cwd: string, opts?: BuildWorkerOptions): Promise<string>;
37
+ export declare function buildWorker(cwd: string, options?: BuildWorkerOptions): Promise<string>;
@@ -92,8 +92,8 @@ export const __aaiConfig = {
92
92
  ...toAgentConfig(__aaiAgent),
93
93
  toolSchemas: agentToolsToSchemas(__aaiAgent.tools ?? {}),
94
94
  };
95
- ${runtime ? `export const __aaiCreateRuntime = (opts: Record<string, unknown>) =>
96
- createRuntime({ ...opts, agent: __aaiAgent });
95
+ ${runtime ? `export const __aaiCreateRuntime = (options: Record<string, unknown>) =>
96
+ createRuntime({ ...options, agent: __aaiAgent });
97
97
  ` : ""}`;
98
98
  }
99
99
  /**
@@ -105,21 +105,21 @@ ${runtime ? `export const __aaiCreateRuntime = (opts: Record<string, unknown>) =
105
105
  * @internal — build hook for aai-server/the studio; not a supported public
106
106
  * API and not covered by semver.
107
107
  */
108
- async function buildWorker(cwd, opts = {}) {
108
+ async function buildWorker(cwd, options = {}) {
109
109
  const wrapperPath = path.join(cwd, WRAPPER_ENTRY_REL);
110
110
  const [, toolFiles, systemPromptFile] = await Promise.all([
111
111
  fs.mkdir(path.dirname(wrapperPath), { recursive: true }),
112
112
  discoverToolFiles(cwd),
113
113
  hasSystemPromptFile(cwd)
114
114
  ]);
115
- await fs.writeFile(wrapperPath, wrapperEntrySource(opts.runtime !== false, toolFiles, systemPromptFile), "utf-8");
116
- const plugins = opts.plugins ?? [];
115
+ await fs.writeFile(wrapperPath, wrapperEntrySource(options.runtime !== false, toolFiles, systemPromptFile), "utf-8");
116
+ const plugins = options.plugins ?? [];
117
117
  let result;
118
118
  try {
119
119
  result = await withPreservedNodeEnv(() => build({
120
120
  root: cwd,
121
121
  logLevel: "silent",
122
- ...opts.configFile === false && { configFile: false },
122
+ ...options.configFile === false && { configFile: false },
123
123
  ...plugins.length > 0 && { plugins },
124
124
  ssr: { noExternal: true },
125
125
  build: {
@@ -130,7 +130,7 @@ async function buildWorker(cwd, opts = {}) {
130
130
  fileName: "worker"
131
131
  },
132
132
  target: "node20",
133
- minify: opts.minify ? "oxc" : false,
133
+ minify: options.minify ? "oxc" : false,
134
134
  write: false,
135
135
  rollupOptions: { output: {
136
136
  entryFileNames: "[name].js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alexkroman1/aai-cli",
3
- "version": "14.0.0",
3
+ "version": "15.0.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -38,6 +38,11 @@
38
38
  "@dev/source": "./src/project-config.ts",
39
39
  "types": "./dist/project-config.d.ts",
40
40
  "import": "./dist/project-config.mjs"
41
+ },
42
+ "./start": {
43
+ "@dev/source": "./src/start.ts",
44
+ "types": "./dist/start.d.ts",
45
+ "import": "./dist/start.mjs"
41
46
  }
42
47
  },
43
48
  "dependencies": {
@@ -52,9 +57,9 @@
52
57
  "p-timeout": "^7.0.1",
53
58
  "vite": "^8.2.2",
54
59
  "zod": "^4.5.4",
55
- "@alexkroman1/aai": "14.0.0",
56
- "@alexkroman1/aai-runtime": "14.0.0",
57
- "@alexkroman1/aai-ui": "14.0.0"
60
+ "@alexkroman1/aai": "15.0.0",
61
+ "@alexkroman1/aai-runtime": "15.0.0",
62
+ "@alexkroman1/aai-ui": "15.0.0"
58
63
  },
59
64
  "devDependencies": {
60
65
  "playwright": "^1.62.1",
@@ -1,43 +0,0 @@
1
- #!/usr/bin/env node
2
- import { a as errorCode } from "./_utils-D5JGcjiW.mjs";
3
- import path from "node:path";
4
- import { parseEnv } from "node:util";
5
- import fs from "node:fs/promises";
6
- //#region src/_server-common.ts
7
- /**
8
- * Build the `ctx.env` record that agent tools will see at runtime.
9
- *
10
- * Only variables explicitly declared in `.env` are included — matching
11
- * the platform sandbox behavior where `ctx.env`
12
- * contains only secrets set via `aai secret put`. This prevents agents
13
- * from accidentally depending on shell-level vars (PATH, HOME, etc.) that
14
- * won't exist in production.
15
- *
16
- * Values are resolved by merging the `.env` file with the current
17
- * environment — existing shell exports take precedence over `.env`
18
- * defaults, without mutating `process.env`.
19
- *
20
- * @param cwd - Project directory containing `.env` (optional).
21
- * @param baseEnv - Override the environment to read values from (tests only).
22
- */
23
- async function resolveServerEnv(cwd, baseEnv) {
24
- let fileEntries = {};
25
- if (cwd) {
26
- let content = null;
27
- try {
28
- content = await fs.readFile(path.join(cwd, ".env"), "utf-8");
29
- } catch (err) {
30
- if (errorCode(err) !== "ENOENT") throw err;
31
- }
32
- if (content !== null) fileEntries = parseEnv(content);
33
- }
34
- const source = baseEnv ?? process.env;
35
- const env = {};
36
- for (const [key, fileVal] of Object.entries(fileEntries)) {
37
- const val = source[key] ?? fileVal;
38
- if (val !== void 0 && val !== "") env[key] = val;
39
- }
40
- return env;
41
- }
42
- //#endregion
43
- export { resolveServerEnv as t };