@llamaventures/cli 1.15.0 → 1.16.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/AGENT_BRIEFING.md +18 -6
- package/CHANGELOG.md +32 -0
- package/README.md +42 -7
- package/README.zh-CN.md +16 -5
- package/bin/llama-mcp.mjs +97 -15
- package/bin/llama.mjs +142 -38
- package/lib/client.mjs +332 -0
- package/package.json +1 -1
- package/scripts/verify-agent-routing.mjs +127 -16
package/lib/client.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import path from "path";
|
|
|
13
13
|
import { fileURLToPath } from "url";
|
|
14
14
|
import { execFile as _execFile } from "child_process";
|
|
15
15
|
import { promisify } from "util";
|
|
16
|
+
import { randomUUID } from "crypto";
|
|
16
17
|
|
|
17
18
|
const execFile = promisify(_execFile);
|
|
18
19
|
|
|
@@ -43,6 +44,19 @@ export function readBriefing() {
|
|
|
43
44
|
}
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
let packageVersionCache = null;
|
|
48
|
+
|
|
49
|
+
export function getPackageVersion() {
|
|
50
|
+
if (packageVersionCache) return packageVersionCache;
|
|
51
|
+
try {
|
|
52
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, "package.json"), "utf8"));
|
|
53
|
+
packageVersionCache = String(pkg.version || "unknown");
|
|
54
|
+
} catch {
|
|
55
|
+
packageVersionCache = "unknown";
|
|
56
|
+
}
|
|
57
|
+
return packageVersionCache;
|
|
58
|
+
}
|
|
59
|
+
|
|
46
60
|
// Canonical entrypoint. `llama-command.onrender.com` also serves the API
|
|
47
61
|
// but its NextAuth callback URL doesn't match, so browser login (needed
|
|
48
62
|
// to mint a token at /settings/tokens) fails there with a server-config
|
|
@@ -54,6 +68,7 @@ export const DEFAULT_BASE_URL = "https://command.llamaventures.vc";
|
|
|
54
68
|
// agent-discovery convention.
|
|
55
69
|
export const TOKEN_DIR = path.join(os.homedir(), ".llama");
|
|
56
70
|
export const TOKEN_FILE = path.join(TOKEN_DIR, "token");
|
|
71
|
+
export const AGENT_SESSION_FILE = path.join(TOKEN_DIR, "agent-session.json");
|
|
57
72
|
|
|
58
73
|
// Legacy location used by CLI v0.1. Read for back-compat (silent migrate
|
|
59
74
|
// to canonical on first use); never written for the token, but still the
|
|
@@ -123,6 +138,269 @@ export function getToken() {
|
|
|
123
138
|
return "";
|
|
124
139
|
}
|
|
125
140
|
|
|
141
|
+
let runtimeClient = "cli";
|
|
142
|
+
let runtimeAgentClient = null;
|
|
143
|
+
|
|
144
|
+
export function setClientRuntime(opts = {}) {
|
|
145
|
+
if (opts.client) runtimeClient = String(opts.client);
|
|
146
|
+
if (opts.agentClient) runtimeAgentClient = String(opts.agentClient);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function detectAgentClient() {
|
|
150
|
+
if (runtimeAgentClient) return runtimeAgentClient;
|
|
151
|
+
if (process.env.LLAMA_AGENT_CLIENT) return process.env.LLAMA_AGENT_CLIENT;
|
|
152
|
+
if (process.env.CODEX_SANDBOX || process.env.CODEX_CLI || process.env.OPENAI_CODEX) return "codex";
|
|
153
|
+
if (process.env.CLAUDECODE || process.env.CLAUDE_CODE || process.env.CLAUDE_CODE_ENTRYPOINT) {
|
|
154
|
+
return "claude-code";
|
|
155
|
+
}
|
|
156
|
+
if (process.env.CURSOR_AGENT || process.env.CURSOR_TRACE_ID) return "cursor";
|
|
157
|
+
return "unknown";
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function readAgentSession() {
|
|
161
|
+
try {
|
|
162
|
+
return JSON.parse(fs.readFileSync(AGENT_SESSION_FILE, "utf8"));
|
|
163
|
+
} catch {
|
|
164
|
+
return {};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function writeAgentSession(session) {
|
|
169
|
+
try {
|
|
170
|
+
fs.mkdirSync(TOKEN_DIR, { recursive: true, mode: 0o700 });
|
|
171
|
+
fs.writeFileSync(AGENT_SESSION_FILE, `${JSON.stringify(session, null, 2)}\n`, { mode: 0o600 });
|
|
172
|
+
fs.chmodSync(AGENT_SESSION_FILE, 0o600);
|
|
173
|
+
} catch {
|
|
174
|
+
// Telemetry state is best-effort. Never break the actual CLI command.
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function currentAgentSessionId() {
|
|
179
|
+
const session = readAgentSession();
|
|
180
|
+
if (session.sessionId) return session.sessionId;
|
|
181
|
+
const created = {
|
|
182
|
+
sessionId: randomUUID(),
|
|
183
|
+
createdAt: new Date().toISOString(),
|
|
184
|
+
};
|
|
185
|
+
writeAgentSession(created);
|
|
186
|
+
return created.sessionId;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function getLastAgentEvent() {
|
|
190
|
+
const session = readAgentSession();
|
|
191
|
+
return session.lastEventId ? session : null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function rememberAgentEvent(event) {
|
|
195
|
+
if (!event?.eventId) return;
|
|
196
|
+
const session = {
|
|
197
|
+
...readAgentSession(),
|
|
198
|
+
sessionId: event.sessionId || currentAgentSessionId(),
|
|
199
|
+
lastEventId: event.eventId,
|
|
200
|
+
lastCandidateId: event.candidateId ?? null,
|
|
201
|
+
lastCommand: event.command ?? null,
|
|
202
|
+
lastQuery: event.query ?? null,
|
|
203
|
+
lastSurface: event.surface ?? null,
|
|
204
|
+
lastRecordedAt: new Date().toISOString(),
|
|
205
|
+
};
|
|
206
|
+
writeAgentSession(session);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function agentClientHeaders(command) {
|
|
210
|
+
return {
|
|
211
|
+
"X-Llama-Client": runtimeClient,
|
|
212
|
+
"X-Llama-Client-Version": getPackageVersion(),
|
|
213
|
+
"X-Llama-Agent-Client": detectAgentClient(),
|
|
214
|
+
"X-Llama-Agent-Session": currentAgentSessionId(),
|
|
215
|
+
"X-Llama-Command": command || "unknown",
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const SECRET_KEY_RE = /(token|secret|password|authorization|cookie|api[_-]?key|keychain|jwt)/i;
|
|
220
|
+
|
|
221
|
+
function truncateText(text, max = 2000) {
|
|
222
|
+
return text.length > max ? `${text.slice(0, max)}...[truncated]` : text;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function sanitizeTelemetryValue(value, depth = 0) {
|
|
226
|
+
if (depth > 4) return "[max-depth]";
|
|
227
|
+
if (value === null || value === undefined) return value;
|
|
228
|
+
if (typeof value === "string") return truncateText(value);
|
|
229
|
+
if (typeof value === "number" || typeof value === "boolean") return value;
|
|
230
|
+
if (Array.isArray(value)) {
|
|
231
|
+
return value.slice(0, 20).map((item) => sanitizeTelemetryValue(item, depth + 1));
|
|
232
|
+
}
|
|
233
|
+
if (typeof value === "object") {
|
|
234
|
+
const out = {};
|
|
235
|
+
for (const [key, val] of Object.entries(value).slice(0, 40)) {
|
|
236
|
+
out[key] = SECRET_KEY_RE.test(key) ? "[redacted]" : sanitizeTelemetryValue(val, depth + 1);
|
|
237
|
+
}
|
|
238
|
+
return out;
|
|
239
|
+
}
|
|
240
|
+
return String(value);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function parseEndpoint(endpoint) {
|
|
244
|
+
try {
|
|
245
|
+
return new URL(endpoint, "https://command.llamaventures.vc");
|
|
246
|
+
} catch {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function endpointArgs(endpoint, body) {
|
|
252
|
+
const args = {};
|
|
253
|
+
const url = parseEndpoint(endpoint);
|
|
254
|
+
if (url) {
|
|
255
|
+
for (const [key, value] of url.searchParams.entries()) args[key] = value;
|
|
256
|
+
}
|
|
257
|
+
if (body && typeof body === "object" && !Array.isArray(body)) {
|
|
258
|
+
Object.assign(args, body);
|
|
259
|
+
}
|
|
260
|
+
return sanitizeTelemetryValue(args);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function inferCommand(method, endpoint) {
|
|
264
|
+
const url = parseEndpoint(endpoint);
|
|
265
|
+
const pathname = url?.pathname || endpoint.split("?")[0] || "";
|
|
266
|
+
const verb = String(method || "GET").toUpperCase();
|
|
267
|
+
if (pathname === "/api/agent/client-events") return "telemetry.record";
|
|
268
|
+
if (pathname === "/api/agent/eval-feedback") return "eval.feedback";
|
|
269
|
+
if (pathname === "/api/wiki/search") return "wiki.search";
|
|
270
|
+
if (pathname === "/api/wiki/save") return "wiki.save";
|
|
271
|
+
if (/^\/api\/wiki\/[^/]+$/.test(pathname)) return verb === "GET" ? "wiki.read" : "wiki.write";
|
|
272
|
+
if (pathname === "/api/deals") return verb === "GET" ? "deal.search" : "deal.write";
|
|
273
|
+
if (pathname === "/api/deals/create") return "deal.create";
|
|
274
|
+
if (pathname === "/api/deals/update") return "deal.update";
|
|
275
|
+
if (/^\/api\/deals\/[^/]+\/threads\/[^/]+$/.test(pathname)) return "deal.agent.run";
|
|
276
|
+
if (/^\/api\/deals\/[^/]+\/threads$/.test(pathname)) return "deal.thread.create";
|
|
277
|
+
if (/^\/api\/deals\/[^/]+\/facts/.test(pathname)) return verb === "GET" ? "deal.fact.list" : "deal.fact.write";
|
|
278
|
+
if (/^\/api\/deals\/[^/]+\/posts$/.test(pathname)) return "deal.post";
|
|
279
|
+
if (/^\/api\/deals\/[^/]+\/blocks/.test(pathname)) return verb === "GET" ? "brief.blocks" : "brief.write";
|
|
280
|
+
if (/^\/api\/deals\/[^/]+$/.test(pathname)) return verb === "GET" ? "deal.show" : "deal.write";
|
|
281
|
+
if (pathname === "/api/me") return "auth.status";
|
|
282
|
+
if (pathname.startsWith("/api/agent/skills")) return "skills.read";
|
|
283
|
+
if (pathname === "/api/agent/manifest") return "agent.bootstrap";
|
|
284
|
+
if (pathname === "/api/agent/briefing") return "agent.briefing";
|
|
285
|
+
return `${verb.toLowerCase()} ${pathname || endpoint}`;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function queryForCommand(command, args) {
|
|
289
|
+
if (!command.endsWith(".search")) return null;
|
|
290
|
+
return args?.q || args?.search || args?.query || null;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function summarizeResultIds(data) {
|
|
294
|
+
const result = {};
|
|
295
|
+
const topDeals = [];
|
|
296
|
+
const topWiki = [];
|
|
297
|
+
|
|
298
|
+
const collectDeal = (deal) => {
|
|
299
|
+
const id = deal?.uuid || deal?.id || deal?.dealId || deal?.deal_uuid;
|
|
300
|
+
if (!id) return;
|
|
301
|
+
topDeals.push({ id, name: deal.companyName || deal.company_name || deal.name || null });
|
|
302
|
+
};
|
|
303
|
+
const collectWiki = (item) => {
|
|
304
|
+
if (!item?.slug) return;
|
|
305
|
+
topWiki.push({ slug: item.slug, title: item.title || null });
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
if (Array.isArray(data)) {
|
|
309
|
+
for (const item of data.slice(0, 20)) {
|
|
310
|
+
collectDeal(item);
|
|
311
|
+
collectWiki(item);
|
|
312
|
+
}
|
|
313
|
+
result.resultCount = data.length;
|
|
314
|
+
} else if (data && typeof data === "object") {
|
|
315
|
+
const deals = Array.isArray(data.deals) ? data.deals : [];
|
|
316
|
+
const articles = Array.isArray(data.articles) ? data.articles : [];
|
|
317
|
+
const results = Array.isArray(data.results) ? data.results : [];
|
|
318
|
+
for (const deal of deals.slice(0, 20)) collectDeal(deal);
|
|
319
|
+
for (const item of [...articles, ...results].slice(0, 20)) collectWiki(item);
|
|
320
|
+
if (typeof data.total === "number") result.total = data.total;
|
|
321
|
+
if (deals.length) result.resultCount = deals.length;
|
|
322
|
+
if (articles.length || results.length) result.resultCount = articles.length + results.length;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (topDeals.length) result.deals = topDeals;
|
|
326
|
+
if (topWiki.length) result.wiki = topWiki;
|
|
327
|
+
return result;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function summarizeResult(data) {
|
|
331
|
+
if (data === null || data === undefined) return null;
|
|
332
|
+
if (Array.isArray(data)) return `${data.length} result(s)`;
|
|
333
|
+
if (typeof data === "object") {
|
|
334
|
+
if (Array.isArray(data.deals)) return `${data.deals.length} deal result(s); total=${data.total ?? "unknown"}`;
|
|
335
|
+
if (Array.isArray(data.results)) return `${data.results.length} result(s)`;
|
|
336
|
+
if (data.ok !== undefined) return `ok=${Boolean(data.ok)}`;
|
|
337
|
+
}
|
|
338
|
+
return truncateText(typeof data === "string" ? data : JSON.stringify(sanitizeTelemetryValue(data)), 2000);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function shouldSkipTelemetry(endpoint) {
|
|
342
|
+
if (process.env.LLAMA_TELEMETRY === "0") return true;
|
|
343
|
+
const pathname = parseEndpoint(endpoint)?.pathname || endpoint;
|
|
344
|
+
return pathname === "/api/agent/client-events" || pathname === "/api/agent/eval-feedback";
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async function recordClientTelemetry({
|
|
348
|
+
authHeaders,
|
|
349
|
+
method,
|
|
350
|
+
endpoint,
|
|
351
|
+
body,
|
|
352
|
+
command,
|
|
353
|
+
status,
|
|
354
|
+
httpStatus,
|
|
355
|
+
latencyMs,
|
|
356
|
+
data,
|
|
357
|
+
errorMessage,
|
|
358
|
+
}) {
|
|
359
|
+
if (shouldSkipTelemetry(endpoint)) return;
|
|
360
|
+
const args = endpointArgs(endpoint, body);
|
|
361
|
+
const sessionId = currentAgentSessionId();
|
|
362
|
+
const payload = {
|
|
363
|
+
client: runtimeClient,
|
|
364
|
+
clientVersion: getPackageVersion(),
|
|
365
|
+
agentClient: detectAgentClient(),
|
|
366
|
+
sessionId,
|
|
367
|
+
command,
|
|
368
|
+
method: String(method || "GET").toUpperCase(),
|
|
369
|
+
endpoint,
|
|
370
|
+
status,
|
|
371
|
+
httpStatus,
|
|
372
|
+
latencyMs,
|
|
373
|
+
args,
|
|
374
|
+
query: queryForCommand(command, args),
|
|
375
|
+
resultSummary: status === "success" ? summarizeResult(data) : null,
|
|
376
|
+
resultIds: status === "success" ? summarizeResultIds(data) : {},
|
|
377
|
+
errorMessage: errorMessage ? truncateText(String(errorMessage), 2000) : null,
|
|
378
|
+
};
|
|
379
|
+
try {
|
|
380
|
+
const res = await fetch(`${getBaseUrl()}/api/agent/client-events`, {
|
|
381
|
+
method: "POST",
|
|
382
|
+
headers: {
|
|
383
|
+
"Content-Type": "application/json",
|
|
384
|
+
...agentClientHeaders("telemetry.record"),
|
|
385
|
+
...authHeaders,
|
|
386
|
+
},
|
|
387
|
+
body: JSON.stringify(payload),
|
|
388
|
+
});
|
|
389
|
+
if (!res.ok) return;
|
|
390
|
+
const recorded = await res.json().catch(() => null);
|
|
391
|
+
rememberAgentEvent({
|
|
392
|
+
...recorded,
|
|
393
|
+
sessionId,
|
|
394
|
+
command,
|
|
395
|
+
query: payload.query,
|
|
396
|
+
surface: command.startsWith("deal.") ? "deal" : command.startsWith("wiki.") ? "wiki" : null,
|
|
397
|
+
});
|
|
398
|
+
} catch {
|
|
399
|
+
// Best-effort by design. The actual llama command already succeeded or
|
|
400
|
+
// failed; telemetry must never alter that outcome.
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
126
404
|
// Try `gcloud auth print-identity-token`. Returns the JWT or null. Zero-config
|
|
127
405
|
// win for any team member who has gcloud + their @llamaventures.vc account
|
|
128
406
|
// already set up — the server's Bearer auth path verifies and auto-creates
|
|
@@ -227,10 +505,13 @@ export async function requestSse(method, endpoint, body, opts = {}) {
|
|
|
227
505
|
async function requestWithRetry(method, endpoint, body, allowRetry) {
|
|
228
506
|
const authHeaders = await getAuthHeaders();
|
|
229
507
|
if (Object.keys(authHeaders).length === 0) throw noAuthError();
|
|
508
|
+
const command = inferCommand(method, endpoint);
|
|
509
|
+
const start = Date.now();
|
|
230
510
|
const res = await fetch(`${getBaseUrl()}${endpoint}`, {
|
|
231
511
|
method,
|
|
232
512
|
headers: {
|
|
233
513
|
"Content-Type": "application/json",
|
|
514
|
+
...agentClientHeaders(command),
|
|
234
515
|
...authHeaders,
|
|
235
516
|
},
|
|
236
517
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
@@ -267,18 +548,45 @@ async function requestWithRetry(method, endpoint, body, allowRetry) {
|
|
|
267
548
|
}
|
|
268
549
|
if (!res.ok) {
|
|
269
550
|
const message = typeof data === "object" && data?.error ? data.error : `HTTP ${res.status}`;
|
|
551
|
+
await recordClientTelemetry({
|
|
552
|
+
authHeaders,
|
|
553
|
+
method,
|
|
554
|
+
endpoint,
|
|
555
|
+
body,
|
|
556
|
+
command,
|
|
557
|
+
status: "error",
|
|
558
|
+
httpStatus: res.status,
|
|
559
|
+
latencyMs: Date.now() - start,
|
|
560
|
+
data: null,
|
|
561
|
+
errorMessage: message,
|
|
562
|
+
});
|
|
270
563
|
throw new Error(message);
|
|
271
564
|
}
|
|
565
|
+
await recordClientTelemetry({
|
|
566
|
+
authHeaders,
|
|
567
|
+
method,
|
|
568
|
+
endpoint,
|
|
569
|
+
body,
|
|
570
|
+
command,
|
|
571
|
+
status: "success",
|
|
572
|
+
httpStatus: res.status,
|
|
573
|
+
latencyMs: Date.now() - start,
|
|
574
|
+
data,
|
|
575
|
+
errorMessage: null,
|
|
576
|
+
});
|
|
272
577
|
return data;
|
|
273
578
|
}
|
|
274
579
|
|
|
275
580
|
async function requestSseWithRetry(method, endpoint, body, opts, allowRetry) {
|
|
276
581
|
const authHeaders = await getAuthHeaders();
|
|
277
582
|
if (Object.keys(authHeaders).length === 0) throw noAuthError();
|
|
583
|
+
const command = inferCommand(method, endpoint);
|
|
584
|
+
const start = Date.now();
|
|
278
585
|
const res = await fetch(`${getBaseUrl()}${endpoint}`, {
|
|
279
586
|
method,
|
|
280
587
|
headers: {
|
|
281
588
|
"Content-Type": "application/json",
|
|
589
|
+
...agentClientHeaders(command),
|
|
282
590
|
...authHeaders,
|
|
283
591
|
},
|
|
284
592
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
@@ -309,6 +617,18 @@ async function requestSseWithRetry(method, endpoint, body, opts, allowRetry) {
|
|
|
309
617
|
data = text;
|
|
310
618
|
}
|
|
311
619
|
const message = typeof data === "object" && data?.error ? data.error : `HTTP ${res.status}`;
|
|
620
|
+
await recordClientTelemetry({
|
|
621
|
+
authHeaders,
|
|
622
|
+
method,
|
|
623
|
+
endpoint,
|
|
624
|
+
body,
|
|
625
|
+
command,
|
|
626
|
+
status: "error",
|
|
627
|
+
httpStatus: res.status,
|
|
628
|
+
latencyMs: Date.now() - start,
|
|
629
|
+
data: null,
|
|
630
|
+
errorMessage: message,
|
|
631
|
+
});
|
|
312
632
|
throw new Error(message);
|
|
313
633
|
}
|
|
314
634
|
|
|
@@ -343,6 +663,18 @@ async function requestSseWithRetry(method, endpoint, body, opts, allowRetry) {
|
|
|
343
663
|
for (const frame of frames) handleFrame(frame);
|
|
344
664
|
}
|
|
345
665
|
if (buf.trim()) handleFrame(buf);
|
|
666
|
+
await recordClientTelemetry({
|
|
667
|
+
authHeaders,
|
|
668
|
+
method,
|
|
669
|
+
endpoint,
|
|
670
|
+
body,
|
|
671
|
+
command,
|
|
672
|
+
status: "success",
|
|
673
|
+
httpStatus: res.status,
|
|
674
|
+
latencyMs: Date.now() - start,
|
|
675
|
+
data: { ok: true, textLength: text.length, events: events.length },
|
|
676
|
+
errorMessage: null,
|
|
677
|
+
});
|
|
346
678
|
return { text, events };
|
|
347
679
|
}
|
|
348
680
|
|
package/package.json
CHANGED
|
@@ -22,6 +22,7 @@ assert.equal(
|
|
|
22
22
|
);
|
|
23
23
|
const calls = [];
|
|
24
24
|
let threadSeq = 0;
|
|
25
|
+
let eventSeq = 0;
|
|
25
26
|
|
|
26
27
|
async function readJson(req) {
|
|
27
28
|
let raw = "";
|
|
@@ -65,11 +66,47 @@ const server = createServer(async (req, res) => {
|
|
|
65
66
|
path: url.pathname,
|
|
66
67
|
query: Object.fromEntries(url.searchParams.entries()),
|
|
67
68
|
body,
|
|
69
|
+
headers: {
|
|
70
|
+
client: req.headers["x-llama-client"] ?? null,
|
|
71
|
+
clientVersion: req.headers["x-llama-client-version"] ?? null,
|
|
72
|
+
agentClient: req.headers["x-llama-agent-client"] ?? null,
|
|
73
|
+
session: req.headers["x-llama-agent-session"] ?? null,
|
|
74
|
+
command: req.headers["x-llama-command"] ?? null,
|
|
75
|
+
},
|
|
68
76
|
});
|
|
69
77
|
|
|
78
|
+
if (req.method === "POST" && url.pathname === "/api/agent/client-events") {
|
|
79
|
+
eventSeq += 1;
|
|
80
|
+
writeJson(res, {
|
|
81
|
+
ok: true,
|
|
82
|
+
eventId: eventSeq,
|
|
83
|
+
candidateId: body?.command?.endsWith(".search") ? eventSeq + 1000 : null,
|
|
84
|
+
});
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (req.method === "POST" && url.pathname === "/api/agent/eval-feedback") {
|
|
89
|
+
writeJson(res, {
|
|
90
|
+
ok: true,
|
|
91
|
+
candidate: {
|
|
92
|
+
id: 42,
|
|
93
|
+
source_event_id: body?.eventId ?? null,
|
|
94
|
+
feedback: body?.action ?? null,
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
70
100
|
if (req.method === "GET" && url.pathname === "/api/agent/manifest") {
|
|
71
101
|
writeJson(res, {
|
|
72
102
|
ok: true,
|
|
103
|
+
contract: {
|
|
104
|
+
contract_version: "agent-contract.v1",
|
|
105
|
+
cli: {
|
|
106
|
+
client_version: url.searchParams.get("clientVersion"),
|
|
107
|
+
status: "ok",
|
|
108
|
+
},
|
|
109
|
+
},
|
|
73
110
|
briefing: "runtime briefing: use skills_search, skills_read, and object_inspect",
|
|
74
111
|
llama_os: {
|
|
75
112
|
visible_skill_count: 49,
|
|
@@ -85,6 +122,21 @@ const server = createServer(async (req, res) => {
|
|
|
85
122
|
return;
|
|
86
123
|
}
|
|
87
124
|
|
|
125
|
+
if (req.method === "GET" && url.pathname === "/api/agent/briefing") {
|
|
126
|
+
writeJson(res, {
|
|
127
|
+
ok: true,
|
|
128
|
+
contract: {
|
|
129
|
+
contract_version: "agent-contract.v1",
|
|
130
|
+
cli: {
|
|
131
|
+
client_version: url.searchParams.get("clientVersion"),
|
|
132
|
+
status: "ok",
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
briefing: "server-owned briefing: check CLI, use Pipeline First, prefer CLI/MCP",
|
|
136
|
+
});
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
88
140
|
if (req.method === "GET" && url.pathname === "/api/agent/skills") {
|
|
89
141
|
writeJson(res, {
|
|
90
142
|
ok: true,
|
|
@@ -136,6 +188,16 @@ const server = createServer(async (req, res) => {
|
|
|
136
188
|
return;
|
|
137
189
|
}
|
|
138
190
|
|
|
191
|
+
if (req.method === "GET" && url.pathname === "/api/wiki/search") {
|
|
192
|
+
writeJson(res, [
|
|
193
|
+
{
|
|
194
|
+
slug: "llama-weekly-2026-06-16",
|
|
195
|
+
title: "Llama Weekly 2026-06-16",
|
|
196
|
+
},
|
|
197
|
+
]);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
139
201
|
if (req.method === "POST" && /^\/api\/deals\/[^/]+\/threads$/.test(url.pathname)) {
|
|
140
202
|
threadSeq += 1;
|
|
141
203
|
writeJson(res, { id: `thread-${threadSeq}` });
|
|
@@ -191,25 +253,34 @@ function resetCalls() {
|
|
|
191
253
|
threadSeq = 0;
|
|
192
254
|
}
|
|
193
255
|
|
|
256
|
+
function businessCalls() {
|
|
257
|
+
return calls.filter((call) => call.path !== "/api/agent/client-events");
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function telemetryCalls() {
|
|
261
|
+
return calls.filter((call) => call.path === "/api/agent/client-events");
|
|
262
|
+
}
|
|
263
|
+
|
|
194
264
|
function paths() {
|
|
195
|
-
return
|
|
265
|
+
return businessCalls().map((call) => `${call.method} ${call.path}`);
|
|
196
266
|
}
|
|
197
267
|
|
|
198
268
|
function assertNoEnrichCall() {
|
|
199
269
|
assert.equal(
|
|
200
|
-
|
|
270
|
+
businessCalls().some((call) => call.path.endsWith("/enrich")),
|
|
201
271
|
false,
|
|
202
272
|
`expected no /enrich call, got ${paths().join(", ")}`,
|
|
203
273
|
);
|
|
204
274
|
}
|
|
205
275
|
|
|
206
276
|
function assertThreadRun({ title, messageIncludes }) {
|
|
207
|
-
|
|
208
|
-
assert.
|
|
209
|
-
assert.
|
|
210
|
-
assert.
|
|
277
|
+
const relevant = businessCalls();
|
|
278
|
+
assert.equal(relevant.length, 2, `expected thread create + SSE run, got ${paths().join(", ")}`);
|
|
279
|
+
assert.match(relevant[0].path, /^\/api\/deals\/[^/]+\/threads$/);
|
|
280
|
+
assert.equal(relevant[0].body?.title, title);
|
|
281
|
+
assert.match(relevant[1].path, /^\/api\/deals\/[^/]+\/threads\/thread-1$/);
|
|
211
282
|
for (const needle of messageIncludes) {
|
|
212
|
-
assert.match(
|
|
283
|
+
assert.match(relevant[1].body?.message ?? "", new RegExp(escapeRegExp(needle)));
|
|
213
284
|
}
|
|
214
285
|
}
|
|
215
286
|
|
|
@@ -313,18 +384,29 @@ const baseUrl = `http://${address.address}:${address.port}`;
|
|
|
313
384
|
const homeDir = await mkdtemp(path.join(os.tmpdir(), "llama-cli-routing-"));
|
|
314
385
|
|
|
315
386
|
try {
|
|
387
|
+
resetCalls();
|
|
388
|
+
const onboardRun = await runCli(["agent-onboard"], baseUrl, homeDir);
|
|
389
|
+
assert.match(onboardRun.stdout, /server-owned briefing/);
|
|
390
|
+
assert.deepEqual(paths(), ["GET /api/agent/briefing"]);
|
|
391
|
+
assert.ok(businessCalls()[0].query.clientVersion, "agent-onboard passes clientVersion");
|
|
392
|
+
assert.equal(telemetryCalls()[0].body?.command, "agent.briefing");
|
|
393
|
+
assert.equal(telemetryCalls()[0].body?.client, "cli");
|
|
394
|
+
assert.ok(telemetryCalls()[0].body?.sessionId, "telemetry includes an agent session id");
|
|
395
|
+
assert.equal(businessCalls()[0].headers.command, "agent.briefing");
|
|
396
|
+
|
|
316
397
|
resetCalls();
|
|
317
398
|
const bootstrapRun = await runCli(["agent", "bootstrap", "--limit", "3"], baseUrl, homeDir);
|
|
318
399
|
assert.match(bootstrapRun.stdout, /runtime briefing/);
|
|
319
400
|
assert.deepEqual(paths(), ["GET /api/agent/manifest"]);
|
|
320
|
-
assert.equal(
|
|
401
|
+
assert.equal(businessCalls()[0].query.limit, "3");
|
|
402
|
+
assert.ok(businessCalls()[0].query.clientVersion, "agent bootstrap passes clientVersion");
|
|
321
403
|
|
|
322
404
|
resetCalls();
|
|
323
405
|
const skillSearchRun = await runCli(["skills", "search", "pipeline", "--limit", "5"], baseUrl, homeDir);
|
|
324
406
|
assert.match(skillSearchRun.stdout, /llama-command/);
|
|
325
407
|
assert.deepEqual(paths(), ["GET /api/agent/skills"]);
|
|
326
|
-
assert.equal(
|
|
327
|
-
assert.equal(
|
|
408
|
+
assert.equal(businessCalls()[0].query.q, "pipeline");
|
|
409
|
+
assert.equal(businessCalls()[0].query.limit, "5");
|
|
328
410
|
|
|
329
411
|
resetCalls();
|
|
330
412
|
const skillShowRun = await runCli(["skills", "show", "llama-command"], baseUrl, homeDir);
|
|
@@ -336,7 +418,34 @@ try {
|
|
|
336
418
|
assert.match(explainRun.stdout, /Status: deleted/);
|
|
337
419
|
assert.match(explainRun.stdout, /Deleted by Kevin Yu/);
|
|
338
420
|
assert.deepEqual(paths(), ["GET /api/agent/explain"]);
|
|
339
|
-
assert.equal(
|
|
421
|
+
assert.equal(businessCalls()[0].query.q, "https://command.llamaventures.vc/wiki/missing-page");
|
|
422
|
+
|
|
423
|
+
resetCalls();
|
|
424
|
+
const wikiRun = await runCli(["wiki", "search", "llama weekly"], baseUrl, homeDir);
|
|
425
|
+
assert.match(wikiRun.stdout, /llama-weekly-2026-06-16/);
|
|
426
|
+
assert.deepEqual(paths(), ["GET /api/wiki/search"]);
|
|
427
|
+
assert.equal(telemetryCalls()[0].body?.command, "wiki.search");
|
|
428
|
+
assert.equal(telemetryCalls()[0].body?.query, "llama weekly");
|
|
429
|
+
|
|
430
|
+
resetCalls();
|
|
431
|
+
const evalRun = await runCli(
|
|
432
|
+
[
|
|
433
|
+
"eval",
|
|
434
|
+
"bad",
|
|
435
|
+
"--last",
|
|
436
|
+
"--reason",
|
|
437
|
+
"missed dev weekly",
|
|
438
|
+
"--expect",
|
|
439
|
+
"wiki:llamaos-weekly-2026-06-17",
|
|
440
|
+
],
|
|
441
|
+
baseUrl,
|
|
442
|
+
homeDir,
|
|
443
|
+
);
|
|
444
|
+
assert.match(evalRun.stdout, /"feedback": "bad"/);
|
|
445
|
+
assert.deepEqual(paths(), ["POST /api/agent/eval-feedback"]);
|
|
446
|
+
assert.equal(businessCalls()[0].body?.action, "bad");
|
|
447
|
+
assert.equal(businessCalls()[0].body?.eventId, 6);
|
|
448
|
+
assert.equal(businessCalls()[0].body?.expected?.wikiSlugs?.[0], "llamaos-weekly-2026-06-17");
|
|
340
449
|
|
|
341
450
|
resetCalls();
|
|
342
451
|
const enrichRun = await runCli(
|
|
@@ -369,9 +478,9 @@ try {
|
|
|
369
478
|
homeDir,
|
|
370
479
|
);
|
|
371
480
|
assert.deepEqual(paths(), ["POST /api/deals/deal-cli/enrich"]);
|
|
372
|
-
assert.equal(
|
|
373
|
-
assert.equal(
|
|
374
|
-
assert.equal(
|
|
481
|
+
assert.equal(businessCalls()[0].body?.apply, true);
|
|
482
|
+
assert.equal(businessCalls()[0].body?.dryRun, false);
|
|
483
|
+
assert.equal(businessCalls()[0].body?.executor, "server_agent");
|
|
375
484
|
|
|
376
485
|
resetCalls();
|
|
377
486
|
const agentRun = await runCli(
|
|
@@ -414,14 +523,16 @@ try {
|
|
|
414
523
|
const bootstrapPayload = JSON.parse(mcpBootstrap.content?.[0]?.text ?? "{}");
|
|
415
524
|
assert.equal(bootstrapPayload.ok, true);
|
|
416
525
|
assert.deepEqual(paths(), ["GET /api/agent/manifest"]);
|
|
417
|
-
assert.equal(
|
|
526
|
+
assert.equal(businessCalls()[0].query.limit, "2");
|
|
527
|
+
assert.ok(businessCalls()[0].query.clientVersion, "mcp agent_bootstrap passes clientVersion");
|
|
528
|
+
assert.equal(telemetryCalls()[0].body?.client, "mcp");
|
|
418
529
|
|
|
419
530
|
resetCalls();
|
|
420
531
|
const mcpSkills = await callMcpTool("skills_search", { q: "command", limit: 4 }, baseUrl, homeDir);
|
|
421
532
|
const skillsPayload = JSON.parse(mcpSkills.content?.[0]?.text ?? "{}");
|
|
422
533
|
assert.equal(skillsPayload.skills?.[0]?.slug, "llama-command");
|
|
423
534
|
assert.deepEqual(paths(), ["GET /api/agent/skills"]);
|
|
424
|
-
assert.equal(
|
|
535
|
+
assert.equal(businessCalls()[0].query.q, "command");
|
|
425
536
|
|
|
426
537
|
resetCalls();
|
|
427
538
|
const mcpSkillRead = await callMcpTool("skills_read", { slug: "llama-command" }, baseUrl, homeDir);
|