@llamaventures/cli 1.24.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,31 @@ 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
+
28
+ ## [1.25.0] - 2026-08-05
29
+
30
+ ### Added
31
+ - Add preview-first, revision-fenced `llama admin workflow remediate` for the
32
+ two allowlisted Workflow V2 false-green intake checks in Llama Command 3.26.0.
33
+
9
34
  ## [1.24.0] - 2026-08-05
10
35
 
11
36
  ### Added
package/README.md CHANGED
@@ -121,6 +121,18 @@ llama admin workflow audit --deal <dealId>
121
121
  llama admin workflow audit --all
122
122
  ```
123
123
 
124
+ For the two supported false-green intake checks, remediation is preview-first
125
+ and one deal at a time. Apply requires the exact revision returned by preview:
126
+
127
+ ```bash
128
+ llama admin workflow remediate --deal <dealId> --guard intake.reason_why
129
+ llama admin workflow remediate --deal <dealId> --guard intake.reason_why \
130
+ --apply --expected-revision <revision> --reason "Confirmed missing canonical evidence"
131
+ ```
132
+
133
+ The server can only invalidate a system-generated green. It cannot advance a
134
+ deal, satisfy a check, change status/ownership, or override a human resolution.
135
+
124
136
  Status vocabulary — `Interested`: tracked before any contact ·
125
137
  `Outreached`: contacted, no response yet · `Sourced`: real relationship
126
138
  signal exists. `sourceDirection` is separate: `Inbound` came to the firm,
package/README.zh-CN.md CHANGED
@@ -142,6 +142,18 @@ llama admin workflow audit --deal <dealId>
142
142
  llama admin workflow audit --all
143
143
  ```
144
144
 
145
+ 对当前支持的两个 intake 假绿检查,修复必须先 preview,并且一次只处理一个 deal。
146
+ Apply 必须携带 preview 返回的精确 revision:
147
+
148
+ ```bash
149
+ llama admin workflow remediate --deal <dealId> --guard intake.reason_why
150
+ llama admin workflow remediate --deal <dealId> --guard intake.reason_why \
151
+ --apply --expected-revision <revision> --reason "已确认 canonical 证据缺失"
152
+ ```
153
+
154
+ 服务端只能把系统生成的绿灯改回未满足;不能推进 deal、把检查改成已满足、修改
155
+ status/owner,也不能覆盖人工 resolution。
156
+
145
157
  Status 语义——`Interested`:接触前先记录关注 · `Outreached`:已联系、
146
158
  尚无回应 · `Sourced`:已有真实关系信号。`sourceDirection` 是独立维度:
147
159
  `Inbound` 流入,`Outbound` 我们主动。
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
@@ -37,6 +37,10 @@ import { deleteBundle, detectBackend, readBundle, writeBundle } from "../lib/oau
37
37
  import { maybeNudgeUpdate, getUpdateNudge } from "../lib/version-check.mjs";
38
38
  import { getBuildInfo } from "../lib/build-info.mjs";
39
39
  import { workflowAuditPath } from "../lib/workflow-audit.mjs";
40
+ import {
41
+ WORKFLOW_REMEDIATION_PATH,
42
+ workflowRemediationBody,
43
+ } from "../lib/workflow-remediation.mjs";
40
44
 
41
45
  const requireFromHere = createRequire(import.meta.url);
42
46
  const { version: PKG_VERSION } = requireFromHere("../package.json");
@@ -310,9 +314,6 @@ function buildEnrichmentAgentMessage(flags) {
310
314
  "monid",
311
315
  ];
312
316
  const budget = flags["budget-cents"] || flags.budgetCents || "50";
313
- const memo = boolFlag(flags, "memo", "generate-memo", "generateMemo")
314
- ? "Generate memo only after enrichment because the caller explicitly requested it."
315
- : "Do not generate memo.";
316
317
  return [
317
318
  "Run server-side deal enrichment for this deal.",
318
319
  `Use sources: ${sources.join(", ")}.`,
@@ -321,7 +322,7 @@ function buildEnrichmentAgentMessage(flags) {
321
322
  "Write canonical evidence links, sourced deal facts, stable deal fields, and typed factual values where supported.",
322
323
  "For typed factual values, call read_typed_factual_layer first and use upsert_typed_fact for queryable fields.",
323
324
  "Search snippets alone are not high-confidence evidence; fetch direct sources where possible.",
324
- memo,
325
+ "Do not generate Memo; the durable Memo Agent in Llama Command owns that separate workflow.",
325
326
  "End with what was written, what was skipped, and open questions.",
326
327
  ].join(" ");
327
328
  }
@@ -405,7 +406,7 @@ Deals:
405
406
  llama deal update <dealId> leadInvestor "Acme Capital"
406
407
  llama deal enrich <dealId> [--dry-run] [--apply] [--executor server_agent|external_agent|planner]
407
408
  [--sources website,github,linkedin,yc,monid] [--budget-cents 50]
408
- [--memo] [--prompt] [--harness-only]
409
+ [--prompt] [--harness-only]
409
410
  dry-run returns the harness; --apply --executor server_agent runs the server Deal Agent.
410
411
  llama deal agent run <dealId> --message "collect founder evidence and update typed facts"
411
412
  llama deal extra set <dealId> <key> <value> # system-admin only
@@ -535,6 +536,8 @@ Where does this HTML / thesis / artifact go?
535
536
  (renders at /deals/<id>/browse/<slug>; see "Deal page HTML" below)
536
537
  Cross-deal / institutional? ..... llama wiki save <slug> --title "..." --file <path>.html --sources "..."
537
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)
538
541
  Founder-facing public share? .... Netlify (with netlify-access-guard skill), only when user explicitly
539
542
  says "share publicly". Llama Command outranks Netlify for everything
540
543
  internal — don't reach for Netlify by default.
@@ -548,17 +551,19 @@ Wiki:
548
551
  llama wiki save <slug> --title "..." --file path.html --sources "..." [--content-type html]
549
552
  (.html / .htm extension auto-implies content_type=html)
550
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.
551
559
  ➜ Use Wiki when the artifact is NOT tied to one specific deal — sector landscape, market map,
552
560
  thesis, framework, methodology. For deal-specific HTML use "llama html publish <deal>" instead.
553
561
  Delete / restore (soft — reversible):
554
562
  llama wiki delete <slug> [--lang en|zh]
555
563
  llama wiki restore <slug> [--lang en|zh]
556
564
 
557
- 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):
558
566
  llama memo show <dealId> [--out <path>] [--json] # default: html → stdout (pipeable to file / browser)
559
- llama memo regenerate <dealId> [--opus] [--instructions "..."] # --instructions steers THIS run (e.g. "focus on team risk"); progress → stderr
560
- llama memo save <dealId> --file <path> # paste a hand-written HTML as manual override
561
- llama memo reset <dealId> [--all] # default drops manual override; --all drops every version
562
567
 
563
568
  Deal page HTML (hand-authored sandboxed pages on /deals/<id>/browse/<slug>):
564
569
  ➜ Use this for DEAL-SPECIFIC artifacts: IC memo for X, dashboard for X, 2×2 for X.
@@ -605,6 +610,8 @@ Deal page HTML (hand-authored sandboxed pages on /deals/<id>/browse/<slug>):
605
610
 
606
611
  Admin (system admin only — server returns 403 for non-admin tokens):
607
612
  llama admin workflow audit --deal <uuid> | --all
613
+ llama admin workflow remediate --deal <uuid> --guard intake.reason_why[,intake.founder_identity]
614
+ [--apply --expected-revision <n> --reason "..."]
608
615
  llama admin auth-events [--kind X] [--actor email] [--subject email] [--since 24h|7d|30d|<ISO>] [--limit 100]
609
616
  llama admin deal-events [--kind X] [--actor email] [--deal <uuid>] [--since 24h] [--limit 100]
610
617
  llama admin agent-events [--kind tool_call|loop_stalled|max_turns_reached] [--agent-kind deal|secretary|main|inbox]
@@ -1641,7 +1648,7 @@ async function main() {
1641
1648
  throw new Error(
1642
1649
  "Usage: llama deal enrich <dealId> [--dry-run] [--apply] " +
1643
1650
  "[--executor server_agent|external_agent|planner] " +
1644
- "[--sources website,github,linkedin,yc,monid] [--budget-cents 50] [--memo] [--prompt]"
1651
+ "[--sources website,github,linkedin,yc,monid] [--budget-cents 50] [--prompt]"
1645
1652
  );
1646
1653
  }
1647
1654
  const { flags } = parseFlags(rest.slice(1), [
@@ -1650,9 +1657,6 @@ async function main() {
1650
1657
  "executor",
1651
1658
  "sources",
1652
1659
  "budget-cents",
1653
- "memo",
1654
- "generate-memo",
1655
- "generateMemo",
1656
1660
  "prompt",
1657
1661
  "handoff",
1658
1662
  "harness-only",
@@ -1680,7 +1684,6 @@ async function main() {
1680
1684
  executor,
1681
1685
  sources,
1682
1686
  budgetCents,
1683
- generateMemo: boolFlag(flags, "memo", "generate-memo", "generateMemo"),
1684
1687
  }
1685
1688
  );
1686
1689
  if (flags.prompt === true || flags.handoff === true) {
@@ -2117,9 +2120,13 @@ async function main() {
2117
2120
  llama wiki save <slug> --title "..." --content "..." --sources "url1;url2" [--type company] [--related "A;B"] [--lang en|zh] [--content-type markdown|html]
2118
2121
  or
2119
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 ...]
2120
2125
 
2121
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.
2122
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
+
2123
2130
  Routing — is this the right command?
2124
2131
  ✓ Cross-deal / institutional knowledge (sector landscape, market map, thesis, framework, methodology)
2125
2132
  → YES, you're in the right place.
@@ -2133,6 +2140,58 @@ Routing — is this the right command?
2133
2140
  if (inlineContent && filePath) {
2134
2141
  throw new Error("Pass either --content OR --file, not both.");
2135
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
+
2136
2195
  // Read body — either inline or from file.
2137
2196
  let body;
2138
2197
  let inferredType = "markdown";
@@ -2412,12 +2471,28 @@ Routing — is this the right command?
2412
2471
  );
2413
2472
  }
2414
2473
  if (sub === "workflow") {
2415
- if (rest[0] !== "audit") {
2416
- throw new Error("Usage: llama admin workflow audit --deal <uuid> | --all");
2474
+ if (!["audit", "remediate"].includes(rest[0])) {
2475
+ throw new Error("Usage: llama admin workflow audit|remediate ...");
2476
+ }
2477
+ if (rest[0] === "audit") {
2478
+ const { flags } = parseFlags(rest.slice(1), ["deal", "all"]);
2479
+ // @core-api-operation GET /api/admin/workflow-audit
2480
+ print(await request("GET", workflowAuditPath(flags)));
2481
+ return;
2417
2482
  }
2418
- const { flags } = parseFlags(rest.slice(1), ["deal", "all"]);
2419
- // @core-api-operation GET /api/admin/workflow-audit
2420
- print(await request("GET", workflowAuditPath(flags)));
2483
+ const { flags } = parseFlags(rest.slice(1), [
2484
+ "deal",
2485
+ "guard",
2486
+ "apply",
2487
+ "expected-revision",
2488
+ "reason",
2489
+ ]);
2490
+ const body = workflowRemediationBody(
2491
+ flags,
2492
+ flags.apply === true ? `cli-workflow-remediation-${randomUUID()}` : undefined,
2493
+ );
2494
+ // @core-api-operation POST /api/admin/workflow-remediation
2495
+ print(await request("POST", WORKFLOW_REMEDIATION_PATH, body));
2421
2496
  return;
2422
2497
  }
2423
2498
  const { flags } = parseFlags(rest);
@@ -2490,13 +2565,9 @@ Routing — is this the right command?
2490
2565
  throw new Error(`Unknown mentions subcommand "${sub}". Use: list / show / resolve / unread.`);
2491
2566
  }
2492
2567
 
2493
- // ----- Memo (long-form HTML investment memo) -----
2494
- // The Memo tab in the deal page renders HTML stored in deal_memos.
2495
- // Two sources of memo content:
2496
- // - composed: generated by the server-side memo composer on demand
2497
- // - manual: a hand-written HTML you paste in
2498
- // Manual always beats composed on read; reset to drop the manual row
2499
- // 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.
2500
2571
  if (area === "memo") {
2501
2572
  const sub = action;
2502
2573
 
@@ -2520,9 +2591,7 @@ Routing — is this the right command?
2520
2591
  const html = data?.memo?.html;
2521
2592
  if (!html) {
2522
2593
  if (data?.requires_compose) {
2523
- throw new Error(
2524
- "No memo for this deal yet — run `llama memo regenerate <dealId>` to compose one."
2525
- );
2594
+ throw new Error("No memo for this deal yet — generate it with the Memo Agent in Llama Command.");
2526
2595
  }
2527
2596
  throw new Error("Memo response missing html field.");
2528
2597
  }
@@ -2538,152 +2607,8 @@ Routing — is this the right command?
2538
2607
  return;
2539
2608
  }
2540
2609
 
2541
- // regenerate — kick off the server-side composer. Streams panel
2542
- // progress events to stderr so you can see live status; prints
2543
- // final summary JSON (version, model, duration) to stdout.
2544
- if (sub === "regenerate") {
2545
- const dealId = rest[0];
2546
- if (!dealId) {
2547
- throw new Error(
2548
- 'Usage: llama memo regenerate <dealId> [--opus] [--instructions "..."]'
2549
- );
2550
- }
2551
- const { flags } = parseFlags(rest.slice(1));
2552
- const tier = flags.opus ? "opus" : "sonnet";
2553
- const authHeaders = await getAuthHeaders();
2554
- if (Object.keys(authHeaders).length === 0) {
2555
- throw new Error(
2556
- "Not authenticated. Run `gcloud auth login` or `llama token set <llc_...>` first."
2557
- );
2558
- }
2559
- const res = await fetch(
2560
- `${getBaseUrl()}/api/deals/${encodeURIComponent(dealId)}/memo`,
2561
- {
2562
- method: "POST",
2563
- headers: { "Content-Type": "application/json", ...authHeaders },
2564
- body: JSON.stringify({
2565
- action: "regenerate",
2566
- stream: true,
2567
- model: tier,
2568
- instructions: flags.instructions
2569
- ? String(flags.instructions)
2570
- : undefined,
2571
- }),
2572
- }
2573
- );
2574
- if (!res.ok || !res.body) {
2575
- const text = await res.text().catch(() => "");
2576
- throw new Error(`HTTP ${res.status}: ${text.slice(0, 300)}`);
2577
- }
2578
-
2579
- const reader = res.body.getReader();
2580
- const decoder = new TextDecoder();
2581
- let buffer = "";
2582
- let doneEvent = null;
2583
- const startedAt = Date.now();
2584
- const progress = { done: 0, total: 12, placeholders: 0, retries: 0 };
2585
-
2586
- while (true) {
2587
- const { value, done: streamDone } = await reader.read();
2588
- if (streamDone) break;
2589
- buffer += decoder.decode(value, { stream: true });
2590
- let idx;
2591
- while ((idx = buffer.indexOf("\n\n")) !== -1) {
2592
- const frame = buffer.slice(0, idx);
2593
- buffer = buffer.slice(idx + 2);
2594
- const dataLine = frame.split("\n").find((l) => l.startsWith("data:"));
2595
- if (!dataLine) continue;
2596
- let event;
2597
- try {
2598
- event = JSON.parse(dataLine.replace(/^data:\s?/, ""));
2599
- } catch {
2600
- continue;
2601
- }
2602
- const elapsed = ((Date.now() - startedAt) / 1000).toFixed(1);
2603
- const phase = event.phase || "?";
2604
- if (phase === "panel_done") {
2605
- progress.done = event.panels_completed ?? progress.done + 1;
2606
- progress.total = event.panels_total ?? progress.total;
2607
- if (event.status === "placeholder") progress.placeholders += 1;
2608
- if (event.status === "retry-recovered") progress.retries += 1;
2609
- const mark =
2610
- event.status === "ok"
2611
- ? "✓"
2612
- : event.status === "retry-recovered"
2613
- ? "↻"
2614
- : "⚠";
2615
- console.error(
2616
- `${elapsed}s ${mark} ${event.panel} [${progress.done}/${progress.total}]`
2617
- );
2618
- } else if (phase === "anchor_done") {
2619
- console.error(
2620
- `${elapsed}s anchor → ${event.verdict_label || event.verdict}`
2621
- );
2622
- } else if (phase === "assembling") {
2623
- console.error(`${elapsed}s assembling…`);
2624
- } else if (phase === "done") {
2625
- doneEvent = event;
2626
- } else if (phase === "error") {
2627
- throw new Error(`Memo composer error: ${event.error}`);
2628
- }
2629
- }
2630
- }
2631
-
2632
- if (!doneEvent) {
2633
- throw new Error("Stream ended without 'done' event.");
2634
- }
2635
- print({
2636
- ok: true,
2637
- version: doneEvent.version,
2638
- degraded: doneEvent.degraded,
2639
- model: doneEvent.model,
2640
- duration_ms: doneEvent.duration_ms,
2641
- placeholders: progress.placeholders,
2642
- retries: progress.retries,
2643
- });
2644
- return;
2645
- }
2646
-
2647
- // save — upload hand-written HTML as a manual override.
2648
- if (sub === "save") {
2649
- const { flags } = parseFlags(rest);
2650
- const dealId = rest[0];
2651
- if (!dealId || !flags.file) {
2652
- throw new Error("Usage: llama memo save <dealId> --file <path>");
2653
- }
2654
- const { readFileSync } = await import("fs");
2655
- const html = readFileSync(String(flags.file), "utf-8");
2656
- if (!html.trim()) throw new Error(`File ${flags.file} is empty.`);
2657
- print(
2658
- await request(
2659
- "PUT",
2660
- `/api/deals/${encodeURIComponent(dealId)}/memo`,
2661
- { html }
2662
- )
2663
- );
2664
- return;
2665
- }
2666
-
2667
- // reset — default drops only the manual override (next read returns
2668
- // the composed row, if any); --all drops every version for this deal.
2669
- if (sub === "reset") {
2670
- const dealId = rest[0];
2671
- if (!dealId) {
2672
- throw new Error("Usage: llama memo reset <dealId> [--all]");
2673
- }
2674
- const { flags } = parseFlags(rest.slice(1));
2675
- print(
2676
- await request(
2677
- "DELETE",
2678
- `/api/deals/${encodeURIComponent(dealId)}/memo`,
2679
- { scope: flags.all ? "all" : "override_only" }
2680
- )
2681
- );
2682
- return;
2683
- }
2684
-
2685
2610
  throw new Error(
2686
- `Unknown memo subcommand "${sub || ""}". Use: show / regenerate / save / reset.`
2611
+ `Unknown memo subcommand "${sub || ""}". Use: show. Memo generation is available only in Llama Command.`
2687
2612
  );
2688
2613
  }
2689
2614
 
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "format": "llama.core-api-contract.v1",
3
3
  "name": "llama-core-api",
4
- "apiVersion": "3.25.0",
4
+ "apiVersion": "3.33.0",
5
5
  "openapiVersion": "3.0.3",
6
- "sha256": "f06bbd31d4e0e3fbf5f418a772616375f2a907535b0c7783060ba87b0565a0f7",
7
- "pathCount": 235,
8
- "operationCount": 306
6
+ "sha256": "d673c2f38f63be2cd92415d9c384b40d115165ca4ba11bfb93ddaf4036226eed",
7
+ "pathCount": 238,
8
+ "operationCount": 312
9
9
  }
@@ -18,6 +18,10 @@
18
18
  "method": "GET",
19
19
  "path": "/api/admin/workflow-audit"
20
20
  },
21
+ {
22
+ "method": "POST",
23
+ "path": "/api/admin/workflow-remediation"
24
+ },
21
25
  {
22
26
  "method": "GET",
23
27
  "path": "/api/agent/activity"
@@ -82,14 +86,6 @@
82
86
  "method": "GET",
83
87
  "path": "/api/deals/{dealId}"
84
88
  },
85
- {
86
- "method": "GET",
87
- "path": "/api/deals/{dealId}/workflow"
88
- },
89
- {
90
- "method": "POST",
91
- "path": "/api/deals/{dealId}/workflow"
92
- },
93
89
  {
94
90
  "method": "POST",
95
91
  "path": "/api/deals/{dealId}/agent-runs/{runId}/revert"
@@ -230,22 +226,10 @@
230
226
  "method": "POST",
231
227
  "path": "/api/deals/{dealId}/links/{linkId}/restore"
232
228
  },
233
- {
234
- "method": "DELETE",
235
- "path": "/api/deals/{dealId}/memo"
236
- },
237
229
  {
238
230
  "method": "GET",
239
231
  "path": "/api/deals/{dealId}/memo"
240
232
  },
241
- {
242
- "method": "POST",
243
- "path": "/api/deals/{dealId}/memo"
244
- },
245
- {
246
- "method": "PUT",
247
- "path": "/api/deals/{dealId}/memo"
248
- },
249
233
  {
250
234
  "method": "POST",
251
235
  "path": "/api/deals/{dealId}/posts"
@@ -274,6 +258,14 @@
274
258
  "method": "GET",
275
259
  "path": "/api/deals/{dealId}/timeline"
276
260
  },
261
+ {
262
+ "method": "GET",
263
+ "path": "/api/deals/{dealId}/workflow"
264
+ },
265
+ {
266
+ "method": "POST",
267
+ "path": "/api/deals/{dealId}/workflow"
268
+ },
277
269
  {
278
270
  "method": "POST",
279
271
  "path": "/api/external/chat"
@@ -350,6 +342,10 @@
350
342
  "method": "GET",
351
343
  "path": "/api/wiki/{slug}"
352
344
  },
345
+ {
346
+ "method": "POST",
347
+ "path": "/api/wiki/{slug}/file"
348
+ },
353
349
  {
354
350
  "method": "POST",
355
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.24.0",
5
- "sourceSha": "501f361cc0aeaa8746e1387a11f6224ca66bca13",
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.25.0",
11
+ "apiVersion": "3.33.0",
12
12
  "openapiVersion": "3.0.3",
13
- "sha256": "f06bbd31d4e0e3fbf5f418a772616375f2a907535b0c7783060ba87b0565a0f7"
13
+ "sha256": "d673c2f38f63be2cd92415d9c384b40d115165ca4ba11bfb93ddaf4036226eed"
14
14
  }
15
15
  }
@@ -0,0 +1,45 @@
1
+ export const WORKFLOW_REMEDIATION_PATH = "/api/admin/workflow-remediation";
2
+ export const WORKFLOW_REMEDIATION_GUARDS = [
3
+ "intake.founder_identity",
4
+ "intake.reason_why",
5
+ ];
6
+
7
+ export function workflowRemediationBody(flags, requestId) {
8
+ const dealId = typeof flags.deal === "string" ? flags.deal.trim() : "";
9
+ const rawGuards = typeof flags.guard === "string" ? flags.guard : "";
10
+ const guardKeys = [...new Set(
11
+ rawGuards.split(",").map((value) => value.trim()).filter(Boolean),
12
+ )];
13
+ if (!dealId || guardKeys.length < 1 || guardKeys.length > 2 ||
14
+ guardKeys.some((key) => !WORKFLOW_REMEDIATION_GUARDS.includes(key))) {
15
+ throw new Error(
16
+ "Usage: llama admin workflow remediate --deal <uuid> " +
17
+ "--guard intake.reason_why[,intake.founder_identity] " +
18
+ "[--apply --expected-revision <n> --reason \"...\"]",
19
+ );
20
+ }
21
+
22
+ if (flags.apply !== true) {
23
+ if (flags["expected-revision"] !== undefined || flags.reason !== undefined) {
24
+ throw new Error("--expected-revision and --reason require --apply");
25
+ }
26
+ return { action: "preview", dealId, guardKeys };
27
+ }
28
+
29
+ const expectedRevision = Number(flags["expected-revision"]);
30
+ const reason = typeof flags.reason === "string" ? flags.reason.trim() : "";
31
+ if (!Number.isInteger(expectedRevision) || expectedRevision < 0 || !reason) {
32
+ throw new Error("--apply requires --expected-revision <n> from preview and --reason \"...\"");
33
+ }
34
+ if (typeof requestId !== "string" || !requestId) {
35
+ throw new Error("workflow remediation apply requires a request ID");
36
+ }
37
+ return {
38
+ action: "apply",
39
+ dealId,
40
+ guardKeys,
41
+ expectedRevision,
42
+ requestId,
43
+ reason,
44
+ };
45
+ }
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@llamaventures/cli",
3
- "version": "1.24.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 && 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
  }