slidemind 0.1.4 → 0.1.6

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 CHANGED
@@ -64,7 +64,7 @@ slidemind/
64
64
  ├── .claude-plugin/
65
65
  │ └── plugin.json Claude Code plugin manifest
66
66
  ├── bin/
67
- │ └── slidemind.mjs CLI dispatcher (presentations, download-bundle, init, build, build-pptx, push, ls engines, search, insert-slide, add-slide, install, pair/whoami/use/logout)
67
+ │ └── slidemind.mjs CLI dispatcher (presentations, read, download-bundle, init, build, build-pptx, view-draft, push, ls engines, search, insert-slide, add-slide, install, pair/whoami/use/logout)
68
68
  ├── skills/ canonical, installable skill payload — thin CLI workflow glue only
69
69
  │ ├── create-slides/
70
70
  │ │ └── SKILL.md authoring skill: host gate + intake/plan + create flow + CLI⇄in-app command map.
@@ -77,7 +77,7 @@ slidemind/
77
77
  ├── src/ implementation of the slidemind CLI
78
78
  │ ├── bridge/ add-in local-bridge: client.mjs, bundle.mjs (materialize), build-and-push.mjs (server render; native-slide splicing)
79
79
  │ ├── builders/ build.mjs (HTML preview), review.mjs (spatial preview-review gate + local server), resolve-skill.mjs (→ bundle), lint.mjs (runs the add-in's engine)
80
- │ ├── mcp/ MCP server: list/push + list_engines/search_engine/view_slide/insert_slide/add_slide_to_deck/download_result
80
+ │ ├── mcp/ MCP server: list/read/push + list_engines/search_engine/view_slide/insert_slide/add_slide_to_deck/download_result
81
81
  │ └── installers/ per-harness installers: claude.mjs, codex.mjs, opencode.mjs, lib.mjs
82
82
  ├── archive/ retired code/assets (local exporter, icon library, brand design files) — NOT shipped
83
83
  ├── decks/ the user's working decks; sources are slides/*.jsx, design via .bundle-ref.json
@@ -93,11 +93,13 @@ We **do not** ship separate slash commands. Earlier versions had a top-level `co
93
93
 
94
94
  ```bash
95
95
  slidemind presentations # list PowerPoint windows registered with the add-in
96
+ slidemind read [presId] # pull the current content of an open presentation (--mode summary|raw) (HOST REQUIRED)
96
97
  slidemind download-bundle [deck] # materialize the active corporate design's bundle locally
97
98
  slidemind new <name> # scaffold ./decks/<name>/
98
99
  slidemind build <deck> # build self-contained HTML preview
99
100
  slidemind review <deck> # spatial preview-review gate; blocks for approve/comments (host-free)
100
101
  slidemind build-pptx <deck> # render native .pptx via the add-in (HOST REQUIRED)
102
+ slidemind view-draft <deck> # render the deck's slides to per-slide PNGs so the agent can look before review; no push, no .pptx (host-free once the bundle is materialized)
101
103
  slidemind push <deck> # render + push slides into a running PowerPoint (HOST REQUIRED)
102
104
  slidemind ls engines # list the live engine catalog (image + slide; --type/--subtype) (HOST REQUIRED)
103
105
  slidemind search "<query>" # search an engine for imagery or slides (--engine/--intent/--top) (HOST REQUIRED)
@@ -144,7 +146,9 @@ slidemind build <deck> # HTML preview (reads the materialized bundle)
144
146
  slidemind build <deck> --open # HTML + open in browser
145
147
  slidemind review <deck> # spatial preview-review gate; blocks until approve/comments (host-free)
146
148
  slidemind build-pptx <deck> # native PPTX rendered server-side via the add-in (host required)
149
+ slidemind view-draft <deck> # per-slide PNG draft preview, no push/no .pptx — the agent's look-before-review self-check (host-free once the bundle is cached)
147
150
  slidemind push <deck> # render + push into a running PowerPoint (host required)
151
+ slidemind read [presId] # pull current content of an open presentation (--mode summary|raw) (host required)
148
152
  slidemind ls engines # list the live engine catalog (image + slide) (host required)
149
153
  slidemind search "<query>" # search an engine for imagery or slides (host required)
150
154
  slidemind insert-slide [opts] # reuse found slide(s) live into a window (host required)
package/README.md CHANGED
@@ -128,21 +128,23 @@ npx slidemind <command>
128
128
 
129
129
  The CLI talks to PowerPoint through the add-in's cloud relay, so you pair once per machine.
130
130
 
131
- 1. In PowerPoint, open the **slidemind add-in** task pane and click **"Connect CLI"**.
132
- 2. It shows a one-time command. Copy and run it:
131
+ 1. In PowerPoint, open the **slidemind add-in** task pane → **Settings → General → Connect CLI**, and press **Pair**.
132
+ 2. It shows a one-time command (it expires in ~10 min — press **Pair** again for a fresh one). Copy and run it:
133
133
 
134
134
  ```bash
135
135
  slidemind pair <code>
136
136
  ```
137
137
 
138
- 3. Verify the link:
138
+ 3. **Restart the add-in once.** Pairing turns CLI integration on, but the task pane that's already running may not pick up the connection — so close the slidemind task pane and reopen it from the ribbon (if needed, reopen the presentation). The wizard doesn't prompt for this, but a freshly-booted pane is what registers your window with the CLI.
139
+
140
+ 4. Verify the link:
139
141
 
140
142
  ```bash
141
143
  slidemind whoami # lists the paired environment(s)
142
144
  slidemind presentations # lists open PowerPoint windows the add-in sees
143
145
  ```
144
146
 
145
- If `slidemind presentations` is empty, the add-in isn't reachable — open PowerPoint with the slidemind add-in and an active corporate design, then retry.
147
+ If `slidemind presentations` is empty even though PowerPoint is open with the add-in and an active corporate design, restart the add-in (step 3) and retry.
146
148
 
147
149
  ```bash
148
150
  slidemind logout # unpair this machine
@@ -158,6 +160,7 @@ slidemind build my-deck # build a self-contained HTML preview (offline
158
160
  slidemind review my-deck # spatial preview review: leave comments / approve in the browser (offline)
159
161
  slidemind build-pptx my-deck # render a native .pptx via the add-in (host required)
160
162
  slidemind push my-deck # stream slides straight into an open PowerPoint (host required)
163
+ slidemind read <presId> # pull the current content of an open presentation (host required)
161
164
  slidemind ls engines # list the active design's image + slide engines (host required)
162
165
  slidemind search "shield" # search an engine for imagery or slides (host required)
163
166
  slidemind insert-slide ... # reuse a found slide live into an open PowerPoint (host required)
@@ -191,6 +194,8 @@ slidemind new <name> Scaffold ./decks/<name>/
191
194
  slidemind build <deck> Build a self-contained HTML preview (offline)
192
195
  slidemind review <deck> Spatial preview review; blocks for approve/comments (offline)
193
196
  slidemind build-pptx <deck> Render a native .pptx via the add-in (host required)
197
+ slidemind view-draft <deck> Render slides to per-slide PNGs, no push — look before review (offline once cached)
198
+ --to <presId> --out <dir> --open
194
199
  slidemind push <deck> Build + push slides into an open PowerPoint (host required)
195
200
  slidemind ls engines List the live engine catalog (image + slide) (host required)
196
201
  slidemind search "<query>" Search an engine for imagery or slides (host required)
@@ -201,6 +206,8 @@ slidemind add-slide [opts] Fold reused slide results into a deck as native
201
206
  --search <id> --pick <n[,n,...]> --deck <deck> [--to <presId>] [--at <N>]
202
207
  slidemind download-bundle [deck] Materialize the active corporate design locally (host required)
203
208
  slidemind presentations List PowerPoint windows registered with the add-in
209
+ slidemind read [presId] Pull the current content of an open presentation (host required)
210
+ --mode summary|raw (default raw) [--to <presId>] [--json]
204
211
  slidemind pair <code> Pair with an add-in (copy the code from "Connect CLI")
205
212
  slidemind whoami Show paired environment(s)
206
213
  slidemind use <url|env> Set the default environment
@@ -287,7 +294,8 @@ Equivalently: `npm install -g slidemind@latest && slidemind init --for all`.
287
294
  | `slidemind: command not found` (or not recognized) | The npm global bin isn't on your `PATH`. Run `npm prefix -g` and add its `bin` (Windows: `%APPDATA%\npm`) to `PATH`, or use `npx slidemind`. |
288
295
  | PowerShell: *running scripts is disabled* | npm's `.ps1` shim hit the execution policy. Use `slidemind.cmd <command>`, or `Set-ExecutionPolicy -Scope CurrentUser RemoteSigned`. |
289
296
  | `slidemind presentations` is empty | The add-in isn't reachable. Open PowerPoint with the slidemind add-in and an active corporate design; click the task pane once to re-register, then retry. |
290
- | `Not paired` | Run `slidemind pair <code>` using the code from the add-in's **"Connect CLI"**. Codes are one-time and expire — generate a fresh one if it's stale. |
297
+ | `slidemind presentations` is empty right after pairing (PowerPoint + add-in already open) | The just-paired task pane never registered. **Restart the add-in** — close the slidemind task pane and reopen it from the ribbon (or reopen the presentation). The pairing wizard doesn't prompt for this. |
298
+ | `Not paired` | In the add-in, open **Settings → General → Connect CLI**, press **Pair**, and run the shown `slidemind pair <code>`. Codes are one-time and expire — generate a fresh one if it's stale. |
291
299
  | `build-pptx` / `push` / `search` error about no host | These require an open PowerPoint host with an active corporate design. `slidemind build` (HTML) is the only fully-offline preview. |
292
300
  | `EACCES` on `npm install -g` (macOS/Linux) | Don't `sudo npm`. Use a user-owned Node via [nvm](https://github.com/nvm-sh/nvm) or Homebrew, or set a user-writable npm prefix. |
293
301
 
package/bin/slidemind.mjs CHANGED
@@ -10,6 +10,7 @@
10
10
  // slidemind build <deck> build self-contained HTML deck
11
11
  // slidemind review <deck> spatial preview-review gate (blocks for approve/comments)
12
12
  // slidemind build-pptx <deck> render the deck as native .pptx (via the add-in)
13
+ // slidemind view-draft <deck> render the deck's slides to per-slide PNGs (no push)
13
14
  // slidemind uninstall --for <target>
14
15
  // slidemind update [--for <target>] [--ref <version>]
15
16
  // npm install -g slidemind@latest + redeploy
@@ -52,6 +53,12 @@ function printHelp() {
52
53
  ` slidemind build-pptx <deck> Export native .pptx via the add-in (requires an open host)\n` +
53
54
  ` --to <presId> target a specific window (required if >1 open)\n` +
54
55
  ` --out <path> output path (default <deck>/<deck>.pptx)\n` +
56
+ ` slidemind view-draft <deck> Render the deck's slides to per-slide PNGs (no push, no .pptx)\n` +
57
+ ` so the agent can look before review; host-free once the\n` +
58
+ ` bundle is materialized\n` +
59
+ ` --to <presId> window to fetch the bundle from (only if not cached)\n` +
60
+ ` --out <dir> output dir (default <deck>/.draft-preview/)\n` +
61
+ ` --open open each rendered PNG\n` +
55
62
  ` slidemind search "<query>" Search an engine for images or slides (requires an open host)\n` +
56
63
  ` --engine <id> engine to search (default: first image engine; required for slides)\n` +
57
64
  ` --intent <i> auto | content | visual (slide engines)\n` +
@@ -77,6 +84,12 @@ function printHelp() {
77
84
  ` --to <presId> target a specific window\n` +
78
85
  ` --force re-download even if the cached bundle is current\n` +
79
86
  ` slidemind presentations List PowerPoint windows registered with the add-in\n` +
87
+ ` slidemind doctor Diagnose CLI/add-in pairing, backend, and pane readiness\n` +
88
+ ` --json print machine-readable status\n` +
89
+ ` slidemind read [presId] Pull the current content of an open presentation (requires a host)\n` +
90
+ ` --mode <m> summary | raw (default: raw)\n` +
91
+ ` --to <presId> target a specific window (required if >1 open)\n` +
92
+ ` --json print the raw content payload\n` +
80
93
  ` slidemind push <deck> [opts] Build deck and push slides into a running PowerPoint\n` +
81
94
  ` --to <presId> target a specific window (required if >1 open)\n` +
82
95
  ` --at <index> insert after slide N (0-based); default = append\n` +
@@ -247,6 +260,87 @@ async function cmdBuildPptx(rest) {
247
260
  }
248
261
  }
249
262
 
263
+ function dataUriToPng(dataUri) {
264
+ const m = /^data:image\/png;base64,(.+)$/.exec(dataUri || "");
265
+ return m ? Buffer.from(m[1], "base64") : null;
266
+ }
267
+
268
+ async function cmdViewDraft(rest) {
269
+ const { deck, passthrough } = splitDeckArg(rest, "view-draft");
270
+ let to = null;
271
+ let outDir = null;
272
+ let open = false;
273
+ for (let i = 0; i < passthrough.length; i++) {
274
+ const a = passthrough[i];
275
+ if (a === "--to") {
276
+ to = passthrough[++i];
277
+ if (!to) fail("--to expects a presId");
278
+ } else if (a === "--out" || a === "-o") {
279
+ outDir = passthrough[++i];
280
+ if (!outDir) fail("--out expects a directory");
281
+ } else if (a === "--open") {
282
+ open = true;
283
+ } else if (a.startsWith("-")) {
284
+ fail(`unknown flag: ${a}`);
285
+ } else {
286
+ fail(`unexpected argument: ${a}`);
287
+ }
288
+ }
289
+ const deckDir = resolveDeckArg(deck);
290
+ if (!fs.existsSync(path.join(deckDir, "slides"))) {
291
+ fail(`expected 'slides/' folder in ${deckDir}`);
292
+ }
293
+ // Draft previews are produced locally by the add-in's shipped capture engine
294
+ // (no push, no .pptx) — the same module that hosts the render+push path.
295
+ const mod = await import(
296
+ pathToFileURL(path.join(SRC_DIR, "bridge", "build-and-push.mjs")).href
297
+ );
298
+ const result = await mod.viewDraft(deckDir, {
299
+ presentationId: to,
300
+ onProgress: renderProgressPrinter(),
301
+ });
302
+
303
+ const previewDir = outDir
304
+ ? path.resolve(process.cwd(), outDir)
305
+ : path.join(deckDir, ".draft-preview");
306
+ fs.mkdirSync(previewDir, { recursive: true });
307
+
308
+ const written = [];
309
+ for (const slide of result.slides ?? []) {
310
+ const png = slide.dataUri ? dataUriToPng(slide.dataUri) : null;
311
+ if (png) {
312
+ const stem = slide.fileName.replace(/\.[^.]+$/, "");
313
+ const file = path.join(previewDir, `${stem}.png`);
314
+ fs.writeFileSync(file, png);
315
+ written.push(file);
316
+ const flag = slide.status === "degraded" ? " (DEGRADED)" : "";
317
+ process.stdout.write(` ${slide.fileName}${flag} → ${file}\n`);
318
+ if (slide.note) process.stderr.write(` note: ${slide.note}\n`);
319
+ } else {
320
+ process.stdout.write(` ${slide.fileName} — ${slide.note ?? slide.status}\n`);
321
+ }
322
+ }
323
+ if (written.length === 0) {
324
+ process.stdout.write(
325
+ "No draft preview images were produced (no freeform slides to capture).\n",
326
+ );
327
+ } else {
328
+ process.stdout.write(
329
+ `Wrote ${written.length} draft preview${written.length === 1 ? "" : "s"} to ${previewDir} — NOT pushed. ` +
330
+ "Fix anything visibly wrong in the JSX, then re-run, or 'slidemind review' / 'push' when sound.\n",
331
+ );
332
+ }
333
+ for (const w of result.warnings ?? []) {
334
+ process.stderr.write(` warning: ${w}\n`);
335
+ }
336
+ if (open && written.length > 0) {
337
+ const { openInBrowser } = await import(
338
+ pathToFileURL(path.join(BUILDERS_DIR, "build.mjs")).href
339
+ );
340
+ for (const file of written) openInBrowser(file);
341
+ }
342
+ }
343
+
250
344
  async function cmdSearch(rest) {
251
345
  let asJson = false;
252
346
  let to = null;
@@ -298,7 +392,7 @@ async function cmdSearch(rest) {
298
392
  const bundle = await import(
299
393
  pathToFileURL(path.join(SRC_DIR, "bridge", "bundle.mjs")).href
300
394
  );
301
- const registry = await client.requireHost();
395
+ const registry = await client.requirePaneHost();
302
396
  // The engine is per-user, not window-bound — so search just attaches to any
303
397
  // connected window (preferring a live one). No "which window?" prompt; pass
304
398
  // --to only to pin a specific one. The chosen window is printed below so
@@ -336,15 +430,20 @@ async function cmdSearch(rest) {
336
430
  subtype = "image";
337
431
  }
338
432
 
339
- const res = await client.engineSearch({
340
- presId,
433
+ // Window-agnostic: tries the picked pane, then fails over to any other live
434
+ // window on the same backend if it's suspended/frozen. The answering window
435
+ // (res.presId) is what reuse must target — the searchId is bound to it.
436
+ const res = await client.engineSearchAnyPane({
437
+ registry,
438
+ explicitPresId: to,
341
439
  engineId,
342
440
  query,
343
441
  subtype,
344
442
  intent: intent ?? null,
345
443
  top: top ?? null,
346
- backendURL,
347
444
  });
445
+ const ranPresId = res?.presId ?? presId;
446
+ const ranEntry = registry.find((p) => p.presId === ranPresId) ?? entry;
348
447
  if (asJson) {
349
448
  process.stdout.write(`${JSON.stringify(res, null, 2)}\n`);
350
449
  return;
@@ -387,14 +486,14 @@ async function cmdSearch(rest) {
387
486
  // add-ins that don't echo a top-level subtype.
388
487
  const isSlideResults =
389
488
  res.subtype === "slide" || results.some((r) => r.kind === "slide");
390
- const envTag = entry.env ? ` [${entry.env}]` : "";
489
+ const envTag = ranEntry.env ? ` [${ranEntry.env}]` : "";
391
490
  if (isSlideResults) {
392
491
  // The searchId is bound to the window the search ran on — reuse must target
393
492
  // that same window, so spell out its presId in the next-step commands.
394
493
  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`,
494
+ `searchId ${res.searchId} ran on window ${ranPresId}${envTag} — reuse from THIS window:\n` +
495
+ ` slidemind insert-slide --search ${res.searchId} --pick <n> --to ${ranPresId}\n` +
496
+ ` slidemind add-slide --search ${res.searchId} --pick <n> --deck <deck> --to ${ranPresId}\n`,
398
497
  );
399
498
  } else {
400
499
  process.stdout.write(
@@ -517,7 +616,7 @@ async function cmdInsertSlide(rest) {
517
616
  const client = await import(
518
617
  pathToFileURL(path.join(SRC_DIR, "bridge", "client.mjs")).href
519
618
  );
520
- const registry = await client.requireHost();
619
+ const registry = await client.requirePaneHost();
521
620
  const entry = client.pickPresentationEntry(registry, to);
522
621
  const res = await client.insertSlideLive({
523
622
  presId: entry.presId,
@@ -578,7 +677,7 @@ async function cmdAddSlide(rest) {
578
677
  const client = await import(
579
678
  pathToFileURL(path.join(SRC_DIR, "bridge", "client.mjs")).href
580
679
  );
581
- const registry = await client.requireHost();
680
+ const registry = await client.requirePaneHost();
582
681
  // The searchId is presentation-scoped, so --to targets the window the search
583
682
  // ran on. Without it, a registry with >1 open window is a dead-end.
584
683
  const entry = client.pickPresentationEntry(registry, to);
@@ -651,7 +750,7 @@ async function cmdDownloadBundle(rest) {
651
750
  const deckRef = positional[0]
652
751
  ? bundle.readDeckBundleRef(resolveDeckArg(positional[0]))
653
752
  : null;
654
- const registry = await client.requireHost();
753
+ const registry = await client.requireRenderHost();
655
754
  const entry = client.pickPresentationEntry(registry, to, {
656
755
  preferBrandId: deckRef?.presFP ?? null,
657
756
  });
@@ -709,7 +808,185 @@ async function cmdPresentations(rest) {
709
808
  for (const p of list) {
710
809
  const label = p.title || p.fileName || "(untitled)";
711
810
  const env = p.env ? `[${p.env}]\t` : "";
712
- process.stdout.write(`${p.presId}\t${env}${label}\n`);
811
+ const state =
812
+ p.connectionState ||
813
+ (p.connected === false || p.suspendedAt ? "suspended" : "connected");
814
+ const design = p.designState ? `\tcd:${p.designState}` : "";
815
+ process.stdout.write(`${p.presId}\t${env}${label}\t${state}${design}\n`);
816
+ }
817
+ }
818
+
819
+ function publicConnectionStatus(status, client) {
820
+ return {
821
+ paired: status.paired,
822
+ backendCount: status.backendCount,
823
+ backends: (status.backends ?? []).map((b) => ({
824
+ env: b.env,
825
+ ok: b.ok,
826
+ readinessV1: b.readinessV1 === true,
827
+ capabilities: b.capabilities ?? null,
828
+ error: b.error,
829
+ summary: b.summary ?? null,
830
+ presentations: (b.presentations ?? []).map(client.toPublicPresentation),
831
+ })),
832
+ presentations: (status.presentations ?? []).map(client.toPublicPresentation),
833
+ };
834
+ }
835
+
836
+ function connectionNextAction(pub) {
837
+ if (!pub.paired) {
838
+ return 'Not paired. In the add-in open Settings → General → Connect CLI, press Pair, then run the shown `slidemind pair <code>`.';
839
+ }
840
+ if ((pub.backends ?? []).every((b) => !b.ok)) {
841
+ return "Every paired backend is unreachable. Check network/VPN/backend status, or re-pair if the environment moved.";
842
+ }
843
+ if ((pub.presentations ?? []).length === 0) {
844
+ return "No PowerPoint window is registered. Open PowerPoint, load the slidemind add-in, and activate or create a corporate design.";
845
+ }
846
+ const renderReady = pub.presentations.some(
847
+ (p) =>
848
+ p.capabilities?.render === true ||
849
+ (!p.connectionState && p.connected !== false) ||
850
+ p.designState === "ready",
851
+ );
852
+ if (!renderReady) {
853
+ return "PowerPoint is connected, but no window is render-ready. Wait for corporate design matching or activate/create a corporate design.";
854
+ }
855
+ const paneReady = pub.presentations.some(
856
+ (p) =>
857
+ p.capabilities?.paneOps === true ||
858
+ ((!p.connectionState || p.connectionState === "connected") &&
859
+ p.connected !== false &&
860
+ !p.suspendedAt),
861
+ );
862
+ if (!paneReady) {
863
+ return "Rendering is available, but pane operations are suspended. Bring PowerPoint forward and click/reopen the slidemind pane before push/search/read.";
864
+ }
865
+ return "Ready.";
866
+ }
867
+
868
+ async function cmdDoctor(rest) {
869
+ const asJson = rest.includes("--json");
870
+ const cfg = await loadBridgeModule("config");
871
+ const sessions = cfg.readSessions();
872
+ const client = await import(
873
+ pathToFileURL(path.join(SRC_DIR, "bridge", "client.mjs")).href
874
+ );
875
+ if (sessions.length === 0) {
876
+ const pub = {
877
+ paired: false,
878
+ backendCount: 0,
879
+ backends: [],
880
+ presentations: [],
881
+ nextAction: connectionNextAction({ paired: false }),
882
+ };
883
+ process[asJson ? "stdout" : "stderr"].write(
884
+ asJson ? `${JSON.stringify(pub, null, 2)}\n` : `${pub.nextAction}\n`,
885
+ );
886
+ return;
887
+ }
888
+ const status = publicConnectionStatus(await client.connectionStatus(), client);
889
+ status.nextAction = connectionNextAction(status);
890
+ if (asJson) {
891
+ process.stdout.write(`${JSON.stringify(status, null, 2)}\n`);
892
+ return;
893
+ }
894
+ process.stdout.write(`Paired backends: ${status.backendCount}\n`);
895
+ for (const b of status.backends) {
896
+ process.stdout.write(
897
+ ` ${b.env}: ${b.ok ? "reachable" : `unreachable — ${b.error}`}` +
898
+ (b.readinessV1 ? " (readiness)" : " (legacy)") +
899
+ `\n`,
900
+ );
901
+ }
902
+ if (status.presentations.length === 0) {
903
+ process.stdout.write("Open presentations: none\n");
904
+ } else {
905
+ process.stdout.write("Open presentations:\n");
906
+ for (const p of status.presentations) {
907
+ const label = p.title || p.fileName || "(untitled)";
908
+ const state =
909
+ p.connectionState ||
910
+ (p.connected === false || p.suspendedAt ? "suspended" : "connected");
911
+ const design = p.designState || (p.brandId ? "ready" : "unknown");
912
+ const caps = p.capabilities
913
+ ? ` render:${p.capabilities.render === true ? "yes" : "no"} pane:${p.capabilities.paneOps === true ? "yes" : "no"}`
914
+ : "";
915
+ process.stdout.write(
916
+ ` ${p.presId}\t${p.env ? `[${p.env}] ` : ""}${label}\t${state}\tcd:${design}${caps}\n`,
917
+ );
918
+ }
919
+ }
920
+ process.stdout.write(`Next action: ${status.nextAction}\n`);
921
+ }
922
+
923
+ // Pull the current content of an open presentation. Content is window-bound, so
924
+ // pick exactly one window (positional presId or --to; pickPresentationEntry errors
925
+ // listing the windows if several are open and none was named). `--mode summary`
926
+ // prints a per-slide outline; default `raw` prints each slide's plain text.
927
+ async function cmdRead(rest) {
928
+ let presId = null;
929
+ let mode = "raw";
930
+ let asJson = false;
931
+ for (let i = 0; i < rest.length; i++) {
932
+ const a = rest[i];
933
+ if (a === "--mode") {
934
+ const next = rest[++i];
935
+ if (next !== "summary" && next !== "raw") {
936
+ fail(`--mode expects 'summary' or 'raw', got '${next ?? ""}'`);
937
+ }
938
+ mode = next;
939
+ } else if (a === "--to") {
940
+ presId = rest[++i];
941
+ if (!presId) fail("--to expects a presId");
942
+ } else if (a === "--json") {
943
+ asJson = true;
944
+ } else if (a.startsWith("-")) {
945
+ fail(`unknown flag: ${a}`);
946
+ } else if (!presId) {
947
+ presId = a; // positional presId
948
+ } else {
949
+ fail(`unexpected argument: ${a}`);
950
+ }
951
+ }
952
+ const client = await import(
953
+ pathToFileURL(path.join(SRC_DIR, "bridge", "client.mjs")).href
954
+ );
955
+ const registry = await client.requirePaneHost();
956
+ const entry = client.pickPresentationEntry(registry, presId);
957
+ const res = await client.getPresentationContent({
958
+ presId: entry.presId,
959
+ mode,
960
+ backendURL: entry.backendURL ?? null,
961
+ });
962
+ if (!res?.ok) {
963
+ fail(`read failed: ${res?.error ?? "unknown error"}`);
964
+ }
965
+ if (asJson) {
966
+ process.stdout.write(JSON.stringify(res, null, 2) + "\n");
967
+ return;
968
+ }
969
+ const slides = Array.isArray(res.slides) ? res.slides : [];
970
+ if (slides.length === 0) {
971
+ process.stderr.write("(presentation has no slides)\n");
972
+ return;
973
+ }
974
+ if (res.mode === "summary") {
975
+ for (const s of slides) {
976
+ const label = s.title || "(untitled)";
977
+ const sub = s.subtitle ? ` — ${s.subtitle}` : "";
978
+ const layout = s.layout ? ` [${s.layout}]` : "";
979
+ process.stdout.write(
980
+ `${String(s.index).padStart(3)}.${layout} ${label}${sub}\n`,
981
+ );
982
+ }
983
+ } else {
984
+ slides.forEach((s, i) => {
985
+ if (i > 0) process.stdout.write("\n");
986
+ const layout = s.layout ? ` (${s.layout})` : "";
987
+ process.stdout.write(`── Slide ${s.index}${layout} ──\n`);
988
+ process.stdout.write(`${s.text && s.text.length ? s.text : "(no text)"}\n`);
989
+ });
713
990
  }
714
991
  }
715
992
 
@@ -760,8 +1037,30 @@ async function cmdPair(rest) {
760
1037
  others > 0
761
1038
  ? ` (${sessions.length} environments connected — commands now fan out across all of them)`
762
1039
  : "";
1040
+ let verify = "";
1041
+ try {
1042
+ const client = await import(
1043
+ pathToFileURL(path.join(SRC_DIR, "bridge", "client.mjs")).href
1044
+ );
1045
+ const deadline = Date.now() + 8000;
1046
+ for (;;) {
1047
+ const status = await client.connectionStatus();
1048
+ if ((status.presentations ?? []).length > 0) {
1049
+ verify = " Pane is registered.";
1050
+ break;
1051
+ }
1052
+ if (Date.now() >= deadline) {
1053
+ verify =
1054
+ " No pane is registered yet; if PowerPoint is already open, reopen or click the slidemind task pane, then run 'slidemind doctor'.";
1055
+ break;
1056
+ }
1057
+ await new Promise((resolve) => setTimeout(resolve, 1000));
1058
+ }
1059
+ } catch {
1060
+ // Pairing itself succeeded; diagnostics remain available via doctor.
1061
+ }
763
1062
  process.stdout.write(
764
- `Paired with ${label || env}${suffix}. Run 'slidemind presentations' to verify.\n`,
1063
+ `Paired with ${label || env}${suffix}.${verify} Run 'slidemind doctor' to verify.\n`,
765
1064
  );
766
1065
  }
767
1066
 
@@ -1187,6 +1486,9 @@ async function main(rawArgv) {
1187
1486
  case "build-pptx":
1188
1487
  await cmdBuildPptx(rest);
1189
1488
  return 0;
1489
+ case "view-draft":
1490
+ await cmdViewDraft(rest);
1491
+ return 0;
1190
1492
  case "search":
1191
1493
  await cmdSearch(rest);
1192
1494
  return 0;
@@ -1220,6 +1522,14 @@ async function main(rawArgv) {
1220
1522
  case "presentations":
1221
1523
  await cmdPresentations(rest);
1222
1524
  return 0;
1525
+ case "doctor":
1526
+ case "connection":
1527
+ case "status":
1528
+ await cmdDoctor(rest);
1529
+ return 0;
1530
+ case "read":
1531
+ await cmdRead(rest);
1532
+ return 0;
1223
1533
  case "push":
1224
1534
  await cmdPush(rest);
1225
1535
  return 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slidemind",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
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": {