@llamaventures/cli 1.4.4 → 1.5.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 +27 -7
- package/bin/llama.mjs +259 -14
- package/package.json +1 -1
package/AGENT_BRIEFING.md
CHANGED
|
@@ -33,7 +33,7 @@ Conversation produces value → that value flows somewhere. This is not optional
|
|
|
33
33
|
|---|---|---|
|
|
34
34
|
| Deal metadata (status, stage, valuation, founders, notes, etc.) | Pipeline (Postgres) | `llama deal create` / `llama deal update` |
|
|
35
35
|
| Brief blocks (text / link / embed / callout) | Pipeline | `llama brief add-text` / `add-link` / `add-callout` |
|
|
36
|
-
| **HTML artifact, internal — IC report, dashboard, market map, 2×2, any hand-authored page** | **Llama Command native** (Postgres + sandboxed iframe at `/deals/<id>/browse/<slug>`) |
|
|
36
|
+
| **HTML artifact, internal — IC report, dashboard, market map, 2×2, any hand-authored page** | **Llama Command native** (Postgres + sandboxed iframe at `/deals/<id>/browse/<slug>`) | Default path when the user says "deploy to llama", "deploy to llama command", "部署到 llama command", "put this HTML on the deal page", "在 deal 里看这个". **You MUST declare intent — "new artifact" vs "update existing":**<br><br>**New artifact:** `llama html upload <dealId> --new --title "<artifact name>" --file <path>` (CLI slugifies the title; pass `--doc <slug>` to override).<br>**Update existing:** `llama html upload <dealId> --doc <slug> --file <path>` (slug must already exist — run `llama html docs <dealId>` first to see what's there).<br><br>The bare form `llama html upload <id> --file <path>` REFUSES if `main` already has content. Do NOT default to Netlify for internal pages. |
|
|
37
37
|
| HTML artifact, external — founder-facing share link | Netlify | Only when the user explicitly says "share link", "give it to the founder", "publish publicly". Use the `netlify-access-guard` workflow (server-side password + edge 401 verification). |
|
|
38
38
|
| Insights, decisions, framework improvements | Wiki | `llama wiki save` (with attribution — see below) |
|
|
39
39
|
| Large files (deck / PDF / transcript) | Drive deal folder | the deal's `folder_url` (from `llama deal show`) → upload via your filesystem / Drive tool |
|
|
@@ -108,13 +108,33 @@ llama brief add-text <dealId> --heading "..." --body "..."
|
|
|
108
108
|
llama brief add-link <dealId> --url "..." --label "..."
|
|
109
109
|
llama brief add-callout <dealId> --tone insight|warning|info|success --heading "..." --body "..."
|
|
110
110
|
|
|
111
|
-
# Deal HTML — native deploy to /deals/<id>/browse/<slug>
|
|
111
|
+
# Deal HTML — native deploy to /deals/<id>/browse/<slug>
|
|
112
112
|
# Default path when user says "deploy to llama / 部署到 llama command / put this HTML on the deal page".
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
llama html
|
|
116
|
-
llama html
|
|
117
|
-
llama html
|
|
113
|
+
# Each deal can host many slug-scoped artifacts. ALWAYS declare intent: new vs update.
|
|
114
|
+
|
|
115
|
+
llama html docs <dealId> # list slugs currently on this deal
|
|
116
|
+
llama html docs create <dealId> <slug> [--title "..."] # pre-create a slot (optional; upload --new also creates)
|
|
117
|
+
llama html docs archive <dealId> <slug> # soft-archive a doc
|
|
118
|
+
|
|
119
|
+
# Add a NEW artifact (slug must NOT already exist):
|
|
120
|
+
llama html upload <dealId> --new --title "Consumer-Facing Thesis" --file ./thesis.html
|
|
121
|
+
llama html upload <dealId> --new --doc thesis --title "Consumer-Facing Thesis" --file ./thesis.html
|
|
122
|
+
|
|
123
|
+
# Update an EXISTING artifact (slug must already exist):
|
|
124
|
+
llama html upload <dealId> --doc <slug> --file ./report.html [--assets ./assets]
|
|
125
|
+
|
|
126
|
+
# Common helpers (all accept --doc <slug>; default 'main'):
|
|
127
|
+
llama html show <dealId> [--doc <slug>] [--out path] [--json] # current HTML → stdout
|
|
128
|
+
llama html versions <dealId> [--doc <slug>] # version history (incl. soft-deleted)
|
|
129
|
+
llama html restore <dealId> <version> [--doc <slug>] # promote old version to latest
|
|
130
|
+
llama html reset <dealId> [--doc <slug>] # soft-delete latest (browse reverts to empty)
|
|
131
|
+
|
|
132
|
+
# Safety contract (since 1.5.0):
|
|
133
|
+
# - Bare `llama html upload <id> --file X` REFUSES if 'main' already has content.
|
|
134
|
+
# The error names the existing artifact and suggests --doc main / --new --title "...".
|
|
135
|
+
# - --slug is silently accepted as an alias for --doc (agent-confusion mitigation).
|
|
136
|
+
# - Unknown flags print a warning to stderr suggesting a likely match.
|
|
137
|
+
# - JSON output gains `mode: 'created' | 'updated'` so callers can branch.
|
|
118
138
|
|
|
119
139
|
# Wiki (knowledge base)
|
|
120
140
|
llama wiki search "<query>"
|
package/bin/llama.mjs
CHANGED
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
import { LLAMA_CLI_CLIENT_ID, pkceLoopbackFlow, revokeToken as revokeOAuthToken } from "../lib/oauth-flow.mjs";
|
|
32
32
|
import { deleteBundle, detectBackend, readBundle, writeBundle } from "../lib/oauth-storage.mjs";
|
|
33
33
|
|
|
34
|
-
function parseFlags(args) {
|
|
34
|
+
function parseFlags(args, knownFlags = null) {
|
|
35
35
|
const flags = {};
|
|
36
36
|
const positional = [];
|
|
37
37
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -49,9 +49,82 @@ function parseFlags(args) {
|
|
|
49
49
|
positional.push(arg);
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
|
+
// Opt-in unknown-flag warning. Handlers that pass a `knownFlags` array
|
|
53
|
+
// get a stderr nudge when they see typos like `--slug` for `--doc`.
|
|
54
|
+
// Don't reject — agents wrap legacy options, breaking them silently is
|
|
55
|
+
// worse than a one-line warning.
|
|
56
|
+
if (Array.isArray(knownFlags)) {
|
|
57
|
+
const known = new Set(knownFlags);
|
|
58
|
+
for (const key of Object.keys(flags)) {
|
|
59
|
+
if (!known.has(key)) {
|
|
60
|
+
const suggestion = closestKnownFlag(key, knownFlags);
|
|
61
|
+
process.stderr.write(
|
|
62
|
+
suggestion
|
|
63
|
+
? `warning: unknown flag --${key} (did you mean --${suggestion}?)\n`
|
|
64
|
+
: `warning: unknown flag --${key}\n`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
52
69
|
return { flags, positional };
|
|
53
70
|
}
|
|
54
71
|
|
|
72
|
+
function closestKnownFlag(input, candidates) {
|
|
73
|
+
let best = null;
|
|
74
|
+
let bestScore = Infinity;
|
|
75
|
+
for (const c of candidates) {
|
|
76
|
+
const d = levenshtein(input, c);
|
|
77
|
+
const tolerance = Math.max(2, Math.floor(c.length / 3));
|
|
78
|
+
if (d < bestScore && d <= tolerance) {
|
|
79
|
+
best = c;
|
|
80
|
+
bestScore = d;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return best;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function levenshtein(a, b) {
|
|
87
|
+
if (a === b) return 0;
|
|
88
|
+
const m = a.length;
|
|
89
|
+
const n = b.length;
|
|
90
|
+
if (m === 0) return n;
|
|
91
|
+
if (n === 0) return m;
|
|
92
|
+
const dp = Array(n + 1).fill(0).map((_, i) => i);
|
|
93
|
+
for (let i = 1; i <= m; i++) {
|
|
94
|
+
let prev = dp[0];
|
|
95
|
+
dp[0] = i;
|
|
96
|
+
for (let j = 1; j <= n; j++) {
|
|
97
|
+
const tmp = dp[j];
|
|
98
|
+
dp[j] = a[i - 1] === b[j - 1]
|
|
99
|
+
? prev
|
|
100
|
+
: 1 + Math.min(prev, dp[j - 1], dp[j]);
|
|
101
|
+
prev = tmp;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return dp[n];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Slug shape used by deal_documents.slug (matches server-side SLUG_RE).
|
|
108
|
+
function isValidDocSlug(s) {
|
|
109
|
+
return typeof s === "string" && /^[a-z0-9][a-z0-9_-]{0,63}$/.test(s);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Best-effort title → slug. Strips diacritics, lowercases, collapses
|
|
113
|
+
// non-alnum to single hyphens, trims, caps at 64. Returns null if the
|
|
114
|
+
// result wouldn't pass `isValidDocSlug` (caller must then require --doc).
|
|
115
|
+
function slugifyTitle(title) {
|
|
116
|
+
if (typeof title !== "string") return null;
|
|
117
|
+
const slug = title
|
|
118
|
+
.toLowerCase()
|
|
119
|
+
.normalize("NFKD")
|
|
120
|
+
.replace(/[̀-ͯ]/g, "")
|
|
121
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
122
|
+
.replace(/^-+|-+$/g, "")
|
|
123
|
+
.slice(0, 64);
|
|
124
|
+
if (!slug || !/^[a-z0-9]/.test(slug)) return null;
|
|
125
|
+
return slug;
|
|
126
|
+
}
|
|
127
|
+
|
|
55
128
|
// Client-side fuzzy match — used as a fallback when the server hasn't yet
|
|
56
129
|
// shipped the search/filter API (Fix B, 2026-04-25). Once the server
|
|
57
130
|
// returns the `{deals,total,limit,offset}` envelope, this path is never
|
|
@@ -262,14 +335,30 @@ Memo (long-form HTML investment memo — Memo tab in the UI):
|
|
|
262
335
|
llama memo save <dealId> --file <path> # paste a hand-written HTML as manual override
|
|
263
336
|
llama memo reset <dealId> [--all] # default drops manual override; --all drops every version
|
|
264
337
|
|
|
265
|
-
Deal page HTML (hand-authored sandboxed
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
338
|
+
Deal page HTML (hand-authored sandboxed pages on /deals/<id>/browse/<slug>):
|
|
339
|
+
Each deal can host many HTML artifacts (IC report, dashboard, market map, …).
|
|
340
|
+
Each one has a stable slug. UPLOAD must declare intent — update an existing
|
|
341
|
+
artifact or add a new one — to avoid silent overwrites.
|
|
342
|
+
|
|
343
|
+
List existing artifacts:
|
|
344
|
+
llama html docs <dealId> # who-has-what
|
|
345
|
+
llama html docs create <dealId> <slug> [--title "..."] # pre-create a slot
|
|
346
|
+
llama html docs archive <dealId> <slug> # soft-archive (browse hides)
|
|
347
|
+
|
|
348
|
+
Update an EXISTING artifact (slug must exist):
|
|
349
|
+
llama html upload <dealId> --doc <slug> --file <path> [--assets DIR]
|
|
350
|
+
|
|
351
|
+
Add a NEW artifact (slug must NOT already exist):
|
|
352
|
+
llama html upload <dealId> --new --title "..." --file <path> [--doc <slug>] [--assets DIR]
|
|
353
|
+
(omit --doc → CLI slugifies the title; appends -2 / -3 on collision)
|
|
354
|
+
|
|
355
|
+
Default (no --doc, no --new) targets slug 'main' but REFUSES if 'main'
|
|
356
|
+
already has content — pass --doc main or --new --title "..." explicitly.
|
|
357
|
+
|
|
358
|
+
llama html show <dealId> [--doc <slug>] [--out <path>] [--json] # default: current html → stdout
|
|
359
|
+
llama html versions <dealId> [--doc <slug>] # list version history
|
|
360
|
+
llama html restore <dealId> <version> [--doc <slug>] # promote an old version to new latest
|
|
361
|
+
llama html reset <dealId> [--doc <slug>] # soft-delete latest; /browse reverts to empty
|
|
273
362
|
|
|
274
363
|
Caps: HTML 5 MB, each asset 50 MB, total bundle 100 MB. Every write
|
|
275
364
|
triggers SSE push — any browser viewing /deals/<id>/browse refreshes
|
|
@@ -1954,15 +2043,169 @@ https://command.llamaventures.vc/settings/tokens, run
|
|
|
1954
2043
|
const dealId = rest[0];
|
|
1955
2044
|
if (!dealId) {
|
|
1956
2045
|
throw new Error(
|
|
1957
|
-
"Usage
|
|
1958
|
-
"
|
|
2046
|
+
"Usage:\n" +
|
|
2047
|
+
" Update an existing artifact:\n" +
|
|
2048
|
+
" llama html upload <dealId> --doc <slug> --file PATH [--assets DIR]\n" +
|
|
2049
|
+
" Create a new artifact:\n" +
|
|
2050
|
+
" llama html upload <dealId> --new --title \"...\" --file PATH [--doc <slug>]\n" +
|
|
2051
|
+
" Stream from stdin (either form above with --stdin in place of --file PATH).\n" +
|
|
2052
|
+
"\n" +
|
|
2053
|
+
"Default (no --doc, no --new) targets slug 'main' but REFUSES if 'main'\n" +
|
|
2054
|
+
"already has content — pass --doc main to update it explicitly, or\n" +
|
|
2055
|
+
"--new --title \"...\" to add a NEW artifact alongside.",
|
|
1959
2056
|
);
|
|
1960
2057
|
}
|
|
1961
|
-
const
|
|
1962
|
-
|
|
2058
|
+
const knownFlags = [
|
|
2059
|
+
"doc", "slug", "new", "title",
|
|
2060
|
+
"file", "stdin", "assets", "source",
|
|
2061
|
+
];
|
|
2062
|
+
const { flags } = parseFlags(rest.slice(1), knownFlags);
|
|
2063
|
+
|
|
2064
|
+
// --slug is the natural agent guess (DB column is `document_slug`).
|
|
2065
|
+
// Accept it as an alias for --doc so the failure mode that bit
|
|
2066
|
+
// Gavin (silent fall-through to 'main') can't happen again.
|
|
2067
|
+
if (flags.slug && !flags.doc) {
|
|
2068
|
+
process.stderr.write("note: --slug accepted as alias for --doc.\n");
|
|
2069
|
+
flags.doc = flags.slug;
|
|
2070
|
+
} else if (flags.slug && flags.doc) {
|
|
2071
|
+
process.stderr.write("note: both --doc and --slug given; --doc wins.\n");
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
const isNew = Boolean(flags.new);
|
|
2075
|
+
const explicitDoc =
|
|
1963
2076
|
typeof flags.doc === "string" && flags.doc.trim()
|
|
1964
2077
|
? flags.doc.trim()
|
|
1965
|
-
:
|
|
2078
|
+
: null;
|
|
2079
|
+
const titleFlag =
|
|
2080
|
+
typeof flags.title === "string" && flags.title.trim()
|
|
2081
|
+
? flags.title.trim()
|
|
2082
|
+
: null;
|
|
2083
|
+
|
|
2084
|
+
// Pre-flight: ask the server what slugs already exist on this deal.
|
|
2085
|
+
// One extra GET round-trip — cheap insurance against silent overwrite.
|
|
2086
|
+
let existing = [];
|
|
2087
|
+
try {
|
|
2088
|
+
const docList = await request(
|
|
2089
|
+
"GET",
|
|
2090
|
+
`/api/deals/${encodeURIComponent(dealId)}/documents`,
|
|
2091
|
+
);
|
|
2092
|
+
existing = Array.isArray(docList?.documents) ? docList.documents : [];
|
|
2093
|
+
} catch (err) {
|
|
2094
|
+
// If the deal exists but the list endpoint somehow errors, we
|
|
2095
|
+
// shouldn't block the whole upload — surface the warning and
|
|
2096
|
+
// proceed in "no existing docs" mode. The server is still the
|
|
2097
|
+
// ultimate gate for permission failures.
|
|
2098
|
+
process.stderr.write(
|
|
2099
|
+
`warning: could not pre-check existing documents (${err.message}). Continuing.\n`,
|
|
2100
|
+
);
|
|
2101
|
+
}
|
|
2102
|
+
const findDoc = (s) =>
|
|
2103
|
+
existing.find((d) => d && d.slug === s) || null;
|
|
2104
|
+
const docHasHtml = (d) =>
|
|
2105
|
+
Boolean(d && (d.latest_version > 0 || d.latest_updated_at));
|
|
2106
|
+
|
|
2107
|
+
let slug;
|
|
2108
|
+
let mode; // 'created' | 'updated'
|
|
2109
|
+
|
|
2110
|
+
if (isNew) {
|
|
2111
|
+
// Create-new branch. Caller must provide --doc OR --title (we
|
|
2112
|
+
// derive the slug from the title in the latter case).
|
|
2113
|
+
let candidate = explicitDoc || (titleFlag ? slugifyTitle(titleFlag) : null);
|
|
2114
|
+
if (!candidate) {
|
|
2115
|
+
throw new Error(
|
|
2116
|
+
"--new requires --doc <slug> or --title \"...\" so the new artifact has a stable identifier.",
|
|
2117
|
+
);
|
|
2118
|
+
}
|
|
2119
|
+
if (!isValidDocSlug(candidate)) {
|
|
2120
|
+
throw new Error(
|
|
2121
|
+
`slug "${candidate}" must match /^[a-z0-9][a-z0-9_-]{0,63}$/`,
|
|
2122
|
+
);
|
|
2123
|
+
}
|
|
2124
|
+
if (findDoc(candidate)) {
|
|
2125
|
+
if (explicitDoc) {
|
|
2126
|
+
const existingDoc = findDoc(candidate);
|
|
2127
|
+
const meta = docHasHtml(existingDoc)
|
|
2128
|
+
? ` (currently at v${existingDoc.latest_version}, last update ${existingDoc.latest_updated_at})`
|
|
2129
|
+
: "";
|
|
2130
|
+
throw new Error(
|
|
2131
|
+
`--new --doc ${candidate} but a document with slug "${candidate}" already exists${meta}.\n` +
|
|
2132
|
+
`Pick a different slug, or drop --new to UPDATE the existing one.`,
|
|
2133
|
+
);
|
|
2134
|
+
}
|
|
2135
|
+
// Auto-resolve title collisions: foo -> foo-2 -> foo-3 -> ...
|
|
2136
|
+
let suffix = 2;
|
|
2137
|
+
while (findDoc(`${candidate}-${suffix}`)) suffix++;
|
|
2138
|
+
const oldCandidate = candidate;
|
|
2139
|
+
candidate = `${candidate}-${suffix}`;
|
|
2140
|
+
process.stderr.write(
|
|
2141
|
+
`note: slug "${oldCandidate}" already in use; using "${candidate}" instead.\n`,
|
|
2142
|
+
);
|
|
2143
|
+
}
|
|
2144
|
+
slug = candidate;
|
|
2145
|
+
mode = "created";
|
|
2146
|
+
// Stamp the doc metadata first (title, etc.) so the UI selection
|
|
2147
|
+
// page shows a nice name. PUT auto-creates the row too, but
|
|
2148
|
+
// POST gives us a chance to set --title.
|
|
2149
|
+
await request(
|
|
2150
|
+
"POST",
|
|
2151
|
+
`/api/deals/${encodeURIComponent(dealId)}/documents`,
|
|
2152
|
+
{ slug, title: titleFlag || slug },
|
|
2153
|
+
);
|
|
2154
|
+
} else if (explicitDoc) {
|
|
2155
|
+
// Update an existing slug.
|
|
2156
|
+
if (!isValidDocSlug(explicitDoc)) {
|
|
2157
|
+
throw new Error(
|
|
2158
|
+
`slug "${explicitDoc}" must match /^[a-z0-9][a-z0-9_-]{0,63}$/`,
|
|
2159
|
+
);
|
|
2160
|
+
}
|
|
2161
|
+
const target = findDoc(explicitDoc);
|
|
2162
|
+
if (!target) {
|
|
2163
|
+
if (explicitDoc === "main") {
|
|
2164
|
+
// 'main' is the legacy default — fine to auto-init on first
|
|
2165
|
+
// upload to an empty deal.
|
|
2166
|
+
slug = "main";
|
|
2167
|
+
mode = "created";
|
|
2168
|
+
} else {
|
|
2169
|
+
const slugList = existing.length
|
|
2170
|
+
? existing.map((d) => d.slug).join(", ")
|
|
2171
|
+
: "(none)";
|
|
2172
|
+
throw new Error(
|
|
2173
|
+
`No document with slug "${explicitDoc}" exists on this deal.\n` +
|
|
2174
|
+
`To create it: add --new --title "..."\n` +
|
|
2175
|
+
`Or pre-create: llama html docs create ${dealId} ${explicitDoc} --title "..."\n` +
|
|
2176
|
+
`Existing slugs: ${slugList}`,
|
|
2177
|
+
);
|
|
2178
|
+
}
|
|
2179
|
+
} else {
|
|
2180
|
+
slug = explicitDoc;
|
|
2181
|
+
mode = docHasHtml(target) ? "updated" : "created";
|
|
2182
|
+
}
|
|
2183
|
+
} else {
|
|
2184
|
+
// Bare upload — no --doc, no --new. Safe-default to 'main' only
|
|
2185
|
+
// if 'main' is empty / absent. Otherwise refuse, naming the
|
|
2186
|
+
// existing artifact so the caller can pick an explicit intent.
|
|
2187
|
+
const main = findDoc("main");
|
|
2188
|
+
if (docHasHtml(main)) {
|
|
2189
|
+
const versionInfo = main.latest_version
|
|
2190
|
+
? ` (v${main.latest_version}, ${main.latest_updated_at || "last update unknown"})`
|
|
2191
|
+
: "";
|
|
2192
|
+
const slugList = existing.length
|
|
2193
|
+
? existing.map((d) => d.slug).join(", ")
|
|
2194
|
+
: "main";
|
|
2195
|
+
throw new Error(
|
|
2196
|
+
`Refusing to silently overwrite the existing 'main' artifact${versionInfo}.\n` +
|
|
2197
|
+
`\n` +
|
|
2198
|
+
`If you meant to UPDATE 'main': --doc main\n` +
|
|
2199
|
+
`If you meant to add a NEW artifact: --new --title "<name>"\n` +
|
|
2200
|
+
`\n` +
|
|
2201
|
+
`Existing slugs on this deal: ${slugList}\n` +
|
|
2202
|
+
`List details: llama html docs ${dealId}`,
|
|
2203
|
+
);
|
|
2204
|
+
}
|
|
2205
|
+
slug = "main";
|
|
2206
|
+
mode = main ? "updated" : "created";
|
|
2207
|
+
}
|
|
2208
|
+
|
|
1966
2209
|
let html;
|
|
1967
2210
|
if (flags.file) {
|
|
1968
2211
|
const { readFileSync } = await import("fs");
|
|
@@ -1992,6 +2235,7 @@ https://command.llamaventures.vc/settings/tokens, run
|
|
|
1992
2235
|
});
|
|
1993
2236
|
print({
|
|
1994
2237
|
ok: true,
|
|
2238
|
+
mode,
|
|
1995
2239
|
document_slug: slug,
|
|
1996
2240
|
version: data?.version,
|
|
1997
2241
|
bytes: data?.bytes ?? Buffer.byteLength(html, "utf8"),
|
|
@@ -2104,6 +2348,7 @@ https://command.llamaventures.vc/settings/tokens, run
|
|
|
2104
2348
|
}
|
|
2105
2349
|
print({
|
|
2106
2350
|
ok: true,
|
|
2351
|
+
mode,
|
|
2107
2352
|
document_slug: slug,
|
|
2108
2353
|
version: body.version,
|
|
2109
2354
|
asset_count: body.asset_count,
|