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/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 the brand engine for icons/images (requires an open host)\n` +
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
- // Icon/image search now runs against the add-in's live brand engine over the
219
- // bridge — an open host is required, and the engine comes from the active
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
- const entry = client.pickPresentationEntry(registry, to);
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({ presId, engineId, query, backendURL });
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
- const count = res.resultCount ?? (res.thumbnails?.length ?? 0);
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
- `searchId ${res.searchId} — ${count} result(s) from engine ${engineId}\n`,
533
+ `Inserted ${res.insertedCount ?? picks.length} slide(s) into presentation ${entry.presId}\n`,
255
534
  );
256
- (res.thumbnails ?? []).forEach((t, i) => {
257
- process.stdout.write(` ${i + 1}. ${t.title ?? "(untitled)"}\n`);
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
- `Download a result by selection number via the agent (slidemind_download_result).\n`,
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
- `${result.cached ? "Bundle already current" : "Materialized bundle"} for ${result.presFP} (etag ${result.etag ?? "?"})\n ${result.dir}\n`,
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",
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
- "js-yaml": "^4.1.1",
39
- "recast": "^0.23.9",
35
+ "puppeteer": "24.43.0",
40
36
  "zod": "^3.23.8"
41
37
  },
42
38
  "engines": {