@llamaventures/cli 2.6.0 → 2.8.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
@@ -93,6 +93,8 @@ Before researching or creating a company:
93
93
  1. `llama deal search "<company, founder or topic>"`. Core API 5.41.0+ also searches descriptions and sector/subsector in either stored language. Use `--sector "robotics"` alone or combine it with a query and `--state`/`--limit`; filters apply before the limit. These are literal substrings, not semantic synonyms. A sector filter requires server confirmation; never treat an older unfiltered response as sector-filtered results.
94
94
  Use short keywords. Inspect `searchMatch` fields and `search.has_more`; truncated results are not exhaustive. On zero matches, broaden the sector or wording. Current Page updates are immediately searchable without manual index or tag maintenance.
95
95
  2. Read the best candidate with `--detail overview` and verify its specific category; a broad sector match does not establish a narrower classification.
96
+ For "which Deals are invested / in diligence / latest investments", never guess from names: run `llama deal search --status Invested --limit 200` (Core API 5.42.0+) and order by `investmentDate`/`markedInvestedAt`, stating how many Deals you checked.
97
+ To answer from Information, prefer a targeted read over `--detail memory` (Core API 5.42.0+): `--information-q "<text>"` for snippets, `--information-type <type>` for one kind of record, and `--information-id <id>` for full values. Values over 8,000 characters in text/type reads are omitted; read that id. Never treat an unconfirmed (older server) response as filtered.
96
98
  3. Expand only the resource needed for the task.
97
99
  4. Create only when no existing company matches.
98
100
 
@@ -252,6 +254,17 @@ request, and verify the exact Artifact version and hash through Command.
252
254
  separate domains. Deal Memory is also separate. None expands the four-action
253
255
  Occam Deal space.
254
256
 
257
+ ## Wiki publishing
258
+
259
+ Read `contract.wiki_publishing` from authenticated bootstrap and use
260
+ `llama help wiki` for the save commands. New authorized publications default
261
+ to complete self-contained HTML. Preserve full report content, charts,
262
+ interactions and existing language/version markers, with rendering assets
263
+ embedded. Read the exact slug and language before edits and preserve its
264
+ format. Honor explicit Markdown requests. For MCP, set `content_type: html`
265
+ and the intended `lang`. Verify the save receipt and canonical page before
266
+ reporting completion.
267
+
255
268
  ## Report UX friction
256
269
 
257
270
  When a user, agent or both encounter a concrete obstacle (including a task that
package/CHANGELOG.md CHANGED
@@ -6,6 +6,51 @@ this project adheres to [Semantic Versioning](https://semver.org).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [2.8.0] - 2026-11-12
10
+
11
+ ### Added
12
+
13
+ - Export the API-backed MCP tool catalog from `lib/mcp-api-tools.mjs`.
14
+ `registerApiTools(server, { request, formatError, version, only })` registers
15
+ the Deal, Deal Memory, bootstrap, Page schema, skills, preference and Wiki
16
+ tools against an injected request function, so a remote MCP host can serve
17
+ the same catalog as the stdio server. The module also exports
18
+ `writeDealCommandSchema`. Tools that need local credentials or files stay in
19
+ `llama-mcp`. No tool name, schema or behavior changed for stdio users.
20
+
21
+ ### Changed
22
+
23
+ - `write_deal` and `wiki_save` descriptions each name the other as the
24
+ alternative destination: a file or HTML about one company is a Deal
25
+ Artifact; knowledge not tied to one company belongs in the Wiki.
26
+
27
+ ## [2.7.1] - 2026-11-10
28
+
29
+ ### Added
30
+
31
+ - Add `llama help wiki`, save usage guidance, and MCP publishing descriptions
32
+ that lead agents to the current authenticated Wiki publishing contract and
33
+ complete self-contained HTML. Existing Markdown save behavior is preserved.
34
+ - Allow MCP `wiki_save` to send `lang`, matching CLI and Core support for
35
+ language-specific publications and updates.
36
+
37
+ ## [2.7.0] - 2026-11-05
38
+
39
+ ### Added
40
+
41
+ - Read Deal Information selectively with `llama deal read <dealId>
42
+ --information-id <ids> | --information-type <types> | --information-q <text>`
43
+ or MCP `read_deal.informationIds/informationTypes/informationQuery`, instead
44
+ of loading all memory with `--detail memory`. Core API 5.42.0+ returns full
45
+ values for id reads and snippets for text reads, omits values over 8,000
46
+ characters in text/type reads, and echoes the applied filter; responses from
47
+ an older Core that ignored the filter are rejected.
48
+ - Filter Deal search by pipeline status with `llama deal search --status
49
+ Invested[,Diligence]` or MCP `search_deals.status`, alone or combined with a
50
+ query, sector, state and limit, so "every invested Deal" is one bounded query
51
+ instead of a guess. Core API 5.42.0+ echoes the applied statuses; an older
52
+ Core is rejected.
53
+
9
54
  ## [2.6.0] - 2026-11-02
10
55
 
11
56
  ### Added
package/README.md CHANGED
@@ -68,6 +68,15 @@ llama deal search "humanoid" --sector "robotics" --state active
68
68
  ```
69
69
 
70
70
  `--sector` searches sector and subsector and can be used without a query.
71
+ `--status` matches the current pipeline status exactly (any of a comma list)
72
+ and also works alone; with Core API 5.42.0+ it enumerates, for example, every
73
+ invested Deal in one bounded query:
74
+
75
+ ```bash
76
+ llama deal search --status Invested --limit 200
77
+ ```
78
+
79
+ Results carry `investmentDate` and `markedInvestedAt` for ordering.
71
80
  MCP `search_deals` accepts the same optional `q`, `sector`, `state` and `limit`.
72
81
  If Core does not confirm the sector filter in its response, the client reports
73
82
  an unsupported-server error instead of returning an unfiltered pipeline.
@@ -80,6 +89,27 @@ wording, then read candidate Deals to verify specific categories and claims.
80
89
  Search reads current Page data directly, so updates and clears are reflected
81
90
  immediately without a separate indexing job or manual tag maintenance.
82
91
 
92
+
93
+ ### Read Information selectively
94
+
95
+ With Core API 5.42.0 or newer, read only the Information you need instead of
96
+ `--detail memory` (which can return megabytes on a large Deal):
97
+
98
+ ```bash
99
+ llama deal read <dealId> --information-q "ARR"
100
+ llama deal read <dealId> --information-type founder_claim,team
101
+ llama deal read <dealId> --information-id <recordId>,<recordId>
102
+ ```
103
+
104
+ `--information-q` matches literal text in a record's type, subject, labels and
105
+ value and returns `match.snippets` around each hit. In text and type reads, a
106
+ value over 8,000 characters comes back as `{ omitted, chars }`; read that record
107
+ with `--information-id` for its full value. Filters combine with AND, return at
108
+ most 50 records, and `informationFilter` echoes what Core applied
109
+ (`matched > returned` means narrow the filter). MCP `read_deal` accepts the same
110
+ filters as `informationIds`, `informationTypes` and `informationQuery`. If Core
111
+ does not confirm the filter, the client reports an unsupported-server error
112
+ instead of returning unfiltered memory.
83
113
  ### Create
84
114
 
85
115
  ```json
package/README.zh-CN.md CHANGED
@@ -70,6 +70,34 @@ llama deal search "humanoid" --sector "robotics" --state active
70
70
  更宽的行业或其他措辞,再读取候选 Deal 核实具体分类和结论。搜索直接读取当前
71
71
  Page 数据,更新和清空立即生效,无需另外维护索引或标签。
72
72
 
73
+
74
+ `--status` 按当前 pipeline 状态精确匹配(可用逗号给多个),也可以单独使用。
75
+ Core API 5.42.0 及以上版本可以一次列出所有已投 Deal:
76
+
77
+ ```bash
78
+ llama deal search --status Invested --limit 200
79
+ ```
80
+
81
+ 结果里带 `investmentDate` 和 `markedInvestedAt`,可以按投资时间排序。
82
+
83
+ ### 按需读取 Information
84
+
85
+ Core API 5.42.0 及以上版本可以只读需要的 Information,不必用 `--detail memory`
86
+ (大 Deal 可能一次返回数 MB):
87
+
88
+ ```bash
89
+ llama deal read <dealId> --information-q "ARR"
90
+ llama deal read <dealId> --information-type founder_claim,team
91
+ llama deal read <dealId> --information-id <recordId>,<recordId>
92
+ ```
93
+
94
+ `--information-q` 在记录的 type、subject、labels 和 value 中做字面匹配,并在
95
+ `match.snippets` 里返回命中处的上下文。按文本或类型读取时,超过 8,000 字符的
96
+ value 会以 `{ omitted, chars }` 返回,需要全文时用 `--information-id` 读取该记录。
97
+ 多个条件按 AND 组合,最多返回 50 条;`informationFilter` 回显 Core 实际应用的
98
+ 条件(`matched > returned` 表示需要再收窄)。MCP `read_deal` 用
99
+ `informationIds`、`informationTypes`、`informationQuery` 接受同样的条件。如果
100
+ Core 没有确认过滤条件,客户端会报"服务端不支持",而不是返回未过滤的全部内容。
73
101
  ### 创建 Deal
74
102
 
75
103
  ```json
package/bin/llama-mcp.mjs CHANGED
@@ -1,17 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { buildArtifactReadPath, buildWikiReadPath } from "../lib/content-read.mjs";
4
3
  import { createRequire } from "node:module";
5
4
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
6
  import { z } from "zod";
7
+ import { registerApiTools, structuredBootstrapManifest, textResult } from "../lib/mcp-api-tools.mjs";
8
8
  import {
9
9
  formatErrorForDisplay,
10
10
  getAuthHeaders,
11
11
  readBriefing,
12
12
  request,
13
13
  setClientRuntime,
14
- structuredBootstrapManifest,
15
14
  } from "../lib/client.mjs";
16
15
  import {
17
16
  clearExternalSession,
@@ -20,16 +19,7 @@ import {
20
19
  startExternalSession,
21
20
  uploadExternalFile,
22
21
  } from "../lib/external.mjs";
23
- import {
24
- buildDealReadPath,
25
- buildDealSearchPath,
26
- verifyDealSearchResult,
27
- compactDealWriteResult,
28
- prepareDealCommand,
29
- } from "../lib/deal-actions.mjs";
30
22
  import { submitFeedback, readFeedback, resolveFeedback } from "../lib/feedback.mjs";
31
- import { buildDealMemoryPath } from "../lib/deal-memory-actions.mjs";
32
- import { buildPageSchemaPath, MAX_PAGE_SCHEMA_FIELDS } from "../lib/page-schema.mjs";
33
23
 
34
24
  const requireFromHere = createRequire(import.meta.url);
35
25
  const { version: PKG_VERSION } = requireFromHere("../package.json");
@@ -37,13 +27,6 @@ setClientRuntime({ client: "mcp" });
37
27
 
38
28
  const server = new McpServer({ name: "llama-mcp", version: PKG_VERSION });
39
29
 
40
- function textResult(text, isError = false) {
41
- return {
42
- content: [{ type: "text", text }],
43
- ...(isError ? { isError: true } : {}),
44
- };
45
- }
46
-
47
30
  async function callApi(method, path, body, transform = (value) => value) {
48
31
  try {
49
32
  const result = transform(await request(method, path, body));
@@ -84,168 +67,9 @@ server.registerTool("feedback_resolve", {
84
67
  catch (error) { return textResult(formatErrorForDisplay(error), true); }
85
68
  });
86
69
 
87
- const originSchema = z.object({
88
- kind: z.enum(["user", "agent", "system"]),
89
- originalUserUtterance: z.string().min(1).optional(),
90
- originatingChatRecordId: z.string().uuid().optional(),
91
- });
92
-
93
- const writeDealCommandSchema = z.discriminatedUnion("operation", [
94
- z.object({
95
- operation: z.literal("input.submit"),
96
- dealId: z.string().uuid(),
97
- format: z.string().min(1).max(120).optional(),
98
- content: z.any(),
99
- source: z.record(z.string(), z.any()).optional(),
100
- origin: originSchema,
101
- idempotencyKey: z.string().min(1).max(240).optional(),
102
- }).strict(),
103
- z.object({
104
- operation: z.literal("information.put"),
105
- dealId: z.string().uuid(),
106
- informationId: z.string().uuid().optional(),
107
- type: z.string().min(1).max(120),
108
- labels: z.array(z.string()).optional(),
109
- subject: z.record(z.string(), z.any()).optional(),
110
- value: z.any(),
111
- expectedVersion: z.number().int().positive().optional(),
112
- origin: originSchema,
113
- idempotencyKey: z.string().min(1).max(240).optional(),
114
- }).strict(),
115
- z.object({
116
- operation: z.literal("page.patch"),
117
- dealId: z.string().uuid(),
118
- patch: z.record(z.string(), z.any()),
119
- expectedRevision: z.number().int().nonnegative().optional(),
120
- origin: originSchema,
121
- idempotencyKey: z.string().min(1).max(240).optional(),
122
- }).strict(),
123
- z.object({
124
- operation: z.literal("artifact.put"),
125
- dealId: z.string().uuid(),
126
- artifactId: z.string().uuid().optional().describe("Reuse the existing ID to append a complete file version, including on rename. Omit for a new document without a title collision, or explicitly use a fresh UUID for a distinct same-name source."),
127
- kind: z.string().min(1).max(120),
128
- title: z.string().min(1).max(500),
129
- mimeType: z.string().min(1).max(240),
130
- contentBase64: z.string().min(1).optional(),
131
- storageKey: z.string().min(1).optional(),
132
- storageUrl: z.string().url().optional(),
133
- byteSize: z.number().int().nonnegative().optional(),
134
- sha256: z.string().regex(/^[0-9a-f]{64}$/).optional(),
135
- metadata: z.record(z.string(), z.any()).optional(),
136
- origin: originSchema,
137
- idempotencyKey: z.string().min(1).max(240).optional(),
138
- }).strict(),
139
- ]);
140
-
141
- // Deal is deliberately a closed four-tool surface. Do not add split resource
142
- // tools here: one more tool is one more branch every agent must reason about.
143
- server.registerTool(
144
- "search_deals",
145
- {
146
- description: "Search current Deal metadata by company name/key, description, sector/subsector or founder name in either stored language. Use short keywords; matching is a case-insensitive literal substring, not semantic search. Combine q with sector/state/limit, or use sector alone for category discovery. Sector filters before limit and requires a supporting Core server. Exact names rank first. Inspect searchMatch fields and search.has_more; truncated results are not exhaustive. On zero matches, try broader or alternative wording before concluding no Deal exists. Read candidates to verify specific categories and claims. Always search before creating a company.",
147
- inputSchema: {
148
- q: z.string().optional(),
149
- sector: z.string().trim().min(1).max(200).optional(),
150
- state: z.enum(["active", "archived", "trashed"]).optional(),
151
- limit: z.number().int().min(1).max(2000).optional(),
152
- },
153
- },
154
- // @core-api-operation GET /api/occam/deals
155
- async ({ q, sector, state, limit } = {}) => callApi(
156
- "GET", buildDealSearchPath(q, { sector, state, limit }), undefined,
157
- (result) => verifyDealSearchResult(result, { sector }),
158
- ),
159
- );
160
-
161
- server.registerTool(
162
- "read_deal",
163
- {
164
- description: "Read one Deal. Live Page is current human-visible state; Information is memory and never updates Page automatically. Use artifactId to read original source text with page/paragraph anchors. Continue with nextOffset and source.sha256. Page field semantics come from agent_bootstrap, not every Deal read.",
165
- inputSchema: {
166
- dealId: z.string().min(1),
167
- detail: z.enum(["overview", "memory", "files", "conversation", "history", "all"]).optional(),
168
- artifactId: z.string().uuid().optional(),
169
- version: z.number().int().min(1).optional(),
170
- offset: z.number().int().min(0).optional(),
171
- limit: z.number().int().min(1).max(50000).optional(),
172
- sha256: z.string().regex(/^[a-f0-9]{64}$/).optional(),
173
- },
174
- },
175
- async ({ dealId, detail, artifactId, ...options }) => {
176
- if (artifactId) {
177
- if (detail) throw new Error("Use artifactId or detail, not both");
178
- // @core-api-operation GET /api/occam/deals/{dealId}/artifacts/{artifactId}
179
- return callApi("GET", buildArtifactReadPath(dealId, artifactId, options));
180
- }
181
- if (Object.values(options).some(value => value !== undefined)) throw new Error("Content options require artifactId");
182
- // @core-api-operation GET /api/occam/deals/{dealId}
183
- return callApi("GET", buildDealReadPath(dealId, detail || "overview"));
184
- },
185
- );
186
-
187
- server.registerTool(
188
- "create_deal",
189
- {
190
- description: "Create one Deal intent. Core owns Drive, initial resources, idempotency, and Events.",
191
- inputSchema: {
192
- companyName: z.string().min(1).max(240),
193
- companyKey: z.string().min(1).max(240).optional(),
194
- page: z.record(z.string(), z.any()).optional(),
195
- information: z.array(z.record(z.string(), z.any())).optional(),
196
- origin: originSchema,
197
- idempotencyKey: z.string().min(1).max(240).optional(),
198
- },
199
- },
200
- async (input) => callApi("POST", "/api/occam/deals/commands", prepareDealCommand("create", input)),
201
- );
202
-
203
- server.registerTool(
204
- "write_deal",
205
- {
206
- description: "The only Deal mutation tool. Pass one command: input.submit, information.put, page.patch, or artifact.put. Before artifact.put, read Deal files: reuse artifactId for a revision; a same-kind/title collision without an ID returns OCCAM_CONFLICT. Titles are candidates, not identity. Select the intended existing ID from the request/content, or explicitly generate a fresh UUID for a distinct same-name source. Send complete revised bytes, preserve applicable metadata, and verify the ID/version. Core hosts text/html in Command; do not mirror HTML to Drive or require a Drive link. Non-HTML source files, including original transcripts and recordings, go to Drive through artifact.put. Keep source-file bodies out of Input, Information, Page and metadata; save extracted facts and summaries with source references and preserve the user request. page.patch does not edit file bytes. All Page content fields are Agent-writable; author controls attribution, not permission. Before page.patch, use get_live_deal_page_schema for the exact fields being changed and write bilingual Page prose. page.patch is JSON Merge Patch: arrays replace whole arrays, so read-modify-write and preserve sibling slots. A Llama user's own judgment is human_subjective_view.people or human_subjective_view.business and must carry value.speaker, value.rawText quoted verbatim from origin.originalUserUtterance, and value.summary (the agent's restatement); it is rejected otherwise.",
207
- // MCP SDK 1.30 only publishes object schemas at the tool root. Nesting one
208
- // command preserves the real discriminated union instead of flattening four
209
- // incompatible operations into false affordances.
210
- inputSchema: { command: writeDealCommandSchema },
211
- },
212
- async ({ command }) => callApi(
213
- "POST",
214
- "/api/occam/deals/commands",
215
- prepareDealCommand("write", command),
216
- (result) => compactDealWriteResult(command, result),
217
- ),
218
- );
219
-
220
- // Deal Memory is a separate sidecar domain. These do not add an Occam Deal
221
- // business resource or widen the deliberately closed four-tool Deal surface.
222
- server.registerTool(
223
- "get_deal_memory",
224
- {
225
- description: "Read the complete canonical Markdown Deal Story and its opaque concurrency version. A returned placeholder is an existing Story whose version is required for a write; only 404 means no Story exists.",
226
- inputSchema: { dealId: z.string().uuid() },
227
- },
228
- // @core-api-operation GET /api/deal-memory/{dealId}/story
229
- async ({ dealId }) => callApi("GET", buildDealMemoryPath(dealId)),
230
- );
231
-
232
- server.registerTool(
233
- "update_deal_memory",
234
- {
235
- description: "Replace the complete Markdown Deal Story with one coherent current understanding; never append an update log, restate Live Deal Page/Deal Information fields, or write when understanding would not materially improve. Frontmatter must contain matching deal_id plus stable uuid and created timestamps; updated must strictly advance. Pass the version from get_deal_memory for every returned Story, including a placeholder; only 404 permits creation without expected_version.",
236
- inputSchema: {
237
- dealId: z.string().uuid(),
238
- markdown: z.string().min(1).max(1_048_576),
239
- expected_version: z.string().min(1).optional(),
240
- },
241
- },
242
- // @core-api-operation PUT /api/deal-memory/{dealId}/story
243
- async ({ dealId, markdown, expected_version }) => callApi(
244
- "PUT",
245
- buildDealMemoryPath(dealId),
246
- { markdown, ...(expected_version ? { expected_version } : {}) },
247
- ),
248
- );
70
+ // Deal, Deal Memory, bootstrap, Page schema, skills, preferences and Wiki tools
71
+ // are shared with remote MCP hosts; see lib/mcp-api-tools.mjs.
72
+ registerApiTools(server, { request, formatError: formatErrorForDisplay, version: PKG_VERSION });
249
73
 
250
74
  server.registerTool(
251
75
  "auth_status",
@@ -265,184 +89,6 @@ server.registerTool(
265
89
  },
266
90
  );
267
91
 
268
- server.registerTool(
269
- "agent_bootstrap",
270
- {
271
- description: "Mandatory before Llama work: fetch the authenticated two-part private Brain (Investment Framework V3 + Llama Command operating skill), a compact Live Deal Page schema index, runtime contract, and visible Llama OS skills. Read exact Page fields separately with get_live_deal_page_schema.",
272
- inputSchema: { limit: z.number().int().min(1).max(100).optional() },
273
- },
274
- async ({ limit } = {}) => {
275
- const params = new URLSearchParams({ clientVersion: PKG_VERSION });
276
- params.set("page_schema", "progressive");
277
- if (limit) params.set("limit", String(limit));
278
- try {
279
- const result = await request("GET", `/api/agent/manifest?${params}`);
280
- return textResult(JSON.stringify(structuredBootstrapManifest(result), null, 2));
281
- } catch (error) {
282
- return textResult(formatErrorForDisplay(error), true);
283
- }
284
- },
285
- );
286
-
287
- server.registerTool(
288
- "get_live_deal_page_schema",
289
- {
290
- description: "Read the Live Deal Page schema progressively. Call with no selector for the compact index; before page.patch, request only exact fields or one section. This is read-only context, not a fifth Deal action.",
291
- inputSchema: {
292
- fields: z.array(z.string().min(1)).min(1).max(MAX_PAGE_SCHEMA_FIELDS).optional(),
293
- section: z.string().min(1).optional(),
294
- },
295
- },
296
- async ({ fields, section } = {}) => {
297
- try {
298
- // @core-api-operation GET /api/agent/page-schema
299
- return await callApi("GET", buildPageSchemaPath({ fields, section }));
300
- } catch (error) {
301
- return textResult(formatErrorForDisplay(error), true);
302
- }
303
- },
304
- );
305
-
306
- server.registerTool(
307
- "skills_search",
308
- {
309
- description: "Search authenticated runtime skills; read only the relevant result.",
310
- inputSchema: { q: z.string().min(1), limit: z.number().int().min(1).max(100).optional() },
311
- },
312
- async ({ q, limit }) => {
313
- const params = new URLSearchParams({ q });
314
- if (limit) params.set("limit", String(limit));
315
- return callApi("GET", `/api/agent/skills?${params}`);
316
- },
317
- );
318
-
319
- server.registerTool(
320
- "skills_read",
321
- {
322
- description: "Read one authenticated runtime skill by slug.",
323
- inputSchema: { slug: z.string().min(1) },
324
- },
325
- async ({ slug }) => callApi("GET", `/api/agent/skills/${encodeURIComponent(slug)}`),
326
- );
327
-
328
- server.registerTool(
329
- "pref_list",
330
- {
331
- description: "List standing agent preferences.",
332
- inputSchema: { status: z.enum(["active", "proposed", "retired", "all"]).optional() },
333
- },
334
- async ({ status } = {}) => {
335
- const params = new URLSearchParams();
336
- if (status) params.set("status", status);
337
- return callApi("GET", `/api/agent/preferences${params.size ? `?${params}` : ""}`);
338
- },
339
- );
340
-
341
- server.registerTool(
342
- "pref_add",
343
- {
344
- description: "Save a durable user or team preference; procedures belong in skills.",
345
- inputSchema: {
346
- key: z.string().min(1),
347
- content: z.string().min(1).max(280),
348
- scope: z.enum(["user", "team"]).optional(),
349
- evidence: z.string().optional(),
350
- },
351
- },
352
- async ({ key, content, scope, evidence }) =>
353
- callApi("POST", "/api/agent/preferences", { key, content, scope, evidence }),
354
- );
355
-
356
- server.registerTool(
357
- "pref_set_status",
358
- {
359
- description: "Activate or retire a standing preference.",
360
- inputSchema: { id: z.number().int().positive(), status: z.enum(["active", "retired"]) },
361
- },
362
- async ({ id, status }) => callApi("PATCH", `/api/agent/preferences/${id}`, { status }),
363
- );
364
-
365
- server.registerTool(
366
- "object_inspect",
367
- {
368
- description: "Explain a Command URL or object before guessing that it is broken.",
369
- inputSchema: {
370
- q: z.string().optional(),
371
- type: z.string().optional(),
372
- id: z.string().optional(),
373
- lang: z.enum(["en", "zh"]).optional(),
374
- },
375
- },
376
- async ({ q, type, id, lang } = {}) => {
377
- const params = new URLSearchParams();
378
- if (q) params.set("q", q);
379
- if (type) params.set("type", type);
380
- if (id) params.set("id", id);
381
- if (lang) params.set("lang", lang);
382
- return callApi("GET", `/api/agent/explain?${params}`);
383
- },
384
- );
385
-
386
- server.registerTool(
387
- "wiki_search",
388
- {
389
- description: "Search institutional knowledge in the internal Wiki.",
390
- inputSchema: { q: z.string().min(1) },
391
- },
392
- async ({ q }) => callApi("GET", `/api/wiki/search?q=${encodeURIComponent(q)}`),
393
- );
394
-
395
- server.registerTool(
396
- "wiki_read",
397
- {
398
- description: "Read one Wiki article. format=text extracts its original upload or page body. attachment follows an ID listed in attachments. Continuations require offset and sha256.",
399
- inputSchema: {
400
- slug: z.string().min(1), lang: z.enum(["en", "zh"]).optional(),
401
- format: z.literal("text").optional(), attachment: z.string().optional(),
402
- offset: z.number().int().min(0).optional(), limit: z.number().int().min(1).max(50000).optional(),
403
- sha256: z.string().regex(/^[a-f0-9]{64}$/).optional(),
404
- },
405
- },
406
- // @core-api-operation GET /api/wiki/{slug}
407
- async ({ slug, ...options }) => callApi("GET", buildWikiReadPath(slug, options)),
408
- );
409
-
410
- server.registerTool(
411
- "wiki_save",
412
- {
413
- description: "Create or update cross-Deal institutional knowledge, not Deal data.",
414
- inputSchema: {
415
- slug: z.string().min(1),
416
- title: z.string().min(1),
417
- content: z.string(),
418
- sources: z.array(z.string()).min(1),
419
- content_type: z.enum(["markdown", "html"]).optional(),
420
- },
421
- },
422
- async ({ slug, title, content, sources, content_type }) =>
423
- callApi("POST", "/api/wiki/save", { slug, title, content, sources, content_type }),
424
- );
425
-
426
- server.registerTool(
427
- "wiki_delete",
428
- {
429
- description: "Soft-delete a Wiki article.",
430
- inputSchema: { slug: z.string().min(1), lang: z.enum(["en", "zh"]).optional() },
431
- },
432
- async ({ slug, lang }) =>
433
- callApi("DELETE", `/api/wiki/${encodeURIComponent(slug)}?lang=${lang === "zh" ? "zh" : "en"}`),
434
- );
435
-
436
- server.registerTool(
437
- "wiki_restore",
438
- {
439
- description: "Restore a soft-deleted Wiki article.",
440
- inputSchema: { slug: z.string().min(1), lang: z.enum(["en", "zh"]).optional() },
441
- },
442
- async ({ slug, lang }) =>
443
- callApi("POST", `/api/wiki/${encodeURIComponent(slug)}/restore?lang=${lang === "zh" ? "zh" : "en"}`),
444
- );
445
-
446
92
  server.registerTool(
447
93
  "pitch_start",
448
94
  {
package/bin/llama.mjs CHANGED
@@ -36,7 +36,9 @@ import {
36
36
  DEAL_ACTIONS,
37
37
  buildDealReadPath,
38
38
  buildDealSearchPath,
39
+ informationReadFilter,
39
40
  verifyDealSearchResult,
41
+ verifyInformationReadResult,
40
42
  compactDealWriteResult,
41
43
  prepareDealCommand,
42
44
  readJsonInput,
@@ -54,7 +56,7 @@ const { version: PKG_VERSION } = requireFromHere("../package.json");
54
56
  const HELP_ROOT = `Llama Command CLI 2 — small authenticated tools for agents.
55
57
 
56
58
  Deal has exactly four actions:
57
- llama deal search [query] [--sector <text>] [--state active|archived|trashed] [--limit 10]
59
+ llama deal search [query] [--sector <text>] [--status <status>] [--state active|archived|trashed] [--limit 10]
58
60
  llama deal read <dealId> [--detail overview|memory|files|conversation|history|all]
59
61
  llama deal create --json <file|->
60
62
  llama deal write --json <file|->
@@ -82,12 +84,39 @@ Separate preserved domains:
82
84
  llama admin auth-events|deal-events|agent-events|feedback
83
85
  llama pitch start|say|upload|status|finalize|end
84
86
 
85
- Run \`llama help deal\` for the mutation contract.`;
87
+ Run \`llama help deal\` for the mutation contract and \`llama help wiki\` for publishing.`;
88
+
89
+ const HELP_WIKI = `Llama Wiki — publish complete institutional knowledge pages
90
+
91
+ llama agent bootstrap
92
+ llama wiki search <query>
93
+ llama wiki read <slug> [--lang en|zh]
94
+ llama wiki save <slug> --title "..." --file page.html --content-type html --lang en --sources "url1;url2"
95
+ llama wiki delete <slug> [--lang en|zh]
96
+ llama wiki restore <slug> [--lang en|zh]
97
+
98
+ Follow the current authenticated contract.wiki_publishing guidance. New
99
+ publications default to complete self-contained HTML. Preserve the report's
100
+ full content, charts, interactions and existing language/version controls.
101
+ Embed CSS, JavaScript, chart data and rendering assets in the HTML.
102
+
103
+ --file reads the full file; .html/.htm selects HTML automatically. When sending
104
+ HTML through --content, explicitly set --content-type html. Use --lang zh for
105
+ Chinese content. Sources are required; separate them with semicolons or pipes.
106
+
107
+ Honor an explicit Markdown request with --content-type markdown. Read an
108
+ existing slug and language before editing and preserve its content_type.
109
+ Format conversion or replacement needs authorization for that change.
110
+
111
+ Read the receipt's canonical_url, content_type, langs_available, verified and
112
+ warnings, then read back the article and check the rendered page and its
113
+ interactions before reporting completion. Report any checks still pending.
114
+ Wiki publication requires the user's request or authorization.`;
86
115
 
87
116
  const HELP_DEAL = `Llama Command CLI 2 — Deal contract
88
117
 
89
118
  Exactly four actions:
90
- llama deal search [query] [--sector <text>] [--state active|archived|trashed] [--limit 10]
119
+ llama deal search [query] [--sector <text>] [--status <status>] [--state active|archived|trashed] [--limit 10]
91
120
  llama deal read <dealId> [--detail overview|memory|files|conversation|history|all]
92
121
  llama deal create --json <file|->
93
122
  llama deal write --json <file|->
@@ -99,10 +128,23 @@ Combine --sector with a query/state/limit, or search a sector without a query:
99
128
  llama deal search "humanoid" --sector "robotics"
100
129
  Metadata search and --sector require Core API 5.41.0 or newer. A sector filter
101
130
  must be confirmed by Core; old unfiltered responses are rejected.
131
+ --status matches the current pipeline status exactly (any of a comma list) and
132
+ works alone; it requires Core API 5.42.0+. To list every invested Deal:
133
+ llama deal search --status Invested --limit 200
134
+ then order by investmentDate / markedInvestedAt.
102
135
  Use short keywords. searchMatch lists matched fields; search.has_more means
103
136
  results are truncated. On zero matches, try broader or alternative wording.
104
137
  Read candidate Deals to verify categories and substantive claims.
105
138
 
139
+ Read Information selectively (Core API 5.42.0+), instead of --detail memory:
140
+ llama deal read <dealId> --information-q "ARR" literal text; returns snippets
141
+ llama deal read <dealId> --information-type founder_claim exact types, comma-separated
142
+ llama deal read <dealId> --information-id <id>[,<id>] full values, up to 50
143
+ Filters combine with AND. Values over 8,000 characters in a text or type read
144
+ come back as { omitted, chars }; read that record by id. informationFilter
145
+ echoes what Core applied; matched > returned means narrow the filter. An older
146
+ Core that ignores the filter is rejected rather than shown as filtered.
147
+
106
148
  Read original source content:
107
149
  llama deal read <dealId> --artifact <artifactId> [--version N] [--offset N --sha256 HASH] [--limit N] [--output <file>]
108
150
  llama wiki read <slug> --format text [--attachment <referenceId>] [--offset N --sha256 HASH] [--output <file>]
@@ -268,6 +310,7 @@ function usage(area) {
268
310
  area === "memory" ? HELP_MEMORY :
269
311
  area === "page-schema" ? HELP_PAGE_SCHEMA :
270
312
  area === "feedback" ? HELP_FEEDBACK :
313
+ area === "wiki" ? HELP_WIKI :
271
314
  HELP_ROOT,
272
315
  );
273
316
  }
@@ -540,7 +583,7 @@ async function handleWiki(action, rest) {
540
583
  }
541
584
  if (action === "save") {
542
585
  if (!slug || typeof flags.title !== "string" || typeof flags.sources !== "string") {
543
- throw new Error('Usage: llama wiki save <slug> --title "..." --content "..."|--file <path> --sources "url1;url2"');
586
+ throw new Error('Usage: llama wiki save <slug> --title "..." --file page.html --content-type html --sources "url1;url2". See llama help wiki for HTML and Markdown publishing.');
544
587
  }
545
588
  if (flags.content && flags.file) throw new Error("Use either --content or --file, not both.");
546
589
  const content = flags.file ? await readFile(String(flags.file), "utf8") : flags.content;
@@ -749,7 +792,7 @@ async function main() {
749
792
  }
750
793
 
751
794
  if (area === "deal" && action === "search") {
752
- const { flags, positional } = parseFlags(rest, ["state", "limit", "sector"]);
795
+ const { flags, positional } = parseFlags(rest, ["state", "limit", "sector", "status"]);
753
796
  // @core-api-operation GET /api/occam/deals
754
797
  const result = await request("GET", buildDealSearchPath(positional.join(" ").trim(), flags));
755
798
  print(verifyDealSearchResult(result, flags));
@@ -757,16 +800,19 @@ async function main() {
757
800
  }
758
801
  if (area === "deal" && action === "read") {
759
802
  const dealId = rest[0];
760
- const { flags } = parseFlags(rest.slice(1), ["detail", "artifact", "version", "format", "offset", "limit", "sha256", "output"]);
803
+ const { flags } = parseFlags(rest.slice(1), ["detail", "artifact", "version", "format", "offset", "limit", "sha256", "output", "information-id", "information-type", "information-q"]);
804
+ const filter = informationReadFilter({ ids: flags["information-id"], types: flags["information-type"], q: flags["information-q"] });
761
805
  if (flags.artifact !== undefined) {
762
806
  if (flags.detail !== undefined) throw new Error("Use --artifact or --detail, not both");
807
+ if (filter) throw new Error("Use --artifact or --information-*, not both");
763
808
  // @core-api-operation GET /api/occam/deals/{dealId}/artifacts/{artifactId}
764
809
  const result = await request("GET", buildArtifactReadPath(dealId, flags.artifact, flags));
765
810
  print(await saveContentSource(result, flags.output));
766
811
  } else {
767
812
  if (["version", "format", "offset", "limit", "sha256", "output"].some(key => flags[key] !== undefined)) throw new Error("Content options require --artifact");
768
813
  // @core-api-operation GET /api/occam/deals/{dealId}
769
- print(await request("GET", buildDealReadPath(dealId, flags.detail === true ? "overview" : flags.detail || "overview")));
814
+ const result = await request("GET", buildDealReadPath(dealId, flags.detail === true ? "overview" : flags.detail || "overview", filter));
815
+ print(verifyInformationReadResult(result, filter));
770
816
  }
771
817
  return;
772
818
  }
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "format": "llama.core-api-contract.v1",
3
3
  "name": "llama-core-api",
4
- "apiVersion": "5.41.0",
4
+ "apiVersion": "5.42.0",
5
5
  "openapiVersion": "3.0.3",
6
- "sha256": "4276125d5c00112ce0a25a563870817a9671db573de832abd874c1a8dff17c4b",
6
+ "sha256": "c244351c5d6b4f9f917a234229a3e7044558e8c4870decf0e8b1518a166f8799",
7
7
  "pathCount": 133,
8
8
  "operationCount": 164
9
9
  }
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "format": "llama.cli-build.v1",
3
3
  "packageName": "@llamaventures/cli",
4
- "packageVersion": "2.6.0",
5
- "sourceSha": "98937214995aba0029521dbeea358cb9e9d32ccf",
4
+ "packageVersion": "2.8.0",
5
+ "sourceSha": "ec9daefda51090ca3331a22db1db45b3c56c3452",
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.41.0",
11
+ "apiVersion": "5.42.0",
12
12
  "openapiVersion": "3.0.3",
13
- "sha256": "4276125d5c00112ce0a25a563870817a9671db573de832abd874c1a8dff17c4b"
13
+ "sha256": "c244351c5d6b4f9f917a234229a3e7044558e8c4870decf0e8b1518a166f8799"
14
14
  }
15
15
  }
package/lib/client.mjs CHANGED
@@ -594,13 +594,7 @@ export function formatErrorForDisplay(error) {
594
594
  return `Error${code}: ${message}`;
595
595
  }
596
596
 
597
- export function structuredBootstrapManifest(manifest) {
598
- if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
599
- return manifest;
600
- }
601
- const { briefing: _renderedDuplicate, ...structured } = manifest;
602
- return structured;
603
- }
597
+ export { structuredBootstrapManifest } from "./mcp-api-tools.mjs";
604
598
 
605
599
  async function requestWithRetry(method, endpoint, body, opts, allowRetry) {
606
600
  const authHeaders = await getAuthHeaders();
@@ -169,11 +169,27 @@ export function buildDealSearchPath(query, flags = {}) {
169
169
  if (query) params.set("q", query);
170
170
  const sector = dealSearchSector(flags);
171
171
  if (sector !== undefined) params.set("sector", sector);
172
+ const status = dealSearchStatus(flags);
173
+ if (status !== undefined) params.set("status", status.join(","));
172
174
  if (flags.state && flags.state !== true) params.set("state", String(flags.state));
173
175
  if (flags.limit && flags.limit !== true) params.set("limit", String(flags.limit));
174
176
  return `/api/occam/deals${params.size ? `?${params}` : ""}`;
175
177
  }
176
178
 
179
+ /** Exact pipeline statuses (Core API 5.42.0+), e.g. "Invested" or ["Invested", "Diligence"]. */
180
+ function dealSearchStatus(flags) {
181
+ if (flags.status === undefined) return undefined;
182
+ if (flags.status === true) throw new Error("--status requires a pipeline status, e.g. Invested");
183
+ const values = (Array.isArray(flags.status) ? flags.status : String(flags.status).split(","))
184
+ .map((value) => String(value).trim())
185
+ .filter(Boolean);
186
+ if (!values.length) throw new Error("--status requires a pipeline status, e.g. Invested");
187
+ if (values.length > 10 || values.some((value) => value.length > 60)) {
188
+ throw new Error("--status accepts at most 10 statuses of at most 60 characters");
189
+ }
190
+ return [...new Set(values)];
191
+ }
192
+
177
193
  function dealSearchSector(flags) {
178
194
  if (flags.sector === undefined) return undefined;
179
195
  if (typeof flags.sector !== "string" || !flags.sector.trim() || flags.sector.trim().length > 200) {
@@ -183,6 +199,10 @@ function dealSearchSector(flags) {
183
199
  }
184
200
 
185
201
  export function verifyDealSearchResult(result, flags = {}) {
202
+ const status = dealSearchStatus(flags);
203
+ if (status !== undefined && JSON.stringify(result?.search?.filters?.status ?? null) !== JSON.stringify(status)) {
204
+ throw new Error("Core did not confirm the requested status filter. Filtering by pipeline status requires Core API 5.42.0 or newer; this response was not accepted as filtered results.");
205
+ }
186
206
  const sector = dealSearchSector(flags);
187
207
  if (sector !== undefined && result?.search?.filters?.sector !== sector) {
188
208
  throw new Error("Core did not confirm the requested sector filter. Upgrade the Llama Command server before retrying --sector; this response was not accepted as filtered results.");
@@ -190,7 +210,55 @@ export function verifyDealSearchResult(result, flags = {}) {
190
210
  return result;
191
211
  }
192
212
 
193
- export function buildDealReadPath(dealId, detail = "overview") {
213
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
214
+
215
+ function listInput(value) {
216
+ if (value === undefined || value === null) return undefined;
217
+ const values = (Array.isArray(value) ? value : String(value).split(","))
218
+ .map((item) => String(item).trim())
219
+ .filter(Boolean);
220
+ return values.length ? [...new Set(values)] : undefined;
221
+ }
222
+
223
+ /**
224
+ * Targeted Information read: ids (full values), exact types, and/or literal
225
+ * text. Returns undefined when no filter was requested. Core API 5.42.0+.
226
+ */
227
+ export function informationReadFilter({ ids, types, q } = {}) {
228
+ const filter = {};
229
+ const idList = listInput(ids);
230
+ const typeList = listInput(types);
231
+ if (idList) {
232
+ if (idList.length > 50) throw new Error("--information-id accepts at most 50 ids");
233
+ if (idList.some((id) => !UUID_RE.test(id))) throw new Error("--information-id must be Information record UUIDs");
234
+ filter.ids = idList.map((id) => id.toLowerCase());
235
+ }
236
+ if (typeList) {
237
+ if (typeList.length > 20) throw new Error("--information-type accepts at most 20 types");
238
+ if (typeList.some((type) => type.length > 120)) throw new Error("--information-type values must be at most 120 characters");
239
+ filter.types = typeList;
240
+ }
241
+ if (q !== undefined && q !== null) {
242
+ if (q === true) throw new Error("--information-q requires text");
243
+ const text = String(q).trim();
244
+ if (text.length < 2 || text.length > 200) throw new Error("--information-q must be 2-200 characters");
245
+ filter.q = text;
246
+ }
247
+ return Object.keys(filter).length ? filter : undefined;
248
+ }
249
+
250
+ /** Reject an unfiltered response from a Core that predates targeted reads. */
251
+ export function verifyInformationReadResult(result, filter) {
252
+ if (!filter) return result;
253
+ const applied = result?.informationFilter;
254
+ const same = (a, b) => JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
255
+ if (!applied || !same(applied.ids, filter.ids) || !same(applied.types, filter.types) || !same(applied.q, filter.q)) {
256
+ throw new Error("Core did not confirm the requested Information filter. Targeted Information reads require Core API 5.42.0 or newer; this response was not accepted as filtered results. Use --detail memory on an older server.");
257
+ }
258
+ return result;
259
+ }
260
+
261
+ export function buildDealReadPath(dealId, detail = "overview", filter) {
194
262
  if (!dealId || dealId.startsWith("--")) {
195
263
  throw new Error("Usage: llama deal read <dealId> [--detail overview|memory|files|conversation|history|all]");
196
264
  }
@@ -203,7 +271,13 @@ export function buildDealReadPath(dealId, detail = "overview") {
203
271
  all: "information,artifacts,chat,feed",
204
272
  }[detail];
205
273
  if (expansion === undefined) throw new Error(`Unknown detail: ${detail}`);
206
- return `/api/occam/deals/${encodeURIComponent(dealId)}${expansion ? `?expand=${expansion}` : ""}`;
274
+ const params = new URLSearchParams();
275
+ if (expansion) params.set("expand", expansion);
276
+ if (filter?.ids) params.set("information_id", filter.ids.join(","));
277
+ if (filter?.types) params.set("information_type", filter.types.join(","));
278
+ if (filter?.q !== undefined) params.set("information_q", filter.q);
279
+ const query = params.toString().replace(/%2C/g, ",");
280
+ return `/api/occam/deals/${encodeURIComponent(dealId)}${query ? `?${query}` : ""}`;
207
281
  }
208
282
 
209
283
  /**
@@ -0,0 +1,414 @@
1
+ // The API-backed Llama tool catalog, independent of transport and of how the
2
+ // caller authenticates. The stdio server (bin/llama-mcp.mjs) passes the local
3
+ // HTTP client; a remote MCP endpoint hosted by Llama Command passes its own
4
+ // in-process request function. Nothing here may touch local credentials,
5
+ // local files, or process state: tools that need those stay in bin/.
6
+
7
+ import { z } from "zod";
8
+ import { buildArtifactReadPath, buildWikiReadPath } from "./content-read.mjs";
9
+ import {
10
+ buildDealReadPath,
11
+ buildDealSearchPath,
12
+ verifyDealSearchResult,
13
+ informationReadFilter,
14
+ verifyInformationReadResult,
15
+ compactDealWriteResult,
16
+ prepareDealCommand,
17
+ } from "./deal-actions.mjs";
18
+ import { buildDealMemoryPath } from "./deal-memory-actions.mjs";
19
+ import { buildPageSchemaPath, MAX_PAGE_SCHEMA_FIELDS } from "./page-schema.mjs";
20
+
21
+ export function textResult(text, isError = false) {
22
+ return {
23
+ content: [{ type: "text", text }],
24
+ ...(isError ? { isError: true } : {}),
25
+ };
26
+ }
27
+
28
+ /** The manifest also carries a rendered duplicate of itself; tools return the structured half. */
29
+ export function structuredBootstrapManifest(manifest) {
30
+ if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
31
+ return manifest;
32
+ }
33
+ const { briefing: _renderedDuplicate, ...structured } = manifest;
34
+ return structured;
35
+ }
36
+
37
+ export const originSchema = z.object({
38
+ kind: z.enum(["user", "agent", "system"]),
39
+ originalUserUtterance: z.string().min(1).optional(),
40
+ originatingChatRecordId: z.string().uuid().optional(),
41
+ });
42
+
43
+ export const writeDealCommandSchema = z.discriminatedUnion("operation", [
44
+ z.object({
45
+ operation: z.literal("input.submit"),
46
+ dealId: z.string().uuid(),
47
+ format: z.string().min(1).max(120).optional(),
48
+ content: z.any(),
49
+ source: z.record(z.string(), z.any()).optional(),
50
+ origin: originSchema,
51
+ idempotencyKey: z.string().min(1).max(240).optional(),
52
+ }).strict(),
53
+ z.object({
54
+ operation: z.literal("information.put"),
55
+ dealId: z.string().uuid(),
56
+ informationId: z.string().uuid().optional(),
57
+ type: z.string().min(1).max(120),
58
+ labels: z.array(z.string()).optional(),
59
+ subject: z.record(z.string(), z.any()).optional(),
60
+ value: z.any(),
61
+ expectedVersion: z.number().int().positive().optional(),
62
+ origin: originSchema,
63
+ idempotencyKey: z.string().min(1).max(240).optional(),
64
+ }).strict(),
65
+ z.object({
66
+ operation: z.literal("page.patch"),
67
+ dealId: z.string().uuid(),
68
+ patch: z.record(z.string(), z.any()),
69
+ expectedRevision: z.number().int().nonnegative().optional(),
70
+ origin: originSchema,
71
+ idempotencyKey: z.string().min(1).max(240).optional(),
72
+ }).strict(),
73
+ z.object({
74
+ operation: z.literal("artifact.put"),
75
+ dealId: z.string().uuid(),
76
+ artifactId: z.string().uuid().optional().describe("Reuse the existing ID to append a complete file version, including on rename. Omit for a new document without a title collision, or explicitly use a fresh UUID for a distinct same-name source."),
77
+ kind: z.string().min(1).max(120),
78
+ title: z.string().min(1).max(500),
79
+ mimeType: z.string().min(1).max(240),
80
+ contentBase64: z.string().min(1).optional(),
81
+ storageKey: z.string().min(1).optional(),
82
+ storageUrl: z.string().url().optional(),
83
+ byteSize: z.number().int().nonnegative().optional(),
84
+ sha256: z.string().regex(/^[0-9a-f]{64}$/).optional(),
85
+ metadata: z.record(z.string(), z.any()).optional(),
86
+ origin: originSchema,
87
+ idempotencyKey: z.string().min(1).max(240).optional(),
88
+ }).strict(),
89
+ ]);
90
+
91
+ /**
92
+ * Register the API-backed tools on an MCP server.
93
+ *
94
+ * @param server an McpServer
95
+ * @param deps.request async (method, path, body) => parsed JSON; throws on failure
96
+ * @param deps.formatError (error) => string shown to the agent
97
+ * @param deps.version client version reported to bootstrap
98
+ * @param deps.only optional list of tool names; others are not registered
99
+ */
100
+ export function registerApiTools(server, { request, formatError, version, only } = {}) {
101
+ if (typeof request !== "function") throw new Error("registerApiTools requires a request function");
102
+ const describeError = formatError ?? ((error) => `Error: ${error?.message ?? String(error)}`);
103
+ const formatErrorForDisplay = describeError;
104
+ const allowed = only ? new Set(only) : null;
105
+ const registerTool = (name, ...rest) => {
106
+ if (allowed && !allowed.has(name)) return;
107
+ server.registerTool(name, ...rest);
108
+ };
109
+
110
+ async function callApi(method, path, body, transform = (value) => value) {
111
+ try {
112
+ const result = transform(await request(method, path, body));
113
+ return textResult(typeof result === "string" ? result : JSON.stringify(result, null, 2));
114
+ } catch (error) {
115
+ return textResult(describeError(error), true);
116
+ }
117
+ }
118
+
119
+
120
+ // Deal is deliberately a closed four-tool surface. Do not add split resource
121
+ // tools here: one more tool is one more branch every agent must reason about.
122
+ registerTool(
123
+ "search_deals",
124
+ {
125
+ description: "Search current Deal metadata by company name/key, description, sector/subsector or founder name in either stored language. Use short keywords; matching is a case-insensitive literal substring, not semantic search. Combine q with sector/status/state/limit, or use sector alone for category discovery. status filters by exact pipeline status (e.g. [\"Invested\"]); to list every invested Deal use status [\"Invested\"] with limit 200 and order by investmentDate/markedInvestedAt, instead of guessing from names. Sector filters before limit and requires a supporting Core server. Exact names rank first. Inspect searchMatch fields and search.has_more; truncated results are not exhaustive. On zero matches, try broader or alternative wording before concluding no Deal exists. Read candidates to verify specific categories and claims. Always search before creating a company.",
126
+ inputSchema: {
127
+ q: z.string().optional(),
128
+ sector: z.string().trim().min(1).max(200).optional(),
129
+ status: z.array(z.string().min(1).max(60)).min(1).max(10).optional(),
130
+ state: z.enum(["active", "archived", "trashed"]).optional(),
131
+ limit: z.number().int().min(1).max(2000).optional(),
132
+ },
133
+ },
134
+ // @core-api-operation GET /api/occam/deals
135
+ async ({ q, sector, status, state, limit } = {}) => callApi(
136
+ "GET", buildDealSearchPath(q, { sector, status, state, limit }), undefined,
137
+ (result) => verifyDealSearchResult(result, { sector, status }),
138
+ ),
139
+ );
140
+
141
+ registerTool(
142
+ "read_deal",
143
+ {
144
+ description: "Read one Deal. Live Page is current human-visible state; Information is memory and never updates Page automatically. Read Information selectively with informationIds (full values, up to 50), informationTypes, and/or informationQuery (literal text; returns match.snippets, and values over 8,000 characters as { omitted, chars }: then read that id). Prefer these over detail=memory, which returns up to ~2 MB. Use artifactId to read original source text with page/paragraph anchors. Continue with nextOffset and source.sha256. Page field semantics come from agent_bootstrap, not every Deal read.",
145
+ inputSchema: {
146
+ dealId: z.string().min(1),
147
+ detail: z.enum(["overview", "memory", "files", "conversation", "history", "all"]).optional(),
148
+ artifactId: z.string().uuid().optional(),
149
+ version: z.number().int().min(1).optional(),
150
+ offset: z.number().int().min(0).optional(),
151
+ limit: z.number().int().min(1).max(50000).optional(),
152
+ sha256: z.string().regex(/^[a-f0-9]{64}$/).optional(),
153
+ informationIds: z.array(z.string().uuid()).min(1).max(50).optional(),
154
+ informationTypes: z.array(z.string().min(1).max(120)).min(1).max(20).optional(),
155
+ informationQuery: z.string().min(2).max(200).optional(),
156
+ },
157
+ },
158
+ async ({ dealId, detail, artifactId, informationIds, informationTypes, informationQuery, ...options }) => {
159
+ const filter = informationReadFilter({ ids: informationIds, types: informationTypes, q: informationQuery });
160
+ if (artifactId) {
161
+ if (detail) throw new Error("Use artifactId or detail, not both");
162
+ if (filter) throw new Error("Use artifactId or information filters, not both");
163
+ // @core-api-operation GET /api/occam/deals/{dealId}/artifacts/{artifactId}
164
+ return callApi("GET", buildArtifactReadPath(dealId, artifactId, options));
165
+ }
166
+ if (Object.values(options).some(value => value !== undefined)) throw new Error("Content options require artifactId");
167
+ // @core-api-operation GET /api/occam/deals/{dealId}
168
+ return callApi("GET", buildDealReadPath(dealId, detail || "overview", filter), undefined,
169
+ (result) => verifyInformationReadResult(result, filter));
170
+ },
171
+ );
172
+
173
+ registerTool(
174
+ "create_deal",
175
+ {
176
+ description: "Create one Deal intent. Core owns Drive, initial resources, idempotency, and Events.",
177
+ inputSchema: {
178
+ companyName: z.string().min(1).max(240),
179
+ companyKey: z.string().min(1).max(240).optional(),
180
+ page: z.record(z.string(), z.any()).optional(),
181
+ information: z.array(z.record(z.string(), z.any())).optional(),
182
+ origin: originSchema,
183
+ idempotencyKey: z.string().min(1).max(240).optional(),
184
+ },
185
+ },
186
+ async (input) => callApi("POST", "/api/occam/deals/commands", prepareDealCommand("create", input)),
187
+ );
188
+
189
+ registerTool(
190
+ "write_deal",
191
+ {
192
+ description: "The only Deal mutation tool. Pass one command: input.submit, information.put, page.patch, or artifact.put. A file or HTML about one company belongs here as artifact.put; standalone knowledge not tied to one company belongs in wiki_save. Before artifact.put, read Deal files: reuse artifactId for a revision; a same-kind/title collision without an ID returns OCCAM_CONFLICT. Titles are candidates, not identity. Select the intended existing ID from the request/content, or explicitly generate a fresh UUID for a distinct same-name source. Send complete revised bytes, preserve applicable metadata, and verify the ID/version. Core hosts text/html in Command; do not mirror HTML to Drive or require a Drive link. Non-HTML source files, including original transcripts and recordings, go to Drive through artifact.put. Keep source-file bodies out of Input, Information, Page and metadata; save extracted facts and summaries with source references and preserve the user request. page.patch does not edit file bytes. All Page content fields are Agent-writable; author controls attribution, not permission. Before page.patch, use get_live_deal_page_schema for the exact fields being changed and write bilingual Page prose. page.patch is JSON Merge Patch: arrays replace whole arrays, so read-modify-write and preserve sibling slots. A Llama user's own judgment is human_subjective_view.people or human_subjective_view.business and must carry value.speaker, value.rawText quoted verbatim from origin.originalUserUtterance, and value.summary (the agent's restatement); it is rejected otherwise.",
193
+ // MCP SDK 1.30 only publishes object schemas at the tool root. Nesting one
194
+ // command preserves the real discriminated union instead of flattening four
195
+ // incompatible operations into false affordances.
196
+ inputSchema: { command: writeDealCommandSchema },
197
+ },
198
+ async ({ command }) => callApi(
199
+ "POST",
200
+ "/api/occam/deals/commands",
201
+ prepareDealCommand("write", command),
202
+ (result) => compactDealWriteResult(command, result),
203
+ ),
204
+ );
205
+
206
+ // Deal Memory is a separate sidecar domain. These do not add an Occam Deal
207
+ // business resource or widen the deliberately closed four-tool Deal surface.
208
+ registerTool(
209
+ "get_deal_memory",
210
+ {
211
+ description: "Read the complete canonical Markdown Deal Story and its opaque concurrency version. A returned placeholder is an existing Story whose version is required for a write; only 404 means no Story exists.",
212
+ inputSchema: { dealId: z.string().uuid() },
213
+ },
214
+ // @core-api-operation GET /api/deal-memory/{dealId}/story
215
+ async ({ dealId }) => callApi("GET", buildDealMemoryPath(dealId)),
216
+ );
217
+
218
+ registerTool(
219
+ "update_deal_memory",
220
+ {
221
+ description: "Replace the complete Markdown Deal Story with one coherent current understanding; never append an update log, restate Live Deal Page/Deal Information fields, or write when understanding would not materially improve. Frontmatter must contain matching deal_id plus stable uuid and created timestamps; updated must strictly advance. Pass the version from get_deal_memory for every returned Story, including a placeholder; only 404 permits creation without expected_version.",
222
+ inputSchema: {
223
+ dealId: z.string().uuid(),
224
+ markdown: z.string().min(1).max(1_048_576),
225
+ expected_version: z.string().min(1).optional(),
226
+ },
227
+ },
228
+ // @core-api-operation PUT /api/deal-memory/{dealId}/story
229
+ async ({ dealId, markdown, expected_version }) => callApi(
230
+ "PUT",
231
+ buildDealMemoryPath(dealId),
232
+ { markdown, ...(expected_version ? { expected_version } : {}) },
233
+ ),
234
+ );
235
+
236
+ registerTool(
237
+ "agent_bootstrap",
238
+ {
239
+ description: "Mandatory before Llama work: fetch the authenticated two-part private Brain (Investment Framework V3 + Llama Command operating skill), a compact Live Deal Page schema index, runtime contract including Wiki publishing guidance, and visible Llama OS skills. Read exact Page fields separately with get_live_deal_page_schema.",
240
+ inputSchema: { limit: z.number().int().min(1).max(100).optional() },
241
+ },
242
+ async ({ limit } = {}) => {
243
+ const params = new URLSearchParams({ clientVersion: version });
244
+ params.set("page_schema", "progressive");
245
+ if (limit) params.set("limit", String(limit));
246
+ try {
247
+ const result = await request("GET", `/api/agent/manifest?${params}`);
248
+ return textResult(JSON.stringify(structuredBootstrapManifest(result), null, 2));
249
+ } catch (error) {
250
+ return textResult(formatErrorForDisplay(error), true);
251
+ }
252
+ },
253
+ );
254
+
255
+ registerTool(
256
+ "get_live_deal_page_schema",
257
+ {
258
+ description: "Read the Live Deal Page schema progressively. Call with no selector for the compact index; before page.patch, request only exact fields or one section. This is read-only context, not a fifth Deal action.",
259
+ inputSchema: {
260
+ fields: z.array(z.string().min(1)).min(1).max(MAX_PAGE_SCHEMA_FIELDS).optional(),
261
+ section: z.string().min(1).optional(),
262
+ },
263
+ },
264
+ async ({ fields, section } = {}) => {
265
+ try {
266
+ // @core-api-operation GET /api/agent/page-schema
267
+ return await callApi("GET", buildPageSchemaPath({ fields, section }));
268
+ } catch (error) {
269
+ return textResult(formatErrorForDisplay(error), true);
270
+ }
271
+ },
272
+ );
273
+
274
+ registerTool(
275
+ "skills_search",
276
+ {
277
+ description: "Search authenticated runtime skills; read only the relevant result.",
278
+ inputSchema: { q: z.string().min(1), limit: z.number().int().min(1).max(100).optional() },
279
+ },
280
+ async ({ q, limit }) => {
281
+ const params = new URLSearchParams({ q });
282
+ if (limit) params.set("limit", String(limit));
283
+ return callApi("GET", `/api/agent/skills?${params}`);
284
+ },
285
+ );
286
+
287
+ registerTool(
288
+ "skills_read",
289
+ {
290
+ description: "Read one authenticated runtime skill by slug.",
291
+ inputSchema: { slug: z.string().min(1) },
292
+ },
293
+ async ({ slug }) => callApi("GET", `/api/agent/skills/${encodeURIComponent(slug)}`),
294
+ );
295
+
296
+ registerTool(
297
+ "pref_list",
298
+ {
299
+ description: "List standing agent preferences.",
300
+ inputSchema: { status: z.enum(["active", "proposed", "retired", "all"]).optional() },
301
+ },
302
+ async ({ status } = {}) => {
303
+ const params = new URLSearchParams();
304
+ if (status) params.set("status", status);
305
+ return callApi("GET", `/api/agent/preferences${params.size ? `?${params}` : ""}`);
306
+ },
307
+ );
308
+
309
+ registerTool(
310
+ "pref_add",
311
+ {
312
+ description: "Save a durable user or team preference; procedures belong in skills.",
313
+ inputSchema: {
314
+ key: z.string().min(1),
315
+ content: z.string().min(1).max(280),
316
+ scope: z.enum(["user", "team"]).optional(),
317
+ evidence: z.string().optional(),
318
+ },
319
+ },
320
+ async ({ key, content, scope, evidence }) =>
321
+ callApi("POST", "/api/agent/preferences", { key, content, scope, evidence }),
322
+ );
323
+
324
+ registerTool(
325
+ "pref_set_status",
326
+ {
327
+ description: "Activate or retire a standing preference.",
328
+ inputSchema: { id: z.number().int().positive(), status: z.enum(["active", "retired"]) },
329
+ },
330
+ async ({ id, status }) => callApi("PATCH", `/api/agent/preferences/${id}`, { status }),
331
+ );
332
+
333
+ registerTool(
334
+ "object_inspect",
335
+ {
336
+ description: "Explain a Command URL or object before guessing that it is broken.",
337
+ inputSchema: {
338
+ q: z.string().optional(),
339
+ type: z.string().optional(),
340
+ id: z.string().optional(),
341
+ lang: z.enum(["en", "zh"]).optional(),
342
+ },
343
+ },
344
+ async ({ q, type, id, lang } = {}) => {
345
+ const params = new URLSearchParams();
346
+ if (q) params.set("q", q);
347
+ if (type) params.set("type", type);
348
+ if (id) params.set("id", id);
349
+ if (lang) params.set("lang", lang);
350
+ return callApi("GET", `/api/agent/explain?${params}`);
351
+ },
352
+ );
353
+
354
+ registerTool(
355
+ "wiki_search",
356
+ {
357
+ description: "Search institutional knowledge in the internal Wiki.",
358
+ inputSchema: { q: z.string().min(1) },
359
+ },
360
+ async ({ q }) => callApi("GET", `/api/wiki/search?q=${encodeURIComponent(q)}`),
361
+ );
362
+
363
+ registerTool(
364
+ "wiki_read",
365
+ {
366
+ description: "Read one Wiki article. format=text extracts its original upload or page body. attachment follows an ID listed in attachments. Continuations require offset and sha256.",
367
+ inputSchema: {
368
+ slug: z.string().min(1), lang: z.enum(["en", "zh"]).optional(),
369
+ format: z.literal("text").optional(), attachment: z.string().optional(),
370
+ offset: z.number().int().min(0).optional(), limit: z.number().int().min(1).max(50000).optional(),
371
+ sha256: z.string().regex(/^[a-f0-9]{64}$/).optional(),
372
+ },
373
+ },
374
+ // @core-api-operation GET /api/wiki/{slug}
375
+ async ({ slug, ...options }) => callApi("GET", buildWikiReadPath(slug, options)),
376
+ );
377
+
378
+ registerTool(
379
+ "wiki_save",
380
+ {
381
+ description: "Publish institutional knowledge when authorized. For knowledge not tied to one company; a file or HTML about one company is a Deal Artifact (write_deal artifact.put), unless the user names the Wiki. Follow agent_bootstrap contract.wiki_publishing. New pages default to complete self-contained HTML with embedded rendering assets and full report content/interactions; set content_type=html. Honor explicit Markdown requests. Read the same slug/lang before edits and preserve its format unless conversion is authorized. Verify the receipt, canonical page and interactions before reporting completion.",
382
+ inputSchema: {
383
+ slug: z.string().min(1),
384
+ title: z.string().min(1),
385
+ content: z.string().describe("Complete page source. For HTML, include the full document and embedded rendering dependencies."),
386
+ sources: z.array(z.string()).min(1),
387
+ content_type: z.enum(["markdown", "html"]).optional().describe("Set html for HTML publications. Use markdown for an explicit Markdown request or an existing Markdown article."),
388
+ lang: z.enum(["en", "zh"]).optional().describe("Article language; Core defaults to en when omitted."),
389
+ },
390
+ },
391
+ async ({ slug, title, content, sources, content_type, lang }) =>
392
+ callApi("POST", "/api/wiki/save", { slug, title, content, sources, content_type, lang }),
393
+ );
394
+
395
+ registerTool(
396
+ "wiki_delete",
397
+ {
398
+ description: "Soft-delete a Wiki article.",
399
+ inputSchema: { slug: z.string().min(1), lang: z.enum(["en", "zh"]).optional() },
400
+ },
401
+ async ({ slug, lang }) =>
402
+ callApi("DELETE", `/api/wiki/${encodeURIComponent(slug)}?lang=${lang === "zh" ? "zh" : "en"}`),
403
+ );
404
+
405
+ registerTool(
406
+ "wiki_restore",
407
+ {
408
+ description: "Restore a soft-deleted Wiki article.",
409
+ inputSchema: { slug: z.string().min(1), lang: z.enum(["en", "zh"]).optional() },
410
+ },
411
+ async ({ slug, lang }) =>
412
+ callApi("POST", `/api/wiki/${encodeURIComponent(slug)}/restore?lang=${lang === "zh" ? "zh" : "en"}`),
413
+ );
414
+ }
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@llamaventures/cli",
3
- "version": "2.6.0",
3
+ "version": "2.8.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:auth && 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/feedback.test.mjs scripts/content-read.test.mjs 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/page-schema-cli.test.mjs scripts/page-schema.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/wiki-publishing.test.mjs scripts/feedback.test.mjs scripts/content-read.test.mjs 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/page-schema-cli.test.mjs scripts/page-schema.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",