@llamaventures/cli 1.14.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/CHANGELOG.md +15 -0
- package/CONTRIBUTING.md +3 -2
- package/README.md +6 -3
- package/README.zh-CN.md +5 -3
- package/bin/llama-mcp.mjs +134 -8
- package/bin/llama.mjs +101 -12
- package/lib/client.mjs +78 -0
- package/package.json +6 -1
- package/scripts/verify-agent-routing.mjs +306 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,21 @@ this project adheres to [Semantic Versioning](https://semver.org).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.14.1] — 2026-06-15
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- **`llama deal agent run <dealId> --message "..."`** — starts Llama
|
|
13
|
+
Command's server-side Deal Agent in a deal thread, so the service agent can
|
|
14
|
+
execute deal-scoped work instead of the local CLI user doing it.
|
|
15
|
+
- **`deal_agent_run` MCP tool** — the same narrow server-agent trigger for
|
|
16
|
+
MCP-native clients, without adding a generic API passthrough.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- **`llama deal enrich <dealId> --apply --executor server_agent`** now starts
|
|
20
|
+
the server-side Deal Agent unless `--harness-only` is supplied. Dry-runs and
|
|
21
|
+
external-agent handoff prompts still use the enrichment harness endpoint.
|
|
22
|
+
- **`deal_enrich` MCP tool** now mirrors the same behavior with `harnessOnly`.
|
|
23
|
+
|
|
9
24
|
## [1.14.0] — 2026-06-15
|
|
10
25
|
|
|
11
26
|
### Added
|
package/CONTRIBUTING.md
CHANGED
|
@@ -13,6 +13,7 @@ npm ci
|
|
|
13
13
|
node bin/llama.mjs --help # CLI is ESM, runs straight from source
|
|
14
14
|
node --check bin/llama.mjs # syntax check
|
|
15
15
|
node --check bin/llama-mcp.mjs # syntax check
|
|
16
|
+
npm test # mock-backed CLI/MCP agent routing checks
|
|
16
17
|
```
|
|
17
18
|
|
|
18
19
|
To test the CLI end-to-end against your own credentials, point it at the
|
|
@@ -33,8 +34,8 @@ printf '%s\n' \
|
|
|
33
34
|
| node bin/llama-mcp.mjs | head -200
|
|
34
35
|
```
|
|
35
36
|
|
|
36
|
-
You should see
|
|
37
|
-
no generic API passthrough tool.
|
|
37
|
+
You should see 52 named tools, including `deal_agent_run`, `deal_enrich`, the
|
|
38
|
+
5 `pitch_*`, and no generic API passthrough tool.
|
|
38
39
|
|
|
39
40
|
## Conventions
|
|
40
41
|
|
package/README.md
CHANGED
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
```
|
|
49
49
|
@llamaventures/cli
|
|
50
50
|
├── bin/llama interactive CLI for humans + bash
|
|
51
|
-
└── bin/llama-mcp stdio MCP server,
|
|
51
|
+
└── bin/llama-mcp stdio MCP server, 52 typed tools — for any MCP-native agent
|
|
52
52
|
```
|
|
53
53
|
|
|
54
54
|
Both binaries share `lib/client.mjs` — the **same** auth chain, **same** HTTP
|
|
@@ -187,7 +187,9 @@ llama deal show <dealId>
|
|
|
187
187
|
llama deal create "Acme AI" --description "..." --source Gavin
|
|
188
188
|
llama deal update <dealId> status Diligence
|
|
189
189
|
llama deal enrich <dealId> --dry-run
|
|
190
|
+
llama deal enrich <dealId> --apply --executor server_agent
|
|
190
191
|
llama deal enrich <dealId> --executor external_agent --prompt
|
|
192
|
+
llama deal agent run <dealId> --message "collect founder evidence and update typed facts"
|
|
191
193
|
llama deal delete <dealId> # soft (audit-logged)
|
|
192
194
|
llama deal restore <dealId>
|
|
193
195
|
|
|
@@ -247,13 +249,14 @@ agents can pattern-match without parsing prose.
|
|
|
247
249
|
## MCP server
|
|
248
250
|
|
|
249
251
|
The bundled `llama-mcp` is a **stdio Model Context Protocol** server exposing
|
|
250
|
-
**
|
|
252
|
+
**52 typed tools** that mirror the most-used CLI surface. Every tool is named
|
|
251
253
|
and scoped — there is no generic API passthrough, by design (a public-package
|
|
252
254
|
escape hatch reachable from a prompt-injectable agent context is exactly the
|
|
253
255
|
shape we want to avoid).
|
|
254
256
|
|
|
255
257
|
Coverage is grouped around the workflows agents actually need: auth
|
|
256
|
-
diagnostics; deal search/show/create/update/feed; deal
|
|
258
|
+
diagnostics; deal search/show/create/update/feed; server-side deal agent runs;
|
|
259
|
+
deal enrichment harnesses;
|
|
257
260
|
trust-rated facts; brief blocks and version history; wiki read/write/delete/restore;
|
|
258
261
|
timeline posts and mentions; skill corrections; refresh triggers; external pitch intake;
|
|
259
262
|
memo show/regenerate/save/reset; and deal-scoped HTML docs, versions, bundles, and
|
package/README.zh-CN.md
CHANGED
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
```
|
|
49
49
|
@llamaventures/cli
|
|
50
50
|
├── bin/llama 给人 + bash 用的交互式 CLI
|
|
51
|
-
└── bin/llama-mcp 给 MCP 原生 agent 用的 stdio MCP server,
|
|
51
|
+
└── bin/llama-mcp 给 MCP 原生 agent 用的 stdio MCP server,52 个工具
|
|
52
52
|
```
|
|
53
53
|
|
|
54
54
|
两个 binary 共享 `lib/client.mjs`——**同一**认证链、**同一** HTTP 客户端、
|
|
@@ -208,7 +208,9 @@ llama deal show <dealId>
|
|
|
208
208
|
llama deal create "Acme AI" --description "..." --source Gavin
|
|
209
209
|
llama deal update <dealId> status Diligence
|
|
210
210
|
llama deal enrich <dealId> --dry-run
|
|
211
|
+
llama deal enrich <dealId> --apply --executor server_agent
|
|
211
212
|
llama deal enrich <dealId> --executor external_agent --prompt
|
|
213
|
+
llama deal agent run <dealId> --message "collect founder evidence and update typed facts"
|
|
212
214
|
llama deal delete <dealId> # 软删除(审计日志记录)
|
|
213
215
|
llama deal restore <dealId>
|
|
214
216
|
|
|
@@ -259,12 +261,12 @@ MCP server 在 `isError: true` 内容里返回相同的前缀,agent 不用解
|
|
|
259
261
|
## MCP server
|
|
260
262
|
|
|
261
263
|
随包发布的 `llama-mcp` 是一个 **stdio Model Context Protocol** server,
|
|
262
|
-
暴露 **
|
|
264
|
+
暴露 **52 个 typed tools**——基本镜像 CLI 最常用的命令。每个 tool 都是
|
|
263
265
|
具名、scoped 的;**没有**通用的 API passthrough,这是有意设计的(公开
|
|
264
266
|
package 里一个能被 prompt-injection 触达的逃生通道,正是我们要避开的形状)。
|
|
265
267
|
|
|
266
268
|
覆盖面按 agent 真正会用的工作流分组:auth 诊断;deal search/show/create/
|
|
267
|
-
update/feed;deal enrichment harness;带 trust ladder 的事实;brief blocks
|
|
269
|
+
update/feed;服务端 deal agent run;deal enrichment harness;带 trust ladder 的事实;brief blocks
|
|
268
270
|
和版本历史;wiki 读写删恢复;timeline posts 和 mentions;skill corrections;
|
|
269
271
|
refresh triggers;外部 pitch intake;memo show/regenerate/save/reset;
|
|
270
272
|
以及 deal-scoped HTML docs、versions、bundles、restore/reset。
|
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,14 +876,33 @@ 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
|
+
|
|
789
896
|
server.registerTool(
|
|
790
897
|
"deal_enrich",
|
|
791
898
|
{
|
|
792
899
|
description:
|
|
793
900
|
"Run the Llama Command deal enrichment planner/trigger for one deal. " +
|
|
794
901
|
"Default is dry-run: returns evidence plan, source plan, Monid budget/config " +
|
|
795
|
-
"status, and planned writes without changing facts/links/memo.
|
|
796
|
-
"
|
|
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. " +
|
|
797
906
|
"generateMemo never defaults on; pass true only when the user explicitly asks " +
|
|
798
907
|
"for Memo generation after enrichment.",
|
|
799
908
|
inputSchema: {
|
|
@@ -803,7 +912,7 @@ server.registerTool(
|
|
|
803
912
|
executor: z
|
|
804
913
|
.enum(["server_agent", "external_agent", "planner"])
|
|
805
914
|
.optional()
|
|
806
|
-
.describe("who will execute the harness;
|
|
915
|
+
.describe("who will execute the harness; server_agent starts Deal Agent when apply=true"),
|
|
807
916
|
sources: z
|
|
808
917
|
.array(z.enum(["website", "github", "linkedin", "yc", "launch", "web", "monid"]))
|
|
809
918
|
.optional()
|
|
@@ -819,17 +928,34 @@ server.registerTool(
|
|
|
819
928
|
.boolean()
|
|
820
929
|
.optional()
|
|
821
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"),
|
|
822
939
|
},
|
|
823
940
|
},
|
|
824
|
-
async ({ dealId, dryRun, apply, executor, sources, budgetCents, generateMemo }) =>
|
|
825
|
-
|
|
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`, {
|
|
826
951
|
dryRun,
|
|
827
952
|
apply,
|
|
828
|
-
executor,
|
|
953
|
+
executor: effectiveExecutor,
|
|
829
954
|
sources,
|
|
830
955
|
budgetCents,
|
|
831
956
|
generateMemo,
|
|
832
|
-
})
|
|
957
|
+
});
|
|
958
|
+
}
|
|
833
959
|
);
|
|
834
960
|
|
|
835
961
|
// ============================================================
|
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):
|
|
@@ -237,7 +307,9 @@ Deals:
|
|
|
237
307
|
llama deal update <dealId> leadInvestor "Acme Capital"
|
|
238
308
|
llama deal enrich <dealId> [--dry-run] [--apply] [--executor server_agent|external_agent|planner]
|
|
239
309
|
[--sources website,github,linkedin,yc,monid] [--budget-cents 50]
|
|
240
|
-
[--memo] [--prompt]
|
|
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"
|
|
241
313
|
llama deal extra set <dealId> <key> <value> # system-admin only
|
|
242
314
|
Patch one top-level key in deals.extra JSONB. Value is parsed as
|
|
243
315
|
JSON when possible ('{"a":1}', 'true', '3'), else stored as a
|
|
@@ -1160,6 +1232,19 @@ https://command.llamaventures.vc/settings/tokens, run
|
|
|
1160
1232
|
return;
|
|
1161
1233
|
}
|
|
1162
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
|
+
|
|
1163
1248
|
// ----- Deal enrichment: evidence plan + server-side enrichment trigger -----
|
|
1164
1249
|
// The server owns Monid credentials and all write/audit behavior. CLI only
|
|
1165
1250
|
// passes intent; default is dry-run so agents can inspect the harness before
|
|
@@ -1181,31 +1266,35 @@ https://command.llamaventures.vc/settings/tokens, run
|
|
|
1181
1266
|
"budget-cents",
|
|
1182
1267
|
"memo",
|
|
1183
1268
|
"generate-memo",
|
|
1269
|
+
"generateMemo",
|
|
1184
1270
|
"prompt",
|
|
1185
1271
|
"handoff",
|
|
1272
|
+
"harness-only",
|
|
1273
|
+
"message",
|
|
1186
1274
|
]);
|
|
1187
|
-
const sources =
|
|
1188
|
-
flags.sources && flags.sources !== true
|
|
1189
|
-
? String(flags.sources)
|
|
1190
|
-
.split(",")
|
|
1191
|
-
.map((s) => s.trim())
|
|
1192
|
-
.filter(Boolean)
|
|
1193
|
-
: undefined;
|
|
1275
|
+
const sources = splitCsvFlag(flags.sources);
|
|
1194
1276
|
const budgetCents =
|
|
1195
1277
|
flags["budget-cents"] !== undefined && flags["budget-cents"] !== true
|
|
1196
1278
|
? Number(flags["budget-cents"])
|
|
1197
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
|
+
}
|
|
1198
1287
|
|
|
1199
1288
|
const result = await request(
|
|
1200
1289
|
"POST",
|
|
1201
1290
|
`/api/deals/${encodeURIComponent(dealId)}/enrich`,
|
|
1202
1291
|
{
|
|
1203
|
-
dryRun:
|
|
1204
|
-
apply
|
|
1205
|
-
executor
|
|
1292
|
+
dryRun: apply ? false : true,
|
|
1293
|
+
apply,
|
|
1294
|
+
executor,
|
|
1206
1295
|
sources,
|
|
1207
1296
|
budgetCents,
|
|
1208
|
-
generateMemo: flags
|
|
1297
|
+
generateMemo: boolFlag(flags, "memo", "generate-memo", "generateMemo"),
|
|
1209
1298
|
}
|
|
1210
1299
|
);
|
|
1211
1300
|
if (flags.prompt === true || flags.handoff === true) {
|
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,8 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llamaventures/cli",
|
|
3
|
-
"version": "1.14.
|
|
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"
|
|
@@ -11,6 +15,7 @@
|
|
|
11
15
|
"assets/",
|
|
12
16
|
"bin/",
|
|
13
17
|
"lib/",
|
|
18
|
+
"scripts/verify-agent-routing.mjs",
|
|
14
19
|
"AGENT_BRIEFING.md",
|
|
15
20
|
"CHANGELOG.md",
|
|
16
21
|
"CONTRIBUTING.md",
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import assert from "node:assert/strict";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { createServer } from "node:http";
|
|
6
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
7
|
+
import os from "node:os";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
|
|
11
|
+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
12
|
+
const calls = [];
|
|
13
|
+
let threadSeq = 0;
|
|
14
|
+
|
|
15
|
+
async function readJson(req) {
|
|
16
|
+
let raw = "";
|
|
17
|
+
for await (const chunk of req) raw += chunk;
|
|
18
|
+
if (!raw) return null;
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(raw);
|
|
21
|
+
} catch {
|
|
22
|
+
return raw;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function writeJson(res, data) {
|
|
27
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
28
|
+
res.end(JSON.stringify(data));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function writeSse(res) {
|
|
32
|
+
res.writeHead(200, {
|
|
33
|
+
"Content-Type": "text/event-stream",
|
|
34
|
+
"Cache-Control": "no-cache",
|
|
35
|
+
Connection: "keep-alive",
|
|
36
|
+
});
|
|
37
|
+
const events = [
|
|
38
|
+
{ tool_use: { name: "read_typed_factual_layer" } },
|
|
39
|
+
{ tool_result: { name: "read_typed_factual_layer", ok: true, summary: "ok" } },
|
|
40
|
+
{ text: "agent done" },
|
|
41
|
+
];
|
|
42
|
+
for (const event of events) {
|
|
43
|
+
res.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
44
|
+
}
|
|
45
|
+
res.end();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const server = createServer(async (req, res) => {
|
|
49
|
+
try {
|
|
50
|
+
const body = await readJson(req);
|
|
51
|
+
const url = new URL(req.url, "http://localhost");
|
|
52
|
+
calls.push({ method: req.method, path: url.pathname, body });
|
|
53
|
+
|
|
54
|
+
if (req.method === "POST" && /^\/api\/deals\/[^/]+\/threads$/.test(url.pathname)) {
|
|
55
|
+
threadSeq += 1;
|
|
56
|
+
writeJson(res, { id: `thread-${threadSeq}` });
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (req.method === "POST" && /^\/api\/deals\/[^/]+\/threads\/[^/]+$/.test(url.pathname)) {
|
|
61
|
+
writeSse(res);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (req.method === "POST" && /^\/api\/deals\/[^/]+\/enrich$/.test(url.pathname)) {
|
|
66
|
+
writeJson(res, {
|
|
67
|
+
ok: true,
|
|
68
|
+
agentHarness: {
|
|
69
|
+
handoffPrompt: "mock handoff prompt",
|
|
70
|
+
systemInjection: "mock system injection",
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
77
|
+
res.end(JSON.stringify({ error: `Unexpected route ${req.method} ${url.pathname}` }));
|
|
78
|
+
} catch (err) {
|
|
79
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
80
|
+
res.end(JSON.stringify({ error: err?.message ?? String(err) }));
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
function listen(server) {
|
|
85
|
+
return new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function close(server) {
|
|
89
|
+
return new Promise((resolve, reject) => {
|
|
90
|
+
server.close((err) => (err ? reject(err) : resolve()));
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function childEnv(baseUrl, homeDir) {
|
|
95
|
+
return {
|
|
96
|
+
...process.env,
|
|
97
|
+
HOME: homeDir,
|
|
98
|
+
LLAMA_API_URL: baseUrl,
|
|
99
|
+
LLAMA_TOKEN: "llc_mock_agent_routing",
|
|
100
|
+
PATH: "/usr/bin:/bin",
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function resetCalls() {
|
|
105
|
+
calls.length = 0;
|
|
106
|
+
threadSeq = 0;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function paths() {
|
|
110
|
+
return calls.map((call) => `${call.method} ${call.path}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function assertNoEnrichCall() {
|
|
114
|
+
assert.equal(
|
|
115
|
+
calls.some((call) => call.path.endsWith("/enrich")),
|
|
116
|
+
false,
|
|
117
|
+
`expected no /enrich call, got ${paths().join(", ")}`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function assertThreadRun({ title, messageIncludes }) {
|
|
122
|
+
assert.equal(calls.length, 2, `expected thread create + SSE run, got ${paths().join(", ")}`);
|
|
123
|
+
assert.match(calls[0].path, /^\/api\/deals\/[^/]+\/threads$/);
|
|
124
|
+
assert.equal(calls[0].body?.title, title);
|
|
125
|
+
assert.match(calls[1].path, /^\/api\/deals\/[^/]+\/threads\/thread-1$/);
|
|
126
|
+
for (const needle of messageIncludes) {
|
|
127
|
+
assert.match(calls[1].body?.message ?? "", new RegExp(escapeRegExp(needle)));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function escapeRegExp(value) {
|
|
132
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function runCli(args, baseUrl, homeDir) {
|
|
136
|
+
const child = spawn(process.execPath, ["bin/llama.mjs", ...args], {
|
|
137
|
+
cwd: repoRoot,
|
|
138
|
+
env: childEnv(baseUrl, homeDir),
|
|
139
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
140
|
+
});
|
|
141
|
+
let stdout = "";
|
|
142
|
+
let stderr = "";
|
|
143
|
+
child.stdout.on("data", (chunk) => {
|
|
144
|
+
stdout += chunk;
|
|
145
|
+
});
|
|
146
|
+
child.stderr.on("data", (chunk) => {
|
|
147
|
+
stderr += chunk;
|
|
148
|
+
});
|
|
149
|
+
const code = await new Promise((resolve) => child.on("close", resolve));
|
|
150
|
+
assert.equal(code, 0, `CLI failed (${code})\nSTDOUT:\n${stdout}\nSTDERR:\n${stderr}`);
|
|
151
|
+
return { stdout, stderr };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function callMcpTool(name, args, baseUrl, homeDir) {
|
|
155
|
+
const child = spawn(process.execPath, ["bin/llama-mcp.mjs"], {
|
|
156
|
+
cwd: repoRoot,
|
|
157
|
+
env: childEnv(baseUrl, homeDir),
|
|
158
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
159
|
+
});
|
|
160
|
+
let stderr = "";
|
|
161
|
+
let buffer = "";
|
|
162
|
+
child.stderr.on("data", (chunk) => {
|
|
163
|
+
stderr += chunk;
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const result = await new Promise((resolve, reject) => {
|
|
167
|
+
const timeout = setTimeout(() => {
|
|
168
|
+
child.kill();
|
|
169
|
+
reject(new Error(`Timed out waiting for MCP response\nSTDERR:\n${stderr}`));
|
|
170
|
+
}, 8000);
|
|
171
|
+
|
|
172
|
+
child.stdout.on("data", (chunk) => {
|
|
173
|
+
buffer += chunk;
|
|
174
|
+
let idx;
|
|
175
|
+
while ((idx = buffer.indexOf("\n")) >= 0) {
|
|
176
|
+
const line = buffer.slice(0, idx).trim();
|
|
177
|
+
buffer = buffer.slice(idx + 1);
|
|
178
|
+
if (!line) continue;
|
|
179
|
+
let msg;
|
|
180
|
+
try {
|
|
181
|
+
msg = JSON.parse(line);
|
|
182
|
+
} catch {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (msg.id === 2) {
|
|
186
|
+
clearTimeout(timeout);
|
|
187
|
+
child.kill();
|
|
188
|
+
resolve(msg);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
child.on("error", (err) => {
|
|
194
|
+
clearTimeout(timeout);
|
|
195
|
+
reject(err);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
child.stdin.write(
|
|
199
|
+
[
|
|
200
|
+
JSON.stringify({
|
|
201
|
+
jsonrpc: "2.0",
|
|
202
|
+
id: 1,
|
|
203
|
+
method: "initialize",
|
|
204
|
+
params: {
|
|
205
|
+
protocolVersion: "2024-11-05",
|
|
206
|
+
capabilities: {},
|
|
207
|
+
clientInfo: { name: "routing-test", version: "1" },
|
|
208
|
+
},
|
|
209
|
+
}),
|
|
210
|
+
JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }),
|
|
211
|
+
JSON.stringify({
|
|
212
|
+
jsonrpc: "2.0",
|
|
213
|
+
id: 2,
|
|
214
|
+
method: "tools/call",
|
|
215
|
+
params: { name, arguments: args },
|
|
216
|
+
}),
|
|
217
|
+
].join("\n") + "\n",
|
|
218
|
+
);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
assert.ok(!result.error, `MCP returned error: ${JSON.stringify(result.error)}`);
|
|
222
|
+
return result.result;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
await listen(server);
|
|
226
|
+
const address = server.address();
|
|
227
|
+
const baseUrl = `http://${address.address}:${address.port}`;
|
|
228
|
+
const homeDir = await mkdtemp(path.join(os.tmpdir(), "llama-cli-routing-"));
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
resetCalls();
|
|
232
|
+
const enrichRun = await runCli(
|
|
233
|
+
[
|
|
234
|
+
"deal",
|
|
235
|
+
"enrich",
|
|
236
|
+
"deal-cli",
|
|
237
|
+
"--apply",
|
|
238
|
+
"--executor",
|
|
239
|
+
"server_agent",
|
|
240
|
+
"--sources",
|
|
241
|
+
"website,monid",
|
|
242
|
+
"--budget-cents",
|
|
243
|
+
"12",
|
|
244
|
+
],
|
|
245
|
+
baseUrl,
|
|
246
|
+
homeDir,
|
|
247
|
+
);
|
|
248
|
+
assert.match(enrichRun.stdout, /agent done/);
|
|
249
|
+
assertNoEnrichCall();
|
|
250
|
+
assertThreadRun({
|
|
251
|
+
title: "CLI enrichment",
|
|
252
|
+
messageIncludes: ["website, monid", "12 cents", "upsert_typed_fact"],
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
resetCalls();
|
|
256
|
+
await runCli(
|
|
257
|
+
["deal", "enrich", "deal-cli", "--apply", "--executor", "server_agent", "--harness-only"],
|
|
258
|
+
baseUrl,
|
|
259
|
+
homeDir,
|
|
260
|
+
);
|
|
261
|
+
assert.deepEqual(paths(), ["POST /api/deals/deal-cli/enrich"]);
|
|
262
|
+
assert.equal(calls[0].body?.apply, true);
|
|
263
|
+
assert.equal(calls[0].body?.dryRun, false);
|
|
264
|
+
assert.equal(calls[0].body?.executor, "server_agent");
|
|
265
|
+
|
|
266
|
+
resetCalls();
|
|
267
|
+
const agentRun = await runCli(
|
|
268
|
+
["deal", "agent", "run", "deal-cli", "--message", "custom server task"],
|
|
269
|
+
baseUrl,
|
|
270
|
+
homeDir,
|
|
271
|
+
);
|
|
272
|
+
assert.match(agentRun.stdout, /agent done/);
|
|
273
|
+
assertNoEnrichCall();
|
|
274
|
+
assertThreadRun({
|
|
275
|
+
title: "CLI agent run",
|
|
276
|
+
messageIncludes: ["custom server task"],
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
resetCalls();
|
|
280
|
+
const mcpResult = await callMcpTool(
|
|
281
|
+
"deal_enrich",
|
|
282
|
+
{
|
|
283
|
+
dealId: "deal-mcp",
|
|
284
|
+
apply: true,
|
|
285
|
+
executor: "server_agent",
|
|
286
|
+
sources: ["web", "monid"],
|
|
287
|
+
budgetCents: 7,
|
|
288
|
+
},
|
|
289
|
+
baseUrl,
|
|
290
|
+
homeDir,
|
|
291
|
+
);
|
|
292
|
+
assertNoEnrichCall();
|
|
293
|
+
assertThreadRun({
|
|
294
|
+
title: "MCP enrichment",
|
|
295
|
+
messageIncludes: ["web, monid", "7 cents", "upsert_typed_fact"],
|
|
296
|
+
});
|
|
297
|
+
const payload = JSON.parse(mcpResult.content?.[0]?.text ?? "{}");
|
|
298
|
+
assert.equal(payload.ok, true);
|
|
299
|
+
assert.equal(payload.threadId, "thread-1");
|
|
300
|
+
assert.equal(payload.text, "agent done");
|
|
301
|
+
|
|
302
|
+
console.log("agent routing verification passed");
|
|
303
|
+
} finally {
|
|
304
|
+
await close(server);
|
|
305
|
+
await rm(homeDir, { recursive: true, force: true });
|
|
306
|
+
}
|