@llamaventures/cli 1.25.0 → 1.26.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
@@ -150,6 +150,7 @@ The table below details the exact CLI for each destination.
150
150
  | HTML artifact, external — founder-facing share link | Netlify | Only when the user explicitly says "share link", "give it to the founder", "publish publicly". Use the `netlify-access-guard` workflow (server-side password + edge 401 verification). |
151
151
  | Insights, decisions, framework improvements | Wiki (markdown) | `llama wiki save <slug> --content "..."` (with attribution — see below) |
152
152
  | **HTML wiki entry — standalone HTML page hosted at `/wiki/<slug>`** (sector landscape, market map, dashboard, hand-styled thesis page) | **Wiki (HTML)** | `llama wiki save <slug> --title "..." --file <path.html> --sources "..."`. Auto-detects content_type=html from extension. Public page is full-viewport sandboxed iframe takeover (no wiki chrome). Sources/status/title still required; appears in `wiki search` + backlinks. Use when the user says "deploy this HTML to wiki", "wiki 词条", "make this page a wiki entry". HTML must be self-contained (inline CSS/JS, image data URIs or external URLs) — asset bundles aren't supported on wiki yet. **Native comments + working in-page (#) anchor links are injected automatically** — readers discuss inline and the table of contents scrolls; you don't wire anything up (pages that already embed the comment widget are left as-is). |
153
+ | **Document wiki entry — a PDF / DOCX / XLSX read at `/wiki/<slug>`** (deck, market model, memo you were handed) | **Wiki (document)** | `llama wiki save <slug> --title "..." --file <path.{pdf,docx,xlsx}> --sources "..."`. The document itself is what readers open: a PDF in the browser's own viewer with page navigation, a DOCX or XLSX converted for reading with the original still downloadable. Use when someone hands you a file and wants it ON the wiki. Do NOT transcribe it into markdown, and do NOT write an article describing a file nobody can open. |
153
154
  | Large files (deck / PDF / transcript) | Drive deal folder | the deal's `folder_url` (from `llama deal show`) → upload via your filesystem / Drive tool |
154
155
  | Cross-team cues | Inbox + email | `llama post <dealId> "@<teammate> ..." --cue` — use `--cue` only after the user explicitly authorized that recipient |
155
156
 
@@ -288,6 +289,13 @@ llama wiki save <slug> --title "..." --file path.html --sources "..." [--content
288
289
  # --content-type html (or markdown) overrides the inference.
289
290
  # Refuses to switch content_type on an existing slug; delete + re-create
290
291
  # if you really mean to change format.
292
+
293
+ # Document entry — the file IS the entry, readable at /wiki/<slug>:
294
+ llama wiki save <slug> --title "..." --file path.{pdf,docx,xlsx} --sources "..." [--doc-kind ...]
295
+ # PDF opens in the browser's viewer; DOCX and XLSX are converted for reading
296
+ # (a spreadsheet keeps one tab per sheet). The original stays downloadable.
297
+ # Upload the document you have — do NOT transcribe it into markdown first,
298
+ # and do NOT write an article describing a file nobody can open.
291
299
  # Delete / restore (soft, reversible — CONSTITUTION §8):
292
300
  llama wiki delete <slug> [--lang en|zh]
293
301
  llama wiki restore <slug> [--lang en|zh]
package/CHANGELOG.md CHANGED
@@ -6,6 +6,25 @@ this project adheres to [Semantic Versioning](https://semver.org).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.26.0] - 2026-08-11
10
+
11
+ ### Added
12
+ - `llama wiki save <slug> --file <path>.{pdf,docx,xlsx}` uploads the document
13
+ itself as the wiki entry, so readers open it at `/wiki/<slug>` instead of an
14
+ article describing it. A PDF opens in the browser's own viewer; DOCX and XLSX
15
+ are converted for reading with the original still downloadable.
16
+ - `wiki_save_file` MCP tool — the same publish from a local file path, for
17
+ MCP-native agents, mirroring `html_upload_file`.
18
+
19
+ ### Changed
20
+ - Pin the Core API consumer contract to 3.33.0. The wiki document upload calls
21
+ an operation that does not exist on older Llama Command.
22
+
23
+ ### Removed
24
+ - Retire every CLI/MCP Memo mutation path. `llama memo` and MCP now expose the
25
+ current Memo read-only; generation runs only from Llama Command's durable
26
+ Memo Agent. Deal enrichment no longer accepts `--memo` / `generateMemo`.
27
+
9
28
  ## [1.25.0] - 2026-08-05
10
29
 
11
30
  ### Added
package/bin/llama-mcp.mjs CHANGED
@@ -116,9 +116,6 @@ function buildEnrichmentAgentMessage(args = {}) {
116
116
  "monid",
117
117
  ];
118
118
  const budget = args.budgetCents ?? "50";
119
- const memo = args.generateMemo
120
- ? "Generate memo only after enrichment because the caller explicitly requested it."
121
- : "Do not generate memo.";
122
119
  return [
123
120
  "Run server-side deal enrichment for this deal.",
124
121
  `Use sources: ${sources.join(", ")}.`,
@@ -127,7 +124,7 @@ function buildEnrichmentAgentMessage(args = {}) {
127
124
  "Write canonical evidence links, sourced deal facts, stable deal fields, and typed factual values where supported.",
128
125
  "For typed factual values, call read_typed_factual_layer first and use upsert_typed_fact for queryable fields.",
129
126
  "Search snippets alone are not high-confidence evidence; fetch direct sources where possible.",
130
- memo,
127
+ "Do not generate Memo; the durable Memo Agent in Llama Command owns that separate workflow.",
131
128
  "End with what was written, what was skipped, and open questions.",
132
129
  ].join(" ");
133
130
  }
@@ -1046,6 +1043,95 @@ server.registerTool(
1046
1043
  })
1047
1044
  );
1048
1045
 
1046
+ server.registerTool(
1047
+ "wiki_save_file",
1048
+ {
1049
+ description:
1050
+ "Publish a PDF / DOCX / XLSX from a LOCAL FILE PATH as the wiki entry " +
1051
+ "itself. Readers open the document at /wiki/<slug>: a PDF in the " +
1052
+ "browser's own viewer with page navigation, a DOCX or XLSX converted " +
1053
+ "for reading (a spreadsheet keeps one tab per sheet), and the original " +
1054
+ "always downloadable. Use this whenever someone hands you a document " +
1055
+ "and wants it ON the wiki — do NOT transcribe it into markdown for " +
1056
+ "wiki_save, and do NOT write an article describing a file nobody can " +
1057
+ "open. Reads filePath on the machine running this MCP server, so the " +
1058
+ "bytes never pass through tool-call context. Deal-specific documents " +
1059
+ "belong on the deal page instead (html_upload_file).",
1060
+ inputSchema: {
1061
+ slug: z.string().describe("kebab-case slug"),
1062
+ title: z.string(),
1063
+ filePath: z
1064
+ .string()
1065
+ .describe("absolute or relative local path to a .pdf / .docx / .xlsx"),
1066
+ sources: z
1067
+ .array(z.string())
1068
+ .min(1)
1069
+ .describe(
1070
+ "citation list — URLs, doc names, or meeting references. At least one required."
1071
+ ),
1072
+ type: z.string().optional().describe("optional category tag, e.g. 'company'"),
1073
+ doc_kind: z
1074
+ .string()
1075
+ .optional()
1076
+ .describe("optional — how the wiki home browses and search filters"),
1077
+ lang: z.enum(["en", "zh"]).optional().describe("default: en"),
1078
+ },
1079
+ },
1080
+ async ({ slug, title, filePath, sources, type, doc_kind, lang }) => {
1081
+ const { readFileSync } = await import("node:fs");
1082
+ const { basename } = await import("node:path");
1083
+ const ext = String(filePath).toLowerCase().match(/\.(pdf|docx|xlsx)$/)?.[1];
1084
+ if (!ext) {
1085
+ return textResult(
1086
+ `Error: wiki_save_file takes a .pdf, .docx, or .xlsx. For markdown or a ` +
1087
+ `standalone HTML page, use wiki_save.`,
1088
+ true,
1089
+ );
1090
+ }
1091
+ let buf;
1092
+ try {
1093
+ buf = readFileSync(String(filePath));
1094
+ } catch (err) {
1095
+ return textResult(`Error reading ${filePath}: ${err?.message ?? String(err)}`, true);
1096
+ }
1097
+ const MAX = 50 * 1024 * 1024;
1098
+ if (buf.length > MAX) {
1099
+ return textResult(
1100
+ `Error: ${basename(String(filePath))} is ${(buf.length / 1024 / 1024).toFixed(1)} MB; ` +
1101
+ `the wiki caps files at 50 MB. Link to the Drive copy instead.`,
1102
+ true,
1103
+ );
1104
+ }
1105
+ const mime = {
1106
+ pdf: "application/pdf",
1107
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
1108
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
1109
+ }[ext];
1110
+ const form = new FormData();
1111
+ form.append("file", new Blob([buf], { type: mime }), basename(String(filePath)));
1112
+ form.append("title", String(title));
1113
+ form.append("sources", sources.join(";"));
1114
+ form.append("lang", lang === "zh" ? "zh" : "en");
1115
+ if (type) form.append("type", String(type));
1116
+ if (doc_kind) form.append("doc_kind", String(doc_kind));
1117
+
1118
+ const headers = await getAuthHeaders();
1119
+ // @core-api-operation POST /api/wiki/{slug}/file
1120
+ const res = await fetch(
1121
+ `${getBaseUrl()}/api/wiki/${encodeURIComponent(slug)}/file`,
1122
+ { method: "POST", headers, body: form }, // let fetch set the multipart boundary
1123
+ );
1124
+ const body = await res.json().catch(() => ({}));
1125
+ if (!res.ok) {
1126
+ return textResult(
1127
+ `HTTP ${res.status}: ${body?.error || JSON.stringify(body).slice(0, 300)}`,
1128
+ true,
1129
+ );
1130
+ }
1131
+ return textResult(JSON.stringify(body, null, 2));
1132
+ },
1133
+ );
1134
+
1049
1135
  server.registerTool(
1050
1136
  "wiki_delete",
1051
1137
  {
@@ -1212,9 +1298,8 @@ server.registerTool(
1212
1298
  "status, and planned writes without changing facts/links/memo. With " +
1213
1299
  "apply=true and executor=server_agent, this starts the server-side Deal " +
1214
1300
  "Agent unless harnessOnly=true. Set apply=true only when the user " +
1215
- "explicitly wants the enrichment run recorded/applied. " +
1216
- "generateMemo never defaults on; pass true only when the user explicitly asks " +
1217
- "for Memo generation after enrichment.",
1301
+ "explicitly wants the enrichment run recorded/applied. Memo generation is " +
1302
+ "not part of enrichment and is available only in Llama Command's Memo Agent.",
1218
1303
  inputSchema: {
1219
1304
  dealId: z.string().describe("deal uuid"),
1220
1305
  dryRun: z.boolean().optional().describe("default true unless apply=true"),
@@ -1234,10 +1319,6 @@ server.registerTool(
1234
1319
  .max(500)
1235
1320
  .optional()
1236
1321
  .describe("Monid spend cap for this run, in cents; default is 50 when Monid is requested"),
1237
- generateMemo: z
1238
- .boolean()
1239
- .optional()
1240
- .describe("explicitly request memo regeneration after enrichment; default false"),
1241
1322
  harnessOnly: z
1242
1323
  .boolean()
1243
1324
  .optional()
@@ -1248,13 +1329,13 @@ server.registerTool(
1248
1329
  .describe("optional override instruction for the server-side Deal Agent"),
1249
1330
  },
1250
1331
  },
1251
- async ({ dealId, dryRun, apply, executor, sources, budgetCents, generateMemo, harnessOnly, message }) => {
1332
+ async ({ dealId, dryRun, apply, executor, sources, budgetCents, harnessOnly, message }) => {
1252
1333
  const effectiveExecutor = executor ?? "server_agent";
1253
1334
  if (apply === true && effectiveExecutor === "server_agent" && harnessOnly !== true) {
1254
1335
  return runDealAgentTool({
1255
1336
  dealId,
1256
1337
  title: "MCP enrichment",
1257
- message: buildEnrichmentAgentMessage({ sources, budgetCents, generateMemo, message }),
1338
+ message: buildEnrichmentAgentMessage({ sources, budgetCents, message }),
1258
1339
  });
1259
1340
  }
1260
1341
  return callApi("POST", `/api/deals/${encodeURIComponent(dealId)}/enrich`, {
@@ -1263,7 +1344,6 @@ server.registerTool(
1263
1344
  executor: effectiveExecutor,
1264
1345
  sources,
1265
1346
  budgetCents,
1266
- generateMemo,
1267
1347
  });
1268
1348
  }
1269
1349
  );
@@ -1432,7 +1512,7 @@ server.registerTool(
1432
1512
  );
1433
1513
 
1434
1514
  // ============================================================
1435
- // Memo — long-form HTML investment memo (the Memo tab in the UI)
1515
+ // Memo — read-only. Generation runs only from the durable Memo Agent in the UI.
1436
1516
  // ============================================================
1437
1517
 
1438
1518
  server.registerTool(
@@ -1440,9 +1520,7 @@ server.registerTool(
1440
1520
  {
1441
1521
  description:
1442
1522
  "Fetch the current memo for a deal. Returns the envelope: memo " +
1443
- "(html, version, source, updated_by, updated_at), mode " +
1444
- "('composed' = server-generated, 'override' = hand-written), and " +
1445
- "inflight (if a server-side regeneration is in progress). html " +
1523
+ "(html, version, source, updated_by, updated_at) and mode. html " +
1446
1524
  "can be 50-100KB — be deliberate about including it in your reply.",
1447
1525
  inputSchema: {
1448
1526
  dealId: z.string().describe("deal uuid"),
@@ -1452,81 +1530,6 @@ server.registerTool(
1452
1530
  callApi("GET", `/api/deals/${encodeURIComponent(dealId)}/memo`)
1453
1531
  );
1454
1532
 
1455
- server.registerTool(
1456
- "memo_regenerate",
1457
- {
1458
- description:
1459
- "Trigger server-side regeneration of the deal memo. Synchronous: " +
1460
- "returns the final result (version, model, duration_ms, degraded) " +
1461
- "once the composer finishes. Typical duration 2-3 minutes. Use " +
1462
- "tier='opus' for high-stakes deals (higher cost, deeper analysis). " +
1463
- "Pass `instructions` to steer THIS regeneration (e.g. 'focus on team " +
1464
- "risk', 'frame as a follow-on') — applied across all panels, never " +
1465
- "overrides the facts or the verdict.",
1466
- inputSchema: {
1467
- dealId: z.string().describe("deal uuid"),
1468
- tier: z
1469
- .enum(["sonnet", "opus"])
1470
- .optional()
1471
- .describe("LLM tier (default: sonnet)"),
1472
- instructions: z
1473
- .string()
1474
- .optional()
1475
- .describe(
1476
- "Free-text steering for this regeneration only, e.g. 'focus on team risk'. Applied to all panels; never overrides verified facts or the verdict anchor."
1477
- ),
1478
- },
1479
- },
1480
- async ({ dealId, tier, instructions }) =>
1481
- callApi("POST", `/api/deals/${encodeURIComponent(dealId)}/memo`, {
1482
- action: "regenerate",
1483
- stream: false,
1484
- model: tier ?? "sonnet",
1485
- instructions: instructions || undefined,
1486
- })
1487
- );
1488
-
1489
- server.registerTool(
1490
- "memo_save",
1491
- {
1492
- description:
1493
- "Save hand-written HTML as a manual override for a deal's memo. " +
1494
- "Manual overrides take precedence over auto-composed memos on " +
1495
- "read. Pass the full HTML document including <!DOCTYPE html>, " +
1496
- "<style>, and <body> — it's rendered as-is in a sandboxed iframe.",
1497
- inputSchema: {
1498
- dealId: z.string().describe("deal uuid"),
1499
- html: z
1500
- .string()
1501
- .describe("full HTML document"),
1502
- },
1503
- },
1504
- async ({ dealId, html }) =>
1505
- callApi("PUT", `/api/deals/${encodeURIComponent(dealId)}/memo`, { html })
1506
- );
1507
-
1508
- server.registerTool(
1509
- "memo_reset",
1510
- {
1511
- description:
1512
- "Reset memo state. Default drops only the manual override row " +
1513
- "(next read falls back to the auto-composed version, if any). " +
1514
- "Pass scope='all' to drop every version for the deal — destructive, " +
1515
- "use sparingly.",
1516
- inputSchema: {
1517
- dealId: z.string().describe("deal uuid"),
1518
- scope: z
1519
- .enum(["override_only", "all"])
1520
- .optional()
1521
- .describe("default: override_only"),
1522
- },
1523
- },
1524
- async ({ dealId, scope }) =>
1525
- callApi("DELETE", `/api/deals/${encodeURIComponent(dealId)}/memo`, {
1526
- scope: scope ?? "override_only",
1527
- })
1528
- );
1529
-
1530
1533
  // ============================================================
1531
1534
  // Deal page HTML — hand-authored sandboxed page per deal
1532
1535
  // ============================================================
package/bin/llama.mjs CHANGED
@@ -314,9 +314,6 @@ function buildEnrichmentAgentMessage(flags) {
314
314
  "monid",
315
315
  ];
316
316
  const budget = flags["budget-cents"] || flags.budgetCents || "50";
317
- const memo = boolFlag(flags, "memo", "generate-memo", "generateMemo")
318
- ? "Generate memo only after enrichment because the caller explicitly requested it."
319
- : "Do not generate memo.";
320
317
  return [
321
318
  "Run server-side deal enrichment for this deal.",
322
319
  `Use sources: ${sources.join(", ")}.`,
@@ -325,7 +322,7 @@ function buildEnrichmentAgentMessage(flags) {
325
322
  "Write canonical evidence links, sourced deal facts, stable deal fields, and typed factual values where supported.",
326
323
  "For typed factual values, call read_typed_factual_layer first and use upsert_typed_fact for queryable fields.",
327
324
  "Search snippets alone are not high-confidence evidence; fetch direct sources where possible.",
328
- memo,
325
+ "Do not generate Memo; the durable Memo Agent in Llama Command owns that separate workflow.",
329
326
  "End with what was written, what was skipped, and open questions.",
330
327
  ].join(" ");
331
328
  }
@@ -409,7 +406,7 @@ Deals:
409
406
  llama deal update <dealId> leadInvestor "Acme Capital"
410
407
  llama deal enrich <dealId> [--dry-run] [--apply] [--executor server_agent|external_agent|planner]
411
408
  [--sources website,github,linkedin,yc,monid] [--budget-cents 50]
412
- [--memo] [--prompt] [--harness-only]
409
+ [--prompt] [--harness-only]
413
410
  dry-run returns the harness; --apply --executor server_agent runs the server Deal Agent.
414
411
  llama deal agent run <dealId> --message "collect founder evidence and update typed facts"
415
412
  llama deal extra set <dealId> <key> <value> # system-admin only
@@ -539,6 +536,8 @@ Where does this HTML / thesis / artifact go?
539
536
  (renders at /deals/<id>/browse/<slug>; see "Deal page HTML" below)
540
537
  Cross-deal / institutional? ..... llama wiki save <slug> --title "..." --file <path>.html --sources "..."
541
538
  (renders at /wiki/<slug>; see "Wiki" below)
539
+ A document, not a page? ......... llama wiki save <slug> --title "..." --file <path>.{pdf,docx,xlsx} --sources "..."
540
+ (the file itself becomes the entry; see "Wiki" below)
542
541
  Founder-facing public share? .... Netlify (with netlify-access-guard skill), only when user explicitly
543
542
  says "share publicly". Llama Command outranks Netlify for everything
544
543
  internal — don't reach for Netlify by default.
@@ -552,17 +551,19 @@ Wiki:
552
551
  llama wiki save <slug> --title "..." --file path.html --sources "..." [--content-type html]
553
552
  (.html / .htm extension auto-implies content_type=html)
554
553
  Native comments + working in-page (#) links are added automatically — just upload self-contained HTML.
554
+ Document entry — the file itself is the entry, readable at /wiki/<slug>:
555
+ llama wiki save <slug> --title "..." --file path.{pdf,docx,xlsx} --sources "..." [--doc-kind ...]
556
+ PDF opens in the browser's viewer (pages, search, zoom); DOCX and XLSX are converted for reading,
557
+ a spreadsheet keeping one tab per sheet. The original stays downloadable from the page either way.
558
+ Upload the document you have — don't transcribe it into markdown first.
555
559
  ➜ Use Wiki when the artifact is NOT tied to one specific deal — sector landscape, market map,
556
560
  thesis, framework, methodology. For deal-specific HTML use "llama html publish <deal>" instead.
557
561
  Delete / restore (soft — reversible):
558
562
  llama wiki delete <slug> [--lang en|zh]
559
563
  llama wiki restore <slug> [--lang en|zh]
560
564
 
561
- Memo (long-form HTML investment memo Memo tab in the UI):
565
+ Memo (read-only; generation runs only from the Memo Agent in Llama Command):
562
566
  llama memo show <dealId> [--out <path>] [--json] # default: html → stdout (pipeable to file / browser)
563
- llama memo regenerate <dealId> [--opus] [--instructions "..."] # --instructions steers THIS run (e.g. "focus on team risk"); progress → stderr
564
- llama memo save <dealId> --file <path> # paste a hand-written HTML as manual override
565
- llama memo reset <dealId> [--all] # default drops manual override; --all drops every version
566
567
 
567
568
  Deal page HTML (hand-authored sandboxed pages on /deals/<id>/browse/<slug>):
568
569
  ➜ Use this for DEAL-SPECIFIC artifacts: IC memo for X, dashboard for X, 2×2 for X.
@@ -1647,7 +1648,7 @@ async function main() {
1647
1648
  throw new Error(
1648
1649
  "Usage: llama deal enrich <dealId> [--dry-run] [--apply] " +
1649
1650
  "[--executor server_agent|external_agent|planner] " +
1650
- "[--sources website,github,linkedin,yc,monid] [--budget-cents 50] [--memo] [--prompt]"
1651
+ "[--sources website,github,linkedin,yc,monid] [--budget-cents 50] [--prompt]"
1651
1652
  );
1652
1653
  }
1653
1654
  const { flags } = parseFlags(rest.slice(1), [
@@ -1656,9 +1657,6 @@ async function main() {
1656
1657
  "executor",
1657
1658
  "sources",
1658
1659
  "budget-cents",
1659
- "memo",
1660
- "generate-memo",
1661
- "generateMemo",
1662
1660
  "prompt",
1663
1661
  "handoff",
1664
1662
  "harness-only",
@@ -1686,7 +1684,6 @@ async function main() {
1686
1684
  executor,
1687
1685
  sources,
1688
1686
  budgetCents,
1689
- generateMemo: boolFlag(flags, "memo", "generate-memo", "generateMemo"),
1690
1687
  }
1691
1688
  );
1692
1689
  if (flags.prompt === true || flags.handoff === true) {
@@ -2123,9 +2120,13 @@ async function main() {
2123
2120
  llama wiki save <slug> --title "..." --content "..." --sources "url1;url2" [--type company] [--related "A;B"] [--lang en|zh] [--content-type markdown|html]
2124
2121
  or
2125
2122
  llama wiki save <slug> --title "..." --file path/to/article.{md,html} --sources "url1;url2" [--type company] [--related "A;B"] [--lang en|zh] [--content-type markdown|html]
2123
+ or, to put a document itself on the wiki:
2124
+ llama wiki save <slug> --title "..." --file path/to/deck.{pdf,docx,xlsx} --sources "..." [--doc-kind ...]
2126
2125
 
2127
2126
  Pass either --content (inline) or --file (read from disk). With --file, content_type auto-detects from extension (.html/.htm → html, else markdown). Use --content-type to override.
2128
2127
 
2128
+ A .pdf / .docx / .xlsx uploads as the entry itself: readers open the document at /wiki/<slug> — a PDF in the browser's viewer, a spreadsheet with one tab per sheet — and the original stays downloadable. --content-type does not apply there.
2129
+
2129
2130
  Routing — is this the right command?
2130
2131
  ✓ Cross-deal / institutional knowledge (sector landscape, market map, thesis, framework, methodology)
2131
2132
  → YES, you're in the right place.
@@ -2139,6 +2140,58 @@ Routing — is this the right command?
2139
2140
  if (inlineContent && filePath) {
2140
2141
  throw new Error("Pass either --content OR --file, not both.");
2141
2142
  }
2143
+ const splitCsvFlag = (v) => String(v).split(/[;|]/).map((s) => s.trim()).filter(Boolean);
2144
+
2145
+ // A document goes up as bytes, not as text. The server converts DOCX and
2146
+ // XLSX for the reader and keeps the original for download; a PDF opens in
2147
+ // the browser's own viewer. Everything else here stays the JSON path.
2148
+ const docExt = filePath
2149
+ ? String(filePath).toLowerCase().match(/\.(pdf|docx|xlsx)$/)?.[1]
2150
+ : null;
2151
+ if (docExt) {
2152
+ if (flags["content-type"]) {
2153
+ throw new Error(
2154
+ `--content-type does not apply to a .${docExt}: the server decides how to render it from the file itself.`,
2155
+ );
2156
+ }
2157
+ const { readFileSync } = await import("fs");
2158
+ const { basename } = await import("path");
2159
+ const buf = readFileSync(String(filePath));
2160
+ const MAX = 50 * 1024 * 1024;
2161
+ if (buf.length > MAX) {
2162
+ throw new Error(
2163
+ `${basename(String(filePath))} is ${buf.length} bytes; the wiki caps files at ${MAX} (50MB). Link to the Drive copy instead.`,
2164
+ );
2165
+ }
2166
+ const mime = {
2167
+ pdf: "application/pdf",
2168
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
2169
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
2170
+ }[docExt];
2171
+ const form = new FormData();
2172
+ form.append("file", new Blob([buf], { type: mime }), basename(String(filePath)));
2173
+ form.append("lang", flags.lang === "zh" ? "zh" : "en");
2174
+ form.append("title", String(title));
2175
+ form.append("sources", splitCsvFlag(sourcesRaw).join(";"));
2176
+ if (flags.type) form.append("type", String(flags.type));
2177
+ if (flags["doc-kind"]) form.append("doc_kind", String(flags["doc-kind"]));
2178
+
2179
+ const headers = await getAuthHeaders();
2180
+ // @core-api-operation POST /api/wiki/{slug}/file
2181
+ const res = await fetch(
2182
+ `${getBaseUrl()}/api/wiki/${encodeURIComponent(slug)}/file`,
2183
+ { method: "POST", headers, body: form },
2184
+ );
2185
+ const out = await res.json().catch(() => ({}));
2186
+ if (!res.ok) {
2187
+ throw new Error(
2188
+ `HTTP ${res.status}: ${out?.error || JSON.stringify(out).slice(0, 300)}`,
2189
+ );
2190
+ }
2191
+ print(out);
2192
+ return;
2193
+ }
2194
+
2142
2195
  // Read body — either inline or from file.
2143
2196
  let body;
2144
2197
  let inferredType = "markdown";
@@ -2512,13 +2565,9 @@ Routing — is this the right command?
2512
2565
  throw new Error(`Unknown mentions subcommand "${sub}". Use: list / show / resolve / unread.`);
2513
2566
  }
2514
2567
 
2515
- // ----- Memo (long-form HTML investment memo) -----
2516
- // The Memo tab in the deal page renders HTML stored in deal_memos.
2517
- // Two sources of memo content:
2518
- // - composed: generated by the server-side memo composer on demand
2519
- // - manual: a hand-written HTML you paste in
2520
- // Manual always beats composed on read; reset to drop the manual row
2521
- // and fall back to the composed one.
2568
+ // ----- Memo (read-only) -----
2569
+ // Generation is intentionally absent from CLI. The durable Memo Agent in
2570
+ // Llama Command owns the only generation path.
2522
2571
  if (area === "memo") {
2523
2572
  const sub = action;
2524
2573
 
@@ -2542,9 +2591,7 @@ Routing — is this the right command?
2542
2591
  const html = data?.memo?.html;
2543
2592
  if (!html) {
2544
2593
  if (data?.requires_compose) {
2545
- throw new Error(
2546
- "No memo for this deal yet — run `llama memo regenerate <dealId>` to compose one."
2547
- );
2594
+ throw new Error("No memo for this deal yet — generate it with the Memo Agent in Llama Command.");
2548
2595
  }
2549
2596
  throw new Error("Memo response missing html field.");
2550
2597
  }
@@ -2560,152 +2607,8 @@ Routing — is this the right command?
2560
2607
  return;
2561
2608
  }
2562
2609
 
2563
- // regenerate — kick off the server-side composer. Streams panel
2564
- // progress events to stderr so you can see live status; prints
2565
- // final summary JSON (version, model, duration) to stdout.
2566
- if (sub === "regenerate") {
2567
- const dealId = rest[0];
2568
- if (!dealId) {
2569
- throw new Error(
2570
- 'Usage: llama memo regenerate <dealId> [--opus] [--instructions "..."]'
2571
- );
2572
- }
2573
- const { flags } = parseFlags(rest.slice(1));
2574
- const tier = flags.opus ? "opus" : "sonnet";
2575
- const authHeaders = await getAuthHeaders();
2576
- if (Object.keys(authHeaders).length === 0) {
2577
- throw new Error(
2578
- "Not authenticated. Run `gcloud auth login` or `llama token set <llc_...>` first."
2579
- );
2580
- }
2581
- const res = await fetch(
2582
- `${getBaseUrl()}/api/deals/${encodeURIComponent(dealId)}/memo`,
2583
- {
2584
- method: "POST",
2585
- headers: { "Content-Type": "application/json", ...authHeaders },
2586
- body: JSON.stringify({
2587
- action: "regenerate",
2588
- stream: true,
2589
- model: tier,
2590
- instructions: flags.instructions
2591
- ? String(flags.instructions)
2592
- : undefined,
2593
- }),
2594
- }
2595
- );
2596
- if (!res.ok || !res.body) {
2597
- const text = await res.text().catch(() => "");
2598
- throw new Error(`HTTP ${res.status}: ${text.slice(0, 300)}`);
2599
- }
2600
-
2601
- const reader = res.body.getReader();
2602
- const decoder = new TextDecoder();
2603
- let buffer = "";
2604
- let doneEvent = null;
2605
- const startedAt = Date.now();
2606
- const progress = { done: 0, total: 12, placeholders: 0, retries: 0 };
2607
-
2608
- while (true) {
2609
- const { value, done: streamDone } = await reader.read();
2610
- if (streamDone) break;
2611
- buffer += decoder.decode(value, { stream: true });
2612
- let idx;
2613
- while ((idx = buffer.indexOf("\n\n")) !== -1) {
2614
- const frame = buffer.slice(0, idx);
2615
- buffer = buffer.slice(idx + 2);
2616
- const dataLine = frame.split("\n").find((l) => l.startsWith("data:"));
2617
- if (!dataLine) continue;
2618
- let event;
2619
- try {
2620
- event = JSON.parse(dataLine.replace(/^data:\s?/, ""));
2621
- } catch {
2622
- continue;
2623
- }
2624
- const elapsed = ((Date.now() - startedAt) / 1000).toFixed(1);
2625
- const phase = event.phase || "?";
2626
- if (phase === "panel_done") {
2627
- progress.done = event.panels_completed ?? progress.done + 1;
2628
- progress.total = event.panels_total ?? progress.total;
2629
- if (event.status === "placeholder") progress.placeholders += 1;
2630
- if (event.status === "retry-recovered") progress.retries += 1;
2631
- const mark =
2632
- event.status === "ok"
2633
- ? "✓"
2634
- : event.status === "retry-recovered"
2635
- ? "↻"
2636
- : "⚠";
2637
- console.error(
2638
- `${elapsed}s ${mark} ${event.panel} [${progress.done}/${progress.total}]`
2639
- );
2640
- } else if (phase === "anchor_done") {
2641
- console.error(
2642
- `${elapsed}s anchor → ${event.verdict_label || event.verdict}`
2643
- );
2644
- } else if (phase === "assembling") {
2645
- console.error(`${elapsed}s assembling…`);
2646
- } else if (phase === "done") {
2647
- doneEvent = event;
2648
- } else if (phase === "error") {
2649
- throw new Error(`Memo composer error: ${event.error}`);
2650
- }
2651
- }
2652
- }
2653
-
2654
- if (!doneEvent) {
2655
- throw new Error("Stream ended without 'done' event.");
2656
- }
2657
- print({
2658
- ok: true,
2659
- version: doneEvent.version,
2660
- degraded: doneEvent.degraded,
2661
- model: doneEvent.model,
2662
- duration_ms: doneEvent.duration_ms,
2663
- placeholders: progress.placeholders,
2664
- retries: progress.retries,
2665
- });
2666
- return;
2667
- }
2668
-
2669
- // save — upload hand-written HTML as a manual override.
2670
- if (sub === "save") {
2671
- const { flags } = parseFlags(rest);
2672
- const dealId = rest[0];
2673
- if (!dealId || !flags.file) {
2674
- throw new Error("Usage: llama memo save <dealId> --file <path>");
2675
- }
2676
- const { readFileSync } = await import("fs");
2677
- const html = readFileSync(String(flags.file), "utf-8");
2678
- if (!html.trim()) throw new Error(`File ${flags.file} is empty.`);
2679
- print(
2680
- await request(
2681
- "PUT",
2682
- `/api/deals/${encodeURIComponent(dealId)}/memo`,
2683
- { html }
2684
- )
2685
- );
2686
- return;
2687
- }
2688
-
2689
- // reset — default drops only the manual override (next read returns
2690
- // the composed row, if any); --all drops every version for this deal.
2691
- if (sub === "reset") {
2692
- const dealId = rest[0];
2693
- if (!dealId) {
2694
- throw new Error("Usage: llama memo reset <dealId> [--all]");
2695
- }
2696
- const { flags } = parseFlags(rest.slice(1));
2697
- print(
2698
- await request(
2699
- "DELETE",
2700
- `/api/deals/${encodeURIComponent(dealId)}/memo`,
2701
- { scope: flags.all ? "all" : "override_only" }
2702
- )
2703
- );
2704
- return;
2705
- }
2706
-
2707
2610
  throw new Error(
2708
- `Unknown memo subcommand "${sub || ""}". Use: show / regenerate / save / reset.`
2611
+ `Unknown memo subcommand "${sub || ""}". Use: show. Memo generation is available only in Llama Command.`
2709
2612
  );
2710
2613
  }
2711
2614
 
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "format": "llama.core-api-contract.v1",
3
3
  "name": "llama-core-api",
4
- "apiVersion": "3.26.0",
4
+ "apiVersion": "3.33.0",
5
5
  "openapiVersion": "3.0.3",
6
- "sha256": "1ab4db8774b6dc0d94d6d6b4dd79f8272c6a405a3474710ac4162cc8a0dc6981",
7
- "pathCount": 236,
8
- "operationCount": 307
6
+ "sha256": "d673c2f38f63be2cd92415d9c384b40d115165ca4ba11bfb93ddaf4036226eed",
7
+ "pathCount": 238,
8
+ "operationCount": 312
9
9
  }
@@ -86,14 +86,6 @@
86
86
  "method": "GET",
87
87
  "path": "/api/deals/{dealId}"
88
88
  },
89
- {
90
- "method": "GET",
91
- "path": "/api/deals/{dealId}/workflow"
92
- },
93
- {
94
- "method": "POST",
95
- "path": "/api/deals/{dealId}/workflow"
96
- },
97
89
  {
98
90
  "method": "POST",
99
91
  "path": "/api/deals/{dealId}/agent-runs/{runId}/revert"
@@ -234,22 +226,10 @@
234
226
  "method": "POST",
235
227
  "path": "/api/deals/{dealId}/links/{linkId}/restore"
236
228
  },
237
- {
238
- "method": "DELETE",
239
- "path": "/api/deals/{dealId}/memo"
240
- },
241
229
  {
242
230
  "method": "GET",
243
231
  "path": "/api/deals/{dealId}/memo"
244
232
  },
245
- {
246
- "method": "POST",
247
- "path": "/api/deals/{dealId}/memo"
248
- },
249
- {
250
- "method": "PUT",
251
- "path": "/api/deals/{dealId}/memo"
252
- },
253
233
  {
254
234
  "method": "POST",
255
235
  "path": "/api/deals/{dealId}/posts"
@@ -278,6 +258,14 @@
278
258
  "method": "GET",
279
259
  "path": "/api/deals/{dealId}/timeline"
280
260
  },
261
+ {
262
+ "method": "GET",
263
+ "path": "/api/deals/{dealId}/workflow"
264
+ },
265
+ {
266
+ "method": "POST",
267
+ "path": "/api/deals/{dealId}/workflow"
268
+ },
281
269
  {
282
270
  "method": "POST",
283
271
  "path": "/api/external/chat"
@@ -354,6 +342,10 @@
354
342
  "method": "GET",
355
343
  "path": "/api/wiki/{slug}"
356
344
  },
345
+ {
346
+ "method": "POST",
347
+ "path": "/api/wiki/{slug}/file"
348
+ },
357
349
  {
358
350
  "method": "POST",
359
351
  "path": "/api/wiki/{slug}/restore"
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "format": "llama.cli-build.v1",
3
3
  "packageName": "@llamaventures/cli",
4
- "packageVersion": "1.25.0",
5
- "sourceSha": "4b70d873886c521d02cc7c75659425ed6e2989f6",
4
+ "packageVersion": "1.26.0",
5
+ "sourceSha": "c330b8222f075478e50a96e160b647b09f535e3d",
6
6
  "sourceKind": "github",
7
7
  "sourceDirty": false,
8
8
  "coreApiContract": {
9
9
  "format": "llama.core-api-contract.v1",
10
10
  "name": "llama-core-api",
11
- "apiVersion": "3.26.0",
11
+ "apiVersion": "3.33.0",
12
12
  "openapiVersion": "3.0.3",
13
- "sha256": "1ab4db8774b6dc0d94d6d6b4dd79f8272c6a405a3474710ac4162cc8a0dc6981"
13
+ "sha256": "d673c2f38f63be2cd92415d9c384b40d115165ca4ba11bfb93ddaf4036226eed"
14
14
  }
15
15
  }
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@llamaventures/cli",
3
- "version": "1.25.0",
3
+ "version": "1.26.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 && npm run test:contract",
8
8
  "test:agent-routing": "node scripts/verify-agent-routing.mjs",
9
- "test:contract": "node --test scripts/build-manifest.test.mjs scripts/core-api-call-sites.test.mjs scripts/investment-workflow-contract.test.mjs scripts/server-compatibility.test.mjs scripts/verify-core-api-contract.test.mjs scripts/workflow-audit.test.mjs scripts/workflow-remediation.test.mjs && node scripts/verify-core-api-contract.mjs",
9
+ "test:contract": "node --test scripts/build-manifest.test.mjs scripts/core-api-call-sites.test.mjs scripts/investment-workflow-contract.test.mjs scripts/memo-retirement.test.mjs scripts/server-compatibility.test.mjs scripts/verify-core-api-contract.test.mjs scripts/workflow-audit.test.mjs scripts/workflow-remediation.test.mjs && node scripts/verify-core-api-contract.mjs",
10
10
  "verify:artifact": "node scripts/verify-release-artifact.mjs",
11
11
  "verify:release": "npm test && npm run verify:artifact && node scripts/verify-tarball-clean.mjs",
12
12
  "prepack": "node scripts/prepare-build-manifest.mjs",
@@ -56,7 +56,7 @@
56
56
  "access": "public"
57
57
  },
58
58
  "dependencies": {
59
- "@modelcontextprotocol/sdk": "1.29.0",
59
+ "@modelcontextprotocol/sdk": "1.30.0",
60
60
  "@napi-rs/keyring": "^1.3.0",
61
61
  "zod": "^4.4.3"
62
62
  }