@alexkroman1/aai-cli 5.4.0 → 5.5.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 (68) hide show
  1. package/dist/{_agent-D8zBb3M5.mjs → _agent-DMyOab9_.mjs} +2 -2
  2. package/dist/{_config-D3F9km8X.mjs → _config-5AEqhh-O.mjs} +2 -2
  3. package/dist/{_dev-server-cigzayJe.mjs → _dev-server-vV05Fnki.mjs} +1 -1
  4. package/dist/{_init-_G8l4lh2.mjs → _init-BZ9t_Kz-.mjs} +2 -2
  5. package/dist/{_slug-api-19R5kZ0U.mjs → _slug-api-DaqQJHk8.mjs} +1 -1
  6. package/dist/{_templates-DW9CvuDN.mjs → _templates-Bv8CR800.mjs} +1 -1
  7. package/dist/cli.mjs +28 -11
  8. package/dist/{delete-kSXFLxek.mjs → delete-DXilFBb1.mjs} +1 -1
  9. package/dist/{deploy-C0VLnnJe.mjs → deploy-1eaXcfUw.mjs} +2 -2
  10. package/dist/{dev-2oUsvgRX.mjs → dev-gVNdGFYY.mjs} +1 -1
  11. package/dist/{init-BkW9BuZF.mjs → init-DoU4_txp.mjs} +3 -3
  12. package/dist/login-C59ZHzuO.mjs +109 -0
  13. package/dist/login.d.ts +34 -0
  14. package/dist/scaffold/CLAUDE.md +69 -53
  15. package/dist/scaffold/package.json +3 -3
  16. package/dist/{secret-DOva9OGk.mjs → secret-CGAIAbUx.mjs} +1 -1
  17. package/dist/{storage-vM6HjDZZ.mjs → storage-CnhOayhm.mjs} +1 -1
  18. package/dist/templates/code-interpreter/agent.ts +1 -2
  19. package/dist/templates/dispatch-center/agent.ts +5 -5
  20. package/dist/templates/dispatch-center/client.tsx +7 -2
  21. package/dist/templates/embedded-assets/agent.ts +1 -2
  22. package/dist/templates/health-assistant/agent.ts +1 -2
  23. package/dist/templates/infocom-adventure/agent.ts +4 -6
  24. package/dist/templates/infocom-adventure/client.tsx +5 -5
  25. package/dist/templates/math-buddy/agent.ts +6 -5
  26. package/dist/templates/night-owl/agent.ts +1 -2
  27. package/dist/templates/personal-finance/agent.ts +1 -2
  28. package/dist/templates/pipeline-simple/agent.test.ts +13 -13
  29. package/dist/templates/pipeline-simple/agent.ts +5 -6
  30. package/dist/templates/pizza-ordering/agent.ts +1 -2
  31. package/dist/templates/retail/address.ts +29 -0
  32. package/dist/templates/retail/agent.test.ts +1030 -0
  33. package/dist/templates/retail/agent.ts +71 -0
  34. package/dist/templates/retail/authenticate.ts +34 -0
  35. package/dist/templates/retail/client.tsx +459 -0
  36. package/dist/templates/retail/refund.ts +19 -0
  37. package/dist/templates/retail/registry.test.ts +182 -0
  38. package/dist/templates/retail/resolve.test.ts +158 -0
  39. package/dist/templates/retail/resolve.ts +158 -0
  40. package/dist/templates/retail/seed.json +4260 -0
  41. package/dist/templates/retail/seed.test.ts +224 -0
  42. package/dist/templates/retail/shared.test.ts +192 -0
  43. package/dist/templates/retail/shared.ts +430 -0
  44. package/dist/templates/retail/store.test.ts +256 -0
  45. package/dist/templates/retail/store.ts +243 -0
  46. package/dist/templates/retail/swap.test.ts +186 -0
  47. package/dist/templates/retail/swap.ts +138 -0
  48. package/dist/templates/retail/system-prompt.md +111 -0
  49. package/dist/templates/retail/tools/cancel_pending_order.ts +82 -0
  50. package/dist/templates/retail/tools/exchange_delivered_order_items.ts +83 -0
  51. package/dist/templates/retail/tools/find_user_id_by_email.ts +32 -0
  52. package/dist/templates/retail/tools/find_user_id_by_name_zip.ts +38 -0
  53. package/dist/templates/retail/tools/get_item_details.ts +29 -0
  54. package/dist/templates/retail/tools/get_order_details.ts +41 -0
  55. package/dist/templates/retail/tools/get_product_details.ts +33 -0
  56. package/dist/templates/retail/tools/get_user_details.ts +44 -0
  57. package/dist/templates/retail/tools/list_all_product_types.ts +24 -0
  58. package/dist/templates/retail/tools/modify_pending_order_address.ts +46 -0
  59. package/dist/templates/retail/tools/modify_pending_order_items.ts +102 -0
  60. package/dist/templates/retail/tools/modify_pending_order_payment.ts +103 -0
  61. package/dist/templates/retail/tools/modify_user_address.ts +38 -0
  62. package/dist/templates/retail/tools/return_delivered_order_items.ts +101 -0
  63. package/dist/templates/retail/tools/transfer_to_human_agents.ts +22 -0
  64. package/dist/templates/simple/agent.test.ts +1 -1
  65. package/dist/templates/simple/agent.ts +2 -1
  66. package/dist/templates/solo-rpg/agent.ts +1 -2
  67. package/dist/templates/web-researcher/agent.ts +1 -2
  68. package/package.json +3 -3
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { a as serverOrigin, i as readProjectConfig, n as ensureApiKey, r as readGlobalConfig, t as approveServer } from "./_config-D3F9km8X.mjs";
2
+ import { a as readProjectConfig, i as readGlobalConfig, n as ensureApiKey, o as serverOrigin, t as approveServer } from "./_config-5AEqhh-O.mjs";
3
3
  import { existsSync } from "node:fs";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
@@ -92,4 +92,4 @@ async function getServerInfo(cwd, explicitServer) {
92
92
  };
93
93
  }
94
94
  //#endregion
95
- export { resolveDeployTarget as i, getServerInfo as n, isDevMode as r, getMonorepoRoot as t };
95
+ export { resolveServerUrl as a, resolveDeployTarget as i, getServerInfo as n, isDevMode as r, getMonorepoRoot as t };
@@ -115,10 +115,10 @@ async function ensureApiKey(configDir) {
115
115
  await trySaveApiKey(dir, config, envKey);
116
116
  return envKey;
117
117
  }
118
- if (!process.stdin.isTTY) throw new CliError("no_api_key", "No API key configured and no TTY to prompt for one.", "Set the ASSEMBLYAI_API_KEY environment variable, or run `aai` interactively once to save a key.");
118
+ if (!process.stdin.isTTY) throw new CliError("no_api_key", "No API key configured and no TTY to prompt for one.", "Set the ASSEMBLYAI_API_KEY environment variable, or run `aai login` interactively once to save a key.");
119
119
  const apiKey = unwrapCancel(await p.password({ message: "Enter your AssemblyAI API key" }), "Setup cancelled");
120
120
  await trySaveApiKey(dir, config, apiKey);
121
121
  return apiKey;
122
122
  }
123
123
  //#endregion
124
- export { serverOrigin as a, readProjectConfig as i, ensureApiKey as n, writeProjectConfig as o, readGlobalConfig as r, approveServer as t };
124
+ export { readProjectConfig as a, writeProjectConfig as c, readGlobalConfig as i, ensureApiKey as n, serverOrigin as o, getConfigDir as r, writeGlobalConfig as s, approveServer as t };
@@ -4,7 +4,7 @@ import { a as errorMessage, r as errorCode } from "./_utils-Ch0J4s6a.mjs";
4
4
  import { n as fallbackHtmlPlugin } from "./client-bundler-yiWoXrgb.mjs";
5
5
  import { buildWorker } from "./worker-bundler.mjs";
6
6
  import { n as createWorkerEvaluator } from "./_bundler-Cjaxa2wi.mjs";
7
- import { n as ensureApiKey } from "./_config-D3F9km8X.mjs";
7
+ import { n as ensureApiKey } from "./_config-5AEqhh-O.mjs";
8
8
  import { t as resolveServerEnv } from "./_server-common-CnaP_Urf.mjs";
9
9
  import { createRequire } from "node:module";
10
10
  import { existsSync } from "node:fs";
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { c as readJson, d as writeJson, s as isEexist } from "./_utils-Ch0J4s6a.mjs";
3
- import { r as isDevMode, t as getMonorepoRoot } from "./_agent-D8zBb3M5.mjs";
4
- import { REPO_URL, downloadAndMergeTemplate } from "./_templates-DW9CvuDN.mjs";
3
+ import { r as isDevMode, t as getMonorepoRoot } from "./_agent-DMyOab9_.mjs";
4
+ import { REPO_URL, downloadAndMergeTemplate } from "./_templates-Bv8CR800.mjs";
5
5
  import path from "node:path";
6
6
  import fs from "node:fs/promises";
7
7
  //#region _init.ts
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { n as getServerInfo } from "./_agent-D8zBb3M5.mjs";
2
+ import { n as getServerInfo } from "./_agent-DMyOab9_.mjs";
3
3
  import { n as apiRequest, t as HINT_NOT_DEPLOYED } from "./_api-client-MenP4-O7.mjs";
4
4
  //#region _slug-api.ts
5
5
  /**
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { a as errorMessage } from "./_utils-Ch0J4s6a.mjs";
3
- import { t as getMonorepoRoot } from "./_agent-D8zBb3M5.mjs";
3
+ import { t as getMonorepoRoot } from "./_agent-DMyOab9_.mjs";
4
4
  import { existsSync } from "node:fs";
5
5
  import path from "node:path";
6
6
  import fs from "node:fs/promises";
package/dist/cli.mjs CHANGED
@@ -108,7 +108,7 @@ const init = defineCommand({
108
108
  },
109
109
  async run({ args }) {
110
110
  await runCommand(args, async (mode) => {
111
- const { executeInit } = await import("./init-BkW9BuZF.mjs");
111
+ const { executeInit } = await import("./init-DoU4_txp.mjs");
112
112
  return executeInit({
113
113
  dir: args.dir,
114
114
  force: args.force,
@@ -137,7 +137,7 @@ const dev = defineCommand({
137
137
  async run({ args }) {
138
138
  await runCommand(args, async () => {
139
139
  const cwd = await setup({ agent: true });
140
- const { executeDev } = await import("./dev-2oUsvgRX.mjs");
140
+ const { executeDev } = await import("./dev-gVNdGFYY.mjs");
141
141
  return executeDev({
142
142
  cwd,
143
143
  port: args.port
@@ -207,7 +207,7 @@ const deploy = defineCommand({
207
207
  async run({ args }) {
208
208
  await runCommand(args, async () => {
209
209
  const cwd = await setup({ agent: true });
210
- const { executeDeploy } = await import("./deploy-C0VLnnJe.mjs");
210
+ const { executeDeploy } = await import("./deploy-1eaXcfUw.mjs");
211
211
  return executeDeploy({
212
212
  cwd,
213
213
  server: args.server,
@@ -229,7 +229,7 @@ const del = defineCommand({
229
229
  async run({ args }) {
230
230
  await runCommand(args, async () => {
231
231
  const cwd = await setup();
232
- const { executeDelete } = await import("./delete-kSXFLxek.mjs");
232
+ const { executeDelete } = await import("./delete-DXilFBb1.mjs");
233
233
  return executeDelete({
234
234
  cwd,
235
235
  server: args.server
@@ -260,7 +260,7 @@ const secret = defineCommand({
260
260
  async run({ args }) {
261
261
  await runCommand(args, async (mode) => {
262
262
  const cwd = await setup();
263
- const { executeSecretPut, NO_INPUT, readStdin } = await import("./secret-DOva9OGk.mjs");
263
+ const { executeSecretPut, NO_INPUT, readStdin } = await import("./secret-CGAIAbUx.mjs");
264
264
  const value = mode === "json" ? await readStdin() : void 0;
265
265
  if (mode === "json" && !value) throw new CliError(...NO_INPUT);
266
266
  return executeSecretPut(cwd, args.name, value, args.server);
@@ -284,7 +284,7 @@ const secret = defineCommand({
284
284
  async run({ args }) {
285
285
  await runCommand(args, async () => {
286
286
  const cwd = await setup();
287
- const { executeSecretDelete } = await import("./secret-DOva9OGk.mjs");
287
+ const { executeSecretDelete } = await import("./secret-CGAIAbUx.mjs");
288
288
  return executeSecretDelete(cwd, args.name, args.server);
289
289
  });
290
290
  }
@@ -301,7 +301,7 @@ const secret = defineCommand({
301
301
  async run({ args }) {
302
302
  await runCommand(args, async () => {
303
303
  const cwd = await setup();
304
- const { executeSecretList } = await import("./secret-DOva9OGk.mjs");
304
+ const { executeSecretList } = await import("./secret-CGAIAbUx.mjs");
305
305
  return executeSecretList(cwd, args.server);
306
306
  });
307
307
  }
@@ -336,7 +336,7 @@ const storage = defineCommand({
336
336
  },
337
337
  async run({ args }) {
338
338
  await runCommand(args, async () => {
339
- const { executeStorageStatus } = await import("./storage-vM6HjDZZ.mjs");
339
+ const { executeStorageStatus } = await import("./storage-CnhOayhm.mjs");
340
340
  return executeStorageStatus(resolveStorageCwd(args.dir), args.server);
341
341
  });
342
342
  }
@@ -353,7 +353,7 @@ const storage = defineCommand({
353
353
  },
354
354
  async run({ args }) {
355
355
  await runCommand(args, async () => {
356
- const { executeStorageEnable } = await import("./storage-vM6HjDZZ.mjs");
356
+ const { executeStorageEnable } = await import("./storage-CnhOayhm.mjs");
357
357
  return executeStorageEnable(resolveStorageCwd(args.dir), args.server);
358
358
  });
359
359
  }
@@ -375,7 +375,7 @@ const storage = defineCommand({
375
375
  },
376
376
  async run({ args }) {
377
377
  await runCommand(args, async () => {
378
- const { executeStorageDisable } = await import("./storage-vM6HjDZZ.mjs");
378
+ const { executeStorageDisable } = await import("./storage-CnhOayhm.mjs");
379
379
  return executeStorageDisable(resolveStorageCwd(args.dir), {
380
380
  server: args.server,
381
381
  force: args.force
@@ -385,6 +385,22 @@ const storage = defineCommand({
385
385
  })
386
386
  }
387
387
  });
388
+ const login = defineCommand({
389
+ meta: {
390
+ name: "login",
391
+ description: "Sign in with your email and save your API key"
392
+ },
393
+ args: {
394
+ server: sharedArgs.server,
395
+ json: sharedArgs.json
396
+ },
397
+ async run({ args }) {
398
+ await runCommand(args, async () => {
399
+ const { executeLogin } = await import("./login-C59ZHzuO.mjs");
400
+ return executeLogin({ server: args.server });
401
+ });
402
+ }
403
+ });
388
404
  const templates = defineCommand({
389
405
  meta: {
390
406
  name: "templates",
@@ -393,7 +409,7 @@ const templates = defineCommand({
393
409
  args: { json: sharedArgs.json },
394
410
  async run({ args }) {
395
411
  await runCommand(args, async (mode) => {
396
- const { listTemplates } = await import("./_templates-DW9CvuDN.mjs");
412
+ const { listTemplates } = await import("./_templates-Bv8CR800.mjs");
397
413
  const names = await listTemplates();
398
414
  if (mode === "human") {
399
415
  for (const name of names) log.message(name);
@@ -419,6 +435,7 @@ const mainCommand = defineCommand({
419
435
  build,
420
436
  deploy,
421
437
  delete: del,
438
+ login,
422
439
  secret,
423
440
  storage,
424
441
  templates
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { n as log, u as ok } from "./_ui-8kOEB-JH.mjs";
3
- import { n as getServerInfo } from "./_agent-D8zBb3M5.mjs";
3
+ import { n as getServerInfo } from "./_agent-DMyOab9_.mjs";
4
4
  import { n as apiRequest, t as HINT_NOT_DEPLOYED } from "./_api-client-MenP4-O7.mjs";
5
5
  //#region delete.ts
6
6
  async function runDelete(opts) {
@@ -2,9 +2,9 @@
2
2
  import { n as log, t as fmtUrl, u as ok } from "./_ui-8kOEB-JH.mjs";
3
3
  import { a as errorMessage } from "./_utils-Ch0J4s6a.mjs";
4
4
  import { t as buildAgentBundle } from "./_bundler-Cjaxa2wi.mjs";
5
- import { o as writeProjectConfig } from "./_config-D3F9km8X.mjs";
5
+ import { c as writeProjectConfig } from "./_config-5AEqhh-O.mjs";
6
6
  import { t as resolveServerEnv } from "./_server-common-CnaP_Urf.mjs";
7
- import { i as resolveDeployTarget } from "./_agent-D8zBb3M5.mjs";
7
+ import { i as resolveDeployTarget } from "./_agent-DMyOab9_.mjs";
8
8
  import { t as assertTypechecks } from "./_typecheck-gate-9IHWDnl1.mjs";
9
9
  import { n as apiRequest } from "./_api-client-MenP4-O7.mjs";
10
10
  import { gzipSync } from "node:zlib";
@@ -11,7 +11,7 @@ import { styleText } from "node:util";
11
11
  async function executeDev(opts) {
12
12
  const port = parsePort(opts.port);
13
13
  const agentName = path.basename(path.resolve(opts.cwd));
14
- const { startDevServer } = await import("./_dev-server-cigzayJe.mjs");
14
+ const { startDevServer } = await import("./_dev-server-vV05Fnki.mjs");
15
15
  let cleanup;
16
16
  let shuttingDown = false;
17
17
  const onSignal = () => {
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { a as unwrapCancel, n as log, u as ok } from "./_ui-8kOEB-JH.mjs";
3
3
  import { a as errorMessage, c as readJson, l as resolveCwd, o as fileExists, t as AGENT_ENTRY } from "./_utils-Ch0J4s6a.mjs";
4
- import { r as isDevMode, t as getMonorepoRoot } from "./_agent-D8zBb3M5.mjs";
4
+ import { r as isDevMode, t as getMonorepoRoot } from "./_agent-DMyOab9_.mjs";
5
5
  import path from "node:path";
6
6
  import { styleText } from "node:util";
7
7
  import * as p from "@clack/prompts";
@@ -74,7 +74,7 @@ function resolveTargetDir(dir) {
74
74
  }
75
75
  /** Run deploy after init and return deploy metadata if successful. */
76
76
  async function tryDeploy(cwd, server) {
77
- const { executeDeploy } = await import("./deploy-C0VLnnJe.mjs");
77
+ const { executeDeploy } = await import("./deploy-1eaXcfUw.mjs");
78
78
  try {
79
79
  const result = await executeDeploy({
80
80
  cwd,
@@ -92,7 +92,7 @@ async function tryDeploy(cwd, server) {
92
92
  }
93
93
  /** Scaffold the project, optionally showing a spinner. */
94
94
  async function scaffoldProject(dir, cwd, template, silent) {
95
- const { runInit } = await import("./_init-_G8l4lh2.mjs");
95
+ const { runInit } = await import("./_init-BZ9t_Kz-.mjs");
96
96
  const s = silent ? void 0 : p.spinner();
97
97
  s?.start(`Creating ${dir}`);
98
98
  await runInit({
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env node
2
+ import { a as unwrapCancel, n as log, o as CliError, u as ok } from "./_ui-8kOEB-JH.mjs";
3
+ import { i as readGlobalConfig, r as getConfigDir, s as writeGlobalConfig, t as approveServer } from "./_config-5AEqhh-O.mjs";
4
+ import { a as resolveServerUrl } from "./_agent-DMyOab9_.mjs";
5
+ import * as p from "@clack/prompts";
6
+ //#region login.ts
7
+ /**
8
+ * `aai login` — email sign-in against the platform, ending with the
9
+ * account's AssemblyAI API key stored in the global config (the same slot
10
+ * `ensureApiKey` reads), so every other command is untouched by how the
11
+ * key was acquired.
12
+ *
13
+ * Flow (the CLI mirror of the browser studio's two gates):
14
+ * 1. `GET /studio/auth` names the login mode.
15
+ * - `supabase`: Supabase email OTP — `POST /auth/v1/otp` emails a
16
+ * one-time code, the user types it here, `POST /auth/v1/verify`
17
+ * returns the session. The magic LINK in the same email targets the
18
+ * browser; the CLI uses the code because a terminal has no redirect
19
+ * to land on. (The Supabase email template must include the
20
+ * `{{ .Token }}` code for this to work.)
21
+ * - `dev`: mint the same self-describing dev token the studio's local
22
+ * login mints — nothing is emailed anywhere.
23
+ * 2. `GET /studio/account` — when no key is on file yet, prompt for one
24
+ * and `PUT /studio/account/key` (the same mandatory onboarding step
25
+ * the browser shows after sign-in).
26
+ * 3. `GET /studio/account/key` — fetch the key and save it locally.
27
+ * Unlike the browser, the CLI needs the RAW key: `aai dev` runs the
28
+ * provider pipeline in-process on it.
29
+ */
30
+ async function jsonBody(res, what) {
31
+ const body = await res.json().catch(() => null);
32
+ if (!res.ok) throw new CliError("login_failed", `${what} failed: ${body?.error ?? body?.msg ?? `HTTP ${res.status}`}`);
33
+ if (body === null) throw new CliError("login_failed", `${what} returned an invalid response`);
34
+ return body;
35
+ }
36
+ function requireTty() {
37
+ if (!process.stdin.isTTY) throw new CliError("login_interactive", "`aai login` is interactive and needs a TTY.", "Non-interactive setups can set the ASSEMBLYAI_API_KEY environment variable instead.");
38
+ }
39
+ /** The same self-describing token the studio's local-dev login mints. */
40
+ function mintDevToken(email) {
41
+ return `dev.${Buffer.from(JSON.stringify({
42
+ id: `dev:${email}`,
43
+ email
44
+ })).toString("base64url").replace(/=+$/, "")}.dev`;
45
+ }
46
+ /** Supabase email OTP: send the code, prompt for it, verify to a session. */
47
+ async function supabaseSession(auth, email, fetchFn) {
48
+ const base = auth.supabaseUrl.replace(/\/+$/, "");
49
+ const headers = {
50
+ apikey: auth.supabasePublishableKey,
51
+ "Content-Type": "application/json"
52
+ };
53
+ await jsonBody(await fetchFn(`${base}/auth/v1/otp`, {
54
+ method: "POST",
55
+ headers,
56
+ body: JSON.stringify({
57
+ email,
58
+ create_user: true
59
+ })
60
+ }), "Sending the sign-in code");
61
+ log.info(`Sent a sign-in code to ${email}.`);
62
+ const code = unwrapCancel(await p.text({ message: "Enter the code from your email" }), "Login cancelled").trim();
63
+ const session = await jsonBody(await fetchFn(`${base}/auth/v1/verify`, {
64
+ method: "POST",
65
+ headers,
66
+ body: JSON.stringify({
67
+ type: "email",
68
+ email,
69
+ token: code
70
+ })
71
+ }), "Verifying the code");
72
+ if (!session.access_token) throw new CliError("login_failed", "Verifying the code did not return a session.");
73
+ return session.access_token;
74
+ }
75
+ async function executeLogin(opts, deps = {}) {
76
+ const fetchFn = deps.fetchFn ?? globalThis.fetch;
77
+ requireTty();
78
+ const globalConfig = await readGlobalConfig();
79
+ const serverUrl = resolveServerUrl(opts.server, void 0, globalConfig.approvedServers ?? []);
80
+ if (opts.server) await approveServer(serverUrl);
81
+ const auth = await jsonBody(await fetchFn(`${serverUrl}/studio/auth`), "Reading the server's login configuration");
82
+ if (auth.mode === "none") throw new CliError("login_unavailable", "This server has no browser/email login configured.", "Set the ASSEMBLYAI_API_KEY environment variable, or run any platform command to be prompted for a key.");
83
+ const email = unwrapCancel(await p.text({ message: "Email address" }), "Login cancelled").trim();
84
+ const bearer = { Authorization: `Bearer ${auth.mode === "dev" ? mintDevToken(email) : await supabaseSession(auth, email, fetchFn)}` };
85
+ if (!(await jsonBody(await fetchFn(`${serverUrl}/studio/account`, { headers: bearer }), "Loading your account")).hasKey) {
86
+ const newKey = unwrapCancel(await p.password({ message: "Enter your AssemblyAI API key (stored with your account)" }), "Login cancelled").trim();
87
+ await jsonBody(await fetchFn(`${serverUrl}/studio/account/key`, {
88
+ method: "PUT",
89
+ headers: {
90
+ ...bearer,
91
+ "Content-Type": "application/json"
92
+ },
93
+ body: JSON.stringify({ apiKey: newKey })
94
+ }), "Saving your API key");
95
+ }
96
+ const { apiKey } = await jsonBody(await fetchFn(`${serverUrl}/studio/account/key`, { headers: bearer }), "Fetching your API key");
97
+ const dir = getConfigDir();
98
+ await writeGlobalConfig(dir, {
99
+ ...await readGlobalConfig(dir),
100
+ apiKey
101
+ });
102
+ log.success(`Signed in as ${email} — your API key is saved for future commands.`);
103
+ return ok({
104
+ email,
105
+ server: serverUrl
106
+ });
107
+ }
108
+ //#endregion
109
+ export { executeLogin };
@@ -0,0 +1,34 @@
1
+ /**
2
+ * `aai login` — email sign-in against the platform, ending with the
3
+ * account's AssemblyAI API key stored in the global config (the same slot
4
+ * `ensureApiKey` reads), so every other command is untouched by how the
5
+ * key was acquired.
6
+ *
7
+ * Flow (the CLI mirror of the browser studio's two gates):
8
+ * 1. `GET /studio/auth` names the login mode.
9
+ * - `supabase`: Supabase email OTP — `POST /auth/v1/otp` emails a
10
+ * one-time code, the user types it here, `POST /auth/v1/verify`
11
+ * returns the session. The magic LINK in the same email targets the
12
+ * browser; the CLI uses the code because a terminal has no redirect
13
+ * to land on. (The Supabase email template must include the
14
+ * `{{ .Token }}` code for this to work.)
15
+ * - `dev`: mint the same self-describing dev token the studio's local
16
+ * login mints — nothing is emailed anywhere.
17
+ * 2. `GET /studio/account` — when no key is on file yet, prompt for one
18
+ * and `PUT /studio/account/key` (the same mandatory onboarding step
19
+ * the browser shows after sign-in).
20
+ * 3. `GET /studio/account/key` — fetch the key and save it locally.
21
+ * Unlike the browser, the CLI needs the RAW key: `aai dev` runs the
22
+ * provider pipeline in-process on it.
23
+ */
24
+ import { type CommandResult } from "./_output.ts";
25
+ export type LoginDeps = {
26
+ /** Test seam — never set outside tests. */
27
+ fetchFn?: typeof globalThis.fetch;
28
+ };
29
+ export declare function executeLogin(opts: {
30
+ server?: string | undefined;
31
+ }, deps?: LoginDeps): Promise<CommandResult<{
32
+ email: string;
33
+ server: string;
34
+ }>>;
@@ -27,7 +27,9 @@ The fast loop: edit → `pnpm dev` (browser, talk to it) →
27
27
  `node_modules/@alexkroman1/aai-cli/dist/templates/`. Read them directly;
28
28
  `aai init --template <name>` scaffolds a fresh project from one. Closest
29
29
  matches: `simple`, `pipeline-simple`, `web-researcher`, `solo-rpg`,
30
- `pizza-ordering`. When reading SDK types under
30
+ `pizza-ordering`, `retail` (the most complex — 15 tools over a
31
+ relational store, with a `syncState`-driven UI). When reading SDK
32
+ types under
31
33
  `node_modules/@alexkroman1/aai*/dist/`, note the built entry points
32
34
  re-export with source specifiers (`"./sdk/constants.ts"`,
33
35
  `"./components/button.tsx"`) — rewrite `.ts`/`.tsx` to `.d.ts` to find
@@ -77,6 +79,13 @@ export default agent({
77
79
  systemPrompt?: string; // default: general voice assistant
78
80
  // (`system` is an accepted alias)
79
81
  greeting?: string; // default: "Hey there..."
82
+ voice?: string; // TTS voice for the default pipeline, e.g. "michael"
83
+ // (shorthand for tts: assemblyAITts({ voice });
84
+ // invalid with an explicit `tts` or with `s2s`)
85
+ stt?: SttProvider; // pipeline stage overrides — set any subset;
86
+ llm?: LlmProvider | string; // unset stages default to AssemblyAI
87
+ tts?: TtsProvider; // (llm also takes a model-id string)
88
+ s2s?: S2sProvider; // explicit opt-in to speech-to-speech mode
80
89
  sttPrompt?: string; // STT guidance for jargon/acronyms
81
90
  builtinTools?: BuiltinTool[]; // see built-in tools table
82
91
  tools?: Record<string, ToolDef>;
@@ -97,55 +106,55 @@ export default agent({
97
106
  });
98
107
  ```
99
108
 
100
- > When `stt`, `llm`, and `tts` are all provided, the agent runs in
101
- > **Pipeline mode** see the section below. `llm` also accepts a model-id
102
- > string: `"creator/model"` routes through the Vercel AI Gateway
103
- > (`AI_GATEWAY_API_KEY`), a bare id through the AssemblyAI LLM Gateway
104
- > (`ASSEMBLYAI_API_KEY`).
109
+ > Unless `s2s` is set, the agent runs in **Pipeline mode** — see the section
110
+ > below. Declare any subset of `stt`/`llm`/`tts`; unset stages default to
111
+ > AssemblyAI. `llm` also accepts a model-id string: `"creator/model"` routes
112
+ > through the Vercel AI Gateway (`AI_GATEWAY_API_KEY`), a bare id through
113
+ > the AssemblyAI LLM Gateway (`ASSEMBLYAI_API_KEY`).
105
114
 
106
115
  Minimal agent — a cascaded pipeline, which is what you should build unless
107
116
  the user asks for the speech-to-speech API:
108
117
 
109
118
  ```ts
110
- import { agent, assemblyAIPipeline } from "@alexkroman1/aai";
119
+ import { agent } from "@alexkroman1/aai";
111
120
 
112
121
  export default agent({
113
122
  name: "My Agent",
114
- ...assemblyAIPipeline(),
115
123
  });
116
124
  ```
117
125
 
118
- `assemblyAIPipeline()` sets all three stages to AssemblyAI, which bill to
119
- the one key a published agent is guaranteed to have. Override a single stage
120
- by setting it after the spreadeverything else stays as the preset put it:
126
+ No provider fields means the default all-AssemblyAI pipeline: all three
127
+ stages bill to the one key a published agent is guaranteed to have. Pick
128
+ its voice with the `voice` field this is the normal way to choose a
129
+ voice:
121
130
 
122
131
  ```ts
123
- import { agent, assemblyAIPipeline } from "@alexkroman1/aai";
124
- import { assemblyAITts } from "@alexkroman1/aai/tts";
132
+ import { agent } from "@alexkroman1/aai";
125
133
 
126
134
  export default agent({
127
135
  name: "My Agent",
128
- ...assemblyAIPipeline(),
129
- tts: assemblyAITts({ voice: "paul" }),
136
+ voice: "paul",
130
137
  });
131
138
  ```
132
139
 
133
- The same pattern changes the gateway LLM model`llm` accepts the model id
134
- as a plain string:
140
+ Swap a single stage by declaring just that field everything you leave
141
+ unset stays on the default. `llm` accepts the gateway model id as a plain
142
+ string:
135
143
 
136
144
  ```ts
137
- import { agent, assemblyAIPipeline } from "@alexkroman1/aai";
145
+ import { agent } from "@alexkroman1/aai";
138
146
 
139
147
  export default agent({
140
148
  name: "My Agent",
141
- ...assemblyAIPipeline(),
142
149
  llm: "claude-sonnet-4-6",
143
150
  });
144
151
  ```
145
152
 
146
- `agent({ name })` alone is legal and equivalent — an agent that declares no
147
- providers gets `assemblyAIPipeline()` injected as the default. Speech-to-speech
148
- (S2S) mode is an explicit opt-in via the `s2s` field see below.
153
+ `assemblyAIPipeline()` (from `@alexkroman1/aai`) is the explicit spelling of
154
+ the same default — spread it (`...assemblyAIPipeline({ region: "eu" })`) when
155
+ you want the three stages visible in the config or EU data residency across
156
+ STT and the LLM gateway. Speech-to-speech (S2S) mode is an explicit opt-in
157
+ via the `s2s` field — see below.
149
158
 
150
159
  System prompt from file:
151
160
 
@@ -165,8 +174,8 @@ explicit the modern spelling is `with { type: "json" }`, but plain is fine.
165
174
  ## Pipeline mode
166
175
 
167
176
  Pipeline mode is the default: omitting `stt`/`llm`/`tts` (and `s2s`) gives
168
- you the all-AssemblyAI pipeline, exactly as if you had spread
169
- `assemblyAIPipeline()` yourself.
177
+ you the all-AssemblyAI pipeline, and any stage you do declare replaces just
178
+ that stage — the rest keep the default.
170
179
 
171
180
  **S2S mode is an explicit opt-in.** Setting `s2s: assemblyAIS2s()` (imported
172
181
  from `@alexkroman1/aai`, next to `agent()`) selects AssemblyAI's
@@ -194,14 +203,16 @@ providers when:
194
203
  - you want a specific LLM (Anthropic, OpenAI, Gemini, Mistral, xAI, Groq,
195
204
  hundreds of models via OpenRouter, or 25+ models via the AssemblyAI
196
205
  LLM Gateway)
197
- - you want a specific STT model or TTS voice
206
+ - you want a specific STT model, or a non-AssemblyAI TTS provider (for the
207
+ default pipeline's voice, use the `voice` field instead)
198
208
  - you need to swap providers without changing agent code
199
209
 
200
- **The rule:** set all three of `stt`, `llm`, `tts` together, or none.
201
- `agent()`'s parameter type enforces this a partial triple is a compile
202
- error ("missing: llm, tts"), as is combining `s2s` with any pipeline
203
- provider or pipeline-only tuning field. A raw config that skips `agent()`
204
- is still rejected at parse time.
210
+ **The rule:** declare only the stages you're changing any subset of
211
+ `stt`, `llm`, `tts`; each unset stage runs on the AssemblyAI default.
212
+ Combining `s2s` with any pipeline provider or pipeline-only tuning field is
213
+ a compile error naming the rule, as is `voice` alongside an explicit `tts`
214
+ descriptor (the descriptor owns its own voice). A raw config that skips
215
+ `agent()` is still checked at parse time.
205
216
 
206
217
  ```ts
207
218
  import { agent } from "@alexkroman1/aai";
@@ -297,37 +308,31 @@ hundreds of models addressed as `"creator/model"`, e.g.
297
308
  `assemblyAILlm` routes through the [AssemblyAI LLM
298
309
  Gateway](https://www.assemblyai.com/docs/llm-gateway) — an
299
310
  OpenAI-compatible endpoint fronting 25+ models (Claude, GPT, Gemini,
300
- etc.) with the same API key as AssemblyAI STT:
311
+ etc.) with the same API key as AssemblyAI STT. A bare model-id string on
312
+ `llm` is shorthand for it, and unset stages keep the AssemblyAI default:
301
313
 
302
314
  ```ts
303
315
  import { agent } from "@alexkroman1/aai";
304
- import { assemblyAILlm } from "@alexkroman1/aai/llm";
305
- import { assemblyAIStt } from "@alexkroman1/aai/stt";
306
- import { cartesia } from "@alexkroman1/aai/tts";
307
316
 
308
317
  export default agent({
309
318
  name: "My Agent",
310
- stt: assemblyAIStt({ model: "universal-3-5-pro" }),
311
- llm: assemblyAILlm({ model: "claude-sonnet-4-6" }),
312
- tts: cartesia(),
319
+ llm: "claude-sonnet-4-6",
313
320
  });
314
321
  ```
315
322
 
316
- It accepts an optional `region: "eu"` for EU data residency.
323
+ `assemblyAILlm({ model, region: "eu" })` is the explicit form; `region`
324
+ selects EU data residency.
317
325
 
318
- An all-AssemblyAI pipelineone provider, one key:
326
+ Mixing providers works the same way declare the stages you're changing:
319
327
 
320
328
  ```ts
321
329
  import { agent } from "@alexkroman1/aai";
322
- import { assemblyAILlm } from "@alexkroman1/aai/llm";
323
- import { assemblyAIStt } from "@alexkroman1/aai/stt";
324
- import { assemblyAITts } from "@alexkroman1/aai/tts";
330
+ import { cartesia } from "@alexkroman1/aai/tts";
325
331
 
326
332
  export default agent({
327
333
  name: "My Agent",
328
- stt: assemblyAIStt({ model: "universal-3-5-pro" }),
329
- llm: assemblyAILlm({ model: "gemini-2.5-flash-lite" }),
330
- tts: assemblyAITts({ voice: "jane" }),
334
+ llm: "claude-sonnet-4-6",
335
+ tts: cartesia(),
331
336
  });
332
337
  ```
333
338
 
@@ -343,7 +348,9 @@ Bare calls (`assemblyAITts()`, `cartesia()`, `rime()`) use the defaults.
343
348
  Override with `{ voice, model, language }`.
344
349
 
345
350
  **AssemblyAI TTS** shares `ASSEMBLYAI_API_KEY` with AssemblyAI STT and the
346
- LLM Gateway, so an all-AssemblyAI pipeline needs exactly one secret. Each
351
+ LLM Gateway, so an all-AssemblyAI pipeline needs exactly one secret. On the
352
+ default pipeline, `agent({ voice: "michael" })` is the shorthand for
353
+ `tts: assemblyAITts({ voice: "michael" })` — same catalog, same rules. Each
347
354
  voice speaks one language, and this is the whole catalog — **a voice not on
348
355
  this list is rejected after the socket opens, which leaves the agent
349
356
  connected, "ready", and permanently silent**, so pick one from here rather
@@ -721,7 +728,7 @@ import "@alexkroman1/aai-ui/styles.css";
721
728
  import { client, useSession } from "@alexkroman1/aai-ui";
722
729
 
723
730
  function MyApp() {
724
- const { messages, userTranscript, started, running, start, toggle, reset } =
731
+ const { messages, userTranscript, started, running, start, toggle, end } =
725
732
  useSession();
726
733
  return (
727
734
  <div>
@@ -732,7 +739,7 @@ function MyApp() {
732
739
  ) : (
733
740
  <>
734
741
  <button onClick={toggle}>{running ? "Stop" : "Resume"}</button>
735
- <button onClick={reset}>Reset</button>
742
+ <button onClick={end}>End</button>
736
743
  </>
737
744
  )}
738
745
  </div>
@@ -774,8 +781,16 @@ to put it in.
774
781
  | `started` | `boolean` | Whether session started |
775
782
  | `running` | `boolean` | Whether session active |
776
783
 
777
- Methods: `start()`, `toggle()`, `reset()`, `cancel()`, `disconnect()`,
778
- `resetState()`.
784
+ Methods: `start()`, `toggle()`, `end()`, `reset()`, `cancel()`,
785
+ `disconnect()`, `resetState()`.
786
+
787
+ - `end()` hangs up: it flips `started` back to `false` (a start-screen UI
788
+ shows its Start control again) and the next `start()` is a brand-new
789
+ session — fresh per-session tool state, greeting included. Use it for
790
+ End/Hang up/New game buttons.
791
+ - `reset()` clears the conversation but keeps the call live (`started`
792
+ stays `true`) — the control stays on Stop/Resume, and per-session tool
793
+ state survives. Use it for a "clear chat" control, not for ending.
779
794
 
780
795
  ## UI hooks
781
796
 
@@ -949,9 +964,10 @@ Common mistakes when working in aai projects:
949
964
  - **Filter large API responses before returning them from tools.** Return
950
965
  values are injected into LLM context. Truncate, summarize, or extract
951
966
  only what the model needs.
952
- - **Pipeline mode requires all three of `stt` / `llm` / `tts`.** Partial
953
- configs are rejected at parse time. Omit all three for the default
954
- AssemblyAI pipeline; S2S needs an explicit `s2s: assemblyAIS2s()`.
967
+ - **Declare only the pipeline stages you're changing.** Unset stages of
968
+ `stt` / `llm` / `tts` default to AssemblyAI (omit all three for the full
969
+ default pipeline; `voice` picks its TTS voice). S2S needs an explicit
970
+ `s2s: assemblyAIS2s()` and takes no pipeline fields.
955
971
  - **Never hardcode secrets.** Use `ctx.env.MY_KEY`. `.env` for local dev,
956
972
  `aai secret put` for production.
957
973
  - **Don't use `useEffect` + `toolCalls` to derive state.** Use