slidemind 0.1.6 → 0.1.8
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 -7
- package/CLAUDE.md +21 -15
- package/README.md +21 -10
- package/bin/slidemind.mjs +836 -35
- package/package.json +2 -5
- package/skills/create-slides/SKILL.md +85 -60
- package/skills/search/SKILL.md +27 -27
- package/src/bridge/build-and-push.mjs +166 -55
- package/src/bridge/bundle.mjs +49 -7
- package/src/bridge/capture.mjs +2 -2
- package/src/bridge/client.mjs +374 -44
- package/src/bridge/engine-download.mjs +89 -0
- package/src/{mcp → bridge}/recolor.mjs +10 -9
- package/src/builders/build.mjs +261 -23
- package/src/builders/gate-server.mjs +223 -0
- package/src/builders/lint.mjs +2 -2
- package/src/builders/plan-md.mjs +216 -0
- package/src/builders/plan-overlay.js +747 -0
- package/src/builders/plan.mjs +575 -0
- package/src/builders/resolve-skill.mjs +40 -5
- package/src/builders/review-overlay.js +172 -16
- package/src/builders/review.mjs +139 -80
- package/src/installers/claude.mjs +10 -17
- package/src/installers/codex.mjs +14 -77
- package/src/installers/opencode.mjs +10 -18
- package/src/lib/reused-slides.mjs +2 -4
- package/src/mcp/server.mjs +0 -958
|
@@ -6,11 +6,13 @@
|
|
|
6
6
|
// `review` gate.
|
|
7
7
|
//
|
|
8
8
|
// The CLI ships ZERO exporter code: the authored slide JSX + deck-local assets
|
|
9
|
-
// are POSTed to the add-in's
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
9
|
+
// are POSTed to the add-in's render routes, which render them against the
|
|
10
|
+
// deck's corporate design using the SAME OOXML exporter the in-app design
|
|
11
|
+
// persona uses. The returned base64 .pptx is then inserted / replaced (push)
|
|
12
|
+
// or written to disk (build-pptx). A BOUND deck renders pane-free over the
|
|
13
|
+
// brand routes (PowerPoint backgrounded or closed); only the INSERT half of a
|
|
14
|
+
// push, and unbound decks (whose brandId must come from the pane registry),
|
|
15
|
+
// still need a registered PowerPoint window.
|
|
14
16
|
|
|
15
17
|
import fs from "node:fs";
|
|
16
18
|
import path from "node:path";
|
|
@@ -21,6 +23,7 @@ import {
|
|
|
21
23
|
pickPresentationEntry,
|
|
22
24
|
renderPptx,
|
|
23
25
|
replaceSlides,
|
|
26
|
+
requireHost,
|
|
24
27
|
requireRenderHost,
|
|
25
28
|
translateDeck,
|
|
26
29
|
waitForPaneReady,
|
|
@@ -93,8 +96,12 @@ function gatherDeckAssets(deckDir) {
|
|
|
93
96
|
// ref rewritten, and the render retried once against the current design.
|
|
94
97
|
export async function renderDeckToBase64(
|
|
95
98
|
deckDir,
|
|
96
|
-
{ presId, backendURL = null, onProgress = null },
|
|
99
|
+
{ presId = null, brandId = null, backendURL = null, onProgress = null },
|
|
97
100
|
) {
|
|
101
|
+
if (!presId && !brandId) {
|
|
102
|
+
throw new Error("renderDeckToBase64: presId or brandId is required");
|
|
103
|
+
}
|
|
104
|
+
const deckName = path.basename(deckDir);
|
|
98
105
|
const { slides, nativeSlides, orderWarning } = gatherSlides(deckDir);
|
|
99
106
|
const hasJsx = Object.keys(slides).length > 0;
|
|
100
107
|
const deckAssets = gatherDeckAssets(deckDir);
|
|
@@ -103,9 +110,10 @@ export async function renderDeckToBase64(
|
|
|
103
110
|
// silently no-ops), the local HTML/editor preview can't resolve the deck's
|
|
104
111
|
// design files, AND local capture has no engine/bundle to read. `download-
|
|
105
112
|
// bundle <deck>` normally writes this; here we self-heal so `push`/`build-pptx`
|
|
106
|
-
// work even when it wasn't run explicitly.
|
|
113
|
+
// work even when it wasn't run explicitly. (The brand path implies a bound
|
|
114
|
+
// deck — its brandId came from the ref — so this only fires with a presId.)
|
|
107
115
|
let ref = readDeckBundleRef(deckDir);
|
|
108
|
-
if (!ref?.etag) {
|
|
116
|
+
if (!ref?.etag && presId) {
|
|
109
117
|
try {
|
|
110
118
|
const mat = await materializeBundle(presId, { backendURL });
|
|
111
119
|
writeDeckBundleRef(deckDir, { presFP: mat.presFP, etag: mat.etag });
|
|
@@ -129,6 +137,7 @@ export async function renderDeckToBase64(
|
|
|
129
137
|
// .pptx slides ride alongside on either path so the server splices them by
|
|
130
138
|
// play-order filename.
|
|
131
139
|
const attemptRender = async (etag) => {
|
|
140
|
+
const target = { presId, brandId, deckName, backendURL };
|
|
132
141
|
if (useLocalCapture) {
|
|
133
142
|
let captured;
|
|
134
143
|
try {
|
|
@@ -139,13 +148,13 @@ export async function renderDeckToBase64(
|
|
|
139
148
|
"falling back to the server render\n",
|
|
140
149
|
);
|
|
141
150
|
useLocalCapture = false;
|
|
142
|
-
return renderPptx({
|
|
151
|
+
return renderPptx({ ...target, slides, deckAssets, nativeSlides, etag, onProgress });
|
|
143
152
|
}
|
|
144
153
|
const n = Array.isArray(captured?.slides) ? captured.slides.length : 0;
|
|
145
154
|
onProgress?.({ done: n, total: n, stage: "assembling" });
|
|
146
|
-
return translateDeck({
|
|
155
|
+
return translateDeck({ ...target, captured, nativeSlides, etag });
|
|
147
156
|
}
|
|
148
|
-
return renderPptx({
|
|
157
|
+
return renderPptx({ ...target, slides, deckAssets, nativeSlides, etag, onProgress });
|
|
149
158
|
};
|
|
150
159
|
|
|
151
160
|
let result;
|
|
@@ -157,15 +166,18 @@ export async function renderDeckToBase64(
|
|
|
157
166
|
// design truth lives in the add-in, so the right move is forward: refresh
|
|
158
167
|
// the local bundle (so previews/lint/capture-engine match), re-bind the
|
|
159
168
|
// deck, and render once against the CURRENT design. A second stale (CD
|
|
160
|
-
// changed again mid-flight) propagates.
|
|
169
|
+
// changed again mid-flight) propagates. The refresh rides the SAME target
|
|
170
|
+
// the render used — the brand route stays pane-free end to end.
|
|
161
171
|
process.stderr.write(
|
|
162
172
|
"corporate design changed since this deck was authored — refreshing the " +
|
|
163
173
|
"bundle and retrying the render against the current design…\n",
|
|
164
174
|
);
|
|
165
|
-
const mat =
|
|
166
|
-
backendURL,
|
|
167
|
-
|
|
168
|
-
|
|
175
|
+
const mat = brandId
|
|
176
|
+
? await materializeBundle(null, { backendURL, brandId })
|
|
177
|
+
: await materializeBundle(presId, {
|
|
178
|
+
backendURL,
|
|
179
|
+
preferBrandId: ref?.presFP ?? null,
|
|
180
|
+
});
|
|
169
181
|
writeDeckBundleRef(deckDir, { presFP: mat.presFP, etag: mat.etag });
|
|
170
182
|
// The bundle dir is keyed by presFP, so a refreshed capture engine landed
|
|
171
183
|
// in place — re-evaluate in case it appeared (or vanished) on this sync.
|
|
@@ -327,6 +339,45 @@ export async function viewDraft(
|
|
|
327
339
|
return { slides, warnings };
|
|
328
340
|
}
|
|
329
341
|
|
|
342
|
+
// A brand-route miss is definitive for the pane-free path but NOT for the
|
|
343
|
+
// deck: old add-in (no brandRoutes), legacy non-brand presFP in the ref,
|
|
344
|
+
// grant/tenant miss, or a brand with no bundle — every one of these still
|
|
345
|
+
// works through the pane flow, which needs no grant and re-binds the ref.
|
|
346
|
+
function isBrandRouteMiss(err) {
|
|
347
|
+
return (
|
|
348
|
+
err?.code === "brand_route_unavailable" ||
|
|
349
|
+
err?.code === "unknown_brand" ||
|
|
350
|
+
err?.code === "no_active_cd" ||
|
|
351
|
+
err?.status === 404
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Pane-free render for a BOUND deck: sync the bundle over the brand route
|
|
356
|
+
// (proves the backend + grant and refreshes the capture engine/etag exactly
|
|
357
|
+
// like `ready`), re-bind, then render by brandId — no PowerPoint window
|
|
358
|
+
// involved. Returns null when the brand route can't serve this deck (caller
|
|
359
|
+
// falls back to the pane flow); render failures (lint, stale-after-retry)
|
|
360
|
+
// propagate, they are real.
|
|
361
|
+
async function tryDetachedRender(deckDir, deckRef, onProgress) {
|
|
362
|
+
const brandId = deckRef.presFP;
|
|
363
|
+
let backendURL;
|
|
364
|
+
try {
|
|
365
|
+
const mat = await materializeBundle(null, { brandId });
|
|
366
|
+
writeDeckBundleRef(deckDir, { presFP: mat.presFP, etag: mat.etag });
|
|
367
|
+
backendURL = mat.backendURL ?? null;
|
|
368
|
+
if (!backendURL) return null;
|
|
369
|
+
} catch (err) {
|
|
370
|
+
if (isBrandRouteMiss(err)) return null;
|
|
371
|
+
throw err;
|
|
372
|
+
}
|
|
373
|
+
const result = await renderDeckToBase64(deckDir, {
|
|
374
|
+
brandId,
|
|
375
|
+
backendURL,
|
|
376
|
+
onProgress,
|
|
377
|
+
});
|
|
378
|
+
return { result, backendURL };
|
|
379
|
+
}
|
|
380
|
+
|
|
330
381
|
async function renderWithRetarget(deckDir, entry, onProgress) {
|
|
331
382
|
const run = (target) =>
|
|
332
383
|
renderDeckToBase64(deckDir, {
|
|
@@ -379,20 +430,66 @@ export async function buildAndPush({
|
|
|
379
430
|
}
|
|
380
431
|
|
|
381
432
|
const deckRef = readDeckBundleRef(deckDir);
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
433
|
+
|
|
434
|
+
// --- Render half. Pane-free for a bound deck (the window may be closed or
|
|
435
|
+
// backgrounded during the render); the pane matters only for the insert. ---
|
|
436
|
+
let result = null;
|
|
437
|
+
let renderEntry = null;
|
|
438
|
+
if (!presentationId && deckRef?.presFP) {
|
|
439
|
+
const detached = await tryDetachedRender(deckDir, deckRef, onProgress);
|
|
440
|
+
if (detached) result = detached.result;
|
|
441
|
+
}
|
|
442
|
+
if (!result) {
|
|
443
|
+
const registry = await requireRenderHost();
|
|
444
|
+
const entry = pickPresentationEntry(registry, presentationId, {
|
|
445
|
+
preferBrandId: deckRef?.presFP ?? null,
|
|
446
|
+
});
|
|
447
|
+
const rendered = await renderWithRetarget(deckDir, entry, onProgress);
|
|
448
|
+
renderEntry = rendered.entry;
|
|
449
|
+
result = rendered.result;
|
|
450
|
+
}
|
|
451
|
+
|
|
391
452
|
const base64 = result.base64;
|
|
392
453
|
const bytes = Buffer.byteLength(base64, "base64");
|
|
393
454
|
const mode = replace ? "replace" : "insert";
|
|
394
455
|
const operationId = `push-${randomUUID()}`;
|
|
395
456
|
|
|
457
|
+
// The render survived without a window; don't lose it when the insert can't
|
|
458
|
+
// find one — save the .pptx next to the deck and tell the caller (bin prints
|
|
459
|
+
// the recovery instructions).
|
|
460
|
+
const saveFallback = (presIdForResult) => {
|
|
461
|
+
const fallbackFile = path.join(deckDir, `${path.basename(deckDir)}.pptx`);
|
|
462
|
+
fs.writeFileSync(fallbackFile, Buffer.from(base64, "base64"));
|
|
463
|
+
return {
|
|
464
|
+
presId: presIdForResult,
|
|
465
|
+
mode,
|
|
466
|
+
bytes,
|
|
467
|
+
operationId,
|
|
468
|
+
response: null,
|
|
469
|
+
warnings: result.warnings ?? [],
|
|
470
|
+
skippedSlides: result.skippedSlides ?? [],
|
|
471
|
+
paneSuspended: true,
|
|
472
|
+
fallbackFile,
|
|
473
|
+
};
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
// --- Insert half: needs a registered window. On the pane-free render path
|
|
477
|
+
// none was resolved yet; if none exists at all, the deck is saved instead of
|
|
478
|
+
// thrown away. ---
|
|
479
|
+
if (!renderEntry) {
|
|
480
|
+
let registry;
|
|
481
|
+
try {
|
|
482
|
+
registry = await requireHost();
|
|
483
|
+
} catch {
|
|
484
|
+
return saveFallback(null);
|
|
485
|
+
}
|
|
486
|
+
renderEntry = pickPresentationEntry(registry, presentationId, {
|
|
487
|
+
preferBrandId: deckRef?.presFP ?? null,
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
const presId = renderEntry.presId;
|
|
491
|
+
const backendURL = renderEntry.backendURL ?? null;
|
|
492
|
+
|
|
396
493
|
// The SAME operationId on every attempt: the pane keeps an LRU of applied
|
|
397
494
|
// operationIds and drops duplicates, so a retry after an ambiguous failure
|
|
398
495
|
// can never double-insert.
|
|
@@ -418,16 +515,25 @@ export async function buildAndPush({
|
|
|
418
515
|
try {
|
|
419
516
|
resp = await doPush(presId, backendURL);
|
|
420
517
|
} catch (err) {
|
|
421
|
-
//
|
|
518
|
+
// Three recoverable shapes:
|
|
422
519
|
// - pane_suspended (503): the server waited ~25s for a reconnect and gave
|
|
423
|
-
// up — the pane needs the user (foreground
|
|
520
|
+
// up — nothing was delivered; the pane needs the user (foreground
|
|
521
|
+
// PowerPoint / click the pane).
|
|
522
|
+
// - pane_timeout (504): the pane looked connected but never acked within
|
|
523
|
+
// the server's 60s window (just-frozen webview). Delivery is ambiguous,
|
|
524
|
+
// but the retry below re-sends the SAME operationId, so the pane-side
|
|
525
|
+
// dedup LRU makes it safe either way. The server marked the pane
|
|
526
|
+
// suspended on the way out, so the same client-side wake wait applies.
|
|
424
527
|
// - 404: the presId vanished from the registry — a pane reconnect mints a
|
|
425
528
|
// NEW presId for the same window (presId churn).
|
|
426
529
|
// Wait client-side for the pane (rematching by fileName+brandId covers the
|
|
427
|
-
// churn), then retry ONCE. A `bridge_timeout` is NOT caught:
|
|
428
|
-
//
|
|
429
|
-
//
|
|
430
|
-
const recoverable =
|
|
530
|
+
// churn), then retry ONCE. A client-side `bridge_timeout` is NOT caught:
|
|
531
|
+
// there the server-side outcome is entirely unknown (the request may still
|
|
532
|
+
// be executing), so a re-send is deliberately left to the user.
|
|
533
|
+
const recoverable =
|
|
534
|
+
err?.code === "pane_suspended" ||
|
|
535
|
+
err?.code === "pane_timeout" ||
|
|
536
|
+
err?.status === 404;
|
|
431
537
|
if (!recoverable) throw err;
|
|
432
538
|
const live = await waitForPaneReady(presId, {
|
|
433
539
|
backendURL,
|
|
@@ -440,25 +546,11 @@ export async function buildAndPush({
|
|
|
440
546
|
),
|
|
441
547
|
});
|
|
442
548
|
if (!live) {
|
|
443
|
-
if (err?.code !== "pane_suspended"
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
`${path.basename(deckDir)}.pptx`,
|
|
449
|
-
);
|
|
450
|
-
fs.writeFileSync(fallbackFile, Buffer.from(base64, "base64"));
|
|
451
|
-
return {
|
|
452
|
-
presId,
|
|
453
|
-
mode,
|
|
454
|
-
bytes,
|
|
455
|
-
operationId,
|
|
456
|
-
response: null,
|
|
457
|
-
warnings: result.warnings ?? [],
|
|
458
|
-
skippedSlides: result.skippedSlides ?? [],
|
|
459
|
-
paneSuspended: true,
|
|
460
|
-
fallbackFile,
|
|
461
|
-
};
|
|
549
|
+
if (err?.code !== "pane_suspended" && err?.code !== "pane_timeout") {
|
|
550
|
+
throw err;
|
|
551
|
+
}
|
|
552
|
+
// The pane never woke up — same recovery as no window at all.
|
|
553
|
+
return saveFallback(presId);
|
|
462
554
|
}
|
|
463
555
|
livePresId = live.presId;
|
|
464
556
|
resp = await doPush(live.presId, live.backendURL ?? backendURL);
|
|
@@ -474,9 +566,11 @@ export async function buildAndPush({
|
|
|
474
566
|
};
|
|
475
567
|
}
|
|
476
568
|
|
|
477
|
-
// Render a deck to a .pptx file (the `slidemind build-pptx` path).
|
|
478
|
-
//
|
|
479
|
-
//
|
|
569
|
+
// Render a deck to a .pptx file (the `slidemind build-pptx` path). For a BOUND
|
|
570
|
+
// deck against a backend serving brand routes this is fully pane-free — no
|
|
571
|
+
// PowerPoint window needed at all (backgrounded or closed). Otherwise it needs
|
|
572
|
+
// an open host because the brandId must come from the pane registry — but
|
|
573
|
+
// still not an awake pane: the render runs in the add-in's cloud backend.
|
|
480
574
|
export async function buildPptxFile({
|
|
481
575
|
deckDir,
|
|
482
576
|
presentationId = null,
|
|
@@ -485,6 +579,25 @@ export async function buildPptxFile({
|
|
|
485
579
|
}) {
|
|
486
580
|
if (!deckDir) throw new Error("buildPptxFile: deckDir is required");
|
|
487
581
|
const deckRef = readDeckBundleRef(deckDir);
|
|
582
|
+
const out = outPath || path.join(deckDir, `${path.basename(deckDir)}.pptx`);
|
|
583
|
+
|
|
584
|
+
// Pane-free path. `--to <presId>` names a window explicitly and therefore
|
|
585
|
+
// keeps the registry path.
|
|
586
|
+
if (!presentationId && deckRef?.presFP) {
|
|
587
|
+
const detached = await tryDetachedRender(deckDir, deckRef, onProgress);
|
|
588
|
+
if (detached) {
|
|
589
|
+
fs.writeFileSync(out, Buffer.from(detached.result.base64, "base64"));
|
|
590
|
+
return {
|
|
591
|
+
outPath: out,
|
|
592
|
+
presId: null,
|
|
593
|
+
windowless: true,
|
|
594
|
+
slideCount: detached.result.slideCount ?? null,
|
|
595
|
+
warnings: detached.result.warnings ?? [],
|
|
596
|
+
skippedSlides: detached.result.skippedSlides ?? [],
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
488
601
|
const registry = await requireRenderHost();
|
|
489
602
|
const entry = pickPresentationEntry(registry, presentationId, {
|
|
490
603
|
preferBrandId: deckRef?.presFP ?? null,
|
|
@@ -495,8 +608,6 @@ export async function buildPptxFile({
|
|
|
495
608
|
onProgress,
|
|
496
609
|
);
|
|
497
610
|
const presId = renderEntry.presId;
|
|
498
|
-
const out =
|
|
499
|
-
outPath || path.join(deckDir, `${path.basename(deckDir)}.pptx`);
|
|
500
611
|
fs.writeFileSync(out, Buffer.from(result.base64, "base64"));
|
|
501
612
|
return {
|
|
502
613
|
outPath: out,
|
package/src/bridge/bundle.mjs
CHANGED
|
@@ -45,7 +45,11 @@ import path from "node:path";
|
|
|
45
45
|
import http from "node:http";
|
|
46
46
|
import https from "node:https";
|
|
47
47
|
|
|
48
|
-
import {
|
|
48
|
+
import {
|
|
49
|
+
getDesignBundle,
|
|
50
|
+
getDesignBundleForBrand,
|
|
51
|
+
getDesignBundleForBrandAnyBackend,
|
|
52
|
+
} from "./client.mjs";
|
|
49
53
|
|
|
50
54
|
// Highest bundle wire-contract version this CLI understands. The add-in stamps
|
|
51
55
|
// `schemaVersion` on every bundle; a higher value means the CLI is too old.
|
|
@@ -65,6 +69,21 @@ export function bundleDirForPresFP(presFP) {
|
|
|
65
69
|
return path.join(bundleCacheRoot(), sanitizeKey(presFP));
|
|
66
70
|
}
|
|
67
71
|
|
|
72
|
+
// Zero-GET lookup of a materialized bundle's meta by presFP: reads the on-disk
|
|
73
|
+
// bundle.json materializeBundle wrote and returns the parsed record, or null
|
|
74
|
+
// when that CD was never materialized here / the file is unreadable. `new`'s
|
|
75
|
+
// stateless auto-bind uses this to confirm a resolved design's bundle already
|
|
76
|
+
// exists locally BEFORE writing a deck ref — no network, no throw.
|
|
77
|
+
export function readMaterializedBundleMeta(presFP) {
|
|
78
|
+
if (!presFP) return null;
|
|
79
|
+
try {
|
|
80
|
+
const metaPath = path.join(bundleDirForPresFP(presFP), "bundle.json");
|
|
81
|
+
return JSON.parse(fs.readFileSync(metaPath, "utf8"));
|
|
82
|
+
} catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
68
87
|
// Wipe the whole materialized bundle cache so the next build/push/download
|
|
69
88
|
// re-fetches every design bundle fresh from the add-in. `slidemind init` calls
|
|
70
89
|
// this: init is the deliberate (re)setup point — and `slidemind update`
|
|
@@ -220,13 +239,31 @@ function buildReusableImagesManifest(extractedImages) {
|
|
|
220
239
|
// a bundle re-produce (engines, image-source guidelines, sample curation) and
|
|
221
240
|
// deployment-owned content (workflow refs, the lint engine) can never go
|
|
222
241
|
// stale here. `force` re-downloads binaries even when their etags match.
|
|
223
|
-
//
|
|
224
|
-
//
|
|
242
|
+
// Target: `presId` (pane registry), or `brandId` in options for the pane-free
|
|
243
|
+
// brand route (presId may then be null; backendURL null = fan-out).
|
|
244
|
+
// Returns { dir, presFP, etag, version, backendURL, downloaded, pruned,
|
|
245
|
+
// engineUpdated, warnings }.
|
|
225
246
|
export async function materializeBundle(
|
|
226
247
|
presId,
|
|
227
|
-
{ force = false, backendURL = null, preferBrandId = null } = {},
|
|
248
|
+
{ force = false, backendURL = null, preferBrandId = null, brandId = null } = {},
|
|
228
249
|
) {
|
|
229
|
-
|
|
250
|
+
// Two fetch paths for the same wire: by presId through the pane registry
|
|
251
|
+
// (needs a registered window), or pane-free by brandId through the brand
|
|
252
|
+
// route (bound deck, PowerPoint optional). Everything below operates on the
|
|
253
|
+
// wire and is identical for both.
|
|
254
|
+
let wire;
|
|
255
|
+
let wireBackendURL = backendURL;
|
|
256
|
+
if (brandId) {
|
|
257
|
+
if (backendURL) {
|
|
258
|
+
wire = await getDesignBundleForBrand(brandId, { backendURL });
|
|
259
|
+
} else {
|
|
260
|
+
const resolved = await getDesignBundleForBrandAnyBackend(brandId);
|
|
261
|
+
wire = resolved.wire;
|
|
262
|
+
wireBackendURL = resolved.backendURL;
|
|
263
|
+
}
|
|
264
|
+
} else {
|
|
265
|
+
wire = await getDesignBundle(presId, { backendURL, preferBrandId });
|
|
266
|
+
}
|
|
230
267
|
if (!wire || !wire.presFP) {
|
|
231
268
|
throw new Error("design-bundle response is missing presFP");
|
|
232
269
|
}
|
|
@@ -445,6 +482,9 @@ export async function materializeBundle(
|
|
|
445
482
|
|
|
446
483
|
const meta = {
|
|
447
484
|
presFP,
|
|
485
|
+
// In the brand model presFP == brandId; recorded explicitly so consumers
|
|
486
|
+
// (and humans reading the cache) don't need to know that equivalence.
|
|
487
|
+
brandId: presFP,
|
|
448
488
|
version: wire.version ?? null,
|
|
449
489
|
etag: wire.etag ?? null,
|
|
450
490
|
scope: wire.scope ?? null,
|
|
@@ -483,6 +523,9 @@ export async function materializeBundle(
|
|
|
483
523
|
presFP,
|
|
484
524
|
etag: wire.etag ?? null,
|
|
485
525
|
version: wire.version ?? null,
|
|
526
|
+
// Which backend actually served the wire — meaningful on the brandId
|
|
527
|
+
// fan-out path, where the caller didn't know it up front.
|
|
528
|
+
backendURL: wireBackendURL,
|
|
486
529
|
downloaded,
|
|
487
530
|
pruned,
|
|
488
531
|
engineUpdated,
|
|
@@ -613,8 +656,7 @@ export function isImageEngine(engine) {
|
|
|
613
656
|
// `kind`. Returns null when the CD exposes no image engine (e.g. only a
|
|
614
657
|
// slide-search engine) — callers surface a "wire an image engine" message
|
|
615
658
|
// rather than falling back to an engine the bridge can't insert. Single source
|
|
616
|
-
// of truth for the heuristic
|
|
617
|
-
// and the MCP `slidemind_search_engine` tool.
|
|
659
|
+
// of truth for the heuristic used by the CLI dispatcher (`slidemind search`).
|
|
618
660
|
export function pickImageEngine(engines) {
|
|
619
661
|
const list = Array.isArray(engines) ? engines : [];
|
|
620
662
|
return list.find(isImageEngine) ?? null;
|
package/src/bridge/capture.mjs
CHANGED
|
@@ -171,8 +171,8 @@ export async function captureDeckLocally({ deckDir, bundleDir, onProgress }) {
|
|
|
171
171
|
}
|
|
172
172
|
const meta = readBundleMeta(bundleDir);
|
|
173
173
|
const version = meta.captureEngineVersion ?? "0";
|
|
174
|
-
// `?v=` busts Node's module cache so a
|
|
175
|
-
//
|
|
174
|
+
// `?v=` busts Node's module cache so a re-import picks up a refreshed engine
|
|
175
|
+
// at the same path after a bundle sync.
|
|
176
176
|
const engine = await import(
|
|
177
177
|
`${pathToFileURL(enginePath).href}?v=${encodeURIComponent(version)}`
|
|
178
178
|
);
|