slidemind 0.1.3 → 0.1.4
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 +437 -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 +628 -47
- 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 +368 -27
- package/src/builders/lint-slides.mjs +0 -796
- package/src/builders/primitives.json +0 -19
package/bin/slidemind.mjs
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// (`install` is a back-compat alias)
|
|
9
9
|
// slidemind new <name> scaffold ./decks/<name>/{plan.md,slides/}
|
|
10
10
|
// slidemind build <deck> build self-contained HTML deck
|
|
11
|
+
// slidemind review <deck> spatial preview-review gate (blocks for approve/comments)
|
|
11
12
|
// slidemind build-pptx <deck> render the deck as native .pptx (via the add-in)
|
|
12
13
|
// slidemind uninstall --for <target>
|
|
13
14
|
// slidemind update [--for <target>] [--ref <version>]
|
|
@@ -44,12 +45,34 @@ function printHelp() {
|
|
|
44
45
|
`Usage:\n` +
|
|
45
46
|
` slidemind new <name> Scaffold ./decks/<name>/\n` +
|
|
46
47
|
` slidemind build <deck> Build self-contained HTML\n` +
|
|
48
|
+
` slidemind review <deck> Open a spatial preview review; blocks until the user\n` +
|
|
49
|
+
` approves or leaves comments (host-free)\n` +
|
|
50
|
+
` --no-open don't auto-open the browser\n` +
|
|
51
|
+
` --port <n> bind a fixed port (default: ephemeral)\n` +
|
|
47
52
|
` slidemind build-pptx <deck> Export native .pptx via the add-in (requires an open host)\n` +
|
|
48
53
|
` --to <presId> target a specific window (required if >1 open)\n` +
|
|
49
54
|
` --out <path> output path (default <deck>/<deck>.pptx)\n` +
|
|
50
|
-
` slidemind search "<query>" Search
|
|
51
|
-
` --engine <id> engine to search (default: first image engine)\n` +
|
|
55
|
+
` slidemind search "<query>" Search an engine for images or slides (requires an open host)\n` +
|
|
56
|
+
` --engine <id> engine to search (default: first image engine; required for slides)\n` +
|
|
57
|
+
` --intent <i> auto | content | visual (slide engines)\n` +
|
|
58
|
+
` --top <N> max results\n` +
|
|
52
59
|
` --to <presId> target a specific window (required if >1 open)\n` +
|
|
60
|
+
` --json print the raw result payload\n` +
|
|
61
|
+
` slidemind ls engines List the live engine catalog for the active corporate design\n` +
|
|
62
|
+
` --type <t> collection | search\n` +
|
|
63
|
+
` --subtype <s> image | slide | template | content\n` +
|
|
64
|
+
` --json print the raw engine list\n` +
|
|
65
|
+
` slidemind insert-slide [opts] Insert reused slide search results live into a window (requires a host)\n` +
|
|
66
|
+
` --search <id> searchId from 'slidemind search'\n` +
|
|
67
|
+
` --pick <n,..> 1-based selection number(s)\n` +
|
|
68
|
+
` --to <presId> target window (required if >1 open)\n` +
|
|
69
|
+
` --at <N> insert after slide N (0-based); default = append\n` +
|
|
70
|
+
` slidemind add-slide [opts] Fold reused slide search results into a deck as native .pptx\n` +
|
|
71
|
+
` --search <id> searchId from 'slidemind search'\n` +
|
|
72
|
+
` --pick <n,..> 1-based selection number(s)\n` +
|
|
73
|
+
` --deck <deck> deck to add the slides to\n` +
|
|
74
|
+
` --to <presId> target the window the search ran on (required if >1 open)\n` +
|
|
75
|
+
` --at <N> numeric prefix to start at (default: append)\n` +
|
|
53
76
|
` slidemind download-bundle [deck] Materialize the active corporate design's bundle locally\n` +
|
|
54
77
|
` --to <presId> target a specific window\n` +
|
|
55
78
|
` --force re-download even if the cached bundle is current\n` +
|
|
@@ -65,8 +88,8 @@ function printHelp() {
|
|
|
65
88
|
` slidemind whoami Show every paired environment (* = default)\n` +
|
|
66
89
|
` slidemind use <url|env> Set the default environment\n` +
|
|
67
90
|
` slidemind logout [url|env] Unpair one environment (--all to unpair every one)\n` +
|
|
68
|
-
` slidemind init --for <t> Set up slidemind in a harness (registers the MCP server + skill)\n` +
|
|
69
|
-
` slidemind uninstall --for <t> Remove a previously-installed skill\n` +
|
|
91
|
+
` slidemind init --for <t> Set up slidemind in a harness (registers the MCP server + skill; clears the bundle cache)\n` +
|
|
92
|
+
` slidemind uninstall --for <t> Remove a previously-installed skill (clears the bundle cache)\n` +
|
|
70
93
|
` slidemind update [--for <t>] Fetch the latest release and redeploy\n` +
|
|
71
94
|
` --for <t> harness to redeploy into (default: all)\n` +
|
|
72
95
|
` --ref <ver> pin to a release version (e.g. v0.1.0)\n` +
|
|
@@ -144,6 +167,42 @@ async function cmdBuild(rest) {
|
|
|
144
167
|
]);
|
|
145
168
|
}
|
|
146
169
|
|
|
170
|
+
async function cmdReview(rest) {
|
|
171
|
+
const { deck, passthrough } = splitDeckArg(rest, "review");
|
|
172
|
+
await runNode(path.join(BUILDERS_DIR, "review.mjs"), [
|
|
173
|
+
"--deck",
|
|
174
|
+
resolveDeckArg(deck),
|
|
175
|
+
...passthrough,
|
|
176
|
+
]);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Live render progress for build-pptx/push (the job protocol streams it; the
|
|
180
|
+
// sync fallback emits nothing). One stderr line per state change — works in
|
|
181
|
+
// plain logs and TTYs alike.
|
|
182
|
+
function renderProgressPrinter() {
|
|
183
|
+
let last = "";
|
|
184
|
+
let queuedAt = null;
|
|
185
|
+
return (p) => {
|
|
186
|
+
let line = null;
|
|
187
|
+
if (p.state === "queued") {
|
|
188
|
+
queuedAt ??= Date.now();
|
|
189
|
+
if (Date.now() - queuedAt > 5000) {
|
|
190
|
+
line = " render queued behind another render…";
|
|
191
|
+
}
|
|
192
|
+
} else if (p.state === "restarted") {
|
|
193
|
+
line = " render restarted (backend replica was replaced)…";
|
|
194
|
+
} else if (p.stage === "assembling") {
|
|
195
|
+
line = " assembling deck…";
|
|
196
|
+
} else if (p.stage === "rendering" && typeof p.total === "number") {
|
|
197
|
+
line = ` rendering slide ${Math.min((p.done ?? 0) + 1, p.total)}/${p.total}…`;
|
|
198
|
+
}
|
|
199
|
+
if (line && line !== last) {
|
|
200
|
+
last = line;
|
|
201
|
+
process.stderr.write(`${line}\n`);
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
147
206
|
async function cmdBuildPptx(rest) {
|
|
148
207
|
const { deck, passthrough } = splitDeckArg(rest, "build-pptx");
|
|
149
208
|
let to = null;
|
|
@@ -175,6 +234,7 @@ async function cmdBuildPptx(rest) {
|
|
|
175
234
|
deckDir,
|
|
176
235
|
presentationId: to,
|
|
177
236
|
outPath,
|
|
237
|
+
onProgress: renderProgressPrinter(),
|
|
178
238
|
});
|
|
179
239
|
process.stdout.write(
|
|
180
240
|
`Wrote ${result.outPath} (${result.slideCount ?? "?"} slides) via the slidemind add-in\n`,
|
|
@@ -191,6 +251,8 @@ async function cmdSearch(rest) {
|
|
|
191
251
|
let asJson = false;
|
|
192
252
|
let to = null;
|
|
193
253
|
let engineId = null;
|
|
254
|
+
let intent = null;
|
|
255
|
+
let top = null;
|
|
194
256
|
const queryParts = [];
|
|
195
257
|
for (let i = 0; i < rest.length; i++) {
|
|
196
258
|
const a = rest[i];
|
|
@@ -199,9 +261,22 @@ async function cmdSearch(rest) {
|
|
|
199
261
|
} else if (a === "--to") {
|
|
200
262
|
to = rest[++i];
|
|
201
263
|
if (!to) fail("--to expects a presId");
|
|
202
|
-
} else if (a === "--engine" || a === "-e") {
|
|
264
|
+
} else if (a === "--engine" || a === "-e" || a === "--engineId") {
|
|
203
265
|
engineId = rest[++i];
|
|
204
266
|
if (!engineId) fail("--engine expects an engineId");
|
|
267
|
+
} else if (a === "--intent") {
|
|
268
|
+
intent = rest[++i];
|
|
269
|
+
if (!intent) fail("--intent expects auto|content|visual");
|
|
270
|
+
if (!["auto", "content", "visual"].includes(intent)) {
|
|
271
|
+
fail(`--intent expects auto|content|visual, got '${intent}'`);
|
|
272
|
+
}
|
|
273
|
+
} else if (a === "--top") {
|
|
274
|
+
const next = rest[++i];
|
|
275
|
+
const n = parseInt(next, 10);
|
|
276
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
277
|
+
fail(`--top expects a positive integer, got '${next}'`);
|
|
278
|
+
}
|
|
279
|
+
top = n;
|
|
205
280
|
} else if (a.startsWith("-")) {
|
|
206
281
|
fail(`unknown flag: ${a}`);
|
|
207
282
|
} else {
|
|
@@ -211,13 +286,12 @@ async function cmdSearch(rest) {
|
|
|
211
286
|
const query = queryParts.join(" ").trim();
|
|
212
287
|
if (!query) {
|
|
213
288
|
fail(
|
|
214
|
-
`usage: slidemind search "<query>" [--engine <id>] [--to <presId>] [--json]`,
|
|
289
|
+
`usage: slidemind search "<query>" [--engine <id>] [--intent auto|content|visual] [--top <N>] [--to <presId>] [--json]`,
|
|
215
290
|
);
|
|
216
291
|
}
|
|
217
292
|
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
// corporate design's bundle.
|
|
293
|
+
// Engine search runs against the add-in's live engine over the bridge — an
|
|
294
|
+
// open host is required.
|
|
221
295
|
const client = await import(
|
|
222
296
|
pathToFileURL(path.join(SRC_DIR, "bridge", "client.mjs")).href
|
|
223
297
|
);
|
|
@@ -225,11 +299,32 @@ async function cmdSearch(rest) {
|
|
|
225
299
|
pathToFileURL(path.join(SRC_DIR, "bridge", "bundle.mjs")).href
|
|
226
300
|
);
|
|
227
301
|
const registry = await client.requireHost();
|
|
228
|
-
|
|
302
|
+
// The engine is per-user, not window-bound — so search just attaches to any
|
|
303
|
+
// connected window (preferring a live one). No "which window?" prompt; pass
|
|
304
|
+
// --to only to pin a specific one. The chosen window is printed below so
|
|
305
|
+
// follow-up insert/add-slide can target the same pane the searchId lives on.
|
|
306
|
+
const entry = client.pickAnyPresentationEntry(registry, to);
|
|
229
307
|
const presId = entry.presId;
|
|
230
308
|
const backendURL = entry.backendURL ?? null;
|
|
231
309
|
|
|
310
|
+
// Slide search needs an explicit engine — the bundle's image-engine default
|
|
311
|
+
// only applies to image/visual intent. content/visual on a slide engine is
|
|
312
|
+
// the caller's choice via --engine.
|
|
313
|
+
const slideIntent = intent === "content" || intent === "visual";
|
|
314
|
+
// Thread the engine subtype on the wire whenever the CLI can determine it:
|
|
315
|
+
// - the pickImageEngine default branch is, by construction, an image engine;
|
|
316
|
+
// - an explicit slide intent (--intent content|visual) targets a slide
|
|
317
|
+
// engine.
|
|
318
|
+
// This is belt-and-suspenders for an add-in regression where a missing
|
|
319
|
+
// subtype no longer defaulted to "image"; when the subtype is unknown we omit
|
|
320
|
+
// it and let the add-in resolve it from the snapshot.
|
|
321
|
+
let subtype = slideIntent ? "slide" : null;
|
|
232
322
|
if (!engineId) {
|
|
323
|
+
if (slideIntent) {
|
|
324
|
+
fail(
|
|
325
|
+
"slide search requires --engine <id>. Run 'slidemind ls engines --type search --subtype slide' to find one.",
|
|
326
|
+
);
|
|
327
|
+
}
|
|
233
328
|
const mat = await bundle.materializeBundle(presId, { backendURL });
|
|
234
329
|
const pick = bundle.pickImageEngine(bundle.readEnginesFromDir(mat.dir));
|
|
235
330
|
if (!pick) {
|
|
@@ -238,9 +333,18 @@ async function cmdSearch(rest) {
|
|
|
238
333
|
);
|
|
239
334
|
}
|
|
240
335
|
engineId = pick.id;
|
|
336
|
+
subtype = "image";
|
|
241
337
|
}
|
|
242
338
|
|
|
243
|
-
const res = await client.engineSearch({
|
|
339
|
+
const res = await client.engineSearch({
|
|
340
|
+
presId,
|
|
341
|
+
engineId,
|
|
342
|
+
query,
|
|
343
|
+
subtype,
|
|
344
|
+
intent: intent ?? null,
|
|
345
|
+
top: top ?? null,
|
|
346
|
+
backendURL,
|
|
347
|
+
});
|
|
244
348
|
if (asJson) {
|
|
245
349
|
process.stdout.write(`${JSON.stringify(res, null, 2)}\n`);
|
|
246
350
|
return;
|
|
@@ -249,15 +353,272 @@ async function cmdSearch(rest) {
|
|
|
249
353
|
process.stderr.write(`search failed: ${res?.error ?? "unknown error"}\n`);
|
|
250
354
|
return;
|
|
251
355
|
}
|
|
252
|
-
|
|
356
|
+
// Prefer the new `results` array (image + slide engines); fall back to the
|
|
357
|
+
// legacy `thumbnails` shape older add-ins still return for image engines.
|
|
358
|
+
const results = Array.isArray(res.results)
|
|
359
|
+
? res.results
|
|
360
|
+
: (res.thumbnails ?? []).map((t, i) => ({ selection: i + 1, title: t.title }));
|
|
361
|
+
const count = res.resultCount ?? results.length;
|
|
362
|
+
process.stdout.write(
|
|
363
|
+
`searchId ${res.searchId} — ${count} result(s) from engine ${engineId}` +
|
|
364
|
+
(res.intent ? ` (${res.intent})` : "") +
|
|
365
|
+
`\n`,
|
|
366
|
+
);
|
|
367
|
+
const isContent = res.intent === "content";
|
|
368
|
+
results.forEach((r, i) => {
|
|
369
|
+
const sel = r.selection ?? i + 1;
|
|
370
|
+
if (isContent) {
|
|
371
|
+
// Content results: per-result header + the bare extracted text block.
|
|
372
|
+
const score = typeof r.score === "number" ? ` · ${r.score.toFixed(3)}` : "";
|
|
373
|
+
process.stdout.write(` ${sel}. ${r.title ?? "(untitled)"}${score}\n`);
|
|
374
|
+
if (r.text) {
|
|
375
|
+
for (const line of String(r.text).split("\n")) {
|
|
376
|
+
process.stdout.write(` ${line}\n`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
} else {
|
|
380
|
+
// Visual / image results: the numbered list.
|
|
381
|
+
process.stdout.write(` ${sel}. ${r.title ?? "(untitled)"}\n`);
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
// Tailor the next-step hint to the result kind: slide verbs (insert-slide /
|
|
385
|
+
// add-slide) for slide results, the image-download hint otherwise. The result
|
|
386
|
+
// subtype is authoritative; fall back to a per-result `kind` for older
|
|
387
|
+
// add-ins that don't echo a top-level subtype.
|
|
388
|
+
const isSlideResults =
|
|
389
|
+
res.subtype === "slide" || results.some((r) => r.kind === "slide");
|
|
390
|
+
const envTag = entry.env ? ` [${entry.env}]` : "";
|
|
391
|
+
if (isSlideResults) {
|
|
392
|
+
// The searchId is bound to the window the search ran on — reuse must target
|
|
393
|
+
// that same window, so spell out its presId in the next-step commands.
|
|
394
|
+
process.stdout.write(
|
|
395
|
+
`searchId ${res.searchId} ran on window ${presId}${envTag} — reuse from THIS window:\n` +
|
|
396
|
+
` slidemind insert-slide --search ${res.searchId} --pick <n> --to ${presId}\n` +
|
|
397
|
+
` slidemind add-slide --search ${res.searchId} --pick <n> --deck <deck> --to ${presId}\n`,
|
|
398
|
+
);
|
|
399
|
+
} else {
|
|
400
|
+
process.stdout.write(
|
|
401
|
+
`Download an image result for a deck via the agent (slidemind_download_result, search_id ${res.searchId}).\n`,
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async function cmdLs(rest) {
|
|
407
|
+
const [sub, ...subRest] = rest;
|
|
408
|
+
if (sub !== "engines") {
|
|
409
|
+
fail("usage: slidemind ls engines [--type collection|search] [--subtype image|slide|template|content] [--json]");
|
|
410
|
+
}
|
|
411
|
+
let type = null;
|
|
412
|
+
let subtype = null;
|
|
413
|
+
let to = null;
|
|
414
|
+
let asJson = false;
|
|
415
|
+
for (let i = 0; i < subRest.length; i++) {
|
|
416
|
+
const a = subRest[i];
|
|
417
|
+
if (a === "--type") {
|
|
418
|
+
type = subRest[++i];
|
|
419
|
+
if (!type) fail("--type expects collection|search");
|
|
420
|
+
if (!["collection", "search"].includes(type)) {
|
|
421
|
+
fail(`--type expects collection|search, got '${type}'`);
|
|
422
|
+
}
|
|
423
|
+
} else if (a === "--subtype") {
|
|
424
|
+
subtype = subRest[++i];
|
|
425
|
+
if (!subtype) fail("--subtype expects image|slide|template|content");
|
|
426
|
+
if (!["image", "slide", "template", "content"].includes(subtype)) {
|
|
427
|
+
fail(`--subtype expects image|slide|template|content, got '${subtype}'`);
|
|
428
|
+
}
|
|
429
|
+
} else if (a === "--to") {
|
|
430
|
+
// Accepted for symmetry with other commands; engines are backend-scoped,
|
|
431
|
+
// not presId-scoped, so it does not change the catalog.
|
|
432
|
+
to = subRest[++i];
|
|
433
|
+
if (!to) fail("--to expects a presId");
|
|
434
|
+
} else if (a === "--json") {
|
|
435
|
+
asJson = true;
|
|
436
|
+
} else if (a.startsWith("-")) {
|
|
437
|
+
fail(`unknown flag: ${a}`);
|
|
438
|
+
} else {
|
|
439
|
+
fail(`unexpected argument: ${a}`);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
void to;
|
|
443
|
+
const client = await import(
|
|
444
|
+
pathToFileURL(path.join(SRC_DIR, "bridge", "client.mjs")).href
|
|
445
|
+
);
|
|
446
|
+
const { engines } = await client.listEngines({ type, subtype });
|
|
447
|
+
if (asJson) {
|
|
448
|
+
process.stdout.write(`${JSON.stringify(engines, null, 2)}\n`);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
if (engines.length === 0) {
|
|
452
|
+
process.stderr.write("No engines available for the active corporate design.\n");
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
// Show the env tag only when more than one environment contributed engines.
|
|
456
|
+
const multiEnv = new Set(engines.map((e) => e.env)).size > 1;
|
|
457
|
+
for (const e of engines) {
|
|
458
|
+
const kind = [e.type, e.subtype].filter(Boolean).join("/");
|
|
459
|
+
const env = multiEnv && e.env ? `[${e.env}] ` : "";
|
|
460
|
+
const desc = e.description ? ` — ${e.description}` : "";
|
|
461
|
+
process.stdout.write(`${env}${e.id} · ${e.label ?? e.id} · ${kind}${desc}\n`);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function parsePicks(raw, flag = "--pick") {
|
|
466
|
+
if (!raw) fail(`${flag} expects a comma-separated list of selection numbers`);
|
|
467
|
+
// Tolerate a trailing/empty segment ("1," or "1,,2") by dropping blank parts
|
|
468
|
+
// rather than turning them into NaN that fails the whole call; dedupe so the
|
|
469
|
+
// same selection isn't extracted twice.
|
|
470
|
+
const segments = raw
|
|
471
|
+
.split(",")
|
|
472
|
+
.map((s) => s.trim())
|
|
473
|
+
.filter((s) => s.length > 0);
|
|
474
|
+
if (segments.length === 0) {
|
|
475
|
+
fail(`${flag} expects a comma-separated list of selection numbers`);
|
|
476
|
+
}
|
|
477
|
+
const picks = [...new Set(segments.map((s) => parseInt(s, 10)))];
|
|
478
|
+
if (picks.some((n) => !Number.isFinite(n) || n <= 0)) {
|
|
479
|
+
fail(`${flag} expects positive selection numbers, got '${raw}'`);
|
|
480
|
+
}
|
|
481
|
+
return picks;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
async function cmdInsertSlide(rest) {
|
|
485
|
+
let searchId = null;
|
|
486
|
+
let picks = null;
|
|
487
|
+
let to = null;
|
|
488
|
+
let at = null;
|
|
489
|
+
for (let i = 0; i < rest.length; i++) {
|
|
490
|
+
const a = rest[i];
|
|
491
|
+
if (a === "--search") {
|
|
492
|
+
const next = rest[++i];
|
|
493
|
+
const n = parseInt(next, 10);
|
|
494
|
+
if (!Number.isFinite(n)) fail(`--search expects a searchId, got '${next}'`);
|
|
495
|
+
searchId = n;
|
|
496
|
+
} else if (a === "--pick") {
|
|
497
|
+
picks = parsePicks(rest[++i]);
|
|
498
|
+
} else if (a === "--to") {
|
|
499
|
+
to = rest[++i];
|
|
500
|
+
if (!to) fail("--to expects a presId");
|
|
501
|
+
} else if (a === "--at") {
|
|
502
|
+
const next = rest[++i];
|
|
503
|
+
const n = parseInt(next, 10);
|
|
504
|
+
if (!Number.isFinite(n) || n < 0) {
|
|
505
|
+
fail(`--at expects a non-negative integer, got '${next}'`);
|
|
506
|
+
}
|
|
507
|
+
at = n;
|
|
508
|
+
} else if (a.startsWith("-")) {
|
|
509
|
+
fail(`unknown flag: ${a}`);
|
|
510
|
+
} else {
|
|
511
|
+
fail(`unexpected argument: ${a}`);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
if (searchId === null || !picks) {
|
|
515
|
+
fail("usage: slidemind insert-slide --search <id> --pick <n[,n,...]> --to <presId> [--at <N>]");
|
|
516
|
+
}
|
|
517
|
+
const client = await import(
|
|
518
|
+
pathToFileURL(path.join(SRC_DIR, "bridge", "client.mjs")).href
|
|
519
|
+
);
|
|
520
|
+
const registry = await client.requireHost();
|
|
521
|
+
const entry = client.pickPresentationEntry(registry, to);
|
|
522
|
+
const res = await client.insertSlideLive({
|
|
523
|
+
presId: entry.presId,
|
|
524
|
+
searchId,
|
|
525
|
+
selections: picks,
|
|
526
|
+
at,
|
|
527
|
+
backendURL: entry.backendURL ?? null,
|
|
528
|
+
});
|
|
529
|
+
if (!res?.ok) {
|
|
530
|
+
fail(`insert-slide failed: ${res?.error ?? "unknown error"}`);
|
|
531
|
+
}
|
|
253
532
|
process.stdout.write(
|
|
254
|
-
`
|
|
533
|
+
`Inserted ${res.insertedCount ?? picks.length} slide(s) into presentation ${entry.presId}\n`,
|
|
255
534
|
);
|
|
256
|
-
|
|
257
|
-
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
async function cmdAddSlide(rest) {
|
|
538
|
+
let searchId = null;
|
|
539
|
+
let picks = null;
|
|
540
|
+
let deck = null;
|
|
541
|
+
let at = null;
|
|
542
|
+
let to = null;
|
|
543
|
+
for (let i = 0; i < rest.length; i++) {
|
|
544
|
+
const a = rest[i];
|
|
545
|
+
if (a === "--search") {
|
|
546
|
+
const next = rest[++i];
|
|
547
|
+
const n = parseInt(next, 10);
|
|
548
|
+
if (!Number.isFinite(n)) fail(`--search expects a searchId, got '${next}'`);
|
|
549
|
+
searchId = n;
|
|
550
|
+
} else if (a === "--pick") {
|
|
551
|
+
picks = parsePicks(rest[++i]);
|
|
552
|
+
} else if (a === "--deck") {
|
|
553
|
+
deck = rest[++i];
|
|
554
|
+
if (!deck) fail("--deck expects a deck name or path");
|
|
555
|
+
} else if (a === "--to") {
|
|
556
|
+
to = rest[++i];
|
|
557
|
+
if (!to) fail("--to expects a presId");
|
|
558
|
+
} else if (a === "--at") {
|
|
559
|
+
const next = rest[++i];
|
|
560
|
+
const n = parseInt(next, 10);
|
|
561
|
+
if (!Number.isFinite(n) || n < 0) {
|
|
562
|
+
fail(`--at expects a non-negative integer, got '${next}'`);
|
|
563
|
+
}
|
|
564
|
+
at = n;
|
|
565
|
+
} else if (a.startsWith("-")) {
|
|
566
|
+
fail(`unknown flag: ${a}`);
|
|
567
|
+
} else {
|
|
568
|
+
fail(`unexpected argument: ${a}`);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
if (searchId === null || !picks || !deck) {
|
|
572
|
+
fail("usage: slidemind add-slide --search <id> --pick <n[,n,...]> --deck <deck> [--to <presId>] [--at <N>]");
|
|
573
|
+
}
|
|
574
|
+
const deckDir = resolveDeckArg(deck);
|
|
575
|
+
const slidesDir = path.join(deckDir, "slides");
|
|
576
|
+
fs.mkdirSync(slidesDir, { recursive: true });
|
|
577
|
+
|
|
578
|
+
const client = await import(
|
|
579
|
+
pathToFileURL(path.join(SRC_DIR, "bridge", "client.mjs")).href
|
|
580
|
+
);
|
|
581
|
+
const registry = await client.requireHost();
|
|
582
|
+
// The searchId is presentation-scoped, so --to targets the window the search
|
|
583
|
+
// ran on. Without it, a registry with >1 open window is a dead-end.
|
|
584
|
+
const entry = client.pickPresentationEntry(registry, to);
|
|
585
|
+
const presId = entry.presId;
|
|
586
|
+
const backendURL = entry.backendURL ?? null;
|
|
587
|
+
|
|
588
|
+
// Extract each picked slide's bytes over the bridge, then let the shared
|
|
589
|
+
// helper compute the play-order filenames (NN-reused-<shortid>.pptx). The
|
|
590
|
+
// prefix math is the single shared unit (src/lib/reused-slides.mjs) so the
|
|
591
|
+
// CLI and the MCP tool never diverge. Existing files are not renumbered.
|
|
592
|
+
const reused = await import(
|
|
593
|
+
pathToFileURL(path.join(SRC_DIR, "lib", "reused-slides.mjs")).href
|
|
594
|
+
);
|
|
595
|
+
const extracted = [];
|
|
596
|
+
for (const pick of picks) {
|
|
597
|
+
const res = await client.extractSlide({
|
|
598
|
+
presId,
|
|
599
|
+
searchId,
|
|
600
|
+
selection: pick,
|
|
601
|
+
backendURL,
|
|
602
|
+
});
|
|
603
|
+
if (!res?.ok || !res.base64) {
|
|
604
|
+
fail(`add-slide failed for selection ${pick}: ${res?.error ?? "no slide bytes returned"}`);
|
|
605
|
+
}
|
|
606
|
+
extracted.push({ selection: pick, slideId: res.slideId ?? null, base64: res.base64 });
|
|
607
|
+
}
|
|
608
|
+
const existing = fs
|
|
609
|
+
.readdirSync(slidesDir)
|
|
610
|
+
.filter((f) => f.endsWith(".jsx") || f.endsWith(".pptx"));
|
|
611
|
+
const targets = reused.reusedSlideTargets(existing, extracted, at);
|
|
612
|
+
const written = [];
|
|
613
|
+
extracted.forEach((slide, i) => {
|
|
614
|
+
const target = path.join(slidesDir, targets[i].filename);
|
|
615
|
+
fs.writeFileSync(target, Buffer.from(slide.base64, "base64"));
|
|
616
|
+
written.push(path.relative(process.cwd(), target));
|
|
258
617
|
});
|
|
259
618
|
process.stdout.write(
|
|
260
|
-
`
|
|
619
|
+
`Added ${written.length} reused slide(s) to ${path.relative(process.cwd(), deckDir)}:\n` +
|
|
620
|
+
written.map((w) => ` ${w}`).join("\n") +
|
|
621
|
+
`\nThey are native .pptx in slides/ — re-run 'slidemind build-pptx ${deck}' or 'slidemind push ${deck}' to render the deck with them spliced in.\n`,
|
|
261
622
|
);
|
|
262
623
|
}
|
|
263
624
|
|
|
@@ -284,8 +645,16 @@ async function cmdDownloadBundle(rest) {
|
|
|
284
645
|
const bundle = await import(
|
|
285
646
|
pathToFileURL(path.join(SRC_DIR, "bridge", "bundle.mjs")).href
|
|
286
647
|
);
|
|
648
|
+
// When a deck is being (re-)bound, its existing bundle ref breaks ties for
|
|
649
|
+
// a presId open on several paired backends (prefer the one whose connected
|
|
650
|
+
// pane has THIS corporate design active).
|
|
651
|
+
const deckRef = positional[0]
|
|
652
|
+
? bundle.readDeckBundleRef(resolveDeckArg(positional[0]))
|
|
653
|
+
: null;
|
|
287
654
|
const registry = await client.requireHost();
|
|
288
|
-
const entry = client.pickPresentationEntry(registry, to
|
|
655
|
+
const entry = client.pickPresentationEntry(registry, to, {
|
|
656
|
+
preferBrandId: deckRef?.presFP ?? null,
|
|
657
|
+
});
|
|
289
658
|
const presId = entry.presId;
|
|
290
659
|
const result = await bundle.materializeBundle(presId, {
|
|
291
660
|
force,
|
|
@@ -303,8 +672,15 @@ async function cmdDownloadBundle(rest) {
|
|
|
303
672
|
});
|
|
304
673
|
}
|
|
305
674
|
process.stdout.write(
|
|
306
|
-
|
|
675
|
+
`Synced bundle for ${result.presFP} (etag ${result.etag ?? "?"}) — ` +
|
|
676
|
+
`${result.downloaded} downloaded, ${result.pruned} pruned\n ${result.dir}\n`,
|
|
307
677
|
);
|
|
678
|
+
if (result.engineUpdated) {
|
|
679
|
+
process.stdout.write(" lint engine updated\n");
|
|
680
|
+
}
|
|
681
|
+
if (result.captureEngineUpdated) {
|
|
682
|
+
process.stdout.write(" capture engine updated\n");
|
|
683
|
+
}
|
|
308
684
|
// Asset downloads are best-effort (a signed URL may have expired); surface any
|
|
309
685
|
// failures so the user knows the local preview bundle is incomplete.
|
|
310
686
|
for (const w of result.warnings ?? []) {
|
|
@@ -543,6 +919,7 @@ async function cmdPush(rest) {
|
|
|
543
919
|
insertAt: at,
|
|
544
920
|
replace,
|
|
545
921
|
slideIds: slides,
|
|
922
|
+
onProgress: renderProgressPrinter(),
|
|
546
923
|
});
|
|
547
924
|
for (const w of result.warnings ?? []) {
|
|
548
925
|
process.stderr.write(` warning: ${w}\n`);
|
|
@@ -631,6 +1008,22 @@ async function cmdInstall(rest) {
|
|
|
631
1008
|
const ui = await loadInstallerUi();
|
|
632
1009
|
ui.banner(readPackageJson().version, "init");
|
|
633
1010
|
await runInstaller(target, "install", rest);
|
|
1011
|
+
|
|
1012
|
+
// Force every design bundle to re-download from the add-in on the next
|
|
1013
|
+
// build/push/download by wiping the materialized cache. init is the
|
|
1014
|
+
// deliberate refresh point (and `slidemind update` redeploys through it), so
|
|
1015
|
+
// a stale binary or an orphaned per-presentation dir never outlives a re-init.
|
|
1016
|
+
const bundle = await import(
|
|
1017
|
+
pathToFileURL(path.join(SRC_DIR, "bridge", "bundle.mjs")).href
|
|
1018
|
+
);
|
|
1019
|
+
const cleared = bundle.clearBundleCache();
|
|
1020
|
+
await ui.section("cache", "design bundles");
|
|
1021
|
+
ui.step({
|
|
1022
|
+
label: "clear",
|
|
1023
|
+
value: cleared ? `${cleared} bundle${cleared === 1 ? "" : "s"}` : "empty",
|
|
1024
|
+
note: "re-downloads on next build",
|
|
1025
|
+
});
|
|
1026
|
+
|
|
634
1027
|
ui.done(
|
|
635
1028
|
target === "all"
|
|
636
1029
|
? "ready — ask your agent to plan, create, or edit a deck"
|
|
@@ -643,6 +1036,20 @@ async function cmdUninstall(rest) {
|
|
|
643
1036
|
const ui = await loadInstallerUi();
|
|
644
1037
|
ui.banner(readPackageJson().version, "uninstall");
|
|
645
1038
|
await runInstaller(target, "uninstall", rest);
|
|
1039
|
+
|
|
1040
|
+
// Drop the materialized bundle cache too — uninstall is a teardown, so leaving
|
|
1041
|
+
// a per-presentation dir (with brand assets) behind would be stale residue.
|
|
1042
|
+
const bundle = await import(
|
|
1043
|
+
pathToFileURL(path.join(SRC_DIR, "bridge", "bundle.mjs")).href
|
|
1044
|
+
);
|
|
1045
|
+
const cleared = bundle.clearBundleCache();
|
|
1046
|
+
await ui.section("cache", "design bundles");
|
|
1047
|
+
ui.step({
|
|
1048
|
+
status: "remove",
|
|
1049
|
+
label: "clear",
|
|
1050
|
+
value: cleared ? `${cleared} bundle${cleared === 1 ? "" : "s"}` : "empty",
|
|
1051
|
+
});
|
|
1052
|
+
|
|
646
1053
|
ui.done(target === "all" ? "removed from all harnesses" : `removed from ${target}`);
|
|
647
1054
|
}
|
|
648
1055
|
|
|
@@ -774,12 +1181,24 @@ async function main(rawArgv) {
|
|
|
774
1181
|
case "build":
|
|
775
1182
|
await cmdBuild(rest);
|
|
776
1183
|
return 0;
|
|
1184
|
+
case "review":
|
|
1185
|
+
await cmdReview(rest);
|
|
1186
|
+
return 0;
|
|
777
1187
|
case "build-pptx":
|
|
778
1188
|
await cmdBuildPptx(rest);
|
|
779
1189
|
return 0;
|
|
780
1190
|
case "search":
|
|
781
1191
|
await cmdSearch(rest);
|
|
782
1192
|
return 0;
|
|
1193
|
+
case "ls":
|
|
1194
|
+
await cmdLs(rest);
|
|
1195
|
+
return 0;
|
|
1196
|
+
case "insert-slide":
|
|
1197
|
+
await cmdInsertSlide(rest);
|
|
1198
|
+
return 0;
|
|
1199
|
+
case "add-slide":
|
|
1200
|
+
await cmdAddSlide(rest);
|
|
1201
|
+
return 0;
|
|
783
1202
|
case "download-bundle":
|
|
784
1203
|
await cmdDownloadBundle(rest);
|
|
785
1204
|
return 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "slidemind",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
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": {
|
|
@@ -30,13 +30,9 @@
|
|
|
30
30
|
"test": "node --test"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@babel/parser": "^7.26.5",
|
|
34
|
-
"@babel/traverse": "^7.26.5",
|
|
35
|
-
"@babel/types": "^7.26.5",
|
|
36
33
|
"@clack/prompts": "^1.3.0",
|
|
37
34
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
38
|
-
"
|
|
39
|
-
"recast": "^0.23.9",
|
|
35
|
+
"puppeteer": "24.43.0",
|
|
40
36
|
"zod": "^3.23.8"
|
|
41
37
|
},
|
|
42
38
|
"engines": {
|