@llamaventures/cli 1.13.0 → 1.14.1

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.
@@ -0,0 +1,306 @@
1
+ #!/usr/bin/env node
2
+
3
+ import assert from "node:assert/strict";
4
+ import { spawn } from "node:child_process";
5
+ import { createServer } from "node:http";
6
+ import { mkdtemp, rm } from "node:fs/promises";
7
+ import os from "node:os";
8
+ import path from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+
11
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
12
+ const calls = [];
13
+ let threadSeq = 0;
14
+
15
+ async function readJson(req) {
16
+ let raw = "";
17
+ for await (const chunk of req) raw += chunk;
18
+ if (!raw) return null;
19
+ try {
20
+ return JSON.parse(raw);
21
+ } catch {
22
+ return raw;
23
+ }
24
+ }
25
+
26
+ function writeJson(res, data) {
27
+ res.writeHead(200, { "Content-Type": "application/json" });
28
+ res.end(JSON.stringify(data));
29
+ }
30
+
31
+ function writeSse(res) {
32
+ res.writeHead(200, {
33
+ "Content-Type": "text/event-stream",
34
+ "Cache-Control": "no-cache",
35
+ Connection: "keep-alive",
36
+ });
37
+ const events = [
38
+ { tool_use: { name: "read_typed_factual_layer" } },
39
+ { tool_result: { name: "read_typed_factual_layer", ok: true, summary: "ok" } },
40
+ { text: "agent done" },
41
+ ];
42
+ for (const event of events) {
43
+ res.write(`data: ${JSON.stringify(event)}\n\n`);
44
+ }
45
+ res.end();
46
+ }
47
+
48
+ const server = createServer(async (req, res) => {
49
+ try {
50
+ const body = await readJson(req);
51
+ const url = new URL(req.url, "http://localhost");
52
+ calls.push({ method: req.method, path: url.pathname, body });
53
+
54
+ if (req.method === "POST" && /^\/api\/deals\/[^/]+\/threads$/.test(url.pathname)) {
55
+ threadSeq += 1;
56
+ writeJson(res, { id: `thread-${threadSeq}` });
57
+ return;
58
+ }
59
+
60
+ if (req.method === "POST" && /^\/api\/deals\/[^/]+\/threads\/[^/]+$/.test(url.pathname)) {
61
+ writeSse(res);
62
+ return;
63
+ }
64
+
65
+ if (req.method === "POST" && /^\/api\/deals\/[^/]+\/enrich$/.test(url.pathname)) {
66
+ writeJson(res, {
67
+ ok: true,
68
+ agentHarness: {
69
+ handoffPrompt: "mock handoff prompt",
70
+ systemInjection: "mock system injection",
71
+ },
72
+ });
73
+ return;
74
+ }
75
+
76
+ res.writeHead(404, { "Content-Type": "application/json" });
77
+ res.end(JSON.stringify({ error: `Unexpected route ${req.method} ${url.pathname}` }));
78
+ } catch (err) {
79
+ res.writeHead(500, { "Content-Type": "application/json" });
80
+ res.end(JSON.stringify({ error: err?.message ?? String(err) }));
81
+ }
82
+ });
83
+
84
+ function listen(server) {
85
+ return new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
86
+ }
87
+
88
+ function close(server) {
89
+ return new Promise((resolve, reject) => {
90
+ server.close((err) => (err ? reject(err) : resolve()));
91
+ });
92
+ }
93
+
94
+ function childEnv(baseUrl, homeDir) {
95
+ return {
96
+ ...process.env,
97
+ HOME: homeDir,
98
+ LLAMA_API_URL: baseUrl,
99
+ LLAMA_TOKEN: "llc_mock_agent_routing",
100
+ PATH: "/usr/bin:/bin",
101
+ };
102
+ }
103
+
104
+ function resetCalls() {
105
+ calls.length = 0;
106
+ threadSeq = 0;
107
+ }
108
+
109
+ function paths() {
110
+ return calls.map((call) => `${call.method} ${call.path}`);
111
+ }
112
+
113
+ function assertNoEnrichCall() {
114
+ assert.equal(
115
+ calls.some((call) => call.path.endsWith("/enrich")),
116
+ false,
117
+ `expected no /enrich call, got ${paths().join(", ")}`,
118
+ );
119
+ }
120
+
121
+ function assertThreadRun({ title, messageIncludes }) {
122
+ assert.equal(calls.length, 2, `expected thread create + SSE run, got ${paths().join(", ")}`);
123
+ assert.match(calls[0].path, /^\/api\/deals\/[^/]+\/threads$/);
124
+ assert.equal(calls[0].body?.title, title);
125
+ assert.match(calls[1].path, /^\/api\/deals\/[^/]+\/threads\/thread-1$/);
126
+ for (const needle of messageIncludes) {
127
+ assert.match(calls[1].body?.message ?? "", new RegExp(escapeRegExp(needle)));
128
+ }
129
+ }
130
+
131
+ function escapeRegExp(value) {
132
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
133
+ }
134
+
135
+ async function runCli(args, baseUrl, homeDir) {
136
+ const child = spawn(process.execPath, ["bin/llama.mjs", ...args], {
137
+ cwd: repoRoot,
138
+ env: childEnv(baseUrl, homeDir),
139
+ stdio: ["ignore", "pipe", "pipe"],
140
+ });
141
+ let stdout = "";
142
+ let stderr = "";
143
+ child.stdout.on("data", (chunk) => {
144
+ stdout += chunk;
145
+ });
146
+ child.stderr.on("data", (chunk) => {
147
+ stderr += chunk;
148
+ });
149
+ const code = await new Promise((resolve) => child.on("close", resolve));
150
+ assert.equal(code, 0, `CLI failed (${code})\nSTDOUT:\n${stdout}\nSTDERR:\n${stderr}`);
151
+ return { stdout, stderr };
152
+ }
153
+
154
+ async function callMcpTool(name, args, baseUrl, homeDir) {
155
+ const child = spawn(process.execPath, ["bin/llama-mcp.mjs"], {
156
+ cwd: repoRoot,
157
+ env: childEnv(baseUrl, homeDir),
158
+ stdio: ["pipe", "pipe", "pipe"],
159
+ });
160
+ let stderr = "";
161
+ let buffer = "";
162
+ child.stderr.on("data", (chunk) => {
163
+ stderr += chunk;
164
+ });
165
+
166
+ const result = await new Promise((resolve, reject) => {
167
+ const timeout = setTimeout(() => {
168
+ child.kill();
169
+ reject(new Error(`Timed out waiting for MCP response\nSTDERR:\n${stderr}`));
170
+ }, 8000);
171
+
172
+ child.stdout.on("data", (chunk) => {
173
+ buffer += chunk;
174
+ let idx;
175
+ while ((idx = buffer.indexOf("\n")) >= 0) {
176
+ const line = buffer.slice(0, idx).trim();
177
+ buffer = buffer.slice(idx + 1);
178
+ if (!line) continue;
179
+ let msg;
180
+ try {
181
+ msg = JSON.parse(line);
182
+ } catch {
183
+ continue;
184
+ }
185
+ if (msg.id === 2) {
186
+ clearTimeout(timeout);
187
+ child.kill();
188
+ resolve(msg);
189
+ }
190
+ }
191
+ });
192
+
193
+ child.on("error", (err) => {
194
+ clearTimeout(timeout);
195
+ reject(err);
196
+ });
197
+
198
+ child.stdin.write(
199
+ [
200
+ JSON.stringify({
201
+ jsonrpc: "2.0",
202
+ id: 1,
203
+ method: "initialize",
204
+ params: {
205
+ protocolVersion: "2024-11-05",
206
+ capabilities: {},
207
+ clientInfo: { name: "routing-test", version: "1" },
208
+ },
209
+ }),
210
+ JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }),
211
+ JSON.stringify({
212
+ jsonrpc: "2.0",
213
+ id: 2,
214
+ method: "tools/call",
215
+ params: { name, arguments: args },
216
+ }),
217
+ ].join("\n") + "\n",
218
+ );
219
+ });
220
+
221
+ assert.ok(!result.error, `MCP returned error: ${JSON.stringify(result.error)}`);
222
+ return result.result;
223
+ }
224
+
225
+ await listen(server);
226
+ const address = server.address();
227
+ const baseUrl = `http://${address.address}:${address.port}`;
228
+ const homeDir = await mkdtemp(path.join(os.tmpdir(), "llama-cli-routing-"));
229
+
230
+ try {
231
+ resetCalls();
232
+ const enrichRun = await runCli(
233
+ [
234
+ "deal",
235
+ "enrich",
236
+ "deal-cli",
237
+ "--apply",
238
+ "--executor",
239
+ "server_agent",
240
+ "--sources",
241
+ "website,monid",
242
+ "--budget-cents",
243
+ "12",
244
+ ],
245
+ baseUrl,
246
+ homeDir,
247
+ );
248
+ assert.match(enrichRun.stdout, /agent done/);
249
+ assertNoEnrichCall();
250
+ assertThreadRun({
251
+ title: "CLI enrichment",
252
+ messageIncludes: ["website, monid", "12 cents", "upsert_typed_fact"],
253
+ });
254
+
255
+ resetCalls();
256
+ await runCli(
257
+ ["deal", "enrich", "deal-cli", "--apply", "--executor", "server_agent", "--harness-only"],
258
+ baseUrl,
259
+ homeDir,
260
+ );
261
+ assert.deepEqual(paths(), ["POST /api/deals/deal-cli/enrich"]);
262
+ assert.equal(calls[0].body?.apply, true);
263
+ assert.equal(calls[0].body?.dryRun, false);
264
+ assert.equal(calls[0].body?.executor, "server_agent");
265
+
266
+ resetCalls();
267
+ const agentRun = await runCli(
268
+ ["deal", "agent", "run", "deal-cli", "--message", "custom server task"],
269
+ baseUrl,
270
+ homeDir,
271
+ );
272
+ assert.match(agentRun.stdout, /agent done/);
273
+ assertNoEnrichCall();
274
+ assertThreadRun({
275
+ title: "CLI agent run",
276
+ messageIncludes: ["custom server task"],
277
+ });
278
+
279
+ resetCalls();
280
+ const mcpResult = await callMcpTool(
281
+ "deal_enrich",
282
+ {
283
+ dealId: "deal-mcp",
284
+ apply: true,
285
+ executor: "server_agent",
286
+ sources: ["web", "monid"],
287
+ budgetCents: 7,
288
+ },
289
+ baseUrl,
290
+ homeDir,
291
+ );
292
+ assertNoEnrichCall();
293
+ assertThreadRun({
294
+ title: "MCP enrichment",
295
+ messageIncludes: ["web, monid", "7 cents", "upsert_typed_fact"],
296
+ });
297
+ const payload = JSON.parse(mcpResult.content?.[0]?.text ?? "{}");
298
+ assert.equal(payload.ok, true);
299
+ assert.equal(payload.threadId, "thread-1");
300
+ assert.equal(payload.text, "agent done");
301
+
302
+ console.log("agent routing verification passed");
303
+ } finally {
304
+ await close(server);
305
+ await rm(homeDir, { recursive: true, force: true });
306
+ }