@alexkroman1/aai-cli 1.9.2 → 1.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.
@@ -1,18 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import { a as readProjectConfig, c as __exportAll, i as readGlobalConfig, n as approveServer, o as serverOrigin, r as ensureApiKey } from "./_config-zV70t71V.mjs";
2
+ import { a as serverOrigin, i as readProjectConfig, n as ensureApiKey, r as readGlobalConfig, t as approveServer } from "./_config-BMjZt5hG.mjs";
3
3
  import { existsSync } from "node:fs";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  //#region _agent.ts
7
- var _agent_exports = /* @__PURE__ */ __exportAll({
8
- DEFAULT_DEV_SERVER: () => DEFAULT_DEV_SERVER,
9
- DEFAULT_SERVER: () => DEFAULT_SERVER,
10
- getMonorepoRoot: () => getMonorepoRoot,
11
- getServerInfo: () => getServerInfo,
12
- isDevMode: () => isDevMode,
13
- resolveDeployTarget: () => resolveDeployTarget,
14
- resolveServerUrl: () => resolveServerUrl
15
- });
16
7
  const DEFAULT_SERVER = "https://aai-agent.fly.dev";
17
8
  const DEFAULT_DEV_SERVER = "http://localhost:8080";
18
9
  let _cachedMonorepoRoot;
@@ -33,24 +24,19 @@ function isDevMode() {
33
24
  function stripTrailingSlash(url) {
34
25
  return url.replace(/\/+$/, "");
35
26
  }
36
- /** Hostnames that are always safe to target — the request never leaves the machine. */
37
- const LOOPBACK_HOSTNAMES = /* @__PURE__ */ new Set([
38
- "localhost",
39
- "127.0.0.1",
40
- "::1",
41
- "[::1]"
42
- ]);
43
27
  /**
44
- * Whether `origin` may receive a credential without prior user approval:
45
- * the shipped platform, or anything on this machine.
28
+ * Whether `origin` may receive a credential without prior user approval.
29
+ *
30
+ * Only the shipped platform qualifies. Loopback origins from
31
+ * `.aai/project.json` used to be implicitly trusted too, but that let a
32
+ * cloned repo steer a credentialed request (API key + `aai secret` values)
33
+ * to any process listening on a local port — the exact input class this
34
+ * trust model exists to gate. Dev mode still targets `DEFAULT_DEV_SERVER`
35
+ * before the project config is ever consulted, and `--server` approves a
36
+ * local origin like any other.
46
37
  */
47
38
  function isImplicitlyTrusted(origin) {
48
- if (origin === serverOrigin("https://aai-agent.fly.dev")) return true;
49
- try {
50
- return LOOPBACK_HOSTNAMES.has(new URL(origin).hostname);
51
- } catch {
52
- return false;
53
- }
39
+ return origin === serverOrigin(DEFAULT_SERVER);
54
40
  }
55
41
  /**
56
42
  * Resolve which platform server to talk to.
@@ -94,10 +80,20 @@ async function resolveDeployTarget(cwd, explicitServer) {
94
80
  apiKey: await ensureApiKey()
95
81
  };
96
82
  }
83
+ /**
84
+ * Slug shape accepted by the platform (kept in sync with `VALID_SLUG_RE` in
85
+ * aai-server's schemas.ts — the server package is private, so the pattern is
86
+ * duplicated here). Enforced before a slug is ever interpolated into a URL
87
+ * path: `.aai/project.json` is repo-controlled, so a hostile
88
+ * `"slug": "x/../admin"` must not steer a credentialed request to an
89
+ * arbitrary path on an approved origin.
90
+ */
91
+ const VALID_SLUG_RE = /^[a-z0-9][a-z0-9_-]{0,62}[a-z0-9]$/;
97
92
  /** Like resolveDeployTarget, but requires an existing deployment (project config). */
98
93
  async function getServerInfo(cwd, explicitServer) {
99
94
  const { config, serverUrl, apiKey } = await resolveDeployTarget(cwd, explicitServer);
100
95
  if (!config) throw new Error("No .aai/project.json found — run `aai deploy` first");
96
+ if (!VALID_SLUG_RE.test(config.slug)) throw new Error(`Invalid slug in .aai/project.json: ${JSON.stringify(config.slug)}\n Expected lowercase letters, digits, \`-\`, \`_\` (2-64 chars). Fix the file or run \`aai deploy\` to create a fresh deployment.`);
101
97
  return {
102
98
  serverUrl,
103
99
  slug: config.slug,
@@ -105,4 +101,4 @@ async function getServerInfo(cwd, explicitServer) {
105
101
  };
106
102
  }
107
103
  //#endregion
108
- export { resolveDeployTarget as a, isDevMode as i, getMonorepoRoot as n, getServerInfo as r, _agent_exports as t };
104
+ export { resolveDeployTarget as i, getServerInfo as n, isDevMode as r, getMonorepoRoot as t };
@@ -24,8 +24,8 @@ async function apiRequest(url, opts) {
24
24
  ...opts.headers
25
25
  },
26
26
  ...opts.body !== void 0 ? { body: opts.body } : {},
27
- retry: 2,
28
- retryDelay: 300
27
+ retry: opts.retry ?? 2,
28
+ retryDelay: opts.retryDelay ?? 300
29
29
  });
30
30
  } catch (err) {
31
31
  throw toApiError(err, url, opts);
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env node
2
+ import { n as log, u as ok } from "./_ui-CKEIHAtB.mjs";
3
+ import { c as validateAgentExport, r as errorMessage } from "./_utils-ECl2je7-.mjs";
4
+ import { t as buildClient } from "./client-bundler-VdcgFcos.mjs";
5
+ import { buildWorker } from "./worker-bundler.mjs";
6
+ import path from "node:path";
7
+ import { pathToFileURL } from "node:url";
8
+ import fs from "node:fs/promises";
9
+ import { createHash } from "node:crypto";
10
+ import { agentToolsToSchemas, toAgentConfig } from "@alexkroman1/aai/manifest";
11
+ //#region _bundler.ts
12
+ /**
13
+ * Bundle an agent directory: build agent.ts into worker ESM + extract config.
14
+ *
15
+ * - agent.ts is the single entry point: `export default agent({...})`
16
+ * - A single Vite build produces the worker ESM (all deps bundled in).
17
+ * The AgentDef is extracted from that bundle via dynamic import, avoiding a
18
+ * second build pass.
19
+ */
20
+ async function buildAgentBundle(cwd, opts = {}) {
21
+ const [[worker, agentDef], clientFiles] = await Promise.all([buildWorker(cwd, opts).then(async (code) => [code, await evalWorkerBundle(code, cwd)]), buildClient(cwd)]);
22
+ log.step(`Bundling ${agentDef.name}`);
23
+ const config = toAgentConfig(agentDef);
24
+ const toolSchemas = agentToolsToSchemas(agentDef.tools ?? {});
25
+ return {
26
+ worker,
27
+ clientFiles,
28
+ agentConfig: {
29
+ ...config,
30
+ toolSchemas
31
+ }
32
+ };
33
+ }
34
+ /**
35
+ * Write the worker ESM to a temp file and dynamic-import it, returning
36
+ * the AgentDef default export. All dependencies are bundled in, so the
37
+ * file can be evaluated from any directory.
38
+ *
39
+ * Each call imports a uniquely-named file, and Node's ESM registry never
40
+ * evicts — so every call retains one bundle for the process lifetime. That is
41
+ * fine for one-shot commands (`aai build`/`aai deploy`); long-lived callers
42
+ * must go through `createWorkerEvaluator` to at least dedupe identical
43
+ * builds. Evaluating in a discardable context is not an option: tool
44
+ * `execute` functions from the returned AgentDef are called in-process by the
45
+ * dev runtime, which rules out worker threads, and `node:vm` ESM evaluation
46
+ * is still flagged experimental.
47
+ */
48
+ async function evalWorkerBundle(code, cwd) {
49
+ const evalDir = path.join(cwd, ".aai", "eval");
50
+ const tmpPath = path.join(evalDir, `agent-${Date.now()}-${Math.random().toString(36).slice(2)}.mjs`);
51
+ try {
52
+ await fs.mkdir(evalDir, { recursive: true });
53
+ await fs.writeFile(tmpPath, code);
54
+ } catch (err) {
55
+ await fs.rm(tmpPath, { force: true }).catch(() => void 0);
56
+ throw new Error(`Failed to write the eval bundle under ${evalDir} — is the project directory writable? (${errorMessage(err)})`, { cause: err });
57
+ }
58
+ try {
59
+ const mod = await import(pathToFileURL(tmpPath).href);
60
+ const agentDef = mod.default ?? mod;
61
+ validateAgentExport(agentDef);
62
+ return agentDef;
63
+ } finally {
64
+ await fs.rm(tmpPath).catch(() => {});
65
+ }
66
+ }
67
+ /**
68
+ * Memoizing wrapper around `evalWorkerBundle` for long-lived callers (the
69
+ * dev server): byte-identical worker code returns the previously evaluated
70
+ * AgentDef without touching the ESM registry. No-op saves and formatter
71
+ * churn are the common watcher events, so this caps the registry leak (see
72
+ * `evalWorkerBundle`) to genuinely-new bundles — the residual one-module-per-
73
+ * distinct-build leak is accepted for the reasons documented there.
74
+ */
75
+ function createWorkerEvaluator(cwd) {
76
+ let lastHash;
77
+ let lastAgentDef;
78
+ return async (code) => {
79
+ const hash = createHash("sha256").update(code).digest("hex");
80
+ if (lastAgentDef && hash === lastHash) return lastAgentDef;
81
+ const agentDef = await evalWorkerBundle(code, cwd);
82
+ lastHash = hash;
83
+ lastAgentDef = agentDef;
84
+ return agentDef;
85
+ };
86
+ }
87
+ async function executeBuild(cwd) {
88
+ const bundle = await buildAgentBundle(cwd, { minify: true });
89
+ log.success("Build complete");
90
+ return ok({
91
+ name: bundle.agentConfig.name,
92
+ workerBytes: bundle.worker.length
93
+ });
94
+ }
95
+ //#endregion
96
+ export { buildAgentBundle, createWorkerEvaluator, executeBuild };
@@ -1,35 +1,13 @@
1
1
  #!/usr/bin/env node
2
- import { a as unwrapCancel } from "./_ui-YqQ6Fi8K.mjs";
3
- import { a as readJson, c as writeJson } from "./_utils-BeU10C7O.mjs";
4
- import "node:module";
2
+ import { a as unwrapCancel, n as log, o as CliError } from "./_ui-CKEIHAtB.mjs";
3
+ import { l as writeJson, o as readJson, r as errorMessage } from "./_utils-ECl2je7-.mjs";
4
+ import { existsSync } from "node:fs";
5
5
  import path from "node:path";
6
6
  import * as p from "@clack/prompts";
7
7
  import os from "node:os";
8
- import { consola } from "consola";
8
+ import envPaths from "env-paths";
9
9
  import { z } from "zod";
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
22
10
  //#region _config.ts
23
- var _config_exports = /* @__PURE__ */ __exportAll({
24
- approveServer: () => approveServer,
25
- ensureApiKey: () => ensureApiKey,
26
- getConfigDir: () => getConfigDir,
27
- readGlobalConfig: () => readGlobalConfig,
28
- readProjectConfig: () => readProjectConfig,
29
- serverOrigin: () => serverOrigin,
30
- writeGlobalConfig: () => writeGlobalConfig,
31
- writeProjectConfig: () => writeProjectConfig
32
- });
33
11
  /**
34
12
  * `.aai/project.json` lives in the working tree, so everything in it is
35
13
  * untrusted input — a cloned repo can supply any value.
@@ -43,20 +21,48 @@ var _config_exports = /* @__PURE__ */ __exportAll({
43
21
  */
44
22
  const ProjectConfigSchema = z.object({
45
23
  slug: z.string(),
46
- serverUrl: z.string(),
47
- sessionId: z.string().optional()
24
+ serverUrl: z.string()
48
25
  });
49
- function getConfigDir() {
26
+ /**
27
+ * Config dir the CLI used before switching to env-paths. On Linux it matches
28
+ * env-paths exactly; on macOS (XDG-style vs ~/Library/Preferences/aai) and
29
+ * Windows (no trailing `Config` segment) it does not.
30
+ */
31
+ function legacyConfigDir() {
50
32
  if (process.platform === "win32") return path.join(process.env.APPDATA ?? path.join(os.homedir(), "AppData", "Roaming"), "aai");
51
33
  return path.join(process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), ".config"), "aai");
52
34
  }
35
+ /**
36
+ * Resolve the global config directory, preferring the platform-conventional
37
+ * env-paths location while staying backward compatible: an existing config at
38
+ * the legacy path keeps winning so already-authenticated users are not
39
+ * silently logged out. Injectable for tests.
40
+ *
41
+ * `AAI_CONFIG_DIR` overrides everything — it exists so tests (and unusual
42
+ * setups) can redirect ALL global-config reads and writes away from the
43
+ * user's real config. The test suite's `approveServer` calls used to
44
+ * permanently pollute `~/.config/aai/config.json` with approved origins.
45
+ */
46
+ function getConfigDir(dirs = {
47
+ legacy: legacyConfigDir(),
48
+ modern: envPaths("aai", { suffix: "" }).config
49
+ }, exists = existsSync) {
50
+ const override = process.env.AAI_CONFIG_DIR?.trim();
51
+ if (override) return override;
52
+ return exists(path.join(dirs.legacy, "config.json")) ? dirs.legacy : dirs.modern;
53
+ }
53
54
  async function readProjectConfig(agentDir) {
54
55
  const file = path.join(agentDir, ".aai", "project.json");
55
- const parsed = ProjectConfigSchema.safeParse(await readJson(file));
56
- if (!parsed.success) {
57
- consola.debug(`Failed to read project config from ${file}:`, parsed.error);
58
- return null;
56
+ let data;
57
+ try {
58
+ data = await readJson(file);
59
+ } catch (err) {
60
+ const reason = errorMessage(err).replace(`Invalid JSON in ${file}: `, "");
61
+ throw new Error(`project.json is corrupted at ${file}: ${reason}\n Fix or delete the file — deploying without it would create a new agent under a fresh slug.`, { cause: err });
59
62
  }
63
+ if (data === null) return null;
64
+ const parsed = ProjectConfigSchema.safeParse(data);
65
+ if (!parsed.success) return null;
60
66
  return parsed.data;
61
67
  }
62
68
  async function writeProjectConfig(agentDir, data) {
@@ -100,7 +106,21 @@ async function readGlobalConfig(configDir) {
100
106
  return await readJson(path.join(dir, "config.json")) ?? {};
101
107
  }
102
108
  async function writeGlobalConfig(configDir, data) {
103
- await writeJson(path.join(configDir, "config.json"), data);
109
+ await writeJson(path.join(configDir, "config.json"), data, { mode: 384 });
110
+ }
111
+ /**
112
+ * Persist the API key to the global config, warning (not failing) when the
113
+ * config dir is unwritable — the key in hand still works for this run.
114
+ */
115
+ async function trySaveApiKey(dir, config, apiKey) {
116
+ try {
117
+ await writeGlobalConfig(dir, {
118
+ ...config,
119
+ apiKey
120
+ });
121
+ } catch (err) {
122
+ log.warn(`Couldn't save your API key to ${path.join(dir, "config.json")}: ${errorMessage(err)} — you'll be prompted again next run.`);
123
+ }
104
124
  }
105
125
  async function ensureApiKey(configDir) {
106
126
  const dir = configDir ?? getConfigDir();
@@ -108,18 +128,13 @@ async function ensureApiKey(configDir) {
108
128
  if (config.apiKey) return config.apiKey;
109
129
  const envKey = process.env.ASSEMBLYAI_API_KEY;
110
130
  if (envKey) {
111
- await writeGlobalConfig(dir, {
112
- ...config,
113
- apiKey: envKey
114
- });
131
+ await trySaveApiKey(dir, config, envKey);
115
132
  return envKey;
116
133
  }
134
+ 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.");
117
135
  const apiKey = unwrapCancel(await p.password({ message: "Enter your AssemblyAI API key" }));
118
- await writeGlobalConfig(dir, {
119
- ...config,
120
- apiKey
121
- });
136
+ await trySaveApiKey(dir, config, apiKey);
122
137
  return apiKey;
123
138
  }
124
139
  //#endregion
125
- export { readProjectConfig as a, __exportAll as c, readGlobalConfig as i, approveServer as n, serverOrigin as o, ensureApiKey as r, writeProjectConfig as s, _config_exports as t };
140
+ export { serverOrigin as a, readProjectConfig as i, ensureApiKey as n, writeProjectConfig as o, readGlobalConfig as r, approveServer as t };