@alexkroman1/aai-cli 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/dist/_build-p1HHkdon.mjs +132 -0
  3. package/dist/_discover-BzlCDVZ6.mjs +161 -0
  4. package/dist/_init-l_uoyFCN.mjs +82 -0
  5. package/dist/_link-BGXGFYWa.mjs +47 -0
  6. package/dist/_server-common-qLA1QU2C.mjs +36 -0
  7. package/dist/_ui-kJIua5L9.mjs +44 -0
  8. package/dist/cli.mjs +318 -0
  9. package/dist/deploy-KyNJaoP5.mjs +86 -0
  10. package/dist/dev-DBFvKyzk.mjs +39 -0
  11. package/dist/init-BWG5OrQa.mjs +65 -0
  12. package/dist/rag-BnCMnccf.mjs +173 -0
  13. package/dist/secret-CzeHIGzE.mjs +50 -0
  14. package/dist/start-C1qkhU4O.mjs +23 -0
  15. package/package.json +39 -0
  16. package/templates/_shared/.env.example +5 -0
  17. package/templates/_shared/CLAUDE.md +1051 -0
  18. package/templates/_shared/biome.json +32 -0
  19. package/templates/_shared/global.d.ts +1 -0
  20. package/templates/_shared/index.html +16 -0
  21. package/templates/_shared/package.json +23 -0
  22. package/templates/_shared/tsconfig.json +15 -0
  23. package/templates/code-interpreter/agent.ts +27 -0
  24. package/templates/code-interpreter/client.tsx +3 -0
  25. package/templates/css.d.ts +1 -0
  26. package/templates/dispatch-center/agent.ts +1227 -0
  27. package/templates/dispatch-center/client.tsx +505 -0
  28. package/templates/embedded-assets/agent.ts +48 -0
  29. package/templates/embedded-assets/client.tsx +3 -0
  30. package/templates/embedded-assets/knowledge.json +20 -0
  31. package/templates/health-assistant/agent.ts +160 -0
  32. package/templates/health-assistant/client.tsx +3 -0
  33. package/templates/infocom-adventure/agent.ts +164 -0
  34. package/templates/infocom-adventure/client.tsx +300 -0
  35. package/templates/math-buddy/agent.ts +21 -0
  36. package/templates/math-buddy/client.tsx +3 -0
  37. package/templates/memory-agent/agent.ts +20 -0
  38. package/templates/memory-agent/client.tsx +3 -0
  39. package/templates/night-owl/agent.ts +98 -0
  40. package/templates/night-owl/client.tsx +12 -0
  41. package/templates/personal-finance/agent.ts +26 -0
  42. package/templates/personal-finance/client.tsx +3 -0
  43. package/templates/pizza-ordering/agent.ts +218 -0
  44. package/templates/pizza-ordering/client.tsx +264 -0
  45. package/templates/simple/agent.ts +6 -0
  46. package/templates/simple/client.tsx +3 -0
  47. package/templates/smart-research/agent.ts +164 -0
  48. package/templates/smart-research/client.tsx +3 -0
  49. package/templates/solo-rpg/agent.ts +1244 -0
  50. package/templates/solo-rpg/client.tsx +698 -0
  51. package/templates/support/README.md +62 -0
  52. package/templates/support/agent.ts +19 -0
  53. package/templates/support/client.tsx +3 -0
  54. package/templates/travel-concierge/agent.ts +29 -0
  55. package/templates/travel-concierge/client.tsx +3 -0
  56. package/templates/tsconfig.json +1 -0
  57. package/templates/web-researcher/agent.ts +17 -0
  58. package/templates/web-researcher/client.tsx +3 -0
package/dist/cli.mjs ADDED
@@ -0,0 +1,318 @@
1
+ #!/usr/bin/env node
2
+ import { c as resolveCwd, r as getApiKey, t as fileExists } from "./_discover-BzlCDVZ6.mjs";
3
+ import { readFileSync } from "node:fs";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { defineCommand, runMain } from "citty";
7
+ //#region cli.ts
8
+ const cliDir = path.dirname(fileURLToPath(import.meta.url));
9
+ function findPkgJson(dir) {
10
+ try {
11
+ return readFileSync(path.join(dir, "package.json"), "utf-8");
12
+ } catch {
13
+ return readFileSync(path.join(dir, "..", "package.json"), "utf-8");
14
+ }
15
+ }
16
+ const VERSION = JSON.parse(findPkgJson(cliDir)).version;
17
+ async function ensureAgent(cwd, yes) {
18
+ if (!await fileExists(path.join(cwd, "agent.ts"))) {
19
+ const { runInitCommand } = await import("./init-BWG5OrQa.mjs");
20
+ await runInitCommand({ yes }, { quiet: true });
21
+ }
22
+ }
23
+ const init = defineCommand({
24
+ meta: {
25
+ name: "init",
26
+ description: "Scaffold a new agent project"
27
+ },
28
+ args: {
29
+ dir: {
30
+ type: "positional",
31
+ description: "Project directory",
32
+ required: false
33
+ },
34
+ template: {
35
+ type: "string",
36
+ alias: "t",
37
+ description: "Template to use"
38
+ },
39
+ force: {
40
+ type: "boolean",
41
+ alias: "f",
42
+ description: "Overwrite existing agent.ts"
43
+ },
44
+ yes: {
45
+ type: "boolean",
46
+ alias: "y",
47
+ description: "Accept defaults (no prompts)"
48
+ },
49
+ skipApi: {
50
+ type: "boolean",
51
+ description: "Skip API key check"
52
+ },
53
+ skipDeploy: {
54
+ type: "boolean",
55
+ description: "Skip post-init deploy"
56
+ }
57
+ },
58
+ async run({ args }) {
59
+ const { runInitCommand } = await import("./init-BWG5OrQa.mjs");
60
+ await runInitCommand({
61
+ dir: args.dir,
62
+ template: args.template,
63
+ force: args.force,
64
+ yes: args.yes,
65
+ skipApi: args.skipApi,
66
+ skipDeploy: args.skipDeploy
67
+ });
68
+ }
69
+ });
70
+ const dev = defineCommand({
71
+ meta: {
72
+ name: "dev",
73
+ description: "Start a local development server"
74
+ },
75
+ args: {
76
+ port: {
77
+ type: "string",
78
+ alias: "p",
79
+ description: "Port to listen on",
80
+ default: "3000"
81
+ },
82
+ check: {
83
+ type: "boolean",
84
+ description: "Start server, verify health, then exit"
85
+ },
86
+ yes: {
87
+ type: "boolean",
88
+ alias: "y",
89
+ description: "Accept defaults (no prompts)"
90
+ }
91
+ },
92
+ async run({ args }) {
93
+ const cwd = resolveCwd();
94
+ await ensureAgent(cwd, args.yes);
95
+ await getApiKey();
96
+ const { runDevCommand } = await import("./dev-DBFvKyzk.mjs");
97
+ await runDevCommand({
98
+ cwd,
99
+ port: args.port,
100
+ ...args.check ? { check: args.check } : {}
101
+ });
102
+ }
103
+ });
104
+ const build = defineCommand({
105
+ meta: {
106
+ name: "build",
107
+ description: "Bundle and validate (no server or deploy)"
108
+ },
109
+ args: { yes: {
110
+ type: "boolean",
111
+ alias: "y",
112
+ description: "Accept defaults (no prompts)"
113
+ } },
114
+ async run({ args }) {
115
+ const cwd = resolveCwd();
116
+ await ensureAgent(cwd, args.yes);
117
+ const { runBuildCommand } = await import("./_build-p1HHkdon.mjs");
118
+ await runBuildCommand(cwd);
119
+ }
120
+ });
121
+ const deploy = defineCommand({
122
+ meta: {
123
+ name: "deploy",
124
+ description: "Bundle and deploy to production"
125
+ },
126
+ args: {
127
+ server: {
128
+ type: "string",
129
+ alias: "s",
130
+ description: "Server URL"
131
+ },
132
+ dryRun: {
133
+ type: "boolean",
134
+ description: "Validate and bundle without deploying"
135
+ },
136
+ yes: {
137
+ type: "boolean",
138
+ alias: "y",
139
+ description: "Accept defaults (no prompts)"
140
+ }
141
+ },
142
+ async run({ args }) {
143
+ const cwd = resolveCwd();
144
+ await ensureAgent(cwd, args.yes);
145
+ const { runDeployCommand } = await import("./deploy-KyNJaoP5.mjs");
146
+ await runDeployCommand({
147
+ cwd,
148
+ ...args.server ? { server: args.server } : {},
149
+ ...args.dryRun ? { dryRun: args.dryRun } : {}
150
+ });
151
+ }
152
+ });
153
+ const start = defineCommand({
154
+ meta: {
155
+ name: "start",
156
+ description: "Start production server from build"
157
+ },
158
+ args: {
159
+ port: {
160
+ type: "string",
161
+ alias: "p",
162
+ description: "Port to listen on",
163
+ default: "3000"
164
+ },
165
+ yes: {
166
+ type: "boolean",
167
+ alias: "y",
168
+ description: "Accept defaults (no prompts)"
169
+ }
170
+ },
171
+ async run({ args }) {
172
+ const cwd = resolveCwd();
173
+ await getApiKey();
174
+ const { runStartCommand } = await import("./start-C1qkhU4O.mjs");
175
+ await runStartCommand({
176
+ cwd,
177
+ port: args.port
178
+ });
179
+ }
180
+ });
181
+ const secret = defineCommand({
182
+ meta: {
183
+ name: "secret",
184
+ description: "Manage secrets"
185
+ },
186
+ subCommands: {
187
+ put: defineCommand({
188
+ meta: {
189
+ name: "put",
190
+ description: "Create or update a secret"
191
+ },
192
+ args: { name: {
193
+ type: "positional",
194
+ description: "Secret name",
195
+ required: true
196
+ } },
197
+ async run({ args }) {
198
+ const cwd = resolveCwd();
199
+ await getApiKey();
200
+ const { runSecretPut } = await import("./secret-CzeHIGzE.mjs");
201
+ await runSecretPut(cwd, args.name);
202
+ }
203
+ }),
204
+ delete: defineCommand({
205
+ meta: {
206
+ name: "delete",
207
+ description: "Delete a secret"
208
+ },
209
+ args: { name: {
210
+ type: "positional",
211
+ description: "Secret name",
212
+ required: true
213
+ } },
214
+ async run({ args }) {
215
+ const cwd = resolveCwd();
216
+ await getApiKey();
217
+ const { runSecretDelete } = await import("./secret-CzeHIGzE.mjs");
218
+ await runSecretDelete(cwd, args.name);
219
+ }
220
+ }),
221
+ list: defineCommand({
222
+ meta: {
223
+ name: "list",
224
+ description: "List secret names"
225
+ },
226
+ async run() {
227
+ const cwd = resolveCwd();
228
+ await getApiKey();
229
+ const { runSecretList } = await import("./secret-CzeHIGzE.mjs");
230
+ await runSecretList(cwd);
231
+ }
232
+ })
233
+ }
234
+ });
235
+ const rag = defineCommand({
236
+ meta: {
237
+ name: "rag",
238
+ description: "Ingest a site's llms-full.txt into the vector store"
239
+ },
240
+ args: {
241
+ url: {
242
+ type: "positional",
243
+ description: "URL to ingest",
244
+ required: true
245
+ },
246
+ server: {
247
+ type: "string",
248
+ alias: "s",
249
+ description: "Server URL"
250
+ },
251
+ chunkSize: {
252
+ type: "string",
253
+ description: "Max chunk size in tokens",
254
+ default: "512"
255
+ },
256
+ yes: {
257
+ type: "boolean",
258
+ alias: "y",
259
+ description: "Accept defaults (no prompts)"
260
+ }
261
+ },
262
+ async run({ args }) {
263
+ const cwd = resolveCwd();
264
+ await getApiKey();
265
+ const { runRagCommand } = await import("./rag-BnCMnccf.mjs");
266
+ await runRagCommand({
267
+ url: args.url,
268
+ cwd,
269
+ ...args.server ? { server: args.server } : {},
270
+ chunkSize: args.chunkSize
271
+ });
272
+ }
273
+ });
274
+ const link = defineCommand({
275
+ meta: {
276
+ name: "link",
277
+ description: "Link local workspace packages into the current project (dev only)"
278
+ },
279
+ async run() {
280
+ const { runLinkCommand } = await import("./_link-BGXGFYWa.mjs");
281
+ runLinkCommand(resolveCwd());
282
+ }
283
+ });
284
+ const unlink = defineCommand({
285
+ meta: {
286
+ name: "unlink",
287
+ description: "Restore published package versions (reverses aai link)"
288
+ },
289
+ async run() {
290
+ const { runUnlinkCommand } = await import("./_link-BGXGFYWa.mjs");
291
+ runUnlinkCommand(resolveCwd());
292
+ }
293
+ });
294
+ const mainCommand = defineCommand({
295
+ meta: {
296
+ name: "aai",
297
+ version: VERSION,
298
+ description: "Voice agent development kit"
299
+ },
300
+ subCommands: {
301
+ init,
302
+ dev,
303
+ build,
304
+ deploy,
305
+ start,
306
+ secret,
307
+ rag,
308
+ link,
309
+ unlink
310
+ }
311
+ });
312
+ if (process.env.VITEST !== "true") {
313
+ const sub = process.argv[2];
314
+ if (!sub || sub.startsWith("-")) process.argv.splice(2, 0, "init");
315
+ runMain(mainCommand);
316
+ }
317
+ //#endregion
318
+ export { mainCommand };
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env node
2
+ import { l as resolveServerUrl, n as generateSlug, r as getApiKey, s as readProjectConfig, u as writeProjectConfig } from "./_discover-BzlCDVZ6.mjs";
3
+ import { a as step, i as runCommand, o as stepInfo } from "./_ui-kJIua5L9.mjs";
4
+ import { buildAgentBundle } from "./_build-p1HHkdon.mjs";
5
+ //#region _deploy.ts
6
+ async function attemptDeploy(fetchFn, url, slug, apiKey, env, worker, clientFiles) {
7
+ try {
8
+ return await fetchFn(`${url}/${slug}/deploy`, {
9
+ method: "POST",
10
+ headers: {
11
+ "Content-Type": "application/json",
12
+ Authorization: `Bearer ${apiKey}`
13
+ },
14
+ body: JSON.stringify({
15
+ env,
16
+ worker,
17
+ clientFiles
18
+ })
19
+ });
20
+ } catch {
21
+ throw new Error(`deployment failed: could not reach ${url}`);
22
+ }
23
+ }
24
+ const MAX_RETRIES = 20;
25
+ async function runDeploy(opts) {
26
+ const { worker, clientFiles } = opts.bundle;
27
+ const fetchFn = opts.fetch ?? globalThis.fetch.bind(globalThis);
28
+ let slug = opts.slug;
29
+ for (let i = 0; i < MAX_RETRIES; i++) {
30
+ const resp = await attemptDeploy(fetchFn, opts.url, slug, opts.apiKey, opts.env, worker, clientFiles);
31
+ if (resp.ok) return { slug };
32
+ const text = await resp.text();
33
+ if (resp.status === 403 && text.includes("Slug")) {
34
+ slug = generateSlug();
35
+ continue;
36
+ }
37
+ throw new Error(`deploy failed (${resp.status}): ${text}`);
38
+ }
39
+ throw new Error(`deploy failed: could not find available slug after ${MAX_RETRIES} attempts`);
40
+ }
41
+ //#endregion
42
+ //#region deploy.ts
43
+ async function deployBundle(opts) {
44
+ const { bundle, serverUrl, apiKey, cwd, log } = opts;
45
+ let { slug } = opts;
46
+ log(step("Deploy", slug));
47
+ slug = (await runDeploy({
48
+ url: serverUrl,
49
+ bundle,
50
+ env: { ASSEMBLYAI_API_KEY: apiKey },
51
+ slug,
52
+ apiKey
53
+ })).slug;
54
+ await writeProjectConfig(cwd, {
55
+ slug,
56
+ serverUrl
57
+ });
58
+ const agentUrl = `${serverUrl}/${slug}`;
59
+ log(step("Ready", agentUrl));
60
+ return agentUrl;
61
+ }
62
+ async function runDeployCommand(opts) {
63
+ const { cwd } = opts;
64
+ const dryRun = opts.dryRun ?? false;
65
+ const apiKey = dryRun ? "" : await getApiKey();
66
+ const projectConfig = await readProjectConfig(cwd);
67
+ const serverUrl = resolveServerUrl(opts.server, projectConfig?.serverUrl);
68
+ const slug = projectConfig?.slug ?? generateSlug();
69
+ await runCommand(async ({ log }) => {
70
+ const bundle = await buildAgentBundle(cwd, log);
71
+ if (dryRun) {
72
+ log(stepInfo("Dry run", `would deploy as ${slug}`));
73
+ return;
74
+ }
75
+ await deployBundle({
76
+ bundle,
77
+ serverUrl,
78
+ apiKey,
79
+ slug,
80
+ cwd,
81
+ log
82
+ });
83
+ });
84
+ }
85
+ //#endregion
86
+ export { runDeployCommand };
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+ import "./_discover-BzlCDVZ6.mjs";
3
+ import { a as step, i as runCommand, n as info } from "./_ui-kJIua5L9.mjs";
4
+ import { buildAgentBundle } from "./_build-p1HHkdon.mjs";
5
+ import { n as loadAgentDef, r as resolveServerEnv, t as bootServer } from "./_server-common-qLA1QU2C.mjs";
6
+ //#region dev.ts
7
+ async function _startDevServer(cwd, port, log, opts) {
8
+ const bundle = await buildAgentBundle(cwd, log);
9
+ const agentDef = await loadAgentDef(cwd);
10
+ const env = await resolveServerEnv();
11
+ const server = await bootServer(agentDef, bundle.clientDir, env, port);
12
+ log(step("Ready", `http://localhost:${port}`));
13
+ const base = `http://localhost:${port}`;
14
+ try {
15
+ const healthRes = await fetch(`${base}/health`);
16
+ if (!healthRes.ok) throw new Error(`GET /health returned ${healthRes.status}`);
17
+ const health = await healthRes.json();
18
+ if (health.status !== "ok") throw new Error(`GET /health returned ${JSON.stringify(health)}`);
19
+ log(step("Health", health.name ?? "ok"));
20
+ const pageRes = await fetch(`${base}/`);
21
+ if (!pageRes.ok) throw new Error(`GET / returned ${pageRes.status}`);
22
+ const html = await pageRes.text();
23
+ if (!(html.includes("<") && html.includes("html"))) throw new Error("GET / did not return valid HTML");
24
+ log(step("Client", "ok"));
25
+ } catch (err) {
26
+ await server.close();
27
+ throw err;
28
+ }
29
+ if (opts?.check) await server.close();
30
+ else log(info("Ctrl-C to quit"));
31
+ }
32
+ async function runDevCommand(opts) {
33
+ const port = Number.parseInt(opts.port, 10);
34
+ await runCommand(async ({ log }) => {
35
+ await _startDevServer(opts.cwd, port, log, opts.check ? { check: true } : void 0);
36
+ });
37
+ }
38
+ //#endregion
39
+ export { runDevCommand };
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env node
2
+ import { a as isDevMode, c as resolveCwd, f as askText, r as getApiKey, t as fileExists } from "./_discover-BzlCDVZ6.mjs";
3
+ import { a as step, i as runCommand, r as interactive, s as warn } from "./_ui-kJIua5L9.mjs";
4
+ import { existsSync } from "node:fs";
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import fs$1 from "node:fs/promises";
8
+ import { execFile } from "node:child_process";
9
+ import { promisify } from "node:util";
10
+ //#region init.ts
11
+ const execFileAsync = promisify(execFile);
12
+ /** Install deps — uses `aai link` in dev mode, `npm install` otherwise. */
13
+ async function installDeps(cwd, log) {
14
+ if (await fileExists(path.join(cwd, "node_modules"))) return;
15
+ if (isDevMode()) {
16
+ log(step("Link", "local workspace packages (dev mode)"));
17
+ const { runLinkCommand } = await import("./_link-BGXGFYWa.mjs");
18
+ runLinkCommand(cwd);
19
+ return;
20
+ }
21
+ let pkgJson;
22
+ try {
23
+ pkgJson = JSON.parse(await fs$1.readFile(path.join(cwd, "package.json"), "utf-8"));
24
+ } catch {
25
+ pkgJson = {};
26
+ }
27
+ const deps = Object.keys(pkgJson.dependencies ?? {});
28
+ const devDeps = Object.keys(pkgJson.devDependencies ?? {});
29
+ if (deps.length > 0) log(step("Install", deps.join(", ")));
30
+ if (devDeps.length > 0) log(step("Install", `dev: ${devDeps.join(", ")}`));
31
+ try {
32
+ await execFileAsync("npm", ["install"], { cwd });
33
+ } catch {
34
+ log(warn("npm install failed"));
35
+ }
36
+ }
37
+ async function runInitCommand(opts, extra) {
38
+ if (!opts.skipApi) await getApiKey();
39
+ let dir = opts.dir;
40
+ if (!dir) dir = await askText("What is your project named?", "my-voice-agent");
41
+ const cwd = path.resolve(resolveCwd(), dir);
42
+ if (!opts.force && await fileExists(path.join(cwd, "agent.ts"))) throw new Error(`agent.ts already exists in this directory. Use ${interactive("--force")} to overwrite.`);
43
+ const cliDir = path.dirname(fileURLToPath(import.meta.url));
44
+ const templatesDir = existsSync(path.join(cliDir, "templates")) ? path.join(cliDir, "templates") : path.join(cliDir, "..", "templates");
45
+ const { runInit } = await import("./_init-l_uoyFCN.mjs");
46
+ const template = opts.template || "simple";
47
+ await runCommand(async ({ log }) => {
48
+ log(step("Create", dir));
49
+ await runInit({
50
+ targetDir: cwd,
51
+ template,
52
+ templatesDir
53
+ });
54
+ await installDeps(cwd, log);
55
+ });
56
+ process.chdir(cwd);
57
+ delete process.env.INIT_CWD;
58
+ if (!(opts.skipDeploy || extra?.quiet)) {
59
+ const { runDeployCommand } = await import("./deploy-KyNJaoP5.mjs");
60
+ await runDeployCommand({ cwd });
61
+ }
62
+ return cwd;
63
+ }
64
+ //#endregion
65
+ export { runInitCommand };
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env node
2
+ import { i as getServerInfo } from "./_discover-BzlCDVZ6.mjs";
3
+ import { a as step, i as runCommand, n as info, s as warn, t as detail } from "./_ui-kJIua5L9.mjs";
4
+ import { errorMessage } from "@alexkroman1/aai/utils";
5
+ import pLimit from "p-limit";
6
+ //#region rag.ts
7
+ const FETCH_TIMEOUT_MS = 6e4;
8
+ async function runRag(opts) {
9
+ const { url, apiKey, serverUrl, slug, chunkSize, log, setStatus } = opts;
10
+ log(step("Fetch", url));
11
+ const resp = await fetch(url, {
12
+ headers: { "User-Agent": "aai-cli/1.0" },
13
+ redirect: "follow",
14
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
15
+ });
16
+ if (!resp.ok) throw new Error(`Failed to fetch: ${resp.status} ${resp.statusText}`);
17
+ const content = await resp.text();
18
+ if (content.length === 0) {
19
+ log(warn("File is empty"));
20
+ return;
21
+ }
22
+ log(info(`${(content.length / 1024).toFixed(0)} KB`));
23
+ const origin = new URL(url).origin;
24
+ const pages = splitPages(content);
25
+ log(step("Parse", `${pages.length} pages`));
26
+ const { RecursiveChunker } = await import("@chonkiejs/core");
27
+ const allChunks = await chunkPages(pages, await RecursiveChunker.create({ chunkSize }), origin, slugify(origin));
28
+ log(step("Chunk", `${allChunks.length} chunks`));
29
+ const vectorUrl = `${serverUrl}/${slug}/vector`;
30
+ log(info(`target: ${vectorUrl}`));
31
+ const result = await upsertChunks(allChunks, vectorUrl, apiKey, setStatus);
32
+ log(step("Done", `${result.upserted} chunks upserted`));
33
+ if (result.errors > 0) {
34
+ log(warn(`${result.errors} failed`));
35
+ if (result.lastError) log(info(`last error: ${result.lastError}`));
36
+ }
37
+ log(detail(`Agent: ${slug}`));
38
+ }
39
+ async function chunkPages(pages, chunker, origin, siteSlug) {
40
+ const allChunks = [];
41
+ for (const page of pages) {
42
+ page.body = stripNoise(page.body);
43
+ if (!page.body) continue;
44
+ const raw = await chunker.chunk(page.body);
45
+ for (const [i, c] of raw.entries()) {
46
+ const data = page.title ? `${page.title}\n\n${c.text}` : c.text;
47
+ const id = `${siteSlug}:${slugify(page.title || "page")}:${i}`;
48
+ allChunks.push({
49
+ id,
50
+ data,
51
+ metadata: {
52
+ source: origin,
53
+ ...page.title ? { title: page.title } : {},
54
+ tokenCount: c.tokenCount
55
+ }
56
+ });
57
+ }
58
+ }
59
+ return allChunks;
60
+ }
61
+ async function upsertChunks(chunks, vectorUrl, apiKey, setStatus, fetchFn = globalThis.fetch) {
62
+ const total = chunks.length;
63
+ let completed = 0;
64
+ let upserted = 0;
65
+ let errors = 0;
66
+ let lastError = "";
67
+ const updateStatus = () => {
68
+ const pct = Math.round(completed / total * 100);
69
+ setStatus(` Upsert ${completed}/${total} (${pct}%)`);
70
+ };
71
+ updateStatus();
72
+ const limit = pLimit(5);
73
+ await Promise.all(chunks.map((chunk) => limit(async () => {
74
+ try {
75
+ const r = await fetchFn(vectorUrl, {
76
+ method: "POST",
77
+ headers: {
78
+ "Content-Type": "application/json",
79
+ Authorization: `Bearer ${apiKey}`
80
+ },
81
+ body: JSON.stringify({
82
+ op: "upsert",
83
+ id: chunk.id,
84
+ data: chunk.data,
85
+ metadata: chunk.metadata
86
+ })
87
+ });
88
+ if (!r.ok) {
89
+ lastError = await r.text();
90
+ errors++;
91
+ } else upserted++;
92
+ } catch (err) {
93
+ lastError = errorMessage(err);
94
+ errors++;
95
+ }
96
+ completed++;
97
+ updateStatus();
98
+ })));
99
+ setStatus(null);
100
+ return {
101
+ upserted,
102
+ errors,
103
+ lastError
104
+ };
105
+ }
106
+ async function runRagCommand(opts) {
107
+ const { url, cwd } = opts;
108
+ try {
109
+ new URL(url);
110
+ } catch {
111
+ throw new Error(`Invalid URL: ${url}`);
112
+ }
113
+ const { apiKey, serverUrl, slug } = await getServerInfo(cwd, opts.server);
114
+ const chunkSize = Number.parseInt(opts.chunkSize ?? "512", 10);
115
+ await runCommand(async ({ log, setStatus }) => {
116
+ await runRag({
117
+ url,
118
+ apiKey,
119
+ serverUrl,
120
+ slug,
121
+ chunkSize,
122
+ log,
123
+ setStatus
124
+ });
125
+ });
126
+ }
127
+ /** Split llms-full.txt on `***` page separators and extract titles. */
128
+ function splitPages(content) {
129
+ const raw = content.split(/^\*{3,}$/m);
130
+ const pages = [];
131
+ for (const section of raw) {
132
+ const trimmed = section.trim();
133
+ if (!trimmed) continue;
134
+ const page = parsePage(trimmed);
135
+ if (page.body.length > 0) pages.push(page);
136
+ }
137
+ return pages;
138
+ }
139
+ /** Extract title and body from a single page section. */
140
+ function parsePage(trimmed) {
141
+ let title = "";
142
+ let body = trimmed;
143
+ const dashIndex = trimmed.search(/^-{3,}$/m);
144
+ if (dashIndex !== -1) {
145
+ const frontmatter = trimmed.slice(0, dashIndex);
146
+ body = trimmed.slice(dashIndex).replace(/^-+$/m, "").trim();
147
+ const titleMatch = frontmatter.match(/^title:\s*(.+)$/m);
148
+ if (titleMatch) title = titleMatch[1]?.trim() ?? "";
149
+ }
150
+ if (!title) {
151
+ const titleLineMatch = body.match(/^#{1,2}\s+title:\s*(.+)$/m);
152
+ if (titleLineMatch) {
153
+ title = titleLineMatch[1]?.trim() ?? "";
154
+ body = body.replace(/^#{1,2}\s+title:\s*.+\n?/m, "").trim();
155
+ } else {
156
+ const headingMatch = body.match(/^(#{1,3})\s+(.+)$/m);
157
+ if (headingMatch) title = headingMatch[2]?.trim() ?? "";
158
+ }
159
+ }
160
+ return {
161
+ title,
162
+ body
163
+ };
164
+ }
165
+ /** Strip code blocks, HTML/JSX tags, and collapse whitespace from markdown. */
166
+ function stripNoise(text) {
167
+ return text.replace(/^(`{3,}|~{3,}).*[\s\S]*?^\1/gm, "").replace(/^(?:[ ]{4,}|\t).+$/gm, "").replace(/`[^`]+`/g, "").replace(/\{\/\*[\s\S]*?\*\/\}/g, "").replace(/<[^>]+>/g, "").replace(/^\s*\}[^}\n]*$/gm, "").replace(/^\s+$/gm, "").replace(/\n{3,}/g, "\n\n").trim();
168
+ }
169
+ function slugify(s) {
170
+ return s.replace(/^https?:\/\//, "").replace(/^#+\s*/, "").replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase().slice(0, 80);
171
+ }
172
+ //#endregion
173
+ export { runRagCommand };