@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.
- package/dist/_agent-cHGzbDVG.mjs +108 -0
- package/dist/_api-client-yJD7xGfd.mjs +47 -0
- package/dist/_bundler-BRQFvx00.mjs +62 -0
- package/dist/_config-zV70t71V.mjs +125 -0
- package/dist/_dev-server-CBrIeDoI.mjs +214 -0
- package/dist/{_init-Bh5etNEh.mjs → _init-y7K-dzni.mjs} +17 -43
- package/dist/{_output-vvoR6N1x.mjs → _output-fy2bXRNb.mjs} +12 -19
- package/dist/{_ui-nOp7hFVy.mjs → _ui-YqQ6Fi8K.mjs} +14 -20
- package/dist/_utils-BeU10C7O.mjs +55 -0
- package/dist/_vite-env-CmXG4ZAu.mjs +31 -0
- package/dist/cli.mjs +41 -40
- package/dist/client-bundler-D4VKqCxw.mjs +125 -0
- package/dist/client-bundler.mjs +3 -0
- package/dist/{delete-BEC2ZXbk.mjs → delete-DE35s3d5.mjs} +7 -8
- package/dist/{deploy-D8uMWSDH.mjs → deploy-C0LittrE.mjs} +20 -18
- package/dist/{dev-D2EwGHYi.mjs → dev-BvjnXK2x.mjs} +10 -3
- package/dist/{init-GWTdzrrm.mjs → init-kYsRaf7e.mjs} +29 -75
- package/dist/{secret-DlSN4FdF.mjs → secret-D-90QtDQ.mjs} +14 -16
- package/dist/{test-BLYrV5ap.mjs → test-C6jwCnFu.mjs} +30 -5
- package/dist/types.mjs +2 -0
- package/dist/worker-bundler.mjs +68 -0
- package/package.json +24 -13
- package/dist/_agent-De8f1JdZ.mjs +0 -38
- package/dist/_api-client-Cf9Lv2Rw.mjs +0 -46
- package/dist/_bundler-CMMSjh9o.mjs +0 -143
- package/dist/_config-B0FXR5GQ.mjs +0 -77
- package/dist/_default-html-BasMCgr9.mjs +0 -62
- package/dist/_dev-server-DMFyqc9v.mjs +0 -143
- package/dist/_utils-DZo3_J_v.mjs +0 -23
- package/dist/rolldown-runtime-uZa2dNXj.mjs +0 -15
- /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 {
|
|
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
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
115
|
-
|
|
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-
|
|
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
|
|
110
|
+
await writeJson(pkgPath, pkgJson);
|
|
136
111
|
}
|
|
137
112
|
async function runInit(opts) {
|
|
138
|
-
const { targetDir } = opts;
|
|
139
|
-
await downloadAndMergeTemplate(
|
|
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
|
|
129
|
+
if (!isEexist(err)) throw err;
|
|
155
130
|
}
|
|
156
|
-
return targetDir;
|
|
157
131
|
}
|
|
158
132
|
//#endregion
|
|
159
133
|
export { runInit };
|