@alexkroman1/aai-cli 5.8.1 → 5.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 (46) hide show
  1. package/README.md +5 -1
  2. package/bin.mjs +32 -0
  3. package/dist/{_agent-2nVugrN3.mjs → _agent-DS2PUJcl.mjs} +1 -1
  4. package/dist/{_bundler-Cjaxa2wi.mjs → _bundler-DC17suWN.mjs} +2 -2
  5. package/dist/_config-DMyolIk9.mjs +242 -0
  6. package/dist/_config.d.ts +28 -9
  7. package/dist/_dev-env.d.ts +38 -0
  8. package/dist/{_dev-server-BB5N8kdh.mjs → _dev-server-B3ivqyEd.mjs} +75 -55
  9. package/dist/_dev-server.d.ts +2 -2
  10. package/dist/{_init-Dei2JRmn.mjs → _init-CYLvYU-B.mjs} +3 -3
  11. package/dist/{_ui-8kOEB-JH.mjs → _output-CC300DzW.mjs} +1 -42
  12. package/dist/{_server-common-CnaP_Urf.mjs → _server-common-DX8Bfrf5.mjs} +1 -1
  13. package/dist/{_slug-api-CGJSST9B.mjs → _slug-api-fRNNR8tz.mjs} +1 -1
  14. package/dist/{_templates-BfyRyE7z.mjs → _templates-BWJOiWOO.mjs} +2 -2
  15. package/dist/{_typecheck-gate-DvE8S3aQ.mjs → _typecheck-gate-DB-PY0A3.mjs} +2 -1
  16. package/dist/_ui-DfwfDbT-.mjs +68 -0
  17. package/dist/_ui.d.ts +19 -0
  18. package/dist/{_utils-Ch0J4s6a.mjs → _utils-8KKw-bzi.mjs} +9 -2
  19. package/dist/_utils.d.ts +9 -2
  20. package/dist/{build-BXwDB78d.mjs → build-CACFbdQ4.mjs} +5 -4
  21. package/dist/cli.mjs +21 -20
  22. package/dist/{client-bundler-yiWoXrgb.mjs → client-bundler-DONm-khu.mjs} +1 -1
  23. package/dist/client-bundler.mjs +1 -1
  24. package/dist/{delete-DRNfvczK.mjs → delete-DEZ7u3u4.mjs} +6 -4
  25. package/dist/delete.d.ts +5 -0
  26. package/dist/{deploy-Cp-wgME3.mjs → deploy-Ch0d_jje.mjs} +8 -7
  27. package/dist/{dev-C4KyxouE.mjs → dev-CdeRcYiQ.mjs} +7 -6
  28. package/dist/{init-BpBbttOv.mjs → init-Nc6fB774.mjs} +6 -5
  29. package/dist/{login-AA_UdRI-.mjs → login-BeFUiU6M.mjs} +9 -9
  30. package/dist/scaffold/CLAUDE.md +39 -14
  31. package/dist/scaffold/package.json +3 -3
  32. package/dist/{secret-Dr0qnxeb.mjs → secret-4_dYyrpA.mjs} +3 -2
  33. package/dist/{storage-CoQB8d-u.mjs → storage-BTfErOOW.mjs} +3 -2
  34. package/dist/{studio-_UBBvh5V.mjs → studio-LNvXtWak.mjs} +7 -6
  35. package/dist/templates/dispatch-center/agent.ts +4 -6
  36. package/dist/templates/retail/agent.ts +4 -6
  37. package/dist/templates/retail/registry.test.ts +5 -3
  38. package/dist/templates/retail/resolve.test.ts +6 -4
  39. package/dist/templates/retail/seed.test.ts +21 -13
  40. package/dist/templates/retail/shared.test.ts +9 -5
  41. package/dist/templates/solo-rpg/agent.test.ts +7 -5
  42. package/dist/templates/solo-rpg/shared.ts +2 -2
  43. package/dist/{test-3Gq4pqH_.mjs → test-C-V98oC-.mjs} +4 -4
  44. package/dist/typecheck.mjs +65 -14
  45. package/package.json +5 -4
  46. package/dist/_config-Y5V-5Krn.mjs +0 -173
package/README.md CHANGED
@@ -52,8 +52,12 @@ required credentials, and print the agent's public URL.
52
52
  outside one it runs `init`.
53
53
  - `--server <url>` targets a self-hosted platform; the origin is remembered
54
54
  in your user config after explicit approval.
55
+ - `aai login` is the only way to authenticate — it links an account already
56
+ signed in to the studio. An exported `ASSEMBLYAI_API_KEY` does not log you
57
+ in; in a project that variable is a provider credential for `aai dev`.
55
58
  - The API key is stored `0600` in your user config dir (`AAI_CONFIG_DIR`
56
- overrides the location).
59
+ overrides the location). Non-interactive callers point `AAI_CONFIG_DIR` at a
60
+ config dir holding a key from an interactive `aai login`.
57
61
 
58
62
  ## Documentation
59
63
 
package/bin.mjs ADDED
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+ // The `aai` bin in BOTH layouts — the source checkout (`pnpm link --global`,
3
+ // where it loads `cli.ts` directly) and the published tarball (where only
4
+ // `dist/` ships, so it loads `dist/cli.mjs`). It used to be the dev bin only,
5
+ // with `publishConfig.bin` pointing straight at `dist/cli.mjs`.
6
+ //
7
+ // One bin for both is what makes the compile cache reachable. The cache only
8
+ // covers modules compiled AFTER the call, and every dependency of the CLI is
9
+ // external (`deps.neverBundle` in tsdown.config.ts), so `dist/cli.mjs` carries
10
+ // hoisted `import` statements for citty, execa and the rest — all evaluated
11
+ // before any statement in that file could run. A banner or a first-line call
12
+ // inside `cli.ts` would therefore cache nothing that costs anything. Loading
13
+ // the entry through a DYNAMIC import from a wrapper is the only ordering that
14
+ // puts the enable genuinely first.
15
+ import { existsSync } from "node:fs";
16
+ import { enableCompileCache } from "node:module";
17
+ import { fileURLToPath } from "node:url";
18
+
19
+ // Caches V8 bytecode per (Node version, file content) under the user's cache
20
+ // dir, so `aai` pays parse+compile once per version rather than per
21
+ // invocation. Deliberately unguarded: this returns a `{ status }` result and
22
+ // does not throw, so an unwritable cache dir degrades to today's behaviour.
23
+ enableCompileCache();
24
+
25
+ // Source wins when present: a checkout that has also been built must keep
26
+ // running `cli.ts`, or `pnpm link --global` would silently serve a stale
27
+ // `dist/` instead of the working tree.
28
+ const source = new URL("./cli.ts", import.meta.url);
29
+ const entry = existsSync(fileURLToPath(source))
30
+ ? source
31
+ : new URL("./dist/cli.mjs", import.meta.url);
32
+ await import(entry.href);
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { a as readProjectConfig, i as readGlobalConfig, n as ensureApiKey, o as serverOrigin, t as approveServer } from "./_config-Y5V-5Krn.mjs";
2
+ import { a as serverOrigin, i as readProjectConfig, n as ensureApiKey, r as readGlobalConfig, t as approveServer } from "./_config-DMyolIk9.mjs";
3
3
  import { existsSync } from "node:fs";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { u as validateAgentExport } from "./_utils-Ch0J4s6a.mjs";
3
- import { t as buildClient } from "./client-bundler-yiWoXrgb.mjs";
2
+ import { u as validateAgentExport } from "./_utils-8KKw-bzi.mjs";
3
+ import { t as buildClient } from "./client-bundler-DONm-khu.mjs";
4
4
  import { buildWorker } from "./worker-bundler.mjs";
5
5
  import path from "node:path";
6
6
  import { pathToFileURL } from "node:url";
@@ -0,0 +1,242 @@
1
+ #!/usr/bin/env node
2
+ import { t as CliError } from "./_output-CC300DzW.mjs";
3
+ import { a as errorMessage, c as readJson, d as writeJson } from "./_utils-8KKw-bzi.mjs";
4
+ import { mkdtempSync } from "node:fs";
5
+ import path from "node:path";
6
+ import fs from "node:fs/promises";
7
+ import { setTimeout } from "node:timers/promises";
8
+ import { tmpdir } from "node:os";
9
+ import envPaths from "env-paths";
10
+ import { z } from "zod";
11
+ //#region _config.ts
12
+ /**
13
+ * `.aai/project.json` lives in the working tree, so everything in it is
14
+ * untrusted input — a cloned repo can supply any value.
15
+ *
16
+ * `serverUrl` is deliberately NOT validated here. A failed field makes
17
+ * `readProjectConfig` return null for the whole file, which discards the
18
+ * `slug` too — and a deploy with no slug generates a fresh one, silently
19
+ * creating a duplicate agent and overwriting the config. The URL is instead
20
+ * validated where it is used, by `resolveServerUrl`, which rejects anything
21
+ * that isn't an approved http(s) origin.
22
+ */
23
+ const ProjectConfigSchema = z.object({
24
+ /** Deployed agent slug — absent for a pulled project never published. */
25
+ slug: z.string().optional(),
26
+ serverUrl: z.string(),
27
+ /** Studio project this directory is linked to (`aai pull`/`aai push`). */
28
+ studioProject: z.string().optional(),
29
+ /**
30
+ * The workspace files hash at the last pull/push — `aai push` sends it
31
+ * back as the fast-forward token, so an edit made in the studio since
32
+ * then surfaces as a 409 instead of being silently overwritten.
33
+ */
34
+ studioSourceHash: z.string().optional()
35
+ });
36
+ /**
37
+ * Resolve the global config directory (the platform-conventional env-paths
38
+ * location).
39
+ *
40
+ * `AAI_CONFIG_DIR` overrides everything — it exists so tests (and unusual
41
+ * setups) can redirect ALL global-config reads and writes away from the
42
+ * user's real config. The test suite's `approveServer` calls used to
43
+ * permanently pollute `~/.config/aai/config.json` with approved origins.
44
+ */
45
+ function getConfigDir() {
46
+ const override = process.env.AAI_CONFIG_DIR?.trim();
47
+ if (override) return override;
48
+ if (process.env.VITEST) return testConfigDir();
49
+ return envPaths("aai", { suffix: "" }).config;
50
+ }
51
+ /**
52
+ * Per-process throwaway config dir used only under vitest. Memoized: callers
53
+ * read-modify-write the same config across calls, so a fresh dir per call
54
+ * would silently drop what the previous one wrote.
55
+ */
56
+ let _testConfigDir;
57
+ function testConfigDir() {
58
+ _testConfigDir ??= mkdtempSync(path.join(tmpdir(), "aai-vitest-config-"));
59
+ return _testConfigDir;
60
+ }
61
+ async function readProjectConfig(agentDir) {
62
+ const file = path.join(agentDir, ".aai", "project.json");
63
+ let data;
64
+ try {
65
+ data = await readJson(file);
66
+ } catch (err) {
67
+ const reason = errorMessage(err).replace(`Invalid JSON in ${file}: `, "");
68
+ 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 });
69
+ }
70
+ if (data === null) return null;
71
+ const parsed = ProjectConfigSchema.safeParse(data);
72
+ if (!parsed.success) return null;
73
+ return parsed.data;
74
+ }
75
+ async function writeProjectConfig(agentDir, data) {
76
+ await writeJson(path.join(agentDir, ".aai", "project.json"), data);
77
+ }
78
+ /**
79
+ * Merge `patch` into the existing project config rather than replacing the
80
+ * file — a publish recording its `slug` must not drop the studio link
81
+ * fields a pull wrote, and vice versa.
82
+ */
83
+ async function updateProjectConfig(agentDir, patch) {
84
+ let existing = null;
85
+ try {
86
+ existing = await readProjectConfig(agentDir);
87
+ } catch {}
88
+ const merged = {
89
+ ...existing,
90
+ ...patch
91
+ };
92
+ await writeProjectConfig(agentDir, merged);
93
+ return merged;
94
+ }
95
+ /**
96
+ * Origin of `url`, or `null` when it is not an absolute http(s) URL.
97
+ *
98
+ * Non-HTTP schemes are rejected rather than returned: `new URL()` yields the
99
+ * opaque origin `"null"` for them, which would otherwise flow on as if it
100
+ * were a real origin.
101
+ */
102
+ function serverOrigin(url) {
103
+ let parsed;
104
+ try {
105
+ parsed = new URL(url);
106
+ } catch {
107
+ return null;
108
+ }
109
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
110
+ return parsed.origin;
111
+ }
112
+ /** Bounded so a stuck lock degrades to the old racy write, never a hang. */
113
+ const CONFIG_LOCK_TIMEOUT_MS = 2e3;
114
+ const CONFIG_LOCK_RETRY_MS = 20;
115
+ /** Older than this and the holder is assumed dead (crashed mid-update). */
116
+ const CONFIG_LOCK_STALE_MS = 1e4;
117
+ /**
118
+ * Serialize a read-modify-write of the global config ACROSS PROCESSES.
119
+ *
120
+ * `writeJson` makes each individual write atomic, so no reader ever sees a
121
+ * torn file — but the read→modify→write SPAN is not atomic, and every writer
122
+ * here replaces the whole document. Two concurrent CLI invocations therefore
123
+ * lose each other's updates: measured on this repo, 8 parallel commands each
124
+ * approving a distinct origin recorded only 5 of them, and — the case that
125
+ * matters — a concurrent `approveServer` straddling the final write of
126
+ * `aai login` DISCARDS THE API KEY the login just reported saving, leaving
127
+ * the next command with `not_logged_in`. That window is wide open in practice:
128
+ * `aai login` polls for up to five minutes while the user approves in the
129
+ * browser, so any other command run in that time can be mid-update when the
130
+ * key lands.
131
+ *
132
+ * The lock is a `wx` (exclusive-create) lockfile — atomic on every platform
133
+ * we target, and the only primitive available across processes without a
134
+ * daemon. Three deliberate properties:
135
+ *
136
+ * - **Acquisition is bounded** (`CONFIG_LOCK_TIMEOUT_MS`). On timeout the
137
+ * update proceeds UNLOCKED rather than throwing: these are small
138
+ * convenience files, and failing `aai login` because a lockfile is stuck
139
+ * would be strictly worse than the lost update the lock exists to prevent.
140
+ * - **A stale lock is broken** (`CONFIG_LOCK_STALE_MS`). A process killed
141
+ * mid-update leaves the file behind; without this, one crash would make
142
+ * every later config write take the unlocked path forever.
143
+ * - **Never nest.** Re-entering from inside `fn` would self-deadlock until the
144
+ * timeout. `executeLogin` calls `approveServer` and the key update in
145
+ * sequence, not nested — keep it that way.
146
+ */
147
+ async function withGlobalConfigLock(dir, fn) {
148
+ const lockPath = path.join(dir, "config.lock");
149
+ const deadline = Date.now() + CONFIG_LOCK_TIMEOUT_MS;
150
+ let held = false;
151
+ for (;;) try {
152
+ await fs.mkdir(dir, {
153
+ recursive: true,
154
+ mode: 448
155
+ });
156
+ await (await fs.open(lockPath, "wx", 384)).close();
157
+ held = true;
158
+ break;
159
+ } catch (err) {
160
+ if (err.code !== "EEXIST") break;
161
+ if (await fs.stat(lockPath).then((s) => Date.now() - s.mtimeMs).catch(() => 0) > CONFIG_LOCK_STALE_MS) {
162
+ await fs.rm(lockPath, { force: true }).catch(() => void 0);
163
+ continue;
164
+ }
165
+ if (Date.now() >= deadline) break;
166
+ await setTimeout(CONFIG_LOCK_RETRY_MS);
167
+ }
168
+ try {
169
+ return await fn();
170
+ } finally {
171
+ if (held) await fs.rm(lockPath, { force: true }).catch(() => void 0);
172
+ }
173
+ }
174
+ /**
175
+ * Apply `update` to the global config under the cross-process lock, re-reading
176
+ * inside it so the merge is against current contents rather than a snapshot
177
+ * taken before the lock was held. Every read-modify-write of the global config
178
+ * must go through this — a direct `readGlobalConfig`/`writeGlobalConfig` pair
179
+ * is the bug this exists to prevent.
180
+ *
181
+ * Returning the argument unchanged skips the write, so the common no-op case
182
+ * (an origin already approved — i.e. most `--server` invocations) costs a read
183
+ * rather than a rewrite plus the lock contention that comes with it.
184
+ */
185
+ async function updateGlobalConfig(update, configDir) {
186
+ const dir = configDir ?? getConfigDir();
187
+ await withGlobalConfigLock(dir, async () => {
188
+ const current = await readGlobalConfig(dir);
189
+ const next = update(current);
190
+ if (next !== current) await writeGlobalConfig(dir, next);
191
+ });
192
+ }
193
+ /**
194
+ * Record `url`'s origin as user-approved, so later commands in this project
195
+ * may send credentials there without re-passing `--server`.
196
+ */
197
+ async function approveServer(url, configDir) {
198
+ const origin = serverOrigin(url);
199
+ if (!origin) return;
200
+ await updateGlobalConfig((config) => {
201
+ const approved = config.approvedServers ?? [];
202
+ return approved.includes(origin) ? config : {
203
+ ...config,
204
+ approvedServers: [...approved, origin]
205
+ };
206
+ }, configDir);
207
+ }
208
+ async function readGlobalConfig(configDir) {
209
+ const dir = configDir ?? getConfigDir();
210
+ return await readJson(path.join(dir, "config.json")) ?? {};
211
+ }
212
+ async function writeGlobalConfig(configDir, data) {
213
+ await writeJson(path.join(configDir, "config.json"), data, { mode: 384 });
214
+ }
215
+ /**
216
+ * The credential every platform command runs on.
217
+ *
218
+ * ONE source: the key `aai login` saved to the global config. Nothing else
219
+ * authenticates the CLI.
220
+ *
221
+ * There is deliberately no "paste a key" prompt and no `ASSEMBLYAI_API_KEY`
222
+ * fallback. Both produced the same half-configured CLI — able to push,
223
+ * publish, and read/write another account's secrets while linked to no
224
+ * account the user could see in the studio — and both made `aai login`, the
225
+ * real onboarding path, optional in practice. The env var was the worse of
226
+ * the two: it applies to every invocation in a shell, it silently PERSISTED
227
+ * itself into the global config on first use (so the CLI stayed authenticated
228
+ * as that key long after the export was gone), and it collides with the
229
+ * variable the same name serves in a project `.env`, where it is a *provider*
230
+ * credential for the local dev server rather than a platform identity.
231
+ *
232
+ * Non-interactive callers (CI, scripts, the eval harnesses) authenticate by
233
+ * pointing `AAI_CONFIG_DIR` at a config dir holding a key from an interactive
234
+ * `aai login`.
235
+ */
236
+ async function ensureApiKey(configDir) {
237
+ const config = await readGlobalConfig(configDir ?? getConfigDir());
238
+ if (config.apiKey) return config.apiKey;
239
+ throw new CliError("not_logged_in", "You're not logged in.", "Run `aai login` to link your account. Non-interactive setups can point AAI_CONFIG_DIR at a config dir holding a logged-in key.");
240
+ }
241
+ //#endregion
242
+ export { serverOrigin as a, writeProjectConfig as c, readProjectConfig as i, ensureApiKey as n, updateGlobalConfig as o, readGlobalConfig as r, updateProjectConfig as s, approveServer as t };
package/dist/_config.d.ts CHANGED
@@ -54,6 +54,18 @@ export type GlobalConfig = {
54
54
  * were a real origin.
55
55
  */
56
56
  export declare function serverOrigin(url: string): string | null;
57
+ /**
58
+ * Apply `update` to the global config under the cross-process lock, re-reading
59
+ * inside it so the merge is against current contents rather than a snapshot
60
+ * taken before the lock was held. Every read-modify-write of the global config
61
+ * must go through this — a direct `readGlobalConfig`/`writeGlobalConfig` pair
62
+ * is the bug this exists to prevent.
63
+ *
64
+ * Returning the argument unchanged skips the write, so the common no-op case
65
+ * (an origin already approved — i.e. most `--server` invocations) costs a read
66
+ * rather than a rewrite plus the lock contention that comes with it.
67
+ */
68
+ export declare function updateGlobalConfig(update: (current: GlobalConfig) => GlobalConfig, configDir?: string): Promise<void>;
57
69
  /**
58
70
  * Record `url`'s origin as user-approved, so later commands in this project
59
71
  * may send credentials there without re-passing `--server`.
@@ -64,16 +76,23 @@ export declare function writeGlobalConfig(configDir: string, data: GlobalConfig)
64
76
  /**
65
77
  * The credential every platform command runs on.
66
78
  *
67
- * Two sources, in order: the key `aai login` saved, then
68
- * `ASSEMBLYAI_API_KEY` for non-interactive callers (CI, scripts, the eval
69
- * harnesses).
79
+ * ONE source: the key `aai login` saved to the global config. Nothing else
80
+ * authenticates the CLI.
81
+ *
82
+ * There is deliberately no "paste a key" prompt and no `ASSEMBLYAI_API_KEY`
83
+ * fallback. Both produced the same half-configured CLI — able to push,
84
+ * publish, and read/write another account's secrets while linked to no
85
+ * account the user could see in the studio — and both made `aai login`, the
86
+ * real onboarding path, optional in practice. The env var was the worse of
87
+ * the two: it applies to every invocation in a shell, it silently PERSISTED
88
+ * itself into the global config on first use (so the CLI stayed authenticated
89
+ * as that key long after the export was gone), and it collides with the
90
+ * variable the same name serves in a project `.env`, where it is a *provider*
91
+ * credential for the local dev server rather than a platform identity.
70
92
  *
71
- * There is deliberately NO "paste a key" prompt. Pasting one produced a
72
- * half-configured CLI able to push and publish while linked to no account
73
- * the user could see in the studio — and it made `aai login`, which is the
74
- * real onboarding path, optional in practice. It was also the riskier code
75
- * path: a hidden password prompt reads stdin, so a piped invocation could
76
- * have its input eaten and persisted as the API key.
93
+ * Non-interactive callers (CI, scripts, the eval harnesses) authenticate by
94
+ * pointing `AAI_CONFIG_DIR` at a config dir holding a key from an interactive
95
+ * `aai login`.
77
96
  */
78
97
  export declare function ensureApiKey(configDir?: string): Promise<string>;
79
98
  export {};
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The dev server's CONTROL variables — the three things read straight from the
3
+ * shell rather than from the agent's `.env`.
4
+ *
5
+ * That distinction is the reason these live together. `resolveServerEnv` builds
6
+ * `ctx.env` from `.env`-declared keys only, deliberately, so an agent cannot
7
+ * come to depend on a shell variable that will not exist after deploy. These
8
+ * three are not agent config at all — they configure the *dev server process*
9
+ * (who may connect, where it binds, whether it restarts), so they are read from
10
+ * the environment on purpose and must never leak into `ctx.env`.
11
+ *
12
+ * Split out of `_dev-server.ts` purely to keep that module under the
13
+ * file-length cap; it is the only consumer.
14
+ */
15
+ /**
16
+ * The env handed to `createServer` for host-mode connections: provider
17
+ * credentials plus the `AAI_ALLOW_HOST` gate read straight from the shell
18
+ * (it is a control variable, not something an agent declares in `.env`).
19
+ */
20
+ export declare function hostModeEnv(providerEnv: Record<string, string>): Record<string, string>;
21
+ /**
22
+ * Explicit bind host for the dev server, or `undefined` to take the
23
+ * loopback default. An empty `AAI_DEV_HOST` means "unset", not "every
24
+ * interface" — Node treats `listen(port, "")` as 0.0.0.0, which would quietly
25
+ * undo the loopback default this exists to guard.
26
+ */
27
+ export declare function devBindHost(): string | undefined;
28
+ /**
29
+ * File watching is OPT-IN — `AAI_DEV_WATCH=1` turns it on.
30
+ *
31
+ * A restart rebuilds the bundle and replaces the server, which drops nothing
32
+ * mid-request but does end in-flight voice sessions. That is the right default
33
+ * while editing an agent and the wrong one while a benchmark is driving the
34
+ * host for twenty minutes: a stray formatter save, a `.env` touch, or a git
35
+ * operation restarts the server underneath the run, and the harness reports it
36
+ * as a provider failure several records deep.
37
+ */
38
+ export declare function devWatchEnabled(): boolean;
@@ -1,26 +1,77 @@
1
1
  #!/usr/bin/env node
2
- import { n as log } from "./_ui-8kOEB-JH.mjs";
3
- import { a as errorMessage, r as errorCode } from "./_utils-Ch0J4s6a.mjs";
4
- import { n as fallbackHtmlPlugin } from "./client-bundler-yiWoXrgb.mjs";
2
+ import { n as log, r as notify } from "./_ui-DfwfDbT-.mjs";
3
+ import { a as errorMessage, r as errorCode } from "./_utils-8KKw-bzi.mjs";
4
+ import { n as fallbackHtmlPlugin } from "./client-bundler-DONm-khu.mjs";
5
5
  import { buildWorker } from "./worker-bundler.mjs";
6
- import { n as createWorkerEvaluator } from "./_bundler-Cjaxa2wi.mjs";
7
- import { n as ensureApiKey } from "./_config-Y5V-5Krn.mjs";
8
- import { t as resolveServerEnv } from "./_server-common-CnaP_Urf.mjs";
9
- import { createRequire } from "node:module";
6
+ import { n as createWorkerEvaluator } from "./_bundler-DC17suWN.mjs";
7
+ import { n as ensureApiKey } from "./_config-DMyolIk9.mjs";
8
+ import { t as resolveServerEnv } from "./_server-common-DX8Bfrf5.mjs";
10
9
  import { existsSync } from "node:fs";
11
10
  import path from "node:path";
12
11
  import { setTimeout } from "node:timers/promises";
13
12
  import { createRuntime, createServer, requiredProviderEnvVars, withHostCredentialFallback } from "@alexkroman1/aai/runtime";
13
+ import { defaultClientDir } from "@alexkroman1/aai-ui/client-dir";
14
14
  import { watch } from "chokidar";
15
15
  import getPort, { portNumbers } from "get-port";
16
16
  import pDebounce from "p-debounce";
17
+ //#region _dev-env.ts
18
+ /**
19
+ * The dev server's CONTROL variables — the three things read straight from the
20
+ * shell rather than from the agent's `.env`.
21
+ *
22
+ * That distinction is the reason these live together. `resolveServerEnv` builds
23
+ * `ctx.env` from `.env`-declared keys only, deliberately, so an agent cannot
24
+ * come to depend on a shell variable that will not exist after deploy. These
25
+ * three are not agent config at all — they configure the *dev server process*
26
+ * (who may connect, where it binds, whether it restarts), so they are read from
27
+ * the environment on purpose and must never leak into `ctx.env`.
28
+ *
29
+ * Split out of `_dev-server.ts` purely to keep that module under the
30
+ * file-length cap; it is the only consumer.
31
+ */
32
+ /**
33
+ * The env handed to `createServer` for host-mode connections: provider
34
+ * credentials plus the `AAI_ALLOW_HOST` gate read straight from the shell
35
+ * (it is a control variable, not something an agent declares in `.env`).
36
+ */
37
+ function hostModeEnv(providerEnv) {
38
+ const gate = process.env.AAI_ALLOW_HOST;
39
+ return gate === void 0 ? providerEnv : {
40
+ ...providerEnv,
41
+ AAI_ALLOW_HOST: gate
42
+ };
43
+ }
44
+ /**
45
+ * Explicit bind host for the dev server, or `undefined` to take the
46
+ * loopback default. An empty `AAI_DEV_HOST` means "unset", not "every
47
+ * interface" — Node treats `listen(port, "")` as 0.0.0.0, which would quietly
48
+ * undo the loopback default this exists to guard.
49
+ */
50
+ function devBindHost() {
51
+ const host = process.env.AAI_DEV_HOST?.trim();
52
+ return host ? host : void 0;
53
+ }
54
+ /**
55
+ * File watching is OPT-IN — `AAI_DEV_WATCH=1` turns it on.
56
+ *
57
+ * A restart rebuilds the bundle and replaces the server, which drops nothing
58
+ * mid-request but does end in-flight voice sessions. That is the right default
59
+ * while editing an agent and the wrong one while a benchmark is driving the
60
+ * host for twenty minutes: a stray formatter save, a `.env` touch, or a git
61
+ * operation restarts the server underneath the run, and the harness reports it
62
+ * as a provider failure several records deep.
63
+ */
64
+ function devWatchEnabled() {
65
+ return /^(1|true|yes|on)$/i.test(process.env.AAI_DEV_WATCH?.trim() ?? "");
66
+ }
67
+ //#endregion
17
68
  //#region _dev-server.ts
18
69
  /**
19
70
  * Dev server for directory-based agents.
20
71
  *
21
72
  * Imports agent.ts directly for the full agent definition,
22
- * builds a runtime, and starts an HTTP+WebSocket server. Watches for
23
- * file changes and restarts automatically. Optionally runs Vite for
73
+ * builds a runtime, and starts an HTTP+WebSocket server. File watching is
74
+ * opt-in via `AAI_DEV_WATCH=1` (see devWatchEnabled). Optionally runs Vite for
24
75
  * client SPA HMR.
25
76
  */
26
77
  /**
@@ -51,33 +102,13 @@ function agentEnvWarnings(agentDef, env, shellEnv = process.env) {
51
102
  }
52
103
  async function resolveAgentEnv(root, agentDef) {
53
104
  const env = await resolveServerEnv(root);
54
- if (requiredProviderEnvVars(agentDef).includes("ASSEMBLYAI_API_KEY") && !env.ASSEMBLYAI_API_KEY) env.ASSEMBLYAI_API_KEY = await ensureApiKey();
105
+ const required = requiredProviderEnvVars(agentDef);
106
+ const hasShellKey = Boolean(process.env.ASSEMBLYAI_API_KEY);
107
+ if (required.includes("ASSEMBLYAI_API_KEY") && !env.ASSEMBLYAI_API_KEY && !hasShellKey) env.ASSEMBLYAI_API_KEY = await ensureApiKey();
55
108
  for (const warning of agentEnvWarnings(agentDef, env)) log.warn(warning);
56
109
  return env;
57
110
  }
58
111
  /**
59
- * The env handed to `createServer` for host-mode connections: provider
60
- * credentials plus the `AAI_ALLOW_HOST` gate read straight from the shell
61
- * (it is a control variable, not something an agent declares in `.env`).
62
- */
63
- function hostModeEnv(providerEnv) {
64
- const gate = process.env.AAI_ALLOW_HOST;
65
- return gate === void 0 ? providerEnv : {
66
- ...providerEnv,
67
- AAI_ALLOW_HOST: gate
68
- };
69
- }
70
- /**
71
- * Explicit bind host for the dev server, or `undefined` to take the
72
- * loopback default. An empty `AAI_DEV_HOST` means "unset", not "every
73
- * interface" — Node treats `listen(port, "")` as 0.0.0.0, which would quietly
74
- * undo the loopback default this exists to guard.
75
- */
76
- function devBindHost() {
77
- const host = process.env.AAI_DEV_HOST?.trim();
78
- return host ? host : void 0;
79
- }
80
- /**
81
112
  * Load the agent definition by bundling agent.ts (and all its local imports)
82
113
  * into a single ESM file, then importing that. A raw `import(agent.ts?t=...)`
83
114
  * only cache-busts agent.ts itself — transitive imports (./tools.ts, etc.)
@@ -118,7 +149,7 @@ function isIgnoredPath(dir, filePath) {
118
149
  */
119
150
  function watchDirectory(dir, onChange) {
120
151
  const debouncedChange = pDebounce(() => {
121
- log.info("File change detected, restarting...");
152
+ notify("info", "File change detected, restarting...");
122
153
  onChange();
123
154
  }, 300);
124
155
  const watcher = watch(dir, {
@@ -128,26 +159,15 @@ function watchDirectory(dir, onChange) {
128
159
  });
129
160
  watcher.on("error", (err) => {
130
161
  const hint = errorCode(err) === "ENOSPC" ? " The inotify watch limit was reached — raise the fs.inotify max_user_watches sysctl." : "";
131
- log.error(`File watcher error: ${errorMessage(err)}.${hint} Auto-restart on file changes may have stopped; restart \`aai dev\` after fixing.`);
162
+ notify("error", `File watcher error: ${errorMessage(err)}.${hint} Auto-restart on file changes may have stopped; restart \`aai dev\` after fixing.`);
132
163
  });
133
164
  watcher.on("all", () => {
134
165
  debouncedChange().catch((err) => {
135
- log.error(`Watch handler failed: ${errorMessage(err)}`);
166
+ notify("error", `Watch handler failed: ${errorMessage(err)}`);
136
167
  });
137
168
  });
138
169
  return watcher;
139
170
  }
140
- /** Locate the pre-built default aai-ui client (served when no custom client.tsx). */
141
- function resolveDefaultClientDir() {
142
- const require = createRequire(import.meta.url);
143
- let pkgPath;
144
- try {
145
- pkgPath = require.resolve("@alexkroman1/aai-ui/package.json");
146
- } catch (err) {
147
- throw new Error(`Could not locate the default client UI (${errorMessage(err)}) — is @alexkroman1/aai-ui installed? Try reinstalling dependencies (pnpm install).`, { cause: err });
148
- }
149
- return path.join(path.dirname(pkgPath), "dist", "default-client");
150
- }
151
171
  /**
152
172
  * Vite dev-server config for the client SPA. Extracted so the proxy wiring
153
173
  * is unit-testable: `/websocket` MUST proxy with `ws: true` or `aai dev`
@@ -186,7 +206,7 @@ async function startDevServer(opts) {
186
206
  const hasClient = existsSync(path.join(cwd, "client.tsx"));
187
207
  const backendPort = hasClient ? await getPort({ port: portNumbers(port + 1, port + 100) }) : port;
188
208
  const vitePort = port;
189
- const clientDirOpt = hasClient ? {} : { clientDir: resolveDefaultClientDir() };
209
+ const clientDirOpt = hasClient ? {} : { clientDir: defaultClientDir() };
190
210
  const evaluateWorker = createWorkerEvaluator();
191
211
  /** Full build sequence, shared by initial startup and every restart. */
192
212
  async function buildServer() {
@@ -217,12 +237,12 @@ async function startDevServer(opts) {
217
237
  }
218
238
  restarting = true;
219
239
  restart().catch((err) => {
220
- log.error(`Restart failed: ${errorMessage(err)}`);
240
+ notify("error", `Restart failed: ${errorMessage(err)}`);
221
241
  }).finally(() => {
222
242
  restarting = false;
223
243
  });
224
244
  }
225
- const watcher = watchDirectory(cwd, kickRestart);
245
+ const watcher = devWatchEnabled() ? watchDirectory(cwd, kickRestart) : void 0;
226
246
  let viteServer;
227
247
  try {
228
248
  currentServer = await buildServer();
@@ -232,11 +252,11 @@ async function startDevServer(opts) {
232
252
  viteServer = await createViteServer(viteDevConfig(cwd, vitePort, backendPort));
233
253
  await viteServer.listen();
234
254
  viteServer.httpServer?.on("error", (err) => {
235
- log.error(`Vite dev server error: ${errorMessage(err)}`);
255
+ notify("error", `Vite dev server error: ${errorMessage(err)}`);
236
256
  });
237
257
  }
238
258
  } catch (err) {
239
- await watcher.close().catch(() => void 0);
259
+ await watcher?.close().catch(() => void 0);
240
260
  await viteServer?.close().catch(() => void 0);
241
261
  throw err;
242
262
  }
@@ -253,7 +273,7 @@ async function startDevServer(opts) {
253
273
  try {
254
274
  newServer = await buildServer();
255
275
  } catch (err) {
256
- log.error(`Restart failed: ${errorMessage(err)} (previous server still running)`);
276
+ notify("error", `Restart failed: ${errorMessage(err)} (previous server still running)`);
257
277
  return;
258
278
  }
259
279
  if (closed) {
@@ -270,9 +290,9 @@ async function startDevServer(opts) {
270
290
  await newServer.close().catch(() => void 0);
271
291
  return;
272
292
  }
273
- log.success("Restarted");
293
+ notify("success", "Restarted");
274
294
  } catch (err) {
275
- log.error(`Restart failed: ${errorMessage(err)} — dev server is down; save a file to retry.`);
295
+ notify("error", `Restart failed: ${errorMessage(err)} — dev server is down; save a file to retry.`);
276
296
  await newServer.close().catch(() => void 0);
277
297
  }
278
298
  }
@@ -297,7 +317,7 @@ async function startDevServer(opts) {
297
317
  return () => {
298
318
  cleanupPromise ??= (async () => {
299
319
  closed = true;
300
- await watcher.close().catch(() => void 0);
320
+ await watcher?.close().catch(() => void 0);
301
321
  await viteServer?.close().catch(() => void 0);
302
322
  await currentServer.close().catch(() => void 0);
303
323
  })();
@@ -2,8 +2,8 @@
2
2
  * Dev server for directory-based agents.
3
3
  *
4
4
  * Imports agent.ts directly for the full agent definition,
5
- * builds a runtime, and starts an HTTP+WebSocket server. Watches for
6
- * file changes and restarts automatically. Optionally runs Vite for
5
+ * builds a runtime, and starts an HTTP+WebSocket server. File watching is
6
+ * opt-in via `AAI_DEV_WATCH=1` (see devWatchEnabled). Optionally runs Vite for
7
7
  * client SPA HMR.
8
8
  */
9
9
  import type { AgentDef } from "@alexkroman1/aai";
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
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-2nVugrN3.mjs";
4
- import { REPO_URL, downloadAndMergeTemplate } from "./_templates-BfyRyE7z.mjs";
2
+ import { c as readJson, d as writeJson, s as isEexist } from "./_utils-8KKw-bzi.mjs";
3
+ import { r as isDevMode, t as getMonorepoRoot } from "./_agent-DS2PUJcl.mjs";
4
+ import { REPO_URL, downloadAndMergeTemplate } from "./_templates-BWJOiWOO.mjs";
5
5
  import path from "node:path";
6
6
  import fs from "node:fs/promises";
7
7
  //#region _init.ts