@alexkroman1/aai-cli 1.8.3 → 1.9.1

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 (31) hide show
  1. package/dist/_agent-cHGzbDVG.mjs +108 -0
  2. package/dist/_api-client-yJD7xGfd.mjs +47 -0
  3. package/dist/_bundler-BRQFvx00.mjs +62 -0
  4. package/dist/_config-zV70t71V.mjs +125 -0
  5. package/dist/_dev-server-CBrIeDoI.mjs +214 -0
  6. package/dist/{_init-Bh5etNEh.mjs → _init-y7K-dzni.mjs} +17 -43
  7. package/dist/{_output-vvoR6N1x.mjs → _output-fy2bXRNb.mjs} +12 -19
  8. package/dist/{_ui-nOp7hFVy.mjs → _ui-YqQ6Fi8K.mjs} +14 -20
  9. package/dist/_utils-BeU10C7O.mjs +55 -0
  10. package/dist/_vite-env-CmXG4ZAu.mjs +31 -0
  11. package/dist/cli.mjs +41 -40
  12. package/dist/client-bundler-D4VKqCxw.mjs +125 -0
  13. package/dist/client-bundler.mjs +3 -0
  14. package/dist/{delete-BEC2ZXbk.mjs → delete-DE35s3d5.mjs} +7 -8
  15. package/dist/{deploy-D8uMWSDH.mjs → deploy-C0LittrE.mjs} +20 -18
  16. package/dist/{dev-D2EwGHYi.mjs → dev-BvjnXK2x.mjs} +10 -3
  17. package/dist/{init-GWTdzrrm.mjs → init-kYsRaf7e.mjs} +29 -75
  18. package/dist/{secret-DlSN4FdF.mjs → secret-D-90QtDQ.mjs} +14 -16
  19. package/dist/{test-BLYrV5ap.mjs → test-C6jwCnFu.mjs} +30 -5
  20. package/dist/types.mjs +2 -0
  21. package/dist/worker-bundler.mjs +68 -0
  22. package/package.json +24 -13
  23. package/dist/_agent-De8f1JdZ.mjs +0 -38
  24. package/dist/_api-client-Cf9Lv2Rw.mjs +0 -46
  25. package/dist/_bundler-CMMSjh9o.mjs +0 -143
  26. package/dist/_config-B0FXR5GQ.mjs +0 -77
  27. package/dist/_default-html-BasMCgr9.mjs +0 -62
  28. package/dist/_dev-server-DMFyqc9v.mjs +0 -143
  29. package/dist/_utils-DZo3_J_v.mjs +0 -23
  30. package/dist/rolldown-runtime-uZa2dNXj.mjs +0 -15
  31. /package/dist/{_server-common-BVkNI-o4.mjs → _server-common-QpOTRZgi.mjs} +0 -0
@@ -0,0 +1,108 @@
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";
3
+ import { existsSync } from "node:fs";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
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
+ const DEFAULT_SERVER = "https://aai-agent.fly.dev";
17
+ const DEFAULT_DEV_SERVER = "http://localhost:8080";
18
+ let _cachedMonorepoRoot;
19
+ function getMonorepoRoot() {
20
+ if (_cachedMonorepoRoot !== void 0) return _cachedMonorepoRoot;
21
+ const cliDir = path.dirname(fileURLToPath(import.meta.url));
22
+ const root1 = path.resolve(cliDir, "../..");
23
+ const root2 = path.resolve(cliDir, "../../..");
24
+ if (existsSync(path.join(root1, "pnpm-workspace.yaml"))) _cachedMonorepoRoot = root1;
25
+ else if (existsSync(path.join(root2, "pnpm-workspace.yaml"))) _cachedMonorepoRoot = root2;
26
+ else _cachedMonorepoRoot = null;
27
+ return _cachedMonorepoRoot;
28
+ }
29
+ function isDevMode() {
30
+ if (process.env.AAI_NO_DEV === "1") return false;
31
+ return getMonorepoRoot() !== null;
32
+ }
33
+ function stripTrailingSlash(url) {
34
+ return url.replace(/\/+$/, "");
35
+ }
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
+ /**
44
+ * Whether `origin` may receive a credential without prior user approval:
45
+ * the shipped platform, or anything on this machine.
46
+ */
47
+ 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
+ }
54
+ }
55
+ /**
56
+ * Resolve which platform server to talk to.
57
+ *
58
+ * Precedence: an explicit `--server` flag, then dev mode, then the project
59
+ * config, then the shipped default.
60
+ *
61
+ * `configUrl` comes from `.aai/project.json` — a file in the working tree, so
62
+ * a cloned repo controls it. Because callers pair this URL with the user's API
63
+ * key (and, for `aai secret`, with secret values), a config-supplied origin is
64
+ * only honored when it is implicitly trusted or previously approved by the
65
+ * user via `--server`. Otherwise a repo could redirect a credentialed request
66
+ * to a host of its choosing simply by shipping a `project.json`, and
67
+ * `aai deploy` would hand over the developer's key on first run.
68
+ *
69
+ * @param approvedOrigins - Origins from the user-owned global config.
70
+ */
71
+ function resolveServerUrl(explicit, configUrl, approvedOrigins = []) {
72
+ if (explicit) return stripTrailingSlash(explicit);
73
+ if (isDevMode()) return DEFAULT_DEV_SERVER;
74
+ if (!configUrl) return DEFAULT_SERVER;
75
+ const url = stripTrailingSlash(configUrl);
76
+ const origin = serverOrigin(url);
77
+ if (origin === null) throw new Error(`Invalid serverUrl in .aai/project.json: ${configUrl}\n Expected an absolute http(s) URL.`);
78
+ if (isImplicitlyTrusted(origin) || approvedOrigins.includes(origin)) return url;
79
+ throw new Error(`Refusing to send your API key to ${origin}.\n It came from .aai/project.json, which is part of this project's files, not from you.
80
+ If you do intend to use that server, re-run with --server ${origin} to approve it.`);
81
+ }
82
+ /**
83
+ * Resolve everything needed to talk to the platform: project config (null if
84
+ * the project has never been deployed), server URL, and API key.
85
+ */
86
+ async function resolveDeployTarget(cwd, explicitServer) {
87
+ const config = await readProjectConfig(cwd);
88
+ const globalConfig = await readGlobalConfig();
89
+ const serverUrl = resolveServerUrl(explicitServer, config?.serverUrl, globalConfig.approvedServers ?? []);
90
+ if (explicitServer) await approveServer(serverUrl);
91
+ return {
92
+ config,
93
+ serverUrl,
94
+ apiKey: await ensureApiKey()
95
+ };
96
+ }
97
+ /** Like resolveDeployTarget, but requires an existing deployment (project config). */
98
+ async function getServerInfo(cwd, explicitServer) {
99
+ const { config, serverUrl, apiKey } = await resolveDeployTarget(cwd, explicitServer);
100
+ if (!config) throw new Error("No .aai/project.json found — run `aai deploy` first");
101
+ return {
102
+ serverUrl,
103
+ slug: config.slug,
104
+ apiKey
105
+ };
106
+ }
107
+ //#endregion
108
+ export { resolveDeployTarget as a, isDevMode as i, getMonorepoRoot as n, getServerInfo as r, _agent_exports as t };
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env node
2
+ import { FetchError, ofetch } from "ofetch";
3
+ //#region _api-client.ts
4
+ /**
5
+ * Shared HTTP helper for platform API calls (deploy, delete, secrets).
6
+ *
7
+ * Built on ofetch: JSON bodies are serialized (with Content-Type set) and
8
+ * responses parsed automatically, and transient failures (network errors,
9
+ * 5xx/429) are retried before surfacing an error.
10
+ */
11
+ const HINT_INVALID_API_KEY = "Your API key may be invalid. Run `aai` to re-enter your AssemblyAI API key.";
12
+ /**
13
+ * Send an authenticated request to the platform API and return the parsed
14
+ * JSON response. Throws a descriptive error with status-specific hints on
15
+ * failure (the 401 hint is always included; pass more via `hints`).
16
+ */
17
+ async function apiRequest(url, opts) {
18
+ const client = opts.fetch ? ofetch.create({}, { fetch: opts.fetch }) : ofetch;
19
+ try {
20
+ return await client(url, {
21
+ method: opts.method ?? "GET",
22
+ headers: {
23
+ Authorization: `Bearer ${opts.apiKey}`,
24
+ ...opts.headers
25
+ },
26
+ ...opts.body !== void 0 ? { body: opts.body } : {},
27
+ retry: 2,
28
+ retryDelay: 300
29
+ });
30
+ } catch (err) {
31
+ throw toApiError(err, url, opts);
32
+ }
33
+ }
34
+ /** Format an ofetch failure into a descriptive, action-centric error. */
35
+ function toApiError(err, url, opts) {
36
+ if (err instanceof FetchError && err.statusCode !== void 0) {
37
+ const status = err.statusCode;
38
+ const body = typeof err.data === "string" ? err.data : JSON.stringify(err.data ?? "");
39
+ const hint = status === 401 ? HINT_INVALID_API_KEY : opts.hints?.[status];
40
+ return /* @__PURE__ */ new Error(`${opts.action} failed (HTTP ${status}): ${body}${hint ? `\n ${hint}` : ""}`);
41
+ }
42
+ const hint = "Check your network connection and verify the server URL is correct.";
43
+ const cause = err instanceof FetchError && err.cause !== void 0 ? err.cause : err;
44
+ return new Error(`${opts.action} failed: could not reach ${url}\n ${hint}`, { cause });
45
+ }
46
+ //#endregion
47
+ export { apiRequest as t };
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+ import { i as ok } from "./_output-fy2bXRNb.mjs";
3
+ import { n as log } from "./_ui-YqQ6Fi8K.mjs";
4
+ import { s as validateAgentExport } from "./_utils-BeU10C7O.mjs";
5
+ import { t as buildClient } from "./client-bundler-D4VKqCxw.mjs";
6
+ import { buildWorker } from "./worker-bundler.mjs";
7
+ import path from "node:path";
8
+ import { pathToFileURL } from "node:url";
9
+ import fs from "node:fs/promises";
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
+ async function evalWorkerBundle(code, cwd) {
40
+ const evalDir = path.join(cwd, ".aai", "eval");
41
+ await fs.mkdir(evalDir, { recursive: true });
42
+ const tmpPath = path.join(evalDir, `agent-${Date.now()}-${Math.random().toString(36).slice(2)}.mjs`);
43
+ try {
44
+ await fs.writeFile(tmpPath, code);
45
+ const mod = await import(pathToFileURL(tmpPath).href);
46
+ const agentDef = mod.default ?? mod;
47
+ validateAgentExport(agentDef);
48
+ return agentDef;
49
+ } finally {
50
+ await fs.rm(tmpPath).catch(() => {});
51
+ }
52
+ }
53
+ async function executeBuild(cwd) {
54
+ const bundle = await buildAgentBundle(cwd, { minify: true });
55
+ log.success("Build complete");
56
+ return ok({
57
+ name: bundle.agentConfig.name,
58
+ workerBytes: bundle.worker.length
59
+ });
60
+ }
61
+ //#endregion
62
+ export { buildAgentBundle, evalWorkerBundle, executeBuild };
@@ -0,0 +1,125 @@
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";
5
+ import path from "node:path";
6
+ import * as p from "@clack/prompts";
7
+ import os from "node:os";
8
+ import { consola } from "consola";
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
+ //#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
+ /**
34
+ * `.aai/project.json` lives in the working tree, so everything in it is
35
+ * untrusted input — a cloned repo can supply any value.
36
+ *
37
+ * `serverUrl` is deliberately NOT validated here. A failed field makes
38
+ * `readProjectConfig` return null for the whole file, which discards the
39
+ * `slug` too — and a deploy with no slug generates a fresh one, silently
40
+ * creating a duplicate agent and overwriting the config. The URL is instead
41
+ * validated where it is used, by `resolveServerUrl`, which rejects anything
42
+ * that isn't an approved http(s) origin.
43
+ */
44
+ const ProjectConfigSchema = z.object({
45
+ slug: z.string(),
46
+ serverUrl: z.string(),
47
+ sessionId: z.string().optional()
48
+ });
49
+ function getConfigDir() {
50
+ if (process.platform === "win32") return path.join(process.env.APPDATA ?? path.join(os.homedir(), "AppData", "Roaming"), "aai");
51
+ return path.join(process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), ".config"), "aai");
52
+ }
53
+ async function readProjectConfig(agentDir) {
54
+ 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;
59
+ }
60
+ return parsed.data;
61
+ }
62
+ async function writeProjectConfig(agentDir, data) {
63
+ await writeJson(path.join(agentDir, ".aai", "project.json"), data);
64
+ }
65
+ /**
66
+ * Origin of `url`, or `null` when it is not an absolute http(s) URL.
67
+ *
68
+ * Non-HTTP schemes are rejected rather than returned: `new URL()` yields the
69
+ * opaque origin `"null"` for them, which would otherwise flow on as if it
70
+ * were a real origin.
71
+ */
72
+ function serverOrigin(url) {
73
+ let parsed;
74
+ try {
75
+ parsed = new URL(url);
76
+ } catch {
77
+ return null;
78
+ }
79
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
80
+ return parsed.origin;
81
+ }
82
+ /**
83
+ * Record `url`'s origin as user-approved, so later commands in this project
84
+ * may send credentials there without re-passing `--server`.
85
+ */
86
+ async function approveServer(url, configDir) {
87
+ const origin = serverOrigin(url);
88
+ if (!origin) return;
89
+ const dir = configDir ?? getConfigDir();
90
+ const config = await readGlobalConfig(dir);
91
+ const approved = config.approvedServers ?? [];
92
+ if (approved.includes(origin)) return;
93
+ await writeGlobalConfig(dir, {
94
+ ...config,
95
+ approvedServers: [...approved, origin]
96
+ });
97
+ }
98
+ async function readGlobalConfig(configDir) {
99
+ const dir = configDir ?? getConfigDir();
100
+ return await readJson(path.join(dir, "config.json")) ?? {};
101
+ }
102
+ async function writeGlobalConfig(configDir, data) {
103
+ await writeJson(path.join(configDir, "config.json"), data);
104
+ }
105
+ async function ensureApiKey(configDir) {
106
+ const dir = configDir ?? getConfigDir();
107
+ const config = await readGlobalConfig(dir);
108
+ if (config.apiKey) return config.apiKey;
109
+ const envKey = process.env.ASSEMBLYAI_API_KEY;
110
+ if (envKey) {
111
+ await writeGlobalConfig(dir, {
112
+ ...config,
113
+ apiKey: envKey
114
+ });
115
+ return envKey;
116
+ }
117
+ const apiKey = unwrapCancel(await p.password({ message: "Enter your AssemblyAI API key" }));
118
+ await writeGlobalConfig(dir, {
119
+ ...config,
120
+ apiKey
121
+ });
122
+ return apiKey;
123
+ }
124
+ //#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 };
@@ -0,0 +1,214 @@
1
+ #!/usr/bin/env node
2
+ import { r as ensureApiKey } from "./_config-zV70t71V.mjs";
3
+ import { n as log } from "./_ui-YqQ6Fi8K.mjs";
4
+ import { n as errorMessage } from "./_utils-BeU10C7O.mjs";
5
+ import { n as fallbackHtmlPlugin } from "./client-bundler-D4VKqCxw.mjs";
6
+ import { buildWorker } from "./worker-bundler.mjs";
7
+ import { evalWorkerBundle } from "./_bundler-BRQFvx00.mjs";
8
+ import { t as resolveServerEnv } from "./_server-common-QpOTRZgi.mjs";
9
+ import { createRequire } from "node:module";
10
+ import { existsSync } from "node:fs";
11
+ import path from "node:path";
12
+ import { requiredProviderEnvVars, withHostCredentialFallback } from "@alexkroman1/aai/runtime";
13
+ import { watch } from "chokidar";
14
+ import getPort, { portNumbers } from "get-port";
15
+ import pDebounce from "p-debounce";
16
+ //#region _dev-server.ts
17
+ /**
18
+ * Dev server for directory-based agents.
19
+ *
20
+ * Imports agent.ts directly for the full agent definition,
21
+ * builds a runtime, and starts an HTTP+WebSocket server. Watches for
22
+ * file changes and restarts automatically. Optionally runs Vite for
23
+ * client SPA HMR.
24
+ */
25
+ async function resolveAgentEnv(root, agentDef) {
26
+ const env = await resolveServerEnv(root);
27
+ const required = requiredProviderEnvVars(agentDef);
28
+ if (required.includes("ASSEMBLYAI_API_KEY") && !env.ASSEMBLYAI_API_KEY) env.ASSEMBLYAI_API_KEY = await ensureApiKey();
29
+ const missing = required.filter((name) => !(env[name] || process.env[name]));
30
+ if (missing.length > 0) log.warn(`Missing provider credential${missing.length > 1 ? "s" : ""}: ${missing.join(", ")}. Set ${missing.length > 1 ? "them" : "it"} in .env or the environment.`);
31
+ return env;
32
+ }
33
+ /**
34
+ * The env handed to `createServer` for host-mode connections: provider
35
+ * credentials plus the `AAI_ALLOW_HOST` gate read straight from the shell
36
+ * (it is a control variable, not something an agent declares in `.env`).
37
+ */
38
+ function hostModeEnv(providerEnv) {
39
+ const gate = process.env.AAI_ALLOW_HOST;
40
+ return gate === void 0 ? providerEnv : {
41
+ ...providerEnv,
42
+ AAI_ALLOW_HOST: gate
43
+ };
44
+ }
45
+ /**
46
+ * Explicit bind host for the dev server, or `undefined` to take the
47
+ * loopback default. An empty `AAI_DEV_HOST` means "unset", not "every
48
+ * interface" — Node treats `listen(port, "")` as 0.0.0.0, which would quietly
49
+ * undo the loopback default this exists to guard.
50
+ */
51
+ function devBindHost() {
52
+ const host = process.env.AAI_DEV_HOST?.trim();
53
+ return host ? host : void 0;
54
+ }
55
+ /**
56
+ * Load the agent definition by bundling agent.ts (and all its local imports)
57
+ * into a single ESM file, then importing that. A raw `import(agent.ts?t=...)`
58
+ * only cache-busts agent.ts itself — transitive imports (./tools.ts, etc.)
59
+ * stay in Node's ESM registry, so edits to them are ignored on reload.
60
+ * Bundling picks them up and matches the deploy path exactly.
61
+ */
62
+ async function loadAgentDef(cwd) {
63
+ return evalWorkerBundle(await buildWorker(cwd), cwd);
64
+ }
65
+ /**
66
+ * True for paths that should never trigger a restart: anything inside
67
+ * `node_modules/` and any dot-entry (`.git/`, `.aai/`, `.DS_Store`, …).
68
+ * `.git/` especially matters — commits and status checks churn the index
69
+ * and would otherwise cause spurious full backend restarts.
70
+ *
71
+ * Exception: `.env` / `.env.*` files stay watched — env edits should
72
+ * restart the server with the new values.
73
+ */
74
+ function isIgnoredPath(dir, filePath) {
75
+ const rel = path.relative(dir, filePath);
76
+ if (!rel || rel.startsWith("..")) return false;
77
+ return rel.split(path.sep).some((segment) => {
78
+ if (segment === "node_modules") return true;
79
+ if (segment === ".env" || segment.startsWith(".env.")) return false;
80
+ return segment.startsWith(".");
81
+ });
82
+ }
83
+ /**
84
+ * Watch the agent directory for changes and call `onChange` when detected.
85
+ * Debounces to avoid rapid restarts. Uses chokidar for reliable recursive
86
+ * watching across platforms (raw `fs.watch` misses events on Linux).
87
+ */
88
+ function watchDirectory(dir, onChange) {
89
+ const debouncedChange = pDebounce(() => {
90
+ log.info("File change detected, restarting...");
91
+ onChange();
92
+ }, 300);
93
+ const watcher = watch(dir, {
94
+ ignored: (filePath) => isIgnoredPath(dir, filePath),
95
+ ignoreInitial: true,
96
+ persistent: false
97
+ });
98
+ watcher.on("all", () => void debouncedChange());
99
+ return watcher;
100
+ }
101
+ /** Locate the pre-built default aai-ui client (served when no custom client.tsx). */
102
+ function resolveDefaultClientDir() {
103
+ const pkgPath = createRequire(import.meta.url).resolve("@alexkroman1/aai-ui/package.json");
104
+ return path.join(path.dirname(pkgPath), "dist", "default-client");
105
+ }
106
+ /**
107
+ * Start the dev server for a directory-based agent.
108
+ *
109
+ * Returns a cleanup function to shut down the server and watchers.
110
+ */
111
+ async function startDevServer(opts) {
112
+ const { cwd, port } = opts;
113
+ const { createRuntime, createServer } = await import("@alexkroman1/aai/runtime");
114
+ const hasClient = existsSync(path.join(cwd, "client.tsx"));
115
+ const backendPort = hasClient ? await getPort({ port: portNumbers(port + 1, port + 100) }) : port;
116
+ const vitePort = port;
117
+ const clientDirOpt = hasClient ? {} : { clientDir: resolveDefaultClientDir() };
118
+ /** Full build sequence, shared by initial startup and every restart. */
119
+ async function buildServer() {
120
+ const agentDef = await loadAgentDef(cwd);
121
+ const env = await resolveAgentEnv(cwd, agentDef);
122
+ const providerEnv = withHostCredentialFallback(env);
123
+ const runtime = createRuntime({
124
+ agent: agentDef,
125
+ env,
126
+ providerEnv
127
+ });
128
+ return createServer({
129
+ runtime,
130
+ name: agentDef.name,
131
+ env: hostModeEnv(providerEnv),
132
+ hostBaseAgent: agentDef,
133
+ ...clientDirOpt
134
+ });
135
+ }
136
+ const agentServer = await buildServer();
137
+ await agentServer.listen(backendPort, devBindHost());
138
+ let viteServer;
139
+ if (hasClient) {
140
+ const { createServer: createViteServer } = await import("vite");
141
+ const target = `http://localhost:${backendPort}`;
142
+ viteServer = await createViteServer({
143
+ root: cwd,
144
+ plugins: [fallbackHtmlPlugin(cwd)],
145
+ server: {
146
+ port: vitePort,
147
+ proxy: {
148
+ "/health": target,
149
+ "/websocket": {
150
+ target,
151
+ ws: true
152
+ }
153
+ }
154
+ }
155
+ });
156
+ await viteServer.listen();
157
+ }
158
+ let restarting = false;
159
+ let pendingRestart = false;
160
+ let closed = false;
161
+ let currentServer = agentServer;
162
+ const watcher = watchDirectory(cwd, () => {
163
+ if (restarting) {
164
+ pendingRestart = true;
165
+ return;
166
+ }
167
+ restarting = true;
168
+ restart().finally(() => {
169
+ restarting = false;
170
+ });
171
+ });
172
+ async function restart() {
173
+ do {
174
+ pendingRestart = false;
175
+ await restartOnce();
176
+ } while (pendingRestart && !closed);
177
+ }
178
+ async function restartOnce() {
179
+ let newServer;
180
+ try {
181
+ newServer = await buildServer();
182
+ } catch (err) {
183
+ log.error(`Restart failed: ${errorMessage(err)} (previous server still running)`);
184
+ return;
185
+ }
186
+ if (closed) {
187
+ await newServer.close().catch(() => void 0);
188
+ return;
189
+ }
190
+ try {
191
+ await currentServer.close();
192
+ } catch {}
193
+ try {
194
+ await newServer.listen(backendPort, devBindHost());
195
+ currentServer = newServer;
196
+ if (closed) {
197
+ await newServer.close().catch(() => void 0);
198
+ return;
199
+ }
200
+ log.success("Restarted");
201
+ } catch (err) {
202
+ log.error(`Restart failed: ${errorMessage(err)}`);
203
+ await newServer.close().catch(() => void 0);
204
+ }
205
+ }
206
+ return async () => {
207
+ closed = true;
208
+ await watcher.close();
209
+ await viteServer?.close();
210
+ await currentServer.close();
211
+ };
212
+ }
213
+ //#endregion
214
+ export { startDevServer };
@@ -1,27 +1,19 @@
1
1
  #!/usr/bin/env node
2
- import { isDevMode } from "./_agent-De8f1JdZ.mjs";
2
+ import { a as readJson, c as writeJson, i as isEexist } from "./_utils-BeU10C7O.mjs";
3
+ import { i as isDevMode, n as getMonorepoRoot } from "./_agent-cHGzbDVG.mjs";
3
4
  import { existsSync } from "node:fs";
4
5
  import path from "node:path";
5
- import { fileURLToPath } from "node:url";
6
6
  import fs from "node:fs/promises";
7
7
  import os from "node:os";
8
8
  import { downloadTemplate } from "giget";
9
9
  //#region _templates.ts
10
10
  const GIGET_SOURCE = "github:alexkroman/agent/packages/aai-templates";
11
11
  const GIGET_REF = process.env.AAI_TEMPLATES_REF ?? "main";
12
- /** Resolve the local aai-templates package directory (dev mode only). */
13
- function resolveLocalTemplatesDir() {
14
- const cliDir = path.dirname(fileURLToPath(import.meta.url));
15
- const fromSrc = path.resolve(cliDir, "../aai-templates");
16
- const fromDist = path.resolve(cliDir, "../../aai-templates");
17
- if (existsSync(fromSrc)) return fromSrc;
18
- if (existsSync(fromDist)) return fromDist;
19
- throw new Error("Cannot find local aai-templates package");
20
- }
21
12
  /** Resolve the templates directory — local in dev, giget download in prod. */
22
13
  async function resolveTemplatesDir() {
23
14
  if (process.env.AAI_TEMPLATES_DIR) return process.env.AAI_TEMPLATES_DIR;
24
- if (isDevMode()) return resolveLocalTemplatesDir();
15
+ const monorepoRoot = isDevMode() ? getMonorepoRoot() : null;
16
+ if (monorepoRoot) return path.join(monorepoRoot, "packages", "aai-templates");
25
17
  const extractDir = await fs.mkdtemp(path.join(os.tmpdir(), "aai-templates-"));
26
18
  const { dir } = await downloadTemplate(`${GIGET_SOURCE}#${GIGET_REF}`, {
27
19
  dir: extractDir,
@@ -43,23 +35,11 @@ async function downloadAndMergeTemplate(template, targetDir) {
43
35
  force: true
44
36
  });
45
37
  const scaffoldDir = path.join(root, "scaffold");
46
- if (existsSync(scaffoldDir)) {
47
- const entries = await fs.readdir(scaffoldDir, {
48
- recursive: true,
49
- withFileTypes: true
50
- });
51
- for (const entry of entries) {
52
- if (!entry.isFile()) continue;
53
- const rel = path.relative(scaffoldDir, path.join(entry.parentPath, entry.name));
54
- const destPath = path.join(targetDir, rel);
55
- await fs.mkdir(path.dirname(destPath), { recursive: true });
56
- try {
57
- await fs.copyFile(path.join(scaffoldDir, rel), destPath, fs.constants.COPYFILE_EXCL);
58
- } catch (err) {
59
- if (!(err instanceof Error && "code" in err && err.code === "EEXIST")) throw err;
60
- }
61
- }
62
- }
38
+ if (existsSync(scaffoldDir)) await fs.cp(scaffoldDir, targetDir, {
39
+ recursive: true,
40
+ force: false,
41
+ errorOnExist: false
42
+ });
63
43
  }
64
44
  //#endregion
65
45
  //#region _init.ts
@@ -111,16 +91,11 @@ const WORKSPACE_PKG_DIRS = {
111
91
  /** Rewrite workspace deps to link: paths so pnpm links to local source. */
112
92
  async function patchPackageJsonForWorkspace(targetDir) {
113
93
  const pkgPath = path.join(targetDir, "package.json");
114
- let raw;
115
- try {
116
- raw = await fs.readFile(pkgPath, "utf-8");
117
- } catch {
118
- return;
119
- }
120
- const pkgJson = JSON.parse(raw);
94
+ const pkgJson = await readJson(pkgPath);
95
+ if (!pkgJson) return;
121
96
  pkgJson.name = path.basename(targetDir);
122
97
  delete pkgJson.packageManager;
123
- const { getMonorepoRoot } = await import("./_agent-De8f1JdZ.mjs");
98
+ const { getMonorepoRoot } = await import("./_agent-cHGzbDVG.mjs").then((n) => n.t);
124
99
  const root = getMonorepoRoot();
125
100
  if (!root) return;
126
101
  const packagesDir = path.join(root, "packages");
@@ -132,11 +107,11 @@ async function patchPackageJsonForWorkspace(targetDir) {
132
107
  if (dir) deps[key] = `link:${path.relative(targetDir, path.join(packagesDir, dir))}`;
133
108
  }
134
109
  }
135
- await fs.writeFile(pkgPath, `${JSON.stringify(pkgJson, null, 2)}\n`);
110
+ await writeJson(pkgPath, pkgJson);
136
111
  }
137
112
  async function runInit(opts) {
138
- const { targetDir } = opts;
139
- await downloadAndMergeTemplate(opts.template ?? "simple", targetDir);
113
+ const { targetDir, template } = opts;
114
+ await downloadAndMergeTemplate(template, targetDir);
140
115
  if (isDevMode()) {
141
116
  await patchPackageJsonForWorkspace(targetDir);
142
117
  try {
@@ -144,16 +119,15 @@ async function runInit(opts) {
144
119
  } catch {}
145
120
  }
146
121
  try {
147
- await fs.copyFile(path.join(targetDir, ".env.example"), path.join(targetDir, ".env"));
122
+ await fs.copyFile(path.join(targetDir, ".env.example"), path.join(targetDir, ".env"), fs.constants.COPYFILE_EXCL);
148
123
  } catch {}
149
124
  const readmePath = path.join(targetDir, "README.md");
150
125
  const slug = path.basename(path.resolve(targetDir));
151
126
  try {
152
127
  await fs.writeFile(readmePath, readmeContent(slug), { flag: "wx" });
153
128
  } catch (err) {
154
- if (!(err instanceof Error && "code" in err && err.code === "EEXIST")) throw err;
129
+ if (!isEexist(err)) throw err;
155
130
  }
156
- return targetDir;
157
131
  }
158
132
  //#endregion
159
133
  export { runInit };