slidemind 0.0.27 → 0.0.28
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/README.md +1 -1
- package/package.json +1 -1
- package/skills/create-slides/SKILL.md +3 -3
- package/src/bridge/build-and-push.mjs +12 -67
- package/src/bridge/client.mjs +24 -11
- package/src/mcp/server.mjs +10 -26
package/README.md
CHANGED
|
@@ -72,7 +72,7 @@ If `slidemind presentations` returns nothing, the add-in isn't running — open
|
|
|
72
72
|
|
|
73
73
|
## Icon / image search
|
|
74
74
|
|
|
75
|
-
`slidemind search "<query>"` searches the active corporate design's **brand engine** (icons/images) over the local bridge
|
|
75
|
+
`slidemind search "<query>"` searches the active corporate design's **brand engine** (icons/images) over the local bridge, using the add-in's server-side bridge when available so it returns the same results the in-app design agent sees (an open host is required). It prints a `searchId` plus numbered results; the agent then fetches a chosen result into the deck's `assets/` via the MCP tool `slidemind_download_result`. The agent-facing flow (`slidemind_search_engine` → `slidemind_download_result`) is owned by the add-in's design bundle and read by the skill. There is no local icon library anymore.
|
|
76
76
|
|
|
77
77
|
## Install into a coding-agent harness
|
|
78
78
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "slidemind",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.28",
|
|
4
4
|
"description": "Connect Claude Code, Codex & opencode to the slidemind PowerPoint add-in — let your coding agent build and edit native, on-brand PowerPoint decks against your active corporate design, from the terminal.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -34,7 +34,7 @@ Authoring and rendering hard-require an **open PowerPoint host with the slidemin
|
|
|
34
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
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
36
|
|
|
37
|
-
**
|
|
37
|
+
**Backgrounded PowerPoint is expected to work.** Do not pre-emptively ask the user to focus PowerPoint or the task pane. The bridge retains the registered presentation and pane-free routes keep working; live push/replace waits for the task-pane socket to reconnect if it truly dropped. Only if a command fails with *no host registered* (or a bridge wait timeout) do you ask the user to click the slidemind task pane once so it reconnects, then retry the same command — the deck and bundle are already prepared. Reserve the full "open PowerPoint…" instruction in (3) for when nothing is registered at all.
|
|
38
38
|
|
|
39
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
40
|
|
|
@@ -56,9 +56,9 @@ Keep planning about intent and content. Don't materialize the bundle or read bra
|
|
|
56
56
|
- `corporate-design.css`, `layouts/*.jsx`, `layouts/layouts.css`, `assets/`, `samples/` (the brand's own reference slides), `engines.json`.
|
|
57
57
|
- `workflow/` — the add-in-owned authoring craft: `create.md`, `edit.md`, `composition.md`, `layouts.md`, `authoring.md`, `build-and-export.md`.
|
|
58
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/
|
|
59
|
+
4. **Read, then author.** Read the four create-stage references together — `workflow/create.md` (the procedure), `workflow/composition.md` (the composition baseline), `workflow/layouts.md` (palette + working area), and `workflow/authoring.md` (the substrate rules) — plus the deck's `design.md` and the specific `layouts/*.jsx` the plan uses. (That is the read set `workflow/create.md` → Context loading prescribes; follow it as written.) 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.
|
|
60
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**;
|
|
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**; it should work while PowerPoint is backgrounded. If it times out waiting for a true task-pane reconnect, ask the user to click the slidemind task pane once and re-run `push` without rebuilding.
|
|
62
62
|
7. **Sanity pass.** Run the checks in `workflow/create.md` before reporting done.
|
|
63
63
|
|
|
64
64
|
## Recolouring engine icons (CLI specifics)
|
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
|
|
11
11
|
import fs from "node:fs";
|
|
12
12
|
import path from "node:path";
|
|
13
|
+
import { randomUUID } from "node:crypto";
|
|
13
14
|
|
|
14
15
|
import {
|
|
15
16
|
insertSlides,
|
|
16
|
-
listOpenPresentations,
|
|
17
17
|
pickPresentation,
|
|
18
18
|
renderPptx,
|
|
19
19
|
replaceSlides,
|
|
@@ -128,86 +128,31 @@ export async function buildAndPush({
|
|
|
128
128
|
const base64 = result.base64;
|
|
129
129
|
const bytes = Buffer.byteLength(base64, "base64");
|
|
130
130
|
const mode = replace ? "replace" : "insert";
|
|
131
|
+
const operationId = `push-${randomUUID()}`;
|
|
131
132
|
|
|
132
133
|
const doPush = () =>
|
|
133
134
|
replace
|
|
134
|
-
? replaceSlides({ presId, base64 })
|
|
135
|
-
: insertSlides({
|
|
135
|
+
? replaceSlides({ presId, base64, operationId })
|
|
136
|
+
: insertSlides({
|
|
137
|
+
presId,
|
|
138
|
+
base64,
|
|
139
|
+
insertAt,
|
|
140
|
+
sourceSlideIds: slideIds,
|
|
141
|
+
operationId,
|
|
142
|
+
});
|
|
136
143
|
|
|
137
|
-
|
|
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
|
-
}
|
|
144
|
+
const resp = await doPush();
|
|
159
145
|
return {
|
|
160
146
|
presId,
|
|
161
147
|
mode,
|
|
162
148
|
bytes,
|
|
149
|
+
operationId,
|
|
163
150
|
response: resp,
|
|
164
151
|
warnings: result.warnings ?? [],
|
|
165
152
|
skippedSlides: result.skippedSlides ?? [],
|
|
166
153
|
};
|
|
167
154
|
}
|
|
168
155
|
|
|
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
156
|
// Render a deck to a .pptx file (the `slidemind build-pptx` path). Still
|
|
212
157
|
// requires an open host because the render happens server-side.
|
|
213
158
|
export async function buildPptxFile({
|
package/src/bridge/client.mjs
CHANGED
|
@@ -127,7 +127,8 @@ function nodeRequest(urlString, init = {}) {
|
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
const timeoutMs =
|
|
130
|
-
|
|
130
|
+
init.timeoutMs ??
|
|
131
|
+
(parseInt(process.env.SLIDEMIND_BRIDGE_TIMEOUT_MS || "35000", 10) || 35000);
|
|
131
132
|
return new Promise((resolve, reject) => {
|
|
132
133
|
const req = transport.request(
|
|
133
134
|
url,
|
|
@@ -232,9 +233,9 @@ export async function renderPptx({
|
|
|
232
233
|
return res.json(); // { ok, base64, slideCount, warnings, skippedSlides }
|
|
233
234
|
}
|
|
234
235
|
|
|
235
|
-
// Brand-engine search
|
|
236
|
-
//
|
|
237
|
-
//
|
|
236
|
+
// Brand-engine search through the local bridge. New add-ins run this
|
|
237
|
+
// server-side with the relayed user token; older add-ins may fall back to a
|
|
238
|
+
// task-pane proxy. Returns a searchId + numbered result thumbnails.
|
|
238
239
|
export async function engineSearch({ presId, engineId, query }) {
|
|
239
240
|
if (!presId) throw new Error("engineSearch: presId is required");
|
|
240
241
|
if (!engineId) throw new Error("engineSearch: engineId is required");
|
|
@@ -250,9 +251,9 @@ export async function engineSearch({ presId, engineId, query }) {
|
|
|
250
251
|
return res.json(); // { ok, searchId, resultCount, gridDataUri, thumbnails }
|
|
251
252
|
}
|
|
252
253
|
|
|
253
|
-
// Fetch a chosen engine result's bytes
|
|
254
|
-
//
|
|
255
|
-
//
|
|
254
|
+
// Fetch a chosen engine result's bytes through the local bridge. For image
|
|
255
|
+
// engines this returns SVG/PNG bytes (+ optional recoloured svgMarkup) WITHOUT
|
|
256
|
+
// inserting onto a slide.
|
|
256
257
|
export async function engineDownload({
|
|
257
258
|
presId,
|
|
258
259
|
searchId,
|
|
@@ -279,22 +280,31 @@ export async function engineDownload({
|
|
|
279
280
|
return res.json(); // { ok, base64, mimeType, svgMarkup }
|
|
280
281
|
}
|
|
281
282
|
|
|
282
|
-
export async function insertSlides({
|
|
283
|
+
export async function insertSlides({
|
|
284
|
+
presId,
|
|
285
|
+
base64,
|
|
286
|
+
insertAt = null,
|
|
287
|
+
sourceSlideIds = null,
|
|
288
|
+
operationId = null,
|
|
289
|
+
}) {
|
|
283
290
|
if (!presId) throw new Error("insertSlides: presId is required");
|
|
284
291
|
if (!base64) throw new Error("insertSlides: base64 is required");
|
|
285
|
-
const body = { base64, insertAt, sourceSlideIds };
|
|
292
|
+
const body = { base64, insertAt, sourceSlideIds, operationId };
|
|
286
293
|
const res = await bridgeFetch(
|
|
287
294
|
`/local/presentations/${encodeURIComponent(presId)}/insert`,
|
|
288
295
|
{
|
|
289
296
|
method: "POST",
|
|
290
297
|
headers: { "content-type": "application/json" },
|
|
291
298
|
body: JSON.stringify(body),
|
|
299
|
+
timeoutMs:
|
|
300
|
+
parseInt(process.env.SLIDEMIND_BRIDGE_MUTATION_TIMEOUT_MS || "1805000", 10) ||
|
|
301
|
+
1805000,
|
|
292
302
|
},
|
|
293
303
|
);
|
|
294
304
|
return res.json().catch(() => ({}));
|
|
295
305
|
}
|
|
296
306
|
|
|
297
|
-
export async function replaceSlides({ presId, base64 }) {
|
|
307
|
+
export async function replaceSlides({ presId, base64, operationId = null }) {
|
|
298
308
|
if (!presId) throw new Error("replaceSlides: presId is required");
|
|
299
309
|
if (!base64) throw new Error("replaceSlides: base64 is required");
|
|
300
310
|
const res = await bridgeFetch(
|
|
@@ -302,7 +312,10 @@ export async function replaceSlides({ presId, base64 }) {
|
|
|
302
312
|
{
|
|
303
313
|
method: "POST",
|
|
304
314
|
headers: { "content-type": "application/json" },
|
|
305
|
-
body: JSON.stringify({ base64 }),
|
|
315
|
+
body: JSON.stringify({ base64, operationId }),
|
|
316
|
+
timeoutMs:
|
|
317
|
+
parseInt(process.env.SLIDEMIND_BRIDGE_MUTATION_TIMEOUT_MS || "1805000", 10) ||
|
|
318
|
+
1805000,
|
|
306
319
|
},
|
|
307
320
|
);
|
|
308
321
|
return res.json().catch(() => ({}));
|
package/src/mcp/server.mjs
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
//
|
|
8
8
|
// Tools:
|
|
9
9
|
// slidemind_list_open_presentations
|
|
10
|
-
// Lists PowerPoint windows
|
|
11
|
-
//
|
|
10
|
+
// Lists PowerPoint windows registered with the local bridge, including
|
|
11
|
+
// retained entries whose task-pane socket is temporarily disconnected.
|
|
12
12
|
//
|
|
13
13
|
// slidemind_push_deck
|
|
14
14
|
// Renders a deck server-side via the add-in and pushes its slides into one
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
//
|
|
17
17
|
// slidemind_search_engine
|
|
18
18
|
// Searches the active corporate design's brand engine (icons/images today,
|
|
19
|
-
// slide engines later) over the bridge
|
|
19
|
+
// slide engines later) over the bridge.
|
|
20
20
|
//
|
|
21
21
|
// slidemind_download_result
|
|
22
22
|
// Fetches a chosen search result's bytes and writes them into a deck's
|
|
@@ -66,12 +66,13 @@ function errorResult(err) {
|
|
|
66
66
|
let text = err && err.message ? err.message : String(err);
|
|
67
67
|
const code = err && err.code;
|
|
68
68
|
// Known transient states are recoverable without rebuilding anything — tell
|
|
69
|
-
// the agent exactly how to resume
|
|
70
|
-
//
|
|
69
|
+
// the agent exactly how to resume if the bridge timed out waiting for a true
|
|
70
|
+
// task-pane reconnect. Normal backgrounded PowerPoint should not reach this.
|
|
71
71
|
if (code === "pane_suspended") {
|
|
72
72
|
text +=
|
|
73
|
-
"\n\nRecoverable:
|
|
74
|
-
"
|
|
73
|
+
"\n\nRecoverable: the render is already done, but the task-pane bridge did " +
|
|
74
|
+
"not reconnect before the wait timeout. Ask the user to click the slidemind " +
|
|
75
|
+
"task pane once so it reconnects, then retry this same call.";
|
|
75
76
|
} else if (code === "engine_auth_expired") {
|
|
76
77
|
text +=
|
|
77
78
|
"\n\nRecoverable: ask the user to click the slidemind task pane for ~1s so " +
|
|
@@ -140,7 +141,7 @@ server.registerTool(
|
|
|
140
141
|
{
|
|
141
142
|
title: "List open PowerPoint windows",
|
|
142
143
|
description:
|
|
143
|
-
"List PowerPoint windows
|
|
144
|
+
"List PowerPoint windows registered with the slidemind local bridge, including retained entries whose task-pane socket is temporarily disconnected. " +
|
|
144
145
|
"Each entry has a stable presId — pass it as `presentation_id` to slidemind_push_deck. " +
|
|
145
146
|
"If the list is empty the user has no PowerPoint open with the add-in loaded.",
|
|
146
147
|
inputSchema: {},
|
|
@@ -202,23 +203,6 @@ server.registerTool(
|
|
|
202
203
|
replace: replace === true,
|
|
203
204
|
slideIds: slide_ids ?? null,
|
|
204
205
|
});
|
|
205
|
-
if (result.paneSuspended) {
|
|
206
|
-
return {
|
|
207
|
-
content: [
|
|
208
|
-
{
|
|
209
|
-
type: "text",
|
|
210
|
-
text:
|
|
211
|
-
"PowerPoint is backgrounded, so the rendered slides couldn't be " +
|
|
212
|
-
"inserted yet. The render succeeded and was saved to " +
|
|
213
|
-
`${result.fallbackFile}. Ask the user to click the slidemind task ` +
|
|
214
|
-
"pane to bring PowerPoint to the foreground, then call " +
|
|
215
|
-
"slidemind_push_deck again — the deck is already built, so this " +
|
|
216
|
-
"just retries the insert.",
|
|
217
|
-
},
|
|
218
|
-
],
|
|
219
|
-
isError: true,
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
206
|
return textResult(result);
|
|
223
207
|
} catch (err) {
|
|
224
208
|
return errorResult(err);
|
|
@@ -232,7 +216,7 @@ server.registerTool(
|
|
|
232
216
|
title: "Search the brand engine for imagery",
|
|
233
217
|
description:
|
|
234
218
|
"Search the active corporate design's brand engine over the bridge (icons/images today; slide engines later). " +
|
|
235
|
-
"
|
|
219
|
+
"Uses the add-in's server-side bridge when available, so it returns the SAME results the in-app design agent sees. " +
|
|
236
220
|
"Returns a searchId and a numbered set of result thumbnails (and a grid image you can view). " +
|
|
237
221
|
"Pick a number, then call slidemind_download_result with the searchId + selection. " +
|
|
238
222
|
"engine_id is optional — omit it to use the corporate design's first image engine.",
|