slidemind 0.0.27

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.
@@ -0,0 +1,108 @@
1
+ ---
2
+ name: slidemind-create-slides
3
+ description: Author branded slide decks and individual slides against the active corporate design. Owns the user-facing intake → plan → create → edit workflow that turns per-slide .jsx files into editable .pptx (rendered server-side in PowerPoint) plus a local HTML preview. Reads the brand rules, layout palette, and authoring workflow from a materialized design bundle served by the slidemind PowerPoint add-in — it ships neither its own brand nor its own exporter. Use whenever the user asks for a slide, a deck, a presentation, a slideshow, or to convert content into one; interview them for intent and output format first, then stay on-brand by reading the bundle at every step. Skip when the user only wants to open, present, rename, search, or query an existing deck without authoring or editing slide content.
4
+ ---
5
+
6
+ # slidemind-create-slides
7
+
8
+ This skill is **thin workflow glue for the local harness**. Everything that determines how a deck is authored, styled, and rendered lives in the **slidemind PowerPoint add-in** and reaches you through a **materialized design bundle**:
9
+
10
+ - The **brand** — tokens, `corporate-design.css`, the `design.md` brand rules, the closed layout palette, brand assets — is served per corporate design.
11
+ - The **authoring workflow craft** — create, edit, composition, layouts, authoring contract, build/export — is the add-in's deployment-shipped `workflow/*.md`, served on the same bundle so the local harness reads **byte-identical** guidance to the in-app design agent.
12
+ - The **`.pptx` render** happens server-side in the add-in. There is no local exporter.
13
+
14
+ So this file owns only what is genuinely CLI-specific: the **host precondition**, a compact **intake/plan**, the **create flow**, and the **command mapping** that translates the add-in's in-app tool names (used throughout the bundle's `workflow/*.md`) into your `slidemind` commands and MCP tools. Read the workflow + brand from the bundle; never restate or duplicate them here.
15
+
16
+ ## Make sure the slidemind CLI is installed (npm)
17
+
18
+ This skill drives the `slidemind` CLI and its MCP server (`slidemind_list_open_presentations`, `slidemind_push_deck`, `slidemind_search_engine`, `slidemind_download_result`). If `slidemind` isn't on `PATH` — or those `slidemind_*` MCP tools aren't available — install the package from npm, then register it with the harness:
19
+
20
+ ```bash
21
+ npm install -g slidemind
22
+ slidemind init --for <claude|codex|opencode|all> # registers the MCP server + (re)deploys this skill
23
+ ```
24
+
25
+ - For a one-off command without a global install, `npx slidemind <command>` works.
26
+ - `slidemind --version` confirms it's on `PATH`. `slidemind update` upgrades to the latest npm release (`--ref <version>` to pin).
27
+ - If `slidemind init` just registered the MCP server, the harness may need a restart before the `slidemind_*` tools appear.
28
+
29
+ ## Precondition gate (check this first, every time)
30
+
31
+ Authoring and rendering hard-require an **open PowerPoint host with the slidemind add-in loaded and an active corporate design**. Without a host there is no bundle to read and no renderer to push to; `download-bundle`, `build-pptx`, and `push` all fail.
32
+
33
+ 1. Run `slidemind presentations` (or MCP `slidemind_list_open_presentations`).
34
+ 2. One registered → that's your target `presentation_id`. **Several → ask the user which**, by `title`/`fileName` (never raw ids) — once for **which presentation's corporate design to download**, and again at render time for **which presentation to push into** (the CD source and the push target may be different presentations).
35
+ 3. **None registered → stop.** Tell the user: *open PowerPoint, load the slidemind add-in, and pick or create a corporate design, then retry.*
36
+
37
+ **Connections drop when PowerPoint is backgrounded — try first, reconnect only on failure.** The add-in's task pane suspends when PowerPoint isn't the focused app, so its bridge connection can drop and `slidemind presentations` may briefly go empty even though PowerPoint is still open with a deck. So **always just run the command first — never pre-emptively ask the user to do anything.** Only if a command fails with *no host registered* (or a bridge-connection error) do you ask the user to **click into PowerPoint and focus the slidemind task pane** so it reconnects, then **retry the same command** — the deck and bundle are already prepared, so it's just a re-run. Reserve the full "open PowerPoint…" instruction in (3) for when nothing is registered at all.
38
+
39
+ Local HTML preview (`slidemind build`) runs offline, but it reads a **previously materialized bundle** — so the host must have been available at least once.
40
+
41
+ ## Intake → plan (you own this; do it in chat)
42
+
43
+ The add-in's in-app flow has a manager do planning (`present_plan_for_review`); **here, you run it directly with the user.** Don't write slide JSX until the plan is agreed.
44
+
45
+ - **Read scope from the wording**, don't ask redundantly: single slide ("make a slide about X"), small set ("add three slides on Y"), or whole deck ("a deck for Friday"). Only ask "one slide, a small set, or a full deck?" when truly ambiguous.
46
+ - **Interview just enough** to pin: audience/goal, the content and its sources (none / pasted / local files / existing deck / web research), the **output format** (push into PowerPoint, `.pptx` file, HTML preview, or a mix), and the slide outline (each slide's role + a layout *job* like cover/agenda/section/quote/closing/body — the concrete brand layout is chosen later against the bundle).
47
+ - **Replay and get sign-off.** Single slides / tiny sets → a compact in-chat plan. Decks → persist `decks/<slug>/plan.md` (content direction, narrative red thread, sources, per-slide role + layout job — *not* visual shape, *not* brand specifics). A valid existing `decks/<slug>/plan.md` counts as approved; restate what you're building.
48
+
49
+ Keep planning about intent and content. Don't materialize the bundle or read brand/layout/composition detail until create/edit.
50
+
51
+ ## The create flow
52
+
53
+ 1. **Choose the host** (gate above) → `presentation_id`.
54
+ 2. **Materialize the bundle.** `slidemind download-bundle <deck> --to <presId>` downloads the active corporate design and binds the deck to it. The bundle dir is keyed by the deck's `.bundle-ref.json` → `${XDG_CACHE_HOME:-~/.cache}/slidemind/bundles/<presFP>/`. It contains:
55
+ - `design.md` — brand rules + authoring contract pointers + the `### Brand Image Sources` block (the brand source of truth — **read it**).
56
+ - `corporate-design.css`, `layouts/*.jsx`, `layouts/layouts.css`, `assets/`, `samples/` (the brand's own reference slides), `engines.json`.
57
+ - `workflow/` — the add-in-owned authoring craft: `create.md`, `edit.md`, `composition.md`, `layouts.md`, `authoring.md`, `build-and-export.md`.
58
+ 3. **Study brand samples (before composing).** The materialized bundle's `samples/` dir holds the brand's own reference slides. If it's non-empty, pick **3 at random** and **Read** each (the Read tool renders images) *before* writing any JSX — this is the local-harness stand-in for the in-app persona's reference thumbnails, and it is what keeps a deck looking on-brand rather than merely compliant. Study them for whitespace rhythm, shape density, hierarchy, accent use, and layering, then compose in that spirit (never copy a sample 1:1). This is `composition.md`'s samples discipline; skip only when `samples/` is empty.
59
+ 4. **Read, then author.** Read `workflow/create.md` (the procedure), `workflow/layouts.md` + `workflow/authoring.md` (how to compose + the substrate rules), and the deck's `design.md` + the specific `layouts/*.jsx` the plan uses. Then write `decks/<slug>/slides/*.jsx` — one default export per slide, composing one bundle layout (Mode A) or a blank-content layout with freeform children (Mode B), exactly as those docs prescribe. Pull in `workflow/composition.md` when the plan has 3+ parallel ideas, a repeated skeleton, a pacing/rhythm need, or a slide that's merely compliant.
60
+ 5. **Imagery from the engine.** Per the `### Brand Image Sources` block in `design.md`, search the brand engine and place results (see the command mapping — this is `engine_search`/`engine_add` in the workflow docs).
61
+ 6. **Render / push.** `slidemind push <deck>` (into PowerPoint) or `slidemind build-pptx <deck>` (to a file) — both render server-side. This is `build_and_push` in `workflow/build-and-export.md`. If several presentations are open, confirm **which one to push into** first (it can differ from the CD source). **Run the push without asking otherwise**; if it fails because the host disconnected (PowerPoint was backgrounded / the task pane suspended), ask the user to focus PowerPoint and retry — don't rebuild anything, just re-run `push`.
62
+ 7. **Sanity pass.** Run the checks in `workflow/create.md` before reporting done.
63
+
64
+ ## Recolouring engine icons (CLI specifics)
65
+
66
+ Engine results land as files referenced with `<img src="./assets/<file>">`. The bundle's `workflow/*.md` are written for the in-app render; these two facts are specific to the local harness:
67
+
68
+ - **Never open the SVG to find out whether it recolours.** `slidemind_download_result` returns a `recolor` descriptor — `{ recolorable, mode, applied, note }`, where `mode` is `currentColor`, `mono`, `multicolor`, or `raster`. Read it; that *is* the answer.
69
+ - **Two ways to colour an icon, both honoured in the `slidemind build` preview *and* the render:**
70
+ - **Bake it in (prefer this):** pass `icon_color` — a hex you resolve from a token in the bundle's `corporate-design.css` — to `slidemind_download_result`. Works for any `recolorable` glyph and the colour is in the saved file, so preview and render always agree.
71
+ - **Tint via CSS:** set `color` to a token on the `<img>`. This works only for `mode: "currentColor"` icons (the preview inlines the SVG so `color` resolves, and the render substitutes `currentColor`). A `mono` icon will not respond to CSS colour — bake it instead.
72
+ - **Batch the fetch.** Pull every icon a slide needs in one call via `selections: [{ selection, icon_color }, …]` — the downloads run concurrently. Don't call `slidemind_download_result` once per icon.
73
+
74
+ ## Command mapping (CLI ⇄ the bundle's `workflow/*.md`)
75
+
76
+ The bundle's `workflow/*.md` are written for the in-app design agent and name **its** tools. You are the local harness — translate as you read:
77
+
78
+ | In `workflow/*.md` (in-app agent) | Your equivalent (local harness) |
79
+ |---|---|
80
+ | `build_and_push` | `slidemind push <deck>` (into the open PowerPoint) · `slidemind build-pptx <deck>` (to a `.pptx` file) — both render server-side via the bridge |
81
+ | `engine_search` | MCP `slidemind_search_engine({ presentation_id, query })` → returns a `searchId` + numbered results |
82
+ | `engine_add` / `insert_image` | MCP `slidemind_download_result({ presentation_id, deck_path, search_id, selection, icon_color? })` — or batch many at once with `selections: [{ selection, icon_color? }, …]`. Writes each asset into `decks/<slug>/assets/`, returns a `./assets/<file>` reference for `<img src=...>` plus a `recolor` descriptor (see *Recolouring engine icons*) |
83
+ | writing `slides/<topic>.jsx` in the chat workspace | write files under `decks/<slug>/slides/*.jsx` on disk |
84
+ | reading workspace files (`design.md`, `layouts/`, `workflow/*`) | read them from the materialized bundle dir (`<bundle>/…`) and your `decks/<slug>/` |
85
+ | reference-slide thumbnails injected at `add_slide` (in-app persona) | **Read** 3 random images from the materialized bundle's `samples/` dir — the Read tool renders them (see *The create flow* step 3) |
86
+ | `present_plan_for_review` (manager-owned) | **you** run intake → plan with the user (above); persist `decks/<slug>/plan.md` for decks |
87
+
88
+ Everything else in `workflow/*.md` — the composition craft, the substrate/authoring rules, the layout discovery and slot/working-area rules, the sanity bar — applies to you unchanged.
89
+
90
+ ## The brand and the workflow both live in the bundle, not here
91
+
92
+ Do not restate brand values (colour, type scale, spacing, layout names, accent discipline) or re-derive the authoring/composition craft in this file or in slide files — they are in the bundle's `design.md` and `workflow/*.md`. Read them and follow them. Don't load external UI/design-taste plugins during slide work; compose against the bundle's layouts and tokens. If a render returns `bundle_etag_stale`, the corporate design moved — re-run `slidemind download-bundle <deck>` and re-read.
93
+
94
+ ## Failure modes the render lint can't catch
95
+
96
+ Render-time lint rejects contract violations (deny-listed CSS, bad imports, remote `<img>` URLs — defined in the bundle's `design.md`). It can't catch content-shaped failures; check for these in the sanity pass:
97
+
98
+ - Layout-default / placeholder text leaking through an unfilled prop.
99
+ - A bullet list standing in for a structured idea (process, comparison, timeline).
100
+ - An equal-card grid with no hero and no hierarchy.
101
+ - Em dashes / curly quotes leaking from pasted source.
102
+ - Hand-rolled glyphs instead of engine imagery.
103
+ - Slide-local class names colliding with global token utility classes.
104
+ - Internal composition vocabulary (`hero`, `focal`, …) rendered as visible text.
105
+
106
+ ## Where the deck lives
107
+
108
+ `decks/<slug>/` is created in the **current working directory** (the shell's pwd when the skill was invoked). Every `slidemind` command and `mkdir` resolves `decks/` against that cwd — don't `cd` into the skill's install directory. The materialized bundle lives in the cache dir above, not under the deck.
@@ -0,0 +1,232 @@
1
+ // Render a deck server-side via the bridge, then push the bytes into a running
2
+ // PowerPoint window (or write them to a file).
3
+ //
4
+ // The CLI ships ZERO exporter code: the authored slide JSX + deck-local assets
5
+ // are POSTed to the add-in's `render-pptx` route, which renders them against
6
+ // the presentation's own corporate design using the SAME OOXML exporter the
7
+ // in-app design persona uses. The returned base64 .pptx is then inserted /
8
+ // replaced (push) or written to disk (build-pptx). An open PowerPoint host with
9
+ // an active corporate design is therefore a hard precondition for both paths.
10
+
11
+ import fs from "node:fs";
12
+ import path from "node:path";
13
+
14
+ import {
15
+ insertSlides,
16
+ listOpenPresentations,
17
+ pickPresentation,
18
+ renderPptx,
19
+ replaceSlides,
20
+ requireHost,
21
+ } from "./client.mjs";
22
+ import {
23
+ materializeBundle,
24
+ readDeckBundleRef,
25
+ writeDeckBundleRef,
26
+ } from "./bundle.mjs";
27
+
28
+ function gatherSlides(deckDir) {
29
+ const slidesDir = path.join(deckDir, "slides");
30
+ if (!fs.existsSync(slidesDir) || !fs.statSync(slidesDir).isDirectory()) {
31
+ throw new Error(`Expected 'slides/' folder in ${deckDir}`);
32
+ }
33
+ const files = fs
34
+ .readdirSync(slidesDir)
35
+ .filter((f) => f.endsWith(".jsx"))
36
+ .sort();
37
+ if (files.length === 0) {
38
+ throw new Error(`No .jsx slides under ${slidesDir}`);
39
+ }
40
+ const slides = {};
41
+ for (const f of files) {
42
+ slides[f] = fs.readFileSync(path.join(slidesDir, f), "utf8");
43
+ }
44
+ return slides;
45
+ }
46
+
47
+ // Collect deck-local assets (icons the agent downloaded, deck photos) as
48
+ // base64, keyed by their `assets/<rel>` path so the server-side renderer mounts
49
+ // them where the slide JSX references them (`<img src="./assets/<rel>">`).
50
+ function gatherDeckAssets(deckDir) {
51
+ const assetsDir = path.join(deckDir, "assets");
52
+ const out = {};
53
+ if (!fs.existsSync(assetsDir)) return out;
54
+ const walk = (dir, prefix) => {
55
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
56
+ const abs = path.join(dir, entry.name);
57
+ const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
58
+ if (entry.isDirectory()) {
59
+ walk(abs, rel);
60
+ } else {
61
+ out[`assets/${rel}`] = fs.readFileSync(abs).toString("base64");
62
+ }
63
+ }
64
+ };
65
+ walk(assetsDir, "");
66
+ return out;
67
+ }
68
+
69
+ // Render a deck to base64 .pptx over the bridge. Pass the deck's authored-against
70
+ // bundle etag (when recorded) so the server can detect CD skew.
71
+ export async function renderDeckToBase64(deckDir, { presId }) {
72
+ const slides = gatherSlides(deckDir);
73
+ const deckAssets = gatherDeckAssets(deckDir);
74
+ // Bind the deck to a materialized bundle if it isn't already. Without a
75
+ // `.bundle-ref.json` the etag below is null (server-side skew detection
76
+ // silently no-ops) AND the local HTML/editor preview can't resolve the
77
+ // deck's design files. `download-bundle <deck>` normally writes this; here we
78
+ // self-heal so `push`/`build-pptx` work even when it wasn't run explicitly.
79
+ let ref = readDeckBundleRef(deckDir);
80
+ if (!ref?.etag) {
81
+ try {
82
+ const mat = await materializeBundle(presId);
83
+ writeDeckBundleRef(deckDir, { presFP: mat.presFP, etag: mat.etag });
84
+ ref = readDeckBundleRef(deckDir);
85
+ } catch {
86
+ /* best-effort: the add-in still resolves the CD server-side, so the
87
+ render itself succeeds — only skew detection / local preview binding
88
+ are skipped when the bundle can't be materialized here. */
89
+ }
90
+ }
91
+ const result = await renderPptx({
92
+ presId,
93
+ slides,
94
+ deckAssets,
95
+ etag: ref?.etag ?? null,
96
+ });
97
+ if (!result?.base64) {
98
+ throw new Error("render-pptx returned no pptx bytes");
99
+ }
100
+ return result; // { ok, base64, slideCount, warnings, skippedSlides }
101
+ }
102
+
103
+ export async function buildAndPush({
104
+ deckDir,
105
+ presentationId = null,
106
+ insertAt = null,
107
+ replace = false,
108
+ slideIds = null,
109
+ }) {
110
+ if (!deckDir) throw new Error("buildAndPush: deckDir is required");
111
+ // `slideIds` selects a subset of the BUILT deck's slides to push (a SOURCE
112
+ // filter, applied as Office.js sourceSlideIds on insert). The replace path
113
+ // has no source-subset support on the bridge — passing both would silently
114
+ // wipe the WHOLE presentation and re-push every built slide. Reject the combo
115
+ // (mirrors the `--replace` / `--slides` guard in the CLI dispatcher).
116
+ if (replace && Array.isArray(slideIds) && slideIds.length > 0) {
117
+ throw new Error(
118
+ "replace cannot be combined with slideIds — that would wipe the entire " +
119
+ "presentation. Push without replace to insert a slide subset, or " +
120
+ "replace the full deck without slideIds.",
121
+ );
122
+ }
123
+
124
+ const registry = await requireHost();
125
+ const presId = pickPresentation(registry, presentationId);
126
+
127
+ const result = await renderDeckToBase64(deckDir, { presId });
128
+ const base64 = result.base64;
129
+ const bytes = Buffer.byteLength(base64, "base64");
130
+ const mode = replace ? "replace" : "insert";
131
+
132
+ const doPush = () =>
133
+ replace
134
+ ? replaceSlides({ presId, base64 })
135
+ : insertSlides({ presId, base64, insertAt, sourceSlideIds: slideIds });
136
+
137
+ let resp;
138
+ try {
139
+ resp = await pushWithPaneRetry(doPush, presId);
140
+ } catch (err) {
141
+ // The pane stayed backgrounded through every retry. The render already
142
+ // succeeded (it's pane-free), so don't lose it: write the bytes to
143
+ // <deck>.pptx and report a clean, resumable outcome instead of throwing.
144
+ if (err && err.code === "pane_suspended") {
145
+ const fallbackFile = path.join(deckDir, `${path.basename(deckDir)}.pptx`);
146
+ fs.writeFileSync(fallbackFile, Buffer.from(base64, "base64"));
147
+ return {
148
+ presId,
149
+ mode,
150
+ bytes,
151
+ paneSuspended: true,
152
+ fallbackFile,
153
+ warnings: result.warnings ?? [],
154
+ skippedSlides: result.skippedSlides ?? [],
155
+ };
156
+ }
157
+ throw err;
158
+ }
159
+ return {
160
+ presId,
161
+ mode,
162
+ bytes,
163
+ response: resp,
164
+ warnings: result.warnings ?? [],
165
+ skippedSlides: result.skippedSlides ?? [],
166
+ };
167
+ }
168
+
169
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
170
+
171
+ // Push into the live deck (insert/replace) tolerating a transiently backgrounded
172
+ // task pane. Retry ONLY the definitive pre-ack `pane_suspended` (the registry
173
+ // entry exists but its socket is gone, so the emit provably never happened —
174
+ // re-sending the same bytes is safe). NEVER retry a `bridge_timeout`: there the
175
+ // emit may have been delivered, and insert is append-only, so a retry could
176
+ // double-insert.
177
+ async function pushWithPaneRetry(doPush, presId, attempts = 3) {
178
+ let lastErr;
179
+ for (let i = 0; i < attempts; i++) {
180
+ try {
181
+ return await doPush();
182
+ } catch (err) {
183
+ if (!err || err.code !== "pane_suspended") throw err;
184
+ lastErr = err;
185
+ if (i === attempts - 1) break;
186
+ // Give the user a moment to foreground PowerPoint; resolve as soon as the
187
+ // pane re-registers as connected, up to a growing window.
188
+ await waitForConnected(presId, 2000 * (i + 1));
189
+ }
190
+ }
191
+ throw lastErr;
192
+ }
193
+
194
+ async function waitForConnected(presId, windowMs) {
195
+ const deadline = Date.now() + windowMs;
196
+ while (Date.now() < deadline) {
197
+ try {
198
+ const reg = (await listOpenPresentations()).find(
199
+ (p) => p.presId === presId,
200
+ );
201
+ // `connected` is undefined against an older add-in — treat that as ready
202
+ // so we still retry rather than stall.
203
+ if (reg && reg.connected !== false) return;
204
+ } catch {
205
+ /* bridge momentarily unreachable; keep polling within the window */
206
+ }
207
+ await sleep(500);
208
+ }
209
+ }
210
+
211
+ // Render a deck to a .pptx file (the `slidemind build-pptx` path). Still
212
+ // requires an open host because the render happens server-side.
213
+ export async function buildPptxFile({
214
+ deckDir,
215
+ presentationId = null,
216
+ outPath = null,
217
+ }) {
218
+ if (!deckDir) throw new Error("buildPptxFile: deckDir is required");
219
+ const registry = await requireHost();
220
+ const presId = pickPresentation(registry, presentationId);
221
+ const result = await renderDeckToBase64(deckDir, { presId });
222
+ const out =
223
+ outPath || path.join(deckDir, `${path.basename(deckDir)}.pptx`);
224
+ fs.writeFileSync(out, Buffer.from(result.base64, "base64"));
225
+ return {
226
+ outPath: out,
227
+ presId,
228
+ slideCount: result.slideCount ?? null,
229
+ warnings: result.warnings ?? [],
230
+ skippedSlides: result.skippedSlides ?? [],
231
+ };
232
+ }