@llamaventures/cli 1.4.0 → 1.4.2

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/bin/llama-mcp.mjs CHANGED
@@ -40,6 +40,34 @@ async function callApi(method, path, body) {
40
40
  }
41
41
  }
42
42
 
43
+ // Append a block to a deal brief. The /blocks route only accepts atomic
44
+ // full-array PUTs (no POST), so we GET current blocks, prepend the new
45
+ // one (matches UI default since 2026-05-03), and PUT the merged array.
46
+ // Server stamps identity meta on PUT; we don't send any.
47
+ async function addBriefBlock(dealId, block) {
48
+ try {
49
+ const id = globalThis.crypto.randomUUID();
50
+ const cur = await request("GET", `/api/deals/${encodeURIComponent(dealId)}/blocks`);
51
+ const existing = Array.isArray(cur?.blocks) ? cur.blocks : [];
52
+ const result = await request(
53
+ "PUT",
54
+ `/api/deals/${encodeURIComponent(dealId)}/blocks`,
55
+ { blocks: [{ id, ...block }, ...existing] }
56
+ );
57
+ const text = JSON.stringify(
58
+ { ok: result?.ok ?? true, id, count: result?.count ?? existing.length + 1 },
59
+ null,
60
+ 2
61
+ );
62
+ return { content: [{ type: "text", text }] };
63
+ } catch (err) {
64
+ return {
65
+ content: [{ type: "text", text: `Error: ${err?.message ?? String(err)}` }],
66
+ isError: true,
67
+ };
68
+ }
69
+ }
70
+
43
71
  const server = new McpServer({
44
72
  name: "llama-mcp",
45
73
  version: PKG_VERSION,
@@ -202,7 +230,7 @@ server.registerTool(
202
230
  "brief_add_text",
203
231
  {
204
232
  description:
205
- "Append a markdown text block to a deal brief. Supports markdown + mermaid diagrams.",
233
+ "Prepend a markdown text block to a deal brief. Supports markdown + mermaid diagrams.",
206
234
  inputSchema: {
207
235
  dealId: z.string(),
208
236
  heading: z.string().optional().describe("optional block heading"),
@@ -210,18 +238,14 @@ server.registerTool(
210
238
  },
211
239
  },
212
240
  async ({ dealId, heading, body }) =>
213
- callApi("POST", `/api/deals/${encodeURIComponent(dealId)}/blocks`, {
214
- type: "text",
215
- heading,
216
- body,
217
- })
241
+ addBriefBlock(dealId, { type: "text", heading: heading ?? "", body })
218
242
  );
219
243
 
220
244
  server.registerTool(
221
245
  "brief_add_link",
222
246
  {
223
247
  description:
224
- "Append a link block to a deal brief. Server fetches og:image + title via /api/link-preview.",
248
+ "Prepend a link block to a deal brief. Server fetches og:image + title via /api/link-preview.",
225
249
  inputSchema: {
226
250
  dealId: z.string(),
227
251
  url: z.string(),
@@ -229,18 +253,14 @@ server.registerTool(
229
253
  },
230
254
  },
231
255
  async ({ dealId, url, label }) =>
232
- callApi("POST", `/api/deals/${encodeURIComponent(dealId)}/blocks`, {
233
- type: "link",
234
- url,
235
- label,
236
- })
256
+ addBriefBlock(dealId, { type: "link", url, label: label ?? "" })
237
257
  );
238
258
 
239
259
  server.registerTool(
240
260
  "brief_add_callout",
241
261
  {
242
262
  description:
243
- "Append a callout block to a deal brief. Use for emphasized insights or warnings.",
263
+ "Prepend a callout block to a deal brief. Use for emphasized insights or warnings.",
244
264
  inputSchema: {
245
265
  dealId: z.string(),
246
266
  tone: z.string().describe("insight | warning | info | success"),
@@ -249,12 +269,7 @@ server.registerTool(
249
269
  },
250
270
  },
251
271
  async ({ dealId, tone, heading, body }) =>
252
- callApi("POST", `/api/deals/${encodeURIComponent(dealId)}/blocks`, {
253
- type: "callout",
254
- tone,
255
- heading,
256
- body,
257
- })
272
+ addBriefBlock(dealId, { type: "callout", tone, heading: heading ?? "", body })
258
273
  );
259
274
 
260
275
  // ============================================================
@@ -279,15 +294,23 @@ server.registerTool(
279
294
  {
280
295
  description:
281
296
  "Create or update a wiki page. Content should be markdown with attribution " +
282
- "blocks (**[Name · YYYY-MM-DD · source · fact|opinion]**) for traceability.",
297
+ "blocks (**[Name · YYYY-MM-DD · source · fact|opinion]**) for traceability. " +
298
+ "`sources` is a separate citation list (URLs, doc names, or meeting references) " +
299
+ "— at least one is required; URLs embedded inside `content` do not count.",
283
300
  inputSchema: {
284
301
  slug: z.string().describe("kebab-case slug"),
285
302
  title: z.string(),
286
303
  content: z.string().describe("markdown content"),
304
+ sources: z
305
+ .array(z.string())
306
+ .min(1)
307
+ .describe(
308
+ "citation list — URLs, doc names, or meeting references. At least one required."
309
+ ),
287
310
  },
288
311
  },
289
- async ({ slug, title, content }) =>
290
- callApi("POST", "/api/wiki/save", { slug, title, content })
312
+ async ({ slug, title, content, sources }) =>
313
+ callApi("POST", "/api/wiki/save", { slug, title, content, sources })
291
314
  );
292
315
 
293
316
  // ============================================================
package/lib/external.mjs CHANGED
@@ -317,6 +317,40 @@ function guessMimeType(filename) {
317
317
  return ALLOWED_MIME_BY_EXT[ext] || "application/octet-stream";
318
318
  }
319
319
 
320
+ // Paths the upload helper refuses to read. The intent is "user must point
321
+ // at a real, intentional document" — symlinks and well-known config
322
+ // directories are rejected so an automated caller cannot ferry an
323
+ // unintended file into the upload by handing over a misleading path.
324
+ const DISALLOWED_PATH_PREFIXES = [
325
+ ".ssh",
326
+ ".llama",
327
+ ".aws",
328
+ ".config/gcloud",
329
+ ".gnupg",
330
+ ".kube",
331
+ ".docker",
332
+ ];
333
+
334
+ function assertSafeUploadPath(filePath) {
335
+ const lstat = fs.lstatSync(filePath);
336
+ if (lstat.isSymbolicLink()) {
337
+ throw new Error(
338
+ "Upload path is a symbolic link; pass the real file path instead."
339
+ );
340
+ }
341
+ if (!lstat.isFile()) {
342
+ throw new Error("Upload path is not a regular file.");
343
+ }
344
+ const resolved = fs.realpathSync(filePath);
345
+ const home = os.homedir();
346
+ for (const prefix of DISALLOWED_PATH_PREFIXES) {
347
+ const denied = path.join(home, prefix);
348
+ if (resolved === denied || resolved.startsWith(denied + path.sep)) {
349
+ throw new Error("Upload path is not allowed.");
350
+ }
351
+ }
352
+ }
353
+
320
354
  export async function uploadExternalFile(filePath) {
321
355
  const session = readExternalSession();
322
356
  if (!session) {
@@ -329,6 +363,8 @@ export async function uploadExternalFile(filePath) {
329
363
  throw new Error(`File not found: ${filePath}`);
330
364
  }
331
365
 
366
+ assertSafeUploadPath(filePath);
367
+
332
368
  const fileData = fs.readFileSync(filePath);
333
369
  const filename = path.basename(filePath);
334
370
  const mimetype = guessMimeType(filename);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llamaventures/cli",
3
- "version": "1.4.0",
3
+ "version": "1.4.2",
4
4
  "description": "CLI + MCP server for the Llama Ventures investment workbench (command.llamaventures.vc).",
5
5
  "type": "module",
6
6
  "bin": {