@llamaventures/cli 1.15.1 → 1.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENT_BRIEFING.md +17 -7
- package/CHANGELOG.md +70 -1
- package/README.md +31 -6
- package/README.zh-CN.md +5 -4
- package/bin/llama-mcp.mjs +412 -25
- package/bin/llama.mjs +596 -38
- package/lib/client.mjs +353 -4
- package/package.json +3 -2
- package/scripts/verify-agent-routing.mjs +387 -18
package/bin/llama.mjs
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import { createRequire } from "module";
|
|
4
|
+
import { randomUUID } from "crypto";
|
|
3
5
|
import readline from "readline";
|
|
4
6
|
import {
|
|
5
7
|
DEFAULT_BASE_URL,
|
|
@@ -9,6 +11,7 @@ import {
|
|
|
9
11
|
TOKEN_FILE,
|
|
10
12
|
getAuthHeaders,
|
|
11
13
|
getBaseUrl,
|
|
14
|
+
getLastAgentEvent,
|
|
12
15
|
getToken,
|
|
13
16
|
print,
|
|
14
17
|
readBriefing,
|
|
@@ -33,6 +36,22 @@ import { LLAMA_CLI_CLIENT_ID, pkceLoopbackFlow, revokeToken as revokeOAuthToken
|
|
|
33
36
|
import { deleteBundle, detectBackend, readBundle, writeBundle } from "../lib/oauth-storage.mjs";
|
|
34
37
|
import { maybeNudgeUpdate, getUpdateNudge } from "../lib/version-check.mjs";
|
|
35
38
|
|
|
39
|
+
const requireFromHere = createRequire(import.meta.url);
|
|
40
|
+
const { version: PKG_VERSION } = requireFromHere("../package.json");
|
|
41
|
+
|
|
42
|
+
function newHtmlUploadId() {
|
|
43
|
+
return `cli-${randomUUID()}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function normalizeUploadId(value) {
|
|
47
|
+
if (typeof value !== "string" || !value.trim()) return null;
|
|
48
|
+
const id = value.trim();
|
|
49
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(id)) {
|
|
50
|
+
throw new Error("--upload-id must be 1-128 chars: letters, numbers, dot, underscore, colon, or hyphen");
|
|
51
|
+
}
|
|
52
|
+
return id;
|
|
53
|
+
}
|
|
54
|
+
|
|
36
55
|
function parseFlags(args, knownFlags = null) {
|
|
37
56
|
const flags = {};
|
|
38
57
|
const positional = [];
|
|
@@ -71,6 +90,34 @@ function parseFlags(args, knownFlags = null) {
|
|
|
71
90
|
return { flags, positional };
|
|
72
91
|
}
|
|
73
92
|
|
|
93
|
+
function agentOnboardNoAuthMessage() {
|
|
94
|
+
return `Llama Ventures team onboarding requires credentials.
|
|
95
|
+
|
|
96
|
+
Team member?
|
|
97
|
+
- Run \`gcloud auth login\` with your @llamaventures.vc account, OR
|
|
98
|
+
- Mint a token at https://command.llamaventures.vc/settings/tokens
|
|
99
|
+
then \`llama token set <llc_...>\`.
|
|
100
|
+
Re-run \`llama agent-onboard\` after — the workflow contract will print.
|
|
101
|
+
|
|
102
|
+
Founder or external visitor (no Llama account)?
|
|
103
|
+
Run \`llama pitch start --name "Your Name" --email "you@company.com"\`
|
|
104
|
+
to chat with our intake agent — no token required.`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function agentOnboardRejectedMessage() {
|
|
108
|
+
return `Llama Ventures team onboarding requires valid credentials.
|
|
109
|
+
|
|
110
|
+
Server rejected the credentials we sent. Re-mint at
|
|
111
|
+
https://command.llamaventures.vc/settings/tokens, run
|
|
112
|
+
\`llama token set <llc_...>\`, then re-run \`llama agent-onboard\`.`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function fetchServerAgentBriefing() {
|
|
116
|
+
const params = new URLSearchParams({ clientVersion: PKG_VERSION });
|
|
117
|
+
const result = await request("GET", `/api/agent/briefing?${params}`);
|
|
118
|
+
return result?.briefing || "";
|
|
119
|
+
}
|
|
120
|
+
|
|
74
121
|
function closestKnownFlag(input, candidates) {
|
|
75
122
|
let best = null;
|
|
76
123
|
let bestScore = Infinity;
|
|
@@ -127,6 +174,45 @@ function slugifyTitle(title) {
|
|
|
127
174
|
return slug;
|
|
128
175
|
}
|
|
129
176
|
|
|
177
|
+
function parseExpectedIds(raw) {
|
|
178
|
+
const text = typeof raw === "string" ? raw : "";
|
|
179
|
+
const expected = { dealIds: [], wikiSlugs: [], raw: [] };
|
|
180
|
+
for (const item of text.split(",").map((s) => s.trim()).filter(Boolean)) {
|
|
181
|
+
const [kind, ...rest] = item.split(":");
|
|
182
|
+
const value = rest.join(":").trim();
|
|
183
|
+
if (kind === "deal" && value) expected.dealIds.push(value);
|
|
184
|
+
else if ((kind === "wiki" || kind === "slug") && value) expected.wikiSlugs.push(value);
|
|
185
|
+
else expected.raw.push(item);
|
|
186
|
+
}
|
|
187
|
+
return expected;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function submitEvalFeedback(action, flags, queryText = "") {
|
|
191
|
+
const useLast = flags.last !== false;
|
|
192
|
+
const last = useLast ? getLastAgentEvent() : null;
|
|
193
|
+
const eventId =
|
|
194
|
+
flags.event && flags.event !== true
|
|
195
|
+
? Number(flags.event)
|
|
196
|
+
: last?.lastEventId ?? null;
|
|
197
|
+
if ((action === "good" || action === "bad") && !eventId && !queryText) {
|
|
198
|
+
throw new Error(`Usage: llama eval ${action} [--last] [--reason "..."]`);
|
|
199
|
+
}
|
|
200
|
+
const body = {
|
|
201
|
+
action,
|
|
202
|
+
eventId: Number.isFinite(eventId) ? eventId : undefined,
|
|
203
|
+
query: queryText || undefined,
|
|
204
|
+
surface: flags.surface && flags.surface !== true ? String(flags.surface) : last?.lastSurface ?? undefined,
|
|
205
|
+
expected:
|
|
206
|
+
flags.expect && flags.expect !== true
|
|
207
|
+
? parseExpectedIds(String(flags.expect))
|
|
208
|
+
: {},
|
|
209
|
+
reason: flags.reason && flags.reason !== true ? String(flags.reason) : undefined,
|
|
210
|
+
privacyLevel:
|
|
211
|
+
flags.privacy && flags.privacy !== true ? String(flags.privacy) : "internal",
|
|
212
|
+
};
|
|
213
|
+
return request("POST", "/api/agent/eval-feedback", body);
|
|
214
|
+
}
|
|
215
|
+
|
|
130
216
|
// Client-side fuzzy match — used as a fallback when the server hasn't yet
|
|
131
217
|
// shipped the search/filter API (Fix B, 2026-04-25). Once the server
|
|
132
218
|
// returns the `{deals,total,limit,offset}` envelope, this path is never
|
|
@@ -282,6 +368,8 @@ Agent onboarding (run once on first install):
|
|
|
282
368
|
llama skills search "pipeline update" # discover relevant runtime skills
|
|
283
369
|
llama skills show llama-pipeline # read a skill from Command
|
|
284
370
|
llama explain <url-or-object> # explain Command URL/object status + lifecycle
|
|
371
|
+
llama eval good|bad --last # mark the latest CLI/MCP result for eval
|
|
372
|
+
llama eval add "<query>" --expect wiki:<slug>|deal:<uuid>
|
|
285
373
|
|
|
286
374
|
External pitch — talk to Llama Ventures' intake agent (no token required):
|
|
287
375
|
llama pitch start --name "Jane Doe" --email "jane@acme.ai"
|
|
@@ -424,7 +512,7 @@ Mentions / Inbox:
|
|
|
424
512
|
llama mentions unread # just the badge count
|
|
425
513
|
|
|
426
514
|
Where does this HTML / thesis / artifact go?
|
|
427
|
-
About ONE specific deal? ........ llama html
|
|
515
|
+
About ONE specific deal? ........ llama html publish <deal-id-or-name> --file <path> --title "..."
|
|
428
516
|
(renders at /deals/<id>/browse/<slug>; see "Deal page HTML" below)
|
|
429
517
|
Cross-deal / institutional? ..... llama wiki save <slug> --title "..." --file <path>.html --sources "..."
|
|
430
518
|
(renders at /wiki/<slug>; see "Wiki" below)
|
|
@@ -442,7 +530,7 @@ Wiki:
|
|
|
442
530
|
(.html / .htm extension auto-implies content_type=html)
|
|
443
531
|
Native comments + working in-page (#) links are added automatically — just upload self-contained HTML.
|
|
444
532
|
➜ Use Wiki when the artifact is NOT tied to one specific deal — sector landscape, market map,
|
|
445
|
-
thesis, framework, methodology. For deal-specific HTML use "llama html
|
|
533
|
+
thesis, framework, methodology. For deal-specific HTML use "llama html publish <deal>" instead.
|
|
446
534
|
Delete / restore (soft — reversible):
|
|
447
535
|
llama wiki delete <slug> [--lang en|zh]
|
|
448
536
|
llama wiki restore <slug> [--lang en|zh]
|
|
@@ -461,6 +549,11 @@ Deal page HTML (hand-authored sandboxed pages on /deals/<id>/browse/<slug>):
|
|
|
461
549
|
Each one has a stable slug. UPLOAD must declare intent — update an existing
|
|
462
550
|
artifact or add a new one — to avoid silent overwrites.
|
|
463
551
|
|
|
552
|
+
Agent-safe publish path (recommended for Claude Code / Codex / Cursor):
|
|
553
|
+
llama html publish <deal-id-or-name> --file <path> [--title "..."] [--doc <slug>]
|
|
554
|
+
# Defaults to NEW doc unless --doc points at an existing slug; verifies version/bytes/sha256 after upload.
|
|
555
|
+
# Auto-detects sibling *_files asset folders unless --no-auto-assets is set.
|
|
556
|
+
|
|
464
557
|
List existing artifacts:
|
|
465
558
|
llama html docs <dealId> # who-has-what
|
|
466
559
|
llama html docs create <dealId> <slug> [--title "..."] # pre-create a slot
|
|
@@ -489,7 +582,7 @@ Deal page HTML (hand-authored sandboxed pages on /deals/<id>/browse/<slug>):
|
|
|
489
582
|
Caps: HTML 5 MB, each asset 50 MB, total bundle 100 MB. Every write
|
|
490
583
|
triggers SSE push — any browser viewing /deals/<id>/browse refreshes
|
|
491
584
|
automatically. Same write path as the in-app deal agent's
|
|
492
|
-
update_deal_browse_html tool and the MCP
|
|
585
|
+
update_deal_browse_html tool and the MCP html_upload_file tool.
|
|
493
586
|
|
|
494
587
|
Admin (system admin only — server returns 403 for non-admin tokens):
|
|
495
588
|
llama admin auth-events [--kind X] [--actor email] [--subject email] [--since 24h|7d|30d|<ISO>] [--limit 100]
|
|
@@ -525,6 +618,7 @@ Common:
|
|
|
525
618
|
llama agent bootstrap live Llama OS skill manifest from Command
|
|
526
619
|
llama skills search "<query>" discover which skill to read
|
|
527
620
|
llama explain <url-or-object> explain Command URLs, 404s, deleted objects
|
|
621
|
+
llama eval bad --last mark latest CLI/MCP result as an eval candidate
|
|
528
622
|
|
|
529
623
|
Command groups — run \`llama help <group>\` for that group's commands:
|
|
530
624
|
deal create · show · feed · update · enrich · search · collaborators · links · delete
|
|
@@ -532,6 +626,7 @@ Command groups — run \`llama help <group>\` for that group's commands:
|
|
|
532
626
|
facts deal facts + skill corrections (the sourced, trust-rated layer)
|
|
533
627
|
timeline timeline · posts · mentions
|
|
534
628
|
wiki cross-deal knowledge entries (markdown or HTML)
|
|
629
|
+
eval mark real CLI/MCP searches good/bad or add a golden-query candidate
|
|
535
630
|
memo long-form HTML investment memo
|
|
536
631
|
html deal-specific HTML artifacts (/deals/<id>/browse/<slug>)
|
|
537
632
|
pitch external founder intake (no token needed)
|
|
@@ -865,18 +960,15 @@ async function runPitchRepl() {
|
|
|
865
960
|
async function main() {
|
|
866
961
|
const [area, action, ...rest] = process.argv.slice(2);
|
|
867
962
|
if (area === "--version" || area === "-v" || area === "version") {
|
|
868
|
-
const { createRequire } = await import("module");
|
|
869
|
-
const requireFromHere = createRequire(import.meta.url);
|
|
870
|
-
const { version } = requireFromHere("../package.json");
|
|
871
963
|
// `llama version --check` — explicitly check npm for a newer release and
|
|
872
964
|
// print the upgrade line (or "up to date"). Lets an agent surface the
|
|
873
965
|
// nudge on demand, separate from the throttled, TTY-gated auto-nudge.
|
|
874
966
|
if (action === "--check" || action === "check") {
|
|
875
967
|
const nudge = await getUpdateNudge();
|
|
876
|
-
console.log(nudge || `llama CLI ${
|
|
968
|
+
console.log(nudge || `llama CLI ${PKG_VERSION} — up to date`);
|
|
877
969
|
return;
|
|
878
970
|
}
|
|
879
|
-
console.log(
|
|
971
|
+
console.log(PKG_VERSION);
|
|
880
972
|
return;
|
|
881
973
|
}
|
|
882
974
|
if (!area || area === "help" || area === "--help" || area === "-h") {
|
|
@@ -897,12 +989,13 @@ async function main() {
|
|
|
897
989
|
return;
|
|
898
990
|
}
|
|
899
991
|
|
|
900
|
-
// `llama agent-onboard` —
|
|
901
|
-
//
|
|
902
|
-
//
|
|
992
|
+
// `llama agent-onboard` — fetch the server-owned Agent Runtime Contract
|
|
993
|
+
// so an AI agent reads the current Llama Ventures workflow contract. The
|
|
994
|
+
// bundled AGENT_BRIEFING.md is now only a fallback when the server route
|
|
995
|
+
// is unavailable during rollout.
|
|
903
996
|
// Also: `llama agent onboard` (two-word form) for symmetry.
|
|
904
997
|
//
|
|
905
|
-
// Gated behind
|
|
998
|
+
// Gated behind Command auth — without valid credentials we print a short
|
|
906
999
|
// bootstrap stub instead. Stops unauthenticated callers from harvesting
|
|
907
1000
|
// internal command surface / workflow conventions just by running the
|
|
908
1001
|
// public CLI.
|
|
@@ -912,39 +1005,24 @@ async function main() {
|
|
|
912
1005
|
) {
|
|
913
1006
|
const headers = await getAuthHeaders();
|
|
914
1007
|
if (Object.keys(headers).length === 0) {
|
|
915
|
-
console.log(
|
|
916
|
-
`Llama Ventures team onboarding requires credentials.
|
|
917
|
-
|
|
918
|
-
Team member?
|
|
919
|
-
- Run \`gcloud auth login\` with your @llamaventures.vc account, OR
|
|
920
|
-
- Mint a token at https://command.llamaventures.vc/settings/tokens
|
|
921
|
-
then \`llama token set <llc_...>\`.
|
|
922
|
-
Re-run \`llama agent-onboard\` after — the workflow contract will print.
|
|
923
|
-
|
|
924
|
-
Founder or external visitor (no Llama account)?
|
|
925
|
-
Run \`llama pitch start --name "Your Name" --email "you@company.com"\`
|
|
926
|
-
to chat with our intake agent — no token required.`
|
|
927
|
-
);
|
|
1008
|
+
console.log(agentOnboardNoAuthMessage());
|
|
928
1009
|
return;
|
|
929
1010
|
}
|
|
930
1011
|
try {
|
|
931
|
-
await
|
|
1012
|
+
const briefing = await fetchServerAgentBriefing();
|
|
1013
|
+
process.stdout.write(briefing || readBriefing());
|
|
932
1014
|
} catch (e) {
|
|
933
1015
|
const msg = e?.message || "";
|
|
934
1016
|
if (msg.includes("Error[UNAUTHORIZED]") || msg.includes("Error[NO_AUTH]")) {
|
|
935
|
-
console.log(
|
|
936
|
-
`Llama Ventures team onboarding requires valid credentials.
|
|
937
|
-
|
|
938
|
-
Server rejected the credentials we sent. Re-mint at
|
|
939
|
-
https://command.llamaventures.vc/settings/tokens, run
|
|
940
|
-
\`llama token set <llc_...>\`, then re-run \`llama agent-onboard\`.`
|
|
941
|
-
);
|
|
1017
|
+
console.log(agentOnboardRejectedMessage());
|
|
942
1018
|
process.exitCode = 1;
|
|
943
1019
|
return;
|
|
944
1020
|
}
|
|
945
|
-
|
|
1021
|
+
process.stderr.write(
|
|
1022
|
+
`warning: server agent briefing unavailable (${msg}); using bundled fallback.\n`,
|
|
1023
|
+
);
|
|
1024
|
+
process.stdout.write(readBriefing());
|
|
946
1025
|
}
|
|
947
|
-
process.stdout.write(readBriefing());
|
|
948
1026
|
return;
|
|
949
1027
|
}
|
|
950
1028
|
|
|
@@ -954,6 +1032,7 @@ https://command.llamaventures.vc/settings/tokens, run
|
|
|
954
1032
|
if (area === "agent" && action === "bootstrap") {
|
|
955
1033
|
const { flags } = parseFlags(rest, ["json", "limit"]);
|
|
956
1034
|
const params = new URLSearchParams();
|
|
1035
|
+
params.set("clientVersion", PKG_VERSION);
|
|
957
1036
|
if (flags.limit && flags.limit !== true) params.set("limit", String(flags.limit));
|
|
958
1037
|
const manifest = await request("GET", `/api/agent/manifest${params.toString() ? `?${params}` : ""}`);
|
|
959
1038
|
if (flags.json) {
|
|
@@ -1037,6 +1116,43 @@ https://command.llamaventures.vc/settings/tokens, run
|
|
|
1037
1116
|
return;
|
|
1038
1117
|
}
|
|
1039
1118
|
|
|
1119
|
+
if (area === "eval") {
|
|
1120
|
+
const sub = action;
|
|
1121
|
+
if (sub === "good" || sub === "bad") {
|
|
1122
|
+
const { flags, positional } = parseFlags(rest, [
|
|
1123
|
+
"last",
|
|
1124
|
+
"event",
|
|
1125
|
+
"reason",
|
|
1126
|
+
"expect",
|
|
1127
|
+
"surface",
|
|
1128
|
+
"privacy",
|
|
1129
|
+
]);
|
|
1130
|
+
const q = positional.join(" ").trim();
|
|
1131
|
+
print(await submitEvalFeedback(sub, flags, q));
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
if (sub === "add") {
|
|
1135
|
+
const { flags, positional } = parseFlags(rest, [
|
|
1136
|
+
"event",
|
|
1137
|
+
"expect",
|
|
1138
|
+
"reason",
|
|
1139
|
+
"surface",
|
|
1140
|
+
"privacy",
|
|
1141
|
+
]);
|
|
1142
|
+
const q = positional.join(" ").trim();
|
|
1143
|
+
if (!q && !flags.event) {
|
|
1144
|
+
throw new Error(
|
|
1145
|
+
`Usage: llama eval add "<query>" --surface deal|wiki --expect wiki:<slug>|deal:<uuid>`,
|
|
1146
|
+
);
|
|
1147
|
+
}
|
|
1148
|
+
print(await submitEvalFeedback("add", flags, q));
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
throw new Error(
|
|
1152
|
+
"Usage: llama eval good|bad [--last] [--reason ...] OR llama eval add \"<query>\" --expect wiki:<slug>|deal:<uuid>",
|
|
1153
|
+
);
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1040
1156
|
// `llama pitch ...` — external founder-pitch family. No Llama token
|
|
1041
1157
|
// required; bootstraps a session against /api/external/* via PoW + cookie.
|
|
1042
1158
|
// See lib/external.mjs and AGENT_BRIEFING.md for the full surface.
|
|
@@ -2427,10 +2543,268 @@ Routing — is this the right command?
|
|
|
2427
2543
|
// --doc <slug> selects which named document on the deal (default 'main').
|
|
2428
2544
|
// Slugs match /^[a-z0-9][a-z0-9_-]{0,63}$/. Use `llama html docs <dealId>`
|
|
2429
2545
|
// to list available slugs.
|
|
2546
|
+
const MAX_HTML_BYTES = 5 * 1024 * 1024;
|
|
2547
|
+
const MAX_ASSET_BYTES = 50 * 1024 * 1024;
|
|
2548
|
+
const MAX_BUNDLE_BYTES = 100 * 1024 * 1024;
|
|
2549
|
+
|
|
2430
2550
|
function htmlEndpoint(dealId, slug) {
|
|
2431
2551
|
return `/api/deals/${encodeURIComponent(dealId)}/documents/${encodeURIComponent(slug)}/html`;
|
|
2432
2552
|
}
|
|
2433
2553
|
|
|
2554
|
+
function looksLikeHtml(html) {
|
|
2555
|
+
const head = String(html || "").trim().slice(0, 256).toLowerCase();
|
|
2556
|
+
return head.startsWith("<!doctype html") || head.startsWith("<html");
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2559
|
+
function extractHtmlTitle(html) {
|
|
2560
|
+
const title = String(html || "").match(/<title[^>]*>([\s\S]*?)<\/title>/i)?.[1];
|
|
2561
|
+
if (!title) return null;
|
|
2562
|
+
const clean = title.replace(/\s+/g, " ").trim();
|
|
2563
|
+
return clean ? clean.slice(0, 200) : null;
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
function docHasHtml(d) {
|
|
2567
|
+
return Boolean(d && (d.latest_version > 0 || d.latest_updated_at));
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
function findDocBySlug(docs, slug) {
|
|
2571
|
+
return docs.find((d) => d && d.slug === slug) || null;
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2574
|
+
function nextAvailableSlug(base, docs) {
|
|
2575
|
+
let candidate = base;
|
|
2576
|
+
let suffix = 2;
|
|
2577
|
+
while (findDocBySlug(docs, candidate)) {
|
|
2578
|
+
candidate = `${base.slice(0, Math.max(1, 64 - String(suffix).length - 1))}-${suffix}`;
|
|
2579
|
+
suffix += 1;
|
|
2580
|
+
}
|
|
2581
|
+
return candidate;
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2584
|
+
function mimeForAsset(path) {
|
|
2585
|
+
const ext = (String(path).split(".").pop() || "").toLowerCase();
|
|
2586
|
+
return (
|
|
2587
|
+
{
|
|
2588
|
+
jpg: "image/jpeg",
|
|
2589
|
+
jpeg: "image/jpeg",
|
|
2590
|
+
png: "image/png",
|
|
2591
|
+
gif: "image/gif",
|
|
2592
|
+
webp: "image/webp",
|
|
2593
|
+
svg: "image/svg+xml",
|
|
2594
|
+
ico: "image/x-icon",
|
|
2595
|
+
avif: "image/avif",
|
|
2596
|
+
css: "text/css",
|
|
2597
|
+
js: "text/javascript",
|
|
2598
|
+
json: "application/json",
|
|
2599
|
+
woff: "font/woff",
|
|
2600
|
+
woff2: "font/woff2",
|
|
2601
|
+
ttf: "font/ttf",
|
|
2602
|
+
otf: "font/otf",
|
|
2603
|
+
mp4: "video/mp4",
|
|
2604
|
+
webm: "video/webm",
|
|
2605
|
+
pdf: "application/pdf",
|
|
2606
|
+
}[ext] || "application/octet-stream"
|
|
2607
|
+
);
|
|
2608
|
+
}
|
|
2609
|
+
|
|
2610
|
+
async function listHtmlDocs(dealId) {
|
|
2611
|
+
const docList = await request(
|
|
2612
|
+
"GET",
|
|
2613
|
+
`/api/deals/${encodeURIComponent(dealId)}/documents`,
|
|
2614
|
+
);
|
|
2615
|
+
return Array.isArray(docList?.documents) ? docList.documents : [];
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2618
|
+
async function resolveDealForHtmlPublish(dealRef) {
|
|
2619
|
+
const ref = String(dealRef || "").trim();
|
|
2620
|
+
if (!ref) throw new Error("deal id or name is required");
|
|
2621
|
+
try {
|
|
2622
|
+
await listHtmlDocs(ref);
|
|
2623
|
+
return { dealId: ref, resolvedFrom: "id" };
|
|
2624
|
+
} catch {
|
|
2625
|
+
// Not a readable deal id; fall through to pipeline search.
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2628
|
+
const result = await searchDeals(ref, { limit: 10 });
|
|
2629
|
+
const deals = Array.isArray(result?.deals) ? result.deals : [];
|
|
2630
|
+
if (deals.length === 0) {
|
|
2631
|
+
throw new Error(
|
|
2632
|
+
`No deal matched "${ref}". Run \`llama deal search "${ref}"\` first and pass the exact deal id.`,
|
|
2633
|
+
);
|
|
2634
|
+
}
|
|
2635
|
+
const exact = deals.filter(
|
|
2636
|
+
(d) => String(d.companyName || "").toLowerCase() === ref.toLowerCase(),
|
|
2637
|
+
);
|
|
2638
|
+
const candidates = exact.length > 0 ? exact : deals;
|
|
2639
|
+
if (candidates.length !== 1) {
|
|
2640
|
+
const lines = candidates
|
|
2641
|
+
.slice(0, 8)
|
|
2642
|
+
.map((d) => `- ${d.companyName || "(unnamed)"} — ${d.uuid || d.id}`)
|
|
2643
|
+
.join("\n");
|
|
2644
|
+
throw new Error(
|
|
2645
|
+
`Deal name "${ref}" matched multiple records. Re-run with the exact deal id:\n${lines}`,
|
|
2646
|
+
);
|
|
2647
|
+
}
|
|
2648
|
+
const dealId = candidates[0]?.uuid || candidates[0]?.id;
|
|
2649
|
+
if (!dealId) {
|
|
2650
|
+
throw new Error(`Deal search matched "${ref}" but did not return a deal id.`);
|
|
2651
|
+
}
|
|
2652
|
+
return {
|
|
2653
|
+
dealId,
|
|
2654
|
+
dealName: candidates[0]?.companyName || ref,
|
|
2655
|
+
resolvedFrom: "search",
|
|
2656
|
+
};
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2659
|
+
async function detectSiblingAssetsDir(filePath) {
|
|
2660
|
+
const { existsSync, statSync } = await import("fs");
|
|
2661
|
+
const { dirname, basename, extname, join } = await import("path");
|
|
2662
|
+
const dir = dirname(filePath);
|
|
2663
|
+
const ext = extname(filePath);
|
|
2664
|
+
const stem = basename(filePath, ext);
|
|
2665
|
+
const candidates = [
|
|
2666
|
+
`${stem}_files`,
|
|
2667
|
+
`${stem} files`,
|
|
2668
|
+
`${basename(filePath)}_files`,
|
|
2669
|
+
];
|
|
2670
|
+
for (const name of candidates) {
|
|
2671
|
+
const p = join(dir, name);
|
|
2672
|
+
if (existsSync(p) && statSync(p).isDirectory()) return p;
|
|
2673
|
+
}
|
|
2674
|
+
return null;
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
async function collectAssets(assetsRoot) {
|
|
2678
|
+
const { readFileSync, readdirSync, statSync } = await import("fs");
|
|
2679
|
+
const { join, relative, sep, basename } = await import("path");
|
|
2680
|
+
const rootStat = statSync(assetsRoot);
|
|
2681
|
+
if (!rootStat.isDirectory()) {
|
|
2682
|
+
throw new Error(`assets path must be a directory: ${assetsRoot}`);
|
|
2683
|
+
}
|
|
2684
|
+
const collected = [];
|
|
2685
|
+
const walk = (dir) => {
|
|
2686
|
+
for (const name of readdirSync(dir)) {
|
|
2687
|
+
const absPath = join(dir, name);
|
|
2688
|
+
const st = statSync(absPath);
|
|
2689
|
+
if (st.isDirectory()) {
|
|
2690
|
+
walk(absPath);
|
|
2691
|
+
} else if (st.isFile()) {
|
|
2692
|
+
const relPath = relative(assetsRoot, absPath).split(sep).join("/");
|
|
2693
|
+
collected.push({ absPath, relPath, bytes: st.size });
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
};
|
|
2697
|
+
walk(assetsRoot);
|
|
2698
|
+
if (collected.length === 0) {
|
|
2699
|
+
throw new Error(`assets directory is empty: ${assetsRoot}`);
|
|
2700
|
+
}
|
|
2701
|
+
const rootName = basename(assetsRoot);
|
|
2702
|
+
const looksLikeSavePageDir = /[_ ]files$/i.test(rootName);
|
|
2703
|
+
const finalPaths = looksLikeSavePageDir
|
|
2704
|
+
? collected.map((c) => ({ ...c, relPath: `${rootName}/${c.relPath}` }))
|
|
2705
|
+
: collected;
|
|
2706
|
+
let totalBytes = 0;
|
|
2707
|
+
for (const item of finalPaths) {
|
|
2708
|
+
if (item.relPath.split("/").some((seg) => seg === "..")) {
|
|
2709
|
+
throw new Error(`asset path "${item.relPath}" contains "..", refused`);
|
|
2710
|
+
}
|
|
2711
|
+
if (item.bytes > MAX_ASSET_BYTES) {
|
|
2712
|
+
throw new Error(
|
|
2713
|
+
`asset "${item.relPath}" is ${item.bytes} bytes; cap is ${MAX_ASSET_BYTES}`,
|
|
2714
|
+
);
|
|
2715
|
+
}
|
|
2716
|
+
totalBytes += item.bytes;
|
|
2717
|
+
if (totalBytes > MAX_BUNDLE_BYTES) {
|
|
2718
|
+
throw new Error(`total asset bytes exceeds ${MAX_BUNDLE_BYTES}`);
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
return {
|
|
2722
|
+
assets: finalPaths.map((item) => ({
|
|
2723
|
+
...item,
|
|
2724
|
+
data: readFileSync(item.absPath),
|
|
2725
|
+
contentType: mimeForAsset(item.relPath),
|
|
2726
|
+
})),
|
|
2727
|
+
totalBytes,
|
|
2728
|
+
};
|
|
2729
|
+
}
|
|
2730
|
+
|
|
2731
|
+
async function uploadHtmlPayload({ dealId, slug, html, source, assetsDir, uploadId }) {
|
|
2732
|
+
if (!assetsDir) {
|
|
2733
|
+
return request("PUT", htmlEndpoint(dealId, slug), {
|
|
2734
|
+
html,
|
|
2735
|
+
source,
|
|
2736
|
+
client_upload_id: uploadId,
|
|
2737
|
+
}, {
|
|
2738
|
+
headers: { "X-Llama-Upload-Id": uploadId },
|
|
2739
|
+
});
|
|
2740
|
+
}
|
|
2741
|
+
const { assets, totalBytes } = await collectAssets(assetsDir);
|
|
2742
|
+
const form = new FormData();
|
|
2743
|
+
form.append("html", html);
|
|
2744
|
+
form.append("source", source);
|
|
2745
|
+
form.append("client_upload_id", uploadId);
|
|
2746
|
+
for (const asset of assets) {
|
|
2747
|
+
form.append(
|
|
2748
|
+
`asset:${asset.relPath}`,
|
|
2749
|
+
new Blob([asset.data], { type: asset.contentType }),
|
|
2750
|
+
asset.relPath,
|
|
2751
|
+
);
|
|
2752
|
+
}
|
|
2753
|
+
console.error(
|
|
2754
|
+
`Uploading bundle: html ${Buffer.byteLength(html, "utf8")} bytes + ${assets.length} assets (${totalBytes} bytes)`,
|
|
2755
|
+
);
|
|
2756
|
+
const headers = await getAuthHeaders();
|
|
2757
|
+
const res = await fetch(`${getBaseUrl()}${htmlEndpoint(dealId, slug)}`, {
|
|
2758
|
+
method: "PUT",
|
|
2759
|
+
headers: { ...headers, "X-Llama-Upload-Id": uploadId },
|
|
2760
|
+
body: form,
|
|
2761
|
+
});
|
|
2762
|
+
const body = await res.json().catch(() => ({}));
|
|
2763
|
+
if (!res.ok) {
|
|
2764
|
+
throw new Error(
|
|
2765
|
+
`HTTP ${res.status}: ${body?.error || JSON.stringify(body).slice(0, 300)}`,
|
|
2766
|
+
);
|
|
2767
|
+
}
|
|
2768
|
+
return body;
|
|
2769
|
+
}
|
|
2770
|
+
|
|
2771
|
+
async function verifyHtmlUpload({ dealId, slug, expectedVersion, expectedBytes, expectedSha256 }) {
|
|
2772
|
+
const latest = await request("GET", htmlEndpoint(dealId, slug));
|
|
2773
|
+
if (latest?.empty) {
|
|
2774
|
+
throw new Error(`verification failed: ${slug} came back empty after upload`);
|
|
2775
|
+
}
|
|
2776
|
+
if (expectedVersion != null && Number(latest.version) !== Number(expectedVersion)) {
|
|
2777
|
+
throw new Error(
|
|
2778
|
+
`verification failed: expected version ${expectedVersion}, got ${latest.version}`,
|
|
2779
|
+
);
|
|
2780
|
+
}
|
|
2781
|
+
if (
|
|
2782
|
+
expectedBytes != null &&
|
|
2783
|
+
latest.bytes != null &&
|
|
2784
|
+
Number(latest.bytes) !== Number(expectedBytes)
|
|
2785
|
+
) {
|
|
2786
|
+
throw new Error(
|
|
2787
|
+
`verification failed: expected ${expectedBytes} bytes, got ${latest.bytes}`,
|
|
2788
|
+
);
|
|
2789
|
+
}
|
|
2790
|
+
if (
|
|
2791
|
+
expectedSha256 &&
|
|
2792
|
+
latest.sha256 &&
|
|
2793
|
+
String(latest.sha256) !== String(expectedSha256)
|
|
2794
|
+
) {
|
|
2795
|
+
throw new Error(
|
|
2796
|
+
`verification failed: expected sha256 ${expectedSha256}, got ${latest.sha256}`,
|
|
2797
|
+
);
|
|
2798
|
+
}
|
|
2799
|
+
return {
|
|
2800
|
+
ok: true,
|
|
2801
|
+
version: latest.version,
|
|
2802
|
+
bytes: latest.bytes,
|
|
2803
|
+
sha256: latest.sha256,
|
|
2804
|
+
created_at: latest.created_at,
|
|
2805
|
+
};
|
|
2806
|
+
}
|
|
2807
|
+
|
|
2434
2808
|
// Surface a clean `linked_wiki` field on linked docs so the listing
|
|
2435
2809
|
// reads as "this card points at wiki/<slug>" rather than exposing the
|
|
2436
2810
|
// raw source_wiki_* columns. Non-linked docs are returned unchanged.
|
|
@@ -2642,6 +3016,174 @@ Routing — is this the right command?
|
|
|
2642
3016
|
return;
|
|
2643
3017
|
}
|
|
2644
3018
|
|
|
3019
|
+
// publish — agent-safe high-level upload path. The agent gives us a file
|
|
3020
|
+
// path + a deal id/name; the CLI handles search, slug decisions, asset
|
|
3021
|
+
// discovery, upload, and read-after-write verification.
|
|
3022
|
+
if (sub === "publish") {
|
|
3023
|
+
const dealRef = rest[0];
|
|
3024
|
+
const knownFlags = [
|
|
3025
|
+
"file", "title", "doc", "slug", "new", "update",
|
|
3026
|
+
"assets", "no-auto-assets", "source", "no-verify", "upload-id",
|
|
3027
|
+
];
|
|
3028
|
+
const { flags } = parseFlags(rest.slice(1), knownFlags);
|
|
3029
|
+
if (!dealRef || !flags.file || flags.file === true) {
|
|
3030
|
+
throw new Error(
|
|
3031
|
+
"Usage: llama html publish <deal-id-or-name> --file PATH [--title \"...\"] [--doc <slug>] [--update|--new] [--assets DIR]",
|
|
3032
|
+
);
|
|
3033
|
+
}
|
|
3034
|
+
if (flags.slug && !flags.doc) {
|
|
3035
|
+
process.stderr.write("note: --slug accepted as alias for --doc.\n");
|
|
3036
|
+
flags.doc = flags.slug;
|
|
3037
|
+
}
|
|
3038
|
+
const wantsNew = boolFlag(flags, "new");
|
|
3039
|
+
const wantsUpdate = boolFlag(flags, "update");
|
|
3040
|
+
if (wantsNew && wantsUpdate) {
|
|
3041
|
+
throw new Error("Choose only one of --new or --update.");
|
|
3042
|
+
}
|
|
3043
|
+
|
|
3044
|
+
const filePath = String(flags.file);
|
|
3045
|
+
const { readFileSync, statSync } = await import("fs");
|
|
3046
|
+
const { basename, extname } = await import("path");
|
|
3047
|
+
const fileStat = statSync(filePath);
|
|
3048
|
+
if (!fileStat.isFile()) {
|
|
3049
|
+
throw new Error(`--file must point to a readable HTML file: ${filePath}`);
|
|
3050
|
+
}
|
|
3051
|
+
const html = readFileSync(filePath, "utf8");
|
|
3052
|
+
if (!html.trim()) throw new Error("HTML body is empty.");
|
|
3053
|
+
const htmlBytes = Buffer.byteLength(html, "utf8");
|
|
3054
|
+
if (htmlBytes > MAX_HTML_BYTES) {
|
|
3055
|
+
throw new Error(
|
|
3056
|
+
`HTML body is ${(htmlBytes / 1024 / 1024).toFixed(2)} MB; cap is 5 MB. Put large media in an asset folder or Drive, not inline HTML.`,
|
|
3057
|
+
);
|
|
3058
|
+
}
|
|
3059
|
+
if (!looksLikeHtml(html)) {
|
|
3060
|
+
throw new Error("HTML must start with <!doctype html> or <html.");
|
|
3061
|
+
}
|
|
3062
|
+
|
|
3063
|
+
const resolved = await resolveDealForHtmlPublish(dealRef);
|
|
3064
|
+
const docs = await listHtmlDocs(resolved.dealId);
|
|
3065
|
+
const explicitDoc =
|
|
3066
|
+
typeof flags.doc === "string" && flags.doc.trim()
|
|
3067
|
+
? flags.doc.trim()
|
|
3068
|
+
: null;
|
|
3069
|
+
const title =
|
|
3070
|
+
typeof flags.title === "string" && flags.title.trim()
|
|
3071
|
+
? flags.title.trim()
|
|
3072
|
+
: extractHtmlTitle(html) || basename(filePath, extname(filePath));
|
|
3073
|
+
let slug;
|
|
3074
|
+
let mode;
|
|
3075
|
+
let createdMetadata = false;
|
|
3076
|
+
|
|
3077
|
+
if (explicitDoc) {
|
|
3078
|
+
if (!isValidDocSlug(explicitDoc)) {
|
|
3079
|
+
throw new Error(
|
|
3080
|
+
`slug "${explicitDoc}" must match /^[a-z0-9][a-z0-9_-]{0,63}$/`,
|
|
3081
|
+
);
|
|
3082
|
+
}
|
|
3083
|
+
const existingDoc = findDocBySlug(docs, explicitDoc);
|
|
3084
|
+
if (wantsNew && existingDoc) {
|
|
3085
|
+
throw new Error(
|
|
3086
|
+
`--new requested, but document "${explicitDoc}" already exists on this deal.`,
|
|
3087
|
+
);
|
|
3088
|
+
}
|
|
3089
|
+
if (wantsUpdate && !existingDoc) {
|
|
3090
|
+
throw new Error(
|
|
3091
|
+
`--update requested, but document "${explicitDoc}" does not exist on this deal.`,
|
|
3092
|
+
);
|
|
3093
|
+
}
|
|
3094
|
+
slug = explicitDoc;
|
|
3095
|
+
mode = existingDoc && docHasHtml(existingDoc) ? "updated" : "created";
|
|
3096
|
+
if (!existingDoc) createdMetadata = true;
|
|
3097
|
+
} else {
|
|
3098
|
+
const baseSlug = slugifyTitle(title) || slugifyTitle(basename(filePath, extname(filePath)));
|
|
3099
|
+
if (!baseSlug) {
|
|
3100
|
+
throw new Error(
|
|
3101
|
+
"Could not derive a valid slug from the title or filename. Pass --doc <slug>.",
|
|
3102
|
+
);
|
|
3103
|
+
}
|
|
3104
|
+
const existingDoc = findDocBySlug(docs, baseSlug);
|
|
3105
|
+
if (wantsUpdate) {
|
|
3106
|
+
if (!existingDoc) {
|
|
3107
|
+
throw new Error(
|
|
3108
|
+
`--update requested, but derived document "${baseSlug}" does not exist. Pass --doc <existing-slug> or drop --update to create a new doc.`,
|
|
3109
|
+
);
|
|
3110
|
+
}
|
|
3111
|
+
slug = baseSlug;
|
|
3112
|
+
mode = docHasHtml(existingDoc) ? "updated" : "created";
|
|
3113
|
+
} else {
|
|
3114
|
+
slug = existingDoc ? nextAvailableSlug(baseSlug, docs) : baseSlug;
|
|
3115
|
+
mode = "created";
|
|
3116
|
+
createdMetadata = true;
|
|
3117
|
+
if (existingDoc) {
|
|
3118
|
+
process.stderr.write(
|
|
3119
|
+
`note: "${baseSlug}" already exists; publishing as new document "${slug}". Use --update or --doc ${baseSlug} to replace it.\n`,
|
|
3120
|
+
);
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
3124
|
+
|
|
3125
|
+
if (createdMetadata) {
|
|
3126
|
+
await request(
|
|
3127
|
+
"POST",
|
|
3128
|
+
`/api/deals/${encodeURIComponent(resolved.dealId)}/documents`,
|
|
3129
|
+
{ slug, title },
|
|
3130
|
+
);
|
|
3131
|
+
}
|
|
3132
|
+
|
|
3133
|
+
let assetsDir =
|
|
3134
|
+
typeof flags.assets === "string" && flags.assets.trim()
|
|
3135
|
+
? flags.assets.trim()
|
|
3136
|
+
: null;
|
|
3137
|
+
if (!assetsDir && !boolFlag(flags, "no-auto-assets")) {
|
|
3138
|
+
assetsDir = await detectSiblingAssetsDir(filePath);
|
|
3139
|
+
if (assetsDir) {
|
|
3140
|
+
process.stderr.write(`note: auto-detected asset folder ${assetsDir}\n`);
|
|
3141
|
+
}
|
|
3142
|
+
}
|
|
3143
|
+
const source =
|
|
3144
|
+
typeof flags.source === "string" && flags.source.trim()
|
|
3145
|
+
? flags.source.trim()
|
|
3146
|
+
: "cli";
|
|
3147
|
+
const uploadId = normalizeUploadId(flags["upload-id"]) || newHtmlUploadId();
|
|
3148
|
+
const uploaded = await uploadHtmlPayload({
|
|
3149
|
+
dealId: resolved.dealId,
|
|
3150
|
+
slug,
|
|
3151
|
+
html,
|
|
3152
|
+
source,
|
|
3153
|
+
assetsDir,
|
|
3154
|
+
uploadId,
|
|
3155
|
+
});
|
|
3156
|
+
const verification = boolFlag(flags, "no-verify")
|
|
3157
|
+
? { ok: false, skipped: true }
|
|
3158
|
+
: await verifyHtmlUpload({
|
|
3159
|
+
dealId: resolved.dealId,
|
|
3160
|
+
slug,
|
|
3161
|
+
expectedVersion: uploaded?.version,
|
|
3162
|
+
expectedBytes: uploaded?.bytes,
|
|
3163
|
+
expectedSha256: uploaded?.sha256,
|
|
3164
|
+
});
|
|
3165
|
+
|
|
3166
|
+
print({
|
|
3167
|
+
ok: true,
|
|
3168
|
+
mode,
|
|
3169
|
+
deal_uuid: resolved.dealId,
|
|
3170
|
+
resolved_from: resolved.resolvedFrom,
|
|
3171
|
+
deal_name: resolved.dealName,
|
|
3172
|
+
document_slug: slug,
|
|
3173
|
+
title,
|
|
3174
|
+
version: uploaded?.version,
|
|
3175
|
+
bytes: uploaded?.bytes ?? verification.bytes ?? htmlBytes,
|
|
3176
|
+
sha256: uploaded?.sha256 ?? verification.sha256,
|
|
3177
|
+
client_upload_id: uploaded?.client_upload_id ?? uploadId,
|
|
3178
|
+
idempotent_replay: uploaded?.idempotent_replay,
|
|
3179
|
+
asset_count: uploaded?.asset_count,
|
|
3180
|
+
asset_bytes: uploaded?.asset_bytes,
|
|
3181
|
+
verified: verification,
|
|
3182
|
+
viewer: `${getBaseUrl()}/deals/${encodeURIComponent(resolved.dealId)}/browse/${encodeURIComponent(slug)}`,
|
|
3183
|
+
});
|
|
3184
|
+
return;
|
|
3185
|
+
}
|
|
3186
|
+
|
|
2645
3187
|
// upload — PUT a new version. Reads HTML from --file or stdin. With
|
|
2646
3188
|
// --assets <dir>, walks the folder, packages as a multipart bundle,
|
|
2647
3189
|
// and the server stores HTML + per-asset BYTEA rows atomically
|
|
@@ -2676,7 +3218,7 @@ Routing — is this the right command?
|
|
|
2676
3218
|
}
|
|
2677
3219
|
const knownFlags = [
|
|
2678
3220
|
"doc", "slug", "new", "title",
|
|
2679
|
-
"file", "stdin", "assets", "source",
|
|
3221
|
+
"file", "stdin", "assets", "source", "upload-id",
|
|
2680
3222
|
];
|
|
2681
3223
|
const { flags } = parseFlags(rest.slice(1), knownFlags);
|
|
2682
3224
|
|
|
@@ -2845,12 +3387,16 @@ Routing — is this the right command?
|
|
|
2845
3387
|
typeof flags.source === "string" && flags.source.trim()
|
|
2846
3388
|
? flags.source.trim()
|
|
2847
3389
|
: "cli";
|
|
3390
|
+
const uploadId = normalizeUploadId(flags["upload-id"]) || newHtmlUploadId();
|
|
2848
3391
|
|
|
2849
3392
|
// No --assets → JSON path (small, faster).
|
|
2850
3393
|
if (!flags.assets) {
|
|
2851
3394
|
const data = await request("PUT", htmlEndpoint(dealId, slug), {
|
|
2852
3395
|
html,
|
|
2853
3396
|
source,
|
|
3397
|
+
client_upload_id: uploadId,
|
|
3398
|
+
}, {
|
|
3399
|
+
headers: { "X-Llama-Upload-Id": uploadId },
|
|
2854
3400
|
});
|
|
2855
3401
|
print({
|
|
2856
3402
|
ok: true,
|
|
@@ -2858,6 +3404,9 @@ Routing — is this the right command?
|
|
|
2858
3404
|
document_slug: slug,
|
|
2859
3405
|
version: data?.version,
|
|
2860
3406
|
bytes: data?.bytes ?? Buffer.byteLength(html, "utf8"),
|
|
3407
|
+
sha256: data?.sha256,
|
|
3408
|
+
client_upload_id: data?.client_upload_id ?? uploadId,
|
|
3409
|
+
idempotent_replay: data?.idempotent_replay,
|
|
2861
3410
|
deal_uuid: dealId,
|
|
2862
3411
|
viewer: `${getBaseUrl()}/deals/${encodeURIComponent(dealId)}/browse/${encodeURIComponent(slug)}`,
|
|
2863
3412
|
});
|
|
@@ -2937,6 +3486,7 @@ Routing — is this the right command?
|
|
|
2937
3486
|
const form = new FormData();
|
|
2938
3487
|
form.append("html", html);
|
|
2939
3488
|
form.append("source", source);
|
|
3489
|
+
form.append("client_upload_id", uploadId);
|
|
2940
3490
|
let totalBytes = 0;
|
|
2941
3491
|
for (const { absPath, relPath } of finalPaths) {
|
|
2942
3492
|
const buf = readFileSync(absPath);
|
|
@@ -2956,7 +3506,11 @@ Routing — is this the right command?
|
|
|
2956
3506
|
const headers = await getAuthHeaders();
|
|
2957
3507
|
const res = await fetch(`${getBaseUrl()}${htmlEndpoint(dealId, slug)}`, {
|
|
2958
3508
|
method: "PUT",
|
|
2959
|
-
headers: {
|
|
3509
|
+
headers: {
|
|
3510
|
+
...headers,
|
|
3511
|
+
"X-Llama-Upload-Id": uploadId,
|
|
3512
|
+
/* let fetch set the multipart boundary */
|
|
3513
|
+
},
|
|
2960
3514
|
body: form,
|
|
2961
3515
|
});
|
|
2962
3516
|
const body = await res.json().catch(() => ({}));
|
|
@@ -2970,6 +3524,10 @@ Routing — is this the right command?
|
|
|
2970
3524
|
mode,
|
|
2971
3525
|
document_slug: slug,
|
|
2972
3526
|
version: body.version,
|
|
3527
|
+
bytes: body.bytes,
|
|
3528
|
+
sha256: body.sha256,
|
|
3529
|
+
client_upload_id: body.client_upload_id ?? uploadId,
|
|
3530
|
+
idempotent_replay: body.idempotent_replay,
|
|
2973
3531
|
asset_count: body.asset_count,
|
|
2974
3532
|
asset_bytes: body.asset_bytes,
|
|
2975
3533
|
deal_uuid: dealId,
|
|
@@ -3044,7 +3602,7 @@ Routing — is this the right command?
|
|
|
3044
3602
|
}
|
|
3045
3603
|
|
|
3046
3604
|
throw new Error(
|
|
3047
|
-
`Unknown html subcommand "${sub || ""}". Use: docs / link / unlink / show / upload / versions / restore / reset.`,
|
|
3605
|
+
`Unknown html subcommand "${sub || ""}". Use: docs / link / unlink / show / publish / upload / versions / restore / reset.`,
|
|
3048
3606
|
);
|
|
3049
3607
|
}
|
|
3050
3608
|
|