@llamaventures/cli 1.26.0 → 2.0.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 +125 -344
- package/CHANGELOG.md +13 -0
- package/README.md +116 -213
- package/README.zh-CN.md +112 -205
- package/bin/llama-mcp.mjs +209 -1995
- package/bin/llama.mjs +388 -3512
- package/contracts/core-api.json +4 -4
- package/contracts/required-operations.json +27 -348
- package/lib/build-manifest.json +4 -4
- package/lib/client.mjs +12 -5
- package/lib/deal-actions.mjs +102 -0
- package/package.json +2 -2
- package/contracts/investment-workflow-v2.md +0 -19
- package/lib/workflow-audit.mjs +0 -13
- package/lib/workflow-remediation.mjs +0 -45
package/bin/llama-mcp.mjs
CHANGED
|
@@ -1,28 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
// CLI in the same package; both share auth + HTTP via lib/client.mjs.
|
|
5
|
-
//
|
|
6
|
-
// Wire into Claude Code / Cursor / Claude Desktop / OpenClaw via your
|
|
7
|
-
// agent's MCP config — see README for snippets. Auth is identical to the
|
|
8
|
-
// CLI: gcloud (preferred) → $LLAMA_TOKEN → ~/.llama/token.
|
|
9
|
-
|
|
10
|
-
import { createRequire } from "module";
|
|
11
|
-
import { randomUUID } from "crypto";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
12
4
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
13
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
14
6
|
import { z } from "zod";
|
|
15
7
|
import {
|
|
16
8
|
getAuthHeaders,
|
|
17
|
-
getBaseUrl,
|
|
18
9
|
readBriefing,
|
|
19
10
|
request,
|
|
20
|
-
requestSse,
|
|
21
11
|
setClientRuntime,
|
|
22
12
|
} from "../lib/client.mjs";
|
|
23
|
-
|
|
24
|
-
const requireFromHere = createRequire(import.meta.url);
|
|
25
|
-
const { version: PKG_VERSION } = requireFromHere("../package.json");
|
|
26
13
|
import {
|
|
27
14
|
clearExternalSession,
|
|
28
15
|
getExternalSessionStatus,
|
|
@@ -30,38 +17,17 @@ import {
|
|
|
30
17
|
startExternalSession,
|
|
31
18
|
uploadExternalFile,
|
|
32
19
|
} from "../lib/external.mjs";
|
|
20
|
+
import {
|
|
21
|
+
buildDealReadPath,
|
|
22
|
+
buildDealSearchPath,
|
|
23
|
+
prepareDealCommand,
|
|
24
|
+
} from "../lib/deal-actions.mjs";
|
|
33
25
|
|
|
26
|
+
const requireFromHere = createRequire(import.meta.url);
|
|
27
|
+
const { version: PKG_VERSION } = requireFromHere("../package.json");
|
|
34
28
|
setClientRuntime({ client: "mcp" });
|
|
35
29
|
|
|
36
|
-
|
|
37
|
-
return `mcp-${randomUUID()}`;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function normalizeUploadId(value) {
|
|
41
|
-
if (typeof value !== "string" || !value.trim()) return null;
|
|
42
|
-
const id = value.trim();
|
|
43
|
-
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(id)) {
|
|
44
|
-
throw new Error("clientUploadId must be 1-128 chars: letters, numbers, dot, underscore, colon, or hyphen");
|
|
45
|
-
}
|
|
46
|
-
return id;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Wrap a request() call into the MCP CallToolResult shape. Catches errors
|
|
50
|
-
// (NO_AUTH / 401 / 5xx / network) and surfaces them as `isError: true`
|
|
51
|
-
// content so the calling agent sees a clean error string instead of the
|
|
52
|
-
// MCP transport closing.
|
|
53
|
-
async function callApi(method, path, body, opts = {}) {
|
|
54
|
-
try {
|
|
55
|
-
const result = await request(method, path, body, opts);
|
|
56
|
-
const text = typeof result === "string" ? result : JSON.stringify(result, null, 2);
|
|
57
|
-
return { content: [{ type: "text", text }] };
|
|
58
|
-
} catch (err) {
|
|
59
|
-
return {
|
|
60
|
-
content: [{ type: "text", text: `Error: ${err?.message ?? String(err)}` }],
|
|
61
|
-
isError: true,
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
}
|
|
30
|
+
const server = new McpServer({ name: "llama-mcp", version: PKG_VERSION });
|
|
65
31
|
|
|
66
32
|
function textResult(text, isError = false) {
|
|
67
33
|
return {
|
|
@@ -70,2101 +36,349 @@ function textResult(text, isError = false) {
|
|
|
70
36
|
};
|
|
71
37
|
}
|
|
72
38
|
|
|
73
|
-
function
|
|
74
|
-
return textResult(JSON.stringify(value, null, 2), isError);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
async function callWorkflow(dealId, type, fields) {
|
|
78
|
-
try {
|
|
79
|
-
const path = `/api/deals/${encodeURIComponent(dealId)}/workflow`;
|
|
80
|
-
// @core-api-operation GET /api/deals/{dealId}/workflow
|
|
81
|
-
const current = await request("GET", path);
|
|
82
|
-
const expectedRevision = current?.workflow?.revision;
|
|
83
|
-
if (!Number.isInteger(expectedRevision)) {
|
|
84
|
-
return textResult("Error: Investment Workflow V2 is not initialized for this deal.", true);
|
|
85
|
-
}
|
|
86
|
-
// @core-api-operation POST /api/deals/{dealId}/workflow
|
|
87
|
-
return callApi("POST", path, {
|
|
88
|
-
type,
|
|
89
|
-
requestId: `mcp:${type}:${randomUUID()}`,
|
|
90
|
-
expectedRevision,
|
|
91
|
-
...fields,
|
|
92
|
-
});
|
|
93
|
-
} catch (err) {
|
|
94
|
-
return textResult(`Error: ${err?.message ?? String(err)}`, true);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function splitSources(value) {
|
|
99
|
-
if (Array.isArray(value)) return value.filter(Boolean);
|
|
100
|
-
if (!value || value === true) return undefined;
|
|
101
|
-
return String(value)
|
|
102
|
-
.split(",")
|
|
103
|
-
.map((s) => s.trim())
|
|
104
|
-
.filter(Boolean);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function buildEnrichmentAgentMessage(args = {}) {
|
|
108
|
-
if (args.message) return String(args.message);
|
|
109
|
-
const sources = splitSources(args.sources) ?? [
|
|
110
|
-
"website",
|
|
111
|
-
"github",
|
|
112
|
-
"linkedin",
|
|
113
|
-
"yc",
|
|
114
|
-
"launch",
|
|
115
|
-
"web",
|
|
116
|
-
"monid",
|
|
117
|
-
];
|
|
118
|
-
const budget = args.budgetCents ?? "50";
|
|
119
|
-
return [
|
|
120
|
-
"Run server-side deal enrichment for this deal.",
|
|
121
|
-
`Use sources: ${sources.join(", ")}.`,
|
|
122
|
-
`Private Monid budget cap: ${budget} cents.`,
|
|
123
|
-
"Read the enrichment harness first, then collect current company/founder evidence.",
|
|
124
|
-
"Write canonical evidence links, sourced deal facts, stable deal fields, and typed factual values where supported.",
|
|
125
|
-
"For typed factual values, call read_typed_factual_layer first and use upsert_typed_fact for queryable fields.",
|
|
126
|
-
"Search snippets alone are not high-confidence evidence; fetch direct sources where possible.",
|
|
127
|
-
"Do not generate Memo; the durable Memo Agent in Llama Command owns that separate workflow.",
|
|
128
|
-
"End with what was written, what was skipped, and open questions.",
|
|
129
|
-
].join(" ");
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function summarizeAgentEvents(events = []) {
|
|
133
|
-
return events
|
|
134
|
-
.flatMap((event) => {
|
|
135
|
-
if (event.tool_use?.name) return [{ type: "tool_use", name: event.tool_use.name }];
|
|
136
|
-
if (event.tool_result?.name) {
|
|
137
|
-
return [
|
|
138
|
-
{
|
|
139
|
-
type: "tool_result",
|
|
140
|
-
name: event.tool_result.name,
|
|
141
|
-
ok: event.tool_result.ok ?? null,
|
|
142
|
-
summary: event.tool_result.summary ?? null,
|
|
143
|
-
},
|
|
144
|
-
];
|
|
145
|
-
}
|
|
146
|
-
if (event.error) return [{ type: "error", error: String(event.error) }];
|
|
147
|
-
return [];
|
|
148
|
-
})
|
|
149
|
-
.slice(-80);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
async function runDealAgentTool({ dealId, message, title = "MCP agent run" }) {
|
|
153
|
-
try {
|
|
154
|
-
const thread = await request("POST", `/api/deals/${encodeURIComponent(dealId)}/threads`, { title });
|
|
155
|
-
if (!thread?.id) throw new Error("Thread creation did not return an id");
|
|
156
|
-
const result = await requestSse(
|
|
157
|
-
"POST",
|
|
158
|
-
`/api/deals/${encodeURIComponent(dealId)}/threads/${encodeURIComponent(thread.id)}`,
|
|
159
|
-
{ message },
|
|
160
|
-
);
|
|
161
|
-
return textResult(
|
|
162
|
-
JSON.stringify(
|
|
163
|
-
{
|
|
164
|
-
ok: true,
|
|
165
|
-
threadId: thread.id,
|
|
166
|
-
text: result.text,
|
|
167
|
-
toolEvents: summarizeAgentEvents(result.events),
|
|
168
|
-
},
|
|
169
|
-
null,
|
|
170
|
-
2,
|
|
171
|
-
),
|
|
172
|
-
);
|
|
173
|
-
} catch (err) {
|
|
174
|
-
return textResult(`Error: ${err?.message ?? String(err)}`, true);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Append a block to a deal brief. The /blocks route only accepts atomic
|
|
179
|
-
// full-array PUTs (no POST), so we GET current blocks, prepend the new
|
|
180
|
-
// one (matches UI default since 2026-05-03), and PUT the merged array.
|
|
181
|
-
// Server stamps identity meta on PUT; we don't send any.
|
|
182
|
-
async function addBriefBlock(dealId, block, cueAuthorized = false) {
|
|
39
|
+
async function callApi(method, path, body) {
|
|
183
40
|
try {
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
"PUT",
|
|
189
|
-
`/api/deals/${encodeURIComponent(dealId)}/blocks`,
|
|
190
|
-
{ blocks: [{ id, ...block }, ...existing], cue_authorized: cueAuthorized === true }
|
|
191
|
-
);
|
|
192
|
-
const text = JSON.stringify(
|
|
193
|
-
{ ok: result?.ok ?? true, id, count: result?.count ?? existing.length + 1 },
|
|
194
|
-
null,
|
|
195
|
-
2
|
|
196
|
-
);
|
|
197
|
-
return { content: [{ type: "text", text }] };
|
|
198
|
-
} catch (err) {
|
|
199
|
-
return {
|
|
200
|
-
content: [{ type: "text", text: `Error: ${err?.message ?? String(err)}` }],
|
|
201
|
-
isError: true,
|
|
202
|
-
};
|
|
41
|
+
const result = await request(method, path, body);
|
|
42
|
+
return textResult(typeof result === "string" ? result : JSON.stringify(result, null, 2));
|
|
43
|
+
} catch (error) {
|
|
44
|
+
return textResult(`Error: ${error?.message ?? String(error)}`, true);
|
|
203
45
|
}
|
|
204
46
|
}
|
|
205
47
|
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
48
|
+
const originSchema = z.object({
|
|
49
|
+
kind: z.enum(["user", "agent", "system"]),
|
|
50
|
+
originalUserUtterance: z.string().min(1).optional(),
|
|
51
|
+
originatingChatRecordId: z.string().uuid().optional(),
|
|
209
52
|
});
|
|
210
53
|
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
// ============================================================
|
|
214
|
-
|
|
215
|
-
server.registerTool(
|
|
216
|
-
"auth_status",
|
|
217
|
-
{
|
|
218
|
-
description:
|
|
219
|
-
"Verify Llama Command credentials and return current user identity. " +
|
|
220
|
-
"Call this first if any other tool returns Error[NO_AUTH] or Error[UNAUTHORIZED].",
|
|
221
|
-
inputSchema: {},
|
|
222
|
-
},
|
|
223
|
-
async () => {
|
|
224
|
-
const headers = await getAuthHeaders();
|
|
225
|
-
if (Object.keys(headers).length === 0) {
|
|
226
|
-
return {
|
|
227
|
-
content: [
|
|
228
|
-
{
|
|
229
|
-
type: "text",
|
|
230
|
-
text:
|
|
231
|
-
"Error[NO_AUTH]: No credentials found. Mint a token at " +
|
|
232
|
-
"https://command.llamaventures.vc/settings/tokens, then save the " +
|
|
233
|
-
"llc_... value to ~/.llama/token (mode 0600), or set $LLAMA_TOKEN.",
|
|
234
|
-
},
|
|
235
|
-
],
|
|
236
|
-
isError: true,
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
return callApi("GET", "/api/me");
|
|
240
|
-
}
|
|
241
|
-
);
|
|
242
|
-
|
|
243
|
-
server.registerTool(
|
|
244
|
-
"agent_bootstrap",
|
|
245
|
-
{
|
|
246
|
-
description:
|
|
247
|
-
"Fetch the live Llama Command + Llama OS runtime manifest. Use this at " +
|
|
248
|
-
"the start of an agent session to discover current skills, the skill " +
|
|
249
|
-
"bundle version, and the object-inspection contract. Unlike the bundled " +
|
|
250
|
-
"agent_briefing prompt, this comes from authenticated Command runtime.",
|
|
251
|
-
inputSchema: {
|
|
252
|
-
limit: z.number().optional().describe("number of skill summaries to include; default 25"),
|
|
253
|
-
},
|
|
254
|
-
},
|
|
255
|
-
async ({ limit } = {}) => {
|
|
256
|
-
const params = new URLSearchParams();
|
|
257
|
-
params.set("clientVersion", PKG_VERSION);
|
|
258
|
-
if (limit) params.set("limit", String(limit));
|
|
259
|
-
return callApi("GET", `/api/agent/manifest${params.toString() ? `?${params}` : ""}`);
|
|
260
|
-
}
|
|
261
|
-
);
|
|
262
|
-
|
|
263
|
-
server.registerTool(
|
|
264
|
-
"skills_search",
|
|
265
|
-
{
|
|
266
|
-
description:
|
|
267
|
-
"Search the authenticated Llama OS runtime skill library. Call this " +
|
|
268
|
-
"before choosing a workflow for Llama pipeline/wiki/DD/research/ops tasks. " +
|
|
269
|
-
"Returns summaries only; call skills_read for the exact SKILL.md.",
|
|
270
|
-
inputSchema: {
|
|
271
|
-
q: z.string().describe("workflow/task query, e.g. 'wiki delete tombstone' or 'deal DD memo'"),
|
|
272
|
-
limit: z.number().optional().describe("default 20"),
|
|
273
|
-
},
|
|
274
|
-
},
|
|
275
|
-
async ({ q, limit }) => {
|
|
276
|
-
const params = new URLSearchParams({ q });
|
|
277
|
-
if (limit) params.set("limit", String(limit));
|
|
278
|
-
return callApi("GET", `/api/agent/skills?${params}`);
|
|
279
|
-
}
|
|
280
|
-
);
|
|
281
|
-
|
|
282
|
-
server.registerTool(
|
|
283
|
-
"pref_list",
|
|
284
|
-
{
|
|
285
|
-
description:
|
|
286
|
-
"List standing agent preferences (team scope + the caller's user scope). " +
|
|
287
|
-
"These are injected into every server-side agent turn. Use status=proposed " +
|
|
288
|
-
"to review pending proposals awaiting approval.",
|
|
289
|
-
inputSchema: {
|
|
290
|
-
status: z.enum(["active", "proposed", "retired", "all"]).optional()
|
|
291
|
-
.describe("filter; defaults to active"),
|
|
292
|
-
},
|
|
293
|
-
},
|
|
294
|
-
async ({ status }) => {
|
|
295
|
-
const params = new URLSearchParams();
|
|
296
|
-
if (status) params.set("status", status);
|
|
297
|
-
return callApi("GET", `/api/agent/preferences${params.toString() ? `?${params}` : ""}`);
|
|
298
|
-
}
|
|
299
|
-
);
|
|
300
|
-
|
|
301
|
-
server.registerTool(
|
|
302
|
-
"pref_add",
|
|
303
|
-
{
|
|
304
|
-
description:
|
|
305
|
-
"Save a standing preference so every Llama agent follows it from the next " +
|
|
306
|
-
"turn on. Use when the user states a durable way they want agents to work " +
|
|
307
|
-
"(style, workflow, defaults). Content is hard-capped at 280 chars — if it " +
|
|
308
|
-
"does not fit, it is a procedure and belongs in a skill. Team scope needs " +
|
|
309
|
-
"system-admin approval; own user scope activates immediately.",
|
|
310
|
-
inputSchema: {
|
|
311
|
-
key: z.string().describe("short slug, e.g. reply-style.conclusion-first"),
|
|
312
|
-
content: z.string().describe("the preference, max 280 chars"),
|
|
313
|
-
scope: z.enum(["user", "team"]).optional().describe("default user (the caller)"),
|
|
314
|
-
evidence: z.string().optional().describe("what prompted this (run, correction)"),
|
|
315
|
-
},
|
|
316
|
-
},
|
|
317
|
-
async ({ key, content, scope, evidence }) =>
|
|
318
|
-
callApi("POST", "/api/agent/preferences", { key, content, scope, evidence })
|
|
319
|
-
);
|
|
320
|
-
|
|
321
|
-
server.registerTool(
|
|
322
|
-
"pref_set_status",
|
|
323
|
-
{
|
|
324
|
-
description:
|
|
325
|
-
"Approve (activate) or retire a standing preference by id. Own user scope " +
|
|
326
|
-
"is self-service; team scope requires a system admin.",
|
|
327
|
-
inputSchema: {
|
|
328
|
-
id: z.number().describe("preference id from pref_list"),
|
|
329
|
-
status: z.enum(["active", "retired"]).describe("new status"),
|
|
330
|
-
},
|
|
331
|
-
},
|
|
332
|
-
async ({ id, status }) =>
|
|
333
|
-
callApi("PATCH", `/api/agent/preferences/${encodeURIComponent(String(id))}`, { status })
|
|
334
|
-
);
|
|
335
|
-
|
|
336
|
-
server.registerTool(
|
|
337
|
-
"skills_read",
|
|
338
|
-
{
|
|
339
|
-
description:
|
|
340
|
-
"Read one runtime Llama OS skill by slug. Use after skills_search. " +
|
|
341
|
-
"Returns the full SKILL.md content from Llama Command; public npm does " +
|
|
342
|
-
"not bundle private skill text.",
|
|
343
|
-
inputSchema: {
|
|
344
|
-
slug: z.string().describe("skill slug, e.g. llama-command or llama-wiki"),
|
|
345
|
-
},
|
|
346
|
-
},
|
|
347
|
-
async ({ slug }) => callApi("GET", `/api/agent/skills/${encodeURIComponent(slug)}`)
|
|
348
|
-
);
|
|
349
|
-
|
|
350
|
-
server.registerTool(
|
|
351
|
-
"object_inspect",
|
|
352
|
-
{
|
|
353
|
-
description:
|
|
354
|
-
"Explain a Llama Command URL or object id. Use for 404s, deleted wiki " +
|
|
355
|
-
"pages, notifier links, deal URLs, brief blocks, HTML docs, and unknown " +
|
|
356
|
-
"Command objects before guessing that the system is broken.",
|
|
357
|
-
inputSchema: {
|
|
358
|
-
q: z.string().optional().describe("URL or compact query, e.g. wiki:my-slug or a Command URL"),
|
|
359
|
-
type: z.string().optional().describe("explicit object type if not using q"),
|
|
360
|
-
id: z.string().optional().describe("explicit object id if not using q"),
|
|
361
|
-
lang: z.enum(["en", "zh"]).optional().describe("wiki language; default en"),
|
|
362
|
-
},
|
|
363
|
-
},
|
|
364
|
-
async ({ q, type, id, lang } = {}) => {
|
|
365
|
-
const params = new URLSearchParams();
|
|
366
|
-
if (q) params.set("q", q);
|
|
367
|
-
if (type) params.set("type", type);
|
|
368
|
-
if (id) params.set("id", id);
|
|
369
|
-
if (lang) params.set("lang", lang);
|
|
370
|
-
return callApi("GET", `/api/agent/explain?${params}`);
|
|
371
|
-
}
|
|
372
|
-
);
|
|
373
|
-
|
|
54
|
+
// Deal is deliberately a closed four-tool surface. Do not add split resource
|
|
55
|
+
// tools here: one more tool is one more branch every agent must reason about.
|
|
374
56
|
server.registerTool(
|
|
375
|
-
"
|
|
57
|
+
"search_deals",
|
|
376
58
|
{
|
|
377
|
-
description:
|
|
378
|
-
"Read Command's curated agent activity projection. Use this before " +
|
|
379
|
-
"scanning raw timelines or event-bus payloads. Examples: new deals in " +
|
|
380
|
-
"the past 24h, deals with meaningful updates in the past 7d, or recent " +
|
|
381
|
-
"fact/memo/brief events. Returns source ids so callers can drill down.",
|
|
59
|
+
description: "Search compact Live Deal Page candidates. Always use this before creating a company.",
|
|
382
60
|
inputSchema: {
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
.describe("events = raw curated rows; new_deals = deal.created; updated_deals = grouped meaningful deal updates"),
|
|
387
|
-
since: z.string().optional().describe("24h, 7d, 30d, or ISO timestamp; default 24h"),
|
|
388
|
-
limit: z.number().optional().describe("default 50, cap 100"),
|
|
389
|
-
dealId: z.string().optional().describe("optional single deal UUID"),
|
|
390
|
-
entity: z.enum(["deal", "wiki", "all"]).optional().describe("default deal"),
|
|
391
|
-
verb: z.string().optional().describe("comma-separated activity verbs, e.g. fact.added,brief.revised"),
|
|
392
|
-
cursor: z.number().optional().describe("pagination cursor from next_cursor"),
|
|
393
|
-
minSignificance: z.number().optional().describe("1..3; default 2, new_deals default 3"),
|
|
61
|
+
q: z.string().optional(),
|
|
62
|
+
state: z.enum(["active", "archived", "trashed"]).optional(),
|
|
63
|
+
limit: z.number().int().min(1).max(2000).optional(),
|
|
394
64
|
},
|
|
395
65
|
},
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
if (since) params.set("since", since);
|
|
399
|
-
if (limit) params.set("limit", String(limit));
|
|
400
|
-
if (dealId) params.set("deal_id", dealId);
|
|
401
|
-
if (entity) params.set("entity", entity);
|
|
402
|
-
if (verb) params.set("verb", verb);
|
|
403
|
-
if (cursor) params.set("cursor", String(cursor));
|
|
404
|
-
if (minSignificance) params.set("min_sig", String(minSignificance));
|
|
405
|
-
return callApi("GET", `/api/agent/activity?${params}`);
|
|
406
|
-
}
|
|
66
|
+
// @core-api-operation GET /api/occam/deals
|
|
67
|
+
async ({ q, state, limit } = {}) => callApi("GET", buildDealSearchPath(q, { state, limit })),
|
|
407
68
|
);
|
|
408
69
|
|
|
409
|
-
// ============================================================
|
|
410
|
-
// Deals — read
|
|
411
|
-
// ============================================================
|
|
412
|
-
|
|
413
70
|
server.registerTool(
|
|
414
|
-
"
|
|
71
|
+
"read_deal",
|
|
415
72
|
{
|
|
416
|
-
description:
|
|
417
|
-
"Search the Llama Ventures deal pipeline. Fuzzy match on company name, " +
|
|
418
|
-
"founders, description, founder info, notes, deal owner, source, source direction, and location. " +
|
|
419
|
-
"Returns up to `limit` deals (default 200, cap 1000).",
|
|
73
|
+
description: "Read one Deal. Live Page is always returned; expand only the resource needed.",
|
|
420
74
|
inputSchema: {
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
founder: z.string().optional().describe("fuzzy match on founders / founderInfo"),
|
|
424
|
-
owner: z.string().optional().describe("fuzzy match on dealOwner"),
|
|
425
|
-
status: z
|
|
426
|
-
.string()
|
|
427
|
-
.optional()
|
|
428
|
-
.describe(
|
|
429
|
-
"exact match on 'Our Stage' (Interested, Outreached, Sourced, First Meeting, Diligence, Partner Meeting, Term Sheet, Invested, Passed, Stalled, Future, Unknown). Interested means we want to record/track before contact; Outreached means contact was logged but no effective relationship/response exists yet."
|
|
430
|
-
),
|
|
431
|
-
theirStage: z.string().optional().describe("exact match on 'Their Stage'"),
|
|
432
|
-
stage: z
|
|
433
|
-
.string()
|
|
434
|
-
.optional()
|
|
435
|
-
.describe(
|
|
436
|
-
"exact match on Round (Pre-Seed, Seed, Series A, Series B, Series C+, Stealth)"
|
|
437
|
-
),
|
|
438
|
-
sourceDirection: z
|
|
439
|
-
.string()
|
|
440
|
-
.optional()
|
|
441
|
-
.describe("exact match on source direction: Inbound, Outbound, or Unknown"),
|
|
442
|
-
limit: z.number().optional().describe("max results (default 200, cap 1000)"),
|
|
443
|
-
offset: z.number().optional(),
|
|
75
|
+
dealId: z.string().min(1),
|
|
76
|
+
detail: z.enum(["overview", "memory", "files", "conversation", "history", "all"]).optional(),
|
|
444
77
|
},
|
|
445
78
|
},
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
for (const [k, v] of Object.entries(args)) {
|
|
449
|
-
if (v != null && v !== "") params.set(k, String(v));
|
|
450
|
-
}
|
|
451
|
-
return callApi("GET", `/api/deals${params.toString() ? `?${params}` : ""}`);
|
|
452
|
-
}
|
|
79
|
+
// @core-api-operation GET /api/occam/deals/{dealId}
|
|
80
|
+
async ({ dealId, detail }) => callApi("GET", buildDealReadPath(dealId, detail || "overview")),
|
|
453
81
|
);
|
|
454
82
|
|
|
455
83
|
server.registerTool(
|
|
456
|
-
"
|
|
84
|
+
"create_deal",
|
|
457
85
|
{
|
|
458
|
-
description:
|
|
459
|
-
"Get the full canonical record for one deal by uuid. Includes status, " +
|
|
460
|
-
"stage, founders, owner, source, sourceDirection, valuation, all whitelisted writable fields, " +
|
|
461
|
-
"and the `extra` JSONB blob.",
|
|
86
|
+
description: "Create one Deal intent. Core owns Drive, initial resources, idempotency, and Events.",
|
|
462
87
|
inputSchema: {
|
|
463
|
-
|
|
88
|
+
companyName: z.string().min(1).max(240),
|
|
89
|
+
companyKey: z.string().min(1).max(240).optional(),
|
|
90
|
+
page: z.record(z.string(), z.any()).optional(),
|
|
91
|
+
information: z.array(z.record(z.string(), z.any())).optional(),
|
|
92
|
+
origin: originSchema,
|
|
93
|
+
idempotencyKey: z.string().min(1).max(240).optional(),
|
|
464
94
|
},
|
|
465
95
|
},
|
|
466
|
-
async (
|
|
96
|
+
async (input) => callApi("POST", "/api/occam/deals/commands", prepareDealCommand("create", input)),
|
|
467
97
|
);
|
|
468
98
|
|
|
469
|
-
// ============================================================
|
|
470
|
-
// Deals — write
|
|
471
|
-
// ============================================================
|
|
472
|
-
|
|
473
99
|
server.registerTool(
|
|
474
|
-
"
|
|
100
|
+
"write_deal",
|
|
475
101
|
{
|
|
476
|
-
description:
|
|
477
|
-
"Create a new pipeline deal. Source defaults to the caller's user record. " +
|
|
478
|
-
"Owner is an audited responsibility label, not an operating-permission boundary.",
|
|
102
|
+
description: "The only Deal mutation tool: input.submit, information.put, page.patch, or artifact.put.",
|
|
479
103
|
inputSchema: {
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
location: z.string().optional(),
|
|
104
|
+
operation: z.enum(["input.submit", "information.put", "page.patch", "artifact.put"]),
|
|
105
|
+
dealId: z.string().uuid(),
|
|
106
|
+
patch: z.record(z.string(), z.any()).optional(),
|
|
107
|
+
expectedRevision: z.number().int().nonnegative().optional(),
|
|
108
|
+
informationId: z.string().uuid().optional(),
|
|
109
|
+
type: z.string().min(1).max(120).optional(),
|
|
110
|
+
labels: z.array(z.string()).optional(),
|
|
111
|
+
subject: z.record(z.string(), z.any()).optional(),
|
|
112
|
+
value: z.any().optional(),
|
|
113
|
+
expectedVersion: z.number().int().positive().optional(),
|
|
114
|
+
format: z.string().min(1).max(120).optional(),
|
|
115
|
+
content: z.any().optional(),
|
|
116
|
+
source: z.record(z.string(), z.any()).optional(),
|
|
117
|
+
artifactId: z.string().uuid().optional(),
|
|
118
|
+
kind: z.string().min(1).max(120).optional(),
|
|
119
|
+
title: z.string().min(1).max(500).optional(),
|
|
120
|
+
mimeType: z.string().min(1).max(240).optional(),
|
|
121
|
+
contentBase64: z.string().min(1).optional(),
|
|
122
|
+
storageKey: z.string().min(1).optional(),
|
|
123
|
+
storageUrl: z.string().url().optional(),
|
|
124
|
+
byteSize: z.number().int().nonnegative().optional(),
|
|
125
|
+
sha256: z.string().regex(/^[0-9a-f]{64}$/).optional(),
|
|
126
|
+
metadata: z.record(z.string(), z.any()).optional(),
|
|
127
|
+
origin: originSchema,
|
|
128
|
+
idempotencyKey: z.string().min(1).max(240).optional(),
|
|
506
129
|
},
|
|
507
130
|
},
|
|
508
|
-
async (
|
|
131
|
+
async (input) => callApi("POST", "/api/occam/deals/commands", prepareDealCommand("write", input)),
|
|
509
132
|
);
|
|
510
133
|
|
|
511
134
|
server.registerTool(
|
|
512
|
-
"
|
|
135
|
+
"auth_status",
|
|
513
136
|
{
|
|
514
|
-
description:
|
|
515
|
-
|
|
516
|
-
"notes, stage, dealOwner, source, sourceDirection, description, website, location, founders, " +
|
|
517
|
-
"proposedAmount, roundSize, valuation, sector, subsector, foundedYear, leadInvestor, " +
|
|
518
|
-
"investors. Logs a field_change event in deal_events.",
|
|
519
|
-
inputSchema: {
|
|
520
|
-
dealId: z.string(),
|
|
521
|
-
field: z.string().describe("camelCase field name (see description for whitelist)"),
|
|
522
|
-
value: z.union([z.string(), z.number(), z.null()]).describe("new value"),
|
|
523
|
-
},
|
|
137
|
+
description: "Verify Command credentials and current user identity.",
|
|
138
|
+
inputSchema: {},
|
|
524
139
|
},
|
|
525
|
-
async (
|
|
526
|
-
|
|
527
|
-
|
|
140
|
+
async () => {
|
|
141
|
+
const headers = await getAuthHeaders();
|
|
142
|
+
if (!Object.keys(headers).length) {
|
|
143
|
+
return textResult(
|
|
144
|
+
"Error[NO_AUTH]: run `llama auth login`, or save a valid llc_ token with `llama token set`.",
|
|
145
|
+
true,
|
|
146
|
+
);
|
|
528
147
|
}
|
|
529
|
-
return callApi("
|
|
530
|
-
}
|
|
531
|
-
);
|
|
532
|
-
|
|
533
|
-
server.registerTool(
|
|
534
|
-
"workflow_show",
|
|
535
|
-
{
|
|
536
|
-
description: "Read the canonical Investment Workflow V2 state, revision, current transition, and blockers for a deal.",
|
|
537
|
-
inputSchema: { dealId: z.string() },
|
|
538
|
-
},
|
|
539
|
-
async ({ dealId }) => callApi("GET", `/api/deals/${encodeURIComponent(dealId)}/workflow`),
|
|
540
|
-
);
|
|
541
|
-
|
|
542
|
-
server.registerTool(
|
|
543
|
-
"workflow_initialize",
|
|
544
|
-
{
|
|
545
|
-
description: "Persist the canonical Investment Workflow V2 bootstrap state for an unmigrated deal without changing its semantic stage. Intended for audited migration and recovery.",
|
|
546
|
-
inputSchema: { dealId: z.string(), reason: z.string().min(1) },
|
|
148
|
+
return callApi("GET", "/api/me");
|
|
547
149
|
},
|
|
548
|
-
async ({ dealId, reason }) => callWorkflow(dealId, "initialize", { reason }),
|
|
549
150
|
);
|
|
550
151
|
|
|
551
152
|
server.registerTool(
|
|
552
|
-
"
|
|
153
|
+
"agent_bootstrap",
|
|
553
154
|
{
|
|
554
|
-
description: "
|
|
555
|
-
inputSchema: {
|
|
155
|
+
description: "Fetch the authenticated runtime contract and visible Llama OS skills.",
|
|
156
|
+
inputSchema: { limit: z.number().int().min(1).max(100).optional() },
|
|
556
157
|
},
|
|
557
|
-
async ({
|
|
558
|
-
);
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
"workflow_decide_partner_support",
|
|
562
|
-
{
|
|
563
|
-
description: "Record the named Partner's own Support, Need more, or Pass decision. Core verifies caller identity; never use on another person's behalf.",
|
|
564
|
-
inputSchema: { dealId: z.string(), decision: z.enum(["support", "need_more", "pass"]), reason: z.string().min(1) },
|
|
158
|
+
async ({ limit } = {}) => {
|
|
159
|
+
const params = new URLSearchParams({ clientVersion: PKG_VERSION });
|
|
160
|
+
if (limit) params.set("limit", String(limit));
|
|
161
|
+
return callApi("GET", `/api/agent/manifest?${params}`);
|
|
565
162
|
},
|
|
566
|
-
async ({ dealId, decision, reason }) => callWorkflow(dealId, "partner_support_decision", { decision, reason }),
|
|
567
163
|
);
|
|
568
164
|
|
|
569
165
|
server.registerTool(
|
|
570
|
-
"
|
|
166
|
+
"skills_search",
|
|
571
167
|
{
|
|
572
|
-
description: "
|
|
573
|
-
inputSchema: {
|
|
168
|
+
description: "Search authenticated runtime skills; read only the relevant result.",
|
|
169
|
+
inputSchema: { q: z.string().min(1), limit: z.number().int().min(1).max(100).optional() },
|
|
574
170
|
},
|
|
575
|
-
async ({
|
|
576
|
-
);
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
"workflow_resolve_guard",
|
|
580
|
-
{
|
|
581
|
-
description: "Record an audited workflow guard resolution. Waivers require a concrete reason and remain visible in history.",
|
|
582
|
-
inputSchema: { dealId: z.string(), guardKey: z.string(), status: z.enum(["satisfied", "unsatisfied", "waived"]), reason: z.string().min(1) },
|
|
171
|
+
async ({ q, limit }) => {
|
|
172
|
+
const params = new URLSearchParams({ q });
|
|
173
|
+
if (limit) params.set("limit", String(limit));
|
|
174
|
+
return callApi("GET", `/api/agent/skills?${params}`);
|
|
583
175
|
},
|
|
584
|
-
async ({ dealId, guardKey, status, reason }) => callWorkflow(dealId, "resolve_guard", { guardKey, status, reason }),
|
|
585
176
|
);
|
|
586
177
|
|
|
587
178
|
server.registerTool(
|
|
588
|
-
"
|
|
179
|
+
"skills_read",
|
|
589
180
|
{
|
|
590
|
-
description: "
|
|
591
|
-
inputSchema: {
|
|
181
|
+
description: "Read one authenticated runtime skill by slug.",
|
|
182
|
+
inputSchema: { slug: z.string().min(1) },
|
|
592
183
|
},
|
|
593
|
-
async ({
|
|
184
|
+
async ({ slug }) => callApi("GET", `/api/agent/skills/${encodeURIComponent(slug)}`),
|
|
594
185
|
);
|
|
595
186
|
|
|
596
187
|
server.registerTool(
|
|
597
|
-
"
|
|
188
|
+
"pref_list",
|
|
598
189
|
{
|
|
599
|
-
description: "
|
|
600
|
-
inputSchema: {
|
|
190
|
+
description: "List standing agent preferences.",
|
|
191
|
+
inputSchema: { status: z.enum(["active", "proposed", "retired", "all"]).optional() },
|
|
601
192
|
},
|
|
602
|
-
async ({
|
|
603
|
-
);
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
"workflow_update_execution_status",
|
|
607
|
-
{
|
|
608
|
-
description: "Update the canonical post-IC execution status. Available only after the formal IC decision.",
|
|
609
|
-
inputSchema: { dealId: z.string(), executionStatus: z.enum(["Term Sheet", "Verbal Commit", "Invested"]), reason: z.string().min(1) },
|
|
193
|
+
async ({ status } = {}) => {
|
|
194
|
+
const params = new URLSearchParams();
|
|
195
|
+
if (status) params.set("status", status);
|
|
196
|
+
return callApi("GET", `/api/agent/preferences${params.size ? `?${params}` : ""}`);
|
|
610
197
|
},
|
|
611
|
-
async ({ dealId, executionStatus, reason }) => callWorkflow(dealId, "update_execution_status", { executionStatus, reason }),
|
|
612
198
|
);
|
|
613
199
|
|
|
614
|
-
// ============================================================
|
|
615
|
-
// Deal facts (research substrate + trust ladder)
|
|
616
|
-
// ============================================================
|
|
617
|
-
|
|
618
200
|
server.registerTool(
|
|
619
|
-
"
|
|
201
|
+
"pref_add",
|
|
620
202
|
{
|
|
621
|
-
description:
|
|
622
|
-
"Preferred write tool when one source yields multiple facts, or facts plus a Feed note. " +
|
|
623
|
-
"Commits the packet atomically, canonicalizes fact categories, skips exact source-aware " +
|
|
624
|
-
"duplicates, and is safe to retry with the same idempotencyKey. Use deal_fact_add only " +
|
|
625
|
-
"for a genuinely single fact. Canonical categories: company_basics, team, product, market, " +
|
|
626
|
-
"financials, fundraise, risk, milestone, meta.",
|
|
203
|
+
description: "Save a durable user or team preference; procedures belong in skills.",
|
|
627
204
|
inputSchema: {
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
.describe("Stable key for retries. If omitted, the server derives one from packet content."),
|
|
633
|
-
source: z
|
|
634
|
-
.object({
|
|
635
|
-
kind: z.enum(["deck", "web", "meeting_note", "email", "human", "agent_inference"]).optional(),
|
|
636
|
-
title: z.string().optional(),
|
|
637
|
-
url: z.string().optional(),
|
|
638
|
-
contentHash: z.string().optional(),
|
|
639
|
-
})
|
|
640
|
-
.optional(),
|
|
641
|
-
facts: z
|
|
642
|
-
.array(z.object({
|
|
643
|
-
category: z.string(),
|
|
644
|
-
claim: z.string(),
|
|
645
|
-
source: z.string().optional(),
|
|
646
|
-
sourceUrl: z.string().optional(),
|
|
647
|
-
sourceKind: z.enum(["deck", "web", "meeting_note", "email", "human", "agent_inference"]).optional(),
|
|
648
|
-
confidence: z.enum(["high", "medium", "low"]).optional(),
|
|
649
|
-
attested: z.boolean().optional(),
|
|
650
|
-
}))
|
|
651
|
-
.max(50)
|
|
652
|
-
.optional(),
|
|
653
|
-
note: z.string().optional().describe("Opinion, impression, or context to add to the deal Feed."),
|
|
205
|
+
key: z.string().min(1),
|
|
206
|
+
content: z.string().min(1).max(280),
|
|
207
|
+
scope: z.enum(["user", "team"]).optional(),
|
|
208
|
+
evidence: z.string().optional(),
|
|
654
209
|
},
|
|
655
210
|
},
|
|
656
|
-
async ({
|
|
657
|
-
|
|
658
|
-
return callApi("POST", `/api/deals/${encodeURIComponent(dealId)}/ingest`, packet);
|
|
659
|
-
}
|
|
211
|
+
async ({ key, content, scope, evidence }) =>
|
|
212
|
+
callApi("POST", "/api/agent/preferences", { key, content, scope, evidence }),
|
|
660
213
|
);
|
|
661
214
|
|
|
662
215
|
server.registerTool(
|
|
663
|
-
"
|
|
216
|
+
"pref_set_status",
|
|
664
217
|
{
|
|
665
|
-
description:
|
|
666
|
-
|
|
667
|
-
"category, a claim, a source/sourceUrl, a confidence, and a trust rung (unverified → " +
|
|
668
|
-
"agent-verified → human-vouched → endorsed) plus who/what recorded it.",
|
|
669
|
-
inputSchema: {
|
|
670
|
-
dealId: z.string(),
|
|
671
|
-
},
|
|
218
|
+
description: "Activate or retire a standing preference.",
|
|
219
|
+
inputSchema: { id: z.number().int().positive(), status: z.enum(["active", "retired"]) },
|
|
672
220
|
},
|
|
673
|
-
async ({
|
|
674
|
-
callApi("GET", `/api/deals/${encodeURIComponent(dealId)}/facts`)
|
|
221
|
+
async ({ id, status }) => callApi("PATCH", `/api/agent/preferences/${id}`, { status }),
|
|
675
222
|
);
|
|
676
223
|
|
|
677
224
|
server.registerTool(
|
|
678
|
-
"
|
|
225
|
+
"object_inspect",
|
|
679
226
|
{
|
|
680
|
-
description:
|
|
681
|
-
"Record a factual claim about a deal. RESPONSIBILITY: set `attested` honestly — " +
|
|
682
|
-
"true ONLY if you actually verified the claim against its cited source (the fact " +
|
|
683
|
-
"is then stored at trust level 'agent-verified'); false/omitted if you are relaying " +
|
|
684
|
-
"something unconfirmed (stored 'unverified', which is the honest default). You CANNOT " +
|
|
685
|
-
"mark a fact as human-confirmed — only a person can raise it to 'human-vouched'. " +
|
|
686
|
-
"`confidence` is how certain the claim is; `attested` is whether YOU take responsibility " +
|
|
687
|
-
"for having checked it. `source` is a human-readable provenance label; `sourceUrl` is a " +
|
|
688
|
-
"canonical URL that round-trips as sourceUrl/source_url. Category is free text; common " +
|
|
689
|
-
"values include founders | financials | product | market | team | company_basics | risk | fundraise | milestone | meta.",
|
|
227
|
+
description: "Explain a Command URL or object before guessing that it is broken.",
|
|
690
228
|
inputSchema: {
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
sourceUrl: z.string().optional().describe("canonical URL for the evidence, if separate from source"),
|
|
696
|
-
confidence: z.enum(["high", "medium", "low"]).optional(),
|
|
697
|
-
attested: z
|
|
698
|
-
.boolean()
|
|
699
|
-
.optional()
|
|
700
|
-
.describe("true → stored 'agent-verified'; false/omitted → 'unverified'. Answer honestly."),
|
|
229
|
+
q: z.string().optional(),
|
|
230
|
+
type: z.string().optional(),
|
|
231
|
+
id: z.string().optional(),
|
|
232
|
+
lang: z.enum(["en", "zh"]).optional(),
|
|
701
233
|
},
|
|
702
234
|
},
|
|
703
|
-
async ({
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
attested: attested === true,
|
|
711
|
-
})
|
|
712
|
-
);
|
|
713
|
-
|
|
714
|
-
server.registerTool(
|
|
715
|
-
"deal_fact_verify",
|
|
716
|
-
{
|
|
717
|
-
description:
|
|
718
|
-
"Verify a recorded fact. status='confirmed' vouches for it (raises to 'human-vouched'); " +
|
|
719
|
-
"status='disputed' marks it contradicted. Trust-ladder guardrails apply server-side " +
|
|
720
|
-
"(external-org callers are capped at 'unverified'; only Partners reach 'endorsed'). " +
|
|
721
|
-
"Optionally pass correctedValue when disputing.",
|
|
722
|
-
inputSchema: {
|
|
723
|
-
dealId: z.string(),
|
|
724
|
-
factId: z.union([z.string(), z.number()]),
|
|
725
|
-
status: z.enum(["confirmed", "disputed"]),
|
|
726
|
-
correctedValue: z.string().optional(),
|
|
727
|
-
},
|
|
235
|
+
async ({ q, type, id, lang } = {}) => {
|
|
236
|
+
const params = new URLSearchParams();
|
|
237
|
+
if (q) params.set("q", q);
|
|
238
|
+
if (type) params.set("type", type);
|
|
239
|
+
if (id) params.set("id", id);
|
|
240
|
+
if (lang) params.set("lang", lang);
|
|
241
|
+
return callApi("GET", `/api/agent/explain?${params}`);
|
|
728
242
|
},
|
|
729
|
-
async ({ dealId, factId, status, correctedValue }) =>
|
|
730
|
-
callApi(
|
|
731
|
-
"PATCH",
|
|
732
|
-
`/api/deals/${encodeURIComponent(dealId)}/facts/${encodeURIComponent(String(factId))}`,
|
|
733
|
-
{ status, ...(correctedValue !== undefined ? { correctedValue } : {}) }
|
|
734
|
-
)
|
|
735
243
|
);
|
|
736
244
|
|
|
737
|
-
// ============================================================
|
|
738
|
-
// Brief blocks
|
|
739
|
-
// ============================================================
|
|
740
|
-
|
|
741
245
|
server.registerTool(
|
|
742
|
-
"
|
|
246
|
+
"wiki_search",
|
|
743
247
|
{
|
|
744
|
-
description:
|
|
745
|
-
|
|
746
|
-
"and ordered. Each has stable id, optional meta (locked, by_agent, sourceSection).",
|
|
747
|
-
inputSchema: {
|
|
748
|
-
dealId: z.string(),
|
|
749
|
-
},
|
|
248
|
+
description: "Search institutional knowledge in the internal Wiki.",
|
|
249
|
+
inputSchema: { q: z.string().min(1) },
|
|
750
250
|
},
|
|
751
|
-
async ({
|
|
752
|
-
callApi("GET", `/api/deals/${encodeURIComponent(dealId)}/blocks`)
|
|
251
|
+
async ({ q }) => callApi("GET", `/api/wiki/search?q=${encodeURIComponent(q)}`),
|
|
753
252
|
);
|
|
754
253
|
|
|
755
254
|
server.registerTool(
|
|
756
|
-
"
|
|
255
|
+
"wiki_read",
|
|
757
256
|
{
|
|
758
|
-
description:
|
|
759
|
-
|
|
760
|
-
"facts + notes/discussion + legacy posts, merged at query time, newest first. " +
|
|
761
|
-
"Shows contributions from ANYONE — a teammate, their AI assistant, or an " +
|
|
762
|
-
"autonomous system agent; nothing is hidden. Each item carries `who` (the " +
|
|
763
|
-
"accountable person, null only for principal-less system writes) and `agent` " +
|
|
764
|
-
"(the assistant/system label when an AI did the writing, null when a human " +
|
|
765
|
-
"typed it) so you can tell human-typed from assistant-drafted. The AI's " +
|
|
766
|
-
"regenerable brief synthesis is NOT here (that's the Memo) — only " +
|
|
767
|
-
"facts + discussion notes. Each item: kind (fact|note), ts, who, agent, " +
|
|
768
|
-
"origin, text, and for facts: source + trust rung + category.",
|
|
769
|
-
inputSchema: {
|
|
770
|
-
dealId: z.string(),
|
|
771
|
-
},
|
|
257
|
+
description: "Read one Wiki article by exact slug.",
|
|
258
|
+
inputSchema: { slug: z.string().min(1), lang: z.enum(["en", "zh"]).optional() },
|
|
772
259
|
},
|
|
773
|
-
async ({
|
|
774
|
-
callApi("GET", `/api/
|
|
260
|
+
async ({ slug, lang }) =>
|
|
261
|
+
callApi("GET", `/api/wiki/${encodeURIComponent(slug)}?lang=${lang === "zh" ? "zh" : "en"}`),
|
|
775
262
|
);
|
|
776
263
|
|
|
777
264
|
server.registerTool(
|
|
778
|
-
"
|
|
265
|
+
"wiki_save",
|
|
779
266
|
{
|
|
780
|
-
description:
|
|
781
|
-
"Prepend a markdown text block to a deal brief. Supports markdown + mermaid diagrams.",
|
|
267
|
+
description: "Create or update cross-Deal institutional knowledge, not Deal data.",
|
|
782
268
|
inputSchema: {
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
),
|
|
269
|
+
slug: z.string().min(1),
|
|
270
|
+
title: z.string().min(1),
|
|
271
|
+
content: z.string(),
|
|
272
|
+
sources: z.array(z.string()).min(1),
|
|
273
|
+
content_type: z.enum(["markdown", "html"]).optional(),
|
|
789
274
|
},
|
|
790
275
|
},
|
|
791
|
-
async ({
|
|
792
|
-
|
|
276
|
+
async ({ slug, title, content, sources, content_type }) =>
|
|
277
|
+
callApi("POST", "/api/wiki/save", { slug, title, content, sources, content_type }),
|
|
793
278
|
);
|
|
794
279
|
|
|
795
280
|
server.registerTool(
|
|
796
|
-
"
|
|
281
|
+
"wiki_delete",
|
|
797
282
|
{
|
|
798
|
-
description:
|
|
799
|
-
|
|
800
|
-
inputSchema: {
|
|
801
|
-
dealId: z.string(),
|
|
802
|
-
url: z.string(),
|
|
803
|
-
label: z.string().optional().describe("optional human-readable label"),
|
|
804
|
-
cueAuthorized: z.boolean().optional().describe(
|
|
805
|
-
"true only after the user explicitly approved every resolved cue recipient",
|
|
806
|
-
),
|
|
807
|
-
},
|
|
283
|
+
description: "Soft-delete a Wiki article.",
|
|
284
|
+
inputSchema: { slug: z.string().min(1), lang: z.enum(["en", "zh"]).optional() },
|
|
808
285
|
},
|
|
809
|
-
async ({
|
|
810
|
-
|
|
286
|
+
async ({ slug, lang }) =>
|
|
287
|
+
callApi("DELETE", `/api/wiki/${encodeURIComponent(slug)}?lang=${lang === "zh" ? "zh" : "en"}`),
|
|
811
288
|
);
|
|
812
289
|
|
|
813
290
|
server.registerTool(
|
|
814
|
-
"
|
|
291
|
+
"wiki_restore",
|
|
815
292
|
{
|
|
816
|
-
description:
|
|
817
|
-
|
|
818
|
-
inputSchema: {
|
|
819
|
-
dealId: z.string(),
|
|
820
|
-
tone: z.string().describe("insight | warning | info | success"),
|
|
821
|
-
heading: z.string().optional(),
|
|
822
|
-
body: z.string(),
|
|
823
|
-
cueAuthorized: z.boolean().optional().describe(
|
|
824
|
-
"true only after the user explicitly approved every resolved cue recipient",
|
|
825
|
-
),
|
|
826
|
-
},
|
|
293
|
+
description: "Restore a soft-deleted Wiki article.",
|
|
294
|
+
inputSchema: { slug: z.string().min(1), lang: z.enum(["en", "zh"]).optional() },
|
|
827
295
|
},
|
|
828
|
-
async ({
|
|
829
|
-
|
|
296
|
+
async ({ slug, lang }) =>
|
|
297
|
+
callApi("POST", `/api/wiki/${encodeURIComponent(slug)}/restore?lang=${lang === "zh" ? "zh" : "en"}`),
|
|
830
298
|
);
|
|
831
299
|
|
|
832
300
|
server.registerTool(
|
|
833
|
-
"
|
|
301
|
+
"pitch_start",
|
|
834
302
|
{
|
|
835
|
-
description:
|
|
836
|
-
|
|
837
|
-
"(heading/body/url/label/description/tone). Meta toggles: locked (protect from bulk " +
|
|
838
|
-
"overwrite), hidden (fold), sourceSection (route watcher writes). Snapshots the prior " +
|
|
839
|
-
"version to history (reversible via brief_restore_version).",
|
|
840
|
-
inputSchema: {
|
|
841
|
-
dealId: z.string(),
|
|
842
|
-
blockId: z.string(),
|
|
843
|
-
heading: z.string().optional(),
|
|
844
|
-
body: z.string().optional(),
|
|
845
|
-
url: z.string().optional(),
|
|
846
|
-
label: z.string().optional(),
|
|
847
|
-
description: z.string().optional(),
|
|
848
|
-
tone: z.string().optional(),
|
|
849
|
-
locked: z.boolean().optional(),
|
|
850
|
-
hidden: z.boolean().optional(),
|
|
851
|
-
sourceSection: z.string().optional(),
|
|
852
|
-
cueAuthorized: z.boolean().optional().describe(
|
|
853
|
-
"true only after the user explicitly approved every resolved cue recipient",
|
|
854
|
-
),
|
|
855
|
-
},
|
|
303
|
+
description: "Start an external founder pitch session; no internal token required.",
|
|
304
|
+
inputSchema: { name: z.string().min(1).max(100), email: z.string().email() },
|
|
856
305
|
},
|
|
857
|
-
async ({
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
306
|
+
async ({ name, email }) => {
|
|
307
|
+
try {
|
|
308
|
+
return textResult(JSON.stringify(await startExternalSession({ name, email }), null, 2));
|
|
309
|
+
} catch (error) {
|
|
310
|
+
return textResult(`Error: ${error?.message ?? String(error)}`, true);
|
|
861
311
|
}
|
|
862
|
-
const meta = {};
|
|
863
|
-
if (locked !== undefined) meta.locked = locked;
|
|
864
|
-
if (hidden !== undefined) meta.hidden = hidden;
|
|
865
|
-
if (sourceSection !== undefined) meta.sourceSection = sourceSection;
|
|
866
|
-
if (Object.keys(meta).length > 0) patch.meta = meta;
|
|
867
|
-
if (cueAuthorized === true) patch.cue_authorized = true;
|
|
868
|
-
return callApi(
|
|
869
|
-
"PATCH",
|
|
870
|
-
`/api/deals/${encodeURIComponent(dealId)}/blocks/${encodeURIComponent(blockId)}`,
|
|
871
|
-
patch
|
|
872
|
-
);
|
|
873
|
-
}
|
|
874
|
-
);
|
|
875
|
-
|
|
876
|
-
server.registerTool(
|
|
877
|
-
"brief_delete",
|
|
878
|
-
{
|
|
879
|
-
description:
|
|
880
|
-
"Soft-delete a brief block (reversible via brief_restore). Locked blocks are refused.",
|
|
881
|
-
inputSchema: { dealId: z.string(), blockId: z.string() },
|
|
882
312
|
},
|
|
883
|
-
async ({ dealId, blockId }) =>
|
|
884
|
-
callApi("DELETE", `/api/deals/${encodeURIComponent(dealId)}/blocks/${encodeURIComponent(blockId)}`)
|
|
885
313
|
);
|
|
886
314
|
|
|
887
315
|
server.registerTool(
|
|
888
|
-
"
|
|
316
|
+
"pitch_send_message",
|
|
889
317
|
{
|
|
890
|
-
description: "
|
|
891
|
-
inputSchema: {
|
|
318
|
+
description: "Relay the founder's exact message to the external intake agent.",
|
|
319
|
+
inputSchema: { message: z.string().min(1).max(8000) },
|
|
320
|
+
},
|
|
321
|
+
async ({ message }) => {
|
|
322
|
+
try {
|
|
323
|
+
return textResult(JSON.stringify(await sendExternalMessage(message), null, 2));
|
|
324
|
+
} catch (error) {
|
|
325
|
+
return textResult(`Error: ${error?.message ?? String(error)}`, true);
|
|
326
|
+
}
|
|
892
327
|
},
|
|
893
|
-
async ({ dealId, blockId }) =>
|
|
894
|
-
callApi("POST", `/api/deals/${encodeURIComponent(dealId)}/blocks/${encodeURIComponent(blockId)}/restore`)
|
|
895
328
|
);
|
|
896
329
|
|
|
897
330
|
server.registerTool(
|
|
898
|
-
"
|
|
331
|
+
"pitch_upload_file",
|
|
899
332
|
{
|
|
900
|
-
description:
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
333
|
+
description: "Attach a local file to the active external pitch session.",
|
|
334
|
+
inputSchema: { path: z.string().min(1) },
|
|
335
|
+
},
|
|
336
|
+
async ({ path }) => {
|
|
337
|
+
try {
|
|
338
|
+
return textResult(JSON.stringify(await uploadExternalFile(path), null, 2));
|
|
339
|
+
} catch (error) {
|
|
340
|
+
return textResult(`Error: ${error?.message ?? String(error)}`, true);
|
|
341
|
+
}
|
|
908
342
|
},
|
|
909
|
-
async ({ dealId, blockId, limit }) => {
|
|
910
|
-
const qs = limit ? `?limit=${encodeURIComponent(String(limit))}` : "";
|
|
911
|
-
return callApi(
|
|
912
|
-
"GET",
|
|
913
|
-
`/api/deals/${encodeURIComponent(dealId)}/blocks/${encodeURIComponent(blockId)}/history${qs}`
|
|
914
|
-
);
|
|
915
|
-
}
|
|
916
343
|
);
|
|
917
344
|
|
|
918
345
|
server.registerTool(
|
|
919
|
-
"
|
|
920
|
-
{
|
|
921
|
-
|
|
922
|
-
"Restore a brief block to a specific historical version (find historyId via brief_history). " +
|
|
923
|
-
"Itself reversible — the outgoing version is snapshotted before replacement.",
|
|
924
|
-
inputSchema: {
|
|
925
|
-
dealId: z.string(),
|
|
926
|
-
blockId: z.string(),
|
|
927
|
-
historyId: z.number(),
|
|
928
|
-
},
|
|
929
|
-
},
|
|
930
|
-
async ({ dealId, blockId, historyId }) =>
|
|
931
|
-
callApi(
|
|
932
|
-
"POST",
|
|
933
|
-
`/api/deals/${encodeURIComponent(dealId)}/blocks/${encodeURIComponent(blockId)}/history`,
|
|
934
|
-
{ history_id: historyId }
|
|
935
|
-
)
|
|
346
|
+
"pitch_status",
|
|
347
|
+
{ description: "Inspect the active external pitch session.", inputSchema: {} },
|
|
348
|
+
async () => textResult(JSON.stringify(getExternalSessionStatus(), null, 2)),
|
|
936
349
|
);
|
|
937
350
|
|
|
938
|
-
// ============================================================
|
|
939
|
-
// Wiki (knowledge base)
|
|
940
|
-
// ============================================================
|
|
941
|
-
|
|
942
351
|
server.registerTool(
|
|
943
|
-
"
|
|
944
|
-
{
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
inputSchema: {
|
|
950
|
-
q: z.string().describe("search query"),
|
|
951
|
-
},
|
|
352
|
+
"pitch_finalize",
|
|
353
|
+
{ description: "Clear local pitch session state after completion or abandonment.", inputSchema: {} },
|
|
354
|
+
async () => {
|
|
355
|
+
const before = getExternalSessionStatus();
|
|
356
|
+
clearExternalSession();
|
|
357
|
+
return textResult(JSON.stringify({ cleared: before.active, previous_session: before }, null, 2));
|
|
952
358
|
},
|
|
953
|
-
async ({ q }) => callApi("GET", `/api/wiki/search?q=${encodeURIComponent(q)}`)
|
|
954
359
|
);
|
|
955
360
|
|
|
956
|
-
server.
|
|
957
|
-
"
|
|
361
|
+
server.registerPrompt(
|
|
362
|
+
"agent_briefing",
|
|
958
363
|
{
|
|
959
|
-
description:
|
|
960
|
-
"Read a single wiki article from the configured Llama Command " +
|
|
961
|
-
"deployment by exact slug. Returns title, frontmatter, full " +
|
|
962
|
-
"markdown content, and rendered HTML.\n\n" +
|
|
963
|
-
"USE THIS — DO NOT WebFetch — whenever the user gives you a " +
|
|
964
|
-
"wiki URL whose path is `/wiki/<slug>`. Extract the slug from " +
|
|
965
|
-
"the URL path and call this tool with it. WebFetch against the " +
|
|
966
|
-
"browser URL goes through session-cookie auth — your agent " +
|
|
967
|
-
"doesn't have one — so it will look like a permission denial " +
|
|
968
|
-
"even though your token is fine.\n\n" +
|
|
969
|
-
"If you only have a topic name, use `wiki_search` first to " +
|
|
970
|
-
"find the slug.",
|
|
971
|
-
inputSchema: {
|
|
972
|
-
slug: z
|
|
973
|
-
.string()
|
|
974
|
-
.describe(
|
|
975
|
-
"exact kebab-case slug — the last path segment of the wiki URL"
|
|
976
|
-
),
|
|
977
|
-
lang: z
|
|
978
|
-
.enum(["en", "zh"])
|
|
979
|
-
.optional()
|
|
980
|
-
.describe("article language (default 'en')"),
|
|
981
|
-
},
|
|
982
|
-
},
|
|
983
|
-
async ({ slug, lang }) =>
|
|
984
|
-
callApi(
|
|
985
|
-
"GET",
|
|
986
|
-
`/api/wiki/${encodeURIComponent(slug)}?lang=${lang === "zh" ? "zh" : "en"}`
|
|
987
|
-
)
|
|
988
|
-
);
|
|
989
|
-
|
|
990
|
-
server.registerTool(
|
|
991
|
-
"wiki_save",
|
|
992
|
-
{
|
|
993
|
-
description:
|
|
994
|
-
"Create or update a wiki page — Llama's CROSS-DEAL / institutional " +
|
|
995
|
-
"knowledge surface (sector landscape · market map · thesis · framework · " +
|
|
996
|
-
"methodology · anything not tied to ONE specific deal). Renders at " +
|
|
997
|
-
"/wiki/<slug>. " +
|
|
998
|
-
"**Routing — decide BEFORE calling:** " +
|
|
999
|
-
"(a) Deal-specific HTML (IC memo for X, dashboard for X) → use " +
|
|
1000
|
-
"`html_upload` instead, NOT this. " +
|
|
1001
|
-
"(b) Cross-deal / institutional (this tool) → /wiki/<slug>. " +
|
|
1002
|
-
"(c) Founder-facing public share → Netlify only when user explicitly " +
|
|
1003
|
-
"says so; Llama Command outranks Netlify for everything internal. " +
|
|
1004
|
-
"By default `content` is markdown with attribution blocks " +
|
|
1005
|
-
"(**[Name · YYYY-MM-DD · source · fact|opinion]**) for traceability. " +
|
|
1006
|
-
"Set `content_type: 'html'` to deploy a standalone HTML page as the " +
|
|
1007
|
-
"wiki entry (full-viewport sandboxed iframe takeover on /wiki/<slug>; " +
|
|
1008
|
-
"the HTML itself is the page — no wiki chrome). `sources` is a " +
|
|
1009
|
-
"separate citation list (URLs, doc names, or meeting references) — " +
|
|
1010
|
-
"at least one required; URLs inside `content` do not count. For HTML " +
|
|
1011
|
-
"asset bundles use the `llama wiki save --file ... --assets ...` CLI " +
|
|
1012
|
-
"path; MCP only supports single-file HTML.",
|
|
1013
|
-
inputSchema: {
|
|
1014
|
-
slug: z.string().describe("kebab-case slug"),
|
|
1015
|
-
title: z.string(),
|
|
1016
|
-
content: z
|
|
1017
|
-
.string()
|
|
1018
|
-
.describe(
|
|
1019
|
-
"body — markdown source by default, or raw HTML when content_type='html'"
|
|
1020
|
-
),
|
|
1021
|
-
sources: z
|
|
1022
|
-
.array(z.string())
|
|
1023
|
-
.min(1)
|
|
1024
|
-
.describe(
|
|
1025
|
-
"citation list — URLs, doc names, or meeting references. At least one required."
|
|
1026
|
-
),
|
|
1027
|
-
content_type: z
|
|
1028
|
-
.enum(["markdown", "html"])
|
|
1029
|
-
.optional()
|
|
1030
|
-
.describe(
|
|
1031
|
-
"'markdown' (default) renders via the wiki markdown pipeline. " +
|
|
1032
|
-
"'html' stores the body as a standalone HTML page (sandboxed iframe)."
|
|
1033
|
-
),
|
|
1034
|
-
},
|
|
1035
|
-
},
|
|
1036
|
-
async ({ slug, title, content, sources, content_type }) =>
|
|
1037
|
-
callApi("POST", "/api/wiki/save", {
|
|
1038
|
-
slug,
|
|
1039
|
-
title,
|
|
1040
|
-
content,
|
|
1041
|
-
sources,
|
|
1042
|
-
...(content_type ? { content_type } : {}),
|
|
1043
|
-
})
|
|
1044
|
-
);
|
|
1045
|
-
|
|
1046
|
-
server.registerTool(
|
|
1047
|
-
"wiki_save_file",
|
|
1048
|
-
{
|
|
1049
|
-
description:
|
|
1050
|
-
"Publish a PDF / DOCX / XLSX from a LOCAL FILE PATH as the wiki entry " +
|
|
1051
|
-
"itself. Readers open the document at /wiki/<slug>: a PDF in the " +
|
|
1052
|
-
"browser's own viewer with page navigation, a DOCX or XLSX converted " +
|
|
1053
|
-
"for reading (a spreadsheet keeps one tab per sheet), and the original " +
|
|
1054
|
-
"always downloadable. Use this whenever someone hands you a document " +
|
|
1055
|
-
"and wants it ON the wiki — do NOT transcribe it into markdown for " +
|
|
1056
|
-
"wiki_save, and do NOT write an article describing a file nobody can " +
|
|
1057
|
-
"open. Reads filePath on the machine running this MCP server, so the " +
|
|
1058
|
-
"bytes never pass through tool-call context. Deal-specific documents " +
|
|
1059
|
-
"belong on the deal page instead (html_upload_file).",
|
|
1060
|
-
inputSchema: {
|
|
1061
|
-
slug: z.string().describe("kebab-case slug"),
|
|
1062
|
-
title: z.string(),
|
|
1063
|
-
filePath: z
|
|
1064
|
-
.string()
|
|
1065
|
-
.describe("absolute or relative local path to a .pdf / .docx / .xlsx"),
|
|
1066
|
-
sources: z
|
|
1067
|
-
.array(z.string())
|
|
1068
|
-
.min(1)
|
|
1069
|
-
.describe(
|
|
1070
|
-
"citation list — URLs, doc names, or meeting references. At least one required."
|
|
1071
|
-
),
|
|
1072
|
-
type: z.string().optional().describe("optional category tag, e.g. 'company'"),
|
|
1073
|
-
doc_kind: z
|
|
1074
|
-
.string()
|
|
1075
|
-
.optional()
|
|
1076
|
-
.describe("optional — how the wiki home browses and search filters"),
|
|
1077
|
-
lang: z.enum(["en", "zh"]).optional().describe("default: en"),
|
|
1078
|
-
},
|
|
1079
|
-
},
|
|
1080
|
-
async ({ slug, title, filePath, sources, type, doc_kind, lang }) => {
|
|
1081
|
-
const { readFileSync } = await import("node:fs");
|
|
1082
|
-
const { basename } = await import("node:path");
|
|
1083
|
-
const ext = String(filePath).toLowerCase().match(/\.(pdf|docx|xlsx)$/)?.[1];
|
|
1084
|
-
if (!ext) {
|
|
1085
|
-
return textResult(
|
|
1086
|
-
`Error: wiki_save_file takes a .pdf, .docx, or .xlsx. For markdown or a ` +
|
|
1087
|
-
`standalone HTML page, use wiki_save.`,
|
|
1088
|
-
true,
|
|
1089
|
-
);
|
|
1090
|
-
}
|
|
1091
|
-
let buf;
|
|
1092
|
-
try {
|
|
1093
|
-
buf = readFileSync(String(filePath));
|
|
1094
|
-
} catch (err) {
|
|
1095
|
-
return textResult(`Error reading ${filePath}: ${err?.message ?? String(err)}`, true);
|
|
1096
|
-
}
|
|
1097
|
-
const MAX = 50 * 1024 * 1024;
|
|
1098
|
-
if (buf.length > MAX) {
|
|
1099
|
-
return textResult(
|
|
1100
|
-
`Error: ${basename(String(filePath))} is ${(buf.length / 1024 / 1024).toFixed(1)} MB; ` +
|
|
1101
|
-
`the wiki caps files at 50 MB. Link to the Drive copy instead.`,
|
|
1102
|
-
true,
|
|
1103
|
-
);
|
|
1104
|
-
}
|
|
1105
|
-
const mime = {
|
|
1106
|
-
pdf: "application/pdf",
|
|
1107
|
-
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
1108
|
-
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
1109
|
-
}[ext];
|
|
1110
|
-
const form = new FormData();
|
|
1111
|
-
form.append("file", new Blob([buf], { type: mime }), basename(String(filePath)));
|
|
1112
|
-
form.append("title", String(title));
|
|
1113
|
-
form.append("sources", sources.join(";"));
|
|
1114
|
-
form.append("lang", lang === "zh" ? "zh" : "en");
|
|
1115
|
-
if (type) form.append("type", String(type));
|
|
1116
|
-
if (doc_kind) form.append("doc_kind", String(doc_kind));
|
|
1117
|
-
|
|
1118
|
-
const headers = await getAuthHeaders();
|
|
1119
|
-
// @core-api-operation POST /api/wiki/{slug}/file
|
|
1120
|
-
const res = await fetch(
|
|
1121
|
-
`${getBaseUrl()}/api/wiki/${encodeURIComponent(slug)}/file`,
|
|
1122
|
-
{ method: "POST", headers, body: form }, // let fetch set the multipart boundary
|
|
1123
|
-
);
|
|
1124
|
-
const body = await res.json().catch(() => ({}));
|
|
1125
|
-
if (!res.ok) {
|
|
1126
|
-
return textResult(
|
|
1127
|
-
`HTTP ${res.status}: ${body?.error || JSON.stringify(body).slice(0, 300)}`,
|
|
1128
|
-
true,
|
|
1129
|
-
);
|
|
1130
|
-
}
|
|
1131
|
-
return textResult(JSON.stringify(body, null, 2));
|
|
1132
|
-
},
|
|
1133
|
-
);
|
|
1134
|
-
|
|
1135
|
-
server.registerTool(
|
|
1136
|
-
"wiki_delete",
|
|
1137
|
-
{
|
|
1138
|
-
description:
|
|
1139
|
-
"Soft-delete a wiki page (reversible). The entry stops appearing in " +
|
|
1140
|
-
"reads / search / backlinks; for HTML entries the standalone page + " +
|
|
1141
|
-
"assets stop resolving too. Restore with wiki_restore. Use when the " +
|
|
1142
|
-
"user asks to remove / delete / retire a wiki entry.",
|
|
1143
|
-
inputSchema: {
|
|
1144
|
-
slug: z.string().describe("kebab-case slug"),
|
|
1145
|
-
lang: z.enum(["en", "zh"]).optional().describe("default: en"),
|
|
1146
|
-
},
|
|
1147
|
-
},
|
|
1148
|
-
async ({ slug, lang }) =>
|
|
1149
|
-
callApi(
|
|
1150
|
-
"DELETE",
|
|
1151
|
-
`/api/wiki/${encodeURIComponent(slug)}?lang=${lang === "zh" ? "zh" : "en"}`
|
|
1152
|
-
)
|
|
1153
|
-
);
|
|
1154
|
-
|
|
1155
|
-
server.registerTool(
|
|
1156
|
-
"wiki_restore",
|
|
1157
|
-
{
|
|
1158
|
-
description:
|
|
1159
|
-
"Restore a soft-deleted wiki page (undo wiki_delete). Brings back the " +
|
|
1160
|
-
"entry + (for HTML entries) its standalone page and assets.",
|
|
1161
|
-
inputSchema: {
|
|
1162
|
-
slug: z.string().describe("kebab-case slug"),
|
|
1163
|
-
lang: z.enum(["en", "zh"]).optional().describe("default: en"),
|
|
1164
|
-
},
|
|
1165
|
-
},
|
|
1166
|
-
async ({ slug, lang }) =>
|
|
1167
|
-
callApi(
|
|
1168
|
-
"POST",
|
|
1169
|
-
`/api/wiki/${encodeURIComponent(slug)}/restore?lang=${lang === "zh" ? "zh" : "en"}`
|
|
1170
|
-
)
|
|
1171
|
-
);
|
|
1172
|
-
|
|
1173
|
-
// ============================================================
|
|
1174
|
-
// Timeline + posts
|
|
1175
|
-
// ============================================================
|
|
1176
|
-
|
|
1177
|
-
server.registerTool(
|
|
1178
|
-
"timeline",
|
|
1179
|
-
{
|
|
1180
|
-
description:
|
|
1181
|
-
"Get the activity timeline for a deal — field changes, owner approvals, " +
|
|
1182
|
-
"brief edits, posts, watcher events. Append-only audit log.",
|
|
1183
|
-
inputSchema: {
|
|
1184
|
-
dealId: z.string(),
|
|
1185
|
-
},
|
|
1186
|
-
},
|
|
1187
|
-
async ({ dealId }) =>
|
|
1188
|
-
callApi("GET", `/api/deals/${encodeURIComponent(dealId)}/timeline`)
|
|
1189
|
-
);
|
|
1190
|
-
|
|
1191
|
-
server.registerTool(
|
|
1192
|
-
"post",
|
|
1193
|
-
{
|
|
1194
|
-
description:
|
|
1195
|
-
"Post a message to a deal's timeline. Cue-free posts are autonomous. " +
|
|
1196
|
-
"Explicit or implicit teammate cues create inbox/email notifications and require " +
|
|
1197
|
-
"cueAuthorized=true only after explicit user permission.",
|
|
1198
|
-
inputSchema: {
|
|
1199
|
-
dealId: z.string(),
|
|
1200
|
-
message: z.string(),
|
|
1201
|
-
cueAuthorized: z.boolean().optional().describe(
|
|
1202
|
-
"true only after the user explicitly approved cueing every resolved recipient",
|
|
1203
|
-
),
|
|
1204
|
-
},
|
|
1205
|
-
},
|
|
1206
|
-
async ({ dealId, message, cueAuthorized }) =>
|
|
1207
|
-
callApi("POST", `/api/deals/${encodeURIComponent(dealId)}/posts`, {
|
|
1208
|
-
message,
|
|
1209
|
-
cue_authorized: cueAuthorized === true,
|
|
1210
|
-
})
|
|
1211
|
-
);
|
|
1212
|
-
|
|
1213
|
-
// ============================================================
|
|
1214
|
-
// Mentions / inbox
|
|
1215
|
-
// ============================================================
|
|
1216
|
-
|
|
1217
|
-
server.registerTool(
|
|
1218
|
-
"mentions_list",
|
|
1219
|
-
{
|
|
1220
|
-
description:
|
|
1221
|
-
"List @-mentions. Default scope: unresolved mentions where the caller is the " +
|
|
1222
|
-
"recipient. Set everyone=true for team-wide visibility (mutual observability).",
|
|
1223
|
-
inputSchema: {
|
|
1224
|
-
everyone: z
|
|
1225
|
-
.boolean()
|
|
1226
|
-
.optional()
|
|
1227
|
-
.describe("if true, list all team mentions; otherwise just for the caller"),
|
|
1228
|
-
includeResolved: z
|
|
1229
|
-
.boolean()
|
|
1230
|
-
.optional()
|
|
1231
|
-
.describe("if true, also include already-resolved mentions"),
|
|
1232
|
-
},
|
|
1233
|
-
},
|
|
1234
|
-
async ({ everyone, includeResolved } = {}) => {
|
|
1235
|
-
const params = new URLSearchParams();
|
|
1236
|
-
if (everyone) params.set("everyone", "1");
|
|
1237
|
-
else params.set("for_me", "1");
|
|
1238
|
-
if (!includeResolved) params.set("unresolved", "1");
|
|
1239
|
-
return callApi("GET", `/api/mentions?${params}`);
|
|
1240
|
-
}
|
|
1241
|
-
);
|
|
1242
|
-
|
|
1243
|
-
server.registerTool(
|
|
1244
|
-
"mentions_resolve",
|
|
1245
|
-
{
|
|
1246
|
-
description: "Mark an @-mention as resolved (clears it from the recipient's open cues).",
|
|
1247
|
-
inputSchema: { mentionId: z.union([z.string(), z.number()]) },
|
|
1248
|
-
},
|
|
1249
|
-
async ({ mentionId }) =>
|
|
1250
|
-
callApi("POST", `/api/mentions/${encodeURIComponent(String(mentionId))}/resolve`)
|
|
1251
|
-
);
|
|
1252
|
-
|
|
1253
|
-
// ============================================================
|
|
1254
|
-
// Brief refresh (signal-driven re-evaluation)
|
|
1255
|
-
// ============================================================
|
|
1256
|
-
|
|
1257
|
-
server.registerTool(
|
|
1258
|
-
"deal_refresh_brief",
|
|
1259
|
-
{
|
|
1260
|
-
description:
|
|
1261
|
-
"Trigger a stale-section re-evaluation of a deal's brief. Pass force=true to bypass the " +
|
|
1262
|
-
"debounce. Returns a runId (or null if debounced / deal inactive).",
|
|
1263
|
-
inputSchema: {
|
|
1264
|
-
dealId: z.string(),
|
|
1265
|
-
force: z.boolean().optional(),
|
|
1266
|
-
},
|
|
1267
|
-
},
|
|
1268
|
-
async ({ dealId, force }) =>
|
|
1269
|
-
callApi(
|
|
1270
|
-
"POST",
|
|
1271
|
-
`/api/deals/${encodeURIComponent(dealId)}/refresh-brief${force ? "?force=1" : ""}`
|
|
1272
|
-
)
|
|
1273
|
-
);
|
|
1274
|
-
|
|
1275
|
-
server.registerTool(
|
|
1276
|
-
"deal_agent_run",
|
|
1277
|
-
{
|
|
1278
|
-
description:
|
|
1279
|
-
"Run Llama Command's server-side Deal Agent inside a deal thread. " +
|
|
1280
|
-
"Use this when the user explicitly wants the service agent to execute " +
|
|
1281
|
-
"a deal-scoped task instead of the local MCP client doing the work.",
|
|
1282
|
-
inputSchema: {
|
|
1283
|
-
dealId: z.string().describe("deal uuid"),
|
|
1284
|
-
message: z.string().describe("task instruction for the server-side Deal Agent"),
|
|
1285
|
-
title: z.string().optional().describe("optional thread title; defaults to MCP agent run"),
|
|
1286
|
-
},
|
|
1287
|
-
},
|
|
1288
|
-
async ({ dealId, message, title }) =>
|
|
1289
|
-
runDealAgentTool({ dealId, message, title: title || "MCP agent run" })
|
|
1290
|
-
);
|
|
1291
|
-
|
|
1292
|
-
server.registerTool(
|
|
1293
|
-
"deal_enrich",
|
|
1294
|
-
{
|
|
1295
|
-
description:
|
|
1296
|
-
"Run the Llama Command deal enrichment planner/trigger for one deal. " +
|
|
1297
|
-
"Default is dry-run: returns evidence plan, source plan, Monid budget/config " +
|
|
1298
|
-
"status, and planned writes without changing facts/links/memo. With " +
|
|
1299
|
-
"apply=true and executor=server_agent, this starts the server-side Deal " +
|
|
1300
|
-
"Agent unless harnessOnly=true. Set apply=true only when the user " +
|
|
1301
|
-
"explicitly wants the enrichment run recorded/applied. Memo generation is " +
|
|
1302
|
-
"not part of enrichment and is available only in Llama Command's Memo Agent.",
|
|
1303
|
-
inputSchema: {
|
|
1304
|
-
dealId: z.string().describe("deal uuid"),
|
|
1305
|
-
dryRun: z.boolean().optional().describe("default true unless apply=true"),
|
|
1306
|
-
apply: z.boolean().optional().describe("record/apply the enrichment intent server-side"),
|
|
1307
|
-
executor: z
|
|
1308
|
-
.enum(["server_agent", "external_agent", "planner"])
|
|
1309
|
-
.optional()
|
|
1310
|
-
.describe("who will execute the harness; server_agent starts Deal Agent when apply=true"),
|
|
1311
|
-
sources: z
|
|
1312
|
-
.array(z.enum(["website", "github", "linkedin", "yc", "launch", "web", "monid"]))
|
|
1313
|
-
.optional()
|
|
1314
|
-
.describe("source families to use; defaults to the standard enrichment set"),
|
|
1315
|
-
budgetCents: z
|
|
1316
|
-
.number()
|
|
1317
|
-
.int()
|
|
1318
|
-
.min(0)
|
|
1319
|
-
.max(500)
|
|
1320
|
-
.optional()
|
|
1321
|
-
.describe("Monid spend cap for this run, in cents; default is 50 when Monid is requested"),
|
|
1322
|
-
harnessOnly: z
|
|
1323
|
-
.boolean()
|
|
1324
|
-
.optional()
|
|
1325
|
-
.describe("when true, return/apply the enrichment harness endpoint instead of starting Deal Agent"),
|
|
1326
|
-
message: z
|
|
1327
|
-
.string()
|
|
1328
|
-
.optional()
|
|
1329
|
-
.describe("optional override instruction for the server-side Deal Agent"),
|
|
1330
|
-
},
|
|
1331
|
-
},
|
|
1332
|
-
async ({ dealId, dryRun, apply, executor, sources, budgetCents, harnessOnly, message }) => {
|
|
1333
|
-
const effectiveExecutor = executor ?? "server_agent";
|
|
1334
|
-
if (apply === true && effectiveExecutor === "server_agent" && harnessOnly !== true) {
|
|
1335
|
-
return runDealAgentTool({
|
|
1336
|
-
dealId,
|
|
1337
|
-
title: "MCP enrichment",
|
|
1338
|
-
message: buildEnrichmentAgentMessage({ sources, budgetCents, message }),
|
|
1339
|
-
});
|
|
1340
|
-
}
|
|
1341
|
-
return callApi("POST", `/api/deals/${encodeURIComponent(dealId)}/enrich`, {
|
|
1342
|
-
dryRun,
|
|
1343
|
-
apply,
|
|
1344
|
-
executor: effectiveExecutor,
|
|
1345
|
-
sources,
|
|
1346
|
-
budgetCents,
|
|
1347
|
-
});
|
|
1348
|
-
}
|
|
1349
|
-
);
|
|
1350
|
-
|
|
1351
|
-
// ============================================================
|
|
1352
|
-
// External pitch (founder intake) — no Llama Command token required
|
|
1353
|
-
// ============================================================
|
|
1354
|
-
//
|
|
1355
|
-
// These tools let an MCP-native agent (Claude Code / Cursor / OpenClaw /
|
|
1356
|
-
// Codex / etc.) help its user pitch a company to Llama Ventures by relaying
|
|
1357
|
-
// the conversation through our /api/external/* surface. True A2A: the
|
|
1358
|
-
// founder's agent talks to ours, structured intake gets captured, and a
|
|
1359
|
-
// 12-dimension verdict is returned.
|
|
1360
|
-
//
|
|
1361
|
-
// Anti-abuse rate limits are server-enforced. The MCP tools surface
|
|
1362
|
-
// any server-side rejections as text back to the agent.
|
|
1363
|
-
|
|
1364
|
-
function asTextResult(text, isError = false) {
|
|
1365
|
-
return {
|
|
1366
|
-
content: [{ type: "text", text }],
|
|
1367
|
-
...(isError ? { isError: true } : {}),
|
|
1368
|
-
};
|
|
1369
|
-
}
|
|
1370
|
-
|
|
1371
|
-
server.registerTool(
|
|
1372
|
-
"pitch_start",
|
|
1373
|
-
{
|
|
1374
|
-
description:
|
|
1375
|
-
"Start a new pitch session with Llama Ventures' intake agent. Use this " +
|
|
1376
|
-
"when a founder (the user) wants to pitch their company to Llama. " +
|
|
1377
|
-
"Requires their name + email. Returns a session_id; the conversation " +
|
|
1378
|
-
"is then maintained via pitch_send_message until the agent finalizes. " +
|
|
1379
|
-
"Server-enforced rate limits apply (per-IP, per-email, per-session). " +
|
|
1380
|
-
"No Llama Command token needed.",
|
|
1381
|
-
inputSchema: {
|
|
1382
|
-
name: z.string().describe("the founder's full name (max 100 chars)"),
|
|
1383
|
-
email: z.string().describe("the founder's email (deliverable, not a disposable domain)"),
|
|
1384
|
-
},
|
|
1385
|
-
},
|
|
1386
|
-
async ({ name, email }) => {
|
|
1387
|
-
try {
|
|
1388
|
-
const session = await startExternalSession({ name, email });
|
|
1389
|
-
return asTextResult(
|
|
1390
|
-
JSON.stringify(
|
|
1391
|
-
{
|
|
1392
|
-
session_id: session.session_id,
|
|
1393
|
-
name: session.name,
|
|
1394
|
-
email: session.email,
|
|
1395
|
-
started_at: session.started_at,
|
|
1396
|
-
note: "Session active. Use pitch_send_message to relay the founder's pitch to Llama's intake agent. Use pitch_upload_file to attach decks / one-pagers. The intake agent will auto-finalize once it has enough signal.",
|
|
1397
|
-
},
|
|
1398
|
-
null,
|
|
1399
|
-
2
|
|
1400
|
-
)
|
|
1401
|
-
);
|
|
1402
|
-
} catch (err) {
|
|
1403
|
-
return asTextResult(`Error: ${err?.message ?? String(err)}`, true);
|
|
1404
|
-
}
|
|
1405
|
-
}
|
|
1406
|
-
);
|
|
1407
|
-
|
|
1408
|
-
server.registerTool(
|
|
1409
|
-
"pitch_send_message",
|
|
1410
|
-
{
|
|
1411
|
-
description:
|
|
1412
|
-
"Relay a message from the founder to Llama Ventures' intake agent. " +
|
|
1413
|
-
"Returns the intake agent's reply. The intake agent will ask follow-up " +
|
|
1414
|
-
"questions, request files (use pitch_upload_file), and eventually " +
|
|
1415
|
-
"auto-finalize the pitch — at which point the response includes " +
|
|
1416
|
-
"`finalize_payload` with a confirmation_summary and a 12-dimension " +
|
|
1417
|
-
"verdict (overall green/yellow/red + per-dimension notes).",
|
|
1418
|
-
inputSchema: {
|
|
1419
|
-
message: z.string().describe("the founder's message (max 8000 chars)"),
|
|
1420
|
-
},
|
|
1421
|
-
},
|
|
1422
|
-
async ({ message }) => {
|
|
1423
|
-
try {
|
|
1424
|
-
const result = await sendExternalMessage(message);
|
|
1425
|
-
const out = {
|
|
1426
|
-
text: result.text,
|
|
1427
|
-
finalized: result.finalized,
|
|
1428
|
-
finalize_payload: result.finalize_payload,
|
|
1429
|
-
};
|
|
1430
|
-
return asTextResult(JSON.stringify(out, null, 2));
|
|
1431
|
-
} catch (err) {
|
|
1432
|
-
return asTextResult(`Error: ${err?.message ?? String(err)}`, true);
|
|
1433
|
-
}
|
|
1434
|
-
}
|
|
1435
|
-
);
|
|
1436
|
-
|
|
1437
|
-
server.registerTool(
|
|
1438
|
-
"pitch_upload_file",
|
|
1439
|
-
{
|
|
1440
|
-
description:
|
|
1441
|
-
"Attach a file (deck, one-pager, deck PDF, screenshot, etc.) to the " +
|
|
1442
|
-
"active pitch session. Server allows pdf / pptx / ppt / docx / doc / " +
|
|
1443
|
-
"xlsx / xls / png / jpg / webp / heic / heif / txt / md, with " +
|
|
1444
|
-
"server-enforced size and per-session count limits. " +
|
|
1445
|
-
"Returns a drive_file_id; the intake agent will " +
|
|
1446
|
-
"pick the file up via list_uploaded_files / read_uploaded_file on its " +
|
|
1447
|
-
"next turn (so call pitch_send_message with a one-line note like " +
|
|
1448
|
-
"'I just uploaded our pitch deck' so the agent knows to look).",
|
|
1449
|
-
inputSchema: {
|
|
1450
|
-
path: z.string().describe("absolute or relative filesystem path to the file"),
|
|
1451
|
-
},
|
|
1452
|
-
},
|
|
1453
|
-
async ({ path: filePath }) => {
|
|
1454
|
-
try {
|
|
1455
|
-
const result = await uploadExternalFile(filePath);
|
|
1456
|
-
return asTextResult(JSON.stringify(result, null, 2));
|
|
1457
|
-
} catch (err) {
|
|
1458
|
-
return asTextResult(`Error: ${err?.message ?? String(err)}`, true);
|
|
1459
|
-
}
|
|
1460
|
-
}
|
|
1461
|
-
);
|
|
1462
|
-
|
|
1463
|
-
server.registerTool(
|
|
1464
|
-
"pitch_status",
|
|
1465
|
-
{
|
|
1466
|
-
description:
|
|
1467
|
-
"Show the current pitch session state — session_id, started_at, idle " +
|
|
1468
|
-
"minutes, finalized flag. Useful when the agent isn't sure if a " +
|
|
1469
|
-
"session is still active.",
|
|
1470
|
-
inputSchema: {},
|
|
364
|
+
description: "Fetch the authenticated current agent contract; bundled text is only an offline fallback.",
|
|
1471
365
|
},
|
|
1472
366
|
async () => {
|
|
1473
|
-
try {
|
|
1474
|
-
const status = getExternalSessionStatus();
|
|
1475
|
-
return asTextResult(JSON.stringify(status, null, 2));
|
|
1476
|
-
} catch (err) {
|
|
1477
|
-
return asTextResult(`Error: ${err?.message ?? String(err)}`, true);
|
|
1478
|
-
}
|
|
1479
|
-
}
|
|
1480
|
-
);
|
|
1481
|
-
|
|
1482
|
-
server.registerTool(
|
|
1483
|
-
"pitch_finalize",
|
|
1484
|
-
{
|
|
1485
|
-
description:
|
|
1486
|
-
"Clear the local pitch session state. Note: this does not force the " +
|
|
1487
|
-
"server-side intake agent to finalize — the agent decides that on its " +
|
|
1488
|
-
"own once the pitch is sufficient. Use this for cleanup after a session " +
|
|
1489
|
-
"ends, or to abandon a session early. The server-side session will " +
|
|
1490
|
-
"naturally expire after the server's idle timeout.",
|
|
1491
|
-
inputSchema: {},
|
|
1492
|
-
},
|
|
1493
|
-
async () => {
|
|
1494
|
-
try {
|
|
1495
|
-
const before = getExternalSessionStatus();
|
|
1496
|
-
clearExternalSession();
|
|
1497
|
-
return asTextResult(
|
|
1498
|
-
JSON.stringify(
|
|
1499
|
-
{
|
|
1500
|
-
cleared: before.active,
|
|
1501
|
-
previous_session: before.active ? before : null,
|
|
1502
|
-
note: "Local pitch session state cleared. Server-side session may still be active until its idle timeout.",
|
|
1503
|
-
},
|
|
1504
|
-
null,
|
|
1505
|
-
2
|
|
1506
|
-
)
|
|
1507
|
-
);
|
|
1508
|
-
} catch (err) {
|
|
1509
|
-
return asTextResult(`Error: ${err?.message ?? String(err)}`, true);
|
|
1510
|
-
}
|
|
1511
|
-
}
|
|
1512
|
-
);
|
|
1513
|
-
|
|
1514
|
-
// ============================================================
|
|
1515
|
-
// Memo — read-only. Generation runs only from the durable Memo Agent in the UI.
|
|
1516
|
-
// ============================================================
|
|
1517
|
-
|
|
1518
|
-
server.registerTool(
|
|
1519
|
-
"memo_show",
|
|
1520
|
-
{
|
|
1521
|
-
description:
|
|
1522
|
-
"Fetch the current memo for a deal. Returns the envelope: memo " +
|
|
1523
|
-
"(html, version, source, updated_by, updated_at) and mode. html " +
|
|
1524
|
-
"can be 50-100KB — be deliberate about including it in your reply.",
|
|
1525
|
-
inputSchema: {
|
|
1526
|
-
dealId: z.string().describe("deal uuid"),
|
|
1527
|
-
},
|
|
1528
|
-
},
|
|
1529
|
-
async ({ dealId }) =>
|
|
1530
|
-
callApi("GET", `/api/deals/${encodeURIComponent(dealId)}/memo`)
|
|
1531
|
-
);
|
|
1532
|
-
|
|
1533
|
-
// ============================================================
|
|
1534
|
-
// Deal page HTML — hand-authored sandboxed page per deal
|
|
1535
|
-
// ============================================================
|
|
1536
|
-
//
|
|
1537
|
-
// Each deal has its own /deals/<id>/browse page that renders a
|
|
1538
|
-
// hand-authored HTML in a sandboxed iframe (allow-scripts, no
|
|
1539
|
-
// same-origin). Uploads from any caller (web UI, CLI, agent, MCP)
|
|
1540
|
-
// create a new monotonic version + trigger SSE push so any open
|
|
1541
|
-
// viewer refreshes in real time. Old versions are soft-deleted on
|
|
1542
|
-
// replace and can be restored.
|
|
1543
|
-
|
|
1544
|
-
// All html_* tools take an optional documentSlug param. Default 'main'.
|
|
1545
|
-
// Each deal can hold multiple named documents (different HTMLs); use
|
|
1546
|
-
// html_docs_list to discover slugs.
|
|
1547
|
-
const INLINE_HTML_UPLOAD_LIMIT = 50 * 1024;
|
|
1548
|
-
const MAX_HTML_BYTES = 5 * 1024 * 1024;
|
|
1549
|
-
const MAX_ASSET_BYTES = 50 * 1024 * 1024;
|
|
1550
|
-
const MAX_BUNDLE_BYTES = 100 * 1024 * 1024;
|
|
1551
|
-
|
|
1552
|
-
function htmlUrl(dealId, slug) {
|
|
1553
|
-
return `/api/deals/${encodeURIComponent(dealId)}/documents/${encodeURIComponent(slug ?? "main")}/html`;
|
|
1554
|
-
}
|
|
1555
|
-
|
|
1556
|
-
function looksLikeHtml(html) {
|
|
1557
|
-
const head = String(html || "").trim().slice(0, 256).toLowerCase();
|
|
1558
|
-
return head.startsWith("<!doctype html") || head.startsWith("<html");
|
|
1559
|
-
}
|
|
1560
|
-
|
|
1561
|
-
function mimeForAsset(path) {
|
|
1562
|
-
const ext = (String(path).split(".").pop() || "").toLowerCase();
|
|
1563
|
-
return (
|
|
1564
|
-
{
|
|
1565
|
-
jpg: "image/jpeg",
|
|
1566
|
-
jpeg: "image/jpeg",
|
|
1567
|
-
png: "image/png",
|
|
1568
|
-
gif: "image/gif",
|
|
1569
|
-
webp: "image/webp",
|
|
1570
|
-
svg: "image/svg+xml",
|
|
1571
|
-
ico: "image/x-icon",
|
|
1572
|
-
avif: "image/avif",
|
|
1573
|
-
css: "text/css",
|
|
1574
|
-
js: "text/javascript",
|
|
1575
|
-
json: "application/json",
|
|
1576
|
-
woff: "font/woff",
|
|
1577
|
-
woff2: "font/woff2",
|
|
1578
|
-
ttf: "font/ttf",
|
|
1579
|
-
otf: "font/otf",
|
|
1580
|
-
mp4: "video/mp4",
|
|
1581
|
-
webm: "video/webm",
|
|
1582
|
-
pdf: "application/pdf",
|
|
1583
|
-
}[ext] || "application/octet-stream"
|
|
1584
|
-
);
|
|
1585
|
-
}
|
|
1586
|
-
|
|
1587
|
-
async function detectSiblingAssetsDir(filePath) {
|
|
1588
|
-
const { existsSync, statSync } = await import("node:fs");
|
|
1589
|
-
const { dirname, basename, extname, join } = await import("node:path");
|
|
1590
|
-
const dir = dirname(filePath);
|
|
1591
|
-
const stem = basename(filePath, extname(filePath));
|
|
1592
|
-
const candidates = [
|
|
1593
|
-
`${stem}_files`,
|
|
1594
|
-
`${stem} files`,
|
|
1595
|
-
`${basename(filePath)}_files`,
|
|
1596
|
-
];
|
|
1597
|
-
for (const name of candidates) {
|
|
1598
|
-
const p = join(dir, name);
|
|
1599
|
-
if (existsSync(p) && statSync(p).isDirectory()) return p;
|
|
1600
|
-
}
|
|
1601
|
-
return null;
|
|
1602
|
-
}
|
|
1603
|
-
|
|
1604
|
-
async function collectAssets(assetsRoot) {
|
|
1605
|
-
const { readFileSync, readdirSync, statSync } = await import("node:fs");
|
|
1606
|
-
const { join, relative, sep, basename } = await import("node:path");
|
|
1607
|
-
const rootStat = statSync(assetsRoot);
|
|
1608
|
-
if (!rootStat.isDirectory()) {
|
|
1609
|
-
throw new Error(`assetsDir must point to a directory: ${assetsRoot}`);
|
|
1610
|
-
}
|
|
1611
|
-
const collected = [];
|
|
1612
|
-
const walk = (dir) => {
|
|
1613
|
-
for (const name of readdirSync(dir)) {
|
|
1614
|
-
const absPath = join(dir, name);
|
|
1615
|
-
const st = statSync(absPath);
|
|
1616
|
-
if (st.isDirectory()) {
|
|
1617
|
-
walk(absPath);
|
|
1618
|
-
} else if (st.isFile()) {
|
|
1619
|
-
const relPath = relative(assetsRoot, absPath).split(sep).join("/");
|
|
1620
|
-
collected.push({ absPath, relPath, bytes: st.size });
|
|
1621
|
-
}
|
|
1622
|
-
}
|
|
1623
|
-
};
|
|
1624
|
-
walk(assetsRoot);
|
|
1625
|
-
if (collected.length === 0) {
|
|
1626
|
-
throw new Error(`assetsDir is empty: ${assetsRoot}`);
|
|
1627
|
-
}
|
|
1628
|
-
const rootName = basename(assetsRoot);
|
|
1629
|
-
const looksLikeSavePageDir = /[_ ]files$/i.test(rootName);
|
|
1630
|
-
const finalPaths = looksLikeSavePageDir
|
|
1631
|
-
? collected.map((c) => ({ ...c, relPath: `${rootName}/${c.relPath}` }))
|
|
1632
|
-
: collected;
|
|
1633
|
-
let totalBytes = 0;
|
|
1634
|
-
for (const item of finalPaths) {
|
|
1635
|
-
if (item.relPath.split("/").some((seg) => seg === "..")) {
|
|
1636
|
-
throw new Error(`asset path "${item.relPath}" contains "..", refused`);
|
|
1637
|
-
}
|
|
1638
|
-
if (item.bytes > MAX_ASSET_BYTES) {
|
|
1639
|
-
throw new Error(
|
|
1640
|
-
`asset "${item.relPath}" is ${item.bytes} bytes; cap is ${MAX_ASSET_BYTES}`,
|
|
1641
|
-
);
|
|
1642
|
-
}
|
|
1643
|
-
totalBytes += item.bytes;
|
|
1644
|
-
if (totalBytes > MAX_BUNDLE_BYTES) {
|
|
1645
|
-
throw new Error(`total asset bytes exceeds ${MAX_BUNDLE_BYTES}`);
|
|
1646
|
-
}
|
|
1647
|
-
}
|
|
1648
|
-
return {
|
|
1649
|
-
assets: finalPaths.map((item) => ({
|
|
1650
|
-
...item,
|
|
1651
|
-
data: readFileSync(item.absPath),
|
|
1652
|
-
contentType: mimeForAsset(item.relPath),
|
|
1653
|
-
})),
|
|
1654
|
-
totalBytes,
|
|
1655
|
-
};
|
|
1656
|
-
}
|
|
1657
|
-
|
|
1658
|
-
async function uploadHtmlFromFile({
|
|
1659
|
-
dealId,
|
|
1660
|
-
filePath,
|
|
1661
|
-
documentSlug,
|
|
1662
|
-
source = "agent",
|
|
1663
|
-
assetsDir,
|
|
1664
|
-
autoDetectAssets = true,
|
|
1665
|
-
verify = true,
|
|
1666
|
-
clientUploadId,
|
|
1667
|
-
}) {
|
|
1668
|
-
const { readFileSync, statSync } = await import("node:fs");
|
|
1669
|
-
const st = statSync(filePath);
|
|
1670
|
-
if (!st.isFile()) throw new Error(`filePath must point to a file: ${filePath}`);
|
|
1671
|
-
const html = readFileSync(filePath, "utf8");
|
|
1672
|
-
if (!html.trim()) throw new Error("HTML body is empty.");
|
|
1673
|
-
const htmlBytes = Buffer.byteLength(html, "utf8");
|
|
1674
|
-
if (htmlBytes > MAX_HTML_BYTES) {
|
|
1675
|
-
throw new Error(
|
|
1676
|
-
`HTML body is ${(htmlBytes / 1024 / 1024).toFixed(2)} MB; cap is 5 MB.`,
|
|
1677
|
-
);
|
|
1678
|
-
}
|
|
1679
|
-
if (!looksLikeHtml(html)) {
|
|
1680
|
-
throw new Error("HTML must start with <!doctype html> or <html.");
|
|
1681
|
-
}
|
|
1682
|
-
|
|
1683
|
-
let effectiveAssetsDir = assetsDir || null;
|
|
1684
|
-
if (!effectiveAssetsDir && autoDetectAssets !== false) {
|
|
1685
|
-
effectiveAssetsDir = await detectSiblingAssetsDir(filePath);
|
|
1686
|
-
}
|
|
1687
|
-
const uploadId = normalizeUploadId(clientUploadId) || newHtmlUploadId();
|
|
1688
|
-
|
|
1689
|
-
let body;
|
|
1690
|
-
if (!effectiveAssetsDir) {
|
|
1691
|
-
body = await request("PUT", htmlUrl(dealId, documentSlug), {
|
|
1692
|
-
html,
|
|
1693
|
-
source,
|
|
1694
|
-
client_upload_id: uploadId,
|
|
1695
|
-
}, {
|
|
1696
|
-
headers: { "X-Llama-Upload-Id": uploadId },
|
|
1697
|
-
});
|
|
1698
|
-
} else {
|
|
1699
|
-
const { assets, totalBytes } = await collectAssets(effectiveAssetsDir);
|
|
1700
|
-
const form = new FormData();
|
|
1701
|
-
form.append("html", html);
|
|
1702
|
-
form.append("source", source);
|
|
1703
|
-
form.append("client_upload_id", uploadId);
|
|
1704
|
-
for (const asset of assets) {
|
|
1705
|
-
form.append(
|
|
1706
|
-
`asset:${asset.relPath}`,
|
|
1707
|
-
new Blob([asset.data], { type: asset.contentType }),
|
|
1708
|
-
asset.relPath,
|
|
1709
|
-
);
|
|
1710
|
-
}
|
|
1711
|
-
const headers = await getAuthHeaders();
|
|
1712
|
-
const res = await fetch(`${getBaseUrl()}${htmlUrl(dealId, documentSlug)}`, {
|
|
1713
|
-
method: "PUT",
|
|
1714
|
-
headers: { ...headers, "X-Llama-Upload-Id": uploadId },
|
|
1715
|
-
body: form,
|
|
1716
|
-
});
|
|
1717
|
-
body = await res.json().catch(() => ({}));
|
|
1718
|
-
if (!res.ok) {
|
|
1719
|
-
throw new Error(
|
|
1720
|
-
`HTTP ${res.status}: ${body?.error || JSON.stringify(body).slice(0, 300)}`,
|
|
1721
|
-
);
|
|
1722
|
-
}
|
|
1723
|
-
body = { ...body, asset_bytes: body.asset_bytes ?? totalBytes };
|
|
1724
|
-
}
|
|
1725
|
-
|
|
1726
|
-
let verified = { ok: false, skipped: true };
|
|
1727
|
-
if (verify !== false) {
|
|
1728
|
-
const latest = await request("GET", htmlUrl(dealId, documentSlug));
|
|
1729
|
-
if (latest?.empty) throw new Error("verification failed: document came back empty after upload");
|
|
1730
|
-
if (body?.version != null && Number(latest.version) !== Number(body.version)) {
|
|
1731
|
-
throw new Error(`verification failed: expected version ${body.version}, got ${latest.version}`);
|
|
1732
|
-
}
|
|
1733
|
-
if (body?.bytes != null && latest.bytes != null && Number(latest.bytes) !== Number(body.bytes)) {
|
|
1734
|
-
throw new Error(`verification failed: expected ${body.bytes} bytes, got ${latest.bytes}`);
|
|
1735
|
-
}
|
|
1736
|
-
if (body?.sha256 && latest.sha256 && String(latest.sha256) !== String(body.sha256)) {
|
|
1737
|
-
throw new Error(`verification failed: expected sha256 ${body.sha256}, got ${latest.sha256}`);
|
|
1738
|
-
}
|
|
1739
|
-
verified = {
|
|
1740
|
-
ok: true,
|
|
1741
|
-
version: latest.version,
|
|
1742
|
-
bytes: latest.bytes,
|
|
1743
|
-
sha256: latest.sha256,
|
|
1744
|
-
created_at: latest.created_at,
|
|
1745
|
-
};
|
|
1746
|
-
}
|
|
1747
|
-
|
|
1748
|
-
return {
|
|
1749
|
-
ok: true,
|
|
1750
|
-
document_slug: documentSlug || "main",
|
|
1751
|
-
version: body?.version,
|
|
1752
|
-
bytes: body?.bytes ?? verified.bytes ?? htmlBytes,
|
|
1753
|
-
sha256: body?.sha256 ?? verified.sha256,
|
|
1754
|
-
client_upload_id: body?.client_upload_id ?? uploadId,
|
|
1755
|
-
idempotent_replay: body?.idempotent_replay,
|
|
1756
|
-
asset_count: body?.asset_count,
|
|
1757
|
-
asset_bytes: body?.asset_bytes,
|
|
1758
|
-
assets_dir: effectiveAssetsDir,
|
|
1759
|
-
verified,
|
|
1760
|
-
viewer: `${getBaseUrl()}/deals/${encodeURIComponent(dealId)}/browse/${encodeURIComponent(documentSlug || "main")}`,
|
|
1761
|
-
};
|
|
1762
|
-
}
|
|
1763
|
-
|
|
1764
|
-
server.registerTool(
|
|
1765
|
-
"html_show",
|
|
1766
|
-
{
|
|
1767
|
-
description:
|
|
1768
|
-
"Read the current hand-authored HTML 'deal page' for a deal. " +
|
|
1769
|
-
"Returns {empty: true} if no one has uploaded HTML yet, or " +
|
|
1770
|
-
"{empty: false, version, html, bytes, sha256, uploaded_by, source, " +
|
|
1771
|
-
"created_at}. The HTML can be 5-500KB — be deliberate about " +
|
|
1772
|
-
"including the body in your reply. Use html_versions if you " +
|
|
1773
|
-
"just want the version list without the body. Each deal can " +
|
|
1774
|
-
"have multiple named docs — pass documentSlug to target a " +
|
|
1775
|
-
"non-'main' one (use html_docs_list to discover them).",
|
|
1776
|
-
inputSchema: {
|
|
1777
|
-
dealId: z.string().describe("deal uuid"),
|
|
1778
|
-
documentSlug: z
|
|
1779
|
-
.string()
|
|
1780
|
-
.optional()
|
|
1781
|
-
.describe("default: 'main'. Use html_docs_list to discover slugs."),
|
|
1782
|
-
},
|
|
1783
|
-
},
|
|
1784
|
-
async ({ dealId, documentSlug }) =>
|
|
1785
|
-
callApi("GET", htmlUrl(dealId, documentSlug))
|
|
1786
|
-
);
|
|
1787
|
-
|
|
1788
|
-
server.registerTool(
|
|
1789
|
-
"html_upload",
|
|
1790
|
-
{
|
|
1791
|
-
description:
|
|
1792
|
-
"Upload (PUT) a new HTML version for a SPECIFIC DEAL's /browse page " +
|
|
1793
|
-
"(deal-scoped artifact: IC memo for X · dashboard for X · 2×2 for X). " +
|
|
1794
|
-
"Renders at /deals/<id>/browse/<slug>. " +
|
|
1795
|
-
"**Routing — pick the right destination BEFORE calling this:** " +
|
|
1796
|
-
"(a) Deal-specific HTML (this tool) → /deals/<id>/browse/<slug>. " +
|
|
1797
|
-
"(b) Cross-deal / institutional / thesis / sector landscape → use " +
|
|
1798
|
-
"`wiki_save` with content_type='html' instead (/wiki/<slug>). " +
|
|
1799
|
-
"(c) Founder-facing public share link → escape to Netlify only when " +
|
|
1800
|
-
"the user explicitly says 'share with founder' / 'publish publicly'; " +
|
|
1801
|
-
"Llama Command outranks Netlify for everything internal. " +
|
|
1802
|
-
"Creates a NEW version row — the previous version is retained " +
|
|
1803
|
-
"and restorable. Triggers SSE push so any open viewer auto- " +
|
|
1804
|
-
"refreshes. Constraints: HTML body MUST start with " +
|
|
1805
|
-
"<!doctype html> or <html (case-insensitive); max 5 MB. Reliability guard: " +
|
|
1806
|
-
"this inline-string tool refuses bodies over 50KB; use html_upload_file " +
|
|
1807
|
-
"or `llama html publish --file` for memos/reports. ALWAYS " +
|
|
1808
|
-
"call html_show first if anything exists — replace only the " +
|
|
1809
|
-
"relevant section, don't lose unrelated content. Source defaults " +
|
|
1810
|
-
"to 'agent' for MCP-originated uploads. Pass documentSlug to " +
|
|
1811
|
-
"target a non-'main' doc — auto-creates the doc if it doesn't exist.",
|
|
1812
|
-
inputSchema: {
|
|
1813
|
-
dealId: z.string().describe("deal uuid"),
|
|
1814
|
-
html: z.string().describe("complete HTML document"),
|
|
1815
|
-
documentSlug: z
|
|
1816
|
-
.string()
|
|
1817
|
-
.optional()
|
|
1818
|
-
.describe("default: 'main'"),
|
|
1819
|
-
source: z
|
|
1820
|
-
.enum(["web", "cli", "agent"])
|
|
1821
|
-
.optional()
|
|
1822
|
-
.describe("default: agent"),
|
|
1823
|
-
clientUploadId: z.string().optional().describe("optional retry id; reuse the same value if retrying the same small inline upload"),
|
|
1824
|
-
},
|
|
1825
|
-
},
|
|
1826
|
-
async ({ dealId, html, documentSlug, source, clientUploadId }) => {
|
|
1827
|
-
const bytes = Buffer.byteLength(String(html || ""), "utf8");
|
|
1828
|
-
if (bytes > INLINE_HTML_UPLOAD_LIMIT) {
|
|
1829
|
-
return textResult(
|
|
1830
|
-
`Error: html_upload received ${(bytes / 1024).toFixed(1)} KB of inline HTML. ` +
|
|
1831
|
-
`For reliability, do not pass large HTML through MCP tool arguments. ` +
|
|
1832
|
-
`Use html_upload_file({ dealId, filePath, documentSlug }) or run ` +
|
|
1833
|
-
`\`llama html publish <deal-id-or-name> --file <path> --doc <slug>\` instead.`,
|
|
1834
|
-
true,
|
|
1835
|
-
);
|
|
1836
|
-
}
|
|
1837
|
-
let uploadId;
|
|
1838
|
-
try {
|
|
1839
|
-
uploadId = normalizeUploadId(clientUploadId) || newHtmlUploadId();
|
|
1840
|
-
} catch (err) {
|
|
1841
|
-
return textResult(`Error: ${err?.message ?? String(err)}`, true);
|
|
1842
|
-
}
|
|
1843
|
-
return callApi("PUT", htmlUrl(dealId, documentSlug), {
|
|
1844
|
-
html,
|
|
1845
|
-
source: source ?? "agent",
|
|
1846
|
-
client_upload_id: uploadId,
|
|
1847
|
-
}, {
|
|
1848
|
-
headers: { "X-Llama-Upload-Id": uploadId },
|
|
1849
|
-
});
|
|
1850
|
-
}
|
|
1851
|
-
);
|
|
1852
|
-
|
|
1853
|
-
server.registerTool(
|
|
1854
|
-
"html_upload_file",
|
|
1855
|
-
{
|
|
1856
|
-
description:
|
|
1857
|
-
"Agent-safe HTML upload from a LOCAL FILE PATH. Use this instead " +
|
|
1858
|
-
"of html_upload for any substantial memo/report; it avoids moving " +
|
|
1859
|
-
"large HTML through the model/tool-call context. Reads filePath on " +
|
|
1860
|
-
"the machine running this MCP server, preflights size/HTML shape, " +
|
|
1861
|
-
"optionally auto-detects a sibling *_files asset folder, uploads, " +
|
|
1862
|
-
"then reads the document back to verify version/bytes/sha256. For a higher " +
|
|
1863
|
-
"level CLI flow that can resolve deal names and choose create/update, " +
|
|
1864
|
-
"run `llama html publish <deal-id-or-name> --file <path>`.",
|
|
1865
|
-
inputSchema: {
|
|
1866
|
-
dealId: z.string().describe("deal uuid"),
|
|
1867
|
-
filePath: z.string().describe("absolute or relative local filesystem path to the HTML file"),
|
|
1868
|
-
documentSlug: z.string().optional().describe("default: 'main'"),
|
|
1869
|
-
source: z.enum(["web", "cli", "agent"]).optional().describe("default: agent"),
|
|
1870
|
-
assetsDir: z.string().optional().describe("optional local directory of relative assets"),
|
|
1871
|
-
autoDetectAssets: z.boolean().optional().describe("default true; detects sibling *_files folders"),
|
|
1872
|
-
verify: z.boolean().optional().describe("default true; read-after-write verification"),
|
|
1873
|
-
clientUploadId: z.string().optional().describe("optional retry id; reuse the same value if retrying the same failed upload"),
|
|
1874
|
-
},
|
|
1875
|
-
},
|
|
1876
|
-
async ({ dealId, filePath, documentSlug, source, assetsDir, autoDetectAssets, verify, clientUploadId }) => {
|
|
1877
|
-
try {
|
|
1878
|
-
const result = await uploadHtmlFromFile({
|
|
1879
|
-
dealId,
|
|
1880
|
-
filePath,
|
|
1881
|
-
documentSlug,
|
|
1882
|
-
source: source ?? "agent",
|
|
1883
|
-
assetsDir,
|
|
1884
|
-
autoDetectAssets,
|
|
1885
|
-
verify,
|
|
1886
|
-
clientUploadId,
|
|
1887
|
-
});
|
|
1888
|
-
return jsonResult(result);
|
|
1889
|
-
} catch (err) {
|
|
1890
|
-
return textResult(`Error: ${err?.message ?? String(err)}`, true);
|
|
1891
|
-
}
|
|
1892
|
-
},
|
|
1893
|
-
);
|
|
1894
|
-
|
|
1895
|
-
server.registerTool(
|
|
1896
|
-
"html_versions",
|
|
1897
|
-
{
|
|
1898
|
-
description:
|
|
1899
|
-
"List version history for a deal's /browse page HTML. Returns " +
|
|
1900
|
-
"an array of {version, bytes, sha256, uploaded_by, source, created_at, " +
|
|
1901
|
-
"deleted_at} — newest first, including soft-deleted versions. " +
|
|
1902
|
-
"Use to find a target version for html_restore.",
|
|
1903
|
-
inputSchema: {
|
|
1904
|
-
dealId: z.string().describe("deal uuid"),
|
|
1905
|
-
documentSlug: z.string().optional().describe("default: 'main'"),
|
|
1906
|
-
},
|
|
1907
|
-
},
|
|
1908
|
-
async ({ dealId, documentSlug }) =>
|
|
1909
|
-
callApi("GET", `${htmlUrl(dealId, documentSlug)}/history`)
|
|
1910
|
-
);
|
|
1911
|
-
|
|
1912
|
-
server.registerTool(
|
|
1913
|
-
"html_restore",
|
|
1914
|
-
{
|
|
1915
|
-
description:
|
|
1916
|
-
"Restore an old HTML version by copying it forward as a new " +
|
|
1917
|
-
"version (so the latest pointer moves to the restored content). " +
|
|
1918
|
-
"Use html_versions first to discover the version number. " +
|
|
1919
|
-
"Triggers SSE push.",
|
|
1920
|
-
inputSchema: {
|
|
1921
|
-
dealId: z.string().describe("deal uuid"),
|
|
1922
|
-
version: z.number().int().positive().describe("version to restore"),
|
|
1923
|
-
documentSlug: z.string().optional().describe("default: 'main'"),
|
|
1924
|
-
},
|
|
1925
|
-
},
|
|
1926
|
-
async ({ dealId, version, documentSlug }) =>
|
|
1927
|
-
callApi("POST", `${htmlUrl(dealId, documentSlug)}/restore/${version}`)
|
|
1928
|
-
);
|
|
1929
|
-
|
|
1930
|
-
server.registerTool(
|
|
1931
|
-
"html_docs_list",
|
|
1932
|
-
{
|
|
1933
|
-
description:
|
|
1934
|
-
"List all documents (HTML 'pages') on a deal. Each deal can " +
|
|
1935
|
-
"hold multiple — like a folder of files. Returns an array of " +
|
|
1936
|
-
"{slug, title, preview_url, created_by, latest_version, " +
|
|
1937
|
-
"latest_bytes, latest_uploaded_by, latest_updated_at}. The " +
|
|
1938
|
-
"'main' slug is the default doc; non-main slugs are explicit.",
|
|
1939
|
-
inputSchema: {
|
|
1940
|
-
dealId: z.string().describe("deal uuid"),
|
|
1941
|
-
},
|
|
1942
|
-
},
|
|
1943
|
-
async ({ dealId }) =>
|
|
1944
|
-
callApi("GET", `/api/deals/${encodeURIComponent(dealId)}/documents`)
|
|
1945
|
-
);
|
|
1946
|
-
|
|
1947
|
-
server.registerTool(
|
|
1948
|
-
"html_docs_create",
|
|
1949
|
-
{
|
|
1950
|
-
description:
|
|
1951
|
-
"Create a NEW named document slot on a deal (metadata only — " +
|
|
1952
|
-
"upload HTML separately via html_upload with the same slug). " +
|
|
1953
|
-
"Slug must match /^[a-z0-9][a-z0-9_-]{0,63}$/ — lowercase alnum + " +
|
|
1954
|
-
"hyphen/underscore. Examples: 'ic-onepager', 'founder-brief', " +
|
|
1955
|
-
"'market-map'. Title is for display; defaults to the slug.",
|
|
1956
|
-
inputSchema: {
|
|
1957
|
-
dealId: z.string().describe("deal uuid"),
|
|
1958
|
-
slug: z.string().describe("URL-safe id, e.g. 'ic-onepager'"),
|
|
1959
|
-
title: z.string().optional().describe("display title; defaults to slug"),
|
|
1960
|
-
},
|
|
1961
|
-
},
|
|
1962
|
-
async ({ dealId, slug, title }) =>
|
|
1963
|
-
callApi("POST", `/api/deals/${encodeURIComponent(dealId)}/documents`, {
|
|
1964
|
-
slug,
|
|
1965
|
-
title: title ?? slug,
|
|
1966
|
-
})
|
|
1967
|
-
);
|
|
1968
|
-
|
|
1969
|
-
server.registerTool(
|
|
1970
|
-
"html_docs_archive",
|
|
1971
|
-
{
|
|
1972
|
-
description:
|
|
1973
|
-
"Archive a non-'main' doc — hides it from the selection page. " +
|
|
1974
|
-
"HTML/asset versions are retained and the doc can be 'un-archived' " +
|
|
1975
|
-
"later (currently via direct DB or by ensureDealDocument). The " +
|
|
1976
|
-
"'main' doc cannot be archived (it's the default slot).",
|
|
1977
|
-
inputSchema: {
|
|
1978
|
-
dealId: z.string().describe("deal uuid"),
|
|
1979
|
-
slug: z.string().describe("slug to archive (must not be 'main')"),
|
|
1980
|
-
},
|
|
1981
|
-
},
|
|
1982
|
-
async ({ dealId, slug }) =>
|
|
1983
|
-
callApi(
|
|
1984
|
-
"DELETE",
|
|
1985
|
-
`/api/deals/${encodeURIComponent(dealId)}/documents/${encodeURIComponent(slug)}`
|
|
1986
|
-
)
|
|
1987
|
-
);
|
|
1988
|
-
|
|
1989
|
-
server.registerTool(
|
|
1990
|
-
"html_upload_bundle",
|
|
1991
|
-
{
|
|
1992
|
-
description:
|
|
1993
|
-
"Legacy small inline upload for HTML + binary assets as one atomic version. " +
|
|
1994
|
-
"For substantial memos/reports or 'Save Page As Complete' exports, use " +
|
|
1995
|
-
"html_upload_file with filePath + assetsDir instead so large HTML/assets " +
|
|
1996
|
-
"do not move through the model/tool-call context. This inline bundle " +
|
|
1997
|
-
"tool refuses payloads over 50KB. The server stores HTML + each asset as " +
|
|
1998
|
-
"one transactional bundle (deal_browse_assets " +
|
|
1999
|
-
"table), rewrites the HTML refs to version-pinned URLs at " +
|
|
2000
|
-
"/api/deals/<id>/asset/<path>?v=N, and triggers SSE push. " +
|
|
2001
|
-
"Constraints: HTML <= 5 MB; each asset <= 50 MB; total bundle " +
|
|
2002
|
-
"<= 100 MB. Asset paths must match the relative refs in the HTML " +
|
|
2003
|
-
"(no leading './', no '..' segments).",
|
|
2004
|
-
inputSchema: {
|
|
2005
|
-
dealId: z.string().describe("deal uuid"),
|
|
2006
|
-
html: z.string().describe("complete HTML document"),
|
|
2007
|
-
assets: z
|
|
2008
|
-
.array(
|
|
2009
|
-
z.object({
|
|
2010
|
-
path: z
|
|
2011
|
-
.string()
|
|
2012
|
-
.describe(
|
|
2013
|
-
"relative path matching the HTML's src/href ref " +
|
|
2014
|
-
"(e.g. 'images/cover.png' or 'Foo_files/img.jpg')",
|
|
2015
|
-
),
|
|
2016
|
-
contentType: z
|
|
2017
|
-
.string()
|
|
2018
|
-
.describe("MIME type, e.g. 'image/jpeg', 'font/woff2'"),
|
|
2019
|
-
base64: z
|
|
2020
|
-
.string()
|
|
2021
|
-
.describe("base64-encoded file bytes (NO data:URI prefix)"),
|
|
2022
|
-
}),
|
|
2023
|
-
)
|
|
2024
|
-
.min(1)
|
|
2025
|
-
.describe("at least one asset (use html_upload if no assets)"),
|
|
2026
|
-
documentSlug: z
|
|
2027
|
-
.string()
|
|
2028
|
-
.optional()
|
|
2029
|
-
.describe("default: 'main'"),
|
|
2030
|
-
source: z
|
|
2031
|
-
.enum(["web", "cli", "agent"])
|
|
2032
|
-
.optional()
|
|
2033
|
-
.describe("default: agent"),
|
|
2034
|
-
clientUploadId: z.string().optional().describe("optional retry id; reuse the same value if retrying the same bundle upload"),
|
|
2035
|
-
},
|
|
2036
|
-
},
|
|
2037
|
-
async ({ dealId, html, assets, documentSlug, source, clientUploadId }) => {
|
|
2038
|
-
const inlineBytes =
|
|
2039
|
-
Buffer.byteLength(String(html || ""), "utf8") +
|
|
2040
|
-
assets.reduce((sum, a) => sum + Buffer.byteLength(String(a.base64 || ""), "utf8"), 0);
|
|
2041
|
-
if (inlineBytes > INLINE_HTML_UPLOAD_LIMIT) {
|
|
2042
|
-
return textResult(
|
|
2043
|
-
`Error: html_upload_bundle received ${(inlineBytes / 1024).toFixed(1)} KB of inline tool-call payload. ` +
|
|
2044
|
-
`For reliability, do not pass large HTML/assets through MCP arguments. ` +
|
|
2045
|
-
`Use html_upload_file({ dealId, filePath, documentSlug, assetsDir }) or run ` +
|
|
2046
|
-
`\`llama html publish <deal-id-or-name> --file <path> --assets <dir>\` instead.`,
|
|
2047
|
-
true,
|
|
2048
|
-
);
|
|
2049
|
-
}
|
|
2050
|
-
let uploadId;
|
|
2051
|
-
try {
|
|
2052
|
-
uploadId = normalizeUploadId(clientUploadId) || newHtmlUploadId();
|
|
2053
|
-
} catch (err) {
|
|
2054
|
-
return textResult(`Error: ${err?.message ?? String(err)}`, true);
|
|
2055
|
-
}
|
|
2056
|
-
const form = new FormData();
|
|
2057
|
-
form.append("html", html);
|
|
2058
|
-
form.append("source", source ?? "agent");
|
|
2059
|
-
form.append("client_upload_id", uploadId);
|
|
2060
|
-
for (const a of assets) {
|
|
2061
|
-
const bytes = Buffer.from(a.base64, "base64");
|
|
2062
|
-
form.append(
|
|
2063
|
-
`asset:${a.path}`,
|
|
2064
|
-
new Blob([bytes], { type: a.contentType || "application/octet-stream" }),
|
|
2065
|
-
a.path,
|
|
2066
|
-
);
|
|
2067
|
-
}
|
|
2068
|
-
const headers = await getAuthHeaders();
|
|
2069
|
-
const res = await fetch(`${getBaseUrl()}${htmlUrl(dealId, documentSlug)}`, {
|
|
2070
|
-
method: "PUT",
|
|
2071
|
-
headers: { ...headers, "X-Llama-Upload-Id": uploadId }, // let fetch set multipart Content-Type with boundary
|
|
2072
|
-
body: form,
|
|
2073
|
-
});
|
|
2074
|
-
const body = await res.json().catch(() => ({}));
|
|
2075
|
-
if (!res.ok) {
|
|
2076
|
-
throw new Error(
|
|
2077
|
-
`HTTP ${res.status}: ${body?.error || JSON.stringify(body).slice(0, 300)}`,
|
|
2078
|
-
);
|
|
2079
|
-
}
|
|
2080
|
-
return body;
|
|
2081
|
-
},
|
|
2082
|
-
);
|
|
2083
|
-
|
|
2084
|
-
server.registerTool(
|
|
2085
|
-
"html_reset",
|
|
2086
|
-
{
|
|
2087
|
-
description:
|
|
2088
|
-
"Soft-delete the latest HTML version for a deal. The /browse " +
|
|
2089
|
-
"page reverts to its empty state (drop / paste / CLI / agent " +
|
|
2090
|
-
"invitation). Old versions are retained and restorable via " +
|
|
2091
|
-
"html_restore.",
|
|
2092
|
-
inputSchema: {
|
|
2093
|
-
dealId: z.string().describe("deal uuid"),
|
|
2094
|
-
documentSlug: z.string().optional().describe("default: 'main'"),
|
|
2095
|
-
},
|
|
2096
|
-
},
|
|
2097
|
-
async ({ dealId, documentSlug }) =>
|
|
2098
|
-
callApi("DELETE", htmlUrl(dealId, documentSlug))
|
|
2099
|
-
);
|
|
2100
|
-
|
|
2101
|
-
// ============================================================
|
|
2102
|
-
// Prompts
|
|
2103
|
-
// ============================================================
|
|
2104
|
-
//
|
|
2105
|
-
// MCP-native agents discover prompts via prompts/list — they can fetch
|
|
2106
|
-
// and adopt them without any user-side prompt engineering.
|
|
2107
|
-
|
|
2108
|
-
server.registerPrompt(
|
|
2109
|
-
"agent_briefing",
|
|
2110
|
-
{
|
|
2111
|
-
description:
|
|
2112
|
-
"Onboard yourself as a Llama Ventures teammate. Returns the workflow " +
|
|
2113
|
-
"contract: identity, Pipeline First rule, content capture, autonomy " +
|
|
2114
|
-
"levels (L0/L1/L2/L3), communication style, error recovery, CLI/MCP " +
|
|
2115
|
-
"reference, and boundaries. Read this once, internalise it, operate " +
|
|
2116
|
-
"accordingly. Same content as `llama agent-onboard` from the CLI. " +
|
|
2117
|
-
"For the live private Llama OS skill library, call agent_bootstrap, " +
|
|
2118
|
-
"skills_search, and skills_read.",
|
|
2119
|
-
},
|
|
2120
|
-
async () => {
|
|
2121
|
-
// Gate the briefing behind authenticated Command runtime. The server-owned
|
|
2122
|
-
// /api/agent/briefing contract is canonical; bundled AGENT_BRIEFING.md is
|
|
2123
|
-
// only a rollout/offline fallback for authenticated users.
|
|
2124
367
|
const headers = await getAuthHeaders();
|
|
2125
|
-
let
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
stub =
|
|
2129
|
-
"Llama Ventures team onboarding requires credentials.\n\n" +
|
|
2130
|
-
"Team member: run `gcloud auth login` with your @llamaventures.vc " +
|
|
2131
|
-
"account, or mint a token at " +
|
|
2132
|
-
"https://command.llamaventures.vc/settings/tokens and run " +
|
|
2133
|
-
"`llama token set <llc_...>`. Then re-request this prompt.\n\n" +
|
|
2134
|
-
"Founder / external visitor: use the `pitch_*` tools — no token required.";
|
|
368
|
+
let text;
|
|
369
|
+
if (!Object.keys(headers).length) {
|
|
370
|
+
text = "Llama team onboarding requires credentials. Run `llama auth login`; external founders use pitch_* tools.";
|
|
2135
371
|
} else {
|
|
2136
372
|
try {
|
|
2137
373
|
const params = new URLSearchParams({ clientVersion: PKG_VERSION });
|
|
2138
|
-
const
|
|
2139
|
-
|
|
2140
|
-
} catch (
|
|
2141
|
-
|
|
2142
|
-
if (msg.includes("Error[UNAUTHORIZED]") || msg.includes("Error[NO_AUTH]")) {
|
|
2143
|
-
stub =
|
|
2144
|
-
"Llama Ventures team onboarding requires valid credentials. " +
|
|
2145
|
-
"Server rejected the credentials we sent. Re-mint at " +
|
|
2146
|
-
"https://command.llamaventures.vc/settings/tokens.";
|
|
2147
|
-
} else {
|
|
2148
|
-
briefing =
|
|
2149
|
-
"Warning: server agent briefing unavailable; using bundled fallback.\n\n" +
|
|
2150
|
-
readBriefing();
|
|
2151
|
-
}
|
|
374
|
+
const response = await request("GET", `/api/agent/briefing?${params}`);
|
|
375
|
+
text = response?.briefing || readBriefing();
|
|
376
|
+
} catch (error) {
|
|
377
|
+
text = `Warning: live briefing unavailable (${error?.message ?? String(error)}).\n\n${readBriefing()}`;
|
|
2152
378
|
}
|
|
2153
379
|
}
|
|
2154
|
-
return {
|
|
2155
|
-
|
|
2156
|
-
{
|
|
2157
|
-
role: "user",
|
|
2158
|
-
content: { type: "text", text: stub ?? briefing ?? readBriefing() },
|
|
2159
|
-
},
|
|
2160
|
-
],
|
|
2161
|
-
};
|
|
2162
|
-
}
|
|
380
|
+
return { messages: [{ role: "user", content: { type: "text", text } }] };
|
|
381
|
+
},
|
|
2163
382
|
);
|
|
2164
383
|
|
|
2165
|
-
|
|
2166
|
-
// Boot
|
|
2167
|
-
// ============================================================
|
|
2168
|
-
|
|
2169
|
-
const transport = new StdioServerTransport();
|
|
2170
|
-
await server.connect(transport);
|
|
384
|
+
await server.connect(new StdioServerTransport());
|