slidemind 0.1.3 → 0.1.5
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.md +31 -19
- package/README.md +31 -6
- package/bin/slidemind.mjs +442 -18
- package/package.json +2 -6
- package/skills/create-slides/SKILL.md +14 -8
- package/skills/search/SKILL.md +75 -0
- package/src/bridge/build-and-push.mjs +178 -34
- package/src/bridge/bundle.mjs +325 -99
- package/src/bridge/capture.mjs +187 -0
- package/src/bridge/client.mjs +719 -46
- package/src/builders/build.mjs +75 -26
- package/src/builders/lint.mjs +112 -0
- package/src/builders/resolve-skill.mjs +16 -21
- package/src/builders/review-overlay.js +821 -0
- package/src/builders/review.mjs +388 -0
- package/src/lib/reused-slides.mjs +67 -0
- package/src/lib/slide-order.mjs +4 -1
- package/src/mcp/server.mjs +375 -28
- package/src/builders/lint-slides.mjs +0 -796
- package/src/builders/primitives.json +0 -19
package/src/mcp/server.mjs
CHANGED
|
@@ -14,12 +14,26 @@
|
|
|
14
14
|
// Renders a deck server-side via the add-in and pushes its slides into one
|
|
15
15
|
// open PowerPoint window.
|
|
16
16
|
//
|
|
17
|
+
// slidemind_list_engines
|
|
18
|
+
// Lists the live engine catalog (image + slide) for the active corporate
|
|
19
|
+
// design, filterable by type/subtype.
|
|
20
|
+
//
|
|
17
21
|
// slidemind_search_engine
|
|
18
|
-
// Searches the active corporate design's
|
|
19
|
-
//
|
|
22
|
+
// Searches the active corporate design's engine (image/icon OR slide,
|
|
23
|
+
// content/visual intent) over the bridge.
|
|
24
|
+
//
|
|
25
|
+
// slidemind_view_slide
|
|
26
|
+
// Returns a preview image of a chosen slide search result.
|
|
27
|
+
//
|
|
28
|
+
// slidemind_insert_slide
|
|
29
|
+
// Inserts chosen slide search results live into an open PowerPoint window.
|
|
30
|
+
//
|
|
31
|
+
// slidemind_add_slide_to_deck
|
|
32
|
+
// Folds chosen slide search results into a deck as native one-slide .pptx
|
|
33
|
+
// under slides/ (survive re-render, spliced into play order).
|
|
20
34
|
//
|
|
21
35
|
// slidemind_download_result
|
|
22
|
-
// Fetches a chosen search result's bytes and writes them into a deck's
|
|
36
|
+
// Fetches a chosen image search result's bytes and writes them into a deck's
|
|
23
37
|
// assets/ folder for the slide JSX to reference.
|
|
24
38
|
//
|
|
25
39
|
// Everything routes through the add-in's cloud relay at
|
|
@@ -55,6 +69,10 @@ async function loadBundle() {
|
|
|
55
69
|
return import(pathToFileURL(path.join(SRC_DIR, "bridge", "bundle.mjs")).href);
|
|
56
70
|
}
|
|
57
71
|
|
|
72
|
+
async function loadReusedSlides() {
|
|
73
|
+
return import(pathToFileURL(path.join(SRC_DIR, "lib", "reused-slides.mjs")).href);
|
|
74
|
+
}
|
|
75
|
+
|
|
58
76
|
function textResult(value) {
|
|
59
77
|
const text =
|
|
60
78
|
typeof value === "string" ? value : JSON.stringify(value, null, 2);
|
|
@@ -69,9 +87,11 @@ function errorResult(err) {
|
|
|
69
87
|
// task-pane reconnect. Normal backgrounded PowerPoint should not reach this.
|
|
70
88
|
if (code === "pane_suspended") {
|
|
71
89
|
text +=
|
|
72
|
-
"\n\nRecoverable: the
|
|
73
|
-
"
|
|
74
|
-
"
|
|
90
|
+
"\n\nRecoverable: the CLI already waited ~2 minutes for the task pane to " +
|
|
91
|
+
"reconnect. The render itself never needs the pane (it runs server-side) — " +
|
|
92
|
+
"only pushing/engine ops do. Ask the user to click the slidemind task pane " +
|
|
93
|
+
"once, then retry this same call; do NOT ask them to keep PowerPoint " +
|
|
94
|
+
"foregrounded beyond that.";
|
|
75
95
|
} else if (code === "engine_auth_expired") {
|
|
76
96
|
text +=
|
|
77
97
|
"\n\nRecoverable: ask the user to click the slidemind task pane for ~1s so " +
|
|
@@ -134,6 +154,9 @@ const server = new McpServer(
|
|
|
134
154
|
"The CLI may be paired with several add-in environments (local/dev/prod) at once; the list spans " +
|
|
135
155
|
"all of them and each entry is tagged with its env, and routing to the right backend is automatic — " +
|
|
136
156
|
"just pass the presentation_id back. " +
|
|
157
|
+
"Renders run server-side and never need the task pane awake (a suspended/backgrounded PowerPoint " +
|
|
158
|
+
"is fine); only pushing slides and engine search/download touch the pane, and those wait for it " +
|
|
159
|
+
"and retry automatically. " +
|
|
137
160
|
"For brand imagery, slidemind_search_engine then slidemind_download_result.",
|
|
138
161
|
},
|
|
139
162
|
);
|
|
@@ -170,7 +193,10 @@ server.registerTool(
|
|
|
170
193
|
"Use insert_at to place after a specific 0-based slide index. Use replace=true to delete existing slides " +
|
|
171
194
|
"first (destructive). slide_ids restricts the push to a subset of the built deck's slide ids and is for " +
|
|
172
195
|
"the append/insert path only — it CANNOT be combined with replace=true (that combination is rejected, " +
|
|
173
|
-
"because the bridge would otherwise wipe the whole presentation)."
|
|
196
|
+
"because the bridge would otherwise wipe the whole presentation). " +
|
|
197
|
+
"The render itself never needs the task pane awake; if the pane is suspended at push time the CLI waits " +
|
|
198
|
+
"up to ~2 min for it (the user just clicks the pane once) and retries safely. If the result carries " +
|
|
199
|
+
"paneSuspended=true, the rendered .pptx was saved to fallbackFile instead — nothing was lost.",
|
|
174
200
|
inputSchema: {
|
|
175
201
|
deck_path: z
|
|
176
202
|
.string()
|
|
@@ -196,15 +222,46 @@ server.registerTool(
|
|
|
196
222
|
.describe("Optional subset of source slide ids to push. Omit or null = all slides in the built deck."),
|
|
197
223
|
},
|
|
198
224
|
},
|
|
199
|
-
async ({ deck_path, presentation_id, insert_at, replace, slide_ids }) => {
|
|
225
|
+
async ({ deck_path, presentation_id, insert_at, replace, slide_ids }, extra) => {
|
|
200
226
|
try {
|
|
201
227
|
const { buildAndPush } = await loadBuildAndPush();
|
|
228
|
+
// Relay per-slide render progress as MCP progress notifications when the
|
|
229
|
+
// client asked for them (sent a progressToken). Fire-and-forget — a
|
|
230
|
+
// dropped notification must never fail the push.
|
|
231
|
+
const progressToken = extra?._meta?.progressToken;
|
|
232
|
+
const onProgress =
|
|
233
|
+
progressToken === undefined || typeof extra?.sendNotification !== "function"
|
|
234
|
+
? null
|
|
235
|
+
: (p) => {
|
|
236
|
+
const message =
|
|
237
|
+
p.state === "queued"
|
|
238
|
+
? "render queued behind another render"
|
|
239
|
+
: p.state === "restarted"
|
|
240
|
+
? "render restarted (backend replica was replaced)"
|
|
241
|
+
: p.stage === "assembling"
|
|
242
|
+
? "assembling deck"
|
|
243
|
+
: typeof p.total === "number"
|
|
244
|
+
? `rendering slide ${Math.min((p.done ?? 0) + 1, p.total)}/${p.total}`
|
|
245
|
+
: null;
|
|
246
|
+
void extra
|
|
247
|
+
.sendNotification({
|
|
248
|
+
method: "notifications/progress",
|
|
249
|
+
params: {
|
|
250
|
+
progressToken,
|
|
251
|
+
progress: typeof p.done === "number" ? p.done : 0,
|
|
252
|
+
...(typeof p.total === "number" ? { total: p.total } : {}),
|
|
253
|
+
...(message ? { message } : {}),
|
|
254
|
+
},
|
|
255
|
+
})
|
|
256
|
+
.catch(() => {});
|
|
257
|
+
};
|
|
202
258
|
const result = await buildAndPush({
|
|
203
259
|
deckDir: path.resolve(process.cwd(), deck_path),
|
|
204
260
|
presentationId: presentation_id,
|
|
205
261
|
insertAt: insert_at ?? null,
|
|
206
262
|
replace: replace === true,
|
|
207
263
|
slideIds: slide_ids ?? null,
|
|
264
|
+
onProgress,
|
|
208
265
|
});
|
|
209
266
|
return textResult(result);
|
|
210
267
|
} catch (err) {
|
|
@@ -216,38 +273,75 @@ server.registerTool(
|
|
|
216
273
|
server.registerTool(
|
|
217
274
|
"slidemind_search_engine",
|
|
218
275
|
{
|
|
219
|
-
title: "Search
|
|
276
|
+
title: "Search an engine for imagery or slides",
|
|
220
277
|
description:
|
|
221
|
-
"Search the active corporate design's
|
|
222
|
-
"Uses the add-in's server-side bridge
|
|
223
|
-
"Returns a searchId and a numbered set of
|
|
224
|
-
"
|
|
225
|
-
"
|
|
278
|
+
"Search the active corporate design's engine over the bridge — image/icon engines OR slide engines. " +
|
|
279
|
+
"Uses the add-in's server-side bridge, so it returns the SAME results the in-app design agent sees. " +
|
|
280
|
+
"Returns a searchId and a numbered set of results. For image engines (and visual slide search) you also get a grid image you can view; " +
|
|
281
|
+
"for content slide search each result carries the extracted slide `text` plus metadata (slideNumber, presentationTitle, score). " +
|
|
282
|
+
"intent selects the slide search mode: 'content' (text is the signal) or 'visual' (the slide image is the signal); image engines are visual-only. " +
|
|
283
|
+
"Next: reuse an image with slidemind_download_result, view a slide with slidemind_view_slide, insert a slide live with slidemind_insert_slide, or fold a slide into a deck with slidemind_add_slide_to_deck. " +
|
|
284
|
+
"engine_id is optional for image search — omit it to use the corporate design's first image engine; slide search requires engine_id (see slidemind_list_engines). " +
|
|
285
|
+
"presentation_id is OPTIONAL: the engine is not bound to a window, so omit it and the search attaches to any connected window automatically. The window it used is echoed back as `presentation_id` in the result — pass THAT to the follow-up tools (view/insert/add), since the searchId is bound to it.",
|
|
226
286
|
inputSchema: {
|
|
227
287
|
presentation_id: z
|
|
228
288
|
.string()
|
|
229
|
-
.
|
|
289
|
+
.nullable()
|
|
290
|
+
.optional()
|
|
291
|
+
.describe("Optional. Omit to attach to any connected window automatically (the engine isn't window-bound). The window used is returned as `presentation_id`; thread it to slidemind_view_slide / _insert_slide / _add_slide_to_deck."),
|
|
230
292
|
query: z
|
|
231
293
|
.string()
|
|
232
|
-
.describe("Concept query, e.g. 'shield', 'handshake', '
|
|
294
|
+
.describe("Concept query, e.g. 'shield', 'handshake', 'revenue bridge'."),
|
|
233
295
|
engine_id: z
|
|
234
296
|
.string()
|
|
235
297
|
.nullable()
|
|
236
298
|
.optional()
|
|
237
|
-
.describe("Engine id from
|
|
299
|
+
.describe("Engine id from slidemind_list_engines. Omit/null = first image engine (image search only)."),
|
|
300
|
+
intent: z
|
|
301
|
+
.enum(["auto", "content", "visual"])
|
|
302
|
+
.nullable()
|
|
303
|
+
.optional()
|
|
304
|
+
.describe("Slide search mode: 'content' (text), 'visual' (image), or 'auto'. Image engines ignore this."),
|
|
305
|
+
top: z
|
|
306
|
+
.number()
|
|
307
|
+
.int()
|
|
308
|
+
.positive()
|
|
309
|
+
.nullable()
|
|
310
|
+
.optional()
|
|
311
|
+
.describe("Max number of results to return."),
|
|
238
312
|
},
|
|
239
313
|
},
|
|
240
|
-
async ({ presentation_id, query, engine_id }) => {
|
|
314
|
+
async ({ presentation_id, query, engine_id, intent, top }) => {
|
|
241
315
|
try {
|
|
242
316
|
const client = await loadBridgeClient();
|
|
243
317
|
// All engine tools hard-require an open host (the truth lives in the
|
|
244
318
|
// add-in); surface the actionable "open PowerPoint…" message up front.
|
|
245
|
-
await client.requireHost();
|
|
319
|
+
const registry = await client.requireHost();
|
|
320
|
+
// The engine is per-user, not window-bound: attach to any connected window
|
|
321
|
+
// when the caller didn't pin one. The chosen presId is echoed back so the
|
|
322
|
+
// agent threads it to the follow-up tools (the searchId is bound to it).
|
|
323
|
+
const entry = client.pickAnyPresentationEntry(registry, presentation_id ?? null);
|
|
324
|
+
const presId = entry.presId;
|
|
325
|
+
const backendURL = entry.backendURL ?? null;
|
|
246
326
|
let engineId = engine_id ?? null;
|
|
327
|
+
const slideIntent = intent === "content" || intent === "visual";
|
|
328
|
+
// Forward the engine subtype on the wire whenever it's derivable (belt-
|
|
329
|
+
// and-suspenders against an add-in regression where a missing subtype no
|
|
330
|
+
// longer defaulted to "image"): an explicit slide intent targets a slide
|
|
331
|
+
// engine; the pickImageEngine default branch is an image engine. When the
|
|
332
|
+
// subtype is unknown we omit it and let the add-in resolve it.
|
|
333
|
+
let subtype = slideIntent ? "slide" : null;
|
|
247
334
|
if (!engineId) {
|
|
335
|
+
if (slideIntent) {
|
|
336
|
+
return errorResult(
|
|
337
|
+
new Error(
|
|
338
|
+
"slide search requires engine_id. Call slidemind_list_engines (type='search', subtype='slide') to find one.",
|
|
339
|
+
),
|
|
340
|
+
);
|
|
341
|
+
}
|
|
248
342
|
const { materializeBundle, readEnginesFromDir, pickImageEngine } =
|
|
249
343
|
await loadBundle();
|
|
250
|
-
const mat = await materializeBundle(
|
|
344
|
+
const mat = await materializeBundle(presId, { backendURL });
|
|
251
345
|
const pick = pickImageEngine(readEnginesFromDir(mat.dir));
|
|
252
346
|
if (!pick) {
|
|
253
347
|
return errorResult(
|
|
@@ -257,27 +351,55 @@ server.registerTool(
|
|
|
257
351
|
);
|
|
258
352
|
}
|
|
259
353
|
engineId = pick.id;
|
|
354
|
+
subtype = "image";
|
|
260
355
|
}
|
|
261
|
-
|
|
262
|
-
|
|
356
|
+
// Window-agnostic: tries the picked pane, then fails over to any other
|
|
357
|
+
// live window on the same backend if it's suspended/frozen. res.presId is
|
|
358
|
+
// the window that actually answered — echo THAT back (the searchId is
|
|
359
|
+
// bound to it) so follow-up view/insert/add target the right pane.
|
|
360
|
+
const res = await client.engineSearchAnyPane({
|
|
361
|
+
registry,
|
|
362
|
+
explicitPresId: presentation_id ?? null,
|
|
263
363
|
engineId,
|
|
264
364
|
query,
|
|
365
|
+
subtype,
|
|
366
|
+
intent: intent ?? null,
|
|
367
|
+
top: top ?? null,
|
|
265
368
|
});
|
|
369
|
+
const ranPresId = res?.presId ?? presId;
|
|
266
370
|
const content = [];
|
|
267
371
|
const grid = imageContentFromDataUri(res?.gridDataUri);
|
|
268
372
|
if (grid) content.push(grid);
|
|
373
|
+
// Prefer the new `results` array (image + slide engines); fall back to the
|
|
374
|
+
// legacy `thumbnails` shape older add-ins still return for image engines.
|
|
375
|
+
const rawResults = Array.isArray(res?.results)
|
|
376
|
+
? res.results
|
|
377
|
+
: (res?.thumbnails ?? []).map((t, i) => ({ selection: i + 1, title: t.title }));
|
|
378
|
+
const results = rawResults.map((r, i) => ({
|
|
379
|
+
selection: r.selection ?? i + 1,
|
|
380
|
+
title: r.title ?? null,
|
|
381
|
+
...(typeof r.score === "number" ? { score: r.score } : {}),
|
|
382
|
+
...(r.kind ? { kind: r.kind } : {}),
|
|
383
|
+
...(typeof r.text === "string" ? { text: r.text } : {}),
|
|
384
|
+
...(typeof r.slideNumber === "number" ? { slideNumber: r.slideNumber } : {}),
|
|
385
|
+
...(r.presentationTitle ? { presentationTitle: r.presentationTitle } : {}),
|
|
386
|
+
}));
|
|
269
387
|
content.push({
|
|
270
388
|
type: "text",
|
|
271
389
|
text: JSON.stringify(
|
|
272
390
|
{
|
|
273
391
|
ok: res?.ok ?? false,
|
|
392
|
+
// The window the search ran on (auto-attached when the caller didn't
|
|
393
|
+
// pin one; may have failed over from the picked pane). The searchId
|
|
394
|
+
// is bound to it — pass this presentation_id to slidemind_view_slide
|
|
395
|
+
// / _insert_slide / _add_slide_to_deck.
|
|
396
|
+
presentation_id: ranPresId,
|
|
274
397
|
engineId,
|
|
398
|
+
subtype: res?.subtype ?? null,
|
|
399
|
+
intent: res?.intent ?? null,
|
|
275
400
|
searchId: res?.searchId ?? null,
|
|
276
|
-
resultCount: res?.resultCount ??
|
|
277
|
-
results
|
|
278
|
-
selection: i + 1,
|
|
279
|
-
title: t.title ?? null,
|
|
280
|
-
})),
|
|
401
|
+
resultCount: res?.resultCount ?? results.length,
|
|
402
|
+
results,
|
|
281
403
|
},
|
|
282
404
|
null,
|
|
283
405
|
2,
|
|
@@ -290,6 +412,231 @@ server.registerTool(
|
|
|
290
412
|
},
|
|
291
413
|
);
|
|
292
414
|
|
|
415
|
+
server.registerTool(
|
|
416
|
+
"slidemind_list_engines",
|
|
417
|
+
{
|
|
418
|
+
title: "List available engines",
|
|
419
|
+
description:
|
|
420
|
+
"List the LIVE engine catalog for the active corporate design over the bridge (not just the bundle's curated set). " +
|
|
421
|
+
"Spans every paired add-in environment; each engine carries an `env` tag. " +
|
|
422
|
+
"Filter by `type` (collection|search) and/or `subtype` (image|slide|template|content). " +
|
|
423
|
+
"Use the returned engine id with slidemind_search_engine (slide search requires one).",
|
|
424
|
+
inputSchema: {
|
|
425
|
+
presentation_id: z
|
|
426
|
+
.string()
|
|
427
|
+
.nullable()
|
|
428
|
+
.optional()
|
|
429
|
+
.describe("Optional presId — engines are backend-scoped, so this is informational only."),
|
|
430
|
+
type: z
|
|
431
|
+
.enum(["collection", "search"])
|
|
432
|
+
.nullable()
|
|
433
|
+
.optional()
|
|
434
|
+
.describe("Filter by engine type."),
|
|
435
|
+
subtype: z
|
|
436
|
+
.enum(["image", "slide", "template", "content"])
|
|
437
|
+
.nullable()
|
|
438
|
+
.optional()
|
|
439
|
+
.describe("Filter by engine subtype."),
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
async ({ type, subtype }) => {
|
|
443
|
+
try {
|
|
444
|
+
const client = await loadBridgeClient();
|
|
445
|
+
const { engines } = await client.listEngines({
|
|
446
|
+
type: type ?? null,
|
|
447
|
+
subtype: subtype ?? null,
|
|
448
|
+
});
|
|
449
|
+
return textResult({ engines });
|
|
450
|
+
} catch (err) {
|
|
451
|
+
return errorResult(err);
|
|
452
|
+
}
|
|
453
|
+
},
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
server.registerTool(
|
|
457
|
+
"slidemind_view_slide",
|
|
458
|
+
{
|
|
459
|
+
title: "View a slide search result",
|
|
460
|
+
description:
|
|
461
|
+
"Fetch a chosen slide search result's preview image so you can see it before reusing it. " +
|
|
462
|
+
"Pass the searchId from slidemind_search_engine and the 1-based selection number. " +
|
|
463
|
+
"Returns an image content block. Then reuse with slidemind_insert_slide (live) or slidemind_add_slide_to_deck (into a deck).",
|
|
464
|
+
inputSchema: {
|
|
465
|
+
presentation_id: z
|
|
466
|
+
.string()
|
|
467
|
+
.describe("The presentation_id returned by slidemind_search_engine (the window the search ran on). The searchId is bound to that window, so this must be exactly that presentation_id."),
|
|
468
|
+
search_id: z
|
|
469
|
+
.number()
|
|
470
|
+
.int()
|
|
471
|
+
.describe("searchId returned by slidemind_search_engine."),
|
|
472
|
+
selection: z
|
|
473
|
+
.number()
|
|
474
|
+
.int()
|
|
475
|
+
.positive()
|
|
476
|
+
.describe("1-based result number to view."),
|
|
477
|
+
},
|
|
478
|
+
},
|
|
479
|
+
async ({ presentation_id, search_id, selection }) => {
|
|
480
|
+
try {
|
|
481
|
+
const client = await loadBridgeClient();
|
|
482
|
+
await client.requireHost();
|
|
483
|
+
const res = await client.viewSlide({
|
|
484
|
+
presId: presentation_id,
|
|
485
|
+
searchId: search_id,
|
|
486
|
+
selection,
|
|
487
|
+
});
|
|
488
|
+
if (!res?.ok || !res.dataUri) {
|
|
489
|
+
return errorResult(new Error(res?.error ?? "slide view returned no image"));
|
|
490
|
+
}
|
|
491
|
+
const image = imageContentFromDataUri(res.dataUri);
|
|
492
|
+
if (!image) {
|
|
493
|
+
return errorResult(new Error("slide view returned an unparseable data URI"));
|
|
494
|
+
}
|
|
495
|
+
return { content: [image] };
|
|
496
|
+
} catch (err) {
|
|
497
|
+
return errorResult(err);
|
|
498
|
+
}
|
|
499
|
+
},
|
|
500
|
+
);
|
|
501
|
+
|
|
502
|
+
server.registerTool(
|
|
503
|
+
"slidemind_insert_slide",
|
|
504
|
+
{
|
|
505
|
+
title: "Insert reused slide(s) live into PowerPoint",
|
|
506
|
+
description:
|
|
507
|
+
"Insert one or more chosen slide search results directly into an open PowerPoint window (the 'reuse → insert live' path). " +
|
|
508
|
+
"Pass the searchId from slidemind_search_engine and the 1-based selection number(s). " +
|
|
509
|
+
"By default the slides are appended; use `at` to insert after a specific 0-based slide index. " +
|
|
510
|
+
"To instead fold reused slides into a deck you are authoring, use slidemind_add_slide_to_deck.",
|
|
511
|
+
inputSchema: {
|
|
512
|
+
presentation_id: z
|
|
513
|
+
.string()
|
|
514
|
+
.describe("The presentation_id returned by slidemind_search_engine (the window the search ran on). The searchId is bound to that window, so this must be exactly that presentation_id."),
|
|
515
|
+
search_id: z
|
|
516
|
+
.number()
|
|
517
|
+
.int()
|
|
518
|
+
.describe("searchId returned by slidemind_search_engine."),
|
|
519
|
+
selections: z
|
|
520
|
+
.array(z.number().int().positive())
|
|
521
|
+
.min(1)
|
|
522
|
+
.describe("1-based result number(s) to insert."),
|
|
523
|
+
at: z
|
|
524
|
+
.number()
|
|
525
|
+
.int()
|
|
526
|
+
.nonnegative()
|
|
527
|
+
.nullable()
|
|
528
|
+
.optional()
|
|
529
|
+
.describe("0-based slide index to insert after. Omit or null = append at end."),
|
|
530
|
+
},
|
|
531
|
+
},
|
|
532
|
+
async ({ presentation_id, search_id, selections, at }) => {
|
|
533
|
+
try {
|
|
534
|
+
const client = await loadBridgeClient();
|
|
535
|
+
await client.requireHost();
|
|
536
|
+
const res = await client.insertSlideLive({
|
|
537
|
+
presId: presentation_id,
|
|
538
|
+
searchId: search_id,
|
|
539
|
+
selections,
|
|
540
|
+
at: at ?? null,
|
|
541
|
+
});
|
|
542
|
+
if (!res?.ok) {
|
|
543
|
+
return errorResult(new Error(res?.error ?? "insert slide failed"));
|
|
544
|
+
}
|
|
545
|
+
return textResult({
|
|
546
|
+
ok: true,
|
|
547
|
+
insertedCount: res.insertedCount ?? selections.length,
|
|
548
|
+
insertedSlideIds: res.insertedSlideIds ?? [],
|
|
549
|
+
});
|
|
550
|
+
} catch (err) {
|
|
551
|
+
return errorResult(err);
|
|
552
|
+
}
|
|
553
|
+
},
|
|
554
|
+
);
|
|
555
|
+
|
|
556
|
+
server.registerTool(
|
|
557
|
+
"slidemind_add_slide_to_deck",
|
|
558
|
+
{
|
|
559
|
+
title: "Fold reused slide(s) into a deck",
|
|
560
|
+
description:
|
|
561
|
+
"Fold one or more chosen slide search results into a deck as native one-slide .pptx files under <deck>/slides/ (the 'reuse → fold into a deck' path). " +
|
|
562
|
+
"These native slides survive re-render: every slidemind_push_deck / build-pptx re-includes them, spliced into play order by their numeric filename prefix alongside the deck's .jsx slides. " +
|
|
563
|
+
"Pass the searchId from slidemind_search_engine and the 1-based selection number(s). " +
|
|
564
|
+
"By default the slides are appended after the last numeric prefix; `at` sets the numeric prefix the first added slide takes. " +
|
|
565
|
+
"To instead insert directly into an open window, use slidemind_insert_slide.",
|
|
566
|
+
inputSchema: {
|
|
567
|
+
presentation_id: z
|
|
568
|
+
.string()
|
|
569
|
+
.describe("The presentation_id returned by slidemind_search_engine (the window the search ran on). The searchId is bound to that window, so this must be exactly that presentation_id."),
|
|
570
|
+
deck_path: z
|
|
571
|
+
.string()
|
|
572
|
+
.describe("Path to the deck folder (slides are written under <deck>/slides/). Absolute or relative to CWD."),
|
|
573
|
+
search_id: z
|
|
574
|
+
.number()
|
|
575
|
+
.int()
|
|
576
|
+
.describe("searchId returned by slidemind_search_engine."),
|
|
577
|
+
selections: z
|
|
578
|
+
.array(z.number().int().positive())
|
|
579
|
+
.min(1)
|
|
580
|
+
.describe("1-based result number(s) to fold into the deck."),
|
|
581
|
+
at: z
|
|
582
|
+
.number()
|
|
583
|
+
.int()
|
|
584
|
+
.nonnegative()
|
|
585
|
+
.nullable()
|
|
586
|
+
.optional()
|
|
587
|
+
.describe("Numeric prefix the first added slide takes. Omit or null = append after the last existing prefix."),
|
|
588
|
+
},
|
|
589
|
+
},
|
|
590
|
+
async ({ presentation_id, deck_path, search_id, selections, at }) => {
|
|
591
|
+
try {
|
|
592
|
+
const client = await loadBridgeClient();
|
|
593
|
+
await client.requireHost();
|
|
594
|
+
const deckDir = path.resolve(process.cwd(), deck_path);
|
|
595
|
+
const slidesDir = path.join(deckDir, "slides");
|
|
596
|
+
fs.mkdirSync(slidesDir, { recursive: true });
|
|
597
|
+
|
|
598
|
+
// Extract each picked slide's bytes, then let the shared helper compute
|
|
599
|
+
// the play-order filenames — the SAME prefix math the CLI's add-slide
|
|
600
|
+
// uses (src/lib/reused-slides.mjs), so the two paths never diverge.
|
|
601
|
+
const { reusedSlideTargets } = await loadReusedSlides();
|
|
602
|
+
const extracted = [];
|
|
603
|
+
for (const selection of selections) {
|
|
604
|
+
const res = await client.extractSlide({
|
|
605
|
+
presId: presentation_id,
|
|
606
|
+
searchId: search_id,
|
|
607
|
+
selection,
|
|
608
|
+
});
|
|
609
|
+
if (!res?.ok || !res.base64) {
|
|
610
|
+
return errorResult(
|
|
611
|
+
new Error(
|
|
612
|
+
`add slide failed for selection ${selection}: ${res?.error ?? "no slide bytes returned"}`,
|
|
613
|
+
),
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
extracted.push({ selection, slideId: res.slideId ?? null, base64: res.base64 });
|
|
617
|
+
}
|
|
618
|
+
const existing = fs
|
|
619
|
+
.readdirSync(slidesDir)
|
|
620
|
+
.filter((f) => f.endsWith(".jsx") || f.endsWith(".pptx"));
|
|
621
|
+
const targets = reusedSlideTargets(existing, extracted, at);
|
|
622
|
+
const written = extracted.map((slide, i) => {
|
|
623
|
+
const { filename } = targets[i];
|
|
624
|
+
const target = path.join(slidesDir, filename);
|
|
625
|
+
fs.writeFileSync(target, Buffer.from(slide.base64, "base64"));
|
|
626
|
+
return {
|
|
627
|
+
selection: slide.selection,
|
|
628
|
+
filename,
|
|
629
|
+
reference: `./slides/${filename}`,
|
|
630
|
+
path: target,
|
|
631
|
+
};
|
|
632
|
+
});
|
|
633
|
+
return textResult({ ok: true, deckDir, written });
|
|
634
|
+
} catch (err) {
|
|
635
|
+
return errorResult(err);
|
|
636
|
+
}
|
|
637
|
+
},
|
|
638
|
+
);
|
|
639
|
+
|
|
293
640
|
// Fetch one chosen result over the bridge and write it into the deck's
|
|
294
641
|
// assets/. Returns the written reference plus a `recolor` descriptor so the
|
|
295
642
|
// agent never has to open the SVG to learn how it recolours. Throws on a failed
|
|
@@ -347,7 +694,7 @@ server.registerTool(
|
|
|
347
694
|
inputSchema: {
|
|
348
695
|
presentation_id: z
|
|
349
696
|
.string()
|
|
350
|
-
.describe("
|
|
697
|
+
.describe("The presentation_id returned by slidemind_search_engine (the window the search ran on). The searchId is bound to that window, so this must be exactly that presentation_id."),
|
|
351
698
|
deck_path: z
|
|
352
699
|
.string()
|
|
353
700
|
.describe("Path to the deck folder (assets are written under <deck>/assets/)."),
|