@alexkroman1/aai-cli 0.10.3 → 0.11.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.
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env node
2
+ //#region _api-client.ts
3
+ /**
4
+ * Shared HTTP helpers for platform API calls (deploy, delete, secrets).
5
+ */
6
+ const HINT_INVALID_API_KEY = "Your API key may be invalid. Check ~/.config/aai/config.json or set ASSEMBLYAI_API_KEY.";
7
+ /**
8
+ * Send an authenticated request to the platform API.
9
+ *
10
+ * Adds the `Authorization` header and, on network failure, throws with a
11
+ * contextual hint (localhost → "is the dev server running?", remote →
12
+ * "check your network connection").
13
+ */
14
+ async function apiRequest(url, init, fetchFn = globalThis.fetch.bind(globalThis)) {
15
+ const { apiKey, action, ...rest } = init;
16
+ const headers = {
17
+ Authorization: `Bearer ${apiKey}`,
18
+ ...rest.body ? { "Content-Type": "application/json" } : {},
19
+ ...rest.headers
20
+ };
21
+ try {
22
+ return await fetchFn(url, {
23
+ ...rest,
24
+ headers
25
+ });
26
+ } catch (err) {
27
+ throw new Error(`${action} failed: could not reach ${url}\n Check your network connection and verify the server URL is correct.`, { cause: err });
28
+ }
29
+ }
30
+ /** Format a non-ok API response into a descriptive error. */
31
+ function apiError(action, status, body, hint) {
32
+ return /* @__PURE__ */ new Error(`${action} failed (HTTP ${status}): ${body}${hint ? `\n ${hint}` : ""}`);
33
+ }
34
+ //#endregion
35
+ export { apiError as n, apiRequest as r, HINT_INVALID_API_KEY as t };
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env node
2
+ import path from "node:path";
3
+ import fs from "node:fs/promises";
4
+ import { errorMessage } from "@alexkroman1/aai/utils";
5
+ import { build } from "vite";
6
+ //#region _bundler.ts
7
+ var BundleError = class extends Error {
8
+ constructor(message, options) {
9
+ super(message, options);
10
+ this.name = "BundleError";
11
+ }
12
+ };
13
+ const TEXT_EXTENSIONS = new Set([
14
+ ".html",
15
+ ".htm",
16
+ ".css",
17
+ ".js",
18
+ ".mjs",
19
+ ".cjs",
20
+ ".ts",
21
+ ".mts",
22
+ ".json",
23
+ ".map",
24
+ ".svg",
25
+ ".xml",
26
+ ".txt",
27
+ ".md"
28
+ ]);
29
+ async function readDirFiles(dir) {
30
+ let entries;
31
+ try {
32
+ entries = await fs.readdir(dir, {
33
+ recursive: true,
34
+ withFileTypes: true
35
+ });
36
+ } catch (err) {
37
+ if (err instanceof Error && "code" in err && err.code === "ENOENT") return {};
38
+ throw err;
39
+ }
40
+ const files = {};
41
+ await Promise.all(entries.filter((e) => e.isFile()).map(async (e) => {
42
+ const full = path.join(e.parentPath, e.name);
43
+ const ext = path.extname(e.name).toLowerCase();
44
+ const rel = path.relative(dir, full);
45
+ if (TEXT_EXTENSIONS.has(ext)) files[rel] = await fs.readFile(full, "utf-8");
46
+ else files[rel] = `base64:${(await fs.readFile(full)).toString("base64")}`;
47
+ }));
48
+ return files;
49
+ }
50
+ /**
51
+ * Bundle an agent project using Vite.
52
+ *
53
+ * - Worker: `vite build --ssr agent.ts` (uses project's vite.config.ts)
54
+ * - Client: `vite build` (uses project's vite.config.ts)
55
+ */
56
+ async function bundleAgent(agent, opts) {
57
+ const aaiDir = path.join(agent.dir, ".aai");
58
+ const buildDir = path.join(aaiDir, "build");
59
+ const clientDir = path.join(aaiDir, "client");
60
+ try {
61
+ await build({
62
+ root: agent.dir,
63
+ logLevel: "warn",
64
+ build: {
65
+ ssr: path.join(agent.dir, "agent.ts"),
66
+ outDir: buildDir,
67
+ emptyOutDir: true,
68
+ rollupOptions: { output: { entryFileNames: "worker.js" } }
69
+ }
70
+ });
71
+ } catch (err) {
72
+ throw new BundleError(errorMessage(err), { cause: err });
73
+ }
74
+ if (!(opts?.skipClient ?? !agent.clientEntry)) try {
75
+ await build({
76
+ root: agent.dir,
77
+ base: "./",
78
+ logLevel: "warn",
79
+ build: {
80
+ outDir: clientDir,
81
+ emptyOutDir: true
82
+ }
83
+ });
84
+ } catch (err) {
85
+ throw new BundleError(errorMessage(err), { cause: err });
86
+ }
87
+ const worker = await fs.readFile(path.join(buildDir, "worker.js"), "utf-8");
88
+ const clientFiles = await readDirFiles(clientDir);
89
+ return {
90
+ slug: agent.slug,
91
+ worker,
92
+ clientFiles,
93
+ clientDir,
94
+ workerBytes: Buffer.byteLength(worker)
95
+ };
96
+ }
97
+ async function buildAgentBundle(cwd) {
98
+ const { loadAgent } = await import("./_discover-a8yIuqEp.mjs").then((n) => n.t);
99
+ const { log } = await import("./_ui-DWGXImbO.mjs");
100
+ const agent = await loadAgent(cwd);
101
+ if (!agent) throw new Error("No agent found — run `aai init` first");
102
+ log.step(`Bundling ${agent.slug}`);
103
+ let bundle;
104
+ try {
105
+ bundle = await bundleAgent(agent);
106
+ } catch (err) {
107
+ if (err instanceof BundleError) throw new Error(`Build failed: ${err.message}`, { cause: err });
108
+ throw err;
109
+ }
110
+ return bundle;
111
+ }
112
+ async function runBuildCommand(cwd) {
113
+ const { log } = await import("./_ui-DWGXImbO.mjs");
114
+ await buildAgentBundle(cwd);
115
+ log.success("Build complete");
116
+ }
117
+ //#endregion
118
+ export { buildAgentBundle, runBuildCommand };
@@ -2,13 +2,24 @@
2
2
  import { existsSync } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- import fs$1 from "node:fs/promises";
5
+ import fs from "node:fs/promises";
6
6
  import { consola } from "consola";
7
7
  import { humanId } from "human-id";
8
8
  import { z } from "zod";
9
9
  import { createInterface } from "node:readline";
10
+ //#region \0rolldown/runtime.js
11
+ var __defProp = Object.defineProperty;
12
+ var __exportAll = (all, no_symbols) => {
13
+ let target = {};
14
+ for (var name in all) __defProp(target, name, {
15
+ get: all[name],
16
+ enumerable: true
17
+ });
18
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
19
+ return target;
20
+ };
21
+ //#endregion
10
22
  //#region _prompts.ts
11
- const consola$1 = consola.create({ formatOptions: { date: false } });
12
23
  /**
13
24
  * Prompt the user for a password (masked input).
14
25
  * Returns the entered string.
@@ -47,20 +58,23 @@ function readMasked(stdin) {
47
58
  stdin.on("data", onData);
48
59
  });
49
60
  }
50
- /**
51
- * Prompt the user for text input with a default value.
52
- * Returns the entered string, or the default if empty.
53
- */
54
- async function askText(message, defaultValue) {
55
- return await consola$1.prompt(message, {
56
- type: "text",
57
- placeholder: defaultValue,
58
- initial: defaultValue,
59
- cancel: "reject"
60
- }) || defaultValue;
61
- }
62
61
  //#endregion
63
62
  //#region _discover.ts
63
+ var _discover_exports = /* @__PURE__ */ __exportAll({
64
+ DEFAULT_DEV_SERVER: () => DEFAULT_DEV_SERVER,
65
+ DEFAULT_SERVER: () => DEFAULT_SERVER,
66
+ ensureApiKeyInEnv: () => ensureApiKeyInEnv,
67
+ fileExists: () => fileExists,
68
+ generateSlug: () => generateSlug,
69
+ getApiKey: () => getApiKey,
70
+ getServerInfo: () => getServerInfo,
71
+ isDevMode: () => isDevMode,
72
+ loadAgent: () => loadAgent,
73
+ readProjectConfig: () => readProjectConfig,
74
+ resolveCwd: () => resolveCwd,
75
+ resolveServerUrl: () => resolveServerUrl,
76
+ writeProjectConfig: () => writeProjectConfig
77
+ });
64
78
  const AuthConfigSchema = z.object({ assemblyai_api_key: z.string().optional() });
65
79
  const ProjectConfigSchema = z.object({
66
80
  slug: z.string(),
@@ -84,15 +98,16 @@ const CONFIG_DIR = path.join(process.env.HOME ?? process.env.USERPROFILE ?? ".",
84
98
  const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
85
99
  async function readAuthConfig() {
86
100
  try {
87
- return AuthConfigSchema.parse(JSON.parse(await fs$1.readFile(CONFIG_FILE, "utf-8")));
88
- } catch {
101
+ return AuthConfigSchema.parse(JSON.parse(await fs.readFile(CONFIG_FILE, "utf-8")));
102
+ } catch (error) {
103
+ consola.debug(`Failed to read auth config from ${CONFIG_FILE}:`, error);
89
104
  return {};
90
105
  }
91
106
  }
92
107
  async function writeAuthConfig(config) {
93
- await fs$1.mkdir(CONFIG_DIR, { recursive: true });
94
- await fs$1.writeFile(CONFIG_FILE, `${JSON.stringify(config, null, 2)}\n`);
95
- if (process.platform !== "win32") await fs$1.chmod(CONFIG_FILE, 384);
108
+ await fs.mkdir(CONFIG_DIR, { recursive: true });
109
+ await fs.writeFile(CONFIG_FILE, `${JSON.stringify(config, null, 2)}\n`);
110
+ if (process.platform !== "win32") await fs.chmod(CONFIG_FILE, 384);
96
111
  }
97
112
  /**
98
113
  * Retrieves the AssemblyAI API key from `process.env`, `~/.config/aai/config.json`,
@@ -105,8 +120,9 @@ async function getApiKey() {
105
120
  if (process.env.ASSEMBLYAI_API_KEY) return process.env.ASSEMBLYAI_API_KEY;
106
121
  const config = await readAuthConfig();
107
122
  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");
123
+ const { log } = await import("./_ui-DWGXImbO.mjs");
124
+ log.info("Get your API key at https://www.assemblyai.com/dashboard/signup");
125
+ log.info("Or set the ASSEMBLYAI_API_KEY environment variable to skip this prompt.");
110
126
  let key;
111
127
  while (!key) key = await askPassword("ASSEMBLYAI_API_KEY");
112
128
  config.assemblyai_api_key = key;
@@ -128,8 +144,9 @@ async function ensureApiKeyInEnv() {
128
144
  */
129
145
  async function readProjectConfig(agentDir) {
130
146
  try {
131
- return ProjectConfigSchema.parse(JSON.parse(await fs$1.readFile(path.join(agentDir, ".aai", "project.json"), "utf-8")));
132
- } catch {
147
+ return ProjectConfigSchema.parse(JSON.parse(await fs.readFile(path.join(agentDir, ".aai", "project.json"), "utf-8")));
148
+ } catch (error) {
149
+ consola.debug(`Failed to read project config from ${path.join(agentDir, ".aai", "project.json")}:`, error);
133
150
  return null;
134
151
  }
135
152
  }
@@ -138,16 +155,16 @@ async function readProjectConfig(agentDir) {
138
155
  */
139
156
  async function writeProjectConfig(agentDir, data) {
140
157
  const aaiDir = path.join(agentDir, ".aai");
141
- await fs$1.mkdir(aaiDir, { recursive: true });
142
- await fs$1.writeFile(path.join(aaiDir, "project.json"), `${JSON.stringify(data, null, 2)}\n`);
158
+ await fs.mkdir(aaiDir, { recursive: true });
159
+ await fs.writeFile(path.join(aaiDir, "project.json"), `${JSON.stringify(data, null, 2)}\n`);
143
160
  }
144
161
  /**
145
162
  * Read project config (throws if missing), resolve API key and server URL.
146
- * Shared by secret and rag commands.
163
+ * Shared by secret commands.
147
164
  */
148
165
  async function getServerInfo(cwd, explicitServer, explicitApiKey) {
149
166
  const config = await readProjectConfig(cwd);
150
- if (!config) throw new Error("No .aai/project.json found — deploy first with `aai deploy`");
167
+ if (!config) throw new Error("No .aai/project.json found — run `aai deploy` first");
151
168
  const apiKey = explicitApiKey ?? await getApiKey();
152
169
  return {
153
170
  serverUrl: resolveServerUrl(explicitServer, config.serverUrl),
@@ -155,6 +172,8 @@ async function getServerInfo(cwd, explicitServer, explicitApiKey) {
155
172
  apiKey
156
173
  };
157
174
  }
175
+ /** Default production server URL for agent deployments. */
176
+ const DEFAULT_SERVER = "https://aai-agent.fly.dev";
158
177
  /** Default local dev server URL. */
159
178
  const DEFAULT_DEV_SERVER = "http://localhost:8787";
160
179
  /** Check if the CLI is running from the monorepo (dev mode). */
@@ -172,9 +191,10 @@ function resolveServerUrl(explicit, configUrl) {
172
191
  }
173
192
  async function fileExists(p) {
174
193
  try {
175
- await fs$1.access(p);
194
+ await fs.access(p);
176
195
  return true;
177
- } catch {
196
+ } catch (error) {
197
+ consola.debug(`File access check failed for ${p}:`, error);
178
198
  return false;
179
199
  }
180
200
  }
@@ -197,4 +217,4 @@ async function loadAgent(dir) {
197
217
  };
198
218
  }
199
219
  //#endregion
200
- 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 };
220
+ export { getApiKey as a, readProjectConfig as c, writeProjectConfig as d, askPassword as f, generateSlug as i, resolveCwd as l, ensureApiKeyInEnv as n, getServerInfo as o, fileExists as r, isDevMode as s, _discover_exports as t, resolveServerUrl as u };
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env node
2
+ import { t as downloadAndMergeTemplate } from "./_templates-CtZBILce.mjs";
3
+ import path from "node:path";
4
+ import fs from "node:fs/promises";
5
+ //#region _init.ts
6
+ async function runInit(opts) {
7
+ const { targetDir, template } = opts;
8
+ await downloadAndMergeTemplate(template, targetDir);
9
+ try {
10
+ await fs.copyFile(path.join(targetDir, ".env.example"), path.join(targetDir, ".env"));
11
+ } catch {}
12
+ const readmePath = path.join(targetDir, "README.md");
13
+ const readme = `# ${path.basename(path.resolve(targetDir))}
14
+
15
+ A voice agent built with [aai](https://github.com/anthropics/aai).
16
+
17
+ ## Getting started
18
+
19
+ \`\`\`sh
20
+ npm install # Install dependencies
21
+ aai dev # Run locally (opens browser)
22
+ aai deploy # Deploy to production
23
+ \`\`\`
24
+
25
+ ## Secrets
26
+
27
+ Access secrets in your agent via \`ctx.env.MY_KEY\`.
28
+
29
+ **Local development** — add secrets to \`.env\` (auto-loaded by \`aai dev\`):
30
+
31
+ \`\`\`sh
32
+ ALPHA_VANTAGE_KEY=sk-abc123
33
+ MY_API_KEY=secret-value
34
+ \`\`\`
35
+
36
+ **Production** — set secrets on the server:
37
+
38
+ \`\`\`sh
39
+ aai secret put MY_KEY # Set a secret (prompts for value)
40
+ aai secret list # List secret names
41
+ aai secret delete MY_KEY # Remove a secret
42
+ \`\`\`
43
+
44
+ ## Learn more
45
+
46
+ See \`CLAUDE.md\` for the full agent API reference.
47
+ `;
48
+ try {
49
+ await fs.writeFile(readmePath, readme, { flag: "wx" });
50
+ } catch (err) {
51
+ if (!(err instanceof Error && "code" in err && err.code === "EEXIST")) throw err;
52
+ }
53
+ return targetDir;
54
+ }
55
+ //#endregion
56
+ export { runInit };
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env node
2
+ import { s as isDevMode } from "./_discover-a8yIuqEp.mjs";
3
+ import { existsSync } from "node:fs";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import fs 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
+ /** List available templates with descriptions. */
27
+ async function listTemplates() {
28
+ const root = await resolveTemplatesDir();
29
+ const dir = path.join(root, "templates");
30
+ const names = (await fs.readdir(dir, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name).sort((a, b) => a.localeCompare(b));
31
+ let descriptions = {};
32
+ try {
33
+ descriptions = JSON.parse(await fs.readFile(path.join(root, "templates.json"), "utf-8"));
34
+ } catch {}
35
+ return names.map((name) => ({
36
+ name,
37
+ description: descriptions[name] ?? ""
38
+ }));
39
+ }
40
+ /**
41
+ * Download a template into targetDir, merging scaffold files underneath.
42
+ */
43
+ async function downloadAndMergeTemplate(template, targetDir) {
44
+ const root = await resolveTemplatesDir();
45
+ const templatesDir = path.join(root, "templates");
46
+ const names = (await fs.readdir(templatesDir, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name);
47
+ if (!names.includes(template)) throw new Error(`Unknown template "${template}". Available templates: ${names.join(", ")}`);
48
+ await fs.cp(path.join(templatesDir, template), targetDir, {
49
+ recursive: true,
50
+ force: true
51
+ });
52
+ const scaffoldDir = path.join(root, "scaffold");
53
+ if (existsSync(scaffoldDir)) {
54
+ const entries = await fs.readdir(scaffoldDir, {
55
+ recursive: true,
56
+ withFileTypes: true
57
+ });
58
+ for (const entry of entries) {
59
+ if (!entry.isFile()) continue;
60
+ const rel = path.relative(scaffoldDir, path.join(entry.parentPath, entry.name));
61
+ const destPath = path.join(targetDir, rel);
62
+ await fs.mkdir(path.dirname(destPath), { recursive: true });
63
+ try {
64
+ await fs.copyFile(path.join(scaffoldDir, rel), destPath, fs.constants.COPYFILE_EXCL);
65
+ } catch (err) {
66
+ if (!(err instanceof Error && "code" in err && err.code === "EEXIST")) throw err;
67
+ }
68
+ }
69
+ }
70
+ }
71
+ //#endregion
72
+ export { listTemplates as n, downloadAndMergeTemplate as t };
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+ import * as p from "@clack/prompts";
3
+ import { colorize } from "consola/utils";
4
+ //#region _ui.ts
5
+ /**
6
+ * Unified CLI output using @clack/prompts style (◐ ◇ │).
7
+ *
8
+ * All commands should use these helpers instead of consola directly
9
+ * so the output is visually consistent.
10
+ */
11
+ const log = {
12
+ step: (msg) => p.log.step(msg),
13
+ success: (msg) => p.log.success(msg),
14
+ info: (msg) => p.log.info(msg),
15
+ warn: (msg) => p.log.warn(msg),
16
+ error: (msg) => p.log.error(msg),
17
+ message: (msg) => p.log.message(msg)
18
+ };
19
+ /** Format a URL for display. */
20
+ function fmtUrl(url) {
21
+ return colorize("cyanBright", url);
22
+ }
23
+ /** Parse and validate a port string. Returns the numeric port or throws. */
24
+ function parsePort(raw) {
25
+ const port = Number.parseInt(raw, 10);
26
+ if (Number.isNaN(port) || port < 0 || port > 65535) throw new Error(`Invalid port: ${raw}. Must be a number between 0 and 65535.`);
27
+ return port;
28
+ }
29
+ //#endregion
30
+ export { fmtUrl, log, parsePort };