@llamaventures/cli 1.14.0 → 1.15.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 +15 -1
- package/CHANGELOG.md +34 -0
- package/CONTRIBUTING.md +3 -2
- package/README.md +18 -3
- package/README.zh-CN.md +17 -4
- package/bin/llama-mcp.mjs +213 -9
- package/bin/llama.mjs +201 -12
- package/lib/client.mjs +78 -0
- package/package.json +6 -1
- package/scripts/verify-agent-routing.mjs +447 -0
package/AGENT_BRIEFING.md
CHANGED
|
@@ -20,6 +20,17 @@ Most teammates don't know everything this CLI can do. Part of your job is to sur
|
|
|
20
20
|
- **Point at `llama --help`** for the full surface rather than reciting it. The CLI uses progressive help: `llama --help` is a short overview, `llama <area> --help` drills in.
|
|
21
21
|
- **Stay current.** If you suspect the CLI is stale, run `llama version --check`; if it reports an upgrade, tell the user the one-line `npm i -g @llamaventures/cli@latest` command. Don't nag repeatedly.
|
|
22
22
|
|
|
23
|
+
## Runtime skill library
|
|
24
|
+
|
|
25
|
+
This npm package is public, but Llama OS skills are private. Do not assume the skill text is bundled locally. For team-token sessions, discover the live runtime library through Llama Command:
|
|
26
|
+
|
|
27
|
+
- Start with `llama agent bootstrap` or MCP `agent_bootstrap` when you need the current Command + Llama OS contract.
|
|
28
|
+
- Use `llama skills search "<task>"` or MCP `skills_search` before choosing a Llama workflow.
|
|
29
|
+
- Use `llama skills show <slug>` or MCP `skills_read` only for the relevant skill.
|
|
30
|
+
- Use `llama explain <command-url-or-object>` or MCP `object_inspect` for 404s, deleted wiki pages, notifier links, deal URLs, and unknown Command objects before telling the user "the system is broken."
|
|
31
|
+
|
|
32
|
+
The boundary matters: public CLI/MCP discovers skills, but authenticated Command decides which skill content the token may read.
|
|
33
|
+
|
|
23
34
|
## Pipeline First (hard rule)
|
|
24
35
|
|
|
25
36
|
Any time the user mentions a company name or founder name:
|
|
@@ -259,7 +270,7 @@ Run `llama --help` for the full surface (~40 commands).
|
|
|
259
270
|
|
|
260
271
|
## MCP-native agents
|
|
261
272
|
|
|
262
|
-
If you support [MCP](https://modelcontextprotocol.io), **prefer the MCP server over parsing CLI output.** The same package ships `llama-mcp` (
|
|
273
|
+
If you support [MCP](https://modelcontextprotocol.io), **prefer the MCP server over parsing CLI output.** The same package ships `llama-mcp` (56 typed tools, identical auth chain).
|
|
263
274
|
|
|
264
275
|
Add to your MCP client config (Claude Desktop / Claude Code / Cursor / OpenClaw / Codex / etc.):
|
|
265
276
|
|
|
@@ -270,6 +281,9 @@ Add to your MCP client config (Claude Desktop / Claude Code / Cursor / OpenClaw
|
|
|
270
281
|
Tools available:
|
|
271
282
|
|
|
272
283
|
- `auth_status` — verify creds + identity (call first if anything 401s)
|
|
284
|
+
- `agent_bootstrap` — fetch the live Command + Llama OS runtime manifest
|
|
285
|
+
- `skills_search` / `skills_read` — discover and read authenticated runtime skills
|
|
286
|
+
- `object_inspect` — explain Command URLs, 404s, deleted objects, and lifecycle trail
|
|
273
287
|
- `deal_search` / `deal_show` / `deal_create` / `deal_update`
|
|
274
288
|
- `brief_blocks` / `brief_add_text` / `brief_add_link` / `brief_add_callout`
|
|
275
289
|
- `wiki_search` / `wiki_save` (accepts `content_type: 'markdown' | 'html'` — HTML entries render as full-viewport sandboxed iframe at `/wiki/<slug>`) / `wiki_delete` / `wiki_restore` (soft-delete, reversible)
|
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,40 @@ this project adheres to [Semantic Versioning](https://semver.org).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.15.0] — 2026-06-15
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- **`llama agent bootstrap`** — fetches the live Llama Command + Llama OS
|
|
13
|
+
runtime manifest, including the authenticated skill bundle metadata and
|
|
14
|
+
object-inspection contract.
|
|
15
|
+
- **`llama skills search|show`** — discovers and reads runtime Llama OS skills
|
|
16
|
+
from Llama Command. The public npm package does not bundle private skill
|
|
17
|
+
content; Command returns only what the authenticated token may see.
|
|
18
|
+
- **`llama explain <url-or-object>`** — asks Llama Command to explain URLs,
|
|
19
|
+
deleted objects, 404s, and lifecycle history before an agent guesses that
|
|
20
|
+
the system is broken.
|
|
21
|
+
- MCP parity tools: **`agent_bootstrap`**, **`skills_search`**,
|
|
22
|
+
**`skills_read`**, and **`object_inspect`**.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
- `AGENT_BRIEFING.md` now teaches agents to use the authenticated runtime
|
|
26
|
+
skill gateway instead of assuming a local private `llama-os` checkout.
|
|
27
|
+
|
|
28
|
+
## [1.14.1] — 2026-06-15
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
- **`llama deal agent run <dealId> --message "..."`** — starts Llama
|
|
32
|
+
Command's server-side Deal Agent in a deal thread, so the service agent can
|
|
33
|
+
execute deal-scoped work instead of the local CLI user doing it.
|
|
34
|
+
- **`deal_agent_run` MCP tool** — the same narrow server-agent trigger for
|
|
35
|
+
MCP-native clients, without adding a generic API passthrough.
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
- **`llama deal enrich <dealId> --apply --executor server_agent`** now starts
|
|
39
|
+
the server-side Deal Agent unless `--harness-only` is supplied. Dry-runs and
|
|
40
|
+
external-agent handoff prompts still use the enrichment harness endpoint.
|
|
41
|
+
- **`deal_enrich` MCP tool** now mirrors the same behavior with `harnessOnly`.
|
|
42
|
+
|
|
9
43
|
## [1.14.0] — 2026-06-15
|
|
10
44
|
|
|
11
45
|
### 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, 56 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
|
|
|
@@ -209,6 +211,12 @@ llama approvals decide <approvalId> approved --note "..."
|
|
|
209
211
|
llama timeline <dealId>
|
|
210
212
|
llama post <dealId> "message body" [--link url]
|
|
211
213
|
|
|
214
|
+
# Agent runtime — live Command + private Llama OS skill gateway
|
|
215
|
+
llama agent bootstrap
|
|
216
|
+
llama skills search "wiki delete tombstone"
|
|
217
|
+
llama skills show llama-command
|
|
218
|
+
llama explain https://command.llamaventures.vc/wiki/some-page
|
|
219
|
+
|
|
212
220
|
# Wiki
|
|
213
221
|
llama wiki search "<query>"
|
|
214
222
|
llama wiki read <slug> [--lang en|zh]
|
|
@@ -247,13 +255,15 @@ agents can pattern-match without parsing prose.
|
|
|
247
255
|
## MCP server
|
|
248
256
|
|
|
249
257
|
The bundled `llama-mcp` is a **stdio Model Context Protocol** server exposing
|
|
250
|
-
**
|
|
258
|
+
**56 typed tools** that mirror the most-used CLI surface. Every tool is named
|
|
251
259
|
and scoped — there is no generic API passthrough, by design (a public-package
|
|
252
260
|
escape hatch reachable from a prompt-injectable agent context is exactly the
|
|
253
261
|
shape we want to avoid).
|
|
254
262
|
|
|
255
263
|
Coverage is grouped around the workflows agents actually need: auth
|
|
256
|
-
diagnostics;
|
|
264
|
+
diagnostics; live agent bootstrap; authenticated Llama OS skill search/read;
|
|
265
|
+
Command URL/object inspection; deal search/show/create/update/feed; server-side deal agent runs;
|
|
266
|
+
deal enrichment harnesses;
|
|
257
267
|
trust-rated facts; brief blocks and version history; wiki read/write/delete/restore;
|
|
258
268
|
timeline posts and mentions; skill corrections; refresh triggers; external pitch intake;
|
|
259
269
|
memo show/regenerate/save/reset; and deal-scoped HTML docs, versions, bundles, and
|
|
@@ -274,6 +284,11 @@ The `agent_briefing` MCP **prompt** also returns
|
|
|
274
284
|
[`AGENT_BRIEFING.md`](AGENT_BRIEFING.md) verbatim, so any new agent loading the
|
|
275
285
|
server can self-onboard without leaving the protocol.
|
|
276
286
|
|
|
287
|
+
For current Llama OS skills, use the runtime tools instead of looking for a
|
|
288
|
+
local private repo: `agent_bootstrap`, `skills_search`, `skills_read`, and
|
|
289
|
+
`object_inspect`. The public npm package does not bundle private skill text;
|
|
290
|
+
Command returns only the content visible to the authenticated token.
|
|
291
|
+
|
|
277
292
|
### Wire into your agent
|
|
278
293
|
|
|
279
294
|
<details open>
|
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,56 个工具
|
|
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
|
|
|
@@ -230,6 +232,12 @@ llama approvals decide <approvalId> approved --note "..."
|
|
|
230
232
|
llama timeline <dealId>
|
|
231
233
|
llama post <dealId> "消息内容" [--link url]
|
|
232
234
|
|
|
235
|
+
# Agent runtime——Command 上的 Llama OS skill gateway
|
|
236
|
+
llama agent bootstrap
|
|
237
|
+
llama skills search "wiki delete tombstone"
|
|
238
|
+
llama skills show llama-command
|
|
239
|
+
llama explain https://command.llamaventures.vc/wiki/some-page
|
|
240
|
+
|
|
233
241
|
# Wiki
|
|
234
242
|
llama wiki search "<query>"
|
|
235
243
|
llama wiki save <slug> --title "..." --content "..."
|
|
@@ -259,12 +267,13 @@ MCP server 在 `isError: true` 内容里返回相同的前缀,agent 不用解
|
|
|
259
267
|
## MCP server
|
|
260
268
|
|
|
261
269
|
随包发布的 `llama-mcp` 是一个 **stdio Model Context Protocol** server,
|
|
262
|
-
暴露 **
|
|
270
|
+
暴露 **56 个 typed tools**——基本镜像 CLI 最常用的命令。每个 tool 都是
|
|
263
271
|
具名、scoped 的;**没有**通用的 API passthrough,这是有意设计的(公开
|
|
264
272
|
package 里一个能被 prompt-injection 触达的逃生通道,正是我们要避开的形状)。
|
|
265
273
|
|
|
266
|
-
覆盖面按 agent 真正会用的工作流分组:auth 诊断;
|
|
267
|
-
|
|
274
|
+
覆盖面按 agent 真正会用的工作流分组:auth 诊断;live agent bootstrap;
|
|
275
|
+
授权后的 Llama OS skill search/read;Command URL/object inspect;deal search/show/create/
|
|
276
|
+
update/feed;服务端 deal agent run;deal enrichment harness;带 trust ladder 的事实;brief blocks
|
|
268
277
|
和版本历史;wiki 读写删恢复;timeline posts 和 mentions;skill corrections;
|
|
269
278
|
refresh triggers;外部 pitch intake;memo show/regenerate/save/reset;
|
|
270
279
|
以及 deal-scoped HTML docs、versions、bundles、restore/reset。
|
|
@@ -284,6 +293,10 @@ printf '%s\n' \
|
|
|
284
293
|
[`AGENT_BRIEFING.md`](AGENT_BRIEFING.md)——刚装上 server 的 agent
|
|
285
294
|
不用离开协议就能给自己 onboard。
|
|
286
295
|
|
|
296
|
+
要读当前 Llama OS skills,用 runtime tools:`agent_bootstrap`、
|
|
297
|
+
`skills_search`、`skills_read`、`object_inspect`。公开 npm 包不打包私有
|
|
298
|
+
skill 正文;Command 只按当前 token 的权限返回可见内容。
|
|
299
|
+
|
|
287
300
|
### 接到你的 agent 上
|
|
288
301
|
|
|
289
302
|
<details open>
|
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.
|
|
@@ -105,6 +195,82 @@ server.registerTool(
|
|
|
105
195
|
}
|
|
106
196
|
);
|
|
107
197
|
|
|
198
|
+
server.registerTool(
|
|
199
|
+
"agent_bootstrap",
|
|
200
|
+
{
|
|
201
|
+
description:
|
|
202
|
+
"Fetch the live Llama Command + Llama OS runtime manifest. Use this at " +
|
|
203
|
+
"the start of an agent session to discover current skills, the skill " +
|
|
204
|
+
"bundle version, and the object-inspection contract. Unlike the bundled " +
|
|
205
|
+
"agent_briefing prompt, this comes from authenticated Command runtime.",
|
|
206
|
+
inputSchema: {
|
|
207
|
+
limit: z.number().optional().describe("number of skill summaries to include; default 25"),
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
async ({ limit } = {}) => {
|
|
211
|
+
const params = new URLSearchParams();
|
|
212
|
+
if (limit) params.set("limit", String(limit));
|
|
213
|
+
return callApi("GET", `/api/agent/manifest${params.toString() ? `?${params}` : ""}`);
|
|
214
|
+
}
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
server.registerTool(
|
|
218
|
+
"skills_search",
|
|
219
|
+
{
|
|
220
|
+
description:
|
|
221
|
+
"Search the authenticated Llama OS runtime skill library. Call this " +
|
|
222
|
+
"before choosing a workflow for Llama pipeline/wiki/DD/research/ops tasks. " +
|
|
223
|
+
"Returns summaries only; call skills_read for the exact SKILL.md.",
|
|
224
|
+
inputSchema: {
|
|
225
|
+
q: z.string().describe("workflow/task query, e.g. 'wiki delete tombstone' or 'deal DD memo'"),
|
|
226
|
+
limit: z.number().optional().describe("default 20"),
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
async ({ q, limit }) => {
|
|
230
|
+
const params = new URLSearchParams({ q });
|
|
231
|
+
if (limit) params.set("limit", String(limit));
|
|
232
|
+
return callApi("GET", `/api/agent/skills?${params}`);
|
|
233
|
+
}
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
server.registerTool(
|
|
237
|
+
"skills_read",
|
|
238
|
+
{
|
|
239
|
+
description:
|
|
240
|
+
"Read one runtime Llama OS skill by slug. Use after skills_search. " +
|
|
241
|
+
"Returns the full SKILL.md content from Llama Command; public npm does " +
|
|
242
|
+
"not bundle private skill text.",
|
|
243
|
+
inputSchema: {
|
|
244
|
+
slug: z.string().describe("skill slug, e.g. llama-command or llama-wiki"),
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
async ({ slug }) => callApi("GET", `/api/agent/skills/${encodeURIComponent(slug)}`)
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
server.registerTool(
|
|
251
|
+
"object_inspect",
|
|
252
|
+
{
|
|
253
|
+
description:
|
|
254
|
+
"Explain a Llama Command URL or object id. Use for 404s, deleted wiki " +
|
|
255
|
+
"pages, notifier links, deal URLs, brief blocks, HTML docs, and unknown " +
|
|
256
|
+
"Command objects before guessing that the system is broken.",
|
|
257
|
+
inputSchema: {
|
|
258
|
+
q: z.string().optional().describe("URL or compact query, e.g. wiki:my-slug or a Command URL"),
|
|
259
|
+
type: z.string().optional().describe("explicit object type if not using q"),
|
|
260
|
+
id: z.string().optional().describe("explicit object id if not using q"),
|
|
261
|
+
lang: z.enum(["en", "zh"]).optional().describe("wiki language; default en"),
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
async ({ q, type, id, lang } = {}) => {
|
|
265
|
+
const params = new URLSearchParams();
|
|
266
|
+
if (q) params.set("q", q);
|
|
267
|
+
if (type) params.set("type", type);
|
|
268
|
+
if (id) params.set("id", id);
|
|
269
|
+
if (lang) params.set("lang", lang);
|
|
270
|
+
return callApi("GET", `/api/agent/explain?${params}`);
|
|
271
|
+
}
|
|
272
|
+
);
|
|
273
|
+
|
|
108
274
|
// ============================================================
|
|
109
275
|
// Deals — read
|
|
110
276
|
// ============================================================
|
|
@@ -786,14 +952,33 @@ server.registerTool(
|
|
|
786
952
|
callApi("POST", `/api/deals/${encodeURIComponent(dealId)}/refresh-persona`, { persona })
|
|
787
953
|
);
|
|
788
954
|
|
|
955
|
+
server.registerTool(
|
|
956
|
+
"deal_agent_run",
|
|
957
|
+
{
|
|
958
|
+
description:
|
|
959
|
+
"Run Llama Command's server-side Deal Agent inside a deal thread. " +
|
|
960
|
+
"Use this when the user explicitly wants the service agent to execute " +
|
|
961
|
+
"a deal-scoped task instead of the local MCP client doing the work.",
|
|
962
|
+
inputSchema: {
|
|
963
|
+
dealId: z.string().describe("deal uuid"),
|
|
964
|
+
message: z.string().describe("task instruction for the server-side Deal Agent"),
|
|
965
|
+
title: z.string().optional().describe("optional thread title; defaults to MCP agent run"),
|
|
966
|
+
},
|
|
967
|
+
},
|
|
968
|
+
async ({ dealId, message, title }) =>
|
|
969
|
+
runDealAgentTool({ dealId, message, title: title || "MCP agent run" })
|
|
970
|
+
);
|
|
971
|
+
|
|
789
972
|
server.registerTool(
|
|
790
973
|
"deal_enrich",
|
|
791
974
|
{
|
|
792
975
|
description:
|
|
793
976
|
"Run the Llama Command deal enrichment planner/trigger for one deal. " +
|
|
794
977
|
"Default is dry-run: returns evidence plan, source plan, Monid budget/config " +
|
|
795
|
-
"status, and planned writes without changing facts/links/memo.
|
|
796
|
-
"
|
|
978
|
+
"status, and planned writes without changing facts/links/memo. With " +
|
|
979
|
+
"apply=true and executor=server_agent, this starts the server-side Deal " +
|
|
980
|
+
"Agent unless harnessOnly=true. Set apply=true only when the user " +
|
|
981
|
+
"explicitly wants the enrichment run recorded/applied. " +
|
|
797
982
|
"generateMemo never defaults on; pass true only when the user explicitly asks " +
|
|
798
983
|
"for Memo generation after enrichment.",
|
|
799
984
|
inputSchema: {
|
|
@@ -803,7 +988,7 @@ server.registerTool(
|
|
|
803
988
|
executor: z
|
|
804
989
|
.enum(["server_agent", "external_agent", "planner"])
|
|
805
990
|
.optional()
|
|
806
|
-
.describe("who will execute the harness;
|
|
991
|
+
.describe("who will execute the harness; server_agent starts Deal Agent when apply=true"),
|
|
807
992
|
sources: z
|
|
808
993
|
.array(z.enum(["website", "github", "linkedin", "yc", "launch", "web", "monid"]))
|
|
809
994
|
.optional()
|
|
@@ -819,17 +1004,34 @@ server.registerTool(
|
|
|
819
1004
|
.boolean()
|
|
820
1005
|
.optional()
|
|
821
1006
|
.describe("explicitly request memo regeneration after enrichment; default false"),
|
|
1007
|
+
harnessOnly: z
|
|
1008
|
+
.boolean()
|
|
1009
|
+
.optional()
|
|
1010
|
+
.describe("when true, return/apply the enrichment harness endpoint instead of starting Deal Agent"),
|
|
1011
|
+
message: z
|
|
1012
|
+
.string()
|
|
1013
|
+
.optional()
|
|
1014
|
+
.describe("optional override instruction for the server-side Deal Agent"),
|
|
822
1015
|
},
|
|
823
1016
|
},
|
|
824
|
-
async ({ dealId, dryRun, apply, executor, sources, budgetCents, generateMemo }) =>
|
|
825
|
-
|
|
1017
|
+
async ({ dealId, dryRun, apply, executor, sources, budgetCents, generateMemo, harnessOnly, message }) => {
|
|
1018
|
+
const effectiveExecutor = executor ?? "server_agent";
|
|
1019
|
+
if (apply === true && effectiveExecutor === "server_agent" && harnessOnly !== true) {
|
|
1020
|
+
return runDealAgentTool({
|
|
1021
|
+
dealId,
|
|
1022
|
+
title: "MCP enrichment",
|
|
1023
|
+
message: buildEnrichmentAgentMessage({ sources, budgetCents, generateMemo, message }),
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
return callApi("POST", `/api/deals/${encodeURIComponent(dealId)}/enrich`, {
|
|
826
1027
|
dryRun,
|
|
827
1028
|
apply,
|
|
828
|
-
executor,
|
|
1029
|
+
executor: effectiveExecutor,
|
|
829
1030
|
sources,
|
|
830
1031
|
budgetCents,
|
|
831
1032
|
generateMemo,
|
|
832
|
-
})
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
833
1035
|
);
|
|
834
1036
|
|
|
835
1037
|
// ============================================================
|
|
@@ -1375,7 +1577,9 @@ server.registerPrompt(
|
|
|
1375
1577
|
"contract: identity, Pipeline First rule, content capture, autonomy " +
|
|
1376
1578
|
"levels (L0/L1/L2/L3), communication style, error recovery, CLI/MCP " +
|
|
1377
1579
|
"reference, and boundaries. Read this once, internalise it, operate " +
|
|
1378
|
-
"accordingly. Same content as `llama agent-onboard` from the CLI."
|
|
1580
|
+
"accordingly. Same content as `llama agent-onboard` from the CLI. " +
|
|
1581
|
+
"For the live private Llama OS skill library, call agent_bootstrap, " +
|
|
1582
|
+
"skills_search, and skills_read.",
|
|
1379
1583
|
},
|
|
1380
1584
|
async () => {
|
|
1381
1585
|
// Gate the briefing behind a /api/me check so unauthenticated MCP
|
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,10 +200,83 @@ 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):
|
|
205
275
|
llama agent-onboard # print AGENT_BRIEFING.md — the workflow contract for AI agents
|
|
276
|
+
llama agent bootstrap # fetch live Command + Llama OS skill manifest
|
|
277
|
+
llama skills search "pipeline update" # discover relevant runtime skills
|
|
278
|
+
llama skills show llama-pipeline # read a skill from Command
|
|
279
|
+
llama explain <url-or-object> # explain Command URL/object status + lifecycle
|
|
206
280
|
|
|
207
281
|
External pitch — talk to Llama Ventures' intake agent (no token required):
|
|
208
282
|
llama pitch start --name "Jane Doe" --email "jane@acme.ai"
|
|
@@ -237,7 +311,9 @@ Deals:
|
|
|
237
311
|
llama deal update <dealId> leadInvestor "Acme Capital"
|
|
238
312
|
llama deal enrich <dealId> [--dry-run] [--apply] [--executor server_agent|external_agent|planner]
|
|
239
313
|
[--sources website,github,linkedin,yc,monid] [--budget-cents 50]
|
|
240
|
-
[--memo] [--prompt]
|
|
314
|
+
[--memo] [--prompt] [--harness-only]
|
|
315
|
+
dry-run returns the harness; --apply --executor server_agent runs the server Deal Agent.
|
|
316
|
+
llama deal agent run <dealId> --message "collect founder evidence and update typed facts"
|
|
241
317
|
llama deal extra set <dealId> <key> <value> # system-admin only
|
|
242
318
|
Patch one top-level key in deals.extra JSONB. Value is parsed as
|
|
243
319
|
JSON when possible ('{"a":1}', 'true', '3'), else stored as a
|
|
@@ -440,6 +516,9 @@ Common:
|
|
|
440
516
|
llama deal feed <dealId> every contribution (facts + notes), newest first
|
|
441
517
|
llama post <dealId> "..." add a note to a deal
|
|
442
518
|
llama agent-onboard print the AI-agent workflow contract
|
|
519
|
+
llama agent bootstrap live Llama OS skill manifest from Command
|
|
520
|
+
llama skills search "<query>" discover which skill to read
|
|
521
|
+
llama explain <url-or-object> explain Command URLs, 404s, deleted objects
|
|
443
522
|
|
|
444
523
|
Command groups — run \`llama help <group>\` for that group's commands:
|
|
445
524
|
deal create · show · feed · update · enrich · search · collaborators · links · delete
|
|
@@ -452,6 +531,8 @@ Command groups — run \`llama help <group>\` for that group's commands:
|
|
|
452
531
|
pitch external founder intake (no token needed)
|
|
453
532
|
ownership claim · nominate · approvals
|
|
454
533
|
admin audit events (system admin only)
|
|
534
|
+
agent bootstrap · skills · explain for AI agents
|
|
535
|
+
skills search · show runtime Llama OS skills
|
|
455
536
|
auth setup · tokens · auth status
|
|
456
537
|
|
|
457
538
|
llama help all the full command reference (everything at once)
|
|
@@ -471,6 +552,8 @@ const HELP_AREA_MATCH = {
|
|
|
471
552
|
pitch: [/^External pitch/],
|
|
472
553
|
ownership: [/^Ownership/, /^Approvals/],
|
|
473
554
|
admin: [/^Admin/],
|
|
555
|
+
agent: [/^Agent onboarding/],
|
|
556
|
+
skills: [/^Agent onboarding/],
|
|
474
557
|
auth: [/^Setup/, /^Zero-config/, /^Token discovery/, /^Env/],
|
|
475
558
|
};
|
|
476
559
|
|
|
@@ -859,6 +942,95 @@ https://command.llamaventures.vc/settings/tokens, run
|
|
|
859
942
|
return;
|
|
860
943
|
}
|
|
861
944
|
|
|
945
|
+
// Live runtime bootstrap from Llama Command. Unlike agent-onboard, this is
|
|
946
|
+
// not bundled in the public npm package; Command returns the current
|
|
947
|
+
// authenticated skill manifest and object-inspection contract.
|
|
948
|
+
if (area === "agent" && action === "bootstrap") {
|
|
949
|
+
const { flags } = parseFlags(rest, ["json", "limit"]);
|
|
950
|
+
const params = new URLSearchParams();
|
|
951
|
+
if (flags.limit && flags.limit !== true) params.set("limit", String(flags.limit));
|
|
952
|
+
const manifest = await request("GET", `/api/agent/manifest${params.toString() ? `?${params}` : ""}`);
|
|
953
|
+
if (flags.json) {
|
|
954
|
+
print(manifest);
|
|
955
|
+
} else {
|
|
956
|
+
process.stdout.write(`${manifest.briefing || JSON.stringify(manifest, null, 2)}\n`);
|
|
957
|
+
}
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
if (area === "skills" || (area === "agent" && action === "skills")) {
|
|
962
|
+
const sub = area === "skills" ? action : rest[0];
|
|
963
|
+
const args = area === "skills" ? rest : rest.slice(1);
|
|
964
|
+
if (!sub || sub === "list") {
|
|
965
|
+
const { flags } = parseFlags(args, ["json", "limit"]);
|
|
966
|
+
const params = new URLSearchParams();
|
|
967
|
+
if (flags.limit && flags.limit !== true) params.set("limit", String(flags.limit));
|
|
968
|
+
const result = await request("GET", `/api/agent/skills${params.toString() ? `?${params}` : ""}`);
|
|
969
|
+
print(result);
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
if (sub === "search") {
|
|
973
|
+
const { flags, positional } = parseFlags(args, ["json", "limit"]);
|
|
974
|
+
const q = positional.join(" ").trim();
|
|
975
|
+
if (!q) throw new Error("Usage: llama skills search <query> [--limit 20]");
|
|
976
|
+
const params = new URLSearchParams({ q });
|
|
977
|
+
if (flags.limit && flags.limit !== true) params.set("limit", String(flags.limit));
|
|
978
|
+
const result = await request("GET", `/api/agent/skills?${params}`);
|
|
979
|
+
print(result);
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
if (sub === "show" || sub === "read") {
|
|
983
|
+
const { flags, positional } = parseFlags(args, ["json"]);
|
|
984
|
+
const slug = positional[0];
|
|
985
|
+
if (!slug) throw new Error("Usage: llama skills show <slug> [--json]");
|
|
986
|
+
const result = await request("GET", `/api/agent/skills/${encodeURIComponent(slug)}`);
|
|
987
|
+
if (flags.json) {
|
|
988
|
+
print(result);
|
|
989
|
+
} else {
|
|
990
|
+
process.stdout.write(`${result.skill?.content || JSON.stringify(result, null, 2)}\n`);
|
|
991
|
+
}
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
throw new Error(`Unknown skills subcommand "${sub}". Use: list / search / show.`);
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
if (area === "explain" || (area === "agent" && action === "explain")) {
|
|
998
|
+
const args = area === "explain" ? [action, ...rest].filter(Boolean) : rest;
|
|
999
|
+
const { flags, positional } = parseFlags(args, ["json", "type", "id", "lang"]);
|
|
1000
|
+
const params = new URLSearchParams();
|
|
1001
|
+
const q = positional.join(" ").trim();
|
|
1002
|
+
if (q) params.set("q", q);
|
|
1003
|
+
if (flags.type && flags.type !== true) params.set("type", String(flags.type));
|
|
1004
|
+
if (flags.id && flags.id !== true) params.set("id", String(flags.id));
|
|
1005
|
+
if (flags.lang === "zh") params.set("lang", "zh");
|
|
1006
|
+
if (!params.has("q") && !(params.has("type") && params.has("id"))) {
|
|
1007
|
+
throw new Error("Usage: llama explain <url-or-object> OR llama explain --type <type> --id <id>");
|
|
1008
|
+
}
|
|
1009
|
+
const result = await request("GET", `/api/agent/explain?${params}`);
|
|
1010
|
+
if (flags.json) {
|
|
1011
|
+
print(result);
|
|
1012
|
+
} else {
|
|
1013
|
+
const target = result.result?.target;
|
|
1014
|
+
const lifecycle = result.result?.lifecycle || [];
|
|
1015
|
+
const lines = [
|
|
1016
|
+
`${target?.objectType || "object"} ${target?.objectId || ""}`,
|
|
1017
|
+
`Status: ${target?.status || "unknown"}`,
|
|
1018
|
+
`Title: ${target?.title || "Untitled"}`,
|
|
1019
|
+
target?.detail ? `Detail: ${target.detail}` : null,
|
|
1020
|
+
target?.url ? `URL: ${target.url}` : null,
|
|
1021
|
+
`Lifecycle events: ${lifecycle.length}`,
|
|
1022
|
+
].filter(Boolean);
|
|
1023
|
+
if (lifecycle[0]) {
|
|
1024
|
+
lines.push(
|
|
1025
|
+
`Latest lifecycle: ${lifecycle[0].action} by ${lifecycle[0].actor_label || "unknown"} at ${lifecycle[0].created_at}`,
|
|
1026
|
+
);
|
|
1027
|
+
if (lifecycle[0].reason) lines.push(`Reason: ${lifecycle[0].reason}`);
|
|
1028
|
+
}
|
|
1029
|
+
process.stdout.write(`${lines.join("\n")}\n`);
|
|
1030
|
+
}
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
862
1034
|
// `llama pitch ...` — external founder-pitch family. No Llama token
|
|
863
1035
|
// required; bootstraps a session against /api/external/* via PoW + cookie.
|
|
864
1036
|
// See lib/external.mjs and AGENT_BRIEFING.md for the full surface.
|
|
@@ -1160,6 +1332,19 @@ https://command.llamaventures.vc/settings/tokens, run
|
|
|
1160
1332
|
return;
|
|
1161
1333
|
}
|
|
1162
1334
|
|
|
1335
|
+
if (area === "deal" && action === "agent") {
|
|
1336
|
+
const sub = rest[0];
|
|
1337
|
+
const dealId = rest[1];
|
|
1338
|
+
const { flags, positional } = parseFlags(rest.slice(2), ["message"]);
|
|
1339
|
+
const message =
|
|
1340
|
+
flags.message && flags.message !== true ? String(flags.message) : positional.join(" ").trim();
|
|
1341
|
+
if (sub !== "run" || !dealId || !message) {
|
|
1342
|
+
throw new Error(`Usage: llama deal agent run <dealId> --message "what the server agent should do"`);
|
|
1343
|
+
}
|
|
1344
|
+
await runDealAgentViaThread(dealId, message, "CLI agent run");
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1163
1348
|
// ----- Deal enrichment: evidence plan + server-side enrichment trigger -----
|
|
1164
1349
|
// The server owns Monid credentials and all write/audit behavior. CLI only
|
|
1165
1350
|
// passes intent; default is dry-run so agents can inspect the harness before
|
|
@@ -1181,31 +1366,35 @@ https://command.llamaventures.vc/settings/tokens, run
|
|
|
1181
1366
|
"budget-cents",
|
|
1182
1367
|
"memo",
|
|
1183
1368
|
"generate-memo",
|
|
1369
|
+
"generateMemo",
|
|
1184
1370
|
"prompt",
|
|
1185
1371
|
"handoff",
|
|
1372
|
+
"harness-only",
|
|
1373
|
+
"message",
|
|
1186
1374
|
]);
|
|
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;
|
|
1375
|
+
const sources = splitCsvFlag(flags.sources);
|
|
1194
1376
|
const budgetCents =
|
|
1195
1377
|
flags["budget-cents"] !== undefined && flags["budget-cents"] !== true
|
|
1196
1378
|
? Number(flags["budget-cents"])
|
|
1197
1379
|
: undefined;
|
|
1380
|
+
const apply = boolFlag(flags, "apply");
|
|
1381
|
+
const executor = flags.executor && flags.executor !== true ? String(flags.executor) : "server_agent";
|
|
1382
|
+
|
|
1383
|
+
if (apply && executor === "server_agent" && !boolFlag(flags, "harness-only")) {
|
|
1384
|
+
await runDealAgentViaThread(dealId, buildEnrichmentAgentMessage(flags), "CLI enrichment");
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1198
1387
|
|
|
1199
1388
|
const result = await request(
|
|
1200
1389
|
"POST",
|
|
1201
1390
|
`/api/deals/${encodeURIComponent(dealId)}/enrich`,
|
|
1202
1391
|
{
|
|
1203
|
-
dryRun:
|
|
1204
|
-
apply
|
|
1205
|
-
executor
|
|
1392
|
+
dryRun: apply ? false : true,
|
|
1393
|
+
apply,
|
|
1394
|
+
executor,
|
|
1206
1395
|
sources,
|
|
1207
1396
|
budgetCents,
|
|
1208
|
-
generateMemo: flags
|
|
1397
|
+
generateMemo: boolFlag(flags, "memo", "generate-memo", "generateMemo"),
|
|
1209
1398
|
}
|
|
1210
1399
|
);
|
|
1211
1400
|
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.
|
|
3
|
+
"version": "1.15.0",
|
|
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,447 @@
|
|
|
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 { existsSync } from "node:fs";
|
|
7
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
8
|
+
import os from "node:os";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
|
|
12
|
+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
13
|
+
assert.equal(
|
|
14
|
+
existsSync(path.join(repoRoot, "docs/agent-skills.bundle.json")),
|
|
15
|
+
false,
|
|
16
|
+
"public llama-cli must not bundle private Llama OS skill content",
|
|
17
|
+
);
|
|
18
|
+
assert.equal(
|
|
19
|
+
existsSync(path.join(repoRoot, "src/data/llama-os-skills.bundle.json")),
|
|
20
|
+
false,
|
|
21
|
+
"public llama-cli must not copy the Command-side skill mirror",
|
|
22
|
+
);
|
|
23
|
+
const calls = [];
|
|
24
|
+
let threadSeq = 0;
|
|
25
|
+
|
|
26
|
+
async function readJson(req) {
|
|
27
|
+
let raw = "";
|
|
28
|
+
for await (const chunk of req) raw += chunk;
|
|
29
|
+
if (!raw) return null;
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(raw);
|
|
32
|
+
} catch {
|
|
33
|
+
return raw;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function writeJson(res, data) {
|
|
38
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
39
|
+
res.end(JSON.stringify(data));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function writeSse(res) {
|
|
43
|
+
res.writeHead(200, {
|
|
44
|
+
"Content-Type": "text/event-stream",
|
|
45
|
+
"Cache-Control": "no-cache",
|
|
46
|
+
Connection: "keep-alive",
|
|
47
|
+
});
|
|
48
|
+
const events = [
|
|
49
|
+
{ tool_use: { name: "read_typed_factual_layer" } },
|
|
50
|
+
{ tool_result: { name: "read_typed_factual_layer", ok: true, summary: "ok" } },
|
|
51
|
+
{ text: "agent done" },
|
|
52
|
+
];
|
|
53
|
+
for (const event of events) {
|
|
54
|
+
res.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
55
|
+
}
|
|
56
|
+
res.end();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const server = createServer(async (req, res) => {
|
|
60
|
+
try {
|
|
61
|
+
const body = await readJson(req);
|
|
62
|
+
const url = new URL(req.url, "http://localhost");
|
|
63
|
+
calls.push({
|
|
64
|
+
method: req.method,
|
|
65
|
+
path: url.pathname,
|
|
66
|
+
query: Object.fromEntries(url.searchParams.entries()),
|
|
67
|
+
body,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
if (req.method === "GET" && url.pathname === "/api/agent/manifest") {
|
|
71
|
+
writeJson(res, {
|
|
72
|
+
ok: true,
|
|
73
|
+
briefing: "runtime briefing: use skills_search, skills_read, and object_inspect",
|
|
74
|
+
llama_os: {
|
|
75
|
+
visible_skill_count: 49,
|
|
76
|
+
included_skill_count: Number(url.searchParams.get("limit") || 25),
|
|
77
|
+
},
|
|
78
|
+
skills: [
|
|
79
|
+
{
|
|
80
|
+
slug: "llama-command",
|
|
81
|
+
description: "Llama Command runtime skill",
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
});
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (req.method === "GET" && url.pathname === "/api/agent/skills") {
|
|
89
|
+
writeJson(res, {
|
|
90
|
+
ok: true,
|
|
91
|
+
q: url.searchParams.get("q"),
|
|
92
|
+
count: 1,
|
|
93
|
+
skills: [
|
|
94
|
+
{
|
|
95
|
+
slug: "llama-command",
|
|
96
|
+
description: "Llama Command runtime skill",
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
});
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (req.method === "GET" && url.pathname === "/api/agent/skills/llama-command") {
|
|
104
|
+
writeJson(res, {
|
|
105
|
+
ok: true,
|
|
106
|
+
skill: {
|
|
107
|
+
slug: "llama-command",
|
|
108
|
+
content: "---\nname: llama-command\n---\n# Llama Command runtime skill\n",
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (req.method === "GET" && url.pathname === "/api/agent/explain") {
|
|
115
|
+
writeJson(res, {
|
|
116
|
+
ok: true,
|
|
117
|
+
result: {
|
|
118
|
+
target: {
|
|
119
|
+
objectType: "wiki_article",
|
|
120
|
+
objectId: "missing-page",
|
|
121
|
+
status: "deleted",
|
|
122
|
+
title: "Missing Page",
|
|
123
|
+
detail: "Deleted by Kevin Yu",
|
|
124
|
+
url: "https://command.llamaventures.vc/wiki/missing-page",
|
|
125
|
+
},
|
|
126
|
+
lifecycle: [
|
|
127
|
+
{
|
|
128
|
+
action: "deleted",
|
|
129
|
+
actor_label: "Kevin Yu",
|
|
130
|
+
created_at: "2026-06-15T19:02:00Z",
|
|
131
|
+
reason: "user_deleted",
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (req.method === "POST" && /^\/api\/deals\/[^/]+\/threads$/.test(url.pathname)) {
|
|
140
|
+
threadSeq += 1;
|
|
141
|
+
writeJson(res, { id: `thread-${threadSeq}` });
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (req.method === "POST" && /^\/api\/deals\/[^/]+\/threads\/[^/]+$/.test(url.pathname)) {
|
|
146
|
+
writeSse(res);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (req.method === "POST" && /^\/api\/deals\/[^/]+\/enrich$/.test(url.pathname)) {
|
|
151
|
+
writeJson(res, {
|
|
152
|
+
ok: true,
|
|
153
|
+
agentHarness: {
|
|
154
|
+
handoffPrompt: "mock handoff prompt",
|
|
155
|
+
systemInjection: "mock system injection",
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
162
|
+
res.end(JSON.stringify({ error: `Unexpected route ${req.method} ${url.pathname}` }));
|
|
163
|
+
} catch (err) {
|
|
164
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
165
|
+
res.end(JSON.stringify({ error: err?.message ?? String(err) }));
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
function listen(server) {
|
|
170
|
+
return new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function close(server) {
|
|
174
|
+
return new Promise((resolve, reject) => {
|
|
175
|
+
server.close((err) => (err ? reject(err) : resolve()));
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function childEnv(baseUrl, homeDir) {
|
|
180
|
+
return {
|
|
181
|
+
...process.env,
|
|
182
|
+
HOME: homeDir,
|
|
183
|
+
LLAMA_API_URL: baseUrl,
|
|
184
|
+
LLAMA_TOKEN: "llc_mock_agent_routing",
|
|
185
|
+
PATH: "/usr/bin:/bin",
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function resetCalls() {
|
|
190
|
+
calls.length = 0;
|
|
191
|
+
threadSeq = 0;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function paths() {
|
|
195
|
+
return calls.map((call) => `${call.method} ${call.path}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function assertNoEnrichCall() {
|
|
199
|
+
assert.equal(
|
|
200
|
+
calls.some((call) => call.path.endsWith("/enrich")),
|
|
201
|
+
false,
|
|
202
|
+
`expected no /enrich call, got ${paths().join(", ")}`,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function assertThreadRun({ title, messageIncludes }) {
|
|
207
|
+
assert.equal(calls.length, 2, `expected thread create + SSE run, got ${paths().join(", ")}`);
|
|
208
|
+
assert.match(calls[0].path, /^\/api\/deals\/[^/]+\/threads$/);
|
|
209
|
+
assert.equal(calls[0].body?.title, title);
|
|
210
|
+
assert.match(calls[1].path, /^\/api\/deals\/[^/]+\/threads\/thread-1$/);
|
|
211
|
+
for (const needle of messageIncludes) {
|
|
212
|
+
assert.match(calls[1].body?.message ?? "", new RegExp(escapeRegExp(needle)));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function escapeRegExp(value) {
|
|
217
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function runCli(args, baseUrl, homeDir) {
|
|
221
|
+
const child = spawn(process.execPath, ["bin/llama.mjs", ...args], {
|
|
222
|
+
cwd: repoRoot,
|
|
223
|
+
env: childEnv(baseUrl, homeDir),
|
|
224
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
225
|
+
});
|
|
226
|
+
let stdout = "";
|
|
227
|
+
let stderr = "";
|
|
228
|
+
child.stdout.on("data", (chunk) => {
|
|
229
|
+
stdout += chunk;
|
|
230
|
+
});
|
|
231
|
+
child.stderr.on("data", (chunk) => {
|
|
232
|
+
stderr += chunk;
|
|
233
|
+
});
|
|
234
|
+
const code = await new Promise((resolve) => child.on("close", resolve));
|
|
235
|
+
assert.equal(code, 0, `CLI failed (${code})\nSTDOUT:\n${stdout}\nSTDERR:\n${stderr}`);
|
|
236
|
+
return { stdout, stderr };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function callMcpTool(name, args, baseUrl, homeDir) {
|
|
240
|
+
const child = spawn(process.execPath, ["bin/llama-mcp.mjs"], {
|
|
241
|
+
cwd: repoRoot,
|
|
242
|
+
env: childEnv(baseUrl, homeDir),
|
|
243
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
244
|
+
});
|
|
245
|
+
let stderr = "";
|
|
246
|
+
let buffer = "";
|
|
247
|
+
child.stderr.on("data", (chunk) => {
|
|
248
|
+
stderr += chunk;
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const result = await new Promise((resolve, reject) => {
|
|
252
|
+
const timeout = setTimeout(() => {
|
|
253
|
+
child.kill();
|
|
254
|
+
reject(new Error(`Timed out waiting for MCP response\nSTDERR:\n${stderr}`));
|
|
255
|
+
}, 8000);
|
|
256
|
+
|
|
257
|
+
child.stdout.on("data", (chunk) => {
|
|
258
|
+
buffer += chunk;
|
|
259
|
+
let idx;
|
|
260
|
+
while ((idx = buffer.indexOf("\n")) >= 0) {
|
|
261
|
+
const line = buffer.slice(0, idx).trim();
|
|
262
|
+
buffer = buffer.slice(idx + 1);
|
|
263
|
+
if (!line) continue;
|
|
264
|
+
let msg;
|
|
265
|
+
try {
|
|
266
|
+
msg = JSON.parse(line);
|
|
267
|
+
} catch {
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
if (msg.id === 2) {
|
|
271
|
+
clearTimeout(timeout);
|
|
272
|
+
child.kill();
|
|
273
|
+
resolve(msg);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
child.on("error", (err) => {
|
|
279
|
+
clearTimeout(timeout);
|
|
280
|
+
reject(err);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
child.stdin.write(
|
|
284
|
+
[
|
|
285
|
+
JSON.stringify({
|
|
286
|
+
jsonrpc: "2.0",
|
|
287
|
+
id: 1,
|
|
288
|
+
method: "initialize",
|
|
289
|
+
params: {
|
|
290
|
+
protocolVersion: "2024-11-05",
|
|
291
|
+
capabilities: {},
|
|
292
|
+
clientInfo: { name: "routing-test", version: "1" },
|
|
293
|
+
},
|
|
294
|
+
}),
|
|
295
|
+
JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }),
|
|
296
|
+
JSON.stringify({
|
|
297
|
+
jsonrpc: "2.0",
|
|
298
|
+
id: 2,
|
|
299
|
+
method: "tools/call",
|
|
300
|
+
params: { name, arguments: args },
|
|
301
|
+
}),
|
|
302
|
+
].join("\n") + "\n",
|
|
303
|
+
);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
assert.ok(!result.error, `MCP returned error: ${JSON.stringify(result.error)}`);
|
|
307
|
+
return result.result;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
await listen(server);
|
|
311
|
+
const address = server.address();
|
|
312
|
+
const baseUrl = `http://${address.address}:${address.port}`;
|
|
313
|
+
const homeDir = await mkdtemp(path.join(os.tmpdir(), "llama-cli-routing-"));
|
|
314
|
+
|
|
315
|
+
try {
|
|
316
|
+
resetCalls();
|
|
317
|
+
const bootstrapRun = await runCli(["agent", "bootstrap", "--limit", "3"], baseUrl, homeDir);
|
|
318
|
+
assert.match(bootstrapRun.stdout, /runtime briefing/);
|
|
319
|
+
assert.deepEqual(paths(), ["GET /api/agent/manifest"]);
|
|
320
|
+
assert.equal(calls[0].query.limit, "3");
|
|
321
|
+
|
|
322
|
+
resetCalls();
|
|
323
|
+
const skillSearchRun = await runCli(["skills", "search", "pipeline", "--limit", "5"], baseUrl, homeDir);
|
|
324
|
+
assert.match(skillSearchRun.stdout, /llama-command/);
|
|
325
|
+
assert.deepEqual(paths(), ["GET /api/agent/skills"]);
|
|
326
|
+
assert.equal(calls[0].query.q, "pipeline");
|
|
327
|
+
assert.equal(calls[0].query.limit, "5");
|
|
328
|
+
|
|
329
|
+
resetCalls();
|
|
330
|
+
const skillShowRun = await runCli(["skills", "show", "llama-command"], baseUrl, homeDir);
|
|
331
|
+
assert.match(skillShowRun.stdout, /# Llama Command runtime skill/);
|
|
332
|
+
assert.deepEqual(paths(), ["GET /api/agent/skills/llama-command"]);
|
|
333
|
+
|
|
334
|
+
resetCalls();
|
|
335
|
+
const explainRun = await runCli(["explain", "https://command.llamaventures.vc/wiki/missing-page"], baseUrl, homeDir);
|
|
336
|
+
assert.match(explainRun.stdout, /Status: deleted/);
|
|
337
|
+
assert.match(explainRun.stdout, /Deleted by Kevin Yu/);
|
|
338
|
+
assert.deepEqual(paths(), ["GET /api/agent/explain"]);
|
|
339
|
+
assert.equal(calls[0].query.q, "https://command.llamaventures.vc/wiki/missing-page");
|
|
340
|
+
|
|
341
|
+
resetCalls();
|
|
342
|
+
const enrichRun = await runCli(
|
|
343
|
+
[
|
|
344
|
+
"deal",
|
|
345
|
+
"enrich",
|
|
346
|
+
"deal-cli",
|
|
347
|
+
"--apply",
|
|
348
|
+
"--executor",
|
|
349
|
+
"server_agent",
|
|
350
|
+
"--sources",
|
|
351
|
+
"website,monid",
|
|
352
|
+
"--budget-cents",
|
|
353
|
+
"12",
|
|
354
|
+
],
|
|
355
|
+
baseUrl,
|
|
356
|
+
homeDir,
|
|
357
|
+
);
|
|
358
|
+
assert.match(enrichRun.stdout, /agent done/);
|
|
359
|
+
assertNoEnrichCall();
|
|
360
|
+
assertThreadRun({
|
|
361
|
+
title: "CLI enrichment",
|
|
362
|
+
messageIncludes: ["website, monid", "12 cents", "upsert_typed_fact"],
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
resetCalls();
|
|
366
|
+
await runCli(
|
|
367
|
+
["deal", "enrich", "deal-cli", "--apply", "--executor", "server_agent", "--harness-only"],
|
|
368
|
+
baseUrl,
|
|
369
|
+
homeDir,
|
|
370
|
+
);
|
|
371
|
+
assert.deepEqual(paths(), ["POST /api/deals/deal-cli/enrich"]);
|
|
372
|
+
assert.equal(calls[0].body?.apply, true);
|
|
373
|
+
assert.equal(calls[0].body?.dryRun, false);
|
|
374
|
+
assert.equal(calls[0].body?.executor, "server_agent");
|
|
375
|
+
|
|
376
|
+
resetCalls();
|
|
377
|
+
const agentRun = await runCli(
|
|
378
|
+
["deal", "agent", "run", "deal-cli", "--message", "custom server task"],
|
|
379
|
+
baseUrl,
|
|
380
|
+
homeDir,
|
|
381
|
+
);
|
|
382
|
+
assert.match(agentRun.stdout, /agent done/);
|
|
383
|
+
assertNoEnrichCall();
|
|
384
|
+
assertThreadRun({
|
|
385
|
+
title: "CLI agent run",
|
|
386
|
+
messageIncludes: ["custom server task"],
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
resetCalls();
|
|
390
|
+
const mcpResult = await callMcpTool(
|
|
391
|
+
"deal_enrich",
|
|
392
|
+
{
|
|
393
|
+
dealId: "deal-mcp",
|
|
394
|
+
apply: true,
|
|
395
|
+
executor: "server_agent",
|
|
396
|
+
sources: ["web", "monid"],
|
|
397
|
+
budgetCents: 7,
|
|
398
|
+
},
|
|
399
|
+
baseUrl,
|
|
400
|
+
homeDir,
|
|
401
|
+
);
|
|
402
|
+
assertNoEnrichCall();
|
|
403
|
+
assertThreadRun({
|
|
404
|
+
title: "MCP enrichment",
|
|
405
|
+
messageIncludes: ["web, monid", "7 cents", "upsert_typed_fact"],
|
|
406
|
+
});
|
|
407
|
+
const payload = JSON.parse(mcpResult.content?.[0]?.text ?? "{}");
|
|
408
|
+
assert.equal(payload.ok, true);
|
|
409
|
+
assert.equal(payload.threadId, "thread-1");
|
|
410
|
+
assert.equal(payload.text, "agent done");
|
|
411
|
+
|
|
412
|
+
resetCalls();
|
|
413
|
+
const mcpBootstrap = await callMcpTool("agent_bootstrap", { limit: 2 }, baseUrl, homeDir);
|
|
414
|
+
const bootstrapPayload = JSON.parse(mcpBootstrap.content?.[0]?.text ?? "{}");
|
|
415
|
+
assert.equal(bootstrapPayload.ok, true);
|
|
416
|
+
assert.deepEqual(paths(), ["GET /api/agent/manifest"]);
|
|
417
|
+
assert.equal(calls[0].query.limit, "2");
|
|
418
|
+
|
|
419
|
+
resetCalls();
|
|
420
|
+
const mcpSkills = await callMcpTool("skills_search", { q: "command", limit: 4 }, baseUrl, homeDir);
|
|
421
|
+
const skillsPayload = JSON.parse(mcpSkills.content?.[0]?.text ?? "{}");
|
|
422
|
+
assert.equal(skillsPayload.skills?.[0]?.slug, "llama-command");
|
|
423
|
+
assert.deepEqual(paths(), ["GET /api/agent/skills"]);
|
|
424
|
+
assert.equal(calls[0].query.q, "command");
|
|
425
|
+
|
|
426
|
+
resetCalls();
|
|
427
|
+
const mcpSkillRead = await callMcpTool("skills_read", { slug: "llama-command" }, baseUrl, homeDir);
|
|
428
|
+
const skillPayload = JSON.parse(mcpSkillRead.content?.[0]?.text ?? "{}");
|
|
429
|
+
assert.match(skillPayload.skill?.content ?? "", /# Llama Command runtime skill/);
|
|
430
|
+
assert.deepEqual(paths(), ["GET /api/agent/skills/llama-command"]);
|
|
431
|
+
|
|
432
|
+
resetCalls();
|
|
433
|
+
const mcpInspect = await callMcpTool(
|
|
434
|
+
"object_inspect",
|
|
435
|
+
{ q: "https://command.llamaventures.vc/wiki/missing-page" },
|
|
436
|
+
baseUrl,
|
|
437
|
+
homeDir,
|
|
438
|
+
);
|
|
439
|
+
const inspectPayload = JSON.parse(mcpInspect.content?.[0]?.text ?? "{}");
|
|
440
|
+
assert.equal(inspectPayload.result?.target?.status, "deleted");
|
|
441
|
+
assert.deepEqual(paths(), ["GET /api/agent/explain"]);
|
|
442
|
+
|
|
443
|
+
console.log("agent routing verification passed");
|
|
444
|
+
} finally {
|
|
445
|
+
await close(server);
|
|
446
|
+
await rm(homeDir, { recursive: true, force: true });
|
|
447
|
+
}
|