@llamaventures/cli 1.16.0 → 1.17.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.
@@ -2,14 +2,21 @@
2
2
 
3
3
  import assert from "node:assert/strict";
4
4
  import { spawn } from "node:child_process";
5
+ import { createHash } from "node:crypto";
5
6
  import { createServer } from "node:http";
6
- import { existsSync } from "node:fs";
7
- import { mkdtemp, rm } from "node:fs/promises";
7
+ import { existsSync, readFileSync } from "node:fs";
8
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
8
9
  import os from "node:os";
9
10
  import path from "node:path";
10
11
  import { fileURLToPath } from "node:url";
11
12
 
12
13
  const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
14
+ const packageJson = JSON.parse(readFileSync(path.join(repoRoot, "package.json"), "utf8"));
15
+ assert.equal(
16
+ packageJson.scripts?.["verify:release"],
17
+ "npm test && npm pack --dry-run",
18
+ "CLI release gate must run agent routing tests and npm pack dry-run",
19
+ );
13
20
  assert.equal(
14
21
  existsSync(path.join(repoRoot, "docs/agent-skills.bundle.json")),
15
22
  false,
@@ -20,9 +27,30 @@ assert.equal(
20
27
  false,
21
28
  "public llama-cli must not copy the Command-side skill mirror",
22
29
  );
30
+ const cliSource = readFileSync(path.join(repoRoot, "bin/llama.mjs"), "utf8");
31
+ assert.match(
32
+ cliSource,
33
+ /About ONE specific deal\? \.{8} llama html publish <deal-id-or-name> --file <path> --title "\.\.\."/,
34
+ "top-level help must route deal-specific HTML to the agent-safe publish path",
35
+ );
36
+ assert.doesNotMatch(
37
+ cliSource,
38
+ /For deal-specific HTML use "llama html upload <dealId>"/,
39
+ "wiki help must not route deal-specific HTML to the low-level upload path",
40
+ );
23
41
  const calls = [];
24
42
  let threadSeq = 0;
25
43
  let eventSeq = 0;
44
+ const htmlDocs = new Map();
45
+
46
+ function sha256Hex(value) {
47
+ return createHash("sha256").update(value).digest("hex");
48
+ }
49
+
50
+ function docsForDeal(dealId) {
51
+ if (!htmlDocs.has(dealId)) htmlDocs.set(dealId, new Map());
52
+ return htmlDocs.get(dealId);
53
+ }
26
54
 
27
55
  async function readJson(req) {
28
56
  let raw = "";
@@ -72,6 +100,7 @@ const server = createServer(async (req, res) => {
72
100
  agentClient: req.headers["x-llama-agent-client"] ?? null,
73
101
  session: req.headers["x-llama-agent-session"] ?? null,
74
102
  command: req.headers["x-llama-command"] ?? null,
103
+ uploadId: req.headers["x-llama-upload-id"] ?? null,
75
104
  },
76
105
  });
77
106
 
@@ -198,6 +227,104 @@ const server = createServer(async (req, res) => {
198
227
  return;
199
228
  }
200
229
 
230
+ if (req.method === "GET" && url.pathname === "/api/deals") {
231
+ writeJson(res, {
232
+ deals: [
233
+ {
234
+ uuid: "deal-html",
235
+ companyName: "Acme AI",
236
+ founders: "Ada Founder",
237
+ description: "mock deal for HTML upload tests",
238
+ },
239
+ ],
240
+ total: 1,
241
+ limit: Number(url.searchParams.get("limit") || 200),
242
+ offset: 0,
243
+ });
244
+ return;
245
+ }
246
+
247
+ const docsMatch = url.pathname.match(/^\/api\/deals\/([^/]+)\/documents$/);
248
+ if (docsMatch) {
249
+ const dealId = decodeURIComponent(docsMatch[1]);
250
+ if (dealId !== "deal-html") {
251
+ res.writeHead(404, { "Content-Type": "application/json" });
252
+ res.end(JSON.stringify({ error: "deal not found" }));
253
+ return;
254
+ }
255
+ if (req.method === "GET") {
256
+ const docs = Array.from(docsForDeal(dealId).entries()).map(([slug, doc]) => ({
257
+ slug,
258
+ title: doc.title,
259
+ latest_version: doc.version ?? null,
260
+ latest_updated_at: doc.version ? "2026-06-23T04:00:00Z" : null,
261
+ }));
262
+ writeJson(res, { documents: docs });
263
+ return;
264
+ }
265
+ if (req.method === "POST") {
266
+ const docs = docsForDeal(dealId);
267
+ const slug = body?.slug;
268
+ docs.set(slug, {
269
+ ...(docs.get(slug) || {}),
270
+ title: body?.title || slug,
271
+ });
272
+ writeJson(res, { ok: true, slug, title: body?.title || slug });
273
+ return;
274
+ }
275
+ }
276
+
277
+ const htmlMatch = url.pathname.match(/^\/api\/deals\/([^/]+)\/documents\/([^/]+)\/html$/);
278
+ if (htmlMatch) {
279
+ const dealId = decodeURIComponent(htmlMatch[1]);
280
+ const slug = decodeURIComponent(htmlMatch[2]);
281
+ const docs = docsForDeal(dealId);
282
+ if (req.method === "PUT") {
283
+ const html = typeof body?.html === "string" ? body.html : "";
284
+ const previous = docs.get(slug) || { title: slug, version: 0 };
285
+ const version = Number(previous.version || 0) + 1;
286
+ const bytes = Buffer.byteLength(html, "utf8");
287
+ const sha256 = sha256Hex(html);
288
+ docs.set(slug, {
289
+ ...previous,
290
+ html,
291
+ version,
292
+ bytes,
293
+ sha256,
294
+ source: body?.source || "cli",
295
+ client_upload_id: body?.client_upload_id || null,
296
+ });
297
+ writeJson(res, {
298
+ ok: true,
299
+ document_slug: slug,
300
+ version,
301
+ bytes,
302
+ sha256,
303
+ client_upload_id: body?.client_upload_id || null,
304
+ idempotent_replay: false,
305
+ });
306
+ return;
307
+ }
308
+ if (req.method === "GET") {
309
+ const doc = docs.get(slug);
310
+ if (!doc?.html) {
311
+ writeJson(res, { empty: true });
312
+ return;
313
+ }
314
+ writeJson(res, {
315
+ empty: false,
316
+ document_slug: slug,
317
+ version: doc.version,
318
+ bytes: doc.bytes,
319
+ sha256: doc.sha256,
320
+ source: doc.source,
321
+ created_at: "2026-06-23T04:00:00Z",
322
+ html: doc.html,
323
+ });
324
+ return;
325
+ }
326
+ }
327
+
201
328
  if (req.method === "POST" && /^\/api\/deals\/[^/]+\/threads$/.test(url.pathname)) {
202
329
  threadSeq += 1;
203
330
  writeJson(res, { id: `thread-${threadSeq}` });
@@ -495,6 +622,68 @@ try {
495
622
  messageIncludes: ["custom server task"],
496
623
  });
497
624
 
625
+ const largeHtmlPath = path.join(homeDir, "full-memo.html");
626
+ const largeHtml =
627
+ "<!doctype html><html><head><title>Full Memo</title></head><body>" +
628
+ `<p>${"agent-safe upload ".repeat(18000)}</p>` +
629
+ "</body></html>";
630
+ assert.ok(
631
+ Buffer.byteLength(largeHtml, "utf8") > 252 * 1024,
632
+ "routing test HTML must exceed the incident-sized 252KB memo",
633
+ );
634
+ await writeFile(largeHtmlPath, largeHtml);
635
+
636
+ resetCalls();
637
+ const publishRun = await runCli(
638
+ [
639
+ "html",
640
+ "publish",
641
+ "Acme AI",
642
+ "--file",
643
+ largeHtmlPath,
644
+ "--title",
645
+ "Full Memo",
646
+ "--doc",
647
+ "full-memo",
648
+ ],
649
+ baseUrl,
650
+ homeDir,
651
+ );
652
+ const publishPayload = JSON.parse(publishRun.stdout);
653
+ assert.equal(publishPayload.ok, true);
654
+ assert.equal(publishPayload.deal_uuid, "deal-html");
655
+ assert.equal(publishPayload.document_slug, "full-memo");
656
+ assert.equal(publishPayload.verified?.ok, true);
657
+ assert.match(publishPayload.client_upload_id, /^cli-[0-9a-f-]{36}$/);
658
+ assert.match(publishPayload.sha256, /^[a-f0-9]{64}$/);
659
+ assert.equal(publishPayload.verified?.sha256, publishPayload.sha256);
660
+ assert.deepEqual(paths(), [
661
+ "GET /api/deals/Acme%20AI/documents",
662
+ "GET /api/deals",
663
+ "GET /api/deals/deal-html/documents",
664
+ "POST /api/deals/deal-html/documents",
665
+ "PUT /api/deals/deal-html/documents/full-memo/html",
666
+ "GET /api/deals/deal-html/documents/full-memo/html",
667
+ ]);
668
+ assert.equal(businessCalls()[4].body?.html, largeHtml);
669
+ assert.equal(businessCalls()[4].body?.source, "cli");
670
+ assert.equal(businessCalls()[4].body?.client_upload_id, publishPayload.client_upload_id);
671
+ assert.equal(businessCalls()[4].headers.uploadId, publishPayload.client_upload_id);
672
+ const publishTelemetry = telemetryCalls().find(
673
+ (call) =>
674
+ call.body?.method === "PUT" &&
675
+ call.body?.endpoint === "/api/deals/deal-html/documents/full-memo/html",
676
+ );
677
+ assert.ok(publishTelemetry, "publish upload request must record telemetry");
678
+ assert.equal(publishTelemetry.body?.args?.html?.redacted, true);
679
+ assert.equal(publishTelemetry.body?.args?.html?.bytes, Buffer.byteLength(largeHtml, "utf8"));
680
+ assert.equal(publishTelemetry.body?.args?.html?.sha256, sha256Hex(largeHtml));
681
+ assert.doesNotMatch(
682
+ JSON.stringify(telemetryCalls()),
683
+ /agent-safe upload agent-safe upload agent-safe upload/,
684
+ "telemetry must not contain raw memo HTML text",
685
+ );
686
+
498
687
  resetCalls();
499
688
  const mcpResult = await callMcpTool(
500
689
  "deal_enrich",
@@ -518,6 +707,75 @@ try {
518
707
  assert.equal(payload.threadId, "thread-1");
519
708
  assert.equal(payload.text, "agent done");
520
709
 
710
+ resetCalls();
711
+ const inlineGuard = await callMcpTool(
712
+ "html_upload",
713
+ {
714
+ dealId: "deal-html",
715
+ documentSlug: "inline-too-large",
716
+ html: largeHtml,
717
+ },
718
+ baseUrl,
719
+ homeDir,
720
+ );
721
+ assert.equal(inlineGuard.isError, true);
722
+ assert.match(inlineGuard.content?.[0]?.text ?? "", /Use html_upload_file/);
723
+ assert.deepEqual(paths(), []);
724
+
725
+ resetCalls();
726
+ const inlineBundleGuard = await callMcpTool(
727
+ "html_upload_bundle",
728
+ {
729
+ dealId: "deal-html",
730
+ documentSlug: "bundle-too-large",
731
+ html: largeHtml,
732
+ assets: [
733
+ {
734
+ path: "full-memo_files/cover.txt",
735
+ contentType: "text/plain",
736
+ base64: Buffer.from("asset").toString("base64"),
737
+ },
738
+ ],
739
+ },
740
+ baseUrl,
741
+ homeDir,
742
+ );
743
+ assert.equal(inlineBundleGuard.isError, true);
744
+ assert.match(inlineBundleGuard.content?.[0]?.text ?? "", /Use html_upload_file/);
745
+ assert.deepEqual(paths(), []);
746
+
747
+ resetCalls();
748
+ const mcpFile = await callMcpTool(
749
+ "html_upload_file",
750
+ {
751
+ dealId: "deal-html",
752
+ documentSlug: "mcp-file",
753
+ filePath: largeHtmlPath,
754
+ },
755
+ baseUrl,
756
+ homeDir,
757
+ );
758
+ const mcpFilePayload = JSON.parse(mcpFile.content?.[0]?.text ?? "{}");
759
+ assert.equal(mcpFilePayload.ok, true);
760
+ assert.equal(mcpFilePayload.verified?.ok, true);
761
+ assert.match(mcpFilePayload.client_upload_id, /^mcp-[0-9a-f-]{36}$/);
762
+ assert.match(mcpFilePayload.sha256, /^[a-f0-9]{64}$/);
763
+ assert.equal(mcpFilePayload.verified?.sha256, mcpFilePayload.sha256);
764
+ assert.deepEqual(paths(), [
765
+ "PUT /api/deals/deal-html/documents/mcp-file/html",
766
+ "GET /api/deals/deal-html/documents/mcp-file/html",
767
+ ]);
768
+ assert.equal(businessCalls()[0].body?.client_upload_id, mcpFilePayload.client_upload_id);
769
+ assert.equal(businessCalls()[0].headers.uploadId, mcpFilePayload.client_upload_id);
770
+ const mcpFileTelemetry = telemetryCalls().find(
771
+ (call) =>
772
+ call.body?.method === "PUT" &&
773
+ call.body?.endpoint === "/api/deals/deal-html/documents/mcp-file/html",
774
+ );
775
+ assert.ok(mcpFileTelemetry, "MCP file upload request must record telemetry");
776
+ assert.equal(mcpFileTelemetry.body?.args?.html?.redacted, true);
777
+ assert.equal(mcpFileTelemetry.body?.args?.html?.sha256, sha256Hex(largeHtml));
778
+
521
779
  resetCalls();
522
780
  const mcpBootstrap = await callMcpTool("agent_bootstrap", { limit: 2 }, baseUrl, homeDir);
523
781
  const bootstrapPayload = JSON.parse(mcpBootstrap.content?.[0]?.text ?? "{}");