@llamaventures/cli 1.3.0 → 1.4.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 CHANGED
@@ -139,7 +139,6 @@ Tools available:
139
139
  - `timeline` / `post`
140
140
  - `mentions_list`
141
141
  - `pitch_start` / `pitch_send_message` / `pitch_upload_file` / `pitch_status` / `pitch_finalize` — public intake (no Llama token needed; for founders / EAs / external agents)
142
- - `llama_api` — escape hatch for any endpoint not yet wrapped (path must start `/api/`)
143
142
 
144
143
  You can also fetch this exact briefing as an MCP prompt named `agent_briefing`.
145
144
 
package/README.md CHANGED
@@ -342,9 +342,8 @@ llama pitch upload ./deck.pdf
342
342
  llama pitch # interactive REPL
343
343
  ```
344
344
 
345
- Server-enforced caps (same as the web flow): 5 sessions/IP/day,
346
- 3 sessions/email/day, 30 min idle timeout, 100 messages/session,
347
- 1 M tokens/session.
345
+ Server-enforced rate limits apply (per-IP, per-email, per-session). If you
346
+ hit a limit, the CLI surfaces the server's response message.
348
347
 
349
348
  This is genuine **agent-to-agent**: your AI helps you tell the story, our
350
349
  intake agent extracts the structured fields and produces the verdict.
package/bin/llama-mcp.mjs CHANGED
@@ -364,9 +364,8 @@ server.registerTool(
364
364
  // founder's agent talks to ours, structured intake gets captured, and a
365
365
  // 12-dimension verdict is returned.
366
366
  //
367
- // Anti-abuse caps are server-enforced (5 sessions/IP/day, 3/email/day,
368
- // 30min idle, 100 msg cap, 1M token cap, global daily cap). The MCP tools
369
- // surface those rejections as text back to the agent.
367
+ // Anti-abuse rate limits are server-enforced. The MCP tools surface
368
+ // any server-side rejections as text back to the agent.
370
369
 
371
370
  function asTextResult(text, isError = false) {
372
371
  return {
@@ -383,8 +382,8 @@ server.registerTool(
383
382
  "when a founder (the user) wants to pitch their company to Llama. " +
384
383
  "Requires their name + email. Returns a session_id; the conversation " +
385
384
  "is then maintained via pitch_send_message until the agent finalizes. " +
386
- "Caps (server-enforced): 5 sessions/IP/day, 3 sessions/email/day, " +
387
- "30min idle timeout. No Llama Command token needed.",
385
+ "Server-enforced rate limits apply (per-IP, per-email, per-session). " +
386
+ "No Llama Command token needed.",
388
387
  inputSchema: {
389
388
  name: z.string().describe("the founder's full name (max 100 chars)"),
390
389
  email: z.string().describe("the founder's email (deliverable, not a disposable domain)"),
@@ -447,8 +446,9 @@ server.registerTool(
447
446
  description:
448
447
  "Attach a file (deck, one-pager, deck PDF, screenshot, etc.) to the " +
449
448
  "active pitch session. Server allows pdf / pptx / ppt / docx / doc / " +
450
- "xlsx / xls / png / jpg / webp / heic / heif / txt / md, max 50 MB, " +
451
- "10 files per session. Returns a drive_file_id; the intake agent will " +
449
+ "xlsx / xls / png / jpg / webp / heic / heif / txt / md, with " +
450
+ "server-enforced size and per-session count limits. " +
451
+ "Returns a drive_file_id; the intake agent will " +
452
452
  "pick the file up via list_uploaded_files / read_uploaded_file on its " +
453
453
  "next turn (so call pitch_send_message with a one-line note like " +
454
454
  "'I just uploaded our pitch deck' so the agent knows to look).",
@@ -493,7 +493,7 @@ server.registerTool(
493
493
  "server-side intake agent to finalize — the agent decides that on its " +
494
494
  "own once the pitch is sufficient. Use this for cleanup after a session " +
495
495
  "ends, or to abandon a session early. The server-side session will " +
496
- "naturally expire after 30min of idle.",
496
+ "naturally expire after the server's idle timeout.",
497
497
  inputSchema: {},
498
498
  },
499
499
  async () => {
@@ -505,7 +505,7 @@ server.registerTool(
505
505
  {
506
506
  cleared: before.active,
507
507
  previous_session: before.active ? before : null,
508
- note: "Local pitch session state cleared. Server-side session may still be active for ~30min until idle timeout.",
508
+ note: "Local pitch session state cleared. Server-side session may still be active until its idle timeout.",
509
509
  },
510
510
  null,
511
511
  2
@@ -517,6 +517,92 @@ server.registerTool(
517
517
  }
518
518
  );
519
519
 
520
+ // ============================================================
521
+ // Memo — long-form HTML investment memo (the Memo tab in the UI)
522
+ // ============================================================
523
+
524
+ server.registerTool(
525
+ "memo_show",
526
+ {
527
+ description:
528
+ "Fetch the current memo for a deal. Returns the envelope: memo " +
529
+ "(html, version, source, updated_by, updated_at), mode " +
530
+ "('composed' = server-generated, 'override' = hand-written), and " +
531
+ "inflight (if a server-side regeneration is in progress). html " +
532
+ "can be 50-100KB — be deliberate about including it in your reply.",
533
+ inputSchema: {
534
+ dealId: z.string().describe("deal uuid"),
535
+ },
536
+ },
537
+ async ({ dealId }) =>
538
+ callApi("GET", `/api/deals/${encodeURIComponent(dealId)}/memo`)
539
+ );
540
+
541
+ server.registerTool(
542
+ "memo_regenerate",
543
+ {
544
+ description:
545
+ "Trigger server-side regeneration of the deal memo. Synchronous: " +
546
+ "returns the final result (version, model, duration_ms, degraded) " +
547
+ "once the composer finishes. Typical duration 2-3 minutes. Use " +
548
+ "tier='opus' for high-stakes deals (higher cost, deeper analysis).",
549
+ inputSchema: {
550
+ dealId: z.string().describe("deal uuid"),
551
+ tier: z
552
+ .enum(["sonnet", "opus"])
553
+ .optional()
554
+ .describe("LLM tier (default: sonnet)"),
555
+ },
556
+ },
557
+ async ({ dealId, tier }) =>
558
+ callApi("POST", `/api/deals/${encodeURIComponent(dealId)}/memo`, {
559
+ action: "regenerate",
560
+ stream: false,
561
+ model: tier ?? "sonnet",
562
+ })
563
+ );
564
+
565
+ server.registerTool(
566
+ "memo_save",
567
+ {
568
+ description:
569
+ "Save hand-written HTML as a manual override for a deal's memo. " +
570
+ "Manual overrides take precedence over auto-composed memos on " +
571
+ "read. Pass the full HTML document including <!DOCTYPE html>, " +
572
+ "<style>, and <body> — it's rendered as-is in a sandboxed iframe.",
573
+ inputSchema: {
574
+ dealId: z.string().describe("deal uuid"),
575
+ html: z
576
+ .string()
577
+ .describe("full HTML document"),
578
+ },
579
+ },
580
+ async ({ dealId, html }) =>
581
+ callApi("PUT", `/api/deals/${encodeURIComponent(dealId)}/memo`, { html })
582
+ );
583
+
584
+ server.registerTool(
585
+ "memo_reset",
586
+ {
587
+ description:
588
+ "Reset memo state. Default drops only the manual override row " +
589
+ "(next read falls back to the auto-composed version, if any). " +
590
+ "Pass scope='all' to drop every version for the deal — destructive, " +
591
+ "use sparingly.",
592
+ inputSchema: {
593
+ dealId: z.string().describe("deal uuid"),
594
+ scope: z
595
+ .enum(["override_only", "all"])
596
+ .optional()
597
+ .describe("default: override_only"),
598
+ },
599
+ },
600
+ async ({ dealId, scope }) =>
601
+ callApi("DELETE", `/api/deals/${encodeURIComponent(dealId)}/memo`, {
602
+ scope: scope ?? "override_only",
603
+ })
604
+ );
605
+
520
606
  // ============================================================
521
607
  // Prompts
522
608
  // ============================================================
package/bin/llama.mjs CHANGED
@@ -242,7 +242,7 @@ Skill corrections (persona-owner pushback — read by persona-watcher):
242
242
  llama skill-correction add <skill-slug> "<correction text>" [--deal <uuid>] [--block <blockId>]
243
243
  llama skill-correction delete <id>
244
244
  Server enforces persona owner OR system admin on POST/DELETE; GET is open.
245
- External personas (owner_email=null, e.g. virtual-liu-yi) are admin-only for write.
245
+ External personas (owner_email=null) are admin-only for write.
246
246
 
247
247
  Mentions / Inbox:
248
248
  llama mentions # default: my unresolved cues
@@ -256,6 +256,12 @@ Wiki:
256
256
  llama wiki read <slug>
257
257
  llama wiki save <slug> --title "..." --content "..." --sources "url1;url2" [--type company] [--related "A;B"]
258
258
 
259
+ Memo (long-form HTML investment memo — Memo tab in the UI):
260
+ llama memo show <dealId> [--out <path>] [--json] # default: html → stdout (pipeable to file / browser)
261
+ llama memo regenerate <dealId> [--opus] # streams panel progress to stderr; result version → stdout
262
+ llama memo save <dealId> --file <path> # paste a hand-written HTML as manual override
263
+ llama memo reset <dealId> [--all] # default drops manual override; --all drops every version
264
+
259
265
  Admin (system admin only — server returns 403 for non-admin tokens):
260
266
  llama admin auth-events [--kind X] [--actor email] [--subject email] [--since 24h|7d|30d|<ISO>] [--limit 100]
261
267
  llama admin deal-events [--kind X] [--actor email] [--deal <uuid>] [--since 24h] [--limit 100]
@@ -318,9 +324,9 @@ Inspect / clean up:
318
324
  llama pitch status # session id, idle minutes, finalized?
319
325
  llama pitch end # clear local session state
320
326
 
321
- Caps (server-enforced):
322
- 5 sessions per IP per day, 3 per email per day, 60min idle timeout,
323
- 100 messages per session, 1M tokens per session.
327
+ Caps:
328
+ Server-enforced per-IP / per-email / per-session rate limits apply.
329
+ The CLI surfaces server messages if a limit is hit.
324
330
 
325
331
  Environment:
326
332
  LLAMA_API_URL override base URL (dev: http://localhost:3000)
@@ -411,7 +417,7 @@ Environment:
411
417
  cleared: !!had,
412
418
  session_file: EXTERNAL_SESSION_FILE,
413
419
  note: had
414
- ? "Local session state cleared. Server-side session may still be active until idle timeout (60min)."
420
+ ? "Local session state cleared. Server-side session may still be active until idle timeout."
415
421
  : "No local session was active.",
416
422
  });
417
423
  return;
@@ -1588,6 +1594,198 @@ https://command.llamaventures.vc/settings/tokens, run
1588
1594
  );
1589
1595
  }
1590
1596
 
1597
+ // ----- Memo (long-form HTML investment memo) -----
1598
+ // The Memo tab in the deal page renders HTML stored in deal_memos.
1599
+ // Two sources of memo content:
1600
+ // - composed: generated by the server-side memo composer on demand
1601
+ // - manual: a hand-written HTML you paste in
1602
+ // Manual always beats composed on read; reset to drop the manual row
1603
+ // and fall back to the composed one.
1604
+ if (area === "memo") {
1605
+ const sub = action;
1606
+
1607
+ // show — fetch the current memo. Default: print HTML to stdout
1608
+ // (pipeable to file or browser). --out writes to a path. --json
1609
+ // returns the full envelope (memo + mode + inflight info).
1610
+ if (sub === "show") {
1611
+ const dealId = rest[0];
1612
+ if (!dealId) {
1613
+ throw new Error("Usage: llama memo show <dealId> [--out <path>] [--json]");
1614
+ }
1615
+ const { flags } = parseFlags(rest.slice(1));
1616
+ const data = await request(
1617
+ "GET",
1618
+ `/api/deals/${encodeURIComponent(dealId)}/memo`
1619
+ );
1620
+ if (flags.json) {
1621
+ print(data);
1622
+ return;
1623
+ }
1624
+ const html = data?.memo?.html;
1625
+ if (!html) {
1626
+ if (data?.requires_compose) {
1627
+ throw new Error(
1628
+ "No memo for this deal yet — run `llama memo regenerate <dealId>` to compose one."
1629
+ );
1630
+ }
1631
+ throw new Error("Memo response missing html field.");
1632
+ }
1633
+ if (flags.out) {
1634
+ const { writeFileSync } = await import("fs");
1635
+ writeFileSync(String(flags.out), html);
1636
+ console.error(`Wrote ${html.length} bytes → ${flags.out}`);
1637
+ return;
1638
+ }
1639
+ // Stdout — supports `llama memo show <id> > memo.html` and piping
1640
+ // to e.g. `open -f -a Safari` for quick preview.
1641
+ process.stdout.write(html);
1642
+ return;
1643
+ }
1644
+
1645
+ // regenerate — kick off the server-side composer. Streams panel
1646
+ // progress events to stderr so you can see live status; prints
1647
+ // final summary JSON (version, model, duration) to stdout.
1648
+ if (sub === "regenerate") {
1649
+ const dealId = rest[0];
1650
+ if (!dealId) {
1651
+ throw new Error("Usage: llama memo regenerate <dealId> [--opus]");
1652
+ }
1653
+ const { flags } = parseFlags(rest.slice(1));
1654
+ const tier = flags.opus ? "opus" : "sonnet";
1655
+ const authHeaders = await getAuthHeaders();
1656
+ if (Object.keys(authHeaders).length === 0) {
1657
+ throw new Error(
1658
+ "Not authenticated. Run `gcloud auth login` or `llama token set <llc_...>` first."
1659
+ );
1660
+ }
1661
+ const res = await fetch(
1662
+ `${getBaseUrl()}/api/deals/${encodeURIComponent(dealId)}/memo`,
1663
+ {
1664
+ method: "POST",
1665
+ headers: { "Content-Type": "application/json", ...authHeaders },
1666
+ body: JSON.stringify({
1667
+ action: "regenerate",
1668
+ stream: true,
1669
+ model: tier,
1670
+ }),
1671
+ }
1672
+ );
1673
+ if (!res.ok || !res.body) {
1674
+ const text = await res.text().catch(() => "");
1675
+ throw new Error(`HTTP ${res.status}: ${text.slice(0, 300)}`);
1676
+ }
1677
+
1678
+ const reader = res.body.getReader();
1679
+ const decoder = new TextDecoder();
1680
+ let buffer = "";
1681
+ let doneEvent = null;
1682
+ const startedAt = Date.now();
1683
+ const progress = { done: 0, total: 12, placeholders: 0, retries: 0 };
1684
+
1685
+ while (true) {
1686
+ const { value, done: streamDone } = await reader.read();
1687
+ if (streamDone) break;
1688
+ buffer += decoder.decode(value, { stream: true });
1689
+ let idx;
1690
+ while ((idx = buffer.indexOf("\n\n")) !== -1) {
1691
+ const frame = buffer.slice(0, idx);
1692
+ buffer = buffer.slice(idx + 2);
1693
+ const dataLine = frame.split("\n").find((l) => l.startsWith("data:"));
1694
+ if (!dataLine) continue;
1695
+ let event;
1696
+ try {
1697
+ event = JSON.parse(dataLine.replace(/^data:\s?/, ""));
1698
+ } catch {
1699
+ continue;
1700
+ }
1701
+ const elapsed = ((Date.now() - startedAt) / 1000).toFixed(1);
1702
+ const phase = event.phase || "?";
1703
+ if (phase === "panel_done") {
1704
+ progress.done = event.panels_completed ?? progress.done + 1;
1705
+ progress.total = event.panels_total ?? progress.total;
1706
+ if (event.status === "placeholder") progress.placeholders += 1;
1707
+ if (event.status === "retry-recovered") progress.retries += 1;
1708
+ const mark =
1709
+ event.status === "ok"
1710
+ ? "✓"
1711
+ : event.status === "retry-recovered"
1712
+ ? "↻"
1713
+ : "⚠";
1714
+ console.error(
1715
+ `${elapsed}s ${mark} ${event.panel} [${progress.done}/${progress.total}]`
1716
+ );
1717
+ } else if (phase === "anchor_done") {
1718
+ console.error(
1719
+ `${elapsed}s anchor → ${event.verdict_label || event.verdict}`
1720
+ );
1721
+ } else if (phase === "assembling") {
1722
+ console.error(`${elapsed}s assembling…`);
1723
+ } else if (phase === "done") {
1724
+ doneEvent = event;
1725
+ } else if (phase === "error") {
1726
+ throw new Error(`Memo composer error: ${event.error}`);
1727
+ }
1728
+ }
1729
+ }
1730
+
1731
+ if (!doneEvent) {
1732
+ throw new Error("Stream ended without 'done' event.");
1733
+ }
1734
+ print({
1735
+ ok: true,
1736
+ version: doneEvent.version,
1737
+ degraded: doneEvent.degraded,
1738
+ model: doneEvent.model,
1739
+ duration_ms: doneEvent.duration_ms,
1740
+ placeholders: progress.placeholders,
1741
+ retries: progress.retries,
1742
+ });
1743
+ return;
1744
+ }
1745
+
1746
+ // save — upload hand-written HTML as a manual override.
1747
+ if (sub === "save") {
1748
+ const { flags } = parseFlags(rest);
1749
+ const dealId = rest[0];
1750
+ if (!dealId || !flags.file) {
1751
+ throw new Error("Usage: llama memo save <dealId> --file <path>");
1752
+ }
1753
+ const { readFileSync } = await import("fs");
1754
+ const html = readFileSync(String(flags.file), "utf-8");
1755
+ if (!html.trim()) throw new Error(`File ${flags.file} is empty.`);
1756
+ print(
1757
+ await request(
1758
+ "PUT",
1759
+ `/api/deals/${encodeURIComponent(dealId)}/memo`,
1760
+ { html }
1761
+ )
1762
+ );
1763
+ return;
1764
+ }
1765
+
1766
+ // reset — default drops only the manual override (next read returns
1767
+ // the composed row, if any); --all drops every version for this deal.
1768
+ if (sub === "reset") {
1769
+ const dealId = rest[0];
1770
+ if (!dealId) {
1771
+ throw new Error("Usage: llama memo reset <dealId> [--all]");
1772
+ }
1773
+ const { flags } = parseFlags(rest.slice(1));
1774
+ print(
1775
+ await request(
1776
+ "DELETE",
1777
+ `/api/deals/${encodeURIComponent(dealId)}/memo`,
1778
+ { scope: flags.all ? "all" : "override_only" }
1779
+ )
1780
+ );
1781
+ return;
1782
+ }
1783
+
1784
+ throw new Error(
1785
+ `Unknown memo subcommand "${sub || ""}". Use: show / regenerate / save / reset.`
1786
+ );
1787
+ }
1788
+
1591
1789
  usage();
1592
1790
  process.exitCode = 1;
1593
1791
  }
package/lib/external.mjs CHANGED
@@ -18,14 +18,11 @@ import { getBaseUrl } from "./client.mjs";
18
18
  const SESSION_DIR = path.join(os.homedir(), ".llama");
19
19
  const SESSION_FILE = path.join(SESSION_DIR, "external-session.json");
20
20
 
21
- // Server-side proof-of-work prefix. Must agree with
22
- // llama-command/src/lib/external-pow-client.ts. ~65k iterations average on
23
- // commodity hardware (~50–500ms in node).
21
+ // Server-side proof-of-work prefix. Server-validated; tune in tandem
22
+ // with the server policy if changed.
24
23
  const POW_DIFFICULTY_PREFIX = "0000";
25
24
 
26
- // Server requires ts_rendered to be at least 3s old (anti-replay). We
27
- // backdate by 4s when computing PoW so the request lands inside the
28
- // validity window without waiting.
25
+ // Backdate offset for the rendered-at timestamp passed to the server.
29
26
  const POW_BACKDATE_MS = 4_000;
30
27
 
31
28
  // ============================================================
@@ -360,13 +357,13 @@ export async function uploadExternalFile(filePath) {
360
357
 
361
358
  if (!res.ok) {
362
359
  if (res.status === 413) {
363
- throw new Error("File too large (max 50 MB).");
360
+ throw new Error("File too large.");
364
361
  }
365
362
  if (res.status === 415) {
366
363
  throw new Error(`MIME type "${mimetype}" not in server allowlist.`);
367
364
  }
368
365
  if (res.status === 429) {
369
- throw new Error("Upload cap reached (10 files per session).");
366
+ throw new Error("Upload cap reached.");
370
367
  }
371
368
  if (res.status === 401 || res.status === 403) {
372
369
  throw new Error(
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@llamaventures/cli",
3
- "version": "1.3.0",
4
- "description": "Llama Ventures CLI + MCP server. Internal team tool for command.llamaventures.vc.",
3
+ "version": "1.4.0",
4
+ "description": "CLI + MCP server for the Llama Ventures investment workbench (command.llamaventures.vc).",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "llama": "bin/llama.mjs",