@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/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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llamaventures/cli",
3
- "version": "1.15.0",
3
+ "version": "1.16.0",
4
4
  "description": "CLI + MCP server for the Llama Ventures investment workbench (command.llamaventures.vc).",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -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 calls.map((call) => `${call.method} ${call.path}`);
265
+ return businessCalls().map((call) => `${call.method} ${call.path}`);
196
266
  }
197
267
 
198
268
  function assertNoEnrichCall() {
199
269
  assert.equal(
200
- calls.some((call) => call.path.endsWith("/enrich")),
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
- assert.equal(calls.length, 2, `expected thread create + SSE run, got ${paths().join(", ")}`);
208
- assert.match(calls[0].path, /^\/api\/deals\/[^/]+\/threads$/);
209
- assert.equal(calls[0].body?.title, title);
210
- assert.match(calls[1].path, /^\/api\/deals\/[^/]+\/threads\/thread-1$/);
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(calls[1].body?.message ?? "", new RegExp(escapeRegExp(needle)));
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(calls[0].query.limit, "3");
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(calls[0].query.q, "pipeline");
327
- assert.equal(calls[0].query.limit, "5");
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(calls[0].query.q, "https://command.llamaventures.vc/wiki/missing-page");
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(calls[0].body?.apply, true);
373
- assert.equal(calls[0].body?.dryRun, false);
374
- assert.equal(calls[0].body?.executor, "server_agent");
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(calls[0].query.limit, "2");
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(calls[0].query.q, "command");
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);