@llamaventures/cli 1.3.1 → 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/bin/llama-mcp.mjs CHANGED
@@ -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
@@ -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]
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llamaventures/cli",
3
- "version": "1.3.1",
3
+ "version": "1.4.0",
4
4
  "description": "CLI + MCP server for the Llama Ventures investment workbench (command.llamaventures.vc).",
5
5
  "type": "module",
6
6
  "bin": {