@marco-kueks/agent-factory-cli 0.1.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/index.js +784 -0
- package/package.json +40 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,784 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { program } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/dev.ts
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
import chalk from "chalk";
|
|
9
|
+
import ora from "ora";
|
|
10
|
+
import chokidar from "chokidar";
|
|
11
|
+
import { join as join3 } from "path";
|
|
12
|
+
|
|
13
|
+
// src/utils/config.ts
|
|
14
|
+
import { join } from "path";
|
|
15
|
+
var defaultConfig = {
|
|
16
|
+
port: 3000,
|
|
17
|
+
host: "localhost",
|
|
18
|
+
cors: {
|
|
19
|
+
origins: ["http://localhost:3000"],
|
|
20
|
+
credentials: true
|
|
21
|
+
},
|
|
22
|
+
logging: {
|
|
23
|
+
level: "info",
|
|
24
|
+
format: "pretty"
|
|
25
|
+
},
|
|
26
|
+
auth: {
|
|
27
|
+
type: "none"
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
async function loadConfig(cwd) {
|
|
31
|
+
const configPath = join(cwd, "af.config.ts");
|
|
32
|
+
try {
|
|
33
|
+
const module = await import(configPath);
|
|
34
|
+
const config = module.default || module;
|
|
35
|
+
return {
|
|
36
|
+
...defaultConfig,
|
|
37
|
+
...config,
|
|
38
|
+
cors: {
|
|
39
|
+
...defaultConfig.cors,
|
|
40
|
+
...config.cors
|
|
41
|
+
},
|
|
42
|
+
logging: {
|
|
43
|
+
...defaultConfig.logging,
|
|
44
|
+
...config.logging
|
|
45
|
+
},
|
|
46
|
+
auth: {
|
|
47
|
+
...defaultConfig.auth,
|
|
48
|
+
...config.auth
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
} catch {
|
|
52
|
+
return defaultConfig;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/utils/agent.ts
|
|
57
|
+
import { join as join2 } from "path";
|
|
58
|
+
async function loadAgent(cwd) {
|
|
59
|
+
const agentPath = join2(cwd, "src/agent.ts");
|
|
60
|
+
try {
|
|
61
|
+
const module = await import(agentPath);
|
|
62
|
+
const agent = module.default || module;
|
|
63
|
+
if (!agent.name) {
|
|
64
|
+
throw new Error("Agent must have a name");
|
|
65
|
+
}
|
|
66
|
+
if (!agent.version) {
|
|
67
|
+
throw new Error("Agent must have a version");
|
|
68
|
+
}
|
|
69
|
+
if (!agent.systemPrompt) {
|
|
70
|
+
throw new Error("Agent must have a systemPrompt");
|
|
71
|
+
}
|
|
72
|
+
return agent;
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (error instanceof Error && error.message.includes("Cannot find module")) {
|
|
75
|
+
throw new Error(`Agent not found at ${agentPath}`);
|
|
76
|
+
}
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// src/commands/dev.ts
|
|
82
|
+
var devCommand = new Command("dev").description("Start development server with hot reload").option("-p, --port <port>", "Port to run on", "3000").option("-c, --channel <channel>", "Channel to open (web, api)", "web").option("--no-open", "Do not open browser").action(async (options) => {
|
|
83
|
+
const spinner = ora();
|
|
84
|
+
const cwd = process.cwd();
|
|
85
|
+
console.log();
|
|
86
|
+
console.log(chalk.bold("Agent Factory Dev Server"));
|
|
87
|
+
console.log();
|
|
88
|
+
spinner.start("Loading configuration");
|
|
89
|
+
const config = await loadConfig(cwd);
|
|
90
|
+
const port = parseInt(options.port) || config.port || 3000;
|
|
91
|
+
spinner.succeed("Configuration loaded");
|
|
92
|
+
spinner.start("Loading agent");
|
|
93
|
+
let agent = await loadAgent(cwd);
|
|
94
|
+
spinner.succeed(`Agent "${agent.name}" loaded`);
|
|
95
|
+
const server = Bun.serve({
|
|
96
|
+
port,
|
|
97
|
+
async fetch(req) {
|
|
98
|
+
const url = new URL(req.url);
|
|
99
|
+
if (url.pathname === "/health") {
|
|
100
|
+
return Response.json({ status: "ok", agent: agent.name });
|
|
101
|
+
}
|
|
102
|
+
if (url.pathname === "/api/chat" && req.method === "POST") {
|
|
103
|
+
const body = await req.json();
|
|
104
|
+
return Response.json({
|
|
105
|
+
response: `[Dev Mode] Received: ${body.message}`,
|
|
106
|
+
conversationId: body.conversationId || crypto.randomUUID()
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
if (url.pathname === "/" && options.channel === "web") {
|
|
110
|
+
return new Response(getDevHtml(agent.name), {
|
|
111
|
+
headers: { "Content-Type": "text/html" }
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return new Response("Not Found", { status: 404 });
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
console.log();
|
|
118
|
+
console.log(chalk.green("Server running at"), chalk.cyan(`http://localhost:${port}`));
|
|
119
|
+
console.log();
|
|
120
|
+
if (options.channel === "web" && options.open) {
|
|
121
|
+
const openUrl = `http://localhost:${port}`;
|
|
122
|
+
if (process.platform === "darwin") {
|
|
123
|
+
Bun.spawn(["open", openUrl]);
|
|
124
|
+
} else if (process.platform === "linux") {
|
|
125
|
+
Bun.spawn(["xdg-open", openUrl]);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
spinner.start("Watching for changes");
|
|
129
|
+
const watcher = chokidar.watch([join3(cwd, "src"), join3(cwd, "af.config.ts")], {
|
|
130
|
+
ignoreInitial: true,
|
|
131
|
+
ignored: /node_modules/
|
|
132
|
+
});
|
|
133
|
+
watcher.on("change", async (path) => {
|
|
134
|
+
spinner.text = `Reloading (${path.replace(cwd, ".")})`;
|
|
135
|
+
try {
|
|
136
|
+
agent = await loadAgent(cwd);
|
|
137
|
+
spinner.succeed(`Reloaded "${agent.name}"`);
|
|
138
|
+
spinner.start("Watching for changes");
|
|
139
|
+
} catch (error) {
|
|
140
|
+
spinner.fail(`Reload failed: ${error}`);
|
|
141
|
+
spinner.start("Watching for changes");
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
process.on("SIGINT", () => {
|
|
145
|
+
console.log();
|
|
146
|
+
spinner.stop();
|
|
147
|
+
watcher.close();
|
|
148
|
+
server.stop();
|
|
149
|
+
console.log(chalk.gray("Server stopped"));
|
|
150
|
+
process.exit(0);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
function getDevHtml(agentName) {
|
|
154
|
+
return `<!DOCTYPE html>
|
|
155
|
+
<html lang="en">
|
|
156
|
+
<head>
|
|
157
|
+
<meta charset="UTF-8">
|
|
158
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
159
|
+
<title>${agentName} - Dev</title>
|
|
160
|
+
<style>
|
|
161
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
162
|
+
body { font-family: system-ui, -apple-system, sans-serif; background: #0a0a0a; color: #fafafa; height: 100vh; display: flex; flex-direction: column; }
|
|
163
|
+
header { padding: 1rem; border-bottom: 1px solid #333; }
|
|
164
|
+
header h1 { font-size: 1rem; font-weight: 500; }
|
|
165
|
+
header span { color: #888; font-size: 0.875rem; }
|
|
166
|
+
#messages { flex: 1; overflow-y: auto; padding: 1rem; display: flex; flex-direction: column; gap: 0.75rem; }
|
|
167
|
+
.message { max-width: 80%; padding: 0.75rem 1rem; border-radius: 0.75rem; line-height: 1.5; }
|
|
168
|
+
.message.user { align-self: flex-end; background: #2563eb; }
|
|
169
|
+
.message.assistant { align-self: flex-start; background: #27272a; }
|
|
170
|
+
form { padding: 1rem; border-top: 1px solid #333; display: flex; gap: 0.5rem; }
|
|
171
|
+
input { flex: 1; padding: 0.75rem 1rem; background: #18181b; border: 1px solid #333; border-radius: 0.5rem; color: #fafafa; font-size: 1rem; outline: none; }
|
|
172
|
+
input:focus { border-color: #2563eb; }
|
|
173
|
+
button { padding: 0.75rem 1.5rem; background: #2563eb; border: none; border-radius: 0.5rem; color: white; font-size: 1rem; cursor: pointer; }
|
|
174
|
+
button:hover { background: #1d4ed8; }
|
|
175
|
+
</style>
|
|
176
|
+
</head>
|
|
177
|
+
<body>
|
|
178
|
+
<header>
|
|
179
|
+
<h1>${agentName}</h1>
|
|
180
|
+
<span>Development Mode</span>
|
|
181
|
+
</header>
|
|
182
|
+
<div id="messages"></div>
|
|
183
|
+
<form id="chat-form">
|
|
184
|
+
<input type="text" id="input" placeholder="Type a message..." autocomplete="off" />
|
|
185
|
+
<button type="submit">Send</button>
|
|
186
|
+
</form>
|
|
187
|
+
<script>
|
|
188
|
+
const messages = document.getElementById('messages');
|
|
189
|
+
const form = document.getElementById('chat-form');
|
|
190
|
+
const input = document.getElementById('input');
|
|
191
|
+
let conversationId = null;
|
|
192
|
+
|
|
193
|
+
function addMessage(role, content) {
|
|
194
|
+
const div = document.createElement('div');
|
|
195
|
+
div.className = 'message ' + role;
|
|
196
|
+
div.textContent = content;
|
|
197
|
+
messages.appendChild(div);
|
|
198
|
+
messages.scrollTop = messages.scrollHeight;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
form.addEventListener('submit', async (e) => {
|
|
202
|
+
e.preventDefault();
|
|
203
|
+
const message = input.value.trim();
|
|
204
|
+
if (!message) return;
|
|
205
|
+
|
|
206
|
+
addMessage('user', message);
|
|
207
|
+
input.value = '';
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
const res = await fetch('/api/chat', {
|
|
211
|
+
method: 'POST',
|
|
212
|
+
headers: { 'Content-Type': 'application/json' },
|
|
213
|
+
body: JSON.stringify({ message, conversationId }),
|
|
214
|
+
});
|
|
215
|
+
const data = await res.json();
|
|
216
|
+
conversationId = data.conversationId;
|
|
217
|
+
addMessage('assistant', data.response);
|
|
218
|
+
} catch (err) {
|
|
219
|
+
addMessage('assistant', 'Error: ' + err.message);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
input.focus();
|
|
224
|
+
</script>
|
|
225
|
+
</body>
|
|
226
|
+
</html>`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// src/commands/build.ts
|
|
230
|
+
import { Command as Command2 } from "commander";
|
|
231
|
+
import chalk2 from "chalk";
|
|
232
|
+
import ora2 from "ora";
|
|
233
|
+
import { join as join4 } from "path";
|
|
234
|
+
|
|
235
|
+
// src/utils/validate.ts
|
|
236
|
+
function validateAgent(agent) {
|
|
237
|
+
const errors = [];
|
|
238
|
+
if (!agent.name) {
|
|
239
|
+
errors.push("Agent name is required");
|
|
240
|
+
} else if (!/^[a-z0-9-]+$/.test(agent.name)) {
|
|
241
|
+
errors.push("Agent name must be lowercase alphanumeric with hyphens only");
|
|
242
|
+
}
|
|
243
|
+
if (!agent.version) {
|
|
244
|
+
errors.push("Agent version is required");
|
|
245
|
+
} else if (!/^\d+\.\d+\.\d+/.test(agent.version)) {
|
|
246
|
+
errors.push("Agent version must follow semver format (e.g., 1.0.0)");
|
|
247
|
+
}
|
|
248
|
+
if (!agent.systemPrompt) {
|
|
249
|
+
errors.push("System prompt is required");
|
|
250
|
+
} else if (typeof agent.systemPrompt === "string" && agent.systemPrompt.trim().length === 0) {
|
|
251
|
+
errors.push("System prompt cannot be empty");
|
|
252
|
+
}
|
|
253
|
+
if (agent.model) {
|
|
254
|
+
const validProviders = ["anthropic", "openai", "google", "custom"];
|
|
255
|
+
if (!validProviders.includes(agent.model.provider)) {
|
|
256
|
+
errors.push(`Invalid model provider: ${agent.model.provider}`);
|
|
257
|
+
}
|
|
258
|
+
if (!agent.model.name) {
|
|
259
|
+
errors.push("Model name is required when model is specified");
|
|
260
|
+
}
|
|
261
|
+
if (agent.model.temperature !== undefined) {
|
|
262
|
+
if (agent.model.temperature < 0 || agent.model.temperature > 2) {
|
|
263
|
+
errors.push("Model temperature must be between 0 and 2");
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (agent.model.maxTokens !== undefined) {
|
|
267
|
+
if (agent.model.maxTokens < 1) {
|
|
268
|
+
errors.push("Model maxTokens must be at least 1");
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (agent.tools) {
|
|
273
|
+
for (const tool of agent.tools) {
|
|
274
|
+
const toolErrors = validateTool(tool);
|
|
275
|
+
errors.push(...toolErrors);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (agent.state) {
|
|
279
|
+
const validStorage = ["memory", "redis", "postgres", "custom"];
|
|
280
|
+
if (!validStorage.includes(agent.state.storage)) {
|
|
281
|
+
errors.push(`Invalid state storage: ${agent.state.storage}`);
|
|
282
|
+
}
|
|
283
|
+
if (agent.state.ttl !== undefined && agent.state.ttl < 0) {
|
|
284
|
+
errors.push("State TTL must be non-negative");
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return errors;
|
|
288
|
+
}
|
|
289
|
+
function validateTool(tool) {
|
|
290
|
+
const errors = [];
|
|
291
|
+
if (!tool.name) {
|
|
292
|
+
errors.push("Tool name is required");
|
|
293
|
+
} else if (!/^[a-z_][a-z0-9_]*$/.test(tool.name)) {
|
|
294
|
+
errors.push(`Tool name "${tool.name}" must be snake_case`);
|
|
295
|
+
}
|
|
296
|
+
if (!tool.description) {
|
|
297
|
+
errors.push(`Tool "${tool.name || "unknown"}" requires a description`);
|
|
298
|
+
}
|
|
299
|
+
if (!tool.parameters) {
|
|
300
|
+
errors.push(`Tool "${tool.name || "unknown"}" requires parameters definition`);
|
|
301
|
+
} else if (tool.parameters.type !== "object") {
|
|
302
|
+
errors.push(`Tool "${tool.name || "unknown"}" parameters type must be "object"`);
|
|
303
|
+
}
|
|
304
|
+
if (typeof tool.handler !== "function") {
|
|
305
|
+
errors.push(`Tool "${tool.name || "unknown"}" requires a handler function`);
|
|
306
|
+
}
|
|
307
|
+
return errors;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// src/commands/build.ts
|
|
311
|
+
var buildCommand = new Command2("build").description("Build and validate agent for production").option("-o, --outdir <dir>", "Output directory", "dist").action(async (options) => {
|
|
312
|
+
const spinner = ora2();
|
|
313
|
+
const cwd = process.cwd();
|
|
314
|
+
console.log();
|
|
315
|
+
console.log(chalk2.bold("Building Agent"));
|
|
316
|
+
console.log();
|
|
317
|
+
spinner.start("Loading configuration");
|
|
318
|
+
const config = await loadConfig(cwd);
|
|
319
|
+
spinner.succeed("Configuration loaded");
|
|
320
|
+
spinner.start("Loading agent");
|
|
321
|
+
const agent = await loadAgent(cwd);
|
|
322
|
+
spinner.succeed(`Agent "${agent.name}" loaded`);
|
|
323
|
+
spinner.start("Validating agent");
|
|
324
|
+
const errors = validateAgent(agent);
|
|
325
|
+
if (errors.length > 0) {
|
|
326
|
+
spinner.fail("Validation failed");
|
|
327
|
+
console.log();
|
|
328
|
+
for (const error of errors) {
|
|
329
|
+
console.log(chalk2.red(" ✗"), error);
|
|
330
|
+
}
|
|
331
|
+
console.log();
|
|
332
|
+
process.exit(1);
|
|
333
|
+
}
|
|
334
|
+
spinner.succeed("Agent validated");
|
|
335
|
+
spinner.start("Building");
|
|
336
|
+
const outdir = join4(cwd, options.outdir);
|
|
337
|
+
const result = await Bun.build({
|
|
338
|
+
entrypoints: [join4(cwd, "src/agent.ts")],
|
|
339
|
+
outdir,
|
|
340
|
+
target: "node",
|
|
341
|
+
minify: true
|
|
342
|
+
});
|
|
343
|
+
if (!result.success) {
|
|
344
|
+
spinner.fail("Build failed");
|
|
345
|
+
console.log();
|
|
346
|
+
for (const log of result.logs) {
|
|
347
|
+
console.log(chalk2.red(" ✗"), log.message);
|
|
348
|
+
}
|
|
349
|
+
process.exit(1);
|
|
350
|
+
}
|
|
351
|
+
spinner.succeed("Build completed");
|
|
352
|
+
console.log();
|
|
353
|
+
console.log(chalk2.green("Success!"), `Built to ${chalk2.cyan(options.outdir)}`);
|
|
354
|
+
console.log();
|
|
355
|
+
console.log("Output files:");
|
|
356
|
+
for (const output of result.outputs) {
|
|
357
|
+
console.log(chalk2.gray(" •"), output.path.replace(cwd, "."));
|
|
358
|
+
}
|
|
359
|
+
console.log();
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// src/commands/test.ts
|
|
363
|
+
import { Command as Command3 } from "commander";
|
|
364
|
+
import chalk3 from "chalk";
|
|
365
|
+
import ora3 from "ora";
|
|
366
|
+
import { join as join5 } from "path";
|
|
367
|
+
import { readdir, readFile } from "fs/promises";
|
|
368
|
+
import YAML from "yaml";
|
|
369
|
+
var testCommand = new Command3("test").description("Run test conversations").argument("[pattern]", "Test file pattern", "*.test.yaml").option("-v, --verbose", "Show detailed output").action(async (pattern, options) => {
|
|
370
|
+
const spinner = ora3();
|
|
371
|
+
const cwd = process.cwd();
|
|
372
|
+
console.log();
|
|
373
|
+
console.log(chalk3.bold("Running Tests"));
|
|
374
|
+
console.log();
|
|
375
|
+
spinner.start("Loading agent");
|
|
376
|
+
const agent = await loadAgent(cwd);
|
|
377
|
+
spinner.succeed(`Agent "${agent.name}" loaded`);
|
|
378
|
+
spinner.start("Finding test files");
|
|
379
|
+
const testsDir = join5(cwd, "tests");
|
|
380
|
+
let testFiles = [];
|
|
381
|
+
try {
|
|
382
|
+
const files = await readdir(testsDir);
|
|
383
|
+
testFiles = files.filter((f) => f.endsWith(".test.yaml") || f.endsWith(".test.yml"));
|
|
384
|
+
} catch {
|
|
385
|
+
spinner.warn("No tests directory found");
|
|
386
|
+
console.log();
|
|
387
|
+
console.log(chalk3.gray("Create tests in"), chalk3.cyan("tests/*.test.yaml"));
|
|
388
|
+
console.log();
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
if (testFiles.length === 0) {
|
|
392
|
+
spinner.warn("No test files found");
|
|
393
|
+
console.log();
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
spinner.succeed(`Found ${testFiles.length} test file(s)`);
|
|
397
|
+
const results = [];
|
|
398
|
+
for (const file of testFiles) {
|
|
399
|
+
const filePath = join5(testsDir, file);
|
|
400
|
+
const content = await readFile(filePath, "utf-8");
|
|
401
|
+
const testCase = YAML.parse(content);
|
|
402
|
+
if (options.verbose) {
|
|
403
|
+
console.log();
|
|
404
|
+
console.log(chalk3.gray("Running:"), testCase.name);
|
|
405
|
+
}
|
|
406
|
+
const result = await runTest(testCase, agent, options.verbose);
|
|
407
|
+
results.push(result);
|
|
408
|
+
if (result.passed) {
|
|
409
|
+
console.log(chalk3.green(" ✓"), result.name);
|
|
410
|
+
} else {
|
|
411
|
+
console.log(chalk3.red(" ✗"), result.name);
|
|
412
|
+
for (const error of result.errors) {
|
|
413
|
+
console.log(chalk3.red(" →"), error);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
const passed = results.filter((r) => r.passed).length;
|
|
418
|
+
const failed = results.filter((r) => !r.passed).length;
|
|
419
|
+
console.log();
|
|
420
|
+
if (failed === 0) {
|
|
421
|
+
console.log(chalk3.green("All tests passed!"), chalk3.gray(`(${passed}/${results.length})`));
|
|
422
|
+
} else {
|
|
423
|
+
console.log(chalk3.red("Tests failed:"), chalk3.gray(`${passed}/${results.length} passed`));
|
|
424
|
+
}
|
|
425
|
+
console.log();
|
|
426
|
+
if (failed > 0) {
|
|
427
|
+
process.exit(1);
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
async function runTest(testCase, agent, verbose) {
|
|
431
|
+
const errors = [];
|
|
432
|
+
for (const message of testCase.conversation || []) {
|
|
433
|
+
if (message.role === "assistant" && message.assertions) {
|
|
434
|
+
for (const assertion of message.assertions) {
|
|
435
|
+
const passed = checkAssertion(assertion, "[mock response]");
|
|
436
|
+
if (!passed) {
|
|
437
|
+
errors.push(`Assertion failed: ${assertion.type} - ${JSON.stringify(assertion.value)}`);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
if (errors.length === 0 && testCase.assertions) {
|
|
443
|
+
for (const assertion of testCase.assertions) {
|
|
444
|
+
const passed = checkAssertion(assertion, "[mock response]");
|
|
445
|
+
if (!passed) {
|
|
446
|
+
errors.push(`Assertion failed: ${assertion.type} - ${JSON.stringify(assertion.value)}`);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return {
|
|
451
|
+
name: testCase.name,
|
|
452
|
+
passed: errors.length === 0,
|
|
453
|
+
errors
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
function checkAssertion(assertion, response) {
|
|
457
|
+
switch (assertion.type) {
|
|
458
|
+
case "contains":
|
|
459
|
+
return typeof assertion.value === "string" && response.includes(assertion.value);
|
|
460
|
+
case "matches":
|
|
461
|
+
return typeof assertion.value === "string" && new RegExp(assertion.value).test(response);
|
|
462
|
+
case "toolCalled":
|
|
463
|
+
return true;
|
|
464
|
+
case "stateEquals":
|
|
465
|
+
return true;
|
|
466
|
+
default:
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// src/commands/deploy.ts
|
|
472
|
+
import { Command as Command4 } from "commander";
|
|
473
|
+
import chalk4 from "chalk";
|
|
474
|
+
import ora4 from "ora";
|
|
475
|
+
var deployCommand = new Command4("deploy").description("Deploy agent to Agent Factory cloud").option("-e, --env <environment>", "Target environment", "staging").option("--dry-run", "Show what would be deployed without deploying").action(async (options) => {
|
|
476
|
+
const spinner = ora4();
|
|
477
|
+
const cwd = process.cwd();
|
|
478
|
+
console.log();
|
|
479
|
+
console.log(chalk4.bold("Deploying Agent"));
|
|
480
|
+
console.log();
|
|
481
|
+
spinner.start("Loading configuration");
|
|
482
|
+
const config = await loadConfig(cwd);
|
|
483
|
+
spinner.succeed("Configuration loaded");
|
|
484
|
+
spinner.start("Loading agent");
|
|
485
|
+
const agent = await loadAgent(cwd);
|
|
486
|
+
spinner.succeed(`Agent "${agent.name}" loaded`);
|
|
487
|
+
spinner.start("Validating agent");
|
|
488
|
+
const errors = validateAgent(agent);
|
|
489
|
+
if (errors.length > 0) {
|
|
490
|
+
spinner.fail("Validation failed");
|
|
491
|
+
console.log();
|
|
492
|
+
for (const error of errors) {
|
|
493
|
+
console.log(chalk4.red(" ✗"), error);
|
|
494
|
+
}
|
|
495
|
+
console.log();
|
|
496
|
+
process.exit(1);
|
|
497
|
+
}
|
|
498
|
+
spinner.succeed("Agent validated");
|
|
499
|
+
if (options.dryRun) {
|
|
500
|
+
console.log();
|
|
501
|
+
console.log(chalk4.yellow("Dry run mode - no changes will be made"));
|
|
502
|
+
console.log();
|
|
503
|
+
console.log("Would deploy:");
|
|
504
|
+
console.log(chalk4.gray(" •"), `Agent: ${chalk4.cyan(agent.name)}`);
|
|
505
|
+
console.log(chalk4.gray(" •"), `Version: ${chalk4.cyan(agent.version)}`);
|
|
506
|
+
console.log(chalk4.gray(" •"), `Environment: ${chalk4.cyan(options.env)}`);
|
|
507
|
+
console.log();
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
spinner.start(`Deploying to ${options.env}`);
|
|
511
|
+
const apiKey = process.env.AGENT_FACTORY_API_KEY;
|
|
512
|
+
if (!apiKey) {
|
|
513
|
+
spinner.fail("Missing AGENT_FACTORY_API_KEY environment variable");
|
|
514
|
+
console.log();
|
|
515
|
+
console.log("Set your API key:");
|
|
516
|
+
console.log(chalk4.gray(" $"), chalk4.cyan("export AGENT_FACTORY_API_KEY=your_api_key"));
|
|
517
|
+
console.log();
|
|
518
|
+
process.exit(1);
|
|
519
|
+
}
|
|
520
|
+
const apiUrl = process.env.AGENT_FACTORY_API_URL || "https://api.agent-factory.dev";
|
|
521
|
+
try {
|
|
522
|
+
const response = await fetch(`${apiUrl}/v1/agents/deploy`, {
|
|
523
|
+
method: "POST",
|
|
524
|
+
headers: {
|
|
525
|
+
"Content-Type": "application/json",
|
|
526
|
+
Authorization: `Bearer ${apiKey}`
|
|
527
|
+
},
|
|
528
|
+
body: JSON.stringify({
|
|
529
|
+
name: agent.name,
|
|
530
|
+
version: agent.version,
|
|
531
|
+
environment: options.env,
|
|
532
|
+
config: agent
|
|
533
|
+
})
|
|
534
|
+
});
|
|
535
|
+
if (!response.ok) {
|
|
536
|
+
const error = await response.json();
|
|
537
|
+
throw new Error(error.message || `HTTP ${response.status}`);
|
|
538
|
+
}
|
|
539
|
+
const result = await response.json();
|
|
540
|
+
spinner.succeed(`Deployed to ${options.env}`);
|
|
541
|
+
console.log();
|
|
542
|
+
console.log(chalk4.green("Success!"), "Agent deployed");
|
|
543
|
+
console.log();
|
|
544
|
+
console.log("Deployment details:");
|
|
545
|
+
console.log(chalk4.gray(" •"), `ID: ${chalk4.cyan(result.deploymentId)}`);
|
|
546
|
+
console.log(chalk4.gray(" •"), `URL: ${chalk4.cyan(result.url)}`);
|
|
547
|
+
console.log();
|
|
548
|
+
} catch (error) {
|
|
549
|
+
spinner.fail("Deployment failed");
|
|
550
|
+
console.log();
|
|
551
|
+
console.log(chalk4.red("Error:"), error instanceof Error ? error.message : String(error));
|
|
552
|
+
console.log();
|
|
553
|
+
process.exit(1);
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
// src/commands/validate.ts
|
|
558
|
+
import { Command as Command5 } from "commander";
|
|
559
|
+
import chalk5 from "chalk";
|
|
560
|
+
import ora5 from "ora";
|
|
561
|
+
var validateCommand = new Command5("validate").description("Validate agent configuration").option("--strict", "Enable strict validation").action(async (options) => {
|
|
562
|
+
const spinner = ora5();
|
|
563
|
+
const cwd = process.cwd();
|
|
564
|
+
console.log();
|
|
565
|
+
console.log(chalk5.bold("Validating Agent"));
|
|
566
|
+
console.log();
|
|
567
|
+
spinner.start("Loading agent");
|
|
568
|
+
let agent;
|
|
569
|
+
try {
|
|
570
|
+
agent = await loadAgent(cwd);
|
|
571
|
+
spinner.succeed(`Agent "${agent.name}" loaded`);
|
|
572
|
+
} catch (error) {
|
|
573
|
+
spinner.fail("Failed to load agent");
|
|
574
|
+
console.log();
|
|
575
|
+
console.log(chalk5.red("Error:"), error instanceof Error ? error.message : String(error));
|
|
576
|
+
console.log();
|
|
577
|
+
process.exit(1);
|
|
578
|
+
}
|
|
579
|
+
spinner.start("Validating configuration");
|
|
580
|
+
const errors = validateAgent(agent);
|
|
581
|
+
const warnings = options.strict ? getStrictWarnings(agent) : [];
|
|
582
|
+
if (errors.length === 0 && warnings.length === 0) {
|
|
583
|
+
spinner.succeed("Agent is valid");
|
|
584
|
+
console.log();
|
|
585
|
+
console.log(chalk5.green("✓"), "No issues found");
|
|
586
|
+
console.log();
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
if (errors.length > 0) {
|
|
590
|
+
spinner.fail("Validation failed");
|
|
591
|
+
console.log();
|
|
592
|
+
console.log(chalk5.red("Errors:"));
|
|
593
|
+
for (const error of errors) {
|
|
594
|
+
console.log(chalk5.red(" ✗"), error);
|
|
595
|
+
}
|
|
596
|
+
} else {
|
|
597
|
+
spinner.succeed("Validation passed with warnings");
|
|
598
|
+
}
|
|
599
|
+
if (warnings.length > 0) {
|
|
600
|
+
console.log();
|
|
601
|
+
console.log(chalk5.yellow("Warnings:"));
|
|
602
|
+
for (const warning of warnings) {
|
|
603
|
+
console.log(chalk5.yellow(" ⚠"), warning);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
console.log();
|
|
607
|
+
if (errors.length > 0) {
|
|
608
|
+
process.exit(1);
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
function getStrictWarnings(agent) {
|
|
612
|
+
const warnings = [];
|
|
613
|
+
if (!agent.description) {
|
|
614
|
+
warnings.push("Agent is missing a description");
|
|
615
|
+
}
|
|
616
|
+
if (!agent.tools || agent.tools.length === 0) {
|
|
617
|
+
warnings.push("Agent has no tools defined");
|
|
618
|
+
}
|
|
619
|
+
return warnings;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// src/commands/logs.ts
|
|
623
|
+
import { Command as Command6 } from "commander";
|
|
624
|
+
import chalk6 from "chalk";
|
|
625
|
+
import ora6 from "ora";
|
|
626
|
+
var logsCommand = new Command6("logs").description("Stream production logs").option("-e, --env <environment>", "Environment to stream from", "production").option("-f, --follow", "Follow log output").option("-n, --lines <number>", "Number of lines to show", "100").action(async (options) => {
|
|
627
|
+
const spinner = ora6();
|
|
628
|
+
console.log();
|
|
629
|
+
console.log(chalk6.bold("Streaming Logs"));
|
|
630
|
+
console.log();
|
|
631
|
+
const apiKey = process.env.AGENT_FACTORY_API_KEY;
|
|
632
|
+
if (!apiKey) {
|
|
633
|
+
console.log(chalk6.red("Error:"), "Missing AGENT_FACTORY_API_KEY environment variable");
|
|
634
|
+
console.log();
|
|
635
|
+
console.log("Set your API key:");
|
|
636
|
+
console.log(chalk6.gray(" $"), chalk6.cyan("export AGENT_FACTORY_API_KEY=your_api_key"));
|
|
637
|
+
console.log();
|
|
638
|
+
process.exit(1);
|
|
639
|
+
}
|
|
640
|
+
const apiUrl = process.env.AGENT_FACTORY_API_URL || "https://api.agent-factory.dev";
|
|
641
|
+
if (options.follow) {
|
|
642
|
+
spinner.start(`Connecting to ${options.env} logs`);
|
|
643
|
+
try {
|
|
644
|
+
const wsUrl = apiUrl.replace("https://", "wss://").replace("http://", "ws://");
|
|
645
|
+
const ws = new WebSocket(`${wsUrl}/v1/logs/stream?env=${options.env}`, {
|
|
646
|
+
headers: {
|
|
647
|
+
Authorization: `Bearer ${apiKey}`
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
ws.onopen = () => {
|
|
651
|
+
spinner.succeed(`Connected to ${options.env}`);
|
|
652
|
+
console.log();
|
|
653
|
+
console.log(chalk6.gray("Streaming logs... (Ctrl+C to stop)"));
|
|
654
|
+
console.log();
|
|
655
|
+
};
|
|
656
|
+
ws.onmessage = (event) => {
|
|
657
|
+
const log = JSON.parse(event.data);
|
|
658
|
+
const levelColor = log.level === "error" ? chalk6.red : log.level === "warn" ? chalk6.yellow : chalk6.gray;
|
|
659
|
+
console.log(chalk6.gray(new Date(log.timestamp).toISOString()), levelColor(`[${log.level}]`), log.message);
|
|
660
|
+
};
|
|
661
|
+
ws.onerror = () => {
|
|
662
|
+
spinner.fail("Connection error");
|
|
663
|
+
process.exit(1);
|
|
664
|
+
};
|
|
665
|
+
ws.onclose = () => {
|
|
666
|
+
console.log();
|
|
667
|
+
console.log(chalk6.gray("Connection closed"));
|
|
668
|
+
process.exit(0);
|
|
669
|
+
};
|
|
670
|
+
process.on("SIGINT", () => {
|
|
671
|
+
console.log();
|
|
672
|
+
ws.close();
|
|
673
|
+
});
|
|
674
|
+
} catch (error) {
|
|
675
|
+
spinner.fail("Failed to connect");
|
|
676
|
+
console.log();
|
|
677
|
+
console.log(chalk6.red("Error:"), error instanceof Error ? error.message : String(error));
|
|
678
|
+
console.log();
|
|
679
|
+
process.exit(1);
|
|
680
|
+
}
|
|
681
|
+
} else {
|
|
682
|
+
spinner.start("Fetching logs");
|
|
683
|
+
try {
|
|
684
|
+
const response = await fetch(`${apiUrl}/v1/logs?env=${options.env}&lines=${options.lines}`, {
|
|
685
|
+
headers: {
|
|
686
|
+
Authorization: `Bearer ${apiKey}`
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
if (!response.ok) {
|
|
690
|
+
throw new Error(`HTTP ${response.status}`);
|
|
691
|
+
}
|
|
692
|
+
const logs = await response.json();
|
|
693
|
+
spinner.succeed(`Fetched ${logs.length} log entries`);
|
|
694
|
+
console.log();
|
|
695
|
+
for (const log of logs) {
|
|
696
|
+
const levelColor = log.level === "error" ? chalk6.red : log.level === "warn" ? chalk6.yellow : chalk6.gray;
|
|
697
|
+
console.log(chalk6.gray(new Date(log.timestamp).toISOString()), levelColor(`[${log.level}]`), log.message);
|
|
698
|
+
}
|
|
699
|
+
console.log();
|
|
700
|
+
} catch (error) {
|
|
701
|
+
spinner.fail("Failed to fetch logs");
|
|
702
|
+
console.log();
|
|
703
|
+
console.log(chalk6.red("Error:"), error instanceof Error ? error.message : String(error));
|
|
704
|
+
console.log();
|
|
705
|
+
process.exit(1);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
// src/commands/state.ts
|
|
711
|
+
import { Command as Command7 } from "commander";
|
|
712
|
+
import chalk7 from "chalk";
|
|
713
|
+
import ora7 from "ora";
|
|
714
|
+
var stateCommand = new Command7("state").description("Inspect conversation state").argument("<id>", "Conversation ID").option("-e, --env <environment>", "Environment", "production").option("--json", "Output as JSON").action(async (id, options) => {
|
|
715
|
+
const spinner = ora7();
|
|
716
|
+
console.log();
|
|
717
|
+
console.log(chalk7.bold("Conversation State"));
|
|
718
|
+
console.log();
|
|
719
|
+
const apiKey = process.env.AGENT_FACTORY_API_KEY;
|
|
720
|
+
if (!apiKey) {
|
|
721
|
+
console.log(chalk7.red("Error:"), "Missing AGENT_FACTORY_API_KEY environment variable");
|
|
722
|
+
console.log();
|
|
723
|
+
console.log("Set your API key:");
|
|
724
|
+
console.log(chalk7.gray(" $"), chalk7.cyan("export AGENT_FACTORY_API_KEY=your_api_key"));
|
|
725
|
+
console.log();
|
|
726
|
+
process.exit(1);
|
|
727
|
+
}
|
|
728
|
+
const apiUrl = process.env.AGENT_FACTORY_API_URL || "https://api.agent-factory.dev";
|
|
729
|
+
spinner.start("Fetching conversation state");
|
|
730
|
+
try {
|
|
731
|
+
const response = await fetch(`${apiUrl}/v1/conversations/${id}/state?env=${options.env}`, {
|
|
732
|
+
headers: {
|
|
733
|
+
Authorization: `Bearer ${apiKey}`
|
|
734
|
+
}
|
|
735
|
+
});
|
|
736
|
+
if (!response.ok) {
|
|
737
|
+
if (response.status === 404) {
|
|
738
|
+
throw new Error("Conversation not found");
|
|
739
|
+
}
|
|
740
|
+
throw new Error(`HTTP ${response.status}`);
|
|
741
|
+
}
|
|
742
|
+
const state = await response.json();
|
|
743
|
+
spinner.succeed("State retrieved");
|
|
744
|
+
if (options.json) {
|
|
745
|
+
console.log();
|
|
746
|
+
console.log(JSON.stringify(state, null, 2));
|
|
747
|
+
console.log();
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
console.log();
|
|
751
|
+
console.log(chalk7.gray("Conversation:"), chalk7.cyan(state.conversationId));
|
|
752
|
+
console.log(chalk7.gray("Created:"), new Date(state.createdAt).toLocaleString());
|
|
753
|
+
console.log(chalk7.gray("Updated:"), new Date(state.updatedAt).toLocaleString());
|
|
754
|
+
console.log(chalk7.gray("Messages:"), state.messageCount);
|
|
755
|
+
console.log();
|
|
756
|
+
console.log(chalk7.bold("State:"));
|
|
757
|
+
if (Object.keys(state.state).length === 0) {
|
|
758
|
+
console.log(chalk7.gray(" (empty)"));
|
|
759
|
+
} else {
|
|
760
|
+
for (const [key, value] of Object.entries(state.state)) {
|
|
761
|
+
const displayValue = typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
762
|
+
console.log(chalk7.gray(" •"), `${key}:`, chalk7.cyan(displayValue));
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
console.log();
|
|
766
|
+
} catch (error) {
|
|
767
|
+
spinner.fail("Failed to fetch state");
|
|
768
|
+
console.log();
|
|
769
|
+
console.log(chalk7.red("Error:"), error instanceof Error ? error.message : String(error));
|
|
770
|
+
console.log();
|
|
771
|
+
process.exit(1);
|
|
772
|
+
}
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
// src/index.ts
|
|
776
|
+
program.name("af").description("Agent Factory CLI").version("0.1.0");
|
|
777
|
+
program.addCommand(devCommand);
|
|
778
|
+
program.addCommand(buildCommand);
|
|
779
|
+
program.addCommand(testCommand);
|
|
780
|
+
program.addCommand(deployCommand);
|
|
781
|
+
program.addCommand(validateCommand);
|
|
782
|
+
program.addCommand(logsCommand);
|
|
783
|
+
program.addCommand(stateCommand);
|
|
784
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@marco-kueks/agent-factory-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI tool for developing, testing, and deploying AI agents",
|
|
5
|
+
"keywords": ["ai", "agents", "cli", "developer-tools", "llm"],
|
|
6
|
+
"author": "marco-kueks",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/MarcoNaik/agent-factory.git",
|
|
11
|
+
"directory": "packages/cli"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://github.com/MarcoNaik/agent-factory#readme",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/MarcoNaik/agent-factory/issues"
|
|
16
|
+
},
|
|
17
|
+
"type": "module",
|
|
18
|
+
"bin": {
|
|
19
|
+
"af": "./dist/index.js"
|
|
20
|
+
},
|
|
21
|
+
"main": "./dist/index.js",
|
|
22
|
+
"files": ["dist"],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "bun build ./src/index.ts --outdir ./dist --target node --external commander --external chalk --external ora --external chokidar --external yaml && chmod +x ./dist/index.js",
|
|
25
|
+
"dev": "bun run ./src/index.ts",
|
|
26
|
+
"test": "bun test"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@marco-kueks/agent-factory-core": "^0.1.0",
|
|
30
|
+
"chalk": "^5.3.0",
|
|
31
|
+
"chokidar": "^3.5.3",
|
|
32
|
+
"commander": "^12.0.0",
|
|
33
|
+
"ora": "^8.0.0",
|
|
34
|
+
"yaml": "^2.3.4"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"bun-types": "^1.0.0",
|
|
38
|
+
"typescript": "^5.3.0"
|
|
39
|
+
}
|
|
40
|
+
}
|