@llamaventures/cli 1.2.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/AGENT_BRIEFING.md +181 -0
- package/LICENSE +21 -0
- package/README.md +273 -0
- package/bin/llama-mcp.mjs +606 -0
- package/bin/llama.mjs +1441 -0
- package/lib/client.mjs +215 -0
- package/lib/external.mjs +386 -0
- package/package.json +42 -0
package/bin/llama.mjs
ADDED
|
@@ -0,0 +1,1441 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import readline from "readline";
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_BASE_URL,
|
|
6
|
+
LEGACY_DIR,
|
|
7
|
+
LEGACY_FILE,
|
|
8
|
+
TOKEN_DIR,
|
|
9
|
+
TOKEN_FILE,
|
|
10
|
+
getAuthHeaders,
|
|
11
|
+
getBaseUrl,
|
|
12
|
+
getToken,
|
|
13
|
+
print,
|
|
14
|
+
readBriefing,
|
|
15
|
+
readCanonicalToken,
|
|
16
|
+
readLegacyConfig,
|
|
17
|
+
request,
|
|
18
|
+
tryGcloudIdentityToken,
|
|
19
|
+
writeCanonicalToken,
|
|
20
|
+
writeLegacyConfig,
|
|
21
|
+
} from "../lib/client.mjs";
|
|
22
|
+
import {
|
|
23
|
+
clearExternalSession,
|
|
24
|
+
EXTERNAL_SESSION_FILE,
|
|
25
|
+
getExternalSessionStatus,
|
|
26
|
+
readExternalSession,
|
|
27
|
+
sendExternalMessage,
|
|
28
|
+
startExternalSession,
|
|
29
|
+
uploadExternalFile,
|
|
30
|
+
} from "../lib/external.mjs";
|
|
31
|
+
|
|
32
|
+
function parseFlags(args) {
|
|
33
|
+
const flags = {};
|
|
34
|
+
const positional = [];
|
|
35
|
+
for (let i = 0; i < args.length; i++) {
|
|
36
|
+
const arg = args[i];
|
|
37
|
+
if (arg.startsWith("--")) {
|
|
38
|
+
const key = arg.slice(2);
|
|
39
|
+
const next = args[i + 1];
|
|
40
|
+
if (!next || next.startsWith("--")) {
|
|
41
|
+
flags[key] = true;
|
|
42
|
+
} else {
|
|
43
|
+
flags[key] = next;
|
|
44
|
+
i++;
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
positional.push(arg);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return { flags, positional };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Client-side fuzzy match — used as a fallback when the server hasn't yet
|
|
54
|
+
// shipped the search/filter API (Fix B, 2026-04-25). Once the server
|
|
55
|
+
// returns the `{deals,total,limit,offset}` envelope, this path is never
|
|
56
|
+
// taken.
|
|
57
|
+
function clientSideMatch(deal, filters) {
|
|
58
|
+
const incl = (haystack, needle) =>
|
|
59
|
+
!!haystack && String(haystack).toLowerCase().includes(needle.toLowerCase());
|
|
60
|
+
const eq = (haystack, needle) =>
|
|
61
|
+
String(haystack ?? "").toLowerCase() === String(needle).toLowerCase();
|
|
62
|
+
|
|
63
|
+
if (filters.q) {
|
|
64
|
+
const fields = [
|
|
65
|
+
deal.companyName, deal.founders, deal.founderInfo,
|
|
66
|
+
deal.description, deal.notes, deal.dealOwner,
|
|
67
|
+
deal.source, deal.location,
|
|
68
|
+
];
|
|
69
|
+
if (!fields.some((f) => incl(f, filters.q))) return false;
|
|
70
|
+
}
|
|
71
|
+
if (filters.companyName && !incl(deal.companyName, filters.companyName)) return false;
|
|
72
|
+
if (filters.founder && !(incl(deal.founders, filters.founder) || incl(deal.founderInfo, filters.founder))) return false;
|
|
73
|
+
if (filters.owner && !incl(deal.dealOwner, filters.owner)) return false;
|
|
74
|
+
if (filters.status && !eq(deal.status, filters.status)) return false;
|
|
75
|
+
if (filters.theirStage && !eq(deal.theirStage, filters.theirStage)) return false;
|
|
76
|
+
if (filters.stage && !eq(deal.stage, filters.stage)) return false;
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Build the `?...` query string for /api/deals from CLI flags + positional q.
|
|
81
|
+
function buildDealsQuery(q, flags) {
|
|
82
|
+
const params = new URLSearchParams();
|
|
83
|
+
if (q) params.set("q", q);
|
|
84
|
+
for (const key of ["companyName", "founder", "owner", "status", "theirStage", "stage", "limit", "offset"]) {
|
|
85
|
+
if (flags[key] !== undefined && flags[key] !== true) {
|
|
86
|
+
params.set(key, String(flags[key]));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return params;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Hit /api/deals with the given filters. Handles both response shapes:
|
|
93
|
+
// - bare array (old API or no params) → client-side filter, return envelope
|
|
94
|
+
// - {deals,total,limit,offset} (new API) → return as-is
|
|
95
|
+
async function searchDeals(q, flags) {
|
|
96
|
+
const params = buildDealsQuery(q, flags);
|
|
97
|
+
const qs = params.toString();
|
|
98
|
+
const result = await request("GET", `/api/deals${qs ? `?${qs}` : ""}`);
|
|
99
|
+
|
|
100
|
+
if (Array.isArray(result)) {
|
|
101
|
+
// Fix B not deployed yet, OR no params sent. Filter locally so the
|
|
102
|
+
// CLI behavior is consistent regardless of server version.
|
|
103
|
+
const filters = {
|
|
104
|
+
q,
|
|
105
|
+
companyName: flags.companyName,
|
|
106
|
+
founder: flags.founder,
|
|
107
|
+
owner: flags.owner,
|
|
108
|
+
status: flags.status,
|
|
109
|
+
theirStage: flags.theirStage,
|
|
110
|
+
stage: flags.stage,
|
|
111
|
+
};
|
|
112
|
+
const filtered = result.filter((d) => clientSideMatch(d, filters));
|
|
113
|
+
const limit = Number(flags.limit) > 0 ? Number(flags.limit) : 200;
|
|
114
|
+
const offset = Number(flags.offset) > 0 ? Number(flags.offset) : 0;
|
|
115
|
+
return {
|
|
116
|
+
deals: filtered.slice(offset, offset + limit),
|
|
117
|
+
total: filtered.length,
|
|
118
|
+
limit,
|
|
119
|
+
offset,
|
|
120
|
+
_source: "client-filter",
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function usage() {
|
|
127
|
+
console.log(`Llama Command CLI
|
|
128
|
+
|
|
129
|
+
Agent onboarding (run once on first install):
|
|
130
|
+
llama agent-onboard # print AGENT_BRIEFING.md — the workflow contract for AI agents
|
|
131
|
+
|
|
132
|
+
External pitch — talk to Llama Ventures' intake agent (no token required):
|
|
133
|
+
llama pitch start --name "Jane Doe" --email "jane@acme.ai"
|
|
134
|
+
llama pitch say "We're building X..." # single message, prints reply
|
|
135
|
+
llama pitch upload ./deck.pdf # attach a file
|
|
136
|
+
llama pitch # interactive REPL (existing session)
|
|
137
|
+
llama pitch status # session info
|
|
138
|
+
llama pitch end # clear local session
|
|
139
|
+
|
|
140
|
+
Setup:
|
|
141
|
+
llama auth status # show current credentials + verify with server
|
|
142
|
+
llama token set <llc_token> [--base https://command.llamaventures.vc]
|
|
143
|
+
llama token show
|
|
144
|
+
|
|
145
|
+
Zero-config: if you've already run \`gcloud auth login\` with your
|
|
146
|
+
@llamaventures.vc account, you don't need to set anything — the CLI
|
|
147
|
+
auto-detects \`gcloud auth print-identity-token\` and uses Bearer auth.
|
|
148
|
+
Manually-set \`llc_\` tokens are used as a fallback.
|
|
149
|
+
|
|
150
|
+
Deals:
|
|
151
|
+
llama deal create "Company" --source <name> --description "..." --website https://...
|
|
152
|
+
llama deal show <dealId>
|
|
153
|
+
llama deal update <dealId> <field> <value>
|
|
154
|
+
llama deal search <query> [--founder name] [--owner <user-key>] [--status Diligence]
|
|
155
|
+
[--theirStage Raising] [--stage Seed]
|
|
156
|
+
[--limit 200] [--offset 0]
|
|
157
|
+
llama deal list [--owner ...] [--status ...] [...same flags as search]
|
|
158
|
+
|
|
159
|
+
Collaborators (besides owner — attribution candidates, no approval):
|
|
160
|
+
llama deal collab list <dealId>
|
|
161
|
+
llama deal collab add <dealId> --user <userId|email>
|
|
162
|
+
llama deal collab remove <dealId> --user <userId|email> # soft-delete
|
|
163
|
+
llama deal collab restore <dealId> --user <userId|email>
|
|
164
|
+
|
|
165
|
+
Soft-delete:
|
|
166
|
+
All UI/CLI deletes are soft. Real delete = direct DB only. Each
|
|
167
|
+
removal/restore writes a deal_events row so the timeline records who
|
|
168
|
+
did what when. Trash views via ?include_deleted=1 on read endpoints.
|
|
169
|
+
|
|
170
|
+
Brief blocks (text/link/embed/callout):
|
|
171
|
+
llama brief blocks <dealId> # list (excludes trashed)
|
|
172
|
+
llama brief block <dealId> <blockId> # fetch single block (with body)
|
|
173
|
+
llama brief delete <dealId> <blockId> # soft-delete
|
|
174
|
+
llama brief restore <dealId> <blockId>
|
|
175
|
+
|
|
176
|
+
Deal links (separate from brief link blocks — these live in deal_links):
|
|
177
|
+
llama deal link list <dealId> [--include-deleted]
|
|
178
|
+
llama deal link add <dealId> --url <url> [--label "..."]
|
|
179
|
+
llama deal link delete <dealId> <linkId> # soft-delete
|
|
180
|
+
llama deal link restore <dealId> <linkId>
|
|
181
|
+
|
|
182
|
+
Ownership:
|
|
183
|
+
llama claim <dealId> # propose self as owner
|
|
184
|
+
llama nominate <dealId> --user <userId> # partner nominates someone else
|
|
185
|
+
llama nominations list # pending nominations for me
|
|
186
|
+
llama nominations decide <approvalId> accepted|declined # accept/decline a nomination
|
|
187
|
+
|
|
188
|
+
Approvals (partner queue — self-claim approvals):
|
|
189
|
+
llama approvals list
|
|
190
|
+
llama approvals decide <approvalId> approved|rejected [--note "..."]
|
|
191
|
+
|
|
192
|
+
Timeline / Posts:
|
|
193
|
+
llama timeline <dealId> # full unified feed
|
|
194
|
+
llama post <dealId> "message body" [--link url] [--link-name "name"]
|
|
195
|
+
|
|
196
|
+
Brief blocks:
|
|
197
|
+
llama brief blocks <dealId> # list current block array
|
|
198
|
+
llama brief block <dealId> <blockId> # fetch one block's body (manifest in 'llama deal show')
|
|
199
|
+
llama brief add-text <dealId> --heading "..." --body "..."
|
|
200
|
+
llama brief add-link <dealId> --url "..." --label "..." [--description "..."]
|
|
201
|
+
llama brief add-embed <dealId> --url "..." [--label "..."]
|
|
202
|
+
llama brief add-callout <dealId> --tone insight|info|warning|success --heading "..." --body "..."
|
|
203
|
+
llama brief edit <dealId> <blockId> [--heading ...] [--body ...] [--url ...] [--label ...] [--tone ...]
|
|
204
|
+
[--source-section <key>] [--lock|--unlock] [--hide|--unhide]
|
|
205
|
+
llama brief delete <dealId> <blockId>
|
|
206
|
+
llama brief history <dealId> <blockId> [--limit 50] # prior versions of this block (newest first)
|
|
207
|
+
llama brief restore-version <dealId> <blockId> <historyId> # restore from a history entry; the outgoing
|
|
208
|
+
# version is itself snapshotted (reversible)
|
|
209
|
+
|
|
210
|
+
Common flags on every add-*:
|
|
211
|
+
--source-section <key> Target a structured section (team, highlights, recommendation,
|
|
212
|
+
<persona>_analysis, ...). Without this, blocks land in "_other"
|
|
213
|
+
at the bottom of the TOC. AI writers want this.
|
|
214
|
+
--reply-to <blockId> Make the block a reply to <blockId>. Snapshots parent's heading
|
|
215
|
+
+ 200-char excerpt into meta so the back-link survives parent
|
|
216
|
+
edits/deletes. Renders as an amber strip with a jump-link.
|
|
217
|
+
--position top|bottom Where to insert. Default: top (matches UI behavior since
|
|
218
|
+
2026-05-03). Use bottom for batched writes that need to
|
|
219
|
+
preserve insertion order.
|
|
220
|
+
|
|
221
|
+
Brief / persona refresh + agent-run revert:
|
|
222
|
+
llama deal refresh-brief <dealId> [--force] # re-eval stale sections
|
|
223
|
+
# --force = every unlocked watcher-managed section
|
|
224
|
+
llama deal refresh-persona <dealId> <persona-key> # server validates persona key
|
|
225
|
+
llama deal revert-run <dealId> <runId> --section <key> # legacy 4-section model only
|
|
226
|
+
# section: company|team|highlights|recommendation
|
|
227
|
+
|
|
228
|
+
Deal soft-delete / restore / trash list:
|
|
229
|
+
llama deal delete <dealId> # soft (audit-logged via deal_events)
|
|
230
|
+
llama deal restore <dealId> # ⚠ session-only on server today (token → 401)
|
|
231
|
+
llama deal trash # list deleted deals
|
|
232
|
+
|
|
233
|
+
Deal facts (AI-extracted or human-asserted, with verification):
|
|
234
|
+
llama deal fact list <dealId> # ⚠ session-only on server today
|
|
235
|
+
llama deal fact add <dealId> --category <cat> --claim "<text>" [--source <url>] [--confidence high|medium|low]
|
|
236
|
+
llama deal fact verify <dealId> <factId> --status confirmed|disputed [--corrected-value "..."]
|
|
237
|
+
|
|
238
|
+
Skill corrections (persona-owner pushback — read by persona-watcher):
|
|
239
|
+
llama skill-correction list <skill-slug> [--include-deleted]
|
|
240
|
+
llama skill-correction add <skill-slug> "<correction text>" [--deal <uuid>] [--block <blockId>]
|
|
241
|
+
llama skill-correction delete <id>
|
|
242
|
+
Server enforces persona owner OR system admin on POST/DELETE; GET is open.
|
|
243
|
+
External personas (owner_email=null, e.g. virtual-liu-yi) are admin-only for write.
|
|
244
|
+
|
|
245
|
+
Mentions / Inbox:
|
|
246
|
+
llama mentions # default: my unresolved cues
|
|
247
|
+
llama mentions list [--everyone] [--all] # --everyone = team-wide; --all = include resolved
|
|
248
|
+
llama mentions show <mentionId> # full row
|
|
249
|
+
llama mentions resolve <mentionId> # mark thread resolved (idempotent)
|
|
250
|
+
llama mentions unread # just the badge count
|
|
251
|
+
|
|
252
|
+
Wiki:
|
|
253
|
+
llama wiki search <query>
|
|
254
|
+
llama wiki read <slug>
|
|
255
|
+
llama wiki save <slug> --title "..." --content "..." --sources "url1;url2" [--type company] [--related "A;B"]
|
|
256
|
+
|
|
257
|
+
Admin (system admin only — server returns 403 for non-admin tokens):
|
|
258
|
+
llama admin auth-events [--kind X] [--actor email] [--subject email] [--since 24h|7d|30d|<ISO>] [--limit 100]
|
|
259
|
+
llama admin deal-events [--kind X] [--actor email] [--deal <uuid>] [--since 24h] [--limit 100]
|
|
260
|
+
llama admin agent-events [--kind tool_call|loop_stalled|max_turns_reached] [--agent-kind deal|secretary|main|inbox]
|
|
261
|
+
[--actor email] [--tool name] [--deal <uuid>] [--errors-only] [--since 24h] [--limit 100]
|
|
262
|
+
|
|
263
|
+
Same data as the /admin web console tabs (Auth events / Deal Activity / Agent Activity)
|
|
264
|
+
but scriptable. Pipe through jq / grep for monitoring & forensics.
|
|
265
|
+
|
|
266
|
+
Token discovery (in order):
|
|
267
|
+
1. $LLAMA_TOKEN env var
|
|
268
|
+
2. ~/.llama/token (canonical, single line)
|
|
269
|
+
3. ~/.llama-command/config.json (legacy v0.1 — auto-migrated forward on first read)
|
|
270
|
+
|
|
271
|
+
Env:
|
|
272
|
+
LLAMA_TOKEN token override
|
|
273
|
+
LLAMA_API_URL API base URL override
|
|
274
|
+
`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ============================================================
|
|
278
|
+
// `llama pitch` family — external founder-pitch intake
|
|
279
|
+
// ============================================================
|
|
280
|
+
//
|
|
281
|
+
// No Llama Command token required. Bootstraps a session against
|
|
282
|
+
// /api/external/* via PoW + cookie. Subcommands:
|
|
283
|
+
//
|
|
284
|
+
// llama pitch → REPL (requires existing session)
|
|
285
|
+
// llama pitch start --name X --email Y
|
|
286
|
+
// llama pitch say "<msg>"
|
|
287
|
+
// llama pitch upload <path>
|
|
288
|
+
// llama pitch status
|
|
289
|
+
// llama pitch end
|
|
290
|
+
|
|
291
|
+
async function handlePitch(action, rest) {
|
|
292
|
+
if (!action || action === "help" || action === "--help" || action === "-h") {
|
|
293
|
+
console.log(`Llama Ventures pitch intake — chat with our intake agent (no token required).
|
|
294
|
+
|
|
295
|
+
Setup:
|
|
296
|
+
llama pitch start --name "Your Name" --email "you@company.com"
|
|
297
|
+
|
|
298
|
+
Single message (non-interactive):
|
|
299
|
+
llama pitch say "We're building an AI dev tool for X..."
|
|
300
|
+
|
|
301
|
+
Upload a file (deck / pitch / one-pager):
|
|
302
|
+
llama pitch upload ./deck.pdf
|
|
303
|
+
|
|
304
|
+
Interactive REPL (requires existing session):
|
|
305
|
+
llama pitch
|
|
306
|
+
|
|
307
|
+
Inspect / clean up:
|
|
308
|
+
llama pitch status # session id, idle minutes, finalized?
|
|
309
|
+
llama pitch end # clear local session state
|
|
310
|
+
|
|
311
|
+
Caps (server-enforced):
|
|
312
|
+
5 sessions per IP per day, 3 per email per day, 30min idle timeout,
|
|
313
|
+
100 messages per session, 1M tokens per session.
|
|
314
|
+
`);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (action === "start") {
|
|
319
|
+
const { flags } = parseFlags(rest);
|
|
320
|
+
if (!flags.name || !flags.email) {
|
|
321
|
+
throw new Error(
|
|
322
|
+
"pitch start: --name and --email are required.\n" +
|
|
323
|
+
" Example: llama pitch start --name \"Jane Doe\" --email \"jane@acme.ai\""
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
const existing = readExternalSession();
|
|
327
|
+
if (existing && !existing.finalized) {
|
|
328
|
+
const status = getExternalSessionStatus();
|
|
329
|
+
if (status.active) {
|
|
330
|
+
throw new Error(
|
|
331
|
+
`An active pitch session already exists (started ${existing.started_at}, idle ${status.idle_minutes}min).\n` +
|
|
332
|
+
` Run \`llama pitch end\` to clear it, or \`llama pitch say "..."\` to continue.`
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
process.stderr.write("Computing proof-of-work + opening session...\n");
|
|
337
|
+
const session = await startExternalSession({
|
|
338
|
+
name: String(flags.name),
|
|
339
|
+
email: String(flags.email),
|
|
340
|
+
});
|
|
341
|
+
print({
|
|
342
|
+
session_id: session.session_id,
|
|
343
|
+
name: session.name,
|
|
344
|
+
email: session.email,
|
|
345
|
+
started_at: session.started_at,
|
|
346
|
+
hint: 'Now run `llama pitch say "..."` to chat, or just `llama pitch` for interactive REPL.',
|
|
347
|
+
});
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (action === "say") {
|
|
352
|
+
const message = rest.join(" ").trim();
|
|
353
|
+
if (!message) {
|
|
354
|
+
throw new Error('pitch say: message required. Example: llama pitch say "We\'re building X"');
|
|
355
|
+
}
|
|
356
|
+
const result = await sendExternalMessage(message);
|
|
357
|
+
process.stdout.write(result.text + "\n");
|
|
358
|
+
if (result.finalized) {
|
|
359
|
+
process.stderr.write("\n--- Pitch session finalized by the agent ---\n");
|
|
360
|
+
if (result.finalize_payload) {
|
|
361
|
+
process.stderr.write(JSON.stringify(result.finalize_payload, null, 2) + "\n");
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (action === "upload") {
|
|
368
|
+
const filePath = rest[0];
|
|
369
|
+
if (!filePath) {
|
|
370
|
+
throw new Error("pitch upload: file path required. Example: llama pitch upload ./deck.pdf");
|
|
371
|
+
}
|
|
372
|
+
process.stderr.write(`Uploading ${filePath}...\n`);
|
|
373
|
+
const result = await uploadExternalFile(filePath);
|
|
374
|
+
print(result);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (action === "status") {
|
|
379
|
+
print(getExternalSessionStatus());
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (action === "end") {
|
|
384
|
+
const had = readExternalSession();
|
|
385
|
+
clearExternalSession();
|
|
386
|
+
print({
|
|
387
|
+
ok: true,
|
|
388
|
+
cleared: !!had,
|
|
389
|
+
session_file: EXTERNAL_SESSION_FILE,
|
|
390
|
+
note: had
|
|
391
|
+
? "Local session state cleared. Server-side session may still be active until idle timeout (30min)."
|
|
392
|
+
: "No local session was active.",
|
|
393
|
+
});
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// No action → REPL mode (requires existing session)
|
|
398
|
+
if (action === undefined || (rest.length === 0 && !["start", "say", "upload", "status", "end"].includes(action))) {
|
|
399
|
+
// Treat any unknown bare action as "join existing session in REPL mode"
|
|
400
|
+
const session = readExternalSession();
|
|
401
|
+
if (!session) {
|
|
402
|
+
throw new Error(
|
|
403
|
+
"No active pitch session. Start one with:\n" +
|
|
404
|
+
' llama pitch start --name "Your Name" --email "you@company.com"'
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
if (session.finalized) {
|
|
408
|
+
throw new Error(
|
|
409
|
+
"This pitch session is finalized. Run `llama pitch end` then `pitch start` for a new one."
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
await runPitchRepl();
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
throw new Error(`Unknown pitch subcommand: ${action}. Run \`llama pitch help\` for the full list.`);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
async function runPitchRepl() {
|
|
420
|
+
const rl = readline.createInterface({
|
|
421
|
+
input: process.stdin,
|
|
422
|
+
output: process.stdout,
|
|
423
|
+
prompt: "you> ",
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
console.log("Connected to Llama Ventures intake agent. Type your pitch — :q to exit, :upload <path> to attach a file.");
|
|
427
|
+
console.log("");
|
|
428
|
+
|
|
429
|
+
const send = async (msg) => {
|
|
430
|
+
process.stdout.write("\nllama> ");
|
|
431
|
+
let buffered = "";
|
|
432
|
+
const result = await sendExternalMessage(msg, {
|
|
433
|
+
onChunk: (chunk) => {
|
|
434
|
+
process.stdout.write(chunk);
|
|
435
|
+
buffered += chunk;
|
|
436
|
+
},
|
|
437
|
+
});
|
|
438
|
+
if (!buffered) process.stdout.write(result.text);
|
|
439
|
+
process.stdout.write("\n\n");
|
|
440
|
+
if (result.finalized) {
|
|
441
|
+
console.log("--- Pitch session finalized ---");
|
|
442
|
+
if (result.finalize_payload) {
|
|
443
|
+
console.log(JSON.stringify(result.finalize_payload, null, 2));
|
|
444
|
+
}
|
|
445
|
+
rl.close();
|
|
446
|
+
return true;
|
|
447
|
+
}
|
|
448
|
+
return false;
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
rl.prompt();
|
|
452
|
+
rl.on("line", async (line) => {
|
|
453
|
+
const trimmed = line.trim();
|
|
454
|
+
if (trimmed === ":q" || trimmed === ":quit" || trimmed === ":exit") {
|
|
455
|
+
rl.close();
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
if (trimmed.startsWith(":upload ")) {
|
|
459
|
+
const filePath = trimmed.slice(8).trim();
|
|
460
|
+
try {
|
|
461
|
+
process.stdout.write("uploading...\n");
|
|
462
|
+
const result = await uploadExternalFile(filePath);
|
|
463
|
+
console.log(`uploaded: ${result.filename} (${result.drive_file_id})`);
|
|
464
|
+
} catch (err) {
|
|
465
|
+
console.error("upload error:", err.message);
|
|
466
|
+
}
|
|
467
|
+
rl.prompt();
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
if (!trimmed) {
|
|
471
|
+
rl.prompt();
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
try {
|
|
475
|
+
const finalized = await send(trimmed);
|
|
476
|
+
if (finalized) return;
|
|
477
|
+
} catch (err) {
|
|
478
|
+
console.error("error:", err.message);
|
|
479
|
+
}
|
|
480
|
+
rl.prompt();
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
await new Promise((resolve) => rl.on("close", resolve));
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
async function main() {
|
|
487
|
+
const [area, action, ...rest] = process.argv.slice(2);
|
|
488
|
+
if (!area || area === "help" || area === "--help" || area === "-h") {
|
|
489
|
+
usage();
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// `llama agent-onboard` — print the bundled AGENT_BRIEFING.md so an AI
|
|
494
|
+
// agent reads it once and internalises the Llama Ventures workflow
|
|
495
|
+
// contract. Same content the `agent_briefing` MCP prompt returns.
|
|
496
|
+
// Also: `llama agent onboard` (two-word form) for symmetry.
|
|
497
|
+
//
|
|
498
|
+
// Gated behind /api/me — without valid credentials we print a short
|
|
499
|
+
// bootstrap stub instead. Stops unauthenticated callers from harvesting
|
|
500
|
+
// internal command surface / workflow conventions just by running the
|
|
501
|
+
// public CLI.
|
|
502
|
+
if (
|
|
503
|
+
area === "agent-onboard" ||
|
|
504
|
+
(area === "agent" && (action === "onboard" || action === "briefing"))
|
|
505
|
+
) {
|
|
506
|
+
const headers = await getAuthHeaders();
|
|
507
|
+
if (Object.keys(headers).length === 0) {
|
|
508
|
+
console.log(
|
|
509
|
+
`Llama Ventures team onboarding requires credentials.
|
|
510
|
+
|
|
511
|
+
Team member?
|
|
512
|
+
- Run \`gcloud auth login\` with your @llamaventures.vc account, OR
|
|
513
|
+
- Mint a token at https://command.llamaventures.vc/settings/tokens
|
|
514
|
+
then \`llama token set <llc_...>\`.
|
|
515
|
+
Re-run \`llama agent-onboard\` after — the workflow contract will print.
|
|
516
|
+
|
|
517
|
+
Founder or external visitor (no Llama account)?
|
|
518
|
+
Run \`llama pitch start --name "Your Name" --email "you@company.com"\`
|
|
519
|
+
to chat with our intake agent — no token required.`
|
|
520
|
+
);
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
try {
|
|
524
|
+
await request("GET", "/api/me");
|
|
525
|
+
} catch (e) {
|
|
526
|
+
const msg = e?.message || "";
|
|
527
|
+
if (msg.includes("Error[UNAUTHORIZED]") || msg.includes("Error[NO_AUTH]")) {
|
|
528
|
+
console.log(
|
|
529
|
+
`Llama Ventures team onboarding requires valid credentials.
|
|
530
|
+
|
|
531
|
+
Server rejected the credentials we sent. Re-mint at
|
|
532
|
+
https://command.llamaventures.vc/settings/tokens, run
|
|
533
|
+
\`llama token set <llc_...>\`, then re-run \`llama agent-onboard\`.`
|
|
534
|
+
);
|
|
535
|
+
process.exitCode = 1;
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
throw e;
|
|
539
|
+
}
|
|
540
|
+
process.stdout.write(readBriefing());
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// `llama pitch ...` — external founder-pitch family. No Llama token
|
|
545
|
+
// required; bootstraps a session against /api/external/* via PoW + cookie.
|
|
546
|
+
// See lib/external.mjs and AGENT_BRIEFING.md for the full surface.
|
|
547
|
+
if (area === "pitch") {
|
|
548
|
+
await handlePitch(action, rest);
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if (area === "token" && action === "set") {
|
|
553
|
+
const { flags, positional } = parseFlags(rest);
|
|
554
|
+
const token = positional[0];
|
|
555
|
+
if (!token?.startsWith("llc_")) throw new Error("Expected a token starting with llc_");
|
|
556
|
+
if (token.length !== 36) {
|
|
557
|
+
throw new Error(
|
|
558
|
+
`Token has length ${token.length}; expected 36 (llc_ + 32 hex chars).\n` +
|
|
559
|
+
` This usually means you copied the masked preview ("llc_xxxx…yyyy") from\n` +
|
|
560
|
+
` the token list instead of the full string from the mint response.\n` +
|
|
561
|
+
` Re-mint at https://command.llamaventures.vc/settings/tokens and use the\n` +
|
|
562
|
+
` Copy button — it captures the full value.`
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
if (flags.base) {
|
|
566
|
+
// baseUrl still lives in legacy config — rarely overridden. Keep the
|
|
567
|
+
// file there so we don't introduce a second config surface.
|
|
568
|
+
const legacy = readLegacyConfig();
|
|
569
|
+
legacy.baseUrl = String(flags.base).replace(/\/$/, "");
|
|
570
|
+
writeLegacyConfig(legacy);
|
|
571
|
+
}
|
|
572
|
+
// Round-trip the token against /api/me before persisting. Catches the
|
|
573
|
+
// pasted-preview / wrong-token / wrong-host cases at "set" time instead
|
|
574
|
+
// of letting them fester until the next CLI call (or worse, a CI run).
|
|
575
|
+
// --skip-verify is an escape hatch for offline / pre-deploy testing.
|
|
576
|
+
if (!flags["skip-verify"]) {
|
|
577
|
+
try {
|
|
578
|
+
const res = await fetch(`${getBaseUrl()}/api/me`, {
|
|
579
|
+
headers: { "X-Llama-Token": token },
|
|
580
|
+
});
|
|
581
|
+
if (res.status === 401 || res.status === 403) {
|
|
582
|
+
const body = await res.text();
|
|
583
|
+
throw new Error(
|
|
584
|
+
`Server rejected this token (HTTP ${res.status}). Not saving.\n` +
|
|
585
|
+
` Response: ${body.slice(0, 200)}\n` +
|
|
586
|
+
` Base URL: ${getBaseUrl()}\n` +
|
|
587
|
+
` Re-check that you copied the full token from the mint dialog\n` +
|
|
588
|
+
` ("Shown once") and not the masked preview from the list view.\n` +
|
|
589
|
+
` Override with --skip-verify if you know the server is unreachable.`
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
if (!res.ok) {
|
|
593
|
+
throw new Error(`Verify call failed: HTTP ${res.status}. Not saving.`);
|
|
594
|
+
}
|
|
595
|
+
} catch (e) {
|
|
596
|
+
if (e instanceof Error && e.message.startsWith("Server rejected") || e.message.startsWith("Verify call failed")) {
|
|
597
|
+
throw e;
|
|
598
|
+
}
|
|
599
|
+
// Network / DNS failure — surface but let the user override.
|
|
600
|
+
throw new Error(
|
|
601
|
+
`Could not reach ${getBaseUrl()} to verify token: ${e.message}\n` +
|
|
602
|
+
` Add --skip-verify if you want to save anyway.`
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
writeCanonicalToken(token);
|
|
607
|
+
console.log(`Saved token to ~/.llama/token (mode 0600).`);
|
|
608
|
+
console.log(`Base URL: ${getBaseUrl()}`);
|
|
609
|
+
if (!flags["skip-verify"]) console.log(`Verified against ${getBaseUrl()}/api/me — token works.`);
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (area === "token" && action === "show") {
|
|
614
|
+
const token = getToken();
|
|
615
|
+
if (!token) {
|
|
616
|
+
console.log("No token set.");
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
console.log(`${token.slice(0, 8)}...${token.slice(-4)} @ ${getBaseUrl()}`);
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Self-diagnosis for agents and humans — what credentials do we have, and
|
|
624
|
+
// are they accepted by the server right now? Designed so an agent can
|
|
625
|
+
// parse the output and decide whether to drive a recovery flow.
|
|
626
|
+
if (area === "auth" && action === "status") {
|
|
627
|
+
const bearer = await tryGcloudIdentityToken();
|
|
628
|
+
const token = getToken();
|
|
629
|
+
const tokenSrc = process.env.LLAMA_TOKEN
|
|
630
|
+
? "$LLAMA_TOKEN"
|
|
631
|
+
: readCanonicalToken()
|
|
632
|
+
? "~/.llama/token"
|
|
633
|
+
: readLegacyConfig().token
|
|
634
|
+
? "~/.llama-command/config.json (legacy)"
|
|
635
|
+
: null;
|
|
636
|
+
|
|
637
|
+
let serverCheck = "skipped (no credentials)";
|
|
638
|
+
if (bearer || token) {
|
|
639
|
+
try {
|
|
640
|
+
const me = await request("GET", "/api/me");
|
|
641
|
+
serverCheck = `ok — authenticated as ${me?.email ?? "unknown"} (role: ${me?.role ?? "unknown"})`;
|
|
642
|
+
} catch (e) {
|
|
643
|
+
serverCheck = `failed — ${e.message.split("\n")[0]}`;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
print({
|
|
648
|
+
baseUrl: getBaseUrl(),
|
|
649
|
+
gcloudIdentityToken: bearer ? "present" : "absent",
|
|
650
|
+
llamaToken: token ? `${token.slice(0, 8)}...${token.slice(-4)}` : "absent",
|
|
651
|
+
llamaTokenSource: tokenSrc,
|
|
652
|
+
serverCheck,
|
|
653
|
+
});
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
if (area === "deal" && action === "create") {
|
|
658
|
+
const { flags, positional } = parseFlags(rest);
|
|
659
|
+
const companyName = positional.join(" ").trim();
|
|
660
|
+
if (!companyName) throw new Error("Usage: llama deal create \"Company\" [--source Name]");
|
|
661
|
+
const body = {
|
|
662
|
+
companyName,
|
|
663
|
+
source: flags.source,
|
|
664
|
+
description: flags.description,
|
|
665
|
+
website: flags.website,
|
|
666
|
+
notes: flags.notes,
|
|
667
|
+
status: flags.status,
|
|
668
|
+
theirStage: flags["their-stage"],
|
|
669
|
+
stage: flags.stage,
|
|
670
|
+
proposedAmount: flags["proposed-amount"],
|
|
671
|
+
roundSize: flags["round-size"],
|
|
672
|
+
valuation: flags.valuation,
|
|
673
|
+
founders: flags.founders,
|
|
674
|
+
location: flags.location,
|
|
675
|
+
};
|
|
676
|
+
print(await request("POST", "/api/deals/create", body));
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
if (area === "deal" && action === "show") {
|
|
681
|
+
const dealId = rest[0];
|
|
682
|
+
if (!dealId) throw new Error("Usage: llama deal show <dealId>");
|
|
683
|
+
print(await request("GET", `/api/deals/${encodeURIComponent(dealId)}/command-center`));
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
if (area === "deal" && action === "update") {
|
|
688
|
+
const [dealId, field, ...valueParts] = rest;
|
|
689
|
+
const value = valueParts.join(" ");
|
|
690
|
+
if (!dealId || !field) throw new Error("Usage: llama deal update <dealId> <field> <value>");
|
|
691
|
+
print(await request("POST", "/api/deals/update", { dealId, field, value }));
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
if (area === "deal" && action === "search") {
|
|
696
|
+
const { flags, positional } = parseFlags(rest);
|
|
697
|
+
const q = positional.join(" ").trim();
|
|
698
|
+
if (!q && Object.keys(flags).length === 0) {
|
|
699
|
+
throw new Error(
|
|
700
|
+
`Usage: llama deal search <query> [--founder ...] [--owner ...] [--status ...] [--stage ...] [--limit N]`
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
print(await searchDeals(q, flags));
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
if (area === "deal" && action === "list") {
|
|
708
|
+
const { flags } = parseFlags(rest);
|
|
709
|
+
print(await searchDeals("", flags));
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// ----- Collaborators (deal team — non-owner contributors) -----
|
|
714
|
+
// Accepts --user as numeric id OR @llamaventures.vc email; emails are
|
|
715
|
+
// resolved to id via /api/users so the CLI matches how the web picker
|
|
716
|
+
// works (you don't need to memorize ids).
|
|
717
|
+
if (area === "deal" && action === "collab") {
|
|
718
|
+
const sub = rest[0];
|
|
719
|
+
const dealId = rest[1];
|
|
720
|
+
const { flags } = parseFlags(rest.slice(2));
|
|
721
|
+
|
|
722
|
+
if (!sub || !dealId) {
|
|
723
|
+
throw new Error(
|
|
724
|
+
"Usage: llama deal collab list|add|remove <dealId> [--user <userId|email>]"
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
if (sub === "list") {
|
|
729
|
+
print(await request("GET", `/api/deals/${encodeURIComponent(dealId)}/collaborators`));
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
if (sub !== "add" && sub !== "remove" && sub !== "restore") {
|
|
734
|
+
throw new Error(`Unknown collab sub-command "${sub}". Use list, add, remove, or restore.`);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
if (!flags.user) {
|
|
738
|
+
throw new Error(`Usage: llama deal collab ${sub} <dealId> --user <userId|email>`);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
let userId = Number(flags.user);
|
|
742
|
+
if (!Number.isFinite(userId)) {
|
|
743
|
+
const email = String(flags.user).toLowerCase();
|
|
744
|
+
const usersPayload = await request("GET", "/api/users");
|
|
745
|
+
const list = Array.isArray(usersPayload) ? usersPayload : usersPayload.users ?? [];
|
|
746
|
+
const match = list.find((u) => String(u.email).toLowerCase() === email);
|
|
747
|
+
if (!match) throw new Error(`No active user with email "${flags.user}"`);
|
|
748
|
+
userId = match.id;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
if (sub === "add") {
|
|
752
|
+
print(await request(
|
|
753
|
+
"POST",
|
|
754
|
+
`/api/deals/${encodeURIComponent(dealId)}/collaborators`,
|
|
755
|
+
{ userId }
|
|
756
|
+
));
|
|
757
|
+
} else if (sub === "remove") {
|
|
758
|
+
print(await request(
|
|
759
|
+
"DELETE",
|
|
760
|
+
`/api/deals/${encodeURIComponent(dealId)}/collaborators/${userId}`
|
|
761
|
+
));
|
|
762
|
+
} else {
|
|
763
|
+
print(await request(
|
|
764
|
+
"POST",
|
|
765
|
+
`/api/deals/${encodeURIComponent(dealId)}/collaborators/${userId}/restore`
|
|
766
|
+
));
|
|
767
|
+
}
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// ----- Deal links (URLs attached to a deal — separate from brief link blocks) -----
|
|
772
|
+
// Soft-delete: removal sets deleted_at, restore clears it. List excludes
|
|
773
|
+
// trashed by default; pass --include-deleted to see them.
|
|
774
|
+
if (area === "deal" && action === "link") {
|
|
775
|
+
const sub = rest[0];
|
|
776
|
+
const dealId = rest[1];
|
|
777
|
+
if (!sub || !dealId) {
|
|
778
|
+
throw new Error(
|
|
779
|
+
"Usage: llama deal link list|add|delete|restore <dealId> [...flags|<linkId>]"
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
if (sub === "list") {
|
|
784
|
+
const { flags } = parseFlags(rest.slice(2));
|
|
785
|
+
const qs = flags["include-deleted"] ? "?include_deleted=1" : "";
|
|
786
|
+
print(await request("GET", `/api/deals/${encodeURIComponent(dealId)}/links${qs}`));
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
if (sub === "add") {
|
|
791
|
+
const { flags } = parseFlags(rest.slice(2));
|
|
792
|
+
if (!flags.url) throw new Error("Usage: llama deal link add <dealId> --url <url> [--label \"...\"]");
|
|
793
|
+
print(await request(
|
|
794
|
+
"POST",
|
|
795
|
+
`/api/deals/${encodeURIComponent(dealId)}/links`,
|
|
796
|
+
{ url: String(flags.url), label: flags.label ? String(flags.label) : "" }
|
|
797
|
+
));
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
if (sub === "delete" || sub === "restore") {
|
|
802
|
+
const linkId = rest[2];
|
|
803
|
+
if (!linkId) throw new Error(`Usage: llama deal link ${sub} <dealId> <linkId>`);
|
|
804
|
+
const path = `/api/deals/${encodeURIComponent(dealId)}/links/${encodeURIComponent(linkId)}`;
|
|
805
|
+
print(await request(
|
|
806
|
+
sub === "delete" ? "DELETE" : "POST",
|
|
807
|
+
sub === "delete" ? path : `${path}/restore`
|
|
808
|
+
));
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
throw new Error(`Unknown link sub-command "${sub}". Use list, add, delete, or restore.`);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// ----- Deal soft-delete / restore / trash list -----
|
|
816
|
+
// Server side: DELETE /api/deals/:id uses authenticate() (token works).
|
|
817
|
+
// POST /restore currently uses session-only auth() — known asymmetry,
|
|
818
|
+
// pending server fix to swap to authenticate() for parity.
|
|
819
|
+
if (area === "deal" && action === "delete") {
|
|
820
|
+
const dealId = rest[0];
|
|
821
|
+
if (!dealId) throw new Error("Usage: llama deal delete <dealId>");
|
|
822
|
+
print(await request("DELETE", `/api/deals/${encodeURIComponent(dealId)}`));
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
if (area === "deal" && action === "restore") {
|
|
827
|
+
const dealId = rest[0];
|
|
828
|
+
if (!dealId) throw new Error("Usage: llama deal restore <dealId>");
|
|
829
|
+
// NOTE: server uses session-only auth() today — token callers get 401
|
|
830
|
+
// until server is updated. CLI surface is forward-compatible.
|
|
831
|
+
print(await request("POST", `/api/deals/${encodeURIComponent(dealId)}/restore`));
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
if (area === "deal" && action === "trash") {
|
|
836
|
+
print(await request("GET", "/api/deals/deleted"));
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// ----- Deal facts (AI-extracted or human-asserted, with verification) -----
|
|
841
|
+
// NOTE: server routes currently use session-only auth() — token-only
|
|
842
|
+
// callers will get 401 until /api/deals/:id/facts and
|
|
843
|
+
// /api/deals/:id/facts/:factId switch to authenticate(). CLI surface
|
|
844
|
+
// is forward-compatible.
|
|
845
|
+
if (area === "deal" && action === "fact") {
|
|
846
|
+
const sub = rest[0];
|
|
847
|
+
const dealId = rest[1];
|
|
848
|
+
const { flags } = parseFlags(rest.slice(2));
|
|
849
|
+
|
|
850
|
+
if (!sub || !dealId) {
|
|
851
|
+
throw new Error("Usage: llama deal fact list|add|verify <dealId> [...]");
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
if (sub === "list") {
|
|
855
|
+
print(await request("GET", `/api/deals/${encodeURIComponent(dealId)}/facts`));
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
if (sub === "add") {
|
|
860
|
+
if (!flags.category || !flags.claim) {
|
|
861
|
+
throw new Error(
|
|
862
|
+
`Usage: llama deal fact add <dealId> --category <cat> --claim "<text>" ` +
|
|
863
|
+
`[--source <url>] [--confidence high|medium|low]`
|
|
864
|
+
);
|
|
865
|
+
}
|
|
866
|
+
print(await request("POST", `/api/deals/${encodeURIComponent(dealId)}/facts`, {
|
|
867
|
+
category: String(flags.category),
|
|
868
|
+
claim: String(flags.claim),
|
|
869
|
+
source: flags.source ? String(flags.source) : "",
|
|
870
|
+
confidence: flags.confidence ? String(flags.confidence) : "medium",
|
|
871
|
+
}));
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
if (sub === "verify") {
|
|
876
|
+
const factId = rest[2];
|
|
877
|
+
if (!factId) {
|
|
878
|
+
throw new Error(
|
|
879
|
+
`Usage: llama deal fact verify <dealId> <factId> ` +
|
|
880
|
+
`--status confirmed|disputed [--corrected-value "..."]`
|
|
881
|
+
);
|
|
882
|
+
}
|
|
883
|
+
if (!flags.status || !["confirmed", "disputed"].includes(String(flags.status))) {
|
|
884
|
+
throw new Error("--status must be 'confirmed' or 'disputed'");
|
|
885
|
+
}
|
|
886
|
+
const body = { status: String(flags.status) };
|
|
887
|
+
if (flags["corrected-value"] !== undefined && flags["corrected-value"] !== true) {
|
|
888
|
+
body.correctedValue = String(flags["corrected-value"]);
|
|
889
|
+
}
|
|
890
|
+
print(await request(
|
|
891
|
+
"PATCH",
|
|
892
|
+
`/api/deals/${encodeURIComponent(dealId)}/facts/${encodeURIComponent(factId)}`,
|
|
893
|
+
body
|
|
894
|
+
));
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
throw new Error(`Unknown fact sub-command "${sub}". Use list, add, or verify.`);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// ----- Brief refresh: trigger stale-section re-eval watcher run -----
|
|
902
|
+
// Server only fires for unlocked sections that are stale per the
|
|
903
|
+
// freshness policy; --force runs every unlocked watcher-managed section.
|
|
904
|
+
if (area === "deal" && action === "refresh-brief") {
|
|
905
|
+
const { flags } = parseFlags(rest);
|
|
906
|
+
const dealId = rest[0];
|
|
907
|
+
if (!dealId) throw new Error("Usage: llama deal refresh-brief <dealId> [--force]");
|
|
908
|
+
const qs = flags.force ? "?force=true" : "";
|
|
909
|
+
print(await request("POST", `/api/deals/${encodeURIComponent(dealId)}/refresh-brief${qs}`));
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// ----- Persona refresh: re-run a single persona-watcher -----
|
|
914
|
+
// Persona keys are validated server-side. Returns runId or null
|
|
915
|
+
// (debounced / deal inactive). Used by /admin and the per-block
|
|
916
|
+
// "重新生成" flow.
|
|
917
|
+
if (area === "deal" && action === "refresh-persona") {
|
|
918
|
+
const dealId = rest[0];
|
|
919
|
+
const persona = rest[1];
|
|
920
|
+
if (!dealId || !persona) {
|
|
921
|
+
throw new Error(`Usage: llama deal refresh-persona <dealId> <persona-key>`);
|
|
922
|
+
}
|
|
923
|
+
print(await request(
|
|
924
|
+
"POST",
|
|
925
|
+
`/api/deals/${encodeURIComponent(dealId)}/refresh-persona`,
|
|
926
|
+
{ persona }
|
|
927
|
+
));
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// ----- Agent-run revert (legacy 4-section brief model) -----
|
|
932
|
+
// Reverts a single section to the `before` value snapshotted by the
|
|
933
|
+
// watcher run. Does NOT re-fire the watcher (intentional: human action).
|
|
934
|
+
// Logs `brief_reverted` in deal_events. Section keys are the legacy
|
|
935
|
+
// top-level sections, not block ids.
|
|
936
|
+
if (area === "deal" && action === "revert-run") {
|
|
937
|
+
const { flags } = parseFlags(rest);
|
|
938
|
+
const dealId = rest[0];
|
|
939
|
+
const runId = rest[1];
|
|
940
|
+
const valid = ["company", "team", "highlights", "recommendation"];
|
|
941
|
+
if (!dealId || !runId || !flags.section) {
|
|
942
|
+
throw new Error(
|
|
943
|
+
`Usage: llama deal revert-run <dealId> <runId> --section ${valid.join("|")}`
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
if (!valid.includes(String(flags.section))) {
|
|
947
|
+
throw new Error(`--section must be one of ${valid.join(", ")}`);
|
|
948
|
+
}
|
|
949
|
+
print(await request(
|
|
950
|
+
"POST",
|
|
951
|
+
`/api/deals/${encodeURIComponent(dealId)}/agent-runs/${encodeURIComponent(runId)}/revert`,
|
|
952
|
+
{ section: String(flags.section) }
|
|
953
|
+
));
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
if (area === "approvals" && action === "list") {
|
|
958
|
+
print(await request("GET", "/api/partner/approvals"));
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
if (area === "approvals" && action === "decide") {
|
|
963
|
+
const { flags, positional } = parseFlags(rest);
|
|
964
|
+
const approvalId = Number(positional[0]);
|
|
965
|
+
const decision = positional[1];
|
|
966
|
+
if (!Number.isFinite(approvalId) || !["approved", "rejected"].includes(decision)) {
|
|
967
|
+
throw new Error("Usage: llama approvals decide <approvalId> approved|rejected [--note ...]");
|
|
968
|
+
}
|
|
969
|
+
print(await request("POST", "/api/partner/approvals", {
|
|
970
|
+
approvalId,
|
|
971
|
+
decision,
|
|
972
|
+
note: flags.note || "",
|
|
973
|
+
}));
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
// ----- Ownership: self-claim -----
|
|
978
|
+
if (area === "claim") {
|
|
979
|
+
const dealId = action; // second positional
|
|
980
|
+
if (!dealId) throw new Error("Usage: llama claim <dealId>");
|
|
981
|
+
const me = await request("GET", "/api/me");
|
|
982
|
+
print(await request(
|
|
983
|
+
"POST",
|
|
984
|
+
`/api/deals/${encodeURIComponent(dealId)}/propose-owner`,
|
|
985
|
+
{ userId: me.id }
|
|
986
|
+
));
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// ----- Ownership: partner nominates someone else -----
|
|
991
|
+
if (area === "nominate") {
|
|
992
|
+
const dealId = action;
|
|
993
|
+
const { flags } = parseFlags(rest);
|
|
994
|
+
const userId = Number(flags.user);
|
|
995
|
+
if (!dealId || !Number.isFinite(userId)) {
|
|
996
|
+
throw new Error("Usage: llama nominate <dealId> --user <userId>");
|
|
997
|
+
}
|
|
998
|
+
print(await request(
|
|
999
|
+
"POST",
|
|
1000
|
+
`/api/deals/${encodeURIComponent(dealId)}/propose-owner`,
|
|
1001
|
+
{ userId }
|
|
1002
|
+
));
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// ----- Nominations inbox (for the nominee) -----
|
|
1007
|
+
if (area === "nominations" && action === "list") {
|
|
1008
|
+
print(await request("GET", "/api/me/nominations"));
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
if (area === "nominations" && action === "decide") {
|
|
1012
|
+
const approvalId = Number(rest[0]);
|
|
1013
|
+
const decision = rest[1];
|
|
1014
|
+
if (!Number.isFinite(approvalId) || !["accepted", "declined"].includes(decision)) {
|
|
1015
|
+
throw new Error("Usage: llama nominations decide <approvalId> accepted|declined");
|
|
1016
|
+
}
|
|
1017
|
+
print(await request("POST", `/api/nominations/${approvalId}`, { decision }));
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// ----- Timeline -----
|
|
1022
|
+
if (area === "timeline") {
|
|
1023
|
+
const dealId = action;
|
|
1024
|
+
if (!dealId) throw new Error("Usage: llama timeline <dealId>");
|
|
1025
|
+
print(await request("GET", `/api/deals/${encodeURIComponent(dealId)}/timeline`));
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
// ----- Post to timeline -----
|
|
1030
|
+
if (area === "post") {
|
|
1031
|
+
const dealId = action;
|
|
1032
|
+
const { flags, positional } = parseFlags(rest);
|
|
1033
|
+
const body = positional[0];
|
|
1034
|
+
if (!dealId || !body) {
|
|
1035
|
+
throw new Error(`Usage: llama post <dealId> "message body" [--link url] [--link-name "name"]`);
|
|
1036
|
+
}
|
|
1037
|
+
const attachments = flags.link
|
|
1038
|
+
? [{ url: String(flags.link), name: flags["link-name"] ? String(flags["link-name"]) : String(flags.link) }]
|
|
1039
|
+
: [];
|
|
1040
|
+
print(await request(
|
|
1041
|
+
"POST",
|
|
1042
|
+
`/api/deals/${encodeURIComponent(dealId)}/posts`,
|
|
1043
|
+
{ body, attachments }
|
|
1044
|
+
));
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// ----- Wiki: search -----
|
|
1049
|
+
if (area === "wiki" && action === "search") {
|
|
1050
|
+
const { positional } = parseFlags(rest);
|
|
1051
|
+
const q = positional.join(" ").trim();
|
|
1052
|
+
if (!q) throw new Error("Usage: llama wiki search <query>");
|
|
1053
|
+
print(await request("GET", `/api/wiki/search?q=${encodeURIComponent(q)}`));
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// ----- Wiki: read a single article (EN by default) -----
|
|
1058
|
+
if (area === "wiki" && action === "read") {
|
|
1059
|
+
const slug = rest[0];
|
|
1060
|
+
if (!slug) throw new Error("Usage: llama wiki read <slug>");
|
|
1061
|
+
const results = await request("GET", `/api/wiki/search?q=${encodeURIComponent(slug)}`);
|
|
1062
|
+
const match = Array.isArray(results) ? results.find((r) => r.slug === slug) : null;
|
|
1063
|
+
print(match || { error: `Article "${slug}" not found.` });
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// ----- Wiki: save (create or update) -----
|
|
1068
|
+
if (area === "wiki" && action === "save") {
|
|
1069
|
+
const { flags, positional } = parseFlags(rest);
|
|
1070
|
+
const slug = positional[0];
|
|
1071
|
+
const title = flags.title;
|
|
1072
|
+
const content = flags.content;
|
|
1073
|
+
const sourcesRaw = flags.sources;
|
|
1074
|
+
if (!slug || !title || !content || !sourcesRaw) {
|
|
1075
|
+
throw new Error(
|
|
1076
|
+
`Usage: llama wiki save <slug> --title "..." --content "..." --sources "url1;url2" [--type company] [--related "A;B"] [--lang en|zh]`
|
|
1077
|
+
);
|
|
1078
|
+
}
|
|
1079
|
+
const splitCsv = (v) => String(v).split(/[;|]/).map((s) => s.trim()).filter(Boolean);
|
|
1080
|
+
const payload = {
|
|
1081
|
+
slug,
|
|
1082
|
+
title: String(title),
|
|
1083
|
+
content: String(content),
|
|
1084
|
+
sources: splitCsv(sourcesRaw),
|
|
1085
|
+
type: flags.type ? String(flags.type) : undefined,
|
|
1086
|
+
related: flags.related ? splitCsv(flags.related) : undefined,
|
|
1087
|
+
lang: flags.lang === "zh" ? "zh" : "en",
|
|
1088
|
+
status: flags.status ? String(flags.status) : undefined,
|
|
1089
|
+
};
|
|
1090
|
+
print(await request("POST", "/api/wiki/save", payload));
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
// ----- Brief blocks: list / add-* / edit / delete -----
|
|
1095
|
+
// The block-based deal brief stores an ordered array of typed blocks
|
|
1096
|
+
// (text / link / embed / callout) per deal. These commands wrap the
|
|
1097
|
+
// /api/deals/:id/blocks{,/:id} endpoints. To add a block we read +
|
|
1098
|
+
// append + PUT (two roundtrips); single-block edit + delete have
|
|
1099
|
+
// dedicated PATCH/DELETE endpoints.
|
|
1100
|
+
if (area === "brief" && action === "blocks") {
|
|
1101
|
+
const dealId = rest[0];
|
|
1102
|
+
if (!dealId) throw new Error("Usage: llama brief blocks <dealId>");
|
|
1103
|
+
print(await request("GET", `/api/deals/${encodeURIComponent(dealId)}/blocks`));
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// Per-block read — pairs with the manifest returned by /command-center
|
|
1108
|
+
// (i.e. `llama deal show`). Agent flow: read manifest → pick blocks
|
|
1109
|
+
// by id → fetch only those bodies, instead of pulling the full array.
|
|
1110
|
+
if (area === "brief" && action === "block") {
|
|
1111
|
+
const dealId = rest[0];
|
|
1112
|
+
const blockId = rest[1];
|
|
1113
|
+
if (!dealId || !blockId) throw new Error("Usage: llama brief block <dealId> <blockId>");
|
|
1114
|
+
print(await request(
|
|
1115
|
+
"GET",
|
|
1116
|
+
`/api/deals/${encodeURIComponent(dealId)}/blocks/${encodeURIComponent(blockId)}`
|
|
1117
|
+
));
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
if (area === "brief" && action?.startsWith("add-")) {
|
|
1122
|
+
const type = action.slice(4); // "add-text" → "text"
|
|
1123
|
+
if (!["text", "link", "embed", "callout"].includes(type)) {
|
|
1124
|
+
throw new Error(`Unknown block type "${type}". Use add-text, add-link, add-embed, or add-callout.`);
|
|
1125
|
+
}
|
|
1126
|
+
const { flags } = parseFlags(rest);
|
|
1127
|
+
const dealId = rest[0];
|
|
1128
|
+
if (!dealId) throw new Error(`Usage: llama brief add-${type} <dealId> [...flags]`);
|
|
1129
|
+
|
|
1130
|
+
const id = (typeof crypto !== "undefined" && "randomUUID" in crypto)
|
|
1131
|
+
? crypto.randomUUID()
|
|
1132
|
+
: `b_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
1133
|
+
const meta = { updated_at: new Date().toISOString(), updated_by: "cli", by_agent: false };
|
|
1134
|
+
|
|
1135
|
+
// --source-section <key>: target a structured section (e.g. team /
|
|
1136
|
+
// highlights / <persona>_analysis). Without this, blocks land in the
|
|
1137
|
+
// "_other" group at the bottom of the TOC. AI writers want this
|
|
1138
|
+
// virtually always — without it they cannot contribute to existing
|
|
1139
|
+
// structured sections.
|
|
1140
|
+
if (flags["source-section"]) {
|
|
1141
|
+
meta.sourceSection = String(flags["source-section"]);
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
// --reply-to <blockId>: snapshot the parent block's heading + a
|
|
1145
|
+
// 200-char excerpt into meta so the back-link survives parent edits
|
|
1146
|
+
// or deletion. CLI only — replies are always text in the UI; allow
|
|
1147
|
+
// any add-* type here for symmetry but the UI only renders the
|
|
1148
|
+
// back-link on text + callout blocks today.
|
|
1149
|
+
let cur = null;
|
|
1150
|
+
if (flags["reply-to"]) {
|
|
1151
|
+
const replyTo = String(flags["reply-to"]);
|
|
1152
|
+
cur = await request("GET", `/api/deals/${encodeURIComponent(dealId)}/blocks`);
|
|
1153
|
+
const parent = (cur.blocks ?? []).find((b) => b.id === replyTo);
|
|
1154
|
+
if (!parent) throw new Error(`--reply-to: block ${replyTo} not found on deal ${dealId}`);
|
|
1155
|
+
meta.reply_to = parent.id;
|
|
1156
|
+
// heading: text/callout → heading; link/embed → label; fallback "(untitled block)"
|
|
1157
|
+
meta.reply_to_heading =
|
|
1158
|
+
parent.heading || parent.label || "(untitled block)";
|
|
1159
|
+
// excerpt: text/callout → heading + body; link → label + description; embed → label
|
|
1160
|
+
const excerptParts =
|
|
1161
|
+
parent.type === "text" || parent.type === "callout"
|
|
1162
|
+
? [parent.heading, parent.body]
|
|
1163
|
+
: parent.type === "link"
|
|
1164
|
+
? [parent.label, parent.description]
|
|
1165
|
+
: [parent.label];
|
|
1166
|
+
const excerpt = excerptParts.filter(Boolean).join("\n\n").slice(0, 200);
|
|
1167
|
+
meta.reply_to_excerpt = excerpt;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
let block;
|
|
1171
|
+
if (type === "text") {
|
|
1172
|
+
block = { id, type, heading: flags.heading ? String(flags.heading) : "", body: flags.body ? String(flags.body) : "", meta };
|
|
1173
|
+
} else if (type === "link") {
|
|
1174
|
+
if (!flags.url || !flags.label) throw new Error("add-link requires --url and --label");
|
|
1175
|
+
block = { id, type, url: String(flags.url), label: String(flags.label), description: flags.description ? String(flags.description) : undefined, meta };
|
|
1176
|
+
} else if (type === "embed") {
|
|
1177
|
+
if (!flags.url) throw new Error("add-embed requires --url");
|
|
1178
|
+
block = { id, type, url: String(flags.url), label: flags.label ? String(flags.label) : undefined, meta };
|
|
1179
|
+
} else {
|
|
1180
|
+
block = { id, type, tone: flags.tone ? String(flags.tone) : "insight", heading: flags.heading ? String(flags.heading) : "", body: flags.body ? String(flags.body) : "", meta };
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
// --position top|bottom (default: top). Top matches the UI behavior
|
|
1184
|
+
// changed 2026-05-03 — newly added blocks land at the top of the
|
|
1185
|
+
// brief so the writer (or reader) sees the contribution without
|
|
1186
|
+
// scrolling. Pass `--position bottom` to append, e.g. for batched
|
|
1187
|
+
// AI writes that should preserve insertion order.
|
|
1188
|
+
const position = flags.position ? String(flags.position) : "top";
|
|
1189
|
+
if (position !== "top" && position !== "bottom") {
|
|
1190
|
+
throw new Error(`--position must be "top" or "bottom" (got "${position}")`);
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
if (!cur) cur = await request("GET", `/api/deals/${encodeURIComponent(dealId)}/blocks`);
|
|
1194
|
+
const existing = cur.blocks ?? [];
|
|
1195
|
+
const next = position === "top" ? [block, ...existing] : [...existing, block];
|
|
1196
|
+
print(await request("PUT", `/api/deals/${encodeURIComponent(dealId)}/blocks`, { blocks: next }));
|
|
1197
|
+
console.log(`Created block ${id}`);
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
if (area === "brief" && action === "edit") {
|
|
1202
|
+
const { flags } = parseFlags(rest);
|
|
1203
|
+
const dealId = rest[0];
|
|
1204
|
+
const blockId = rest[1];
|
|
1205
|
+
if (!dealId || !blockId) {
|
|
1206
|
+
throw new Error("Usage: llama brief edit <dealId> <blockId> [--heading ...] [--body ...] [--url ...] [--label ...] [--description ...] [--tone ...] [--source-section ...] [--lock|--unlock] [--hide|--unhide]");
|
|
1207
|
+
}
|
|
1208
|
+
const patch = {};
|
|
1209
|
+
for (const k of ["heading", "body", "url", "label", "description", "tone"]) {
|
|
1210
|
+
if (flags[k] !== undefined && flags[k] !== true) patch[k] = String(flags[k]);
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
// Meta toggles. The PATCH endpoint accepts a meta object that gets
|
|
1214
|
+
// merged with the existing block.meta server-side, so we only need
|
|
1215
|
+
// to send the keys we want to change. lock/hide flags are pure
|
|
1216
|
+
// toggles (no value); source-section takes a key.
|
|
1217
|
+
const metaPatch = {};
|
|
1218
|
+
if (flags.lock === true) metaPatch.locked = true;
|
|
1219
|
+
if (flags.unlock === true) metaPatch.locked = false;
|
|
1220
|
+
if (flags.hide === true) metaPatch.hidden = true;
|
|
1221
|
+
if (flags.unhide === true) metaPatch.hidden = false;
|
|
1222
|
+
if (flags["source-section"] !== undefined && flags["source-section"] !== true) {
|
|
1223
|
+
metaPatch.sourceSection = String(flags["source-section"]);
|
|
1224
|
+
}
|
|
1225
|
+
if (Object.keys(metaPatch).length > 0) patch.meta = metaPatch;
|
|
1226
|
+
|
|
1227
|
+
if (Object.keys(patch).length === 0) throw new Error("at least one field flag required");
|
|
1228
|
+
print(await request("PATCH", `/api/deals/${encodeURIComponent(dealId)}/blocks/${encodeURIComponent(blockId)}`, patch));
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
if (area === "brief" && action === "delete") {
|
|
1233
|
+
const dealId = rest[0];
|
|
1234
|
+
const blockId = rest[1];
|
|
1235
|
+
if (!dealId || !blockId) throw new Error("Usage: llama brief delete <dealId> <blockId>");
|
|
1236
|
+
print(await request("DELETE", `/api/deals/${encodeURIComponent(dealId)}/blocks/${encodeURIComponent(blockId)}`));
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
if (area === "brief" && action === "restore") {
|
|
1241
|
+
const dealId = rest[0];
|
|
1242
|
+
const blockId = rest[1];
|
|
1243
|
+
if (!dealId || !blockId) throw new Error("Usage: llama brief restore <dealId> <blockId>");
|
|
1244
|
+
print(await request(
|
|
1245
|
+
"POST",
|
|
1246
|
+
`/api/deals/${encodeURIComponent(dealId)}/blocks/${encodeURIComponent(blockId)}/restore`
|
|
1247
|
+
));
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
// Per-block content history (Wikipedia model — every overwrite snapshots
|
|
1252
|
+
// the prev full block JSON). Two sub-actions: list versions, and restore
|
|
1253
|
+
// a specific version. Restore is itself reversible — when you restore
|
|
1254
|
+
// version N, the OUTGOING version (the one being replaced) gets snapshotted
|
|
1255
|
+
// into history, so undoing a wrong restore is one more `restore-version`
|
|
1256
|
+
// call away.
|
|
1257
|
+
if (area === "brief" && action === "history") {
|
|
1258
|
+
const { flags } = parseFlags(rest);
|
|
1259
|
+
const dealId = rest[0];
|
|
1260
|
+
const blockId = rest[1];
|
|
1261
|
+
if (!dealId || !blockId) {
|
|
1262
|
+
throw new Error("Usage: llama brief history <dealId> <blockId> [--limit 50]");
|
|
1263
|
+
}
|
|
1264
|
+
const params = new URLSearchParams();
|
|
1265
|
+
if (flags.limit) params.set("limit", String(flags.limit));
|
|
1266
|
+
const qs = params.toString() ? `?${params.toString()}` : "";
|
|
1267
|
+
print(await request(
|
|
1268
|
+
"GET",
|
|
1269
|
+
`/api/deals/${encodeURIComponent(dealId)}/blocks/${encodeURIComponent(blockId)}/history${qs}`
|
|
1270
|
+
));
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
if (area === "brief" && action === "restore-version") {
|
|
1275
|
+
const dealId = rest[0];
|
|
1276
|
+
const blockId = rest[1];
|
|
1277
|
+
const historyId = rest[2];
|
|
1278
|
+
if (!dealId || !blockId || !historyId) {
|
|
1279
|
+
throw new Error("Usage: llama brief restore-version <dealId> <blockId> <historyId>\n" +
|
|
1280
|
+
" Find <historyId> via `llama brief history <dealId> <blockId>`");
|
|
1281
|
+
}
|
|
1282
|
+
const idNum = Number(historyId);
|
|
1283
|
+
if (!Number.isFinite(idNum)) throw new Error(`<historyId> must be a number, got "${historyId}"`);
|
|
1284
|
+
print(await request(
|
|
1285
|
+
"POST",
|
|
1286
|
+
`/api/deals/${encodeURIComponent(dealId)}/blocks/${encodeURIComponent(blockId)}/history`,
|
|
1287
|
+
{ history_id: idNum }
|
|
1288
|
+
));
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
// ----- Admin (system admin only) -----
|
|
1293
|
+
// System-admin gated commands — server enforces via isSystemAdmin()
|
|
1294
|
+
// checking LLAMA_COMMAND_ADMIN_EMAILS env. Non-admin tokens get 403.
|
|
1295
|
+
// CLI doesn't pre-check; if you can't run these, ask the system admin
|
|
1296
|
+
// to mint you an admin token (rare — most ops should never need this surface).
|
|
1297
|
+
//
|
|
1298
|
+
// The three event feeds map 1:1 to the /admin web console tabs:
|
|
1299
|
+
// - auth-events : signin / token / impersonation audit (security)
|
|
1300
|
+
// - deal-events : every field/owner/brief change cross-deal (business)
|
|
1301
|
+
// - agent-events : every AI tool call / loop_stalled / max_turns (AI ops)
|
|
1302
|
+
if (area === "admin") {
|
|
1303
|
+
const sub = action;
|
|
1304
|
+
const valid = ["auth-events", "deal-events", "agent-events"];
|
|
1305
|
+
if (!valid.includes(sub)) {
|
|
1306
|
+
throw new Error(
|
|
1307
|
+
`Unknown admin sub-command "${sub || ""}". Use: ${valid.join(", ")}`
|
|
1308
|
+
);
|
|
1309
|
+
}
|
|
1310
|
+
const { flags } = parseFlags(rest);
|
|
1311
|
+
const params = new URLSearchParams();
|
|
1312
|
+
// Common filters across all three.
|
|
1313
|
+
if (flags.kind) params.set("kind", String(flags.kind));
|
|
1314
|
+
if (flags.actor) params.set("actor", String(flags.actor));
|
|
1315
|
+
if (flags.subject) params.set("subject", String(flags.subject));
|
|
1316
|
+
if (flags.since) params.set("since", String(flags.since));
|
|
1317
|
+
if (flags.limit) params.set("limit", String(flags.limit));
|
|
1318
|
+
if (flags.offset) params.set("offset", String(flags.offset));
|
|
1319
|
+
// Per-feed extras.
|
|
1320
|
+
if (sub === "deal-events" && flags.deal) params.set("deal", String(flags.deal));
|
|
1321
|
+
if (sub === "agent-events") {
|
|
1322
|
+
if (flags["agent-kind"]) params.set("agent_kind", String(flags["agent-kind"]));
|
|
1323
|
+
if (flags.tool) params.set("tool", String(flags.tool));
|
|
1324
|
+
if (flags.deal) params.set("deal", String(flags.deal));
|
|
1325
|
+
if (flags["errors-only"]) params.set("errors_only", "1");
|
|
1326
|
+
}
|
|
1327
|
+
const qs = params.toString() ? `?${params.toString()}` : "";
|
|
1328
|
+
print(await request("GET", `/api/admin/${sub}${qs}`));
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
// ----- Mentions / Inbox -----
|
|
1333
|
+
// Server stores @-cues parsed out of brief blocks and posts in the
|
|
1334
|
+
// `deal_mentions` table. UNIQUE per (source_kind, source_id, user)
|
|
1335
|
+
// means re-saving a block that already cued someone won't re-fire
|
|
1336
|
+
// the email; resolution is mutual-observability (anyone can mark a
|
|
1337
|
+
// thread resolved, we record who).
|
|
1338
|
+
//
|
|
1339
|
+
// The CLI has no direct create — to "mention someone", write
|
|
1340
|
+
// `@FirstName` (or `@email@llamaventures.vc`) inside a brief block
|
|
1341
|
+
// body or a deal post. Hooks server-side do the rest.
|
|
1342
|
+
if (area === "mentions") {
|
|
1343
|
+
const sub = action || "list";
|
|
1344
|
+
if (sub === "list" || sub === undefined) {
|
|
1345
|
+
const { flags } = parseFlags(rest);
|
|
1346
|
+
const params = new URLSearchParams();
|
|
1347
|
+
if (flags.everyone) params.set("everyone", "1");
|
|
1348
|
+
else params.set("for_me", "1");
|
|
1349
|
+
if (!flags.all) params.set("unresolved", "1");
|
|
1350
|
+
print(await request("GET", `/api/mentions?${params.toString()}`));
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1353
|
+
if (sub === "show") {
|
|
1354
|
+
const id = rest[0];
|
|
1355
|
+
if (!id) throw new Error("Usage: llama mentions show <mentionId>");
|
|
1356
|
+
// No dedicated single-row endpoint — fetch all and filter. Cheap
|
|
1357
|
+
// (mentions table is small) and avoids a roundtrip endpoint.
|
|
1358
|
+
const data = await request("GET", "/api/mentions?everyone=1");
|
|
1359
|
+
const row = (data.mentions ?? []).find((m) => String(m.id) === String(id));
|
|
1360
|
+
if (!row) throw new Error(`mention ${id} not found`);
|
|
1361
|
+
print(row);
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
if (sub === "resolve") {
|
|
1365
|
+
const id = rest[0];
|
|
1366
|
+
if (!id) throw new Error("Usage: llama mentions resolve <mentionId>");
|
|
1367
|
+
print(await request("POST", `/api/mentions/${encodeURIComponent(id)}/resolve`));
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
if (sub === "unread") {
|
|
1371
|
+
print(await request("GET", "/api/mentions/unread-count"));
|
|
1372
|
+
return;
|
|
1373
|
+
}
|
|
1374
|
+
throw new Error(`Unknown mentions subcommand "${sub}". Use: list / show / resolve / unread.`);
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
// ----- Skill corrections (persona-owner pushback workflow) -----
|
|
1378
|
+
// Persona owners (or system admins) record long-term rules each
|
|
1379
|
+
// persona-DD skill must obey. Read by the persona-watcher and prepended
|
|
1380
|
+
// to the system prompt at run time. Soft-delete is non-cascading —
|
|
1381
|
+
// deleting a row does NOT auto-fire watcher; the next natural run
|
|
1382
|
+
// (manual / stale_re_eval) just stops including the deleted rule.
|
|
1383
|
+
//
|
|
1384
|
+
// Permissions enforced server-side: only the persona owner (per
|
|
1385
|
+
// PERSONA_SKILLS in src/lib/persona-skills.ts) or a system admin can
|
|
1386
|
+
// POST or DELETE. Anyone can GET. External personas (owner_email=null)
|
|
1387
|
+
// are admin-only for write.
|
|
1388
|
+
if (area === "skill-correction") {
|
|
1389
|
+
const sub = action;
|
|
1390
|
+
|
|
1391
|
+
if (sub === "list") {
|
|
1392
|
+
const skillSlug = rest[0];
|
|
1393
|
+
if (!skillSlug) {
|
|
1394
|
+
throw new Error("Usage: llama skill-correction list <skill-slug> [--include-deleted]");
|
|
1395
|
+
}
|
|
1396
|
+
const { flags } = parseFlags(rest.slice(1));
|
|
1397
|
+
const params = new URLSearchParams({ skill: skillSlug });
|
|
1398
|
+
if (flags["include-deleted"]) params.set("include_deleted", "1");
|
|
1399
|
+
print(await request("GET", `/api/skill-corrections?${params.toString()}`));
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
if (sub === "add") {
|
|
1404
|
+
const { flags, positional } = parseFlags(rest);
|
|
1405
|
+
const skillSlug = positional[0];
|
|
1406
|
+
const correctionText = positional.slice(1).join(" ").trim();
|
|
1407
|
+
if (!skillSlug || !correctionText) {
|
|
1408
|
+
throw new Error(
|
|
1409
|
+
`Usage: llama skill-correction add <skill-slug> "<correction text>" ` +
|
|
1410
|
+
`[--deal <uuid>] [--block <blockId>]`
|
|
1411
|
+
);
|
|
1412
|
+
}
|
|
1413
|
+
print(await request("POST", "/api/skill-corrections", {
|
|
1414
|
+
skill_slug: skillSlug,
|
|
1415
|
+
correction_text: correctionText,
|
|
1416
|
+
triggered_in_deal_uuid: flags.deal ? String(flags.deal) : null,
|
|
1417
|
+
triggered_in_block_id: flags.block ? String(flags.block) : null,
|
|
1418
|
+
}));
|
|
1419
|
+
return;
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
if (sub === "delete") {
|
|
1423
|
+
const id = rest[0];
|
|
1424
|
+
if (!id) throw new Error("Usage: llama skill-correction delete <id>");
|
|
1425
|
+
print(await request("DELETE", `/api/skill-corrections/${encodeURIComponent(id)}`));
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
throw new Error(
|
|
1430
|
+
`Unknown skill-correction subcommand "${sub || ""}". Use: list / add / delete.`
|
|
1431
|
+
);
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
usage();
|
|
1435
|
+
process.exitCode = 1;
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
main().catch((error) => {
|
|
1439
|
+
console.error(`Error: ${error.message}`);
|
|
1440
|
+
process.exit(1);
|
|
1441
|
+
});
|