@alexkroman1/aai-cli 0.9.3 → 0.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/dist/{_build-Cf_bB5Vl.mjs → _build-aW_zyPpr.mjs} +57 -11
- package/dist/{_discover-DsorPpR_.mjs → _discover-D7HCLa_N.mjs} +62 -27
- package/dist/_init-CweK2mJJ.mjs +108 -0
- package/dist/{_link-BGXGFYWa.mjs → _link-D6mmDQ0B.mjs} +5 -3
- package/dist/_server-common-B6EXxxB2.mjs +105 -0
- package/dist/_ui-C9IvR7Fh.mjs +50 -0
- package/dist/cli.mjs +186 -31
- package/dist/delete-DxECFYOp.mjs +40 -0
- package/dist/{deploy-2OxZpIfj.mjs → deploy-DB0PeNtc.mjs} +11 -7
- package/dist/dev-DCtHG1pz.mjs +60 -0
- package/dist/doctor-bWuArZi6.mjs +215 -0
- package/dist/generate-D7zJtqoC.mjs +393 -0
- package/dist/{init-nHgDq3Om.mjs → init-COkINbH_.mjs} +13 -17
- package/dist/{rag-D0xqGIrS.mjs → rag-BNfrEr4A.mjs} +12 -7
- package/dist/{secret-BQl3zBOS.mjs → secret-Zzv7fJ_3.mjs} +2 -2
- package/dist/{start-Df8c2ciS.mjs → start-DQypQkOq.mjs} +5 -5
- package/dist/test-9fFn9DCf.mjs +40 -0
- package/package.json +18 -11
- package/dist/_init-l_uoyFCN.mjs +0 -82
- package/dist/_server-common-CpK4rWGs.mjs +0 -36
- package/dist/_ui-kJIua5L9.mjs +0 -44
- package/dist/dev-BIgNj8Dt.mjs +0 -39
- package/templates/_shared/.env.example +0 -5
- package/templates/_shared/CLAUDE.md +0 -1051
- package/templates/_shared/biome.json +0 -32
- package/templates/_shared/global.d.ts +0 -1
- package/templates/_shared/index.html +0 -16
- package/templates/_shared/package.json +0 -23
- package/templates/_shared/tsconfig.json +0 -15
- package/templates/code-interpreter/agent.ts +0 -27
- package/templates/code-interpreter/client.tsx +0 -3
- package/templates/css.d.ts +0 -1
- package/templates/dispatch-center/agent.ts +0 -1227
- package/templates/dispatch-center/client.tsx +0 -505
- package/templates/embedded-assets/agent.ts +0 -48
- package/templates/embedded-assets/client.tsx +0 -3
- package/templates/embedded-assets/knowledge.json +0 -20
- package/templates/health-assistant/agent.ts +0 -160
- package/templates/health-assistant/client.tsx +0 -3
- package/templates/infocom-adventure/agent.ts +0 -164
- package/templates/infocom-adventure/client.tsx +0 -300
- package/templates/math-buddy/agent.ts +0 -21
- package/templates/math-buddy/client.tsx +0 -3
- package/templates/memory-agent/agent.ts +0 -20
- package/templates/memory-agent/client.tsx +0 -3
- package/templates/night-owl/agent.ts +0 -98
- package/templates/night-owl/client.tsx +0 -12
- package/templates/personal-finance/agent.ts +0 -26
- package/templates/personal-finance/client.tsx +0 -3
- package/templates/pizza-ordering/agent.ts +0 -218
- package/templates/pizza-ordering/client.tsx +0 -264
- package/templates/simple/agent.ts +0 -6
- package/templates/simple/client.tsx +0 -3
- package/templates/smart-research/agent.ts +0 -164
- package/templates/smart-research/client.tsx +0 -3
- package/templates/solo-rpg/agent.ts +0 -1244
- package/templates/solo-rpg/client.tsx +0 -698
- package/templates/support/README.md +0 -62
- package/templates/support/agent.ts +0 -19
- package/templates/support/client.tsx +0 -3
- package/templates/travel-concierge/agent.ts +0 -29
- package/templates/travel-concierge/client.tsx +0 -3
- package/templates/tsconfig.json +0 -1
- package/templates/web-researcher/agent.ts +0 -17
- package/templates/web-researcher/client.tsx +0 -3
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { n as fileExists } from "./_discover-D7HCLa_N.mjs";
|
|
3
|
+
import { a as runCommand, i as parsePort, o as step } from "./_ui-C9IvR7Fh.mjs";
|
|
4
|
+
import { i as loadAgentDef, r as envFileKeys } from "./_server-common-B6EXxxB2.mjs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import fs from "node:fs/promises";
|
|
7
|
+
import { colorize } from "consola/utils";
|
|
8
|
+
import net from "node:net";
|
|
9
|
+
//#region doctor.ts
|
|
10
|
+
const PASS = colorize("greenBright", "✓");
|
|
11
|
+
const WARN = colorize("yellowBright", "!");
|
|
12
|
+
const FAIL = colorize("redBright", "✗");
|
|
13
|
+
function statusIcon(status) {
|
|
14
|
+
if (status === "pass") return PASS;
|
|
15
|
+
if (status === "warn") return WARN;
|
|
16
|
+
return FAIL;
|
|
17
|
+
}
|
|
18
|
+
async function checkNodeVersion() {
|
|
19
|
+
const version = process.version;
|
|
20
|
+
const match = version.match(/^v(\d+)\.(\d+)/);
|
|
21
|
+
if (!match) return {
|
|
22
|
+
name: "Node.js",
|
|
23
|
+
status: "fail",
|
|
24
|
+
message: `Unknown version: ${version}`
|
|
25
|
+
};
|
|
26
|
+
const [major, minor] = [Number(match[1]), Number(match[2])];
|
|
27
|
+
if (major > 22 || major === 22 && minor >= 6) return {
|
|
28
|
+
name: "Node.js",
|
|
29
|
+
status: "pass",
|
|
30
|
+
message: `${version} (>=22.6 required)`
|
|
31
|
+
};
|
|
32
|
+
return {
|
|
33
|
+
name: "Node.js",
|
|
34
|
+
status: "fail",
|
|
35
|
+
message: `${version} — Node >=22.6 is required`,
|
|
36
|
+
fix: "Install Node.js 22.6+ from https://nodejs.org or via nvm: nvm install 22"
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
async function checkApiKey() {
|
|
40
|
+
const key = process.env.ASSEMBLYAI_API_KEY || await (async () => {
|
|
41
|
+
try {
|
|
42
|
+
const configPath = path.join(process.env.HOME ?? process.env.USERPROFILE ?? ".", ".config", "aai", "config.json");
|
|
43
|
+
return JSON.parse(await fs.readFile(configPath, "utf-8")).assemblyai_api_key;
|
|
44
|
+
} catch {}
|
|
45
|
+
})();
|
|
46
|
+
if (!key) return {
|
|
47
|
+
name: "API key",
|
|
48
|
+
status: "fail",
|
|
49
|
+
message: "ASSEMBLYAI_API_KEY not found",
|
|
50
|
+
fix: "Run `aai init` to set up your API key, or set the ASSEMBLYAI_API_KEY environment variable"
|
|
51
|
+
};
|
|
52
|
+
if (key.length < 10) return {
|
|
53
|
+
name: "API key",
|
|
54
|
+
status: "warn",
|
|
55
|
+
message: "ASSEMBLYAI_API_KEY looks too short — may be invalid",
|
|
56
|
+
fix: "Get a valid key from https://www.assemblyai.com/dashboard/signup"
|
|
57
|
+
};
|
|
58
|
+
return {
|
|
59
|
+
name: "API key",
|
|
60
|
+
status: "pass",
|
|
61
|
+
message: "ASSEMBLYAI_API_KEY is set"
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
async function checkDependencies(cwd) {
|
|
65
|
+
if (!await fileExists(path.join(cwd, "package.json"))) return {
|
|
66
|
+
name: "Dependencies",
|
|
67
|
+
status: "warn",
|
|
68
|
+
message: "No package.json found",
|
|
69
|
+
fix: "Run `aai init` to scaffold a project, or `npm init` to create package.json"
|
|
70
|
+
};
|
|
71
|
+
const nodeModules = path.join(cwd, "node_modules");
|
|
72
|
+
if (!await fileExists(nodeModules)) return {
|
|
73
|
+
name: "Dependencies",
|
|
74
|
+
status: "fail",
|
|
75
|
+
message: "node_modules/ not found — dependencies not installed",
|
|
76
|
+
fix: "Run `npm install` to install dependencies"
|
|
77
|
+
};
|
|
78
|
+
if (!await fileExists(path.join(nodeModules, "@alexkroman1", "aai"))) return {
|
|
79
|
+
name: "Dependencies",
|
|
80
|
+
status: "fail",
|
|
81
|
+
message: "@alexkroman1/aai package not found in node_modules",
|
|
82
|
+
fix: "Run `npm install @alexkroman1/aai` to add the SDK"
|
|
83
|
+
};
|
|
84
|
+
return {
|
|
85
|
+
name: "Dependencies",
|
|
86
|
+
status: "pass",
|
|
87
|
+
message: "node_modules/ present, SDK installed"
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
async function checkEnvFile(cwd) {
|
|
91
|
+
const envPath = path.join(cwd, ".env");
|
|
92
|
+
if (!await fileExists(envPath)) {
|
|
93
|
+
if (await fileExists(path.join(cwd, ".env.example"))) return {
|
|
94
|
+
name: ".env file",
|
|
95
|
+
status: "warn",
|
|
96
|
+
message: ".env not found, but .env.example exists",
|
|
97
|
+
fix: "Copy .env.example to .env and fill in the values: cp .env.example .env"
|
|
98
|
+
};
|
|
99
|
+
return {
|
|
100
|
+
name: ".env file",
|
|
101
|
+
status: "pass",
|
|
102
|
+
message: "No .env file (using environment variables or aai config)"
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
const content = await fs.readFile(envPath, "utf-8");
|
|
107
|
+
const keys = envFileKeys(content);
|
|
108
|
+
if (keys.length === 0) return {
|
|
109
|
+
name: ".env file",
|
|
110
|
+
status: "warn",
|
|
111
|
+
message: ".env file is empty (no keys declared)"
|
|
112
|
+
};
|
|
113
|
+
const emptyKeys = [];
|
|
114
|
+
for (const line of content.split("\n")) {
|
|
115
|
+
const trimmed = line.trim();
|
|
116
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
117
|
+
const eq = trimmed.indexOf("=");
|
|
118
|
+
if (eq === -1) continue;
|
|
119
|
+
const val = trimmed.slice(eq + 1).trim();
|
|
120
|
+
if (!val || val === "\"\"" || val === "''") emptyKeys.push(trimmed.slice(0, eq).trim());
|
|
121
|
+
}
|
|
122
|
+
if (emptyKeys.length > 0) return {
|
|
123
|
+
name: ".env file",
|
|
124
|
+
status: "warn",
|
|
125
|
+
message: `${keys.length} key(s) declared, ${emptyKeys.length} empty: ${emptyKeys.join(", ")}`,
|
|
126
|
+
fix: "Fill in the empty values in your .env file"
|
|
127
|
+
};
|
|
128
|
+
return {
|
|
129
|
+
name: ".env file",
|
|
130
|
+
status: "pass",
|
|
131
|
+
message: `${keys.length} key(s) declared`
|
|
132
|
+
};
|
|
133
|
+
} catch {
|
|
134
|
+
return {
|
|
135
|
+
name: ".env file",
|
|
136
|
+
status: "fail",
|
|
137
|
+
message: "Failed to read .env file"
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
async function checkPortAvailable(port) {
|
|
142
|
+
if (await new Promise((resolve) => {
|
|
143
|
+
const server = net.createServer();
|
|
144
|
+
server.once("error", () => resolve(false));
|
|
145
|
+
server.once("listening", () => {
|
|
146
|
+
server.close(() => resolve(true));
|
|
147
|
+
});
|
|
148
|
+
server.listen(port, "127.0.0.1");
|
|
149
|
+
})) return {
|
|
150
|
+
name: "Port",
|
|
151
|
+
status: "pass",
|
|
152
|
+
message: `Port ${port} is available`
|
|
153
|
+
};
|
|
154
|
+
return {
|
|
155
|
+
name: "Port",
|
|
156
|
+
status: "warn",
|
|
157
|
+
message: `Port ${port} is in use`,
|
|
158
|
+
fix: `Use a different port: aai dev --port <number>, or stop the process using port ${port}`
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
async function checkAgentSyntax(cwd) {
|
|
162
|
+
if (!await fileExists(path.join(cwd, "agent.ts"))) return {
|
|
163
|
+
name: "agent.ts",
|
|
164
|
+
status: "fail",
|
|
165
|
+
message: "agent.ts not found",
|
|
166
|
+
fix: "Run `aai init` to scaffold a new agent project"
|
|
167
|
+
};
|
|
168
|
+
try {
|
|
169
|
+
await loadAgentDef(cwd);
|
|
170
|
+
return {
|
|
171
|
+
name: "agent.ts",
|
|
172
|
+
status: "pass",
|
|
173
|
+
message: "Valid agent definition"
|
|
174
|
+
};
|
|
175
|
+
} catch (err) {
|
|
176
|
+
return {
|
|
177
|
+
name: "agent.ts",
|
|
178
|
+
status: "fail",
|
|
179
|
+
message: `Invalid: ${err instanceof Error ? err.message : String(err)}`,
|
|
180
|
+
fix: "Check agent.ts — ensure it exports a default defineAgent() call with name, instructions, greeting, maxSteps, and tools"
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
async function _runDoctor(cwd, port, log) {
|
|
185
|
+
log("");
|
|
186
|
+
log(step("Doctor", "Checking environment health..."));
|
|
187
|
+
log("");
|
|
188
|
+
const results = [];
|
|
189
|
+
results.push(await checkNodeVersion());
|
|
190
|
+
results.push(await checkApiKey());
|
|
191
|
+
results.push(await checkDependencies(cwd));
|
|
192
|
+
results.push(await checkEnvFile(cwd));
|
|
193
|
+
results.push(await checkPortAvailable(port));
|
|
194
|
+
results.push(await checkAgentSyntax(cwd));
|
|
195
|
+
for (const r of results) {
|
|
196
|
+
log(` ${statusIcon(r.status)} ${colorize("bold", r.name)}: ${r.message}`);
|
|
197
|
+
if (r.fix) log(colorize("dim", ` → ${r.fix}`));
|
|
198
|
+
}
|
|
199
|
+
log("");
|
|
200
|
+
const fails = results.filter((r) => r.status === "fail").length;
|
|
201
|
+
const warns = results.filter((r) => r.status === "warn").length;
|
|
202
|
+
if (fails > 0) log(` ${FAIL} ${fails} issue(s) found. Fix them to proceed.`);
|
|
203
|
+
else if (warns > 0) log(` ${WARN} All clear with ${warns} warning(s).`);
|
|
204
|
+
else log(` ${PASS} Everything looks good!`);
|
|
205
|
+
log("");
|
|
206
|
+
if (fails > 0) process.exitCode = 1;
|
|
207
|
+
}
|
|
208
|
+
async function runDoctorCommand(opts) {
|
|
209
|
+
const port = parsePort(opts.port);
|
|
210
|
+
await runCommand(async ({ log }) => {
|
|
211
|
+
await _runDoctor(opts.cwd, port, log);
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
//#endregion
|
|
215
|
+
export { runDoctorCommand };
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { i as getApiKey } from "./_discover-D7HCLa_N.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,10 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { a as
|
|
4
|
-
import { existsSync } from "node:fs";
|
|
2
|
+
import { l as resolveCwd, n as fileExists, o as isDevMode, p as askText, t as ensureApiKeyInEnv } from "./_discover-D7HCLa_N.mjs";
|
|
3
|
+
import { a as runCommand, c as warn, o as step, r as interactive } from "./_ui-C9IvR7Fh.mjs";
|
|
5
4
|
import path from "node:path";
|
|
6
|
-
import
|
|
7
|
-
import fs$1 from "node:fs/promises";
|
|
5
|
+
import fs from "node:fs/promises";
|
|
8
6
|
import { execFile } from "node:child_process";
|
|
9
7
|
import { promisify } from "node:util";
|
|
10
8
|
//#region init.ts
|
|
@@ -14,13 +12,13 @@ async function installDeps(cwd, log) {
|
|
|
14
12
|
if (await fileExists(path.join(cwd, "node_modules"))) return;
|
|
15
13
|
if (isDevMode()) {
|
|
16
14
|
log(step("Link", "local workspace packages (dev mode)"));
|
|
17
|
-
const { runLinkCommand } = await import("./_link-
|
|
15
|
+
const { runLinkCommand } = await import("./_link-D6mmDQ0B.mjs");
|
|
18
16
|
runLinkCommand(cwd);
|
|
19
17
|
return;
|
|
20
18
|
}
|
|
21
19
|
let pkgJson;
|
|
22
20
|
try {
|
|
23
|
-
pkgJson = JSON.parse(await fs
|
|
21
|
+
pkgJson = JSON.parse(await fs.readFile(path.join(cwd, "package.json"), "utf-8"));
|
|
24
22
|
} catch {
|
|
25
23
|
pkgJson = {};
|
|
26
24
|
}
|
|
@@ -30,33 +28,31 @@ async function installDeps(cwd, log) {
|
|
|
30
28
|
if (devDeps.length > 0) log(step("Install", `dev: ${devDeps.join(", ")}`));
|
|
31
29
|
try {
|
|
32
30
|
await execFileAsync("npm", ["install"], { cwd });
|
|
33
|
-
} catch {
|
|
34
|
-
log(warn(
|
|
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."));
|
|
35
34
|
}
|
|
36
35
|
}
|
|
37
36
|
async function runInitCommand(opts, extra) {
|
|
38
|
-
if (!opts.skipApi) await
|
|
37
|
+
if (!opts.skipApi) await ensureApiKeyInEnv();
|
|
39
38
|
let dir = opts.dir;
|
|
40
39
|
if (!dir) dir = await askText("What is your project named?", "my-voice-agent");
|
|
41
40
|
const cwd = path.resolve(resolveCwd(), dir);
|
|
42
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.`);
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
const { runInit } = await import("./_init-l_uoyFCN.mjs");
|
|
46
|
-
const template = opts.template || "simple";
|
|
42
|
+
const { runInit } = await import("./_init-CweK2mJJ.mjs");
|
|
43
|
+
const template = opts.template ?? "simple";
|
|
47
44
|
await runCommand(async ({ log }) => {
|
|
48
45
|
log(step("Create", dir));
|
|
49
46
|
await runInit({
|
|
50
47
|
targetDir: cwd,
|
|
51
|
-
template
|
|
52
|
-
templatesDir
|
|
48
|
+
template
|
|
53
49
|
});
|
|
54
50
|
await installDeps(cwd, log);
|
|
55
51
|
});
|
|
56
52
|
process.chdir(cwd);
|
|
57
53
|
delete process.env.INIT_CWD;
|
|
58
54
|
if (!(opts.skipDeploy || extra?.quiet)) {
|
|
59
|
-
const { runDeployCommand } = await import("./deploy-
|
|
55
|
+
const { runDeployCommand } = await import("./deploy-DB0PeNtc.mjs");
|
|
60
56
|
await runDeployCommand({ cwd });
|
|
61
57
|
}
|
|
62
58
|
return cwd;
|