@llamaventures/cli 2.0.2 → 2.1.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
@@ -44,6 +44,14 @@ working memory; it never becomes Page automatically. Page is the bounded,
44
44
  human-visible current projection, so patch the exact semantic fields described
45
45
  by the live contract instead of hiding structured analysis in `notes`.
46
46
 
47
+ The Markdown Deal Story is a separate sidecar domain, not a sixth Occam
48
+ resource. Use `get_deal_memory` / `llama memory read` for accumulated narrative
49
+ understanding, and `update_deal_memory` / `llama memory write` to replace its
50
+ complete Markdown. Read first and pass the returned opaque version on every
51
+ update; a conflict means read again and reconcile. These calls still go only
52
+ through authenticated Core. Never request AWS credentials or call S3 or the
53
+ sidecar directly.
54
+
47
55
  Every Page content field is Agent-writable. A field's `author` or
48
56
  `semanticOwner` describes attribution, not permission. Preserve a real human
49
57
  judgment with its author; otherwise label the value as an Agent proposal / 待人确认.
@@ -207,4 +215,5 @@ artifact, then decide whether its content deserves Information or a Page patch.
207
215
  `DEAL_COMMAND_RETIRED`. Upgrade the CLI and retry the intended four-action
208
216
  command.
209
217
  - Wiki, admin audit, external pitch, authentication, and skill discovery are
210
- separate domains. They do not expand the Deal action space.
218
+ separate domains. Deal Memory is also separate. None expands the four-action
219
+ Occam Deal space.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,44 @@
1
1
  # Changelog
2
2
 
3
+ All notable changes to `@llamaventures/cli` are documented here.
4
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/);
5
+ this project adheres to [Semantic Versioning](https://semver.org).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [2.1.0] - 2026-09-03
10
+
11
+ ### Added
12
+ - Add `llama memory read|write` and matching `get_deal_memory` /
13
+ `update_deal_memory` MCP tools for one canonical Markdown Deal Story, using
14
+ the existing Llama login through Core's authenticated sidecar adapter.
15
+
16
+ ### Changed
17
+ - Make the authenticated live bootstrap mandatory before Deal work and clarify
18
+ that it loads the server-owned two-part private Brain plus the separate Live
19
+ Deal Page field contract. CLI remains only the tool/transport layer.
20
+ - Teach CLI and MCP Agents that Information never updates Page automatically,
21
+ every Page content field is Agent-writable, and field authorship controls
22
+ attribution rather than permission.
23
+ - Pin the additive Deal Memory operations to Core API 5.1.0 while preserving
24
+ exactly four Occam Deal actions and five Occam resources.
25
+
26
+ ## [2.0.3] - 2026-08-31
27
+
28
+ - Show Core validation codes and field paths instead of collapsing malformed
29
+ Deal writes into `Invalid Occam command`.
30
+ - Publish `write_deal` as four operation-specific MCP command shapes while
31
+ retaining one mutation tool.
32
+ - Add copyable write examples to `llama help deal` and omit the duplicate
33
+ rendered briefing from structured bootstrap output.
34
+ - Let Core's private Page contract classify prose and enums instead of keeping
35
+ a second, drifting public-CLI field whitelist.
36
+ - Return a compact `page.patch` revision receipt with exact read-back fields
37
+ instead of echoing the full Live Deal Page after every write.
38
+ - Turn retired top-level commands such as `llama brief`, `fact`, `post`,
39
+ `timeline`, `html`, `memo`, and `pipeline` into self-correcting errors that
40
+ name the CLI 2 replacement and live contract entrypoint.
41
+
3
42
  ## [2.0.2] - 2026-08-31
4
43
 
5
44
  - Correct the authenticated runtime model: Brain has exactly two private
@@ -33,20 +72,6 @@
33
72
  - Keep Wiki, admin audit, external pitch, authentication, preferences, and
34
73
  progressive skill discovery as separate non-Deal domains.
35
74
 
36
- All notable changes to `@llamaventures/cli` are documented here.
37
- The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/);
38
- this project adheres to [Semantic Versioning](https://semver.org).
39
-
40
- ## [Unreleased]
41
-
42
- ### Changed
43
- - Make the authenticated live bootstrap mandatory before Deal work and clarify
44
- that it loads the server-owned two-part private Brain plus the separate Live
45
- Deal Page field contract. CLI remains only the tool/transport layer.
46
- - Teach CLI and MCP Agents that Information never updates Page automatically,
47
- every Page content field is Agent-writable, and field authorship controls
48
- attribution rather than permission.
49
-
50
75
  ## [1.26.0] - 2026-08-11
51
76
 
52
77
  ### Added
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:
@@ -121,6 +122,27 @@ For user-originated work, preserve the exact words in
121
122
  Chat and Event are system-owned. There is no caller-controlled Event append or
122
123
  general Chat-forging operation.
123
124
 
125
+ ## Deal Memory sidecar
126
+
127
+ Deal Memory is a separate domain alongside the five-resource Deal model. Each
128
+ deal can have one canonical, human-readable Markdown Deal Story:
129
+
130
+ ```bash
131
+ llama memory read <dealId>
132
+ llama memory read <dealId> --raw
133
+ llama memory write <dealId> --markdown deal-story.md
134
+ llama memory write <dealId> --markdown deal-story.md --expected-version '"etag-from-read"'
135
+ ```
136
+
137
+ The first write omits `--expected-version`. Updating an existing story requires
138
+ the opaque `version` returned by `read`; a stale value fails instead of losing a
139
+ concurrent edit. This uses the same `llama auth login` identity. CLI calls the
140
+ authenticated Command Core adapter only—it has no sidecar URL, service token,
141
+ S3 credentials, or direct database access.
142
+
143
+ `llama deal read --detail memory` still means structured Deal Information.
144
+ `llama memory read` means the separate accumulated Markdown Deal Story.
145
+
124
146
  ## Five Deal resources
125
147
 
126
148
  1. Live Deal Page — current human-visible state.
@@ -148,7 +170,9 @@ Its Deal surface is also exactly four tools:
148
170
  - `write_deal`
149
171
 
150
172
  Authentication, skill discovery, Wiki, admin audit, preferences, and external
151
- pitch remain separate non-Deal domains.
173
+ pitch remain separate non-Deal domains. Deal Memory likewise has two separate
174
+ MCP tools, `get_deal_memory` and `update_deal_memory`; it does not add a fifth
175
+ Occam Deal tool.
152
176
 
153
177
  ## Agent bootstrap
154
178
 
package/README.zh-CN.md CHANGED
@@ -116,6 +116,26 @@ llama deal write --json write.json
116
116
  Chat 和 Event 由系统拥有。Agent 不能自行伪造 Event 类型、顺序、作者、
117
117
  时间,也没有通用的 Chat 写入工具。
118
118
 
119
+ ## Deal Memory sidecar
120
+
121
+ Deal Memory 是与五资源 Deal 模型并行、完全解耦的领域。每个 Deal 可以有一个
122
+ 人和 Agent 都能直接阅读的 Markdown Deal Story:
123
+
124
+ ```bash
125
+ llama memory read <dealId>
126
+ llama memory read <dealId> --raw
127
+ llama memory write <dealId> --markdown deal-story.md
128
+ llama memory write <dealId> --markdown deal-story.md --expected-version '"read 返回的 etag"'
129
+ ```
130
+
131
+ 第一次创建时不传 `--expected-version`;以后更新必须使用前一次 `read` 返回的
132
+ 不透明 `version`,旧版本会安全失败,不会覆盖他人的新内容。它复用同一个
133
+ `llama auth login` 身份。CLI 只调用经过认证的 Command Core adapter,不持有
134
+ sidecar URL、service token、S3 凭证,也不直接访问数据库。
135
+
136
+ `llama deal read --detail memory` 仍表示结构化 Deal Information;
137
+ `llama memory read` 表示独立的 Markdown Deal Story。
138
+
119
139
  ## Deal 只有五种业务资源
120
140
 
121
141
  1. Live Deal Page:人类看到的当前状态;
@@ -141,7 +161,9 @@ MCP 的 Deal 工具同样恰好四个:
141
161
  - `write_deal`
142
162
 
143
163
  认证、skill discovery、Wiki、admin audit、preferences 和 external pitch
144
- 属于其他独立领域,不会扩大 Deal action space。
164
+ 属于其他独立领域,不会扩大 Deal action space。Deal Memory 同样是独立领域,
165
+ 对应 `get_deal_memory` 和 `update_deal_memory` 两个 MCP 工具,并不增加第五个
166
+ Occam Deal 工具。
145
167
 
146
168
  ## Agent 启动
147
169
 
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,8 +22,10 @@ import {
20
22
  import {
21
23
  buildDealReadPath,
22
24
  buildDealSearchPath,
25
+ compactDealWriteResult,
23
26
  prepareDealCommand,
24
27
  } from "../lib/deal-actions.mjs";
28
+ import { buildDealMemoryPath } from "../lib/deal-memory-actions.mjs";
25
29
 
26
30
  const requireFromHere = createRequire(import.meta.url);
27
31
  const { version: PKG_VERSION } = requireFromHere("../package.json");
@@ -36,12 +40,12 @@ function textResult(text, isError = false) {
36
40
  };
37
41
  }
38
42
 
39
- async function callApi(method, path, body) {
43
+ async function callApi(method, path, body, transform = (value) => value) {
40
44
  try {
41
- const result = await request(method, path, body);
45
+ const result = transform(await request(method, path, body));
42
46
  return textResult(typeof result === "string" ? result : JSON.stringify(result, null, 2));
43
47
  } catch (error) {
44
- return textResult(`Error: ${error?.message ?? String(error)}`, true);
48
+ return textResult(formatErrorForDisplay(error), true);
45
49
  }
46
50
  }
47
51
 
@@ -51,6 +55,54 @@ const originSchema = z.object({
51
55
  originatingChatRecordId: z.string().uuid().optional(),
52
56
  });
53
57
 
58
+ const writeDealCommandSchema = z.discriminatedUnion("operation", [
59
+ z.object({
60
+ operation: z.literal("input.submit"),
61
+ dealId: z.string().uuid(),
62
+ format: z.string().min(1).max(120).optional(),
63
+ content: z.any(),
64
+ source: z.record(z.string(), z.any()).optional(),
65
+ origin: originSchema,
66
+ idempotencyKey: z.string().min(1).max(240).optional(),
67
+ }).strict(),
68
+ z.object({
69
+ operation: z.literal("information.put"),
70
+ dealId: z.string().uuid(),
71
+ informationId: z.string().uuid().optional(),
72
+ type: z.string().min(1).max(120),
73
+ labels: z.array(z.string()).optional(),
74
+ subject: z.record(z.string(), z.any()).optional(),
75
+ value: z.any(),
76
+ expectedVersion: z.number().int().positive().optional(),
77
+ origin: originSchema,
78
+ idempotencyKey: z.string().min(1).max(240).optional(),
79
+ }).strict(),
80
+ z.object({
81
+ operation: z.literal("page.patch"),
82
+ dealId: z.string().uuid(),
83
+ patch: z.record(z.string(), z.any()),
84
+ expectedRevision: z.number().int().nonnegative().optional(),
85
+ origin: originSchema,
86
+ idempotencyKey: z.string().min(1).max(240).optional(),
87
+ }).strict(),
88
+ z.object({
89
+ operation: z.literal("artifact.put"),
90
+ dealId: z.string().uuid(),
91
+ artifactId: z.string().uuid().optional(),
92
+ kind: z.string().min(1).max(120),
93
+ title: z.string().min(1).max(500),
94
+ mimeType: z.string().min(1).max(240),
95
+ contentBase64: z.string().min(1).optional(),
96
+ storageKey: z.string().min(1).optional(),
97
+ storageUrl: z.string().url().optional(),
98
+ byteSize: z.number().int().nonnegative().optional(),
99
+ sha256: z.string().regex(/^[0-9a-f]{64}$/).optional(),
100
+ metadata: z.record(z.string(), z.any()).optional(),
101
+ origin: originSchema,
102
+ idempotencyKey: z.string().min(1).max(240).optional(),
103
+ }).strict(),
104
+ ]);
105
+
54
106
  // Deal is deliberately a closed four-tool surface. Do not add split resource
55
107
  // tools here: one more tool is one more branch every agent must reason about.
56
108
  server.registerTool(
@@ -70,7 +122,7 @@ server.registerTool(
70
122
  server.registerTool(
71
123
  "read_deal",
72
124
  {
73
- description: "Read one Deal. Live Page and its current private field contract are always returned; Information is memory and never updates Page automatically.",
125
+ 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
126
  inputSchema: {
75
127
  dealId: z.string().min(1),
76
128
  detail: z.enum(["overview", "memory", "files", "conversation", "history", "all"]).optional(),
@@ -99,36 +151,48 @@ server.registerTool(
99
151
  server.registerTool(
100
152
  "write_deal",
101
153
  {
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.",
154
+ 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.",
155
+ // MCP SDK 1.30 only publishes object schemas at the tool root. Nesting one
156
+ // command preserves the real discriminated union instead of flattening four
157
+ // incompatible operations into false affordances.
158
+ inputSchema: { command: writeDealCommandSchema },
159
+ },
160
+ async ({ command }) => callApi(
161
+ "POST",
162
+ "/api/occam/deals/commands",
163
+ prepareDealCommand("write", command),
164
+ (result) => compactDealWriteResult(command, result),
165
+ ),
166
+ );
167
+
168
+ // Deal Memory is a separate sidecar domain. These do not add an Occam Deal
169
+ // business resource or widen the deliberately closed four-tool Deal surface.
170
+ server.registerTool(
171
+ "get_deal_memory",
172
+ {
173
+ description: "Read the complete canonical Markdown Deal Story and its opaque concurrency version.",
174
+ inputSchema: { dealId: z.string().uuid() },
175
+ },
176
+ // @core-api-operation GET /api/deal-memory/{dealId}/story
177
+ async ({ dealId }) => callApi("GET", buildDealMemoryPath(dealId)),
178
+ );
179
+
180
+ server.registerTool(
181
+ "update_deal_memory",
182
+ {
183
+ description: "Create or replace the complete Markdown Deal Story. Existing stories require the version returned by get_deal_memory; stale writes fail safely.",
103
184
  inputSchema: {
104
- operation: z.enum(["input.submit", "information.put", "page.patch", "artifact.put"]),
105
185
  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(),
186
+ markdown: z.string().min(1).max(1_048_576),
187
+ expected_version: z.string().min(1).optional(),
129
188
  },
130
189
  },
131
- async (input) => callApi("POST", "/api/occam/deals/commands", prepareDealCommand("write", input)),
190
+ // @core-api-operation PUT /api/deal-memory/{dealId}/story
191
+ async ({ dealId, markdown, expected_version }) => callApi(
192
+ "PUT",
193
+ buildDealMemoryPath(dealId),
194
+ { markdown, ...(expected_version ? { expected_version } : {}) },
195
+ ),
132
196
  );
133
197
 
134
198
  server.registerTool(
@@ -158,7 +222,12 @@ server.registerTool(
158
222
  async ({ limit } = {}) => {
159
223
  const params = new URLSearchParams({ clientVersion: PKG_VERSION });
160
224
  if (limit) params.set("limit", String(limit));
161
- return callApi("GET", `/api/agent/manifest?${params}`);
225
+ try {
226
+ const result = await request("GET", `/api/agent/manifest?${params}`);
227
+ return textResult(JSON.stringify(structuredBootstrapManifest(result), null, 2));
228
+ } catch (error) {
229
+ return textResult(formatErrorForDisplay(error), true);
230
+ }
162
231
  },
163
232
  );
164
233
 
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,9 +35,14 @@ 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";
42
+ import {
43
+ buildDealMemoryPath,
44
+ readMarkdownInput,
45
+ } from "../lib/deal-memory-actions.mjs";
39
46
 
40
47
  const requireFromHere = createRequire(import.meta.url);
41
48
  const { version: PKG_VERSION } = requireFromHere("../package.json");
@@ -48,6 +55,10 @@ Deal has exactly four actions:
48
55
  llama deal create --json <file|->
49
56
  llama deal write --json <file|->
50
57
 
58
+ Deal Memory is a separate sidecar domain through the same login:
59
+ llama memory read <dealId> [--raw]
60
+ llama memory write <dealId> --markdown <file|-> [--expected-version <version>]
61
+
51
62
  Separate preserved domains:
52
63
  llama auth status|login|logout
53
64
  llama token set|show
@@ -82,13 +93,32 @@ read-modify-write complete Page arrays and preserve sibling slots.
82
93
  create JSON:
83
94
  {"companyName":"Acme","page":{},"information":[],"origin":{"kind":"user","originalUserUtterance":"..."}}
84
95
 
85
- write operation:
86
- input.submit | information.put | page.patch | artifact.put
96
+ write JSON (CLI derives idempotencyKey when omitted):
97
+ input.submit: {"operation":"input.submit","dealId":"<uuid>","content":"<original input>","format":"text","source":{},"origin":{"kind":"user","originalUserUtterance":"..."}}
98
+ 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":"..."}}
99
+ page.patch: {"operation":"page.patch","dealId":"<uuid>","patch":{"description":{"en":"...","zh":"..."}},"origin":{"kind":"agent"}}
100
+ 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":"..."}}
101
+
102
+ For Information, origin records who caused the write and preserves user words;
103
+ value.source records where the evidence came from.
87
104
 
88
105
  Core owns Chat Records, append-only Deal Events, Drive provisioning, audit,
89
106
  and idempotency. User-originated work must preserve exact wording in
90
107
  origin.originalUserUtterance or reference origin.originatingChatRecordId.`;
91
108
 
109
+ const HELP_MEMORY = `Llama Deal Memory — one canonical Markdown Deal Story
110
+
111
+ llama memory read <dealId> [--raw]
112
+ llama memory write <dealId> --markdown <file|-> [--expected-version <version>]
113
+
114
+ Read returns the full JSON record, including its opaque version. --raw prints
115
+ only the exact Markdown. A first write creates the story without a version.
116
+ Every later write must pass the version from the preceding read; stale writes
117
+ fail instead of overwriting newer understanding.
118
+
119
+ This uses your existing Llama authentication. The CLI talks only to Command
120
+ Core and never receives Deal Memory service credentials or AWS access.`;
121
+
92
122
  function parseFlags(args, allowed = null) {
93
123
  const flags = {};
94
124
  const positional = [];
@@ -115,7 +145,7 @@ function parseFlags(args, allowed = null) {
115
145
  }
116
146
 
117
147
  function usage(area) {
118
- console.log(area === "deal" ? HELP_DEAL : HELP_ROOT);
148
+ console.log(area === "deal" ? HELP_DEAL : area === "memory" ? HELP_MEMORY : HELP_ROOT);
119
149
  }
120
150
 
121
151
  function assertDealAction(area, action) {
@@ -127,6 +157,28 @@ function assertDealAction(area, action) {
127
157
  }
128
158
  }
129
159
 
160
+ const RETIRED_TOP_LEVEL_COMMANDS = Object.freeze({
161
+ brief: "llama deal write --json (operation: page.patch)",
162
+ fact: "llama deal write --json (operation: information.put)",
163
+ post: "llama deal write --json (operation: input.submit)",
164
+ timeline: "llama deal read <dealId> --detail history",
165
+ html: "llama deal write --json (operation: artifact.put)",
166
+ memo: "llama deal write --json (operation: artifact.put)",
167
+ pipeline: "llama deal search|read|create|write",
168
+ });
169
+
170
+ function unknownCommandError(area, action) {
171
+ const replacement = RETIRED_TOP_LEVEL_COMMANDS[area];
172
+ if (replacement) {
173
+ return new Error(
174
+ `\`llama ${area}\` was retired in CLI 2.0.\n` +
175
+ ` → Use: ${replacement}\n` +
176
+ " → Contract: llama agent bootstrap",
177
+ );
178
+ }
179
+ return new Error(`Unknown command: llama ${[area, action].filter(Boolean).join(" ")}`);
180
+ }
181
+
130
182
  function onboardingNoAuth() {
131
183
  return `Llama team onboarding requires credentials.
132
184
 
@@ -441,7 +493,7 @@ async function main() {
441
493
  const params = new URLSearchParams({ clientVersion: PKG_VERSION });
442
494
  if (flags.limit && flags.limit !== true) params.set("limit", String(flags.limit));
443
495
  const manifest = await request("GET", `/api/agent/manifest?${params}`);
444
- if (flags.json) print(manifest);
496
+ if (flags.json) print(structuredBootstrapManifest(manifest));
445
497
  else process.stdout.write(`${manifest.briefing || JSON.stringify(manifest, null, 2)}\n`);
446
498
  return;
447
499
  }
@@ -539,9 +591,37 @@ async function main() {
539
591
  if (area === "deal" && ["create", "write"].includes(action)) {
540
592
  const { flags } = parseFlags(rest, ["json"]);
541
593
  const input = await readJsonInput(flags.json);
542
- print(await request("POST", "/api/occam/deals/commands", prepareDealCommand(action, input)));
594
+ const command = prepareDealCommand(action, input);
595
+ const response = await request("POST", "/api/occam/deals/commands", command);
596
+ print(compactDealWriteResult(command, response));
597
+ return;
598
+ }
599
+
600
+ if (area === "memory" && action === "read") {
601
+ const { flags, positional } = parseFlags(rest, ["raw"]);
602
+ const path = buildDealMemoryPath(positional[0]);
603
+ // @core-api-operation GET /api/deal-memory/{dealId}/story
604
+ const result = await request("GET", path);
605
+ if (flags.raw) process.stdout.write(result.markdown);
606
+ else print(result);
543
607
  return;
544
608
  }
609
+ if (area === "memory" && action === "write") {
610
+ const { flags, positional } = parseFlags(rest, ["markdown", "expected-version"]);
611
+ const path = buildDealMemoryPath(positional[0]);
612
+ const markdown = await readMarkdownInput(flags.markdown);
613
+ // @core-api-operation PUT /api/deal-memory/{dealId}/story
614
+ print(await request("PUT", path, {
615
+ markdown,
616
+ ...(typeof flags["expected-version"] === "string"
617
+ ? { expected_version: flags["expected-version"] }
618
+ : {}),
619
+ }));
620
+ return;
621
+ }
622
+ if (area === "memory") {
623
+ throw new Error("Memory actions: read, write.");
624
+ }
545
625
 
546
626
  if (area === "wiki") {
547
627
  await handleWiki(action, rest);
@@ -565,12 +645,12 @@ async function main() {
565
645
  return;
566
646
  }
567
647
 
568
- throw new Error(`Unknown command: llama ${[area, action].filter(Boolean).join(" ")}`);
648
+ throw unknownCommandError(area, action);
569
649
  }
570
650
 
571
651
  main()
572
652
  .then(() => maybeNudgeUpdate())
573
653
  .catch((error) => {
574
- console.error(`Error: ${error.message}`);
654
+ console.error(formatErrorForDisplay(error));
575
655
  process.exit(1);
576
656
  });
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "format": "llama.core-api-contract.v1",
3
3
  "name": "llama-core-api",
4
- "apiVersion": "5.0.0",
4
+ "apiVersion": "5.1.0",
5
5
  "openapiVersion": "3.0.3",
6
- "sha256": "c48ed580f9930b06445f62b94959a2ca4bf9780e59473f17588e1e6554914014",
7
- "pathCount": 110,
8
- "operationCount": 136
6
+ "sha256": "948a8d02ae833e8cfa2302bc3c615fc1d3c792ae774ebc25b16be81fd1b85fee",
7
+ "pathCount": 111,
8
+ "operationCount": 138
9
9
  }
@@ -12,6 +12,7 @@
12
12
  { "method": "GET", "path": "/api/agent/preferences" },
13
13
  { "method": "GET", "path": "/api/agent/skills" },
14
14
  { "method": "GET", "path": "/api/agent/skills/{slug}" },
15
+ { "method": "GET", "path": "/api/deal-memory/{dealId}/story" },
15
16
  { "method": "GET", "path": "/api/me" },
16
17
  { "method": "GET", "path": "/api/oauth/authorize" },
17
18
  { "method": "GET", "path": "/api/occam/deals" },
@@ -28,6 +29,7 @@
28
29
  { "method": "POST", "path": "/api/oauth/token" },
29
30
  { "method": "POST", "path": "/api/occam/deals/commands" },
30
31
  { "method": "POST", "path": "/api/wiki/{slug}/restore" },
31
- { "method": "POST", "path": "/api/wiki/save" }
32
+ { "method": "POST", "path": "/api/wiki/save" },
33
+ { "method": "PUT", "path": "/api/deal-memory/{dealId}/story" }
32
34
  ]
33
35
  }
@@ -1,15 +1,15 @@
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.1.0",
5
+ "sourceSha": "0a29fd1ecdfb2064a90d36e323549a0445b3a21e",
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": "5.0.0",
11
+ "apiVersion": "5.1.0",
12
12
  "openapiVersion": "3.0.3",
13
- "sha256": "c48ed580f9930b06445f62b94959a2ca4bf9780e59473f17588e1e6554914014"
13
+ "sha256": "948a8d02ae833e8cfa2302bc3c615fc1d3c792ae774ebc25b16be81fd1b85fee"
14
14
  }
15
15
  }
package/lib/client.mjs CHANGED
@@ -294,6 +294,9 @@ function inferCommand(method, endpoint) {
294
294
  if (pathname === "/api/occam/deals") return "deal.search";
295
295
  if (pathname === "/api/occam/deals/commands") return "deal.write";
296
296
  if (/^\/api\/occam\/deals\/[^/]+$/.test(pathname)) return "deal.read";
297
+ if (/^\/api\/deal-memory\/[^/]+\/story$/.test(pathname)) {
298
+ return verb === "GET" ? "memory.read" : "memory.write";
299
+ }
297
300
  if (pathname === "/api/me") return "auth.status";
298
301
  if (pathname.startsWith("/api/agent/skills")) return "skills.read";
299
302
  if (pathname === "/api/agent/manifest") return "agent.bootstrap";
@@ -534,7 +537,21 @@ function apiResponseError(data, status) {
534
537
  return error;
535
538
  }
536
539
  if (data?.code !== "MENTION_APPROVAL_REQUIRED") {
537
- const error = new Error(message);
540
+ const issues = Array.isArray(data?.issues)
541
+ ? data.issues.map((issue) => {
542
+ const path = Array.isArray(issue?.path) && issue.path.length
543
+ ? issue.path.reduce(
544
+ (result, part) => typeof part === "number"
545
+ ? `${result}[${part}]`
546
+ : result ? `${result}.${part}` : String(part),
547
+ "",
548
+ )
549
+ : "$";
550
+ const detail = issue?.message || issue?.code || "Invalid value";
551
+ return `- ${path}: ${detail}`;
552
+ })
553
+ : [];
554
+ const error = new Error([message, ...issues].join("\n"));
538
555
  if (typeof data?.code === "string") error.code = data.code;
539
556
  error.details = data;
540
557
  error.status = status;
@@ -563,6 +580,21 @@ function apiResponseError(data, status) {
563
580
  return error;
564
581
  }
565
582
 
583
+ export function formatErrorForDisplay(error) {
584
+ const message = error?.message ?? String(error);
585
+ if (/^Error(?:\[[^\]]+\])?:/u.test(message)) return message;
586
+ const code = typeof error?.code === "string" ? `[${error.code}]` : "";
587
+ return `Error${code}: ${message}`;
588
+ }
589
+
590
+ export function structuredBootstrapManifest(manifest) {
591
+ if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
592
+ return manifest;
593
+ }
594
+ const { briefing: _renderedDuplicate, ...structured } = manifest;
595
+ return structured;
596
+ }
597
+
566
598
  async function requestWithRetry(method, endpoint, body, opts, allowRetry) {
567
599
  const authHeaders = await getAuthHeaders();
568
600
  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
  }
@@ -0,0 +1,31 @@
1
+ import { readFile } from "node:fs/promises";
2
+
3
+ export const MAX_DEAL_STORY_BYTES = 1_048_576;
4
+
5
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
6
+
7
+ export function buildDealMemoryPath(dealId) {
8
+ if (!UUID.test(String(dealId || ""))) {
9
+ throw new Error("Deal Memory requires a valid deal UUID");
10
+ }
11
+ return `/api/deal-memory/${encodeURIComponent(dealId)}/story`;
12
+ }
13
+
14
+ export async function readMarkdownInput(path, stdin = process.stdin) {
15
+ if (!path || path === true) throw new Error("--markdown <file|-> is required");
16
+ const markdown = path === "-"
17
+ ? await new Promise((resolve, reject) => {
18
+ let value = "";
19
+ stdin.setEncoding("utf8");
20
+ stdin.on("data", (chunk) => { value += chunk; });
21
+ stdin.on("end", () => resolve(value));
22
+ stdin.on("error", reject);
23
+ })
24
+ : await readFile(path, "utf8");
25
+ if (!markdown) throw new Error("Deal Story Markdown cannot be empty");
26
+ const bytes = Buffer.byteLength(markdown, "utf8");
27
+ if (bytes > MAX_DEAL_STORY_BYTES) {
28
+ throw new Error(`Deal Story is ${bytes} bytes; maximum is ${MAX_DEAL_STORY_BYTES}`);
29
+ }
30
+ return markdown;
31
+ }
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@llamaventures/cli",
3
- "version": "2.0.2",
3
+ "version": "2.1.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/deal-actions-cli.test.mjs scripts/deal-actions.test.mjs scripts/mcp-four-action-surface.test.mjs scripts/server-compatibility.test.mjs scripts/verify-core-api-contract.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/deal-actions-cli.test.mjs scripts/deal-actions.test.mjs scripts/deal-memory-actions-cli.test.mjs scripts/deal-memory-actions.test.mjs scripts/mcp-four-action-surface.test.mjs scripts/server-compatibility.test.mjs scripts/verify-core-api-contract.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",