@llamaventures/cli 1.15.1 → 1.17.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 { createHash, 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,285 @@ 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
+ const CONTENT_PAYLOAD_KEY_RE = /(^|_)(html|body|content|markdown|message|text)$/i;
221
+
222
+ function truncateText(text, max = 2000) {
223
+ return text.length > max ? `${text.slice(0, max)}...[truncated]` : text;
224
+ }
225
+
226
+ function summarizePayloadText(value) {
227
+ const text = String(value ?? "");
228
+ return {
229
+ redacted: true,
230
+ type: "text_payload",
231
+ chars: text.length,
232
+ bytes: Buffer.byteLength(text, "utf8"),
233
+ sha256: createHash("sha256").update(text).digest("hex"),
234
+ };
235
+ }
236
+
237
+ function sanitizeTelemetryValue(value, depth = 0, keyHint = "") {
238
+ if (depth > 4) return "[max-depth]";
239
+ if (value === null || value === undefined) return value;
240
+ if (typeof value === "string") {
241
+ return CONTENT_PAYLOAD_KEY_RE.test(keyHint)
242
+ ? summarizePayloadText(value)
243
+ : truncateText(value);
244
+ }
245
+ if (typeof value === "number" || typeof value === "boolean") return value;
246
+ if (Array.isArray(value)) {
247
+ return value.slice(0, 20).map((item) => sanitizeTelemetryValue(item, depth + 1, keyHint));
248
+ }
249
+ if (typeof value === "object") {
250
+ const out = {};
251
+ for (const [key, val] of Object.entries(value).slice(0, 40)) {
252
+ out[key] = SECRET_KEY_RE.test(key) ? "[redacted]" : sanitizeTelemetryValue(val, depth + 1, key);
253
+ }
254
+ return out;
255
+ }
256
+ return String(value);
257
+ }
258
+
259
+ function parseEndpoint(endpoint) {
260
+ try {
261
+ return new URL(endpoint, "https://command.llamaventures.vc");
262
+ } catch {
263
+ return null;
264
+ }
265
+ }
266
+
267
+ function endpointArgs(endpoint, body) {
268
+ const args = {};
269
+ const url = parseEndpoint(endpoint);
270
+ if (url) {
271
+ for (const [key, value] of url.searchParams.entries()) args[key] = value;
272
+ }
273
+ if (body && typeof body === "object" && !Array.isArray(body)) {
274
+ Object.assign(args, body);
275
+ }
276
+ return sanitizeTelemetryValue(args);
277
+ }
278
+
279
+ function inferCommand(method, endpoint) {
280
+ const url = parseEndpoint(endpoint);
281
+ const pathname = url?.pathname || endpoint.split("?")[0] || "";
282
+ const verb = String(method || "GET").toUpperCase();
283
+ if (pathname === "/api/agent/client-events") return "telemetry.record";
284
+ if (pathname === "/api/agent/eval-feedback") return "eval.feedback";
285
+ if (pathname === "/api/wiki/search") return "wiki.search";
286
+ if (pathname === "/api/wiki/save") return "wiki.save";
287
+ if (/^\/api\/wiki\/[^/]+$/.test(pathname)) return verb === "GET" ? "wiki.read" : "wiki.write";
288
+ if (pathname === "/api/deals") return verb === "GET" ? "deal.search" : "deal.write";
289
+ if (pathname === "/api/deals/create") return "deal.create";
290
+ if (pathname === "/api/deals/update") return "deal.update";
291
+ if (/^\/api\/deals\/[^/]+\/threads\/[^/]+$/.test(pathname)) return "deal.agent.run";
292
+ if (/^\/api\/deals\/[^/]+\/threads$/.test(pathname)) return "deal.thread.create";
293
+ if (/^\/api\/deals\/[^/]+\/facts/.test(pathname)) return verb === "GET" ? "deal.fact.list" : "deal.fact.write";
294
+ if (/^\/api\/deals\/[^/]+\/posts$/.test(pathname)) return "deal.post";
295
+ if (/^\/api\/deals\/[^/]+\/blocks/.test(pathname)) return verb === "GET" ? "brief.blocks" : "brief.write";
296
+ if (/^\/api\/deals\/[^/]+$/.test(pathname)) return verb === "GET" ? "deal.show" : "deal.write";
297
+ if (pathname === "/api/me") return "auth.status";
298
+ if (pathname.startsWith("/api/agent/skills")) return "skills.read";
299
+ if (pathname === "/api/agent/manifest") return "agent.bootstrap";
300
+ if (pathname === "/api/agent/briefing") return "agent.briefing";
301
+ return `${verb.toLowerCase()} ${pathname || endpoint}`;
302
+ }
303
+
304
+ function queryForCommand(command, args) {
305
+ if (!command.endsWith(".search")) return null;
306
+ return args?.q || args?.search || args?.query || null;
307
+ }
308
+
309
+ function summarizeResultIds(data) {
310
+ const result = {};
311
+ const topDeals = [];
312
+ const topWiki = [];
313
+
314
+ const collectDeal = (deal) => {
315
+ const id = deal?.uuid || deal?.id || deal?.dealId || deal?.deal_uuid;
316
+ if (!id) return;
317
+ topDeals.push({ id, name: deal.companyName || deal.company_name || deal.name || null });
318
+ };
319
+ const collectWiki = (item) => {
320
+ if (!item?.slug) return;
321
+ topWiki.push({ slug: item.slug, title: item.title || null });
322
+ };
323
+
324
+ if (Array.isArray(data)) {
325
+ for (const item of data.slice(0, 20)) {
326
+ collectDeal(item);
327
+ collectWiki(item);
328
+ }
329
+ result.resultCount = data.length;
330
+ } else if (data && typeof data === "object") {
331
+ const deals = Array.isArray(data.deals) ? data.deals : [];
332
+ const articles = Array.isArray(data.articles) ? data.articles : [];
333
+ const results = Array.isArray(data.results) ? data.results : [];
334
+ for (const deal of deals.slice(0, 20)) collectDeal(deal);
335
+ for (const item of [...articles, ...results].slice(0, 20)) collectWiki(item);
336
+ if (typeof data.total === "number") result.total = data.total;
337
+ if (deals.length) result.resultCount = deals.length;
338
+ if (articles.length || results.length) result.resultCount = articles.length + results.length;
339
+ }
340
+
341
+ if (topDeals.length) result.deals = topDeals;
342
+ if (topWiki.length) result.wiki = topWiki;
343
+ return result;
344
+ }
345
+
346
+ function summarizeResult(data) {
347
+ if (data === null || data === undefined) return null;
348
+ if (Array.isArray(data)) return `${data.length} result(s)`;
349
+ if (typeof data === "object") {
350
+ if (Array.isArray(data.deals)) return `${data.deals.length} deal result(s); total=${data.total ?? "unknown"}`;
351
+ if (Array.isArray(data.results)) return `${data.results.length} result(s)`;
352
+ if (data.ok !== undefined) return `ok=${Boolean(data.ok)}`;
353
+ }
354
+ return truncateText(typeof data === "string" ? data : JSON.stringify(sanitizeTelemetryValue(data)), 2000);
355
+ }
356
+
357
+ function shouldSkipTelemetry(endpoint) {
358
+ if (process.env.LLAMA_TELEMETRY === "0") return true;
359
+ const pathname = parseEndpoint(endpoint)?.pathname || endpoint;
360
+ return pathname === "/api/agent/client-events" || pathname === "/api/agent/eval-feedback";
361
+ }
362
+
363
+ async function recordClientTelemetry({
364
+ authHeaders,
365
+ method,
366
+ endpoint,
367
+ body,
368
+ command,
369
+ status,
370
+ httpStatus,
371
+ latencyMs,
372
+ data,
373
+ errorMessage,
374
+ }) {
375
+ if (shouldSkipTelemetry(endpoint)) return;
376
+ const args = endpointArgs(endpoint, body);
377
+ const sessionId = currentAgentSessionId();
378
+ const payload = {
379
+ client: runtimeClient,
380
+ clientVersion: getPackageVersion(),
381
+ agentClient: detectAgentClient(),
382
+ sessionId,
383
+ command,
384
+ method: String(method || "GET").toUpperCase(),
385
+ endpoint,
386
+ status,
387
+ httpStatus,
388
+ latencyMs,
389
+ args,
390
+ query: queryForCommand(command, args),
391
+ resultSummary: status === "success" ? summarizeResult(data) : null,
392
+ resultIds: status === "success" ? summarizeResultIds(data) : {},
393
+ errorMessage: errorMessage ? truncateText(String(errorMessage), 2000) : null,
394
+ };
395
+ try {
396
+ const res = await fetch(`${getBaseUrl()}/api/agent/client-events`, {
397
+ method: "POST",
398
+ headers: {
399
+ "Content-Type": "application/json",
400
+ ...agentClientHeaders("telemetry.record"),
401
+ ...authHeaders,
402
+ },
403
+ body: JSON.stringify(payload),
404
+ });
405
+ if (!res.ok) return;
406
+ const recorded = await res.json().catch(() => null);
407
+ rememberAgentEvent({
408
+ ...recorded,
409
+ sessionId,
410
+ command,
411
+ query: payload.query,
412
+ surface: command.startsWith("deal.") ? "deal" : command.startsWith("wiki.") ? "wiki" : null,
413
+ });
414
+ } catch {
415
+ // Best-effort by design. The actual llama command already succeeded or
416
+ // failed; telemetry must never alter that outcome.
417
+ }
418
+ }
419
+
126
420
  // Try `gcloud auth print-identity-token`. Returns the JWT or null. Zero-config
127
421
  // win for any team member who has gcloud + their @llamaventures.vc account
128
422
  // already set up — the server's Bearer auth path verifies and auto-creates
@@ -216,22 +510,26 @@ function unauthorizedError() {
216
510
  );
217
511
  }
218
512
 
219
- export async function request(method, endpoint, body) {
220
- return requestWithRetry(method, endpoint, body, /* allowRetry */ true);
513
+ export async function request(method, endpoint, body, opts = {}) {
514
+ return requestWithRetry(method, endpoint, body, opts, /* allowRetry */ true);
221
515
  }
222
516
 
223
517
  export async function requestSse(method, endpoint, body, opts = {}) {
224
518
  return requestSseWithRetry(method, endpoint, body, opts, /* allowRetry */ true);
225
519
  }
226
520
 
227
- async function requestWithRetry(method, endpoint, body, allowRetry) {
521
+ async function requestWithRetry(method, endpoint, body, opts, allowRetry) {
228
522
  const authHeaders = await getAuthHeaders();
229
523
  if (Object.keys(authHeaders).length === 0) throw noAuthError();
524
+ const command = inferCommand(method, endpoint);
525
+ const start = Date.now();
230
526
  const res = await fetch(`${getBaseUrl()}${endpoint}`, {
231
527
  method,
232
528
  headers: {
233
529
  "Content-Type": "application/json",
530
+ ...agentClientHeaders(command),
234
531
  ...authHeaders,
532
+ ...(opts.headers || {}),
235
533
  },
236
534
  body: body === undefined ? undefined : JSON.stringify(body),
237
535
  });
@@ -251,7 +549,7 @@ async function requestWithRetry(method, endpoint, body, allowRetry) {
251
549
  refreshed = null;
252
550
  }
253
551
  if (refreshed) {
254
- return requestWithRetry(method, endpoint, body, /* allowRetry */ false);
552
+ return requestWithRetry(method, endpoint, body, opts, /* allowRetry */ false);
255
553
  }
256
554
  throw unauthorizedError();
257
555
  }
@@ -267,18 +565,45 @@ async function requestWithRetry(method, endpoint, body, allowRetry) {
267
565
  }
268
566
  if (!res.ok) {
269
567
  const message = typeof data === "object" && data?.error ? data.error : `HTTP ${res.status}`;
568
+ await recordClientTelemetry({
569
+ authHeaders,
570
+ method,
571
+ endpoint,
572
+ body,
573
+ command,
574
+ status: "error",
575
+ httpStatus: res.status,
576
+ latencyMs: Date.now() - start,
577
+ data: null,
578
+ errorMessage: message,
579
+ });
270
580
  throw new Error(message);
271
581
  }
582
+ await recordClientTelemetry({
583
+ authHeaders,
584
+ method,
585
+ endpoint,
586
+ body,
587
+ command,
588
+ status: "success",
589
+ httpStatus: res.status,
590
+ latencyMs: Date.now() - start,
591
+ data,
592
+ errorMessage: null,
593
+ });
272
594
  return data;
273
595
  }
274
596
 
275
597
  async function requestSseWithRetry(method, endpoint, body, opts, allowRetry) {
276
598
  const authHeaders = await getAuthHeaders();
277
599
  if (Object.keys(authHeaders).length === 0) throw noAuthError();
600
+ const command = inferCommand(method, endpoint);
601
+ const start = Date.now();
278
602
  const res = await fetch(`${getBaseUrl()}${endpoint}`, {
279
603
  method,
280
604
  headers: {
281
605
  "Content-Type": "application/json",
606
+ ...agentClientHeaders(command),
282
607
  ...authHeaders,
283
608
  },
284
609
  body: body === undefined ? undefined : JSON.stringify(body),
@@ -309,6 +634,18 @@ async function requestSseWithRetry(method, endpoint, body, opts, allowRetry) {
309
634
  data = text;
310
635
  }
311
636
  const message = typeof data === "object" && data?.error ? data.error : `HTTP ${res.status}`;
637
+ await recordClientTelemetry({
638
+ authHeaders,
639
+ method,
640
+ endpoint,
641
+ body,
642
+ command,
643
+ status: "error",
644
+ httpStatus: res.status,
645
+ latencyMs: Date.now() - start,
646
+ data: null,
647
+ errorMessage: message,
648
+ });
312
649
  throw new Error(message);
313
650
  }
314
651
 
@@ -343,6 +680,18 @@ async function requestSseWithRetry(method, endpoint, body, opts, allowRetry) {
343
680
  for (const frame of frames) handleFrame(frame);
344
681
  }
345
682
  if (buf.trim()) handleFrame(buf);
683
+ await recordClientTelemetry({
684
+ authHeaders,
685
+ method,
686
+ endpoint,
687
+ body,
688
+ command,
689
+ status: "success",
690
+ httpStatus: res.status,
691
+ latencyMs: Date.now() - start,
692
+ data: { ok: true, textLength: text.length, events: events.length },
693
+ errorMessage: null,
694
+ });
346
695
  return { text, events };
347
696
  }
348
697
 
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@llamaventures/cli",
3
- "version": "1.15.1",
3
+ "version": "1.17.0",
4
4
  "description": "CLI + MCP server for the Llama Ventures investment workbench (command.llamaventures.vc).",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "test": "npm run test:agent-routing",
8
- "test:agent-routing": "node scripts/verify-agent-routing.mjs"
8
+ "test:agent-routing": "node scripts/verify-agent-routing.mjs",
9
+ "verify:release": "npm test && npm pack --dry-run"
9
10
  },
10
11
  "bin": {
11
12
  "llama": "bin/llama.mjs",