@alexkroman1/aai-cli 0.10.3 → 0.11.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.
@@ -1,393 +0,0 @@
1
- #!/usr/bin/env node
2
- import { i as getApiKey } from "./_discover-CbHCotwB.mjs";
3
- import path from "node:path";
4
- import fs from "node:fs/promises";
5
- import { consola } from "consola";
6
- import { z } from "zod";
7
- import { colorize } from "consola/utils";
8
- import { execFile } from "node:child_process";
9
- import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
10
- import { generateText, stepCountIs } from "ai";
11
- import { promisify } from "node:util";
12
- //#region _generate-tools.ts
13
- const execFileAsync = promisify(execFile);
14
- const SKIP_DIRS = new Set([
15
- "node_modules",
16
- ".git",
17
- ".aai",
18
- "dist",
19
- "coverage"
20
- ]);
21
- const MAX_READ_LINES = 2e3;
22
- const MAX_LINE_LENGTH = 2e3;
23
- const MAX_OUTPUT_BYTES = 50 * 1024;
24
- function safePath(workDir, filePath) {
25
- const abs = path.resolve(workDir, filePath);
26
- if (!abs.startsWith(workDir + path.sep) && abs !== workDir) return null;
27
- return abs;
28
- }
29
- function formatGrepOutput(workDir, stdout) {
30
- if (!stdout.trim()) return "No matches found.";
31
- const byFile = /* @__PURE__ */ new Map();
32
- for (const line of stdout.trim().split("\n")) {
33
- const sep = line.indexOf(":");
34
- const sep2 = line.indexOf(":", sep + 1);
35
- if (sep === -1 || sep2 === -1) continue;
36
- const file = path.relative(workDir, line.slice(0, sep));
37
- const lineNum = line.slice(sep + 1, sep2);
38
- let text = line.slice(sep2 + 1);
39
- if (text.length > MAX_LINE_LENGTH) text = `${text.slice(0, MAX_LINE_LENGTH)}...`;
40
- const entries = byFile.get(file) ?? [];
41
- entries.push(` Line ${lineNum}: ${text}`);
42
- byFile.set(file, entries);
43
- }
44
- const output = [];
45
- for (const [file, lines] of byFile) {
46
- output.push(`${file}:`);
47
- output.push(...lines);
48
- }
49
- return output.join("\n");
50
- }
51
- function formatExecError(err) {
52
- if (err && typeof err === "object" && "stdout" in err) {
53
- const e = err;
54
- return `Exit code ${e.code}\n${`${e.stdout}\n${e.stderr}`.trim()}`;
55
- }
56
- return `Error: ${err instanceof Error ? err.message : String(err)}`;
57
- }
58
- function truncateOutput(output) {
59
- if (!output) return "(no output)";
60
- if (output.length > MAX_OUTPUT_BYTES) return `${output.slice(0, MAX_OUTPUT_BYTES)}\n...(truncated, ${output.length} bytes total)`;
61
- return output;
62
- }
63
- function readFileWithLineNumbers(content, offset, limit) {
64
- const allLines = content.split("\n");
65
- const startLine = Math.max(1, offset ?? 1);
66
- const maxLines = limit ?? MAX_READ_LINES;
67
- const endLine = Math.min(allLines.length, startLine + maxLines - 1);
68
- const lines = allLines.slice(startLine - 1, endLine);
69
- let output = "";
70
- let bytes = 0;
71
- let truncatedByBytes = false;
72
- for (let i = 0; i < lines.length; i++) {
73
- const raw = lines[i] ?? "";
74
- const text = raw.length > MAX_LINE_LENGTH ? `${raw.slice(0, MAX_LINE_LENGTH)}... (truncated)` : raw;
75
- const numbered = `${startLine + i}: ${text}\n`;
76
- if (bytes + numbered.length > MAX_OUTPUT_BYTES) {
77
- truncatedByBytes = true;
78
- break;
79
- }
80
- output += numbered;
81
- bytes += numbered.length;
82
- }
83
- const total = allLines.length;
84
- if (truncatedByBytes || endLine < total) {
85
- const shown = endLine - startLine + 1;
86
- output += `\n(Showing lines ${startLine}-${startLine + shown - 1} of ${total}. Use offset=${endLine + 1} to continue.)`;
87
- }
88
- return output;
89
- }
90
- function isRgNoMatch(err) {
91
- return Boolean(err && typeof err === "object" && "code" in err && err.code === 1);
92
- }
93
- function makeFileTools(workDir) {
94
- return {
95
- read: {
96
- description: "Read a file or directory. Returns lines prefixed with line numbers (e.g. `1: content`). Use offset/limit to paginate large files. Defaults to first 2000 lines. For directories, returns a listing of entries. Call this tool in parallel when reading multiple files.",
97
- inputSchema: z.object({
98
- filePath: z.string().describe("Path to the file or directory to read"),
99
- offset: z.number().optional().describe("Line number to start from (1-indexed)"),
100
- limit: z.number().optional().describe("Max number of lines to read (default 2000)")
101
- }),
102
- execute: async (args) => {
103
- const { filePath, offset, limit } = args;
104
- const abs = safePath(workDir, filePath);
105
- if (!abs) return "Error: path outside working directory";
106
- let stat;
107
- try {
108
- stat = await fs.stat(abs);
109
- } catch {
110
- return `Error: file not found: ${filePath}`;
111
- }
112
- if (stat.isDirectory()) return (await fs.readdir(abs, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name)).map((e) => e.isDirectory() ? `${e.name}/` : e.name).join("\n");
113
- return readFileWithLineNumbers(await fs.readFile(abs, "utf-8"), offset, limit);
114
- }
115
- },
116
- edit: {
117
- description: "Performs exact string replacement in a file. You must read the file first before editing. The edit will fail if oldString is not found or matches multiple locations (unless replaceAll is true). Provide enough surrounding context in oldString to make the match unique. Preserve exact indentation from the file.",
118
- inputSchema: z.object({
119
- filePath: z.string().describe("Path to the file to modify"),
120
- oldString: z.string().describe("The exact text to replace"),
121
- newString: z.string().describe("The replacement text (must be different from oldString)"),
122
- replaceAll: z.boolean().optional().describe("Replace all occurrences of oldString (default false)")
123
- }),
124
- execute: async (args) => {
125
- const { filePath, oldString, newString, replaceAll } = args;
126
- if (oldString === newString) return "Error: oldString and newString are identical";
127
- const abs = safePath(workDir, filePath);
128
- if (!abs) return "Error: path outside working directory";
129
- let content;
130
- try {
131
- content = await fs.readFile(abs, "utf-8");
132
- } catch {
133
- return `Error: file not found: ${filePath}`;
134
- }
135
- if (!content.includes(oldString)) return "Error: oldString not found in file. Make sure you are matching the exact text including whitespace and indentation.";
136
- if (replaceAll) {
137
- await fs.writeFile(abs, content.replaceAll(oldString, newString));
138
- return `Replaced ${content.split(oldString).length - 1} occurrence(s).`;
139
- }
140
- const first = content.indexOf(oldString);
141
- if (content.indexOf(oldString, first + 1) !== -1) return "Error: found multiple matches for oldString. Provide more surrounding context to make it unique, or set replaceAll to true.";
142
- await fs.writeFile(abs, content.replace(oldString, newString));
143
- return "OK";
144
- }
145
- },
146
- write: {
147
- description: "Write content to a file, creating it if it doesn't exist or overwriting if it does. Always prefer editing existing files with the edit tool. Only use write for new files or complete rewrites.",
148
- inputSchema: z.object({
149
- filePath: z.string().describe("Path to the file to write"),
150
- content: z.string().describe("The full file content to write")
151
- }),
152
- execute: async (args) => {
153
- const { filePath, content } = args;
154
- const abs = safePath(workDir, filePath);
155
- if (!abs) return "Error: path outside working directory";
156
- await fs.mkdir(path.dirname(abs), { recursive: true });
157
- await fs.writeFile(abs, content);
158
- return `Wrote ${content.length} bytes to ${filePath}`;
159
- }
160
- }
161
- };
162
- }
163
- function makeSearchTools(workDir) {
164
- return {
165
- glob: {
166
- description: "Fast file pattern matching. Supports glob patterns like '**/*.ts' or 'src/**/*.tsx'. Returns matching file paths sorted by modification time (newest first). Use this to find files by name or extension.",
167
- inputSchema: z.object({
168
- pattern: z.string().describe("Glob pattern to match files against"),
169
- path: z.string().optional().describe("Directory to search in (defaults to project root)")
170
- }),
171
- execute: async (args) => {
172
- const { pattern, path: searchPath } = args;
173
- const dir = searchPath ? safePath(workDir, searchPath) ?? workDir : workDir;
174
- try {
175
- const { stdout } = await execFileAsync("rg", [
176
- "--files",
177
- "--glob",
178
- pattern,
179
- "--sort=modified",
180
- dir
181
- ], {
182
- maxBuffer: 1024 * 1024,
183
- timeout: 1e4
184
- });
185
- const files = stdout.trim().split("\n").filter(Boolean).slice(0, 100).map((f) => path.relative(workDir, f));
186
- if (files.length === 0) return "No files found matching pattern.";
187
- const truncated = files.length >= 100 ? "\n(Showing first 100 results.)" : "";
188
- return files.join("\n") + truncated;
189
- } catch (err) {
190
- if (isRgNoMatch(err)) return "No files found matching pattern.";
191
- return `Error running glob: ${err instanceof Error ? err.message : String(err)}`;
192
- }
193
- }
194
- },
195
- grep: {
196
- description: "Fast content search using regex. Searches file contents and returns file paths with line numbers and matching lines. Supports full regex syntax. Use the include parameter to filter by file extension (e.g. '*.ts').",
197
- inputSchema: z.object({
198
- pattern: z.string().describe("Regex pattern to search for"),
199
- path: z.string().optional().describe("Directory to search in (defaults to project root)"),
200
- include: z.string().optional().describe("File pattern to include (e.g. \"*.ts\", \"*.{ts,tsx}\")")
201
- }),
202
- execute: async (args) => {
203
- const { pattern, path: searchPath, include } = args;
204
- const dir = searchPath ? safePath(workDir, searchPath) ?? workDir : workDir;
205
- const rgArgs = [
206
- "-nH",
207
- "--hidden",
208
- "--no-messages",
209
- "--max-count=100",
210
- ...include ? ["--glob", include] : [],
211
- "--regexp",
212
- pattern,
213
- dir
214
- ];
215
- try {
216
- const { stdout } = await execFileAsync("rg", rgArgs, {
217
- maxBuffer: 1024 * 1024,
218
- timeout: 1e4
219
- });
220
- return formatGrepOutput(workDir, stdout);
221
- } catch (err) {
222
- if (isRgNoMatch(err)) return "No matches found.";
223
- return `Error running grep: ${err instanceof Error ? err.message : String(err)}`;
224
- }
225
- }
226
- },
227
- bash: {
228
- description: "Execute a shell command. Use for git, npm, and other terminal operations. Do NOT use for file reading/writing/searching — use the dedicated tools instead. Commands run in the project directory by default.",
229
- inputSchema: z.object({
230
- command: z.string().describe("The shell command to execute"),
231
- description: z.string().describe("Brief description of what this command does (5-10 words)"),
232
- timeout: z.number().optional().describe("Timeout in milliseconds (default 120000)")
233
- }),
234
- execute: async (args) => {
235
- const { command, timeout } = args;
236
- try {
237
- const { stdout, stderr } = await execFileAsync(process.env.SHELL ?? "bash", ["-c", command], {
238
- cwd: workDir,
239
- maxBuffer: 1024 * 1024,
240
- timeout: timeout ?? 12e4,
241
- env: process.env
242
- });
243
- return truncateOutput(`${stdout}${stderr ? `\n${stderr}` : ""}`.trim());
244
- } catch (err) {
245
- return formatExecError(err);
246
- }
247
- }
248
- },
249
- ls: {
250
- description: "List files and directories in a path. Returns entries with '/' suffix for directories. Prefer glob or grep if you know what you're looking for.",
251
- inputSchema: z.object({ path: z.string().optional().describe("Directory path (defaults to project root)") }),
252
- execute: async (args) => {
253
- const { path: dirPath } = args;
254
- const abs = dirPath ? safePath(workDir, dirPath) ?? workDir : workDir;
255
- try {
256
- return (await fs.readdir(abs, { withFileTypes: true })).filter((e) => !SKIP_DIRS.has(e.name)).sort((a, b) => a.name.localeCompare(b.name)).map((e) => e.isDirectory() ? `${e.name}/` : e.name).join("\n");
257
- } catch {
258
- return `Error: directory not found: ${dirPath ?? "."}`;
259
- }
260
- }
261
- }
262
- };
263
- }
264
- function makeTools(workDir) {
265
- return {
266
- ...makeFileTools(workDir),
267
- ...makeSearchTools(workDir)
268
- };
269
- }
270
- //#endregion
271
- //#region generate.ts
272
- const consola$1 = consola.create({
273
- defaults: { message: "" },
274
- formatOptions: { date: false }
275
- });
276
- const TOOL_ICONS = {
277
- read: "→",
278
- edit: "✎",
279
- write: "✏",
280
- glob: "✱",
281
- grep: "◇",
282
- bash: "$",
283
- ls: "▪"
284
- };
285
- const SYSTEM_PROMPT = `You are a pragmatic, expert coding agent that builds voice agents using the AAI framework. You persist until the task is fully handled — do not stop at analysis or partial fixes.
286
-
287
- # Workflow
288
-
289
- 1. Use glob or ls to see the project structure.
290
- 2. Use read on agent.ts to see the current code.
291
- 3. Plan your approach: what name, instructions, greeting, tools, state, and builtinTools does this agent need?
292
- 4. Use write to update agent.ts with the complete implementation. Write the entire file — no placeholders or TODOs.
293
- 5. Use read to verify your changes. If something is wrong, use edit to fix it.
294
-
295
- # Rules
296
-
297
- - The API reference is included below — do NOT read CLAUDE.md, it is already in your context.
298
- - agent.ts must export a default defineAgent() call.
299
- - Only modify agent.ts (and optionally client.tsx for custom UI).
300
- - Do NOT create extra files or install packages.
301
- - Write production-quality code. Tools should have clear descriptions and .describe() on each Zod parameter.
302
- - Handle edge cases in tool execute functions.
303
- - Parallelize tool calls when possible — e.g. read multiple files at once.`;
304
- function toolLabel(name, input) {
305
- const icon = TOOL_ICONS[name] ?? "•";
306
- const file = input.filePath ?? input.path ?? input.pattern ?? "";
307
- if (name === "bash") return `${icon} ${colorize("cyanBright", name)} ${colorize("dim", String(input.command ?? "").slice(0, 60))}`;
308
- if (file) return `${icon} ${colorize("cyanBright", name)} ${file}`;
309
- return `${icon} ${colorize("cyanBright", name)}`;
310
- }
311
- function formatDuration(ms) {
312
- if (ms < 1) return "<1ms";
313
- if (ms < 1e3) return `${Math.round(ms)}ms`;
314
- return `${(ms / 1e3).toFixed(1)}s`;
315
- }
316
- function printCode(filePath, content) {
317
- const lang = path.extname(filePath).slice(1) || "text";
318
- const lines = content.split("\n");
319
- const maxLines = 40;
320
- const truncated = lines.length > maxLines;
321
- const shown = truncated ? lines.slice(0, maxLines) : lines;
322
- console.log(colorize("dim", ` ┌── ${filePath} (${lang})`));
323
- for (const line of shown) console.log(colorize("dim", ` │ ${line}`));
324
- if (truncated) console.log(colorize("dim", ` │ ... (${lines.length - maxLines} more lines)`));
325
- console.log(colorize("dim", " └──"));
326
- }
327
- function printBox(text) {
328
- const lines = text.split("\n");
329
- return [
330
- colorize("dim", " ┌──"),
331
- ...lines.map((line) => colorize("dim", ` │ ${line}`)),
332
- colorize("dim", " └──")
333
- ].join("\n");
334
- }
335
- function maybeShowCode(toolName, input) {
336
- if ((toolName === "write" || toolName === "edit") && input) {
337
- const filePath = String(input.filePath ?? "");
338
- const content = String(input.content ?? input.newString ?? "");
339
- if (filePath && content) printCode(filePath, content);
340
- }
341
- }
342
- async function runGenerateCommand(opts) {
343
- const { cwd, prompt } = opts;
344
- const baseURL = process.env.LLM_BASE_URL ?? "https://llm-gateway.assemblyai.com/v1";
345
- const modelId = process.env.LLM_MODEL ?? "gpt-5.2";
346
- const apiKey = await getApiKey();
347
- let systemPrompt = SYSTEM_PROMPT;
348
- try {
349
- const claudeMd = await fs.readFile(path.join(cwd, "CLAUDE.md"), "utf-8");
350
- systemPrompt += `\n\n# API Reference (CLAUDE.md)\n\n${claudeMd}`;
351
- } catch {}
352
- consola$1.start("Planning...");
353
- try {
354
- const result = await generateText({
355
- model: createOpenAICompatible({
356
- name: "assemblyai",
357
- baseURL,
358
- apiKey
359
- })(modelId),
360
- system: systemPrompt,
361
- prompt,
362
- tools: makeTools(cwd),
363
- maxOutputTokens: 65536,
364
- toolChoice: "auto",
365
- stopWhen: stepCountIs(20),
366
- experimental_onStepStart: ({ stepNumber }) => {
367
- consola$1.start(`Step ${stepNumber + 1} · Thinking...`);
368
- },
369
- experimental_onToolCallStart: ({ toolCall }) => {
370
- const input = toolCall.input;
371
- consola$1.info(toolLabel(toolCall.toolName, input ?? {}));
372
- },
373
- experimental_onToolCallFinish: ({ toolCall, durationMs, ...rest }) => {
374
- const input = toolCall.input;
375
- const label = toolLabel(toolCall.toolName, input ?? {});
376
- const time = formatDuration(durationMs);
377
- const ok = "success" in rest && rest.success;
378
- const mark = ok ? colorize("greenBright", "✔") : colorize("redBright", "✖");
379
- console.log(`${mark} ${label} ${colorize("dim", time)}`);
380
- if (ok) maybeShowCode(toolCall.toolName, input);
381
- },
382
- onStepFinish: ({ finishReason, text }) => {
383
- if (finishReason === "stop" && text) console.log(printBox(text));
384
- }
385
- });
386
- consola$1.success(`Done ${colorize("dim", `(${result.steps.length} steps, ${result.usage.totalTokens} tokens)`)}`);
387
- } catch (err) {
388
- consola$1.error(err instanceof Error ? err.message : String(err));
389
- process.exitCode = 1;
390
- }
391
- }
392
- //#endregion
393
- export { runGenerateCommand };
@@ -1,61 +0,0 @@
1
- #!/usr/bin/env node
2
- import { l as resolveCwd, n as fileExists, o as isDevMode, p as askText, t as ensureApiKeyInEnv } from "./_discover-CbHCotwB.mjs";
3
- import { a as runCommand, c as warn, o as step, r as interactive } from "./_ui-C9IvR7Fh.mjs";
4
- import path from "node:path";
5
- import fs from "node:fs/promises";
6
- import { execFile } from "node:child_process";
7
- import { promisify } from "node:util";
8
- //#region init.ts
9
- const execFileAsync = promisify(execFile);
10
- /** Install deps — uses `aai link` in dev mode, `npm install` otherwise. */
11
- async function installDeps(cwd, log) {
12
- if (await fileExists(path.join(cwd, "node_modules"))) return;
13
- if (isDevMode()) {
14
- log(step("Link", "local workspace packages (dev mode)"));
15
- const { runLinkCommand } = await import("./_link-D6mmDQ0B.mjs");
16
- runLinkCommand(cwd);
17
- return;
18
- }
19
- let pkgJson;
20
- try {
21
- pkgJson = JSON.parse(await fs.readFile(path.join(cwd, "package.json"), "utf-8"));
22
- } catch {
23
- pkgJson = {};
24
- }
25
- const deps = Object.keys(pkgJson.dependencies ?? {});
26
- const devDeps = Object.keys(pkgJson.devDependencies ?? {});
27
- if (deps.length > 0) log(step("Install", deps.join(", ")));
28
- if (devDeps.length > 0) log(step("Install", `dev: ${devDeps.join(", ")}`));
29
- try {
30
- await execFileAsync("npm", ["install"], { cwd });
31
- } catch (err) {
32
- log(warn(`npm install failed: ${err instanceof Error ? err.message : String(err)}`));
33
- log(warn("Run `npm install` manually in the project directory to install dependencies."));
34
- }
35
- }
36
- async function runInitCommand(opts, extra) {
37
- if (!opts.skipApi) await ensureApiKeyInEnv();
38
- let dir = opts.dir;
39
- if (!dir) dir = await askText("What is your project named?", "my-voice-agent");
40
- const cwd = path.resolve(resolveCwd(), dir);
41
- 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.`);
42
- const { runInit } = await import("./_init-CQ6Cw4k-.mjs");
43
- const template = opts.template ?? "simple";
44
- await runCommand(async ({ log }) => {
45
- log(step("Create", dir));
46
- await runInit({
47
- targetDir: cwd,
48
- template
49
- });
50
- await installDeps(cwd, log);
51
- });
52
- process.chdir(cwd);
53
- delete process.env.INIT_CWD;
54
- if (!(opts.skipDeploy || extra?.quiet)) {
55
- const { runDeployCommand } = await import("./deploy-BWF0H8UK.mjs");
56
- await runDeployCommand({ cwd });
57
- }
58
- return cwd;
59
- }
60
- //#endregion
61
- export { runInitCommand };
@@ -1,178 +0,0 @@
1
- #!/usr/bin/env node
2
- import { a as getServerInfo } from "./_discover-CbHCotwB.mjs";
3
- import { a as runCommand, c as warn, n as info, o as step, t as detail } from "./_ui-C9IvR7Fh.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
- let lastStatusUpdate = 0;
68
- const updateStatus = (force = false) => {
69
- const now = Date.now();
70
- if (!force && now - lastStatusUpdate < 100) return;
71
- lastStatusUpdate = now;
72
- const pct = Math.round(completed / total * 100);
73
- setStatus(` Upsert ${completed}/${total} (${pct}%)`);
74
- };
75
- updateStatus(true);
76
- const limit = pLimit(5);
77
- await Promise.all(chunks.map((chunk) => limit(async () => {
78
- try {
79
- const r = await fetchFn(vectorUrl, {
80
- method: "POST",
81
- headers: {
82
- "Content-Type": "application/json",
83
- Authorization: `Bearer ${apiKey}`
84
- },
85
- body: JSON.stringify({
86
- op: "upsert",
87
- id: chunk.id,
88
- data: chunk.data,
89
- metadata: chunk.metadata
90
- })
91
- });
92
- if (!r.ok) {
93
- lastError = await r.text();
94
- errors++;
95
- } else upserted++;
96
- } catch (err) {
97
- lastError = errorMessage(err);
98
- errors++;
99
- }
100
- completed++;
101
- updateStatus();
102
- })));
103
- setStatus(null);
104
- return {
105
- upserted,
106
- errors,
107
- lastError
108
- };
109
- }
110
- async function runRagCommand(opts) {
111
- const { url, cwd } = opts;
112
- try {
113
- new URL(url);
114
- } catch (err) {
115
- throw new Error(`Invalid URL: ${url}\n Provide a fully qualified URL including the protocol (e.g., https://example.com/docs).`, { cause: err });
116
- }
117
- const chunkSize = Number.parseInt(opts.chunkSize ?? "512", 10);
118
- if (Number.isNaN(chunkSize) || chunkSize <= 0) throw new Error(`Invalid chunk size: "${opts.chunkSize}". Must be a positive integer.`);
119
- const { apiKey, serverUrl, slug } = await getServerInfo(cwd, opts.server);
120
- await runCommand(async ({ log, setStatus }) => {
121
- await runRag({
122
- url,
123
- apiKey,
124
- serverUrl,
125
- slug,
126
- chunkSize,
127
- log,
128
- setStatus
129
- });
130
- });
131
- }
132
- /** Split llms-full.txt on `***` page separators and extract titles. */
133
- function splitPages(content) {
134
- const raw = content.split(/^\*{3,}$/m);
135
- const pages = [];
136
- for (const section of raw) {
137
- const trimmed = section.trim();
138
- if (!trimmed) continue;
139
- const page = parsePage(trimmed);
140
- if (page.body.length > 0) pages.push(page);
141
- }
142
- return pages;
143
- }
144
- /** Extract title and body from a single page section. */
145
- function parsePage(trimmed) {
146
- let title = "";
147
- let body = trimmed;
148
- const dashIndex = trimmed.search(/^-{3,}$/m);
149
- if (dashIndex !== -1) {
150
- const frontmatter = trimmed.slice(0, dashIndex);
151
- body = trimmed.slice(dashIndex).replace(/^-+$/m, "").trim();
152
- const titleMatch = frontmatter.match(/^title:\s*(.+)$/m);
153
- if (titleMatch) title = titleMatch[1]?.trim() ?? "";
154
- }
155
- if (!title) {
156
- const titleLineMatch = body.match(/^#{1,2}\s+title:\s*(.+)$/m);
157
- if (titleLineMatch) {
158
- title = titleLineMatch[1]?.trim() ?? "";
159
- body = body.replace(/^#{1,2}\s+title:\s*.+\n?/m, "").trim();
160
- } else {
161
- const headingMatch = body.match(/^(#{1,3})\s+(.+)$/m);
162
- if (headingMatch) title = headingMatch[2]?.trim() ?? "";
163
- }
164
- }
165
- return {
166
- title,
167
- body
168
- };
169
- }
170
- /** Strip code blocks, HTML/JSX tags, and collapse whitespace from markdown. */
171
- function stripNoise(text) {
172
- 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();
173
- }
174
- function slugify(s) {
175
- return s.replace(/^https?:\/\//, "").replace(/^#+\s*/, "").replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase().slice(0, 80);
176
- }
177
- //#endregion
178
- export { runRagCommand };
@@ -1,50 +0,0 @@
1
- #!/usr/bin/env node
2
- import { a as getServerInfo, f as askPassword } from "./_discover-CbHCotwB.mjs";
3
- import { a as runCommand, o as step, s as stepInfo, t as detail } from "./_ui-C9IvR7Fh.mjs";
4
- //#region secret.ts
5
- async function apiFetch(cwd, pathSuffix, init) {
6
- const { serverUrl, slug, apiKey } = await getServerInfo(cwd);
7
- const resp = await fetch(`${serverUrl}/${slug}/secret${pathSuffix}`, {
8
- ...init,
9
- headers: {
10
- Authorization: `Bearer ${apiKey}`,
11
- ...init?.headers
12
- }
13
- });
14
- if (!resp.ok) {
15
- const text = await resp.text();
16
- throw new Error(`Secret operation failed: ${text}`);
17
- }
18
- return {
19
- resp,
20
- slug
21
- };
22
- }
23
- async function runSecretPut(cwd, name) {
24
- const value = await askPassword(`Enter value for ${name}`);
25
- if (!value) throw new Error("No value provided");
26
- await runCommand(async ({ log }) => {
27
- const { slug } = await apiFetch(cwd, "", {
28
- method: "PUT",
29
- headers: { "Content-Type": "application/json" },
30
- body: JSON.stringify({ [name]: value })
31
- });
32
- log(step("Set", `${name} for ${slug}`));
33
- });
34
- }
35
- async function runSecretDelete(cwd, name) {
36
- await runCommand(async ({ log }) => {
37
- const { slug } = await apiFetch(cwd, `/${name}`, { method: "DELETE" });
38
- log(step("Deleted", `${name} from ${slug}`));
39
- });
40
- }
41
- async function runSecretList(cwd) {
42
- await runCommand(async ({ log }) => {
43
- const { resp } = await apiFetch(cwd, "");
44
- const { vars } = await resp.json();
45
- if (vars.length === 0) log(stepInfo("Secrets", "none set"));
46
- else for (const name of vars) log(detail(name));
47
- });
48
- }
49
- //#endregion
50
- export { runSecretDelete, runSecretList, runSecretPut };