@alexkroman1/aai-cli 0.9.2 → 0.10.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 (65) hide show
  1. package/dist/{_build-Dw66C1m4.mjs → _build-aW_zyPpr.mjs} +61 -17
  2. package/dist/{_discover-DsorPpR_.mjs → _discover-D7HCLa_N.mjs} +62 -27
  3. package/dist/_init-CweK2mJJ.mjs +108 -0
  4. package/dist/{_link-BGXGFYWa.mjs → _link-D6mmDQ0B.mjs} +5 -3
  5. package/dist/_server-common-B6EXxxB2.mjs +105 -0
  6. package/dist/_ui-C9IvR7Fh.mjs +50 -0
  7. package/dist/cli.mjs +186 -31
  8. package/dist/delete-DxECFYOp.mjs +40 -0
  9. package/dist/{deploy-D_TBrC3r.mjs → deploy-DB0PeNtc.mjs} +11 -7
  10. package/dist/dev-DCtHG1pz.mjs +60 -0
  11. package/dist/doctor-bWuArZi6.mjs +215 -0
  12. package/dist/generate-D7zJtqoC.mjs +393 -0
  13. package/dist/{init-z8-W0Q1u.mjs → init-COkINbH_.mjs} +13 -17
  14. package/dist/{rag-D0xqGIrS.mjs → rag-BNfrEr4A.mjs} +12 -7
  15. package/dist/{secret-BQl3zBOS.mjs → secret-Zzv7fJ_3.mjs} +2 -2
  16. package/dist/{start-Df8c2ciS.mjs → start-DQypQkOq.mjs} +5 -5
  17. package/dist/test-9fFn9DCf.mjs +40 -0
  18. package/package.json +18 -11
  19. package/dist/_init-l_uoyFCN.mjs +0 -82
  20. package/dist/_server-common-CpK4rWGs.mjs +0 -36
  21. package/dist/_ui-kJIua5L9.mjs +0 -44
  22. package/dist/dev-DhHd1FRP.mjs +0 -39
  23. package/templates/_shared/.env.example +0 -5
  24. package/templates/_shared/CLAUDE.md +0 -1051
  25. package/templates/_shared/biome.json +0 -32
  26. package/templates/_shared/global.d.ts +0 -1
  27. package/templates/_shared/index.html +0 -16
  28. package/templates/_shared/package.json +0 -23
  29. package/templates/_shared/tsconfig.json +0 -15
  30. package/templates/code-interpreter/agent.ts +0 -27
  31. package/templates/code-interpreter/client.tsx +0 -3
  32. package/templates/css.d.ts +0 -1
  33. package/templates/dispatch-center/agent.ts +0 -1227
  34. package/templates/dispatch-center/client.tsx +0 -505
  35. package/templates/embedded-assets/agent.ts +0 -48
  36. package/templates/embedded-assets/client.tsx +0 -3
  37. package/templates/embedded-assets/knowledge.json +0 -20
  38. package/templates/health-assistant/agent.ts +0 -160
  39. package/templates/health-assistant/client.tsx +0 -3
  40. package/templates/infocom-adventure/agent.ts +0 -164
  41. package/templates/infocom-adventure/client.tsx +0 -300
  42. package/templates/math-buddy/agent.ts +0 -21
  43. package/templates/math-buddy/client.tsx +0 -3
  44. package/templates/memory-agent/agent.ts +0 -20
  45. package/templates/memory-agent/client.tsx +0 -3
  46. package/templates/night-owl/agent.ts +0 -98
  47. package/templates/night-owl/client.tsx +0 -12
  48. package/templates/personal-finance/agent.ts +0 -26
  49. package/templates/personal-finance/client.tsx +0 -3
  50. package/templates/pizza-ordering/agent.ts +0 -218
  51. package/templates/pizza-ordering/client.tsx +0 -264
  52. package/templates/simple/agent.ts +0 -6
  53. package/templates/simple/client.tsx +0 -3
  54. package/templates/smart-research/agent.ts +0 -164
  55. package/templates/smart-research/client.tsx +0 -3
  56. package/templates/solo-rpg/agent.ts +0 -1244
  57. package/templates/solo-rpg/client.tsx +0 -698
  58. package/templates/support/README.md +0 -62
  59. package/templates/support/agent.ts +0 -19
  60. package/templates/support/client.tsx +0 -3
  61. package/templates/travel-concierge/agent.ts +0 -29
  62. package/templates/travel-concierge/client.tsx +0 -3
  63. package/templates/tsconfig.json +0 -1
  64. package/templates/web-researcher/agent.ts +0 -17
  65. package/templates/web-researcher/client.tsx +0 -3
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import { o as loadAgent } from "./_discover-DsorPpR_.mjs";
3
- import { a as step, i as runCommand } from "./_ui-kJIua5L9.mjs";
2
+ import { s as loadAgent } from "./_discover-D7HCLa_N.mjs";
3
+ import { a as runCommand, o as step } from "./_ui-C9IvR7Fh.mjs";
4
4
  import path from "node:path";
5
5
  import fs from "node:fs/promises";
6
6
  import { errorMessage } from "@alexkroman1/aai/utils";
7
7
  import preact from "@preact/preset-vite";
8
8
  import tailwindcss from "@tailwindcss/vite";
9
- import { build } from "vite";
9
+ import { build, createServer } from "vite";
10
10
  //#region _bundler.ts
11
11
  /**
12
12
  * Error thrown when bundling fails.
@@ -14,11 +14,28 @@ import { build } from "vite";
14
14
  * @param message Human-readable error message (typically formatted build output).
15
15
  */
16
16
  var BundleError = class extends Error {
17
- constructor(message) {
18
- super(message);
17
+ constructor(message, options) {
18
+ super(message, options);
19
19
  this.name = "BundleError";
20
20
  }
21
21
  };
22
+ /** File extensions that are safe to read as UTF-8 text. */
23
+ const TEXT_EXTENSIONS = new Set([
24
+ ".html",
25
+ ".htm",
26
+ ".css",
27
+ ".js",
28
+ ".mjs",
29
+ ".cjs",
30
+ ".ts",
31
+ ".mts",
32
+ ".json",
33
+ ".map",
34
+ ".svg",
35
+ ".xml",
36
+ ".txt",
37
+ ".md"
38
+ ]);
22
39
  /** Read all files in a directory as a map of relative paths to contents. */
23
40
  async function readDirFiles(dir) {
24
41
  let entries;
@@ -34,7 +51,10 @@ async function readDirFiles(dir) {
34
51
  const files = {};
35
52
  await Promise.all(entries.filter((e) => e.isFile()).map(async (e) => {
36
53
  const full = path.join(e.parentPath, e.name);
37
- files[path.relative(dir, full)] = await fs.readFile(full, "utf-8");
54
+ const ext = path.extname(e.name).toLowerCase();
55
+ const rel = path.relative(dir, full);
56
+ if (TEXT_EXTENSIONS.has(ext)) files[rel] = await fs.readFile(full, "utf-8");
57
+ else files[rel] = `base64:${(await fs.readFile(full)).toString("base64")}`;
38
58
  }));
39
59
  return files;
40
60
  }
@@ -57,12 +77,10 @@ async function bundleAgent(agent, opts) {
57
77
  root: agent.dir,
58
78
  logLevel: "warn",
59
79
  build: {
60
- rollupOptions: {
61
- input: path.join(agent.dir, "agent.ts"),
62
- output: {
63
- format: "es",
64
- entryFileNames: "worker.js"
65
- }
80
+ lib: {
81
+ entry: path.join(agent.dir, "agent.ts"),
82
+ formats: ["es"],
83
+ fileName: () => "worker.js"
66
84
  },
67
85
  outDir: buildDir,
68
86
  emptyOutDir: true,
@@ -71,9 +89,9 @@ async function bundleAgent(agent, opts) {
71
89
  }
72
90
  });
73
91
  } catch (err) {
74
- throw new BundleError(errorMessage(err));
92
+ throw new BundleError(errorMessage(err), { cause: err });
75
93
  }
76
- if (!(opts?.skipClient || !agent.clientEntry)) try {
94
+ if (!(opts?.skipClient ?? !agent.clientEntry)) try {
77
95
  await build({
78
96
  root: agent.dir,
79
97
  base: "./",
@@ -88,7 +106,7 @@ async function bundleAgent(agent, opts) {
88
106
  }
89
107
  });
90
108
  } catch (err) {
91
- throw new BundleError(errorMessage(err));
109
+ throw new BundleError(errorMessage(err), { cause: err });
92
110
  }
93
111
  const worker = await fs.readFile(path.join(buildDir, "worker.js"), "utf-8");
94
112
  return {
@@ -98,6 +116,32 @@ async function bundleAgent(agent, opts) {
98
116
  workerBytes: Buffer.byteLength(worker)
99
117
  };
100
118
  }
119
+ /**
120
+ * Create a Vite dev server for client HMR during development.
121
+ *
122
+ * The dev server serves client files with hot module replacement enabled and
123
+ * proxies backend requests (`/health`, `/websocket`) to the agent server.
124
+ */
125
+ async function createClientDevServer(agentDir, backendPort, port) {
126
+ const target = `http://localhost:${backendPort}`;
127
+ return await createServer({
128
+ configFile: false,
129
+ root: agentDir,
130
+ plugins: [preact(), tailwindcss()],
131
+ resolve: { dedupe: ["preact", "@preact/signals"] },
132
+ server: {
133
+ port,
134
+ strictPort: true,
135
+ proxy: {
136
+ "/health": target,
137
+ "/websocket": {
138
+ target,
139
+ ws: true
140
+ }
141
+ }
142
+ }
143
+ });
144
+ }
101
145
  //#endregion
102
146
  //#region _build.ts
103
147
  /**
@@ -113,7 +157,7 @@ async function buildAgentBundle(cwd, log) {
113
157
  try {
114
158
  bundle = await bundleAgent(agent);
115
159
  } catch (err) {
116
- if (err instanceof BundleError) throw new Error(`Bundle failed: ${err.message}`);
160
+ if (err instanceof BundleError) throw new Error(`Bundle failed: ${err.message}`, { cause: err });
117
161
  throw err;
118
162
  }
119
163
  const kb = (bundle.workerBytes / 1024).toFixed(1);
@@ -129,4 +173,4 @@ async function runBuildCommand(cwd) {
129
173
  });
130
174
  }
131
175
  //#endregion
132
- export { buildAgentBundle, runBuildCommand };
176
+ export { buildAgentBundle, runBuildCommand, createClientDevServer as t };
@@ -3,44 +3,69 @@ import { existsSync } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import fs$1 from "node:fs/promises";
6
+ import { consola } from "consola";
6
7
  import { humanId } from "human-id";
7
8
  import { z } from "zod";
8
- import * as p from "@clack/prompts";
9
+ import { createInterface } from "node:readline";
9
10
  //#region _prompts.ts
11
+ const consola$1 = consola.create({ formatOptions: { date: false } });
10
12
  /**
11
13
  * Prompt the user for a password (masked input).
12
14
  * Returns the entered string.
13
15
  */
14
16
  async function askPassword(message) {
15
- const value = await p.password({ message });
16
- if (p.isCancel(value)) {
17
- p.cancel("Cancelled.");
18
- process.exit(0);
17
+ const rl = createInterface({
18
+ input: process.stdin,
19
+ output: process.stdout
20
+ });
21
+ process.stdout.write(`${message}: `);
22
+ const stdin = process.stdin;
23
+ const wasRaw = stdin.isRaw;
24
+ if (stdin.isTTY) stdin.setRawMode(true);
25
+ try {
26
+ return await readMasked(stdin);
27
+ } finally {
28
+ if (stdin.isTTY) stdin.setRawMode(wasRaw ?? false);
29
+ rl.close();
19
30
  }
20
- return value;
31
+ }
32
+ function readMasked(stdin) {
33
+ return new Promise((resolve) => {
34
+ let buf = "";
35
+ const onData = (ch) => {
36
+ const c = ch.toString();
37
+ if (c === "\n" || c === "\r") {
38
+ stdin.removeListener("data", onData);
39
+ process.stdout.write("\n");
40
+ resolve(buf);
41
+ } else if (c === "") {
42
+ stdin.removeListener("data", onData);
43
+ process.exit(0);
44
+ } else if (c === "" || c === "\b") buf = buf.slice(0, -1);
45
+ else buf += c;
46
+ };
47
+ stdin.on("data", onData);
48
+ });
21
49
  }
22
50
  /**
23
51
  * Prompt the user for text input with a default value.
24
52
  * Returns the entered string, or the default if empty.
25
53
  */
26
54
  async function askText(message, defaultValue) {
27
- const value = await p.text({
28
- message,
55
+ return await consola$1.prompt(message, {
56
+ type: "text",
29
57
  placeholder: defaultValue,
30
- defaultValue
31
- });
32
- if (p.isCancel(value)) {
33
- p.cancel("Cancelled.");
34
- process.exit(0);
35
- }
36
- return value || defaultValue;
58
+ initial: defaultValue,
59
+ cancel: "reject"
60
+ }) || defaultValue;
37
61
  }
38
62
  //#endregion
39
63
  //#region _discover.ts
40
64
  const AuthConfigSchema = z.object({ assemblyai_api_key: z.string().optional() });
41
65
  const ProjectConfigSchema = z.object({
42
66
  slug: z.string(),
43
- serverUrl: z.string()
67
+ serverUrl: z.string(),
68
+ sessionId: z.string().optional()
44
69
  });
45
70
  /** Resolve the working directory from INIT_CWD or process.cwd(). */
46
71
  function resolveCwd() {
@@ -70,24 +95,34 @@ async function writeAuthConfig(config) {
70
95
  if (process.platform !== "win32") await fs$1.chmod(CONFIG_FILE, 384);
71
96
  }
72
97
  /**
73
- * Retrieves the AssemblyAI API key from `~/.config/aai/config.json`.
74
- * If not found, interactively prompts the user and persists it.
98
+ * Retrieves the AssemblyAI API key from `process.env`, `~/.config/aai/config.json`,
99
+ * or by interactively prompting the user (persisting it to config).
100
+ *
101
+ * Does NOT mutate `process.env`. Callers that need the key available in the
102
+ * environment for child processes should use {@link ensureApiKeyInEnv} instead.
75
103
  */
76
104
  async function getApiKey() {
77
105
  if (process.env.ASSEMBLYAI_API_KEY) return process.env.ASSEMBLYAI_API_KEY;
78
106
  const config = await readAuthConfig();
79
- if (config.assemblyai_api_key) {
80
- process.env.ASSEMBLYAI_API_KEY = config.assemblyai_api_key;
81
- return config.assemblyai_api_key;
82
- }
107
+ if (config.assemblyai_api_key) return config.assemblyai_api_key;
108
+ consola.info("Get your API key at https://www.assemblyai.com/dashboard/signup");
109
+ consola.info("Or set the ASSEMBLYAI_API_KEY environment variable to skip this prompt.\n");
83
110
  let key;
84
111
  while (!key) key = await askPassword("ASSEMBLYAI_API_KEY");
85
112
  config.assemblyai_api_key = key;
86
- process.env.ASSEMBLYAI_API_KEY = key;
87
113
  await writeAuthConfig(config);
88
114
  return key;
89
115
  }
90
116
  /**
117
+ * Resolves the API key via {@link getApiKey} and sets it on `process.env.ASSEMBLYAI_API_KEY`
118
+ * so that child processes and downstream code can read it from the environment.
119
+ */
120
+ async function ensureApiKeyInEnv() {
121
+ const key = await getApiKey();
122
+ process.env.ASSEMBLYAI_API_KEY = key;
123
+ return key;
124
+ }
125
+ /**
91
126
  * Reads `.aai/project.json` from an agent directory.
92
127
  * Returns null if the file doesn't exist.
93
128
  */
@@ -129,7 +164,7 @@ function isDevMode() {
129
164
  }
130
165
  /** Resolve the server URL from an explicit value, project config, or default. */
131
166
  function resolveServerUrl(explicit, configUrl) {
132
- return explicit || configUrl || (isDevMode() ? "http://localhost:8787" : "https://aai-agent.fly.dev");
167
+ return explicit ?? configUrl ?? (isDevMode() ? "http://localhost:8787" : "https://aai-agent.fly.dev");
133
168
  }
134
169
  async function fileExists(p) {
135
170
  try {
@@ -143,8 +178,8 @@ async function fileExists(p) {
143
178
  * Loads agent metadata from a directory by checking for `agent.ts` and
144
179
  * resolving the client entry point.
145
180
  *
146
- * Env vars are NOT read from `.env` — they're managed on the server
147
- * via `aai env add` (like `vercel env add`).
181
+ * Env vars for deployed agents are managed on the server via
182
+ * `aai secret put`. For local dev, `.env` is loaded by `resolveServerEnv`.
148
183
  */
149
184
  async function loadAgent(dir) {
150
185
  if (!await fileExists(path.join(dir, "agent.ts"))) return null;
@@ -158,4 +193,4 @@ async function loadAgent(dir) {
158
193
  };
159
194
  }
160
195
  //#endregion
161
- export { isDevMode as a, resolveCwd as c, askPassword as d, askText as f, getServerInfo as i, resolveServerUrl as l, generateSlug as n, loadAgent as o, getApiKey as r, readProjectConfig as s, fileExists as t, writeProjectConfig as u };
196
+ export { getServerInfo as a, readProjectConfig as c, writeProjectConfig as d, askPassword as f, getApiKey as i, resolveCwd as l, fileExists as n, isDevMode as o, askText as p, generateSlug as r, loadAgent as s, ensureApiKeyInEnv as t, resolveServerUrl as u };
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env node
2
+ import { o as isDevMode } from "./_discover-D7HCLa_N.mjs";
3
+ import { existsSync } from "node:fs";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import fs$1 from "node:fs/promises";
7
+ import { downloadTemplate } from "giget";
8
+ //#region _templates.ts
9
+ const GIGET_SOURCE = "github:alexkroman/agent/packages/aai-templates";
10
+ const GIGET_REF = process.env.AAI_TEMPLATES_REF ?? "main";
11
+ /** Resolve the local aai-templates package directory (dev mode only). */
12
+ function resolveLocalTemplatesDir() {
13
+ const cliDir = path.dirname(fileURLToPath(import.meta.url));
14
+ const fromSrc = path.resolve(cliDir, "../aai-templates");
15
+ const fromDist = path.resolve(cliDir, "../../aai-templates");
16
+ if (existsSync(fromSrc)) return fromSrc;
17
+ if (existsSync(fromDist)) return fromDist;
18
+ throw new Error("Cannot find local aai-templates package");
19
+ }
20
+ /** Resolve the templates directory — local in dev, giget download in prod. */
21
+ async function resolveTemplatesDir() {
22
+ if (isDevMode()) return resolveLocalTemplatesDir();
23
+ const { dir } = await downloadTemplate(`${GIGET_SOURCE}#${GIGET_REF}`, { force: true });
24
+ return dir;
25
+ }
26
+ /**
27
+ * Download a template into targetDir, merging _shared files underneath.
28
+ */
29
+ async function downloadAndMergeTemplate(template, targetDir) {
30
+ const templatesDir = await resolveTemplatesDir();
31
+ const names = (await fs$1.readdir(templatesDir, { withFileTypes: true })).filter((e) => e.isDirectory() && !e.name.startsWith("_") && e.name !== "node_modules").map((e) => e.name);
32
+ if (!names.includes(template)) throw new Error(`unknown template '${template}' -- available: ${names.join(", ")}`);
33
+ await fs$1.cp(path.join(templatesDir, template), targetDir, {
34
+ recursive: true,
35
+ force: true
36
+ });
37
+ const sharedDir = path.join(templatesDir, "_shared");
38
+ if (existsSync(sharedDir)) {
39
+ const entries = await fs$1.readdir(sharedDir, {
40
+ recursive: true,
41
+ withFileTypes: true
42
+ });
43
+ for (const entry of entries) {
44
+ if (!entry.isFile()) continue;
45
+ const rel = path.relative(sharedDir, path.join(entry.parentPath, entry.name));
46
+ const destPath = path.join(targetDir, rel);
47
+ await fs$1.mkdir(path.dirname(destPath), { recursive: true });
48
+ try {
49
+ await fs$1.copyFile(path.join(sharedDir, rel), destPath, fs$1.constants.COPYFILE_EXCL);
50
+ } catch (err) {
51
+ if (!(err instanceof Error && "code" in err && err.code === "EEXIST")) throw err;
52
+ }
53
+ }
54
+ }
55
+ }
56
+ //#endregion
57
+ //#region _init.ts
58
+ async function runInit(opts) {
59
+ const { targetDir, template } = opts;
60
+ await downloadAndMergeTemplate(template, targetDir);
61
+ try {
62
+ await fs$1.copyFile(path.join(targetDir, ".env.example"), path.join(targetDir, ".env"));
63
+ } catch {}
64
+ const readmePath = path.join(targetDir, "README.md");
65
+ const readme = `# ${path.basename(path.resolve(targetDir))}
66
+
67
+ A voice agent built with [aai](https://github.com/anthropics/aai).
68
+
69
+ ## Getting started
70
+
71
+ \`\`\`sh
72
+ npm install # Install dependencies
73
+ npm run dev # Run locally (opens browser)
74
+ npm run deploy # Deploy to production
75
+ \`\`\`
76
+
77
+ ## Secrets
78
+
79
+ Access secrets in your agent via \`ctx.env.MY_KEY\`.
80
+
81
+ **Local development** — add secrets to \`.env\` (auto-loaded by \`aai dev\`):
82
+
83
+ \`\`\`sh
84
+ ALPHA_VANTAGE_KEY=sk-abc123
85
+ MY_API_KEY=secret-value
86
+ \`\`\`
87
+
88
+ **Production** — set secrets on the server:
89
+
90
+ \`\`\`sh
91
+ aai secret put MY_KEY # Set a secret (prompts for value)
92
+ aai secret list # List secret names
93
+ aai secret delete MY_KEY # Remove a secret
94
+ \`\`\`
95
+
96
+ ## Learn more
97
+
98
+ See \`CLAUDE.md\` for the full agent API reference.
99
+ `;
100
+ try {
101
+ await fs$1.writeFile(readmePath, readme, { flag: "wx" });
102
+ } catch (err) {
103
+ if (!(err instanceof Error && "code" in err && err.code === "EEXIST")) throw err;
104
+ }
105
+ return targetDir;
106
+ }
107
+ //#endregion
108
+ export { runInit };
@@ -2,6 +2,8 @@
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
+ import { consola } from "consola";
6
+ import { z } from "zod";
5
7
  import { execFileSync } from "node:child_process";
6
8
  //#region _link.ts
7
9
  const WORKSPACE_PKGS = ["aai", "aai-ui"];
@@ -18,7 +20,7 @@ function rewriteWorkspaceDeps(cwd, rewrite, verb) {
18
20
  const changed = [];
19
21
  for (const pkgDir of WORKSPACE_PKGS) {
20
22
  const localPath = path.join(packagesDir, pkgDir);
21
- const name = JSON.parse(fs.readFileSync(path.join(localPath, "package.json"), "utf-8")).name;
23
+ const name = z.object({ name: z.string() }).parse(JSON.parse(fs.readFileSync(path.join(localPath, "package.json"), "utf-8"))).name;
22
24
  if (!deps[name]) continue;
23
25
  const newVersion = rewrite(deps[name], localPath);
24
26
  if (newVersion !== null) {
@@ -27,11 +29,11 @@ function rewriteWorkspaceDeps(cwd, rewrite, verb) {
27
29
  }
28
30
  }
29
31
  if (changed.length === 0) {
30
- console.log(`No packages to ${verb}.`);
32
+ consola.info(`No packages to ${verb}.`);
31
33
  return;
32
34
  }
33
35
  fs.writeFileSync(pkgJsonPath, `${JSON.stringify(pkgJson, null, 2)}\n`);
34
- console.log(`${verb}: ${changed.join(", ")} → installing...`);
36
+ consola.start(`${verb}: ${changed.join(", ")} → installing...`);
35
37
  execFileSync("npm", ["install"], {
36
38
  cwd,
37
39
  stdio: "inherit"
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env node
2
+ import { i as getApiKey } from "./_discover-D7HCLa_N.mjs";
3
+ import path from "node:path";
4
+ import fs from "node:fs/promises";
5
+ //#region _server-common.ts
6
+ /**
7
+ * Return the variable names declared in a `.env` file.
8
+ *
9
+ * Only used to determine *which* keys the developer intended as agent
10
+ * secrets — actual values are resolved from `process.env` (so shell
11
+ * overrides still win).
12
+ */
13
+ function envFileKeys(content) {
14
+ const keys = [];
15
+ for (const raw of content.split("\n")) {
16
+ const line = raw.trim();
17
+ if (!line || line.startsWith("#")) continue;
18
+ const eq = line.indexOf("=");
19
+ if (eq === -1) continue;
20
+ const key = line.slice(0, eq).trim();
21
+ if (key) keys.push(key);
22
+ }
23
+ return keys;
24
+ }
25
+ /** Load an AgentDef by dynamically importing agent.ts via Node's native TS support. */
26
+ async function loadAgentDef(cwd) {
27
+ const agentDef = (await import(path.resolve(cwd, "agent.ts"))).default;
28
+ if (!agentDef || typeof agentDef !== "object" || !agentDef.name) throw new Error("agent.ts must export a default agent definition (from defineAgent())");
29
+ const missing = [];
30
+ if (typeof agentDef.name !== "string") missing.push("name (string)");
31
+ if (typeof agentDef.instructions !== "string") missing.push("instructions (string)");
32
+ if (typeof agentDef.greeting !== "string") missing.push("greeting (string)");
33
+ if (typeof agentDef.maxSteps !== "number" && typeof agentDef.maxSteps !== "function") missing.push("maxSteps (number or function)");
34
+ if (!agentDef.tools || typeof agentDef.tools !== "object" || Array.isArray(agentDef.tools)) missing.push("tools (object)");
35
+ if (missing.length > 0) throw new Error(`Invalid agent definition: missing or invalid fields: ${missing.join(", ")}. Use defineAgent() to create a valid agent definition.`);
36
+ return agentDef;
37
+ }
38
+ /**
39
+ * Build the `ctx.env` record that agent tools will see at runtime.
40
+ *
41
+ * Only variables explicitly declared in `.env` (plus `ASSEMBLYAI_API_KEY`)
42
+ * are included — matching the platform sandbox behavior where `ctx.env`
43
+ * contains only secrets set via `aai secret put`. This prevents agents
44
+ * from accidentally depending on shell-level vars (PATH, HOME, etc.) that
45
+ * won't exist in production.
46
+ *
47
+ * Values are resolved from `process.env` after loading `.env` via Node's
48
+ * built-in `process.loadEnvFile()`, so shell exports override `.env`.
49
+ *
50
+ * @param cwd - Project directory containing `.env` (optional).
51
+ * @param baseEnv - Override the environment to read values from (tests only).
52
+ */
53
+ async function resolveServerEnv(cwd, baseEnv) {
54
+ let declaredKeys = [];
55
+ if (cwd) {
56
+ const envPath = path.join(cwd, ".env");
57
+ try {
58
+ declaredKeys = envFileKeys(await fs.readFile(envPath, "utf-8"));
59
+ process.loadEnvFile(envPath);
60
+ } catch {}
61
+ }
62
+ const source = baseEnv ?? process.env;
63
+ const env = {};
64
+ for (const key of declaredKeys) {
65
+ const val = source[key];
66
+ if (val !== void 0) env[key] = val;
67
+ }
68
+ if (!env.ASSEMBLYAI_API_KEY) env.ASSEMBLYAI_API_KEY = source.ASSEMBLYAI_API_KEY ?? await getApiKey();
69
+ return env;
70
+ }
71
+ /**
72
+ * Create and start an agent server with static file serving.
73
+ *
74
+ * NOTE: This dynamically imports `@alexkroman1/aai/server` which has peer
75
+ * dependencies on `hono` and `@hono/node-server`. Those packages are listed
76
+ * as direct dependencies of aai-cli (in package.json) solely to satisfy
77
+ * those peer deps — they are not imported directly by aai-cli code.
78
+ */
79
+ async function bootServer(agentDef, clientDir, env, port) {
80
+ const { createServer } = await import("@alexkroman1/aai/server");
81
+ const server = createServer({
82
+ agent: agentDef,
83
+ clientDir,
84
+ env
85
+ });
86
+ await server.listen(port);
87
+ return server;
88
+ }
89
+ /**
90
+ * Boot the agent server without client serving.
91
+ *
92
+ * Used in dev mode where Vite handles client files with HMR,
93
+ * and only the backend (health, WebSocket) runs on this server.
94
+ */
95
+ async function bootBackendServer(agentDef, env, port) {
96
+ const { createServer } = await import("@alexkroman1/aai/server");
97
+ const server = createServer({
98
+ agent: agentDef,
99
+ env
100
+ });
101
+ await server.listen(port);
102
+ return server;
103
+ }
104
+ //#endregion
105
+ export { resolveServerEnv as a, loadAgentDef as i, bootServer as n, envFileKeys as r, bootBackendServer as t };
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+ import { colorize } from "consola/utils";
3
+ //#region _ui.ts
4
+ /** Interactive/info color wrapper (blue). */
5
+ function interactive(s) {
6
+ return colorize("blueBright", s);
7
+ }
8
+ /** Colored step message: bold action label + message. */
9
+ function step(action, msg) {
10
+ return `${colorize("bold", colorize("cyanBright", action))} ${msg}`;
11
+ }
12
+ /** Informational step message: bold blue action + message. */
13
+ function stepInfo(action, msg) {
14
+ return `${colorize("bold", colorize("blueBright", action))} ${msg}`;
15
+ }
16
+ /** Dimmed info sub-line (indented). */
17
+ function info(msg) {
18
+ return colorize("dim", ` ${msg}`);
19
+ }
20
+ /** Detail sub-line (indented). */
21
+ function detail(msg) {
22
+ return ` ${msg}`;
23
+ }
24
+ /** Warning message. */
25
+ function warn(msg) {
26
+ return `${colorize("yellowBright", "!")} ${msg}`;
27
+ }
28
+ /** Parse and validate a port string. Returns the numeric port or throws. */
29
+ function parsePort(raw) {
30
+ const port = Number.parseInt(raw, 10);
31
+ if (Number.isNaN(port) || port < 0 || port > 65535) throw new Error(`Invalid port: ${raw}. Must be a number between 0 and 65535.`);
32
+ return port;
33
+ }
34
+ /**
35
+ * Run an async command function, logging each step to stdout.
36
+ * Replaces the Ink `runWithInk` pattern.
37
+ */
38
+ async function runCommand(fn) {
39
+ const log = (msg) => console.log(msg);
40
+ const setStatus = (msg) => {
41
+ if (msg) process.stdout.write(`\r${colorize("dim", msg)}`);
42
+ else process.stdout.write("\r\x1B[K");
43
+ };
44
+ await fn({
45
+ log,
46
+ setStatus
47
+ });
48
+ }
49
+ //#endregion
50
+ export { runCommand as a, warn as c, parsePort as i, info as n, step as o, interactive as r, stepInfo as s, detail as t };