@llamaventures/cli 2.0.2 → 2.0.3

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/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.0.3] - 2026-08-31
4
+
5
+ - Show Core validation codes and field paths instead of collapsing malformed
6
+ Deal writes into `Invalid Occam command`.
7
+ - Publish `write_deal` as four operation-specific MCP command shapes while
8
+ retaining one mutation tool.
9
+ - Add copyable write examples to `llama help deal` and omit the duplicate
10
+ rendered briefing from structured bootstrap output.
11
+ - Let Core's private Page contract classify prose and enums instead of keeping
12
+ a second, drifting public-CLI field whitelist.
13
+ - Return a compact `page.patch` revision receipt with exact read-back fields
14
+ instead of echoing the full Live Deal Page after every write.
15
+ - Turn retired top-level commands such as `llama brief`, `fact`, `post`,
16
+ `timeline`, `html`, `memo`, and `pipeline` into self-correcting errors that
17
+ name the CLI 2 replacement and live contract entrypoint.
18
+
3
19
  ## [2.0.2] - 2026-08-31
4
20
 
5
21
  - Correct the authenticated runtime model: Brain has exactly two private
package/README.md CHANGED
@@ -90,8 +90,9 @@ When Page prose is human-visible, write one bilingual field value as
90
90
  `{"en":"natural English","zh":"自然中文"}`. The Web language switch selects
91
91
  the reader's version from the same Page revision. Keep language-neutral names,
92
92
  enums, URLs, numbers, dates, and source IDs scalar. Information and raw Input
93
- stay in their original language with provenance. The CLI rejects new scalar
94
- Page prose and incomplete language pairs before they reach Core; historical
93
+ stay in their original language with provenance. Core rejects new scalar Page
94
+ prose according to its private Page contract; the public CLI catches malformed
95
+ half-localized pairs without maintaining a second field whitelist. Historical
95
96
  scalar prose remains readable.
96
97
 
97
98
  Raw user input example:
package/bin/llama-mcp.mjs CHANGED
@@ -5,10 +5,12 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
6
  import { z } from "zod";
7
7
  import {
8
+ formatErrorForDisplay,
8
9
  getAuthHeaders,
9
10
  readBriefing,
10
11
  request,
11
12
  setClientRuntime,
13
+ structuredBootstrapManifest,
12
14
  } from "../lib/client.mjs";
13
15
  import {
14
16
  clearExternalSession,
@@ -20,6 +22,7 @@ import {
20
22
  import {
21
23
  buildDealReadPath,
22
24
  buildDealSearchPath,
25
+ compactDealWriteResult,
23
26
  prepareDealCommand,
24
27
  } from "../lib/deal-actions.mjs";
25
28
 
@@ -36,12 +39,12 @@ function textResult(text, isError = false) {
36
39
  };
37
40
  }
38
41
 
39
- async function callApi(method, path, body) {
42
+ async function callApi(method, path, body, transform = (value) => value) {
40
43
  try {
41
- const result = await request(method, path, body);
44
+ const result = transform(await request(method, path, body));
42
45
  return textResult(typeof result === "string" ? result : JSON.stringify(result, null, 2));
43
46
  } catch (error) {
44
- return textResult(`Error: ${error?.message ?? String(error)}`, true);
47
+ return textResult(formatErrorForDisplay(error), true);
45
48
  }
46
49
  }
47
50
 
@@ -51,6 +54,54 @@ const originSchema = z.object({
51
54
  originatingChatRecordId: z.string().uuid().optional(),
52
55
  });
53
56
 
57
+ const writeDealCommandSchema = z.discriminatedUnion("operation", [
58
+ z.object({
59
+ operation: z.literal("input.submit"),
60
+ dealId: z.string().uuid(),
61
+ format: z.string().min(1).max(120).optional(),
62
+ content: z.any(),
63
+ source: z.record(z.string(), z.any()).optional(),
64
+ origin: originSchema,
65
+ idempotencyKey: z.string().min(1).max(240).optional(),
66
+ }).strict(),
67
+ z.object({
68
+ operation: z.literal("information.put"),
69
+ dealId: z.string().uuid(),
70
+ informationId: z.string().uuid().optional(),
71
+ type: z.string().min(1).max(120),
72
+ labels: z.array(z.string()).optional(),
73
+ subject: z.record(z.string(), z.any()).optional(),
74
+ value: z.any(),
75
+ expectedVersion: z.number().int().positive().optional(),
76
+ origin: originSchema,
77
+ idempotencyKey: z.string().min(1).max(240).optional(),
78
+ }).strict(),
79
+ z.object({
80
+ operation: z.literal("page.patch"),
81
+ dealId: z.string().uuid(),
82
+ patch: z.record(z.string(), z.any()),
83
+ expectedRevision: z.number().int().nonnegative().optional(),
84
+ origin: originSchema,
85
+ idempotencyKey: z.string().min(1).max(240).optional(),
86
+ }).strict(),
87
+ z.object({
88
+ operation: z.literal("artifact.put"),
89
+ dealId: z.string().uuid(),
90
+ artifactId: z.string().uuid().optional(),
91
+ kind: z.string().min(1).max(120),
92
+ title: z.string().min(1).max(500),
93
+ mimeType: z.string().min(1).max(240),
94
+ contentBase64: z.string().min(1).optional(),
95
+ storageKey: z.string().min(1).optional(),
96
+ storageUrl: z.string().url().optional(),
97
+ byteSize: z.number().int().nonnegative().optional(),
98
+ sha256: z.string().regex(/^[0-9a-f]{64}$/).optional(),
99
+ metadata: z.record(z.string(), z.any()).optional(),
100
+ origin: originSchema,
101
+ idempotencyKey: z.string().min(1).max(240).optional(),
102
+ }).strict(),
103
+ ]);
104
+
54
105
  // Deal is deliberately a closed four-tool surface. Do not add split resource
55
106
  // tools here: one more tool is one more branch every agent must reason about.
56
107
  server.registerTool(
@@ -70,7 +121,7 @@ server.registerTool(
70
121
  server.registerTool(
71
122
  "read_deal",
72
123
  {
73
- description: "Read one Deal. Live Page and its current private field contract are always returned; Information is memory and never updates Page automatically.",
124
+ description: "Read one Deal. Live Page is current human-visible state; Information is memory and never updates Page automatically. Page field semantics come from agent_bootstrap, not every Deal read.",
74
125
  inputSchema: {
75
126
  dealId: z.string().min(1),
76
127
  detail: z.enum(["overview", "memory", "files", "conversation", "history", "all"]).optional(),
@@ -99,36 +150,18 @@ server.registerTool(
99
150
  server.registerTool(
100
151
  "write_deal",
101
152
  {
102
- description: "The only Deal mutation tool: input.submit, information.put, page.patch, or artifact.put. All Page content fields are Agent-writable; author controls attribution, not permission. Use the read/bootstrap field contract and bilingual Page prose. page.patch is JSON Merge Patch: arrays replace whole arrays, so read-modify-write and preserve sibling slots.",
103
- inputSchema: {
104
- operation: z.enum(["input.submit", "information.put", "page.patch", "artifact.put"]),
105
- dealId: z.string().uuid(),
106
- patch: z.record(z.string(), z.any()).optional(),
107
- expectedRevision: z.number().int().nonnegative().optional(),
108
- informationId: z.string().uuid().optional(),
109
- type: z.string().min(1).max(120).optional(),
110
- labels: z.array(z.string()).optional(),
111
- subject: z.record(z.string(), z.any()).optional(),
112
- value: z.any().optional(),
113
- expectedVersion: z.number().int().positive().optional(),
114
- format: z.string().min(1).max(120).optional(),
115
- content: z.any().optional(),
116
- source: z.record(z.string(), z.any()).optional(),
117
- artifactId: z.string().uuid().optional(),
118
- kind: z.string().min(1).max(120).optional(),
119
- title: z.string().min(1).max(500).optional(),
120
- mimeType: z.string().min(1).max(240).optional(),
121
- contentBase64: z.string().min(1).optional(),
122
- storageKey: z.string().min(1).optional(),
123
- storageUrl: z.string().url().optional(),
124
- byteSize: z.number().int().nonnegative().optional(),
125
- sha256: z.string().regex(/^[0-9a-f]{64}$/).optional(),
126
- metadata: z.record(z.string(), z.any()).optional(),
127
- origin: originSchema,
128
- idempotencyKey: z.string().min(1).max(240).optional(),
129
- },
153
+ description: "The only Deal mutation tool. Pass one command: input.submit, information.put, page.patch, or artifact.put. All Page content fields are Agent-writable; author controls attribution, not permission. Use the bootstrap field contract and bilingual Page prose. page.patch is JSON Merge Patch: arrays replace whole arrays, so read-modify-write and preserve sibling slots.",
154
+ // MCP SDK 1.30 only publishes object schemas at the tool root. Nesting one
155
+ // command preserves the real discriminated union instead of flattening four
156
+ // incompatible operations into false affordances.
157
+ inputSchema: { command: writeDealCommandSchema },
130
158
  },
131
- async (input) => callApi("POST", "/api/occam/deals/commands", prepareDealCommand("write", input)),
159
+ async ({ command }) => callApi(
160
+ "POST",
161
+ "/api/occam/deals/commands",
162
+ prepareDealCommand("write", command),
163
+ (result) => compactDealWriteResult(command, result),
164
+ ),
132
165
  );
133
166
 
134
167
  server.registerTool(
@@ -158,7 +191,12 @@ server.registerTool(
158
191
  async ({ limit } = {}) => {
159
192
  const params = new URLSearchParams({ clientVersion: PKG_VERSION });
160
193
  if (limit) params.set("limit", String(limit));
161
- return callApi("GET", `/api/agent/manifest?${params}`);
194
+ try {
195
+ const result = await request("GET", `/api/agent/manifest?${params}`);
196
+ return textResult(JSON.stringify(structuredBootstrapManifest(result), null, 2));
197
+ } catch (error) {
198
+ return textResult(formatErrorForDisplay(error), true);
199
+ }
162
200
  },
163
201
  );
164
202
 
package/bin/llama.mjs CHANGED
@@ -4,6 +4,7 @@ import { createRequire } from "node:module";
4
4
  import { readFile } from "node:fs/promises";
5
5
  import readline from "node:readline";
6
6
  import {
7
+ formatErrorForDisplay,
7
8
  getAuthHeaders,
8
9
  getBaseUrl,
9
10
  getToken,
@@ -12,6 +13,7 @@ import {
12
13
  readCanonicalToken,
13
14
  readLegacyConfig,
14
15
  request,
16
+ structuredBootstrapManifest,
15
17
  tryGcloudIdentityToken,
16
18
  writeCanonicalToken,
17
19
  writeLegacyConfig,
@@ -33,6 +35,7 @@ import {
33
35
  DEAL_ACTIONS,
34
36
  buildDealReadPath,
35
37
  buildDealSearchPath,
38
+ compactDealWriteResult,
36
39
  prepareDealCommand,
37
40
  readJsonInput,
38
41
  } from "../lib/deal-actions.mjs";
@@ -82,8 +85,14 @@ read-modify-write complete Page arrays and preserve sibling slots.
82
85
  create JSON:
83
86
  {"companyName":"Acme","page":{},"information":[],"origin":{"kind":"user","originalUserUtterance":"..."}}
84
87
 
85
- write operation:
86
- input.submit | information.put | page.patch | artifact.put
88
+ write JSON (CLI derives idempotencyKey when omitted):
89
+ input.submit: {"operation":"input.submit","dealId":"<uuid>","content":"<original input>","format":"text","source":{},"origin":{"kind":"user","originalUserUtterance":"..."}}
90
+ information.put:{"operation":"information.put","dealId":"<uuid>","type":"founder_claim","labels":["founder-stated"],"subject":{"topic":"traction"},"value":{"content":"...","source":{"kind":"first_meeting_transcript"}},"origin":{"kind":"user","originalUserUtterance":"..."}}
91
+ page.patch: {"operation":"page.patch","dealId":"<uuid>","patch":{"description":{"en":"...","zh":"..."}},"origin":{"kind":"agent"}}
92
+ artifact.put: {"operation":"artifact.put","dealId":"<uuid>","kind":"deck","title":"deck.pdf","mimeType":"application/pdf","storageKey":"<drive-file-id>","storageUrl":"https://drive.google.com/...","byteSize":123,"sha256":"<64 hex>","origin":{"kind":"user","originalUserUtterance":"..."}}
93
+
94
+ For Information, origin records who caused the write and preserves user words;
95
+ value.source records where the evidence came from.
87
96
 
88
97
  Core owns Chat Records, append-only Deal Events, Drive provisioning, audit,
89
98
  and idempotency. User-originated work must preserve exact wording in
@@ -127,6 +136,28 @@ function assertDealAction(area, action) {
127
136
  }
128
137
  }
129
138
 
139
+ const RETIRED_TOP_LEVEL_COMMANDS = Object.freeze({
140
+ brief: "llama deal write --json (operation: page.patch)",
141
+ fact: "llama deal write --json (operation: information.put)",
142
+ post: "llama deal write --json (operation: input.submit)",
143
+ timeline: "llama deal read <dealId> --detail history",
144
+ html: "llama deal write --json (operation: artifact.put)",
145
+ memo: "llama deal write --json (operation: artifact.put)",
146
+ pipeline: "llama deal search|read|create|write",
147
+ });
148
+
149
+ function unknownCommandError(area, action) {
150
+ const replacement = RETIRED_TOP_LEVEL_COMMANDS[area];
151
+ if (replacement) {
152
+ return new Error(
153
+ `\`llama ${area}\` was retired in CLI 2.0.\n` +
154
+ ` → Use: ${replacement}\n` +
155
+ " → Contract: llama agent bootstrap",
156
+ );
157
+ }
158
+ return new Error(`Unknown command: llama ${[area, action].filter(Boolean).join(" ")}`);
159
+ }
160
+
130
161
  function onboardingNoAuth() {
131
162
  return `Llama team onboarding requires credentials.
132
163
 
@@ -441,7 +472,7 @@ async function main() {
441
472
  const params = new URLSearchParams({ clientVersion: PKG_VERSION });
442
473
  if (flags.limit && flags.limit !== true) params.set("limit", String(flags.limit));
443
474
  const manifest = await request("GET", `/api/agent/manifest?${params}`);
444
- if (flags.json) print(manifest);
475
+ if (flags.json) print(structuredBootstrapManifest(manifest));
445
476
  else process.stdout.write(`${manifest.briefing || JSON.stringify(manifest, null, 2)}\n`);
446
477
  return;
447
478
  }
@@ -539,7 +570,9 @@ async function main() {
539
570
  if (area === "deal" && ["create", "write"].includes(action)) {
540
571
  const { flags } = parseFlags(rest, ["json"]);
541
572
  const input = await readJsonInput(flags.json);
542
- print(await request("POST", "/api/occam/deals/commands", prepareDealCommand(action, input)));
573
+ const command = prepareDealCommand(action, input);
574
+ const response = await request("POST", "/api/occam/deals/commands", command);
575
+ print(compactDealWriteResult(command, response));
543
576
  return;
544
577
  }
545
578
 
@@ -565,12 +598,12 @@ async function main() {
565
598
  return;
566
599
  }
567
600
 
568
- throw new Error(`Unknown command: llama ${[area, action].filter(Boolean).join(" ")}`);
601
+ throw unknownCommandError(area, action);
569
602
  }
570
603
 
571
604
  main()
572
605
  .then(() => maybeNudgeUpdate())
573
606
  .catch((error) => {
574
- console.error(`Error: ${error.message}`);
607
+ console.error(formatErrorForDisplay(error));
575
608
  process.exit(1);
576
609
  });
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "format": "llama.cli-build.v1",
3
3
  "packageName": "@llamaventures/cli",
4
- "packageVersion": "2.0.2",
5
- "sourceSha": "dc52c43363b050072c70f45144909d93b4f42028",
4
+ "packageVersion": "2.0.3",
5
+ "sourceSha": "dc1618ab65b6cb2e73b107e7d56315ce2076744e",
6
6
  "sourceKind": "github",
7
7
  "sourceDirty": false,
8
8
  "coreApiContract": {
package/lib/client.mjs CHANGED
@@ -534,7 +534,21 @@ function apiResponseError(data, status) {
534
534
  return error;
535
535
  }
536
536
  if (data?.code !== "MENTION_APPROVAL_REQUIRED") {
537
- const error = new Error(message);
537
+ const issues = Array.isArray(data?.issues)
538
+ ? data.issues.map((issue) => {
539
+ const path = Array.isArray(issue?.path) && issue.path.length
540
+ ? issue.path.reduce(
541
+ (result, part) => typeof part === "number"
542
+ ? `${result}[${part}]`
543
+ : result ? `${result}.${part}` : String(part),
544
+ "",
545
+ )
546
+ : "$";
547
+ const detail = issue?.message || issue?.code || "Invalid value";
548
+ return `- ${path}: ${detail}`;
549
+ })
550
+ : [];
551
+ const error = new Error([message, ...issues].join("\n"));
538
552
  if (typeof data?.code === "string") error.code = data.code;
539
553
  error.details = data;
540
554
  error.status = status;
@@ -563,6 +577,21 @@ function apiResponseError(data, status) {
563
577
  return error;
564
578
  }
565
579
 
580
+ export function formatErrorForDisplay(error) {
581
+ const message = error?.message ?? String(error);
582
+ if (/^Error(?:\[[^\]]+\])?:/u.test(message)) return message;
583
+ const code = typeof error?.code === "string" ? `[${error.code}]` : "";
584
+ return `Error${code}: ${message}`;
585
+ }
586
+
587
+ export function structuredBootstrapManifest(manifest) {
588
+ if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
589
+ return manifest;
590
+ }
591
+ const { briefing: _renderedDuplicate, ...structured } = manifest;
592
+ return structured;
593
+ }
594
+
566
595
  async function requestWithRetry(method, endpoint, body, opts, allowRetry) {
567
596
  const authHeaders = await getAuthHeaders();
568
597
  if (Object.keys(authHeaders).length === 0) throw noAuthError();
@@ -72,62 +72,6 @@ function validateLocalizedTextPairs(value, path = "page") {
72
72
  }
73
73
  }
74
74
 
75
- const LANGUAGE_NEUTRAL_PAGE_KEYS = new Set([
76
- "companyName", "companyKey", "state", "pipelineStatus", "stage",
77
- "theirStage", "ownerUserId", "sourceUserId", "website", "dealOwner",
78
- "source", "sourceDirection", "proposedAmount", "roundSize", "valuation",
79
- "founders", "deckLink", "location", "foundedYear", "leadInvestor",
80
- "dateAdded", "tab", "sourcePerson", "createdByName", "createdByLabel",
81
- "createdVia", "id", "name", "key", "slug", "url", "href", "email",
82
- "linkedin", "handle", "status", "kind", "type", "tier", "confidence",
83
- "currency", "unit", "format", "mimeType", "sha256", "timestamp",
84
- "date", "year",
85
- ]);
86
- const LANGUAGE_NEUTRAL_PAGE_CONTAINERS = new Set([
87
- "manualTags", "investors", "founderEmails", "labels", "tags",
88
- ]);
89
- const OPAQUE_PAGE_CONTAINERS = new Set([
90
- "migration", "extra", "fieldProvenance",
91
- ]);
92
-
93
- function pathKeys(path) {
94
- return path.match(/[^.[\]]+/g)?.filter((part) => !/^\d+$/u.test(part)) ?? [];
95
- }
96
-
97
- function isLanguageNeutralPagePath(path) {
98
- const keys = pathKeys(path);
99
- if (keys.some((key) => OPAQUE_PAGE_CONTAINERS.has(key))) return true;
100
- if (keys.some((key) => LANGUAGE_NEUTRAL_PAGE_CONTAINERS.has(key))) return true;
101
- const key = keys.at(-1) ?? "";
102
- if (LANGUAGE_NEUTRAL_PAGE_KEYS.has(key)) return true;
103
- return /(?:Id|Ids|URL|Url|URI|Uri|Link|Email|Emails|At|Date|Year|Key|Slug|Hash)$/u.test(key);
104
- }
105
-
106
- function validateAgentAuthoredPageProse(value, path = "page") {
107
- if (typeof value === "string") {
108
- if (!isLanguageNeutralPagePath(path)) {
109
- throw new Error(
110
- `${path} agent-authored prose must be one value with non-empty en and zh`,
111
- );
112
- }
113
- return;
114
- }
115
- if (Array.isArray(value)) {
116
- value.forEach((item, index) => validateAgentAuthoredPageProse(item, `${path}[${index}]`));
117
- return;
118
- }
119
- if (!value || typeof value !== "object") return;
120
- if (
121
- Object.prototype.hasOwnProperty.call(value, "en") ||
122
- Object.prototype.hasOwnProperty.call(value, "zh")
123
- ) {
124
- return;
125
- }
126
- for (const [key, child] of Object.entries(value)) {
127
- validateAgentAuthoredPageProse(child, `${path}.${key}`);
128
- }
129
- }
130
-
131
75
  export async function readJsonInput(path, stdin = process.stdin) {
132
76
  if (!path || path === true) throw new Error("--json <file|-> is required");
133
77
  const text = path === "-"
@@ -170,6 +114,32 @@ export function buildDealReadPath(dealId, detail = "overview") {
170
114
  return `/api/occam/deals/${encodeURIComponent(dealId)}${expansion ? `?expand=${expansion}` : ""}`;
171
115
  }
172
116
 
117
+ /**
118
+ * page.patch previously echoed the entire Live Deal Page after every write.
119
+ * Keep the authoritative revision receipt and tell the Agent exactly what to
120
+ * read back, without paying another full-page context cost before verification.
121
+ */
122
+ export function compactDealWriteResult(command, response) {
123
+ if (command?.operation !== "page.patch" || !response?.result?.page) return response;
124
+ const page = response.result.page;
125
+ return {
126
+ ...response,
127
+ result: {
128
+ idempotent: Boolean(response.result.idempotent),
129
+ page: {
130
+ id: page.id,
131
+ revision: page.revision,
132
+ updatedAt: page.updatedAt,
133
+ },
134
+ verify: {
135
+ dealId: command.dealId,
136
+ fields: Object.keys(command.patch ?? {}),
137
+ command: `llama deal read ${command.dealId}`,
138
+ },
139
+ },
140
+ };
141
+ }
142
+
173
143
  export function prepareDealCommand(action, input) {
174
144
  if (!input || typeof input !== "object" || Array.isArray(input)) {
175
145
  throw new Error("Mutation JSON must be an object");
@@ -183,11 +153,9 @@ export function prepareDealCommand(action, input) {
183
153
  }
184
154
  if (action === "create") {
185
155
  validateLocalizedTextPairs(command.page ?? {});
186
- validateAgentAuthoredPageProse(command.page ?? {});
187
156
  }
188
157
  if (action === "write" && command.operation === "page.patch") {
189
158
  validateLocalizedTextPairs(command.patch ?? {});
190
- validateAgentAuthoredPageProse(command.patch ?? {});
191
159
  }
192
160
  return ensureIdempotencyKey(validateCommandOrigin(command));
193
161
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llamaventures/cli",
3
- "version": "2.0.2",
3
+ "version": "2.0.3",
4
4
  "description": "CLI + MCP server for the Llama Ventures investment workbench (command.llamaventures.vc).",
5
5
  "type": "module",
6
6
  "scripts": {