slidemind 0.3.13 → 0.3.14
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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/AGENTS.md +86 -21
- package/README.md +14 -17
- package/bin/slidemind.mjs +65 -4969
- package/package.json +1 -1
- package/skills/create-slides/SKILL.md +113 -74
- package/skills/search/SKILL.md +25 -7
- package/src/bridge/build-and-push.mjs +108 -15
- package/src/bridge/bundle.mjs +69 -0
- package/src/bridge/capture.mjs +19 -0
- package/src/bridge/client/brands.mjs +622 -0
- package/src/bridge/client/engine-routing.mjs +237 -0
- package/src/bridge/client/engine-search.mjs +310 -0
- package/src/bridge/client/engine-slides.mjs +435 -0
- package/src/bridge/client/pane-ops.mjs +335 -0
- package/src/bridge/client/presentations.mjs +587 -0
- package/src/bridge/client/render.mjs +317 -0
- package/src/bridge/client/transport.mjs +695 -0
- package/src/bridge/client.mjs +80 -3452
- package/src/bridge/config.mjs +1 -1
- package/src/bridge/engine-download.mjs +15 -2
- package/src/builders/build.mjs +11 -804
- package/src/builders/draft-check.mjs +376 -0
- package/src/builders/lint.mjs +38 -2
- package/src/builders/preview.html +799 -0
- package/src/builders/review.mjs +23 -4
- package/src/builders/visual-families.mjs +6 -8
- package/src/cli/common.mjs +179 -0
- package/src/cli/concepts.mjs +310 -0
- package/src/cli/connection.mjs +147 -0
- package/src/cli/deck.mjs +608 -0
- package/src/cli/designs.mjs +113 -0
- package/src/cli/diagnose.mjs +141 -0
- package/src/cli/download-bundle.mjs +144 -0
- package/src/cli/engine-helpers.mjs +429 -0
- package/src/cli/engine-results.mjs +320 -0
- package/src/cli/help.mjs +201 -0
- package/src/cli/install.mjs +257 -0
- package/src/cli/read.mjs +120 -0
- package/src/cli/ready.mjs +805 -0
- package/src/cli/search.mjs +608 -0
- package/src/cli/session.mjs +177 -0
- package/src/cli/setup.mjs +282 -0
- package/src/cli/slide-reuse.mjs +334 -0
- package/src/cli/status.mjs +118 -0
- package/src/cli/workspace.mjs +302 -0
- package/src/installers/codex.mjs +1 -1
- package/src/update-check.mjs +231 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
// Server-side OOXML render
|
|
2
|
+
//
|
|
3
|
+
// Two protocols, newest first:
|
|
4
|
+
// 1. Render JOBS (preferred): POST creates a job, the CLI polls cheap status
|
|
5
|
+
// GETs and fetches the result when done. A full deck render routinely
|
|
6
|
+
// outlives the cloud ingress's ~240s request cap — only short polling
|
|
7
|
+
// requests survive that reliably, and they carry per-slide progress.
|
|
8
|
+
// 2. Sync render (fallback): one POST held open for the whole render. Kept
|
|
9
|
+
// for older backends that don't serve /api/cli/capabilities yet.
|
|
10
|
+
|
|
11
|
+
import { resolveBackendForPres } from "./presentations.mjs";
|
|
12
|
+
import { bridgeFetch, envInt, sleep, backendSupportsRenderJobs, assertCliVersionSupported } from "./transport.mjs";
|
|
13
|
+
|
|
14
|
+
// Rebuild a job failure into the exact error shape bridgeFetch throws for the
|
|
15
|
+
// sync route, so callers (etag self-heal, error texts) can't tell the
|
|
16
|
+
// protocols apart.
|
|
17
|
+
export function jobFailureError(jobError, label) {
|
|
18
|
+
const status = jobError?.status ?? 500;
|
|
19
|
+
const detail = jobError?.error ?? "render_failed";
|
|
20
|
+
const err = new Error(
|
|
21
|
+
`slidemind bridge ${status} render job failed for ${label}: ${detail}`,
|
|
22
|
+
);
|
|
23
|
+
err.status = status;
|
|
24
|
+
err.code = detail;
|
|
25
|
+
err.body = { error: detail, ...(jobError?.extra ?? {}) };
|
|
26
|
+
return err;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// The render/translate routes exist on two surfaces sharing one contract:
|
|
30
|
+
// /api/cli/presentations/:presId — brandId resolved from the pane registry
|
|
31
|
+
// /api/cli/brands/:brandId — pane-free (bound deck, window optional)
|
|
32
|
+
export function renderBasePath({ presId, brandId }) {
|
|
33
|
+
return brandId
|
|
34
|
+
? `/api/cli/brands/${encodeURIComponent(brandId)}`
|
|
35
|
+
: `/api/cli/presentations/${encodeURIComponent(presId)}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function renderViaSync({ basePath, body, backendURL }) {
|
|
39
|
+
const res = await bridgeFetch(
|
|
40
|
+
`${basePath}/render-pptx`,
|
|
41
|
+
{
|
|
42
|
+
method: "POST",
|
|
43
|
+
headers: { "content-type": "application/json" },
|
|
44
|
+
body: JSON.stringify(body),
|
|
45
|
+
// Server-side OOXML render is a heavy operation — a full deck of richly
|
|
46
|
+
// composed slides routinely runs well past the 35s default. Treat it like
|
|
47
|
+
// a mutation: give it a generous default and let env override it.
|
|
48
|
+
timeoutMs:
|
|
49
|
+
parseInt(process.env.SLIDEMIND_BRIDGE_RENDER_TIMEOUT_MS || "300000", 10) ||
|
|
50
|
+
300000,
|
|
51
|
+
},
|
|
52
|
+
backendURL,
|
|
53
|
+
);
|
|
54
|
+
return res.json(); // { ok, base64, slideCount, warnings, skippedSlides }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function renderViaJob({ basePath, label, body, backendURL, onProgress }) {
|
|
58
|
+
const pollMs = envInt("SLIDEMIND_BRIDGE_RENDER_POLL_MS", 2500);
|
|
59
|
+
const deadlineMs = envInt("SLIDEMIND_BRIDGE_RENDER_DEADLINE_MS", 900000);
|
|
60
|
+
const resultTimeoutMs = envInt(
|
|
61
|
+
"SLIDEMIND_BRIDGE_RENDER_RESULT_TIMEOUT_MS",
|
|
62
|
+
120000,
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const createJob = async () => {
|
|
66
|
+
// Deliberately NOT wrapped in withTransientRetry: its blanket 404 retry
|
|
67
|
+
// would re-upload a multi-MB slide payload, and creation errors (stale
|
|
68
|
+
// etag, no CD, unknown presId) are definitive.
|
|
69
|
+
const res = await bridgeFetch(
|
|
70
|
+
`${basePath}/render-jobs`,
|
|
71
|
+
{
|
|
72
|
+
method: "POST",
|
|
73
|
+
headers: { "content-type": "application/json" },
|
|
74
|
+
body: JSON.stringify(body),
|
|
75
|
+
timeoutMs:
|
|
76
|
+
parseInt(process.env.SLIDEMIND_BRIDGE_TIMEOUT_MS || "35000", 10) ||
|
|
77
|
+
35000,
|
|
78
|
+
},
|
|
79
|
+
backendURL,
|
|
80
|
+
);
|
|
81
|
+
const data = await res.json();
|
|
82
|
+
if (!data?.jobId) throw new Error("render-jobs returned no jobId");
|
|
83
|
+
return data.jobId;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
let jobId = await createJob();
|
|
87
|
+
let resubmitted = false;
|
|
88
|
+
let lastReported = "";
|
|
89
|
+
const deadline = Date.now() + deadlineMs;
|
|
90
|
+
|
|
91
|
+
for (;;) {
|
|
92
|
+
if (Date.now() > deadline) {
|
|
93
|
+
const err = new Error(
|
|
94
|
+
`slidemind render did not finish within ${deadlineMs}ms ` +
|
|
95
|
+
"(raise SLIDEMIND_BRIDGE_RENDER_DEADLINE_MS for very large decks). " +
|
|
96
|
+
"The render keeps running server-side — retrying may pick up a warm cache.",
|
|
97
|
+
);
|
|
98
|
+
err.code = "bridge_timeout";
|
|
99
|
+
throw err;
|
|
100
|
+
}
|
|
101
|
+
await sleep(pollMs);
|
|
102
|
+
|
|
103
|
+
let status;
|
|
104
|
+
try {
|
|
105
|
+
const res = await bridgeFetch(
|
|
106
|
+
`/api/cli/render-jobs/${encodeURIComponent(jobId)}`,
|
|
107
|
+
{ timeoutMs: 15000 },
|
|
108
|
+
backendURL,
|
|
109
|
+
);
|
|
110
|
+
status = await res.json();
|
|
111
|
+
} catch (err) {
|
|
112
|
+
// The job record vanished (backend Redis flush/restart) — same recovery
|
|
113
|
+
// as a lost replica: resubmit once.
|
|
114
|
+
if (err?.code === "job_not_found") {
|
|
115
|
+
if (resubmitted) throw jobFailureError({ status: 500, error: "job_lost" }, label);
|
|
116
|
+
resubmitted = true;
|
|
117
|
+
onProgress?.({ state: "restarted" });
|
|
118
|
+
jobId = await createJob();
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
// Rate-limited poll: honor the window, then resume.
|
|
122
|
+
if (err?.status === 429) {
|
|
123
|
+
const retryAfterSec = err?.body?.retryAfterSec;
|
|
124
|
+
await sleep(Math.min((retryAfterSec || 5) * 1000, 30000));
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
// Anything else (network blip, 5xx, replica restart) — polling is
|
|
128
|
+
// idempotent; just poll again until the deadline.
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (onProgress) {
|
|
133
|
+
const snapshot = JSON.stringify([status.state, status.progress]);
|
|
134
|
+
if (snapshot !== lastReported) {
|
|
135
|
+
lastReported = snapshot;
|
|
136
|
+
onProgress({ state: status.state, ...(status.progress ?? {}) });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (status.state === "failed") {
|
|
141
|
+
const jobError = status.error ?? { status: 500, error: "render_failed" };
|
|
142
|
+
// The rendering replica died (deploy, crash, scale-in). Renders are
|
|
143
|
+
// idempotent — resubmit once before giving up.
|
|
144
|
+
if (jobError.error === "job_lost" && !resubmitted) {
|
|
145
|
+
resubmitted = true;
|
|
146
|
+
onProgress?.({ state: "restarted" });
|
|
147
|
+
jobId = await createJob();
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
throw jobFailureError(jobError, label);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (status.state === "done") {
|
|
154
|
+
// The result may be tens of MB — generous timeout, and a couple of
|
|
155
|
+
// retries for transient blips (the stored result is re-fetchable).
|
|
156
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
157
|
+
try {
|
|
158
|
+
const res = await bridgeFetch(
|
|
159
|
+
`/api/cli/render-jobs/${encodeURIComponent(jobId)}/result`,
|
|
160
|
+
{ timeoutMs: resultTimeoutMs },
|
|
161
|
+
backendURL,
|
|
162
|
+
);
|
|
163
|
+
return res.json(); // { ok, base64, slideCount, warnings, skippedSlides }
|
|
164
|
+
} catch (err) {
|
|
165
|
+
if (
|
|
166
|
+
attempt >= 2 ||
|
|
167
|
+
err?.code === "result_expired" ||
|
|
168
|
+
err?.code === "job_not_found"
|
|
169
|
+
) {
|
|
170
|
+
throw err;
|
|
171
|
+
}
|
|
172
|
+
await sleep(pollMs);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Server-side OOXML render: ship authored slide JSX (+ deck-local assets) and
|
|
180
|
+
// get back a native .pptx (base64) rendered against the presentation's own CD.
|
|
181
|
+
// `slides`: { "<file>.jsx": "<jsx source>" }. `deckAssets`: { "assets/<f>": "<base64>" }.
|
|
182
|
+
// `onProgress` (optional) receives `{ state, done?, total?, stage? }` ticks on
|
|
183
|
+
// the job protocol; the sync fallback emits none.
|
|
184
|
+
//
|
|
185
|
+
// Target is EXACTLY ONE of:
|
|
186
|
+
// presId — render via the pane registry (any add-in version)
|
|
187
|
+
// brandId — render via the pane-free brand route (needs backendURL + a
|
|
188
|
+
// backend advertising `brandRoutes`); `deckName` labels the render.
|
|
189
|
+
export async function renderPptx({
|
|
190
|
+
presId = null,
|
|
191
|
+
brandId = null,
|
|
192
|
+
deckName = null,
|
|
193
|
+
slides,
|
|
194
|
+
deckAssets = null,
|
|
195
|
+
nativeSlides = null,
|
|
196
|
+
etag = null,
|
|
197
|
+
sourceGuids = null,
|
|
198
|
+
conversions = null,
|
|
199
|
+
reconcile = null,
|
|
200
|
+
backendURL = null,
|
|
201
|
+
onProgress = null,
|
|
202
|
+
}) {
|
|
203
|
+
if (!presId && !brandId) {
|
|
204
|
+
throw new Error("renderPptx: presId or brandId is required");
|
|
205
|
+
}
|
|
206
|
+
const hasSlides = slides && Object.keys(slides).length > 0;
|
|
207
|
+
const hasNative = nativeSlides && Object.keys(nativeSlides).length > 0;
|
|
208
|
+
if (!hasSlides && !hasNative) {
|
|
209
|
+
throw new Error("renderPptx: slides or nativeSlides is required");
|
|
210
|
+
}
|
|
211
|
+
let be = backendURL;
|
|
212
|
+
if (!be) {
|
|
213
|
+
if (brandId) throw new Error("renderPptx: backendURL is required with brandId");
|
|
214
|
+
be = await resolveBackendForPres(presId);
|
|
215
|
+
}
|
|
216
|
+
// Version floor first: shipping a whole deck to a backend that no longer
|
|
217
|
+
// speaks this CLI's contract fails deep and confusingly; fail here instead
|
|
218
|
+
// with the actionable `slidemind update`.
|
|
219
|
+
await assertCliVersionSupported(be);
|
|
220
|
+
const basePath = renderBasePath({ presId, brandId });
|
|
221
|
+
const label = brandId ? (deckName ?? brandId) : presId;
|
|
222
|
+
const body = { slides: slides ?? {} };
|
|
223
|
+
if (deckAssets && Object.keys(deckAssets).length > 0) {
|
|
224
|
+
body.deckAssets = deckAssets;
|
|
225
|
+
}
|
|
226
|
+
// Reused native one-slide .pptx (base64), keyed by play-order filename — the
|
|
227
|
+
// server splices them in by numeric-aware sorted filename (contract #7).
|
|
228
|
+
if (hasNative) body.nativeSlides = nativeSlides;
|
|
229
|
+
if (etag) body.etag = etag;
|
|
230
|
+
// Deck-embedded source: known slide GUIDs (jsx path → sm-<uuid>) so the
|
|
231
|
+
// server's tag bake keeps identities stable; minted ones ride back as
|
|
232
|
+
// `sourceGuids` on the response.
|
|
233
|
+
if (sourceGuids && Object.keys(sourceGuids).length > 0) {
|
|
234
|
+
body.sourceGuids = sourceGuids;
|
|
235
|
+
}
|
|
236
|
+
// Reverse-converted slides (`slidemind adopt`): identity + fidelity evidence
|
|
237
|
+
// per converted jsx file, so the server can gate the first replacement of a
|
|
238
|
+
// converted slide against what it originally converted, plus the verbatim
|
|
239
|
+
// source package whose preserved natives (chart / OLE / SmartArt) it splices
|
|
240
|
+
// back into the rendered slide. Keyed exactly like `slides`. Additive: a deck
|
|
241
|
+
// with no conversions sends the same body it always did.
|
|
242
|
+
if (conversions && Object.keys(conversions).length > 0) {
|
|
243
|
+
body.conversions = conversions;
|
|
244
|
+
}
|
|
245
|
+
// Manual-edit reconcile: for every slide the user edited in PowerPoint since
|
|
246
|
+
// the last build, the baked baseline projection and the current one, keyed
|
|
247
|
+
// exactly like `slides`. The server diffs them, folds text edits into the jsx
|
|
248
|
+
// it renders, and reports the rest — the CLI owns no diff source. Additive: a
|
|
249
|
+
// deck with no pending edits sends the same body it always did, and the
|
|
250
|
+
// response's `reconcile` echo is what proves the backend understood it.
|
|
251
|
+
if (reconcile && Object.keys(reconcile).length > 0) {
|
|
252
|
+
body.reconcile = reconcile;
|
|
253
|
+
}
|
|
254
|
+
// The brand surface has no presId to label the render with — pass the deck
|
|
255
|
+
// slug so job records/temp dirs stay readable (server-side sanitized).
|
|
256
|
+
if (brandId && deckName) body.deckName = deckName;
|
|
257
|
+
|
|
258
|
+
if (await backendSupportsRenderJobs(be)) {
|
|
259
|
+
try {
|
|
260
|
+
return await renderViaJob({ basePath, label, body, backendURL: be, onProgress });
|
|
261
|
+
} catch (err) {
|
|
262
|
+
// The backend speaks the protocol but its job store is down — the sync
|
|
263
|
+
// route renders in-process without Redis, so fall back to it.
|
|
264
|
+
if (err?.code !== "jobs_unavailable") throw err;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return renderViaSync({ basePath, body, backendURL: be });
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Translate a deck the CLI captured LOCALLY (the shipped capture engine) into a
|
|
271
|
+
// native .pptx. The slow Chrome work already ran client-side, so this is a fast
|
|
272
|
+
// single synchronous request — the server runs only the IP-sensitive boxes→OOXML
|
|
273
|
+
// half. Returns the same shape as renderPptx ({ ok, base64, slideCount, … }).
|
|
274
|
+
export async function translateDeck({
|
|
275
|
+
presId = null,
|
|
276
|
+
brandId = null,
|
|
277
|
+
deckName = null,
|
|
278
|
+
captured,
|
|
279
|
+
nativeSlides = null,
|
|
280
|
+
etag = null,
|
|
281
|
+
backendURL = null,
|
|
282
|
+
}) {
|
|
283
|
+
if (!presId && !brandId) {
|
|
284
|
+
throw new Error("translateDeck: presId or brandId is required");
|
|
285
|
+
}
|
|
286
|
+
if (!captured) throw new Error("translateDeck: captured is required");
|
|
287
|
+
let be = backendURL;
|
|
288
|
+
if (!be) {
|
|
289
|
+
if (brandId) throw new Error("translateDeck: backendURL is required with brandId");
|
|
290
|
+
be = await resolveBackendForPres(presId);
|
|
291
|
+
}
|
|
292
|
+
await assertCliVersionSupported(be);
|
|
293
|
+
const body = { captured };
|
|
294
|
+
// Reused native one-slide .pptx (base64), keyed by play-order filename — the
|
|
295
|
+
// server splices them in alongside the locally-captured JSX slides (contract
|
|
296
|
+
// #7). The slow capture already ran client-side; native slides are pass-through.
|
|
297
|
+
if (nativeSlides && Object.keys(nativeSlides).length > 0) {
|
|
298
|
+
body.nativeSlides = nativeSlides;
|
|
299
|
+
}
|
|
300
|
+
if (etag) body.etag = etag;
|
|
301
|
+
if (brandId && deckName) body.deckName = deckName;
|
|
302
|
+
const res = await bridgeFetch(
|
|
303
|
+
`${renderBasePath({ presId, brandId })}/translate`,
|
|
304
|
+
{
|
|
305
|
+
method: "POST",
|
|
306
|
+
headers: { "content-type": "application/json" },
|
|
307
|
+
body: JSON.stringify(body),
|
|
308
|
+
// The body carries inlined image bytes and the response a base64 .pptx —
|
|
309
|
+
// bandwidth, not server compute. Give it the render-grade timeout.
|
|
310
|
+
timeoutMs:
|
|
311
|
+
parseInt(process.env.SLIDEMIND_BRIDGE_RENDER_TIMEOUT_MS || "300000", 10) ||
|
|
312
|
+
300000,
|
|
313
|
+
},
|
|
314
|
+
be,
|
|
315
|
+
);
|
|
316
|
+
return res.json(); // { ok, base64, slideCount, warnings, skippedSlides }
|
|
317
|
+
}
|