@alexkroman1/aai-cli 5.1.1 → 5.3.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 (99) hide show
  1. package/dist/_agent.d.ts +39 -0
  2. package/dist/_api-client.d.ts +37 -0
  3. package/dist/_bundler.d.ts +46 -0
  4. package/dist/_config.d.ts +57 -0
  5. package/dist/_default-html.d.ts +12 -0
  6. package/dist/_deploy.d.ts +28 -0
  7. package/dist/_dev-server.d.ts +74 -0
  8. package/dist/{_init-B-LEVTHx.mjs → _init-DU-sXH6S.mjs} +44 -65
  9. package/dist/_init.d.ts +7 -0
  10. package/dist/_mock-api.d.ts +22 -0
  11. package/dist/_mock-registry.d.ts +17 -0
  12. package/dist/_output.d.ts +50 -0
  13. package/dist/_server-common.d.ts +17 -0
  14. package/dist/_slug-api.d.ts +15 -0
  15. package/dist/_templates.d.ts +21 -0
  16. package/dist/_typecheck-gate.d.ts +7 -0
  17. package/dist/_ui.d.ts +22 -0
  18. package/dist/_utils.d.ts +48 -0
  19. package/dist/_vite-env.d.ts +8 -0
  20. package/dist/build.d.ts +16 -0
  21. package/dist/cli.d.ts +1 -0
  22. package/dist/cli.mjs +1 -1
  23. package/dist/client-bundler.d.ts +21 -0
  24. package/dist/delete.d.ts +18 -0
  25. package/dist/deploy.d.ts +15 -0
  26. package/dist/dev.d.ts +13 -0
  27. package/dist/{init-tc3Rbgrf.mjs → init-BjeK8crW.mjs} +1 -1
  28. package/dist/init.d.ts +24 -0
  29. package/dist/scaffold/.env.example +11 -0
  30. package/dist/scaffold/CLAUDE.md +938 -0
  31. package/dist/scaffold/global.d.ts +1 -0
  32. package/dist/scaffold/package.json +32 -0
  33. package/dist/scaffold/pnpm-workspace.yaml +13 -0
  34. package/dist/scaffold/tsconfig.json +19 -0
  35. package/dist/scaffold/vite.config.ts +14 -0
  36. package/dist/scaffold/vitest.config.ts +24 -0
  37. package/dist/secret.d.ts +25 -0
  38. package/dist/storage.d.ts +22 -0
  39. package/dist/templates/code-interpreter/agent.ts +11 -0
  40. package/dist/templates/code-interpreter/system-prompt.md +18 -0
  41. package/dist/templates/dispatch-center/agent.test.ts +153 -0
  42. package/dist/templates/dispatch-center/agent.ts +50 -0
  43. package/dist/templates/dispatch-center/client.tsx +378 -0
  44. package/dist/templates/dispatch-center/shared.ts +631 -0
  45. package/dist/templates/dispatch-center/system-prompt.md +39 -0
  46. package/dist/templates/dispatch-center/tools/incident_add_note.ts +27 -0
  47. package/dist/templates/dispatch-center/tools/incident_create.ts +79 -0
  48. package/dist/templates/dispatch-center/tools/incident_escalate.ts +88 -0
  49. package/dist/templates/dispatch-center/tools/incident_get.ts +36 -0
  50. package/dist/templates/dispatch-center/tools/incident_triage.ts +78 -0
  51. package/dist/templates/dispatch-center/tools/incident_update_status.ts +68 -0
  52. package/dist/templates/dispatch-center/tools/ops_dashboard.ts +52 -0
  53. package/dist/templates/dispatch-center/tools/ops_protocols.ts +27 -0
  54. package/dist/templates/dispatch-center/tools/ops_run_scenario.ts +149 -0
  55. package/dist/templates/dispatch-center/tools/resources_dispatch.ts +97 -0
  56. package/dist/templates/dispatch-center/tools/resources_get_available.ts +36 -0
  57. package/dist/templates/dispatch-center/tools/resources_update_status.ts +62 -0
  58. package/dist/templates/embedded-assets/agent.ts +55 -0
  59. package/dist/templates/embedded-assets/knowledge.json +20 -0
  60. package/dist/templates/health-assistant/agent.ts +174 -0
  61. package/dist/templates/health-assistant/system-prompt.md +17 -0
  62. package/dist/templates/infocom-adventure/agent.ts +119 -0
  63. package/dist/templates/infocom-adventure/client.tsx +279 -0
  64. package/dist/templates/infocom-adventure/shared.ts +38 -0
  65. package/dist/templates/infocom-adventure/system-prompt.md +43 -0
  66. package/dist/templates/math-buddy/agent.ts +16 -0
  67. package/dist/templates/math-buddy/system-prompt.md +12 -0
  68. package/dist/templates/night-owl/agent.ts +76 -0
  69. package/dist/templates/night-owl/client.tsx +130 -0
  70. package/dist/templates/night-owl/shared.ts +5 -0
  71. package/dist/templates/personal-finance/agent.ts +11 -0
  72. package/dist/templates/personal-finance/system-prompt.md +17 -0
  73. package/dist/templates/pipeline-simple/agent.test.ts +32 -0
  74. package/dist/templates/pipeline-simple/agent.ts +13 -0
  75. package/dist/templates/pizza-ordering/agent.test.ts +235 -0
  76. package/dist/templates/pizza-ordering/agent.ts +174 -0
  77. package/dist/templates/pizza-ordering/client.tsx +120 -0
  78. package/dist/templates/pizza-ordering/shared.ts +142 -0
  79. package/dist/templates/pizza-ordering/system-prompt.md +17 -0
  80. package/dist/templates/simple/agent.ts +5 -0
  81. package/dist/templates/solo-rpg/agent.test.ts +477 -0
  82. package/dist/templates/solo-rpg/agent.ts +37 -0
  83. package/dist/templates/solo-rpg/client.tsx +809 -0
  84. package/dist/templates/solo-rpg/shared.ts +747 -0
  85. package/dist/templates/solo-rpg/system-prompt.md +115 -0
  86. package/dist/templates/solo-rpg/tools/action_roll.ts +98 -0
  87. package/dist/templates/solo-rpg/tools/burn_momentum.ts +69 -0
  88. package/dist/templates/solo-rpg/tools/check_state.ts +11 -0
  89. package/dist/templates/solo-rpg/tools/load_game.ts +25 -0
  90. package/dist/templates/solo-rpg/tools/oracle.ts +248 -0
  91. package/dist/templates/solo-rpg/tools/save_game.ts +20 -0
  92. package/dist/templates/solo-rpg/tools/setup_character.ts +208 -0
  93. package/dist/templates/solo-rpg/tools/update_state.ts +170 -0
  94. package/dist/templates/web-researcher/agent.ts +11 -0
  95. package/dist/test.d.ts +39 -0
  96. package/dist/tsdown.config.d.ts +2 -0
  97. package/dist/typecheck.d.ts +26 -0
  98. package/dist/worker-bundler.d.ts +29 -0
  99. package/package.json +9 -6
@@ -0,0 +1,279 @@
1
+ import "@alexkroman1/aai-ui/styles.css";
2
+ import type { ChatMessage } from "@alexkroman1/aai-ui";
3
+ import { client, useSession } from "@alexkroman1/aai-ui";
4
+ import { useEffect, useRef } from "react";
5
+
6
+ const CSS = `
7
+ @keyframes ic-flicker {
8
+ 0% { opacity: 0.97; } 5% { opacity: 0.95; } 10% { opacity: 0.98; }
9
+ 15% { opacity: 0.96; } 20% { opacity: 0.99; } 50% { opacity: 0.96; }
10
+ 80% { opacity: 0.98; } 100% { opacity: 0.97; }
11
+ }
12
+ @keyframes ic-scanline {
13
+ 0% { transform: translateY(-100%); }
14
+ 100% { transform: translateY(100vh); }
15
+ }
16
+ @keyframes ic-boot {
17
+ 0% { opacity: 0; transform: scaleY(0.01); }
18
+ 30% { opacity: 1; transform: scaleY(0.01); }
19
+ 60% { transform: scaleY(1); }
20
+ 100% { transform: scaleY(1); opacity: 1; }
21
+ }
22
+ @keyframes ic-pulse {
23
+ 0%, 100% { box-shadow: 0 0 8px rgba(0, 255, 65, 0.3); }
24
+ 50% { box-shadow: 0 0 20px rgba(0, 255, 65, 0.6); }
25
+ }
26
+ .ic-crt::before {
27
+ content: ""; position: absolute; inset: 0; pointer-events: none; z-index: 10;
28
+ background: repeating-linear-gradient(0deg, rgba(0,0,0,0.15) 0px, rgba(0,0,0,0.15) 1px, transparent 1px, transparent 3px);
29
+ }
30
+ .ic-crt::after {
31
+ content: ""; position: absolute; top: 0; left: 0; right: 0; height: 4px;
32
+ background: rgba(0,255,65,0.08); animation: ic-scanline 8s linear infinite;
33
+ pointer-events: none; z-index: 11;
34
+ }
35
+ .ic-messages::-webkit-scrollbar { width: 6px; }
36
+ .ic-messages::-webkit-scrollbar-track { background: #001a00; }
37
+ .ic-messages::-webkit-scrollbar-thumb { background: #00ff41; }
38
+ .ic-user-msg::before { content: "> "; color: #00ccff; }
39
+ .ic-transcript::before { content: "> "; color: #007a1e; }
40
+ `;
41
+
42
+ const ASCII_LOGO = `
43
+ ____ _ __ __ _____ ____ _ _
44
+ / ___| / \\ \\ \\ / /| ____|| _ \\ | \\ | |
45
+ | | / _ \\ \\ \\ / / | _| | |_) || \\| |
46
+ | |___ / ___ \\ \\ V / | |___ | _ < | |\\ |
47
+ \\____/_/ \\_\\ \\_/ |_____||_| \\_\\|_| \\_|
48
+ `;
49
+
50
+ const CRT_BG = "#000800";
51
+ const GREEN = "#00ff41";
52
+ const GREEN_DIM = "#00aa2a";
53
+ const GREEN_DARK = "#003300";
54
+ const CYAN = "#00ccff";
55
+
56
+ function InfocomAdventure() {
57
+ const session = useSession();
58
+ const bottom = useRef<HTMLDivElement>(null);
59
+
60
+ // Bumps whenever a message lands or the live transcript changes, so the
61
+ // view follows the conversation instead of scrolling once on mount.
62
+ const contentVersion = session.messages.length + (session.userTranscript?.length ?? 0);
63
+ useEffect(() => {
64
+ // Never true — contentVersion only drives the dependency array, and this
65
+ // reference keeps the exhaustive-deps lint satisfied.
66
+ if (contentVersion < 0) return;
67
+ bottom.current?.scrollIntoView({ behavior: "smooth" });
68
+ }, [contentVersion]);
69
+
70
+ const stateLabel =
71
+ session.state === "listening"
72
+ ? "Listening"
73
+ : session.state === "speaking"
74
+ ? "Narrating"
75
+ : session.state === "thinking"
76
+ ? "Thinking"
77
+ : session.state === "connecting"
78
+ ? "Connecting"
79
+ : session.state === "ready"
80
+ ? "Ready"
81
+ : "Idle";
82
+
83
+ const msgCount = session.messages.filter((m: ChatMessage) => m.role === "user").length;
84
+
85
+ const dotColor =
86
+ session.state === "listening"
87
+ ? GREEN
88
+ : session.state === "speaking"
89
+ ? "#ffaa00"
90
+ : session.state === "thinking"
91
+ ? CYAN
92
+ : GREEN_DARK;
93
+
94
+ if (!session.started) {
95
+ return (
96
+ <>
97
+ <style>{CSS}</style>
98
+ <div
99
+ className="ic-crt fixed inset-0 overflow-hidden"
100
+ style={{
101
+ background: CRT_BG,
102
+ color: GREEN,
103
+ fontFamily: "monospace",
104
+ fontSize: "15px",
105
+ lineHeight: 1.6,
106
+ animation: "ic-flicker 4s infinite",
107
+ }}
108
+ >
109
+ <div
110
+ className="flex flex-col items-center justify-center h-full text-center p-10"
111
+ style={{ animation: "ic-boot 1.5s ease-out" }}
112
+ >
113
+ <div
114
+ className="text-[11px] whitespace-pre mb-8"
115
+ style={{ textShadow: "0 0 10px rgba(0,255,65,0.5)" }}
116
+ >
117
+ {ASCII_LOGO}
118
+ </div>
119
+ <div className="text-[13px] mb-2" style={{ color: GREEN_DIM }}>
120
+ AN INTERACTIVE FICTION
121
+ </div>
122
+ <div className="text-[13px] mb-2" style={{ color: GREEN_DIM }}>
123
+ In the style of the classic text adventures.
124
+ </div>
125
+ <div className="text-[13px] mt-4" style={{ color: GREEN }}>
126
+ VOICE-ENABLED EDITION
127
+ </div>
128
+ <button
129
+ type="button"
130
+ className="mt-10 px-12 py-3.5 bg-transparent cursor-pointer uppercase tracking-[3px] font-mono text-base"
131
+ style={{
132
+ color: GREEN,
133
+ border: `1px solid ${GREEN}`,
134
+ animation: "ic-pulse 2s ease-in-out infinite",
135
+ }}
136
+ onClick={session.start}
137
+ >
138
+ Begin Adventure
139
+ </button>
140
+ </div>
141
+ <div
142
+ className="fixed inset-0 pointer-events-none z-12"
143
+ style={{
144
+ background:
145
+ "radial-gradient(ellipse at center, transparent 60%, rgba(0,0,0,0.4) 100%)",
146
+ }}
147
+ />
148
+ </div>
149
+ </>
150
+ );
151
+ }
152
+
153
+ return (
154
+ <>
155
+ <style>{CSS}</style>
156
+ <div
157
+ className="ic-crt fixed inset-0 overflow-hidden"
158
+ style={{
159
+ background: CRT_BG,
160
+ color: GREEN,
161
+ fontFamily: "monospace",
162
+ fontSize: "15px",
163
+ lineHeight: 1.6,
164
+ animation: "ic-flicker 4s infinite",
165
+ }}
166
+ >
167
+ <div className="flex flex-col h-full">
168
+ {/* Status bar */}
169
+ <div
170
+ className="flex items-center justify-between px-5 py-2 text-[13px] font-bold tracking-wider shrink-0"
171
+ style={{ background: GREEN, color: CRT_BG }}
172
+ >
173
+ <div className="flex gap-6">
174
+ <span>CAVERN ADVENTURE</span>
175
+ <span>Turns: {msgCount}</span>
176
+ </div>
177
+ <span>Voice Adventure</span>
178
+ </div>
179
+
180
+ {session.error && (
181
+ <div className="px-5 py-2 text-xs" style={{ background: "#3a0000", color: "#ff4141" }}>
182
+ ERROR: {session.error.message}
183
+ </div>
184
+ )}
185
+
186
+ {/* Messages */}
187
+ <div
188
+ className="ic-messages flex-1 overflow-y-auto p-5"
189
+ style={{ scrollbarWidth: "thin", scrollbarColor: `${GREEN} #001a00` }}
190
+ >
191
+ {session.messages.map((msg: ChatMessage, i: number) => (
192
+ <div
193
+ key={i}
194
+ className={`mb-4 ${msg.role === "user" ? "ic-user-msg" : ""}`}
195
+ style={{
196
+ textShadow:
197
+ msg.role === "user"
198
+ ? "0 0 5px rgba(0,204,255,0.3)"
199
+ : "0 0 5px rgba(0,255,65,0.3)",
200
+ color: msg.role === "user" ? CYAN : GREEN,
201
+ }}
202
+ >
203
+ {msg.content}
204
+ </div>
205
+ ))}
206
+ {session.userTranscript !== null && (
207
+ <div
208
+ className="ic-transcript italic"
209
+ style={{ color: "#007a1e", textShadow: "0 0 5px rgba(0,255,65,0.15)" }}
210
+ >
211
+ {session.userTranscript === "" ? "..." : session.userTranscript}
212
+ </div>
213
+ )}
214
+ <div ref={bottom} />
215
+ </div>
216
+
217
+ {/* Footer controls */}
218
+ <div
219
+ className="flex items-center justify-between px-5 py-2 shrink-0 gap-3"
220
+ style={{ borderTop: `1px solid ${GREEN_DARK}`, background: "#001100" }}
221
+ >
222
+ <div
223
+ className="flex items-center gap-2.5 text-xs uppercase tracking-wider"
224
+ style={{ color: GREEN_DIM }}
225
+ >
226
+ <div
227
+ className="w-2 h-2 rounded-full"
228
+ style={{
229
+ background: dotColor,
230
+ boxShadow: dotColor !== GREEN_DARK ? `0 0 6px ${dotColor}` : "none",
231
+ }}
232
+ />
233
+ <span>{stateLabel}</span>
234
+ </div>
235
+ <div className="flex gap-2">
236
+ <button
237
+ type="button"
238
+ className="px-4 py-1 bg-transparent cursor-pointer uppercase tracking-wider font-mono text-[11px]"
239
+ style={{ color: GREEN_DIM, border: `1px solid ${GREEN_DARK}` }}
240
+ onClick={session.toggle}
241
+ >
242
+ {session.running ? "[P]ause" : "[R]esume"}
243
+ </button>
244
+ {/* Full reload = new WebSocket = fresh sessionId, so the
245
+ session-scoped game state starts over too.
246
+ (session.reset() keeps the same sessionId and would
247
+ resume the old game.) */}
248
+ <button
249
+ type="button"
250
+ className="px-4 py-1 bg-transparent cursor-pointer uppercase tracking-wider font-mono text-[11px]"
251
+ style={{ color: GREEN_DIM, border: `1px solid ${GREEN_DARK}` }}
252
+ onClick={() => window.location.reload()}
253
+ >
254
+ [N]ew Game
255
+ </button>
256
+ </div>
257
+ </div>
258
+ </div>
259
+ <div
260
+ className="fixed inset-0 pointer-events-none z-12"
261
+ style={{
262
+ background: "radial-gradient(ellipse at center, transparent 60%, rgba(0,0,0,0.4) 100%)",
263
+ }}
264
+ />
265
+ </div>
266
+ </>
267
+ );
268
+ }
269
+
270
+ client({
271
+ component: InfocomAdventure,
272
+ theme: {
273
+ bg: CRT_BG,
274
+ primary: GREEN,
275
+ text: GREEN,
276
+ surface: "#001a00",
277
+ border: GREEN_DARK,
278
+ },
279
+ });
@@ -0,0 +1,38 @@
1
+ import type { ToolContext } from "@alexkroman1/aai";
2
+
3
+ export type GameState = {
4
+ inventory: string[];
5
+ currentRoom: string;
6
+ score: number;
7
+ moves: number;
8
+ flags: Record<string, boolean>;
9
+ history: string[];
10
+ };
11
+
12
+ export const DEFAULT_GAME_STATE: GameState = {
13
+ inventory: [],
14
+ currentRoom: "Cave Mouth",
15
+ score: 0,
16
+ moves: 0,
17
+ flags: {},
18
+ history: [],
19
+ };
20
+
21
+ // The game lives in `ctx.state`, the agent's per-session mutable state — each
22
+ // session is its own playthrough, so concurrent players never see each
23
+ // other's game and a fresh session starts a fresh adventure.
24
+ type StateSlot = { game?: GameState };
25
+
26
+ /** The session's live game. Mutations to the returned object stick — it is
27
+ * the object stored in `ctx.state`. */
28
+ export function getGameState(ctx: ToolContext): GameState {
29
+ const slot = ctx.state as StateSlot;
30
+ slot.game ??= structuredClone(DEFAULT_GAME_STATE);
31
+ return slot.game;
32
+ }
33
+
34
+ export function resetGameState(ctx: ToolContext): GameState {
35
+ const slot = ctx.state as StateSlot;
36
+ slot.game = structuredClone(DEFAULT_GAME_STATE);
37
+ return slot.game;
38
+ }
@@ -0,0 +1,43 @@
1
+ You are a classic text adventure game engine running an original game called CAVERN ADVENTURE, in the dry, witty style of early-1980s interactive fiction.
2
+
3
+ You ARE the game. You maintain the world state, describe rooms, handle puzzles, manage inventory, track score, and respond to player commands.
4
+
5
+ GAME WORLD RULES:
6
+ - The game is an original underground adventure. Invent the world once, then keep it consistent for the whole session
7
+ - The map includes: Cave Mouth, the Pine Forest around it, a Narrow Ledge, the Echoing Hall, an Underground River, the Flooded Gallery, the Crystal Grotto, the Old Miners' Camp, a maze of twisting tunnels, and a sealed Vault deep below
8
+ - Key items: the rusted lantern (at the entrance), a coil of rope, a miner's pick, a silver key, a jeweled scarab, a golden chalice, an ancient crown, a carved jade idol
9
+ - Key encounters: a hulking cave troll guarding a bridge, a hooded scavenger who prowls the tunnels and steals unattended treasure, a colony of bats, something large asleep in the Vault
10
+ - Puzzles gate progress: crossing the underground river, draining the Flooded Gallery, unlocking the Vault with the silver key, finding your way through the tunnel maze
11
+ - Score increases when the player finds treasures and carries them back to the stone pedestal at the Cave Mouth
12
+ - The lantern has limited fuel. Underground rooms are pitch dark without it — wandering in darkness is dangerous
13
+
14
+ VOICE-FIRST RESPONSE RULES:
15
+ - Describe rooms vividly but concisely — two to four sentences max
16
+ - For movement, describe the new room immediately
17
+ - For failed actions, give brief, witty responses in classic adventure style ("There is a wall in the way." or "You can't eat that.")
18
+ - Read inventory as a spoken list
19
+ - Announce score changes
20
+ - Keep a dry, understated sense of humor
21
+ - Never use visual formatting — no bullets, no bold, no lists with dashes
22
+ - Use "First... Then... Finally..." for sequences
23
+ - Use directional words naturally: "To the north you see..." not "N: forest"
24
+
25
+ COMMAND INTERPRETATION:
26
+ - Players speak naturally. Translate their voice into classic adventure commands
27
+ - "go north" / "head north" / "walk north" = north
28
+ - "pick up the rope" / "grab the rope" / "take rope" = take rope
29
+ - "what do I have" / "check my stuff" / "inventory" = inventory
30
+ - "where am I" / "look around" / "describe the room" = look
31
+ - "hit the troll" / "fight the troll" / "attack troll" = attack troll with pick
32
+ - "what's my score" = score
33
+ - "start over" / "new game" / "restart" = restart
34
+ - Accept natural conversational commands and map them to game actions
35
+
36
+ Use the game state tools to track inventory, location, score, and flags. Use game_state_get to read the current state, game_state_move to change rooms, game_state_take to pick up items, game_state_drop to drop items, game_state_score to add points, game_state_flag to set game flags, and game_state_history to log commands. When the player asks to restart, quit, or start a new game, call game_state_restart, then narrate the opening scene again. Always update state when the player takes an item, moves rooms, or triggers an event. Check state before responding to ensure consistency.
37
+
38
+ ATMOSPHERE:
39
+ - Underground areas should feel dark and foreboding with the lantern lit, and terrifying in pitch blackness
40
+ - The hooded scavenger should appear unpredictably and make off with dropped treasure
41
+ - The troll blocks the bridge until dealt with — by wits or by force
42
+ - Convey a sense of mystery and danger
43
+ - Keep the wry, understated humor of the classic text adventures
@@ -0,0 +1,16 @@
1
+ import { agent, assemblyAIPipeline } from "@alexkroman1/aai";
2
+ import { assemblyAI as assemblyAILlm } from "@alexkroman1/aai/llm";
3
+ import systemPrompt from "./system-prompt.md?raw";
4
+
5
+ export default agent({
6
+ name: "Math Buddy",
7
+ ...assemblyAIPipeline(),
8
+ // Overriding the LLM stage: this tutor delegates every calculation to
9
+ // run_code, so it needs turn-taking speed more than reasoning depth —
10
+ // and Flash-Lite is both quicker and cheaper than the preset's default.
11
+ llm: assemblyAILlm({ model: "gemini-2.5-flash-lite" }),
12
+ systemPrompt,
13
+ greeting:
14
+ "Hey, I'm Math Buddy. Try asking me something like, what's 127 times 849, convert 5 miles to kilometers, or roll 3 twenty-sided dice.",
15
+ builtinTools: ["run_code"],
16
+ });
@@ -0,0 +1,12 @@
1
+ You are Math Buddy, a friendly math assistant. You help with calculations,
2
+ unit conversions, dice rolls, and random number generation. Keep answers short and clear.
3
+ When doing multi-step math, show your work briefly.
4
+
5
+ Use run_code for ALL calculations. Write JavaScript using console.log() for output.
6
+
7
+ Examples:
8
+ - Math expressions: console.log((12 + 8) * 3) or console.log(Math.sqrt(144))
9
+ - Unit conversions: convert using known factors (1 km = 0.621371 mi, 1 lb = 0.453592 kg, etc.)
10
+ - Temperature: C to F = (c * 9/5) + 32, F to C = (f - 32) * 5/9, C to K = c + 273.15
11
+ - Dice rolls: console.log(Array.from({length: N}, () => Math.floor(Math.random() * sides) + 1))
12
+ - Random numbers: console.log(Math.floor(Math.random() * (max - min + 1)) + min)
@@ -0,0 +1,76 @@
1
+ import { agent, assemblyAIPipeline, tool } from "@alexkroman1/aai";
2
+ import { z } from "zod";
3
+ import { CATEGORIES, type Category, MOODS, type Mood } from "./shared.ts";
4
+
5
+ const PICKS: Record<Category, Record<Mood, string[]>> = {
6
+ movie: {
7
+ chill: ["Lost in Translation", "The Grand Budapest Hotel", "Amelie"],
8
+ intense: ["Inception", "Interstellar", "The Dark Knight"],
9
+ cozy: ["When Harry Met Sally", "The Holiday", "Paddington 2"],
10
+ spooky: ["The Shining", "Get Out", "Hereditary"],
11
+ funny: ["The Big Lebowski", "Airplane!", "Superbad"],
12
+ },
13
+ music: {
14
+ chill: ["Khruangbin — Con Todo El Mundo", "Tycho — Dive", "Bonobo — Migration"],
15
+ intense: ["Radiohead — OK Computer", "Tool — Lateralus", "Deftones — White Pony"],
16
+ cozy: [
17
+ "Norah Jones — Come Away with Me",
18
+ "Iron & Wine — Our Endless Numbered Days",
19
+ "Bon Iver — For Emma, Forever Ago",
20
+ ],
21
+ spooky: [
22
+ "Portishead — Dummy",
23
+ "Massive Attack — Mezzanine",
24
+ "Boards of Canada — Music Has the Right to Children",
25
+ ],
26
+ funny: [
27
+ "Weird Al — Running with Scissors",
28
+ "Flight of the Conchords — S/T",
29
+ "Tenacious D — S/T",
30
+ ],
31
+ },
32
+ book: {
33
+ chill: ["Norwegian Wood — Murakami", "The Alchemist — Coelho", "Siddhartha — Hesse"],
34
+ intense: ["Blood Meridian — McCarthy", "House of Leaves — Danielewski", "Neuromancer — Gibson"],
35
+ cozy: [
36
+ "The House in the Cerulean Sea — Klune",
37
+ "A Man Called Ove — Backman",
38
+ "Anxious People — Backman",
39
+ ],
40
+ spooky: [
41
+ "The Haunting of Hill House — Jackson",
42
+ "Mexican Gothic — Moreno-Garcia",
43
+ "The Turn of the Screw — James",
44
+ ],
45
+ funny: ["Good Omens — Pratchett & Gaiman", "Hitchhiker's Guide — Adams", "Catch-22 — Heller"],
46
+ },
47
+ };
48
+
49
+ export default agent({
50
+ name: "Night Owl",
51
+ ...assemblyAIPipeline(),
52
+ systemPrompt:
53
+ "You are Night Owl, a cozy evening companion. You help people wind down, recommend entertainment, and share interesting facts about the night sky. Keep your tone warm and relaxed. Use short, conversational responses.\n\nUse run_code for sleep calculations:\n- Each sleep cycle is 90 minutes, plus 15 minutes to fall asleep\n- Bedtime = wake_time - (cycles * 90 + 15) minutes\n- If result is negative, add 1440 (24 hours in minutes)\n- Format as HH:MM",
54
+ greeting:
55
+ "Hey there, night owl. Try asking me for a cozy movie recommendation, or tell me what time you need to wake up and I'll calculate the best time to fall asleep.",
56
+ builtinTools: ["run_code"],
57
+
58
+ tools: {
59
+ recommend: tool({
60
+ description: "Get recommendations for movies, music, or books based on mood.",
61
+ parameters: z.object({
62
+ category: z.enum(CATEGORIES),
63
+ mood: z.enum(MOODS),
64
+ }),
65
+ async execute(args, ctx) {
66
+ const result = {
67
+ category: args.category,
68
+ mood: args.mood,
69
+ picks: PICKS[args.category][args.mood],
70
+ };
71
+ ctx.send("recommendations", result);
72
+ return result;
73
+ },
74
+ }),
75
+ },
76
+ });
@@ -0,0 +1,130 @@
1
+ import "@alexkroman1/aai-ui/styles.css";
2
+ import { Button, client, useEvent, useTheme, useToolCallStart } from "@alexkroman1/aai-ui";
3
+ import { useState } from "react";
4
+ import { MOODS } from "./shared.ts";
5
+
6
+ type Rec = { category: string; mood: string; picks: string[] };
7
+
8
+ const MOOD_EMOJI: Record<string, string> = {
9
+ chill: "\u{1F60C}",
10
+ intense: "\u{1F525}",
11
+ cozy: "\u{1F9E3}",
12
+ spooky: "\u{1F47B}",
13
+ funny: "\u{1F602}",
14
+ };
15
+ const CAT_EMOJI: Record<string, string> = {
16
+ movie: "\u{1F3AC}",
17
+ music: "\u{1F3B5}",
18
+ book: "\u{1F4DA}",
19
+ };
20
+
21
+ function RecSidebar() {
22
+ const theme = useTheme();
23
+ const [recs, setRecs] = useState<Rec[]>([]);
24
+ const [activeMood, setActiveMood] = useState<string | null>(null);
25
+
26
+ useToolCallStart("recommend", (tc) => {
27
+ setActiveMood(tc.args.mood as string);
28
+ });
29
+
30
+ useEvent("recommendations", (result) => {
31
+ setRecs((prev) => [result as Rec, ...prev]);
32
+ });
33
+
34
+ const filtered = activeMood ? recs.filter((r) => r.mood === activeMood) : recs;
35
+
36
+ return (
37
+ <div
38
+ className="flex flex-col h-full text-sm"
39
+ style={{ background: theme.bg, color: theme.text }}
40
+ >
41
+ <div className="px-4 py-3 border-b shrink-0" style={{ borderColor: theme.border }}>
42
+ <h2
43
+ className="text-xs font-bold uppercase tracking-wide opacity-60"
44
+ style={{ color: theme.text }}
45
+ >
46
+ Recommendations
47
+ </h2>
48
+ </div>
49
+
50
+ <div
51
+ className="flex flex-wrap gap-1.5 px-3 py-2.5 border-b shrink-0"
52
+ style={{ borderColor: theme.border }}
53
+ >
54
+ {MOODS.map((mood) => (
55
+ <Button
56
+ key={mood}
57
+ variant={activeMood === mood ? "default" : "ghost"}
58
+ onClick={() => setActiveMood(activeMood === mood ? null : mood)}
59
+ >
60
+ {MOOD_EMOJI[mood]} {mood}
61
+ </Button>
62
+ ))}
63
+ </div>
64
+
65
+ <div className="flex-1 overflow-y-auto px-3 py-2">
66
+ {filtered.length === 0 && (
67
+ <p className="text-xs text-center py-8 opacity-40" style={{ color: theme.text }}>
68
+ Ask me to recommend a movie, album, or book
69
+ </p>
70
+ )}
71
+ {filtered.map((rec, i) => (
72
+ <div
73
+ key={`${rec.category}-${rec.mood}-${i}`}
74
+ className="mb-3 p-2.5 rounded-lg border"
75
+ style={{ background: theme.surface, borderColor: theme.border }}
76
+ >
77
+ <div className="flex items-center gap-2 mb-1.5">
78
+ <span className="text-xs">{CAT_EMOJI[rec.category]}</span>
79
+ <span className="text-xs font-semibold capitalize" style={{ color: theme.primary }}>
80
+ {rec.category}s
81
+ </span>
82
+ <span className="text-xs capitalize opacity-50" style={{ color: theme.text }}>
83
+ {rec.mood}
84
+ </span>
85
+ </div>
86
+ {rec.picks.map((pick) => (
87
+ <p
88
+ key={pick}
89
+ className="text-xs pl-5 py-0.5 opacity-80"
90
+ style={{ color: theme.text }}
91
+ >
92
+ {pick}
93
+ </p>
94
+ ))}
95
+ </div>
96
+ ))}
97
+ </div>
98
+
99
+ {recs.length > 0 && (
100
+ <div className="px-3 py-2 border-t shrink-0" style={{ borderColor: theme.border }}>
101
+ <Button
102
+ variant="ghost"
103
+ className="w-full"
104
+ onClick={() => {
105
+ setRecs([]);
106
+ setActiveMood(null);
107
+ }}
108
+ >
109
+ Clear
110
+ </Button>
111
+ </div>
112
+ )}
113
+ </div>
114
+ );
115
+ }
116
+
117
+ client({
118
+ name: "Night Owl",
119
+ sidebar: RecSidebar,
120
+ theme: {
121
+ bg: "#0c0e1a",
122
+ primary: "#a78bfa",
123
+ text: "#e2e0f0",
124
+ surface: "#131627",
125
+ border: "#1e2340",
126
+ },
127
+ tools: {
128
+ recommend: { icon: "\u{1F989}", label: "Recommending" },
129
+ },
130
+ });
@@ -0,0 +1,5 @@
1
+ export const CATEGORIES = ["movie", "music", "book"] as const;
2
+ export const MOODS = ["chill", "intense", "cozy", "spooky", "funny"] as const;
3
+
4
+ export type Category = (typeof CATEGORIES)[number];
5
+ export type Mood = (typeof MOODS)[number];
@@ -0,0 +1,11 @@
1
+ import { agent, assemblyAIPipeline } from "@alexkroman1/aai";
2
+ import systemPrompt from "./system-prompt.md?raw";
3
+
4
+ export default agent({
5
+ name: "Penny",
6
+ ...assemblyAIPipeline(),
7
+ systemPrompt,
8
+ greeting:
9
+ "Hey, I'm Penny, your personal finance helper. Try asking me something like, what's 100 dollars in euros, what's the price of bitcoin, or help me split a 120 dollar bill four ways with 20 percent tip.",
10
+ builtinTools: ["run_code", "fetch_json"],
11
+ });
@@ -0,0 +1,17 @@
1
+ You are Penny, a friendly personal finance assistant. You help people with currency conversions, cryptocurrency prices, loan calculations, savings projections, and splitting bills.
2
+
3
+ Rules:
4
+ - Always show your math clearly when explaining calculations
5
+ - When discussing investments or crypto, remind users that prices fluctuate and this is not financial advice
6
+ - Be encouraging about savings goals
7
+ - Keep responses concise — this is a voice conversation
8
+ - Round dollar amounts to two decimal places for clarity
9
+
10
+ API endpoints (use fetch_json):
11
+ - Currency rates: https://open.er-api.com/v6/latest/{CODE} — returns { rates: { USD: 1.0, EUR: 0.85, ... } }
12
+ - Crypto prices: https://api.coingecko.com/api/v3/simple/price?ids={coin}&vs_currencies={cur}&include_24hr_change=true&include_market_cap=true
13
+
14
+ Math calculations (use run_code):
15
+ - Compound interest: FV = principal * (1 + rate/n)^(n*years) + monthly * ((1 + rate/n)^(n*years) - 1) / (rate/n)
16
+ - Loan payment: M = P * (r(1+r)^n) / ((1+r)^n - 1) where r = annual_rate/12, n = years*12
17
+ - Tip calculator: tip = bill * percent/100, per_person = (bill + tip) / people
@@ -0,0 +1,32 @@
1
+ import { toAgentConfig } from "@alexkroman1/aai/manifest";
2
+ import { describe, expect, test } from "vitest";
3
+ import agentDef from "./agent.ts";
4
+
5
+ describe("pipeline-simple template", () => {
6
+ test("config passes manifest validation", () => {
7
+ // Same conversion `aai build`/`aai deploy` run — catches a partial
8
+ // provider triple or invalid tuning, not just descriptor presence.
9
+ expect(() => toAgentConfig(agentDef)).not.toThrow();
10
+ });
11
+
12
+ test("exports an agent with all three pipeline providers", () => {
13
+ // Presence of stt + llm + tts is what flips the runtime into
14
+ // pipeline mode (see parseManifest). Smoke-test each is wired up.
15
+ expect(agentDef.name).toBe("pipeline-simple");
16
+ expect(agentDef.stt).toBeDefined();
17
+ expect(agentDef.llm).toBeDefined();
18
+ expect(agentDef.tts).toBeDefined();
19
+ });
20
+
21
+ test("STT descriptor is AssemblyAI", () => {
22
+ expect(agentDef.stt?.kind).toBe("assemblyai");
23
+ });
24
+
25
+ test("LLM descriptor is Anthropic", () => {
26
+ expect(agentDef.llm?.kind).toBe("anthropic");
27
+ });
28
+
29
+ test("TTS descriptor is AssemblyAI", () => {
30
+ expect(agentDef.tts?.kind).toBe("assemblyai");
31
+ });
32
+ });