slidemind 0.3.12 → 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.
Files changed (49) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/AGENTS.md +86 -21
  4. package/README.md +14 -17
  5. package/bin/slidemind.mjs +65 -4969
  6. package/package.json +1 -1
  7. package/skills/create-slides/SKILL.md +113 -74
  8. package/skills/search/SKILL.md +25 -7
  9. package/src/bridge/build-and-push.mjs +108 -15
  10. package/src/bridge/bundle.mjs +69 -0
  11. package/src/bridge/capture.mjs +71 -8
  12. package/src/bridge/client/brands.mjs +622 -0
  13. package/src/bridge/client/engine-routing.mjs +237 -0
  14. package/src/bridge/client/engine-search.mjs +310 -0
  15. package/src/bridge/client/engine-slides.mjs +435 -0
  16. package/src/bridge/client/pane-ops.mjs +335 -0
  17. package/src/bridge/client/presentations.mjs +587 -0
  18. package/src/bridge/client/render.mjs +317 -0
  19. package/src/bridge/client/transport.mjs +695 -0
  20. package/src/bridge/client.mjs +80 -3452
  21. package/src/bridge/config.mjs +1 -1
  22. package/src/bridge/engine-download.mjs +15 -2
  23. package/src/builders/build.mjs +16 -669
  24. package/src/builders/draft-check.mjs +376 -0
  25. package/src/builders/lint.mjs +38 -2
  26. package/src/builders/preview.html +799 -0
  27. package/src/builders/review.mjs +23 -4
  28. package/src/builders/visual-families.mjs +6 -8
  29. package/src/cli/common.mjs +179 -0
  30. package/src/cli/concepts.mjs +310 -0
  31. package/src/cli/connection.mjs +147 -0
  32. package/src/cli/deck.mjs +608 -0
  33. package/src/cli/designs.mjs +113 -0
  34. package/src/cli/diagnose.mjs +141 -0
  35. package/src/cli/download-bundle.mjs +144 -0
  36. package/src/cli/engine-helpers.mjs +429 -0
  37. package/src/cli/engine-results.mjs +320 -0
  38. package/src/cli/help.mjs +201 -0
  39. package/src/cli/install.mjs +257 -0
  40. package/src/cli/read.mjs +120 -0
  41. package/src/cli/ready.mjs +805 -0
  42. package/src/cli/search.mjs +608 -0
  43. package/src/cli/session.mjs +177 -0
  44. package/src/cli/setup.mjs +282 -0
  45. package/src/cli/slide-reuse.mjs +334 -0
  46. package/src/cli/status.mjs +118 -0
  47. package/src/cli/workspace.mjs +302 -0
  48. package/src/installers/codex.mjs +1 -1
  49. package/src/update-check.mjs +231 -0
@@ -50,6 +50,7 @@ import {
50
50
  getDesignBundle,
51
51
  getDesignBundleForBrand,
52
52
  getDesignBundleForBrandAnyBackend,
53
+ listBrands,
53
54
  } from "./client.mjs";
54
55
  import { updateCliInstruction } from "../install-channel.mjs";
55
56
 
@@ -655,6 +656,74 @@ export function readDeckBundleRef(deckDir) {
655
656
  // Resolve the materialized bundle directory a deck should read its design files
656
657
  // from (via the deck's .bundle-ref.json). Returns null when no bundle has been
657
658
  // materialized for the deck's CD yet.
659
+ // Re-sync a bound deck's design bundle to the design as it stands right now.
660
+ //
661
+ // `ready` materializes at session start, but a corporate design can change
662
+ // while a session runs, and every local step afterwards (HTML build, lint,
663
+ // capture, the draft checks, and the etag the server render is validated
664
+ // against) reads this cached copy. So the deck commands call this first: the
665
+ // sync is etag-gated and incremental, so an unchanged design downloads nothing.
666
+ //
667
+ // Returns { result, changed } — `changed` is true when the design's etag moved,
668
+ // in which case the deck's ref is rewritten to the new one. Returns null when
669
+ // the deck is unbound or the backend cannot be reached: a stale-but-present
670
+ // bundle is what keeps the host-free steps host-free.
671
+ export async function refreshDeckBundle(deckDir) {
672
+ if (!deckDir) return null;
673
+ const ref = readDeckBundleRef(deckDir);
674
+ if (!ref?.presFP) return null; // unbound — `ready <deck>` binds it
675
+ // A refresh must never make a host-free step wait like a render does: a dead
676
+ // backend has to fall back to the cached bundle in seconds, not the 35s
677
+ // default plus retries. A negative delay list filters to empty, which also
678
+ // turns the transient retry off — the next command retries anyway.
679
+ const prev = {
680
+ SLIDEMIND_BRIDGE_TIMEOUT_MS: process.env.SLIDEMIND_BRIDGE_TIMEOUT_MS,
681
+ SLIDEMIND_BRIDGE_RETRY_DELAYS_MS: process.env.SLIDEMIND_BRIDGE_RETRY_DELAYS_MS,
682
+ };
683
+ process.env.SLIDEMIND_BRIDGE_TIMEOUT_MS ??=
684
+ process.env.SLIDEMIND_BUNDLE_REFRESH_TIMEOUT_MS || "6000";
685
+ process.env.SLIDEMIND_BRIDGE_RETRY_DELAYS_MS ??= "-1";
686
+ try {
687
+ const result = await materializeBundle(null, { brandId: ref.presFP });
688
+ if (!result) return null;
689
+ const changed = Boolean(result.etag) && result.etag !== ref.etag;
690
+ if (changed) {
691
+ writeDeckBundleRef(deckDir, { presFP: result.presFP, etag: result.etag });
692
+ }
693
+ return { result, changed };
694
+ } catch {
695
+ return null; // offline / unconnected / design withdrawn — keep the cache
696
+ } finally {
697
+ for (const [k, v] of Object.entries(prev)) {
698
+ if (v === undefined) delete process.env[k];
699
+ else process.env[k] = v;
700
+ }
701
+ }
702
+ }
703
+
704
+ // The design an UNBOUND deck can be rendered/previewed against WITHOUT a
705
+ // PowerPoint window — the same ladder `ready` walks once no window is in play:
706
+ // the deck's own binding → the one design cached on this machine → the one
707
+ // design the connected backends list. Returns the presFP, or null when nothing
708
+ // answers unambiguously (0 or several of either; no brand route) — the caller
709
+ // then falls back to the window registry, which is the only remaining source
710
+ // of a design (an open presentation's active corporate design) and the one
711
+ // place that legitimately needs the pane.
712
+ export async function resolveWindowlessBrand(deckDir) {
713
+ const bound = readDeckBundleRef(deckDir)?.presFP;
714
+ if (bound) return bound;
715
+ const cached = listCachedBundles();
716
+ if (cached.length === 1) return cached[0].presFP;
717
+ if (cached.length > 1) return null;
718
+ let brands = [];
719
+ try {
720
+ brands = await listBrands();
721
+ } catch {
722
+ brands = [];
723
+ }
724
+ return brands.length === 1 ? brands[0].presFP : null;
725
+ }
726
+
658
727
  export function resolveBundleDirForDeck(deckDir) {
659
728
  const ref = readDeckBundleRef(deckDir);
660
729
  if (!ref?.presFP) return null;
@@ -145,8 +145,9 @@ export function buildCaptureInput(deckDir, bundleDir) {
145
145
  }
146
146
 
147
147
  // Launch a headless Chrome, run `fn`, then tear it down — mirrors the add-in's
148
- // withBrowser (waits for the process to fully exit before cleanup).
149
- async function withBrowser(fn) {
148
+ // withBrowser (waits for the process to fully exit before cleanup). Exported
149
+ // for the draft-check grid overlay, which reuses the same pinned Chrome.
150
+ export async function withBrowser(fn) {
150
151
  // Test hook: run the engine with no real browser. Only safe with a fake
151
152
  // engine that ignores its `browser` argument — never set in production.
152
153
  if (process.env.SLIDEMIND_CAPTURE_BROWSERLESS === "1") {
@@ -162,11 +163,25 @@ async function withBrowser(fn) {
162
163
  // the bundle-shipped fonts are identical at the same pinned version, so text
163
164
  // measurement parity with the add-in's renderer is preserved.
164
165
  // SLIDEMIND_CAPTURE_FULL_CHROME=1 forces the full binary for A/B debugging.
165
- const browser = await puppeteer.launch({
166
- headless: process.env.SLIDEMIND_CAPTURE_FULL_CHROME === "1" ? true : "shell",
167
- protocolTimeout: 600_000,
168
- args: LAUNCH_ARGS,
169
- });
166
+ const launch = (args) =>
167
+ puppeteer.launch({
168
+ headless: process.env.SLIDEMIND_CAPTURE_FULL_CHROME === "1" ? true : "shell",
169
+ protocolTimeout: 600_000,
170
+ args,
171
+ });
172
+ let browser;
173
+ try {
174
+ browser = await launch(LAUNCH_ARGS);
175
+ } catch (err) {
176
+ // Codex's workspace-write seatbelt denies the mach-port rendezvous the
177
+ // browser process uses to hand off to its renderer ("bootstrap_check_in
178
+ // … Permission denied"), so a multi-process launch dies before the first
179
+ // page. Single-process keeps the renderer in-process — no rendezvous —
180
+ // and paints identically (same Blink/Skia, same fonts). Retried only on a
181
+ // launch failure, so every other environment keeps the stock launch.
182
+ if (!/Failed to launch the browser process/i.test(String(err?.message ?? err))) throw err;
183
+ browser = await launch([...LAUNCH_ARGS, "--single-process", "--no-zygote"]);
184
+ }
170
185
  try {
171
186
  return await fn(browser);
172
187
  } finally {
@@ -199,7 +214,7 @@ async function withBrowser(fn) {
199
214
  * Throws if the engine is missing/corrupt or the local browser can't launch —
200
215
  * the caller decides whether to fall back to the server render path.
201
216
  */
202
- export async function captureDeckLocally({ deckDir, bundleDir, onProgress }) {
217
+ export async function captureDeckLocally({ deckDir, bundleDir, onProgress, only = null }) {
203
218
  const enginePath = path.join(bundleDir, "capture-engine.mjs");
204
219
  if (!fs.existsSync(enginePath)) {
205
220
  throw new Error(`no capture engine in bundle ${bundleDir}`);
@@ -218,6 +233,15 @@ export async function captureDeckLocally({ deckDir, bundleDir, onProgress }) {
218
233
  }
219
234
 
220
235
  const input = buildCaptureInput(deckDir, bundleDir);
236
+ // `only`: capture a subset of the authored slides (a per-slide builder
237
+ // re-checking its own file) — the engine renders each slide on its own, so
238
+ // dropping the others from the input is exact, not an approximation.
239
+ if (Array.isArray(only) && only.length > 0) {
240
+ const keep = new Set(only);
241
+ for (const name of Object.keys(input.slides)) {
242
+ if (!keep.has(name)) delete input.slides[name];
243
+ }
244
+ }
221
245
  return withBrowser((browser) => engine.captureDeck(browser, input, { onProgress }));
222
246
  }
223
247
 
@@ -232,6 +256,45 @@ export async function captureDeckLocally({ deckDir, bundleDir, onProgress }) {
232
256
  * never a silent degradation: such a bundle's lint engine (same deployment)
233
257
  * rejects `<Chart>` as an unknown component before a page is ever built.
234
258
  */
259
+ /**
260
+ * The exporter's connector geometry (`connector-derive`) as a browser script
261
+ * exported from the same artifact (`connectorPreview: { globalName, source }`).
262
+ * The HTML preview's connector overlay calls it on the measured DOM rects, so
263
+ * the preview draws the route and arrowhead the .pptx will have. The CLI ships
264
+ * no connector geometry of its own. null when the bundle has no engine or the
265
+ * engine predates the export (the overlay then draws nothing and says so).
266
+ */
267
+ export async function loadConnectorPreview(bundleDir) {
268
+ if (!bundleDir || !bundleHasCaptureEngine(bundleDir)) return null;
269
+ const enginePath = path.join(bundleDir, "capture-engine.mjs");
270
+ const version = readBundleMeta(bundleDir).captureEngineVersion ?? "0";
271
+ const engine = await import(
272
+ `${pathToFileURL(enginePath).href}?v=${encodeURIComponent(version)}`
273
+ );
274
+ const preview = engine.connectorPreview;
275
+ if (!preview || typeof preview.source !== "string") return null;
276
+ return { globalName: String(preview.globalName || "SlidemindConnectorDerive"), source: preview.source };
277
+ }
278
+
279
+ /**
280
+ * The measured draft checks (`draftChecks: { fill, typeScale, typeScaleTokens,
281
+ * overlap }`) exported from the same artifact — the rules the add-in's own
282
+ * see_draft runs. view-draft calls them on its local capture so both harnesses
283
+ * teach one contract. The CLI ships no draft-check rule of its own; null when
284
+ * the bundle has no engine or the engine predates the export.
285
+ */
286
+ export async function loadDraftChecks(bundleDir) {
287
+ if (!bundleDir || !bundleHasCaptureEngine(bundleDir)) return null;
288
+ const enginePath = path.join(bundleDir, "capture-engine.mjs");
289
+ const version = readBundleMeta(bundleDir).captureEngineVersion ?? "0";
290
+ const engine = await import(
291
+ `${pathToFileURL(enginePath).href}?v=${encodeURIComponent(version)}`
292
+ );
293
+ const checks = engine.draftChecks;
294
+ if (!checks || typeof checks.fill !== "function") return null;
295
+ return checks;
296
+ }
297
+
235
298
  export async function loadChartPreview(bundleDir) {
236
299
  if (!bundleDir || !bundleHasCaptureEngine(bundleDir)) return null;
237
300
  const enginePath = path.join(bundleDir, "capture-engine.mjs");