@alexkroman1/aai-cli 5.11.0 → 5.13.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 (51) hide show
  1. package/dist/{_agent-DS2PUJcl.mjs → _agent-C8Jk-ZcP.mjs} +3 -2
  2. package/dist/_agent.d.ts +1 -0
  3. package/dist/{_api-client-B-upMGkc.mjs → _api-client-BX8Qv51n.mjs} +2 -1
  4. package/dist/{_bundler-DC17suWN.mjs → _bundler-BuIyDdvs.mjs} +30 -11
  5. package/dist/_bundler.d.ts +16 -2
  6. package/dist/{_config-DMyolIk9.mjs → _config-CzwAWOWr.mjs} +3 -3
  7. package/dist/_deploy.d.ts +0 -10
  8. package/dist/_dev-restart.d.ts +59 -0
  9. package/dist/{_dev-server-B3ivqyEd.mjs → _dev-server-Ch7K8e8i.mjs} +181 -92
  10. package/dist/_dev-server.d.ts +17 -0
  11. package/dist/{_init-5hHkHe-V.mjs → _init-CbkrE2wq.mjs} +3 -3
  12. package/dist/_preflight-BtfaYtbE.mjs +46 -0
  13. package/dist/_preflight.d.ts +57 -0
  14. package/dist/{_server-common-DX8Bfrf5.mjs → _server-common-61fL__39.mjs} +1 -1
  15. package/dist/_slug-api-D30zPmsn.mjs +50 -0
  16. package/dist/_slug-api.d.ts +16 -0
  17. package/dist/_studio-D2ciIhbi.mjs +94 -0
  18. package/dist/_studio.d.ts +19 -16
  19. package/dist/{_templates-DmdG-voA.mjs → _templates-B1kt3BGt.mjs} +2 -2
  20. package/dist/{_typecheck-gate-DB-PY0A3.mjs → _typecheck-gate-4v9UBZHh.mjs} +1 -1
  21. package/dist/{_ui-DfwfDbT-.mjs → _ui-u7T4YooX.mjs} +5 -1
  22. package/dist/_ui.d.ts +2 -0
  23. package/dist/{_utils-8KKw-bzi.mjs → _utils-0DHbcqUb.mjs} +2 -2
  24. package/dist/{build-CACFbdQ4.mjs → build-DQJ1ZSm9.mjs} +4 -4
  25. package/dist/cli.d.ts +2 -1
  26. package/dist/cli.mjs +70 -34
  27. package/dist/{client-bundler-DONm-khu.mjs → client-bundler-C9qXL8A2.mjs} +1 -1
  28. package/dist/client-bundler.mjs +1 -1
  29. package/dist/{delete-DEZ7u3u4.mjs → delete-DCRX3d96.mjs} +4 -4
  30. package/dist/{deploy-Ch0d_jje.mjs → deploy-DjsEV-40.mjs} +27 -26
  31. package/dist/deploy.d.ts +0 -2
  32. package/dist/{dev-CdeRcYiQ.mjs → dev-CvPH5dbF.mjs} +3 -3
  33. package/dist/{eject-DSrAp8PL.mjs → eject-LgBRfnbH.mjs} +3 -3
  34. package/dist/{init-BBAoRI_f.mjs → init-BppxMR8r.mjs} +5 -5
  35. package/dist/{login-BeFUiU6M.mjs → login-BQQaxJhL.mjs} +4 -15
  36. package/dist/login.d.ts +0 -9
  37. package/dist/project-config.d.ts +23 -0
  38. package/dist/project-config.mjs +3 -0
  39. package/dist/scaffold/CLAUDE.md +60 -10
  40. package/dist/scaffold/package.json +3 -3
  41. package/dist/{secret-4_dYyrpA.mjs → secret-Ds9dvqAj.mjs} +7 -7
  42. package/dist/{storage-BTfErOOW.mjs → storage-DbrQSHFk.mjs} +2 -2
  43. package/dist/{studio-DPDFKZ9i.mjs → studio-GSFd4s5O.mjs} +12 -179
  44. package/dist/templates/dispatch-center/shared.ts +13 -15
  45. package/dist/templates/pizza-ordering/agent.test.ts +3 -0
  46. package/dist/templates/retail/store.ts +11 -17
  47. package/dist/templates/solo-rpg/agent.test.ts +3 -0
  48. package/dist/{test-C-V98oC-.mjs → test-2LWBcXvY.mjs} +2 -2
  49. package/dist/typecheck.mjs +1 -1
  50. package/package.json +8 -3
  51. package/dist/_slug-api-fRNNR8tz.mjs +0 -27
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { a as serverOrigin, i as readProjectConfig, n as ensureApiKey, r as readGlobalConfig, t as approveServer } from "./_config-DMyolIk9.mjs";
2
+ import { a as serverOrigin, i as readProjectConfig, n as ensureApiKey, r as readGlobalConfig, t as approveServer } from "./_config-CzwAWOWr.mjs";
3
3
  import { existsSync } from "node:fs";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
@@ -107,7 +107,8 @@ async function getServerInfo(cwd, explicitServer) {
107
107
  return {
108
108
  serverUrl,
109
109
  slug: config.slug,
110
- apiKey
110
+ apiKey,
111
+ studioProject: config.studioProject
111
112
  };
112
113
  }
113
114
  //#endregion
package/dist/_agent.d.ts CHANGED
@@ -38,4 +38,5 @@ export declare function getServerInfo(cwd: string, explicitServer?: string): Pro
38
38
  serverUrl: string;
39
39
  slug: string;
40
40
  apiKey: string;
41
+ studioProject: string | undefined;
41
42
  }>;
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { omitUndefined } from "@alexkroman1/aai/utils";
2
3
  import { FetchError, ofetch } from "ofetch";
3
4
  //#region _api-client.ts
4
5
  /**
@@ -25,7 +26,7 @@ async function apiRequest(url, opts) {
25
26
  Authorization: `Bearer ${opts.apiKey}`,
26
27
  ...opts.headers
27
28
  },
28
- ...opts.body !== void 0 ? { body: opts.body } : {},
29
+ ...omitUndefined({ body: opts.body }),
29
30
  retry: opts.retry ?? 2,
30
31
  retryDelay: opts.retryDelay ?? 300
31
32
  });
@@ -1,20 +1,20 @@
1
1
  #!/usr/bin/env node
2
- import { u as validateAgentExport } from "./_utils-8KKw-bzi.mjs";
3
- import { t as buildClient } from "./client-bundler-DONm-khu.mjs";
2
+ import { u as validateAgentExport } from "./_utils-0DHbcqUb.mjs";
3
+ import { t as buildClient } from "./client-bundler-C9qXL8A2.mjs";
4
4
  import { buildWorker } from "./worker-bundler.mjs";
5
5
  import path from "node:path";
6
6
  import { pathToFileURL } from "node:url";
7
7
  import { mkdtemp, rm, writeFile } from "node:fs/promises";
8
- import { hash } from "node:crypto";
9
8
  import { tmpdir } from "node:os";
9
+ import { hash } from "node:crypto";
10
10
  //#region _bundler.ts
11
11
  /**
12
12
  * Bundle an agent directory: build agent.ts into worker ESM + client files.
13
13
  *
14
14
  * agent.ts is the single entry point: `export default agent({...})`. The
15
15
  * worker self-describes (it exports `__aaiConfig` — see `worker-bundler.ts`),
16
- * so nothing here evaluates the bundle: the server extracts the config inside
17
- * a guest sandbox at deploy time.
16
+ * which is what `evalWorkerConfig` reads back: the platform stores no agent
17
+ * config and evaluates nothing, so every read of one happens here.
18
18
  */
19
19
  async function buildAgentBundle(cwd, opts = {}) {
20
20
  const [worker, clientFiles] = await Promise.all([buildWorker(cwd, opts), buildClient(cwd)]);
@@ -42,21 +42,40 @@ async function buildAgentBundle(cwd, opts = {}) {
42
42
  * experimental.
43
43
  */
44
44
  async function evalWorkerBundle(code) {
45
+ const mod = await importWorkerModule(code);
46
+ const agentDef = mod.default ?? mod;
47
+ validateAgentExport(agentDef);
48
+ return agentDef;
49
+ }
50
+ /** Import a built worker from a temp file. See {@link evalWorkerBundle}. */
51
+ async function importWorkerModule(code) {
45
52
  const dir = await mkdtemp(path.join(tmpdir(), "aai-worker-"));
46
53
  const file = path.join(dir, "worker.mjs");
47
- let mod;
48
54
  try {
49
55
  await writeFile(file, code, "utf-8");
50
- mod = await import(pathToFileURL(file).href);
56
+ return await import(pathToFileURL(file).href);
51
57
  } finally {
52
58
  await rm(dir, {
53
59
  recursive: true,
54
60
  force: true
55
61
  }).catch(() => void 0);
56
62
  }
57
- const agentDef = mod.default ?? mod;
58
- validateAgentExport(agentDef);
59
- return agentDef;
63
+ }
64
+ /**
65
+ * The config a built worker describes about itself — its `__aaiConfig`
66
+ * export, generated by `buildWorker`'s wrapper entry (`toAgentConfig(def)`
67
+ * plus the tool schemas).
68
+ *
69
+ * Importing it is also the deploy's SMOKE TEST: a bundle whose top level
70
+ * throws fails here, in the directory that owns it, rather than as a sandbox
71
+ * that never becomes ready after the upload. The platform performs no such
72
+ * check — it evaluates nothing (see `_preflight.ts`).
73
+ *
74
+ * Returns undefined for a bundle built by a CLI old enough not to emit the
75
+ * export; the caller treats that as "nothing to preflight", never an error.
76
+ */
77
+ async function evalWorkerConfig(code) {
78
+ return (await importWorkerModule(code)).__aaiConfig;
60
79
  }
61
80
  /**
62
81
  * Memoizing wrapper around `evalWorkerBundle` for long-lived callers (the
@@ -79,4 +98,4 @@ function createWorkerEvaluator() {
79
98
  };
80
99
  }
81
100
  //#endregion
82
- export { createWorkerEvaluator as n, evalWorkerBundle as r, buildAgentBundle as t };
101
+ export { evalWorkerConfig as i, createWorkerEvaluator as n, evalWorkerBundle as r, buildAgentBundle as t };
@@ -12,8 +12,8 @@ export type DirectoryBundleOutput = {
12
12
  *
13
13
  * agent.ts is the single entry point: `export default agent({...})`. The
14
14
  * worker self-describes (it exports `__aaiConfig` — see `worker-bundler.ts`),
15
- * so nothing here evaluates the bundle: the server extracts the config inside
16
- * a guest sandbox at deploy time.
15
+ * which is what `evalWorkerConfig` reads back: the platform stores no agent
16
+ * config and evaluates nothing, so every read of one happens here.
17
17
  */
18
18
  export declare function buildAgentBundle(cwd: string, opts?: BuildWorkerOptions): Promise<DirectoryBundleOutput>;
19
19
  /**
@@ -35,6 +35,20 @@ export declare function buildAgentBundle(cwd: string, opts?: BuildWorkerOptions)
35
35
  * experimental.
36
36
  */
37
37
  export declare function evalWorkerBundle(code: string): Promise<AgentDef>;
38
+ /**
39
+ * The config a built worker describes about itself — its `__aaiConfig`
40
+ * export, generated by `buildWorker`'s wrapper entry (`toAgentConfig(def)`
41
+ * plus the tool schemas).
42
+ *
43
+ * Importing it is also the deploy's SMOKE TEST: a bundle whose top level
44
+ * throws fails here, in the directory that owns it, rather than as a sandbox
45
+ * that never becomes ready after the upload. The platform performs no such
46
+ * check — it evaluates nothing (see `_preflight.ts`).
47
+ *
48
+ * Returns undefined for a bundle built by a CLI old enough not to emit the
49
+ * export; the caller treats that as "nothing to preflight", never an error.
50
+ */
51
+ export declare function evalWorkerConfig(code: string): Promise<unknown>;
38
52
  /**
39
53
  * Memoizing wrapper around `evalWorkerBundle` for long-lived callers (the
40
54
  * dev server): byte-identical worker code returns the previously evaluated
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
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";
3
+ import { a as errorMessage, c as readJson, d as writeJson } from "./_utils-0DHbcqUb.mjs";
4
4
  import { mkdtempSync } from "node:fs";
5
5
  import path from "node:path";
6
6
  import fs from "node:fs/promises";
7
- import { setTimeout } from "node:timers/promises";
8
7
  import { tmpdir } from "node:os";
8
+ import { setTimeout } from "node:timers/promises";
9
9
  import envPaths from "env-paths";
10
10
  import { z } from "zod";
11
11
  //#region _config.ts
@@ -239,4 +239,4 @@ async function ensureApiKey(configDir) {
239
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
240
  }
241
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 };
242
+ export { serverOrigin as a, writeGlobalConfig as c, readProjectConfig as i, writeProjectConfig as l, ensureApiKey as n, updateGlobalConfig as o, readGlobalConfig as r, updateProjectConfig as s, approveServer as t };
package/dist/_deploy.d.ts CHANGED
@@ -7,14 +7,6 @@ export type DeployOpts = {
7
7
  /** Existing slug for redeployment. Omit for first deploy — server generates one. */
8
8
  slug?: string;
9
9
  apiKey: string;
10
- /**
11
- * Ask the server to WARN (in `warnings`) instead of rejecting when the
12
- * agent's providers are missing credentials — `aai deploy
13
- * --allow-missing-secrets`, for setting them post-deploy with
14
- * `aai secret put` (the studio's publish flow relies on this: its Secrets
15
- * panel needs a deployed slug to attach secrets to).
16
- */
17
- allowMissingSecrets?: boolean;
18
10
  /**
19
11
  * Ask the server to permit a `-preview`-suffixed slug (`aai deploy
20
12
  * --allow-preview-slug`). That suffix is reserved for the studio's
@@ -29,7 +21,5 @@ export type DeployOpts = {
29
21
  };
30
22
  export type DeployResult = {
31
23
  slug: string;
32
- /** Server-side deploy warnings (e.g. the missing-credential preflight). */
33
- warnings?: string[];
34
24
  };
35
25
  export declare function runDeploy(opts: DeployOpts): Promise<DeployResult>;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Restart supervision for `aai dev`.
3
+ *
4
+ * The dev server's subtlest logic is not the wiring (chokidar, Vite, the
5
+ * bundler) but the small state machine that decides WHEN to rebuild and in
6
+ * what order to swap servers: an edit saved mid-boot must be queued rather
7
+ * than race the initial build, a failed build must leave the old server
8
+ * serving, a `listen` that loses the port race must retry, and teardown must
9
+ * be idempotent and win every race against an in-flight rebuild.
10
+ *
11
+ * That machine lives here, behind injected `build`/`listen`/`close`
12
+ * operations, so it can be exercised directly — no file watcher, no bundler,
13
+ * no debounce, no module mocks. `_dev-server.ts` supplies the real operations.
14
+ *
15
+ * The supervised server is opaque (`S`): the supervisor never touches it
16
+ * except through `ops`, so a test can use a plain label.
17
+ */
18
+ export type NotifyLevel = "error" | "warn" | "info" | "success";
19
+ export type RestartOps<S> = {
20
+ /** Build a replacement server. Rejections leave the current one serving. */
21
+ build(): Promise<S>;
22
+ /** Bind the port. Rejections are retried — see {@link LISTEN_ATTEMPTS}. */
23
+ listen(server: S): Promise<void>;
24
+ /** Release the port and any per-server resources. */
25
+ close(server: S): Promise<void>;
26
+ /** User-facing progress and failures. */
27
+ notify(level: NotifyLevel, message: string): void;
28
+ /**
29
+ * Extra teardown (file watcher, Vite) run once by {@link
30
+ * RestartSupervisor.close}, before the current server closes. Failures are
31
+ * swallowed so one leak can't strand the others.
32
+ */
33
+ teardown?: () => Promise<void>;
34
+ /** Injectable so retry specs don't sleep real wall-clock. */
35
+ sleep?: (ms: number) => Promise<void>;
36
+ };
37
+ export type RestartSupervisor<S> = {
38
+ /**
39
+ * A change was detected. Queues instead of racing when a restart (or the
40
+ * initial boot) is still in flight.
41
+ */
42
+ request(): void;
43
+ /**
44
+ * Startup finished with `server`. Releases the queue and runs the one
45
+ * restart an edit saved during boot asked for.
46
+ */
47
+ adopt(server: S): void;
48
+ /** The server currently serving, or `undefined` before {@link adopt}. */
49
+ current(): S | undefined;
50
+ /** Idempotent teardown. Concurrent callers join the in-flight run. */
51
+ close(): Promise<void>;
52
+ };
53
+ /**
54
+ * Create a {@link RestartSupervisor}. It starts in the "restarting" state:
55
+ * callers install their watcher and call {@link RestartSupervisor.request}
56
+ * freely while the initial build runs, then {@link RestartSupervisor.adopt}
57
+ * the built server (or {@link RestartSupervisor.close} on startup failure).
58
+ */
59
+ export declare function createRestartSupervisor<S>(ops: RestartOps<S>): RestartSupervisor<S>;
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
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";
2
+ import { i as outputSilenced, n as log, r as notify } from "./_ui-u7T4YooX.mjs";
3
+ import { a as errorMessage, r as errorCode } from "./_utils-0DHbcqUb.mjs";
4
+ import { n as fallbackHtmlPlugin } from "./client-bundler-C9qXL8A2.mjs";
5
5
  import { buildWorker } from "./worker-bundler.mjs";
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";
6
+ import { n as ensureApiKey } from "./_config-CzwAWOWr.mjs";
7
+ import { n as createWorkerEvaluator } from "./_bundler-BuIyDdvs.mjs";
8
+ import { t as resolveServerEnv } from "./_server-common-61fL__39.mjs";
9
9
  import { existsSync } from "node:fs";
10
10
  import path from "node:path";
11
11
  import { setTimeout } from "node:timers/promises";
12
- import { createRuntime, createServer, requiredProviderEnvVars, withHostCredentialFallback } from "@alexkroman1/aai/runtime";
12
+ import { consoleLogger, createRuntime, createServer, requiredProviderEnvVars, withHostCredentialFallback } from "@alexkroman1/aai/runtime";
13
13
  import { defaultClientDir } from "@alexkroman1/aai-ui/client-dir";
14
14
  import { watch } from "chokidar";
15
15
  import getPort, { portNumbers } from "get-port";
@@ -65,6 +65,130 @@ function devWatchEnabled() {
65
65
  return /^(1|true|yes|on)$/i.test(process.env.AAI_DEV_WATCH?.trim() ?? "");
66
66
  }
67
67
  //#endregion
68
+ //#region _dev-restart.ts
69
+ /**
70
+ * Restart supervision for `aai dev`.
71
+ *
72
+ * The dev server's subtlest logic is not the wiring (chokidar, Vite, the
73
+ * bundler) but the small state machine that decides WHEN to rebuild and in
74
+ * what order to swap servers: an edit saved mid-boot must be queued rather
75
+ * than race the initial build, a failed build must leave the old server
76
+ * serving, a `listen` that loses the port race must retry, and teardown must
77
+ * be idempotent and win every race against an in-flight rebuild.
78
+ *
79
+ * That machine lives here, behind injected `build`/`listen`/`close`
80
+ * operations, so it can be exercised directly — no file watcher, no bundler,
81
+ * no debounce, no module mocks. `_dev-server.ts` supplies the real operations.
82
+ *
83
+ * The supervised server is opaque (`S`): the supervisor never touches it
84
+ * except through `ops`, so a test can use a plain label.
85
+ */
86
+ /** Attempts to bind the port during the close→listen swap. */
87
+ const LISTEN_ATTEMPTS = 3;
88
+ /** Backoff between those attempts. */
89
+ const LISTEN_RETRY_DELAY_MS = 250;
90
+ /**
91
+ * Create a {@link RestartSupervisor}. It starts in the "restarting" state:
92
+ * callers install their watcher and call {@link RestartSupervisor.request}
93
+ * freely while the initial build runs, then {@link RestartSupervisor.adopt}
94
+ * the built server (or {@link RestartSupervisor.close} on startup failure).
95
+ */
96
+ function createRestartSupervisor(ops) {
97
+ const wait = ops.sleep ?? setTimeout;
98
+ let restarting = true;
99
+ let pendingRestart = false;
100
+ let closed = false;
101
+ let current;
102
+ let cleanupPromise;
103
+ function request() {
104
+ if (restarting) {
105
+ pendingRestart = true;
106
+ return;
107
+ }
108
+ restarting = true;
109
+ restart().catch((err) => {
110
+ ops.notify("error", `Restart failed: ${errorMessage(err)}`);
111
+ }).finally(() => {
112
+ restarting = false;
113
+ });
114
+ }
115
+ async function restart() {
116
+ do {
117
+ pendingRestart = false;
118
+ await restartOnce();
119
+ } while (pendingRestart && !closed);
120
+ }
121
+ async function restartOnce() {
122
+ let newServer;
123
+ try {
124
+ newServer = await ops.build();
125
+ } catch (err) {
126
+ ops.notify("error", `Restart failed: ${errorMessage(err)} (previous server still running)`);
127
+ return;
128
+ }
129
+ if (closed) {
130
+ await closeQuietly(newServer);
131
+ return;
132
+ }
133
+ if (current !== void 0) {
134
+ const old = current;
135
+ current = void 0;
136
+ await closeQuietly(old);
137
+ }
138
+ try {
139
+ await listenWithRetry(newServer);
140
+ } catch (err) {
141
+ ops.notify("error", `Restart failed: ${errorMessage(err)} — dev server is down; save a file to retry.`);
142
+ await closeQuietly(newServer);
143
+ return;
144
+ }
145
+ current = newServer;
146
+ if (closed) {
147
+ await closeQuietly(newServer);
148
+ return;
149
+ }
150
+ ops.notify("success", "Restarted");
151
+ }
152
+ /**
153
+ * Listen with a few short-backoff retries. During the close→listen swap the
154
+ * port is momentarily free, so another process can snatch it (or the OS can
155
+ * hold it in TIME_WAIT); one blind attempt would leave the dev server down
156
+ * until the next file change.
157
+ */
158
+ async function listenWithRetry(server) {
159
+ for (let attempt = 1;; attempt++) try {
160
+ await ops.listen(server);
161
+ return;
162
+ } catch (err) {
163
+ if (attempt >= LISTEN_ATTEMPTS || closed) throw err;
164
+ await wait(LISTEN_RETRY_DELAY_MS);
165
+ }
166
+ }
167
+ /** Best-effort close: a synchronous throw is swallowed alongside a rejection. */
168
+ async function closeQuietly(server) {
169
+ try {
170
+ await ops.close(server);
171
+ } catch {}
172
+ }
173
+ return {
174
+ request,
175
+ adopt(server) {
176
+ current = server;
177
+ restarting = false;
178
+ if (pendingRestart) request();
179
+ },
180
+ current: () => current,
181
+ close() {
182
+ cleanupPromise ??= (async () => {
183
+ closed = true;
184
+ await ops.teardown?.().catch(() => void 0);
185
+ if (current !== void 0) await closeQuietly(current);
186
+ })();
187
+ return cleanupPromise;
188
+ }
189
+ };
190
+ }
191
+ //#endregion
68
192
  //#region _dev-server.ts
69
193
  /**
70
194
  * Dev server for directory-based agents.
@@ -143,6 +267,33 @@ function isIgnoredPath(dir, filePath) {
143
267
  });
144
268
  }
145
269
  /**
270
+ * The logger the dev server's runtime writes through.
271
+ *
272
+ * The SDK's default logger is console-backed and `console.log` is STDOUT, so
273
+ * in JSON mode the runtime's own diagnostics — the multi-line "Session mode
274
+ * resolved" dump at startup, every later warning — landed on stdout ahead of
275
+ * the single result line `aai dev` promises there. JSON mode is AUTO-DETECTED
276
+ * on a pipe, so that is the normal case rather than an opt-in one:
277
+ * `aai dev > dev.log`, a process supervisor, a container. It is the same
278
+ * hazard `notify` exists for, one layer down: `silenceOutput()` only reaches
279
+ * this CLI's own `log`, and the runtime is not using it.
280
+ *
281
+ * Human mode keeps the console logger exactly as it was — a TTY has nothing
282
+ * to parse, and stdout is where people are already reading these.
283
+ */
284
+ function createDevLogger(silenced) {
285
+ if (!silenced) return consoleLogger;
286
+ const write = (msg, ctx) => {
287
+ process.stderr.write(`${msg}${ctx === void 0 ? "" : ` ${JSON.stringify(ctx)}`}\n`);
288
+ };
289
+ return {
290
+ info: write,
291
+ warn: write,
292
+ error: write,
293
+ debug: () => void 0
294
+ };
295
+ }
296
+ /**
146
297
  * Watch the agent directory for changes and call `onChange` when detected.
147
298
  * Debounces to avoid rapid restarts. Uses chokidar for reliable recursive
148
299
  * watching across platforms (raw `fs.watch` misses events on Linux).
@@ -208,6 +359,7 @@ async function startDevServer(opts) {
208
359
  const vitePort = port;
209
360
  const clientDirOpt = hasClient ? {} : { clientDir: defaultClientDir() };
210
361
  const evaluateWorker = createWorkerEvaluator();
362
+ const devLogger = createDevLogger(outputSilenced());
211
363
  /** Full build sequence, shared by initial startup and every restart. */
212
364
  async function buildServer() {
213
365
  const agentDef = await loadAgentDef(cwd, evaluateWorker);
@@ -217,7 +369,8 @@ async function startDevServer(opts) {
217
369
  runtime: createRuntime({
218
370
  agent: agentDef,
219
371
  env,
220
- providerEnv
372
+ providerEnv,
373
+ logger: devLogger
221
374
  }),
222
375
  name: agentDef.name,
223
376
  env: hostModeEnv(providerEnv),
@@ -226,27 +379,24 @@ async function startDevServer(opts) {
226
379
  ...clientDirOpt
227
380
  });
228
381
  }
229
- let restarting = true;
230
- let pendingRestart = false;
231
- let closed = false;
232
- let currentServer;
233
- function kickRestart() {
234
- if (restarting) {
235
- pendingRestart = true;
236
- return;
237
- }
238
- restarting = true;
239
- restart().catch((err) => {
240
- notify("error", `Restart failed: ${errorMessage(err)}`);
241
- }).finally(() => {
242
- restarting = false;
243
- });
244
- }
245
- const watcher = devWatchEnabled() ? watchDirectory(cwd, kickRestart) : void 0;
246
382
  let viteServer;
383
+ let watcher;
384
+ const supervisor = createRestartSupervisor({
385
+ build: buildServer,
386
+ listen: (server) => server.listen(backendPort, devBindHost()),
387
+ close: (server) => server.close(),
388
+ notify,
389
+ teardown: async () => {
390
+ await watcher?.close().catch(() => void 0);
391
+ await viteServer?.close().catch(() => void 0);
392
+ }
393
+ });
394
+ watcher = devWatchEnabled() ? watchDirectory(cwd, supervisor.request) : void 0;
395
+ let boundServer;
247
396
  try {
248
- currentServer = await buildServer();
249
- await currentServer.listen(backendPort, devBindHost());
397
+ const initialServer = await buildServer();
398
+ await initialServer.listen(backendPort, devBindHost());
399
+ boundServer = initialServer;
250
400
  if (hasClient) {
251
401
  const { createServer: createViteServer } = await import("vite");
252
402
  viteServer = await createViteServer(viteDevConfig(cwd, vitePort, backendPort));
@@ -255,74 +405,13 @@ async function startDevServer(opts) {
255
405
  notify("error", `Vite dev server error: ${errorMessage(err)}`);
256
406
  });
257
407
  }
408
+ supervisor.adopt(initialServer);
258
409
  } catch (err) {
259
- await watcher?.close().catch(() => void 0);
260
- await viteServer?.close().catch(() => void 0);
410
+ await supervisor.close();
411
+ await boundServer?.close().catch(() => void 0);
261
412
  throw err;
262
413
  }
263
- restarting = false;
264
- if (pendingRestart) kickRestart();
265
- async function restart() {
266
- do {
267
- pendingRestart = false;
268
- await restartOnce();
269
- } while (pendingRestart && !closed);
270
- }
271
- async function restartOnce() {
272
- let newServer;
273
- try {
274
- newServer = await buildServer();
275
- } catch (err) {
276
- notify("error", `Restart failed: ${errorMessage(err)} (previous server still running)`);
277
- return;
278
- }
279
- if (closed) {
280
- await newServer.close().catch(() => void 0);
281
- return;
282
- }
283
- try {
284
- await currentServer.close();
285
- } catch {}
286
- try {
287
- await listenWithRetry(newServer);
288
- currentServer = newServer;
289
- if (closed) {
290
- await newServer.close().catch(() => void 0);
291
- return;
292
- }
293
- notify("success", "Restarted");
294
- } catch (err) {
295
- notify("error", `Restart failed: ${errorMessage(err)} — dev server is down; save a file to retry.`);
296
- await newServer.close().catch(() => void 0);
297
- }
298
- }
299
- /**
300
- * Listen with a few short-backoff retries. During the close→listen swap the
301
- * port is momentarily free, so another process can snatch it (or the OS can
302
- * hold it in TIME_WAIT); one blind attempt would leave the dev server down
303
- * until the next file change.
304
- */
305
- async function listenWithRetry(server) {
306
- const LISTEN_ATTEMPTS = 3;
307
- const LISTEN_RETRY_DELAY_MS = 250;
308
- for (let attempt = 1;; attempt++) try {
309
- await server.listen(backendPort, devBindHost());
310
- return;
311
- } catch (err) {
312
- if (attempt >= LISTEN_ATTEMPTS || closed) throw err;
313
- await setTimeout(LISTEN_RETRY_DELAY_MS);
314
- }
315
- }
316
- let cleanupPromise;
317
- return () => {
318
- cleanupPromise ??= (async () => {
319
- closed = true;
320
- await watcher?.close().catch(() => void 0);
321
- await viteServer?.close().catch(() => void 0);
322
- await currentServer.close().catch(() => void 0);
323
- })();
324
- return cleanupPromise;
325
- };
414
+ return supervisor.close;
326
415
  }
327
416
  //#endregion
328
417
  export { startDevServer };
@@ -7,6 +7,7 @@
7
7
  * client SPA HMR.
8
8
  */
9
9
  import type { AgentDef } from "@alexkroman1/aai";
10
+ import { type Logger } from "@alexkroman1/aai/runtime";
10
11
  import { type FSWatcher } from "chokidar";
11
12
  /**
12
13
  * Warnings about the agent's credentials, computed against the `.env`-derived
@@ -46,6 +47,22 @@ export declare function loadAgentDef(cwd: string, evaluate: (code: string) => Pr
46
47
  * restart the server with the new values.
47
48
  */
48
49
  export declare function isIgnoredPath(dir: string, filePath: string): boolean;
50
+ /**
51
+ * The logger the dev server's runtime writes through.
52
+ *
53
+ * The SDK's default logger is console-backed and `console.log` is STDOUT, so
54
+ * in JSON mode the runtime's own diagnostics — the multi-line "Session mode
55
+ * resolved" dump at startup, every later warning — landed on stdout ahead of
56
+ * the single result line `aai dev` promises there. JSON mode is AUTO-DETECTED
57
+ * on a pipe, so that is the normal case rather than an opt-in one:
58
+ * `aai dev > dev.log`, a process supervisor, a container. It is the same
59
+ * hazard `notify` exists for, one layer down: `silenceOutput()` only reaches
60
+ * this CLI's own `log`, and the runtime is not using it.
61
+ *
62
+ * Human mode keeps the console logger exactly as it was — a TTY has nothing
63
+ * to parse, and stdout is where people are already reading these.
64
+ */
65
+ export declare function createDevLogger(silenced: boolean): Logger;
49
66
  /**
50
67
  * Watch the agent directory for changes and call `onChange` when detected.
51
68
  * Debounces to avoid rapid restarts. Uses chokidar for reliable recursive
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
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-DmdG-voA.mjs";
2
+ import { c as readJson, d as writeJson, s as isEexist } from "./_utils-0DHbcqUb.mjs";
3
+ import { r as isDevMode, t as getMonorepoRoot } from "./_agent-C8Jk-ZcP.mjs";
4
+ import { REPO_URL, downloadAndMergeTemplate } from "./_templates-B1kt3BGt.mjs";
5
5
  import path from "node:path";
6
6
  import fs from "node:fs/promises";
7
7
  //#region _init.ts
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env node
2
+ import { requiredProviderEnvVars } from "@alexkroman1/aai/runtime";
3
+ //#region _preflight.ts
4
+ /**
5
+ * The deploy-time credential preflight — the classic dev/prod failure, caught
6
+ * before the upload instead of at the deployed agent's first session.
7
+ *
8
+ * An agent that ran locally on shell-exported provider keys dies after deploy
9
+ * with what looks like a provider outage: `aai dev` falls back to the shell
10
+ * (`withHostCredentialFallback`), and the platform never will. Naming the key
11
+ * here turns that into one line of output while the developer is still in the
12
+ * directory that owns the `.env`.
13
+ *
14
+ * **This lives in the CLI because the agent's config does.** The platform
15
+ * stores no description of a bundle and never evaluates one (see "The
16
+ * platform stores no agent config" in packages/aai-server/CLAUDE.md), so the
17
+ * only place that knows an agent needs `CARTESIA_API_KEY` is the process that
18
+ * just built it.
19
+ *
20
+ * **It WARNS, and must not be turned into a hard failure.** The CLI sees the
21
+ * env it is about to upload; it cannot see what is already stored against the
22
+ * slug from an earlier `aai secret put`, so a key the platform holds looks
23
+ * missing from here. The server-side check this replaced could see both and
24
+ * so could reject — moving the config to where it is authored costs that
25
+ * accuracy, and a false rejection is worse than a false warning: it blocks a
26
+ * deploy that would have worked.
27
+ */
28
+ /**
29
+ * Env var names the agent needs that the env being uploaded doesn't supply.
30
+ * Empty values count as missing — an empty credential authenticates nothing.
31
+ *
32
+ * Two sources: provider credentials derived from the stt/llm/tts/s2s
33
+ * descriptors (the same registry-backed derivation the runtime resolves keys
34
+ * with), and the agent's own declared `requiredEnv` — an `agent()` field for
35
+ * custom keys tools read from `ctx.env`, which no static derivation can see.
36
+ */
37
+ function missingCredentials(config, env) {
38
+ return [.../* @__PURE__ */ new Set([...requiredProviderEnvVars(config), ...config.requiredEnv ?? []])].filter((name) => !env[name]);
39
+ }
40
+ /** One line naming the missing keys and what to do about them. */
41
+ function missingCredentialMessage(missing) {
42
+ const plural = missing.length > 1;
43
+ return `Missing credential${plural ? "s" : ""} the agent needs to start: ${missing.join(", ")}. Declare ${plural ? "them" : "it"} in .env and redeploy (already set on the platform with \`aai secret put\`? then this is already handled).`;
44
+ }
45
+ //#endregion
46
+ export { missingCredentialMessage, missingCredentials };