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