@llamaventures/cli 1.13.0 → 1.14.1

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/SECURITY.md ADDED
@@ -0,0 +1,62 @@
1
+ # Security Policy
2
+
3
+ ## Reporting security issues
4
+
5
+ **Do not file public GitHub issues for security bugs.** A public issue
6
+ gives an attacker a head-start before we can patch.
7
+
8
+ Instead, email [**gavin@llamaventures.vc**](mailto:gavin@llamaventures.vc)
9
+ with:
10
+
11
+ - A short description of the issue
12
+ - Steps to reproduce
13
+ - Expected vs. actual behavior
14
+ - The CLI version (`npm ls -g @llamaventures/cli`)
15
+ - Any other context that helps us reproduce
16
+
17
+ We aim to acknowledge within ~5 push cycles (best effort), patch
18
+ promptly, and credit the reporter (if desired) once the fix ships.
19
+ We do not run a public bug bounty program.
20
+
21
+ ## Scope
22
+
23
+ In scope:
24
+
25
+ - Authentication / authorization bypass in the CLI client
26
+ - Token handling vulnerabilities (logging, transmission, storage)
27
+ - Supply-chain attacks against the npm package or its dependencies
28
+ - Anything that lets an unauthorized party execute Llama Command
29
+ operations as another team member
30
+
31
+ Out of scope (still report; lower urgency):
32
+
33
+ - Bugs in third-party services we depend on (Google Cloud, npm,
34
+ Anthropic, etc.)
35
+ - Issues only reproducible against a fork or a modified version of
36
+ this CLI
37
+
38
+ Server-side bugs (`command.llamaventures.vc`) are governed separately;
39
+ report through the same channel.
40
+
41
+ ## Supply-chain posture
42
+
43
+ This package follows current best practice:
44
+
45
+ - **`@llamaventures/cli` is published via npm
46
+ [Trusted Publishers](https://docs.npmjs.com/trusted-publishers)** —
47
+ no `NPM_TOKEN` is stored in CI secrets; the GitHub Action exchanges
48
+ an OIDC token at publish time.
49
+ - **All releases ship with `--provenance`** (sigstore-signed). The
50
+ npm registry shows a "Provenance" badge on each version, traceable
51
+ to the exact commit + GitHub Action workflow that built it.
52
+ - **Minimal dependency tree.** The CLI itself is zero-deps; the
53
+ bundled MCP server depends only on
54
+ `@modelcontextprotocol/sdk` (Anthropic-maintained, pinned exact).
55
+ - **Branch protection** on `main` prevents force-push and deletion;
56
+ Dependabot, secret scanning, and push-protection are enabled.
57
+
58
+ ## Disclosure
59
+
60
+ We coordinate disclosure when a fix lands. Once a patched version
61
+ ships, we publish a security advisory on this repo and notify
62
+ affected users via the team's internal channels.
@@ -0,0 +1,25 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 224 76.32">
3
+ <g>
4
+ <g>
5
+ <path d="M69.84,26.89h7.64v4.53h-13.48V7.33h5.84v19.56Z"/>
6
+ <path d="M86.13,7.33v24.04h-5.7V7.33h5.7Z"/>
7
+ <path d="M90.96,16.53c.74-1.5,1.76-2.65,3.04-3.45,1.28-.81,2.72-1.21,4.3-1.21,1.35,0,2.54.28,3.56.83s1.81,1.28,2.36,2.17v-2.73h5.88v19.26h-5.88v-2.73c-.57.9-1.37,1.62-2.39,2.17-1.02.55-2.21.83-3.56.83-1.56,0-2.98-.41-4.27-1.22-1.29-.82-2.3-1.98-3.04-3.49-.75-1.51-1.12-3.25-1.12-5.23s.37-3.72,1.12-5.21ZM103,18.29c-.81-.85-1.81-1.28-2.97-1.28s-2.16.42-2.98,1.26c-.81.84-1.22,2-1.22,3.47s.41,2.64,1.22,3.51c.81.86,1.81,1.29,2.98,1.29s2.16-.42,2.97-1.28c.81-.85,1.22-2.01,1.22-3.49s-.41-2.64-1.22-3.49Z"/>
8
+ <path d="M143.78,14.24c1.4,1.44,2.1,3.44,2.1,6v11.18h-5.82v-10.39c0-1.24-.32-2.19-.97-2.86-.65-.68-1.54-1.01-2.68-1.01s-2.04.34-2.68,1.01c-.65.67-.97,1.63-.97,2.86v10.39h-5.82v-10.39c0-1.24-.32-2.19-.97-2.86-.65-.68-1.54-1.01-2.68-1.01s-2.03.34-2.68,1.01c-.65.67-.98,1.63-.98,2.86v10.39h-5.85V12.28h5.85v2.4c.59-.8,1.37-1.43,2.33-1.9.96-.47,2.04-.7,3.25-.7,1.44,0,2.72.31,3.85.92,1.13.62,2.01,1.5,2.65,2.64.66-1.05,1.56-1.91,2.7-2.57,1.14-.66,2.38-.99,3.72-.99,2.37,0,4.26.72,5.66,2.16Z"/>
9
+ <path d="M149.87,16.6c.74-1.49,1.75-2.63,3.03-3.44s2.71-1.2,4.28-1.2c1.35,0,2.53.27,3.55.82,1.01.55,1.8,1.27,2.34,2.16v-2.72h5.86v19.17h-5.86v-2.72c-.57.89-1.36,1.62-2.38,2.16-1.01.55-2.2.82-3.54.82-1.56,0-2.97-.41-4.25-1.22s-2.29-1.97-3.03-3.47c-.74-1.5-1.11-3.24-1.11-5.21s.37-3.7,1.11-5.19ZM161.86,18.36c-.81-.85-1.8-1.27-2.96-1.27s-2.15.42-2.96,1.25c-.81.84-1.22,1.99-1.22,3.45s.41,2.63,1.22,3.49c.81.86,1.8,1.29,2.96,1.29s2.15-.42,2.96-1.27,1.22-2,1.22-3.47-.4-2.62-1.22-3.47Z"/>
10
+ </g>
11
+ <g>
12
+ <path d="M88.31,42.57l-8.62,24.05-7.19-.14-8.5-23.91h6.3l5.57,17.41,6.27-17.41h6.17Z"/>
13
+ <path d="M106.78,57.98l-13.12.07c.09,1.16.46,2.04,1.12,2.65.66.61,1.46.92,2.42.92,1.42,0,2.41-.6,2.97-1.8h6.17c-.31,1.21-.86,2.45-1.67,3.42-.81.98-1.87,1.77-3.09,2.33s-2.73.9-4.24.9c-1.82,0-3.79-.59-5.21-1.37-1.42-.78-2.42-2.01-3.22-3.45-.8-1.44-1.2-3.11-1.2-5.04s.58-4,1.37-5.45c.79-1.45,2.09-2.48,3.51-3.26,1.42-.78,3.21-1.15,5.06-1.15s3.43.55,4.83,1.31c1.4.75,2.56,2.1,3.35,3.5s1.07,2.89,1.07,4.76c0,.53-.03,1.09-.1,1.67ZM100.83,54.89c0-.98-.33-1.76-1-2.33-.67-.58-1.5-.87-2.5-.87s-1.76.28-2.42.84c-.66.56-1.06,1.34-1.22,2.37h7.13Z"/>
14
+ <path d="M126.7,49.22c1.3,1.41,2.3,3.49,2.3,5.96v11.44h-5.78s-.04-10.69-.04-10.69c0-1.25-.49-2.07-1.13-2.76-.65-.69-1.54-1.06-2.81-1-1.09.05-1.89.57-2.54,1.26s-.91,1.61-.91,2.86v10.32h-5.79v-19.13h5.79v2.47c.58-.82,1.17-1.47,2.15-1.95.98-.48,2.08-.72,3.3-.72,2.18,0,4.17.53,5.47,1.94Z"/>
15
+ <path d="M143.61,61.6v5.02h-2.9c-2.06,0-4.1-.8-5.25-1.81-1.16-1.01-1.43-3.02-1.43-5.31v-7.1s-2.39,0-2.39,0v-4.91h2.39v-4.66s5.79,0,5.79,0v4.66h3.79v4.91h-3.78v7.2c0,.56.15,1.41.41,1.65.27.24.71.37,1.33.37h2.03Z"/>
16
+ <path d="M165.77,47.34v19.01h-5.79l-.03-2.74c-.58.82-1.77,1.75-2.76,2.25-.99.5-2.08.75-3.28.75-1.42,0-2.68-.32-3.77-.95-1.09-.63-1.88-2.02-2.48-3.21-.6-1.2-.9-2.61-.9-4.24v-10.87h5.67v10.1c0,1.24.32,2.21.97,2.9.64.69,1.51,1.03,2.6,1.03s1.99-.34,2.63-1.03c.64-.69.97-1.66.97-2.9v-10.1h6.17Z"/>
17
+ <path d="M178.26,48.17c1-.59,2.41-.82,3.64-.82v6.17h-1.85c-1.42,0-2.38.33-3.09.94-.71.61-1.11,1.55-1.11,3.09v9.06h-5.79v-19.13h5.7v3.1c.67-1.02,1.5-1.83,2.5-2.42Z"/>
18
+ <path d="M202.59,58.03h-13.25c.09,1.19.47,2.09,1.15,2.71.67.63,1.5.94,2.48.94,1.46,0,2.47-.61,3.05-1.84h6.23c-.32,1.25-.9,2.38-1.73,3.38-.83,1-1.88,1.79-3.13,2.36-1.26.57-2.66.85-4.21.85-1.87,0-3.54-.4-5-1.19-1.46-.79-2.6-1.93-3.42-3.41-.82-1.48-1.23-3.21-1.23-5.19s.4-3.71,1.21-5.19c.81-1.48,1.95-2.62,3.41-3.41,1.46-.8,3.14-1.19,5.03-1.19s3.49.39,4.93,1.16c1.44.77,2.56,1.88,3.37,3.31s1.21,3.11,1.21,5.02c0,.55-.03,1.12-.1,1.71ZM196.7,54.79c0-1-.34-1.8-1.03-2.39-.68-.59-1.54-.89-2.57-.89s-1.81.29-2.48.86c-.67.57-1.09,1.37-1.25,2.42h7.33Z"/>
19
+ <path d="M106.77,58.03h-13.25c.09,1.19.47,2.09,1.15,2.71.67.63,1.5.94,2.48.94,1.46,0,2.47-.61,3.05-1.84h6.23c-.32,1.25-.9,2.38-1.73,3.38-.83,1-1.88,1.79-3.13,2.36-1.26.57-2.66.85-4.21.85-1.87,0-3.54-.4-5-1.19-1.46-.79-2.6-1.93-3.42-3.41-.82-1.48-1.23-3.21-1.23-5.19s.4-3.71,1.21-5.19c.81-1.48,1.95-2.62,3.41-3.41,1.46-.8,3.14-1.19,5.03-1.19s3.49.39,4.93,1.16c1.44.77,2.56,1.88,3.37,3.31s1.21,3.11,1.21,5.02c0,.55-.03,1.12-.1,1.71ZM100.88,54.79c0-1-.34-1.8-1.03-2.39-.68-.59-1.54-.89-2.57-.89s-1.81.29-2.48.86c-.67.57-1.09,1.37-1.25,2.42h7.33Z"/>
20
+ <path d="M209.1,65.57c-1.28-.57-2.29-1.35-3.03-2.35-.74-.99-1.15-2.11-1.24-3.34h5.68c.07.66.37,1.2.91,1.61.54.41,1.2.62,1.98.62.72,0,1.27-.14,1.66-.43.39-.28.59-.66.59-1.11,0-.55-.28-.95-.84-1.22-.56-.26-1.47-.56-2.72-.88-1.34-.32-2.47-.66-3.36-1.01-.9-.35-1.67-.92-2.32-1.68-.65-.76-.98-1.8-.98-3.1,0-1.1.3-2.1.89-3,.59-.9,1.47-1.62,2.62-2.14,1.15-.53,2.53-.79,4.12-.79,2.35,0,4.21.59,5.56,1.78,1.36,1.19,2.14,2.77,2.34,4.73h-5.31c-.09-.66-.37-1.19-.86-1.58-.48-.39-1.12-.58-1.9-.58-.67,0-1.19.13-1.55.39s-.54.62-.54,1.08c0,.55.29.96.86,1.23.57.28,1.46.55,2.67.82,1.39.37,2.52.73,3.4,1.08.87.35,1.64.93,2.3,1.72.66.79,1,1.84,1.02,3.17,0,1.12-.31,2.12-.92,3-.62.88-1.5,1.57-2.65,2.07-1.15.5-2.5.75-4.02.75-1.64,0-3.09-.28-4.37-.86Z"/>
21
+ </g>
22
+ </g>
23
+ <path d="M48.7,50.5c3.49.32,6.76,2.94,8.15,6.08,3.09,6.98-1.33,14.02-8.65,15.07-9.08-.47-18.69.74-27.71.12-6.65-.46-10.81-5.53-10.25-12.13l6.28-25.63c-2.05-.02-4.19-.55-6.11-1.26-2.04-.75-4.19-1.79-5.33-3.74-3.04-5.19,1.25-9.8,5.21-12.75-1.81-4.39,2.75-6.87,6.59-6.06.9-3.1,4.51-4.16,7.01-2.12.61.5,1.37,2.23,2.32,1.05.83-1.03,1.11-3.35,1.02-4.64-.06-.81-.79-2.28.57-2.34.91-.04,2.41,1.95,2.85,2.69,1.48,2.49,1.77,5.72.61,8.39,2.83-.4,5.16-1.93,6.73-4.29.48-.72.81-2.17,1.87-1.85.59.18.96,2.01,1.06,2.59.65,3.9-1.25,7.48-4.51,9.55.65,2.66,1.28,5.32,1.63,8.05.99,7.74.41,15.67-1.62,23.2h12.28v.02ZM26.67,18.33c-4.94-4.2-11.7-1.55-15.83,2.4-.27.26-1.14,1.11-1.26,1.39-.09.21.08.17.16.21.66.39,1.73.61,2.01,1.39.47,1.33-.45,2.61-1.85,2.49-.42-.04-1.91-.89-2.08-.76.18,1.93,1.71,2.95,3.37,3.62,3.11,1.24,6.67,1.29,9.98,1.09l.26.14-3.65,14.92c10.85-2.11,18.01-19.14,8.89-26.88h0Z"/>
24
+ <path d="M23.15,21.22c1.95,1.35-.13,4.72-2.34,3.28s-.04-4.95,2.34-3.28Z"/>
25
+ </svg>
package/bin/llama-mcp.mjs CHANGED
@@ -11,7 +11,7 @@ import { createRequire } from "module";
11
11
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
12
12
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
13
13
  import { z } from "zod";
14
- import { getAuthHeaders, readBriefing, request } from "../lib/client.mjs";
14
+ import { getAuthHeaders, readBriefing, request, requestSse } from "../lib/client.mjs";
15
15
 
16
16
  const requireFromHere = createRequire(import.meta.url);
17
17
  const { version: PKG_VERSION } = requireFromHere("../package.json");
@@ -40,6 +40,96 @@ async function callApi(method, path, body) {
40
40
  }
41
41
  }
42
42
 
43
+ function textResult(text, isError = false) {
44
+ return {
45
+ content: [{ type: "text", text }],
46
+ ...(isError ? { isError: true } : {}),
47
+ };
48
+ }
49
+
50
+ function splitSources(value) {
51
+ if (Array.isArray(value)) return value.filter(Boolean);
52
+ if (!value || value === true) return undefined;
53
+ return String(value)
54
+ .split(",")
55
+ .map((s) => s.trim())
56
+ .filter(Boolean);
57
+ }
58
+
59
+ function buildEnrichmentAgentMessage(args = {}) {
60
+ if (args.message) return String(args.message);
61
+ const sources = splitSources(args.sources) ?? [
62
+ "website",
63
+ "github",
64
+ "linkedin",
65
+ "yc",
66
+ "launch",
67
+ "web",
68
+ "monid",
69
+ ];
70
+ const budget = args.budgetCents ?? "50";
71
+ const memo = args.generateMemo
72
+ ? "Generate memo only after enrichment because the caller explicitly requested it."
73
+ : "Do not generate memo.";
74
+ return [
75
+ "Run server-side deal enrichment for this deal.",
76
+ `Use sources: ${sources.join(", ")}.`,
77
+ `Private Monid budget cap: ${budget} cents.`,
78
+ "Read the enrichment harness first, then collect current company/founder evidence.",
79
+ "Write canonical evidence links, sourced deal facts, stable deal fields, and typed factual values where supported.",
80
+ "For typed factual values, call read_typed_factual_layer first and use upsert_typed_fact for queryable fields.",
81
+ "Search snippets alone are not high-confidence evidence; fetch direct sources where possible.",
82
+ memo,
83
+ "End with what was written, what was skipped, and open questions.",
84
+ ].join(" ");
85
+ }
86
+
87
+ function summarizeAgentEvents(events = []) {
88
+ return events
89
+ .flatMap((event) => {
90
+ if (event.tool_use?.name) return [{ type: "tool_use", name: event.tool_use.name }];
91
+ if (event.tool_result?.name) {
92
+ return [
93
+ {
94
+ type: "tool_result",
95
+ name: event.tool_result.name,
96
+ ok: event.tool_result.ok ?? null,
97
+ summary: event.tool_result.summary ?? null,
98
+ },
99
+ ];
100
+ }
101
+ if (event.error) return [{ type: "error", error: String(event.error) }];
102
+ return [];
103
+ })
104
+ .slice(-80);
105
+ }
106
+
107
+ async function runDealAgentTool({ dealId, message, title = "MCP agent run" }) {
108
+ try {
109
+ const thread = await request("POST", `/api/deals/${encodeURIComponent(dealId)}/threads`, { title });
110
+ if (!thread?.id) throw new Error("Thread creation did not return an id");
111
+ const result = await requestSse(
112
+ "POST",
113
+ `/api/deals/${encodeURIComponent(dealId)}/threads/${encodeURIComponent(thread.id)}`,
114
+ { message },
115
+ );
116
+ return textResult(
117
+ JSON.stringify(
118
+ {
119
+ ok: true,
120
+ threadId: thread.id,
121
+ text: result.text,
122
+ toolEvents: summarizeAgentEvents(result.events),
123
+ },
124
+ null,
125
+ 2,
126
+ ),
127
+ );
128
+ } catch (err) {
129
+ return textResult(`Error: ${err?.message ?? String(err)}`, true);
130
+ }
131
+ }
132
+
43
133
  // Append a block to a deal brief. The /blocks route only accepts atomic
44
134
  // full-array PUTs (no POST), so we GET current blocks, prepend the new
45
135
  // one (matches UI default since 2026-05-03), and PUT the merged array.
@@ -786,6 +876,88 @@ server.registerTool(
786
876
  callApi("POST", `/api/deals/${encodeURIComponent(dealId)}/refresh-persona`, { persona })
787
877
  );
788
878
 
879
+ server.registerTool(
880
+ "deal_agent_run",
881
+ {
882
+ description:
883
+ "Run Llama Command's server-side Deal Agent inside a deal thread. " +
884
+ "Use this when the user explicitly wants the service agent to execute " +
885
+ "a deal-scoped task instead of the local MCP client doing the work.",
886
+ inputSchema: {
887
+ dealId: z.string().describe("deal uuid"),
888
+ message: z.string().describe("task instruction for the server-side Deal Agent"),
889
+ title: z.string().optional().describe("optional thread title; defaults to MCP agent run"),
890
+ },
891
+ },
892
+ async ({ dealId, message, title }) =>
893
+ runDealAgentTool({ dealId, message, title: title || "MCP agent run" })
894
+ );
895
+
896
+ server.registerTool(
897
+ "deal_enrich",
898
+ {
899
+ description:
900
+ "Run the Llama Command deal enrichment planner/trigger for one deal. " +
901
+ "Default is dry-run: returns evidence plan, source plan, Monid budget/config " +
902
+ "status, and planned writes without changing facts/links/memo. With " +
903
+ "apply=true and executor=server_agent, this starts the server-side Deal " +
904
+ "Agent unless harnessOnly=true. Set apply=true only when the user " +
905
+ "explicitly wants the enrichment run recorded/applied. " +
906
+ "generateMemo never defaults on; pass true only when the user explicitly asks " +
907
+ "for Memo generation after enrichment.",
908
+ inputSchema: {
909
+ dealId: z.string().describe("deal uuid"),
910
+ dryRun: z.boolean().optional().describe("default true unless apply=true"),
911
+ apply: z.boolean().optional().describe("record/apply the enrichment intent server-side"),
912
+ executor: z
913
+ .enum(["server_agent", "external_agent", "planner"])
914
+ .optional()
915
+ .describe("who will execute the harness; server_agent starts Deal Agent when apply=true"),
916
+ sources: z
917
+ .array(z.enum(["website", "github", "linkedin", "yc", "launch", "web", "monid"]))
918
+ .optional()
919
+ .describe("source families to use; defaults to the standard enrichment set"),
920
+ budgetCents: z
921
+ .number()
922
+ .int()
923
+ .min(0)
924
+ .max(500)
925
+ .optional()
926
+ .describe("Monid spend cap for this run, in cents; default is 50 when Monid is requested"),
927
+ generateMemo: z
928
+ .boolean()
929
+ .optional()
930
+ .describe("explicitly request memo regeneration after enrichment; default false"),
931
+ harnessOnly: z
932
+ .boolean()
933
+ .optional()
934
+ .describe("when true, return/apply the enrichment harness endpoint instead of starting Deal Agent"),
935
+ message: z
936
+ .string()
937
+ .optional()
938
+ .describe("optional override instruction for the server-side Deal Agent"),
939
+ },
940
+ },
941
+ async ({ dealId, dryRun, apply, executor, sources, budgetCents, generateMemo, harnessOnly, message }) => {
942
+ const effectiveExecutor = executor ?? "server_agent";
943
+ if (apply === true && effectiveExecutor === "server_agent" && harnessOnly !== true) {
944
+ return runDealAgentTool({
945
+ dealId,
946
+ title: "MCP enrichment",
947
+ message: buildEnrichmentAgentMessage({ sources, budgetCents, generateMemo, message }),
948
+ });
949
+ }
950
+ return callApi("POST", `/api/deals/${encodeURIComponent(dealId)}/enrich`, {
951
+ dryRun,
952
+ apply,
953
+ executor: effectiveExecutor,
954
+ sources,
955
+ budgetCents,
956
+ generateMemo,
957
+ });
958
+ }
959
+ );
960
+
789
961
  // ============================================================
790
962
  // External pitch (founder intake) — no Llama Command token required
791
963
  // ============================================================
package/bin/llama.mjs CHANGED
@@ -15,6 +15,7 @@ import {
15
15
  readCanonicalToken,
16
16
  readLegacyConfig,
17
17
  request,
18
+ requestSse,
18
19
  tryGcloudIdentityToken,
19
20
  writeCanonicalToken,
20
21
  writeLegacyConfig,
@@ -199,6 +200,75 @@ async function searchDeals(q, flags) {
199
200
  return result;
200
201
  }
201
202
 
203
+ function splitCsvFlag(value) {
204
+ if (!value || value === true) return undefined;
205
+ return String(value)
206
+ .split(",")
207
+ .map((s) => s.trim())
208
+ .filter(Boolean);
209
+ }
210
+
211
+ function boolFlag(flags, ...names) {
212
+ return names.some((name) => flags[name] === true || flags[name] === "true" || flags[name] === "1");
213
+ }
214
+
215
+ function buildEnrichmentAgentMessage(flags) {
216
+ if (flags.message && flags.message !== true) return String(flags.message);
217
+ const sources = splitCsvFlag(flags.sources) ?? [
218
+ "website",
219
+ "github",
220
+ "linkedin",
221
+ "yc",
222
+ "launch",
223
+ "web",
224
+ "monid",
225
+ ];
226
+ const budget = flags["budget-cents"] || flags.budgetCents || "50";
227
+ const memo = boolFlag(flags, "memo", "generate-memo", "generateMemo")
228
+ ? "Generate memo only after enrichment because the caller explicitly requested it."
229
+ : "Do not generate memo.";
230
+ return [
231
+ "Run server-side deal enrichment for this deal.",
232
+ `Use sources: ${sources.join(", ")}.`,
233
+ `Private Monid budget cap: ${budget} cents.`,
234
+ "Read the enrichment harness first, then collect current company/founder evidence.",
235
+ "Write canonical evidence links, sourced deal facts, stable deal fields, and typed factual values where supported.",
236
+ "For typed factual values, call read_typed_factual_layer first and use upsert_typed_fact for queryable fields.",
237
+ "Search snippets alone are not high-confidence evidence; fetch direct sources where possible.",
238
+ memo,
239
+ "End with what was written, what was skipped, and open questions.",
240
+ ].join(" ");
241
+ }
242
+
243
+ async function runDealAgentViaThread(dealId, message, title = "CLI agent run") {
244
+ const thread = await request("POST", `/api/deals/${encodeURIComponent(dealId)}/threads`, { title });
245
+ if (!thread?.id) throw new Error("Thread creation did not return an id");
246
+ process.stderr.write(`Running Deal Agent in thread ${thread.id}\n`);
247
+ const result = await requestSse(
248
+ "POST",
249
+ `/api/deals/${encodeURIComponent(dealId)}/threads/${encodeURIComponent(thread.id)}`,
250
+ { message },
251
+ {
252
+ onEvent(event) {
253
+ if (event.tool_use?.name) {
254
+ process.stderr.write(`[tool] ${event.tool_use.name}\n`);
255
+ }
256
+ if (event.tool_result?.name) {
257
+ const status = event.tool_result.ok ? "ok" : "error";
258
+ process.stderr.write(
259
+ `[tool] ${event.tool_result.name}: ${status} — ${event.tool_result.summary ?? ""}\n`,
260
+ );
261
+ }
262
+ if (event.text) {
263
+ process.stdout.write(event.text);
264
+ }
265
+ },
266
+ },
267
+ );
268
+ if (result.text && !result.text.endsWith("\n")) process.stdout.write("\n");
269
+ return { thread, ...result };
270
+ }
271
+
202
272
  const HELP_FULL = `Llama Command CLI
203
273
 
204
274
  Agent onboarding (run once on first install):
@@ -235,6 +305,11 @@ Deals:
235
305
  llama deal update <dealId> sector "Developer Tools"
236
306
  llama deal update <dealId> foundedYear 2024
237
307
  llama deal update <dealId> leadInvestor "Acme Capital"
308
+ llama deal enrich <dealId> [--dry-run] [--apply] [--executor server_agent|external_agent|planner]
309
+ [--sources website,github,linkedin,yc,monid] [--budget-cents 50]
310
+ [--memo] [--prompt] [--harness-only]
311
+ dry-run returns the harness; --apply --executor server_agent runs the server Deal Agent.
312
+ llama deal agent run <dealId> --message "collect founder evidence and update typed facts"
238
313
  llama deal extra set <dealId> <key> <value> # system-admin only
239
314
  Patch one top-level key in deals.extra JSONB. Value is parsed as
240
315
  JSON when possible ('{"a":1}', 'true', '3'), else stored as a
@@ -439,7 +514,7 @@ Common:
439
514
  llama agent-onboard print the AI-agent workflow contract
440
515
 
441
516
  Command groups — run \`llama help <group>\` for that group's commands:
442
- deal create · show · feed · update · search · collaborators · links · delete
517
+ deal create · show · feed · update · enrich · search · collaborators · links · delete
443
518
  brief brief blocks: list · add · edit · history · refresh
444
519
  facts deal facts + skill corrections (the sourced, trust-rated layer)
445
520
  timeline timeline · posts · mentions
@@ -1157,6 +1232,79 @@ https://command.llamaventures.vc/settings/tokens, run
1157
1232
  return;
1158
1233
  }
1159
1234
 
1235
+ if (area === "deal" && action === "agent") {
1236
+ const sub = rest[0];
1237
+ const dealId = rest[1];
1238
+ const { flags, positional } = parseFlags(rest.slice(2), ["message"]);
1239
+ const message =
1240
+ flags.message && flags.message !== true ? String(flags.message) : positional.join(" ").trim();
1241
+ if (sub !== "run" || !dealId || !message) {
1242
+ throw new Error(`Usage: llama deal agent run <dealId> --message "what the server agent should do"`);
1243
+ }
1244
+ await runDealAgentViaThread(dealId, message, "CLI agent run");
1245
+ return;
1246
+ }
1247
+
1248
+ // ----- Deal enrichment: evidence plan + server-side enrichment trigger -----
1249
+ // The server owns Monid credentials and all write/audit behavior. CLI only
1250
+ // passes intent; default is dry-run so agents can inspect the harness before
1251
+ // creating facts/links or touching memo state.
1252
+ if (area === "deal" && action === "enrich") {
1253
+ const dealId = rest[0];
1254
+ if (!dealId) {
1255
+ throw new Error(
1256
+ "Usage: llama deal enrich <dealId> [--dry-run] [--apply] " +
1257
+ "[--executor server_agent|external_agent|planner] " +
1258
+ "[--sources website,github,linkedin,yc,monid] [--budget-cents 50] [--memo] [--prompt]"
1259
+ );
1260
+ }
1261
+ const { flags } = parseFlags(rest.slice(1), [
1262
+ "dry-run",
1263
+ "apply",
1264
+ "executor",
1265
+ "sources",
1266
+ "budget-cents",
1267
+ "memo",
1268
+ "generate-memo",
1269
+ "generateMemo",
1270
+ "prompt",
1271
+ "handoff",
1272
+ "harness-only",
1273
+ "message",
1274
+ ]);
1275
+ const sources = splitCsvFlag(flags.sources);
1276
+ const budgetCents =
1277
+ flags["budget-cents"] !== undefined && flags["budget-cents"] !== true
1278
+ ? Number(flags["budget-cents"])
1279
+ : undefined;
1280
+ const apply = boolFlag(flags, "apply");
1281
+ const executor = flags.executor && flags.executor !== true ? String(flags.executor) : "server_agent";
1282
+
1283
+ if (apply && executor === "server_agent" && !boolFlag(flags, "harness-only")) {
1284
+ await runDealAgentViaThread(dealId, buildEnrichmentAgentMessage(flags), "CLI enrichment");
1285
+ return;
1286
+ }
1287
+
1288
+ const result = await request(
1289
+ "POST",
1290
+ `/api/deals/${encodeURIComponent(dealId)}/enrich`,
1291
+ {
1292
+ dryRun: apply ? false : true,
1293
+ apply,
1294
+ executor,
1295
+ sources,
1296
+ budgetCents,
1297
+ generateMemo: boolFlag(flags, "memo", "generate-memo", "generateMemo"),
1298
+ }
1299
+ );
1300
+ if (flags.prompt === true || flags.handoff === true) {
1301
+ print(result?.agentHarness?.handoffPrompt || result?.agentHarness?.systemInjection || "");
1302
+ } else {
1303
+ print(result);
1304
+ }
1305
+ return;
1306
+ }
1307
+
1160
1308
  // ----- Collaborators (deal team — non-owner contributors) -----
1161
1309
  // Accepts --user as numeric id OR @llamaventures.vc email; emails are
1162
1310
  // resolved to id via /api/users so the CLI matches how the web picker
package/lib/client.mjs CHANGED
@@ -220,6 +220,10 @@ export async function request(method, endpoint, body) {
220
220
  return requestWithRetry(method, endpoint, body, /* allowRetry */ true);
221
221
  }
222
222
 
223
+ export async function requestSse(method, endpoint, body, opts = {}) {
224
+ return requestSseWithRetry(method, endpoint, body, opts, /* allowRetry */ true);
225
+ }
226
+
223
227
  async function requestWithRetry(method, endpoint, body, allowRetry) {
224
228
  const authHeaders = await getAuthHeaders();
225
229
  if (Object.keys(authHeaders).length === 0) throw noAuthError();
@@ -268,6 +272,80 @@ async function requestWithRetry(method, endpoint, body, allowRetry) {
268
272
  return data;
269
273
  }
270
274
 
275
+ async function requestSseWithRetry(method, endpoint, body, opts, allowRetry) {
276
+ const authHeaders = await getAuthHeaders();
277
+ if (Object.keys(authHeaders).length === 0) throw noAuthError();
278
+ const res = await fetch(`${getBaseUrl()}${endpoint}`, {
279
+ method,
280
+ headers: {
281
+ "Content-Type": "application/json",
282
+ ...authHeaders,
283
+ },
284
+ body: body === undefined ? undefined : JSON.stringify(body),
285
+ });
286
+
287
+ if (res.status === 401 && allowRetry && (await bearerCameFromOAuth())) {
288
+ let refreshed = null;
289
+ try {
290
+ const { forceRefresh } = await import("./oauth-refresh.mjs");
291
+ refreshed = await forceRefresh();
292
+ } catch {
293
+ refreshed = null;
294
+ }
295
+ if (refreshed) {
296
+ return requestSseWithRetry(method, endpoint, body, opts, /* allowRetry */ false);
297
+ }
298
+ throw unauthorizedError();
299
+ }
300
+
301
+ if (res.status === 401) throw unauthorizedError();
302
+
303
+ if (!res.ok) {
304
+ const text = await res.text();
305
+ let data;
306
+ try {
307
+ data = text ? JSON.parse(text) : null;
308
+ } catch {
309
+ data = text;
310
+ }
311
+ const message = typeof data === "object" && data?.error ? data.error : `HTTP ${res.status}`;
312
+ throw new Error(message);
313
+ }
314
+
315
+ const reader = res.body?.getReader();
316
+ const decoder = new TextDecoder();
317
+ const events = [];
318
+ let text = "";
319
+ let buf = "";
320
+ if (!reader) return { text, events };
321
+
322
+ const handleFrame = (frame) => {
323
+ const dataLine = frame.split("\n").find((line) => line.startsWith("data: "));
324
+ if (!dataLine) return;
325
+ let event;
326
+ try {
327
+ event = JSON.parse(dataLine.slice(6));
328
+ } catch {
329
+ return;
330
+ }
331
+ events.push(event);
332
+ opts.onEvent?.(event);
333
+ if (event.text) text += event.text;
334
+ if (event.error) throw new Error(String(event.error));
335
+ };
336
+
337
+ while (true) {
338
+ const { done, value } = await reader.read();
339
+ if (done) break;
340
+ buf += decoder.decode(value, { stream: true });
341
+ const frames = buf.split("\n\n");
342
+ buf = frames.pop() || "";
343
+ for (const frame of frames) handleFrame(frame);
344
+ }
345
+ if (buf.trim()) handleFrame(buf);
346
+ return { text, events };
347
+ }
348
+
271
349
  export function print(data) {
272
350
  if (typeof data === "string") {
273
351
  console.log(data);
package/package.json CHANGED
@@ -1,17 +1,27 @@
1
1
  {
2
2
  "name": "@llamaventures/cli",
3
- "version": "1.13.0",
3
+ "version": "1.14.1",
4
4
  "description": "CLI + MCP server for the Llama Ventures investment workbench (command.llamaventures.vc).",
5
5
  "type": "module",
6
+ "scripts": {
7
+ "test": "npm run test:agent-routing",
8
+ "test:agent-routing": "node scripts/verify-agent-routing.mjs"
9
+ },
6
10
  "bin": {
7
11
  "llama": "bin/llama.mjs",
8
12
  "llama-mcp": "bin/llama-mcp.mjs"
9
13
  },
10
14
  "files": [
15
+ "assets/",
11
16
  "bin/",
12
17
  "lib/",
18
+ "scripts/verify-agent-routing.mjs",
13
19
  "AGENT_BRIEFING.md",
20
+ "CHANGELOG.md",
21
+ "CONTRIBUTING.md",
14
22
  "README.md",
23
+ "README.zh-CN.md",
24
+ "SECURITY.md",
15
25
  "LICENSE"
16
26
  ],
17
27
  "engines": {