slidemind 0.2.4 → 0.3.1

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slidemind",
3
- "version": "0.2.4",
3
+ "version": "0.3.1",
4
4
  "description": "Connect Claude Code to the slidemind PowerPoint add-in — build and edit native, on-brand PowerPoint decks against the active corporate design, from the terminal.",
5
5
  "author": {
6
6
  "name": "statworx",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slidemind",
3
- "version": "0.2.4",
3
+ "version": "0.3.1",
4
4
  "description": "Connect Codex to the slidemind PowerPoint add-in — build and edit native, on-brand PowerPoint decks against the active corporate design. Requires the slidemind CLI (npm install -g slidemind); the skills explain the install when it is missing.",
5
5
  "author": {
6
6
  "name": "statworx",
package/AGENTS.md CHANGED
@@ -27,7 +27,29 @@ npx --yes slidemind@latest setup smc1_<claim>
27
27
  npx runs the package from its cache but never installs it, so `cmdSetup` does
28
28
  the persistent work itself: decode the claim → redeem it → save the session →
29
29
  `npm install -g slidemind@latest` → deploy the skills → run `ready`. The user
30
- needs nothing but Node beforehand, and **there is no `init` step**: `--for auto`
30
+ needs nothing but Node beforehand — and for a machine without even that, the
31
+ tarball ships two **zero-prerequisite bootstrap scripts** the add-in can print
32
+ instead, served publicly by the npm CDNs (same trick as the Claude marketplace
33
+ JSON — private repo, public tarball):
34
+
35
+ ```bash
36
+ # macOS / Linux
37
+ curl -fsSL https://unpkg.com/slidemind/install.sh | sh -s -- smc1_<claim>
38
+ # Windows
39
+ powershell -NoProfile -ExecutionPolicy Bypass -Command "& ([scriptblock]::Create((irm https://unpkg.com/slidemind/install.ps1))) smc1_<claim>"
40
+ ```
41
+
42
+ Each checks Node >= 18, installs — or upgrades an outdated — Node only via an
43
+ already-present package manager (Homebrew / winget; `install` then `upgrade`,
44
+ since each tool errors one of the two on an existing package), then hands off
45
+ to the `npx … setup` line above —
46
+ they host no binaries and add no channel (unlike the retired Azure-SAS
47
+ `install.sh`, npm stays the sole binary source). If Node cannot be installed,
48
+ they abort **before** the claim is touched — the one-time code stays valid and
49
+ the same pasted command works after a manual Node install (links printed). The
50
+ sh variant re-attaches `/dev/tty` as stdin so setup's TTY-gated Microsoft 365
51
+ sign-in still runs under `curl | sh`. `test/packaging.test.mjs` guards that
52
+ both ship. **There is no `init` step**: `--for auto`
31
53
  (the default) resolves through each installer's `detect()` to the coding agents
32
54
  actually present, minus any whose slidemind plugin already supplies the skills.
33
55
  `init` stays for the manual-install path and for adding a harness later.
@@ -153,6 +175,7 @@ slidemind/
153
175
  │ │ setup-claim.mjs (decode/redeem the add-in's one-command claim — the only connect path) + http-json.mjs + config.mjs (per-backend sessions)
154
176
  │ ├── builders/ build.mjs (HTML preview), review.mjs (spatial preview-review gate + local server; stable per-deck port, graceful fallback, tab reuse across rounds), plan.mjs (plan sign-off gate) + plan-overlay.js + plan-md.mjs (markdown→HTML) + gate-server.mjs (shared gate primitives), resolve-skill.mjs (→ bundle), lint.mjs (runs the add-in's engine)
155
177
  │ ├── pptx/ host-free local-.pptx support (fflate-based): read-local.mjs (read a file's slides — output-identical to the host read), slide-swap.mjs (fold a picked slide into a deck as native .pptx — swaps it into the lowest slot the splicer reads, then prunes the package to that slide's dependency closure so a slice carries only what it needs), zip.mjs + ooxml.mjs (shared unzip/order helpers)
178
+ │ ├── lib/ deck-workspace bookkeeping: adopt.mjs (rebuild a deck from a file's embedded source + convert its foreign slides through the add-in's convert-pptx route), conversions.mjs (the deck's `conversions/<stem>.json` identity+evidence records and the preserve source package push threads back), slide-guids.mjs, slide-order.mjs, reused-slides.mjs
156
179
  │ └── installers/ per-harness installers: claude.mjs, codex.mjs, opencode.mjs, lib.mjs
157
180
  ├── archive/ retired code/assets (local exporter, icon library, brand design files) — NOT shipped
158
181
  ├── decks/ the user's working decks; sources are slides/*.jsx, design via .bundle-ref.json
@@ -176,19 +199,21 @@ slidemind new <name> # scaffold ./decks/<name>/ (auto-binds to the
176
199
  slidemind plan <deck> # plan sign-off gate on <deck>/plan.md; markdown editor + live preview + overall feedback, blocks for approve/comments (host-free)
177
200
  slidemind build <deck> # build self-contained HTML preview
178
201
  slidemind review <deck> # spatial preview-review gate; blocks for approve/comments (host-free)
179
- slidemind build-pptx <deck> # compose: add-in; template pack: host-free with --pack <dir>
202
+ slidemind build-pptx <deck> # compose: add-in; template pack: host-free with --pack <dir>; never overwrites manual PowerPoint edits — text edits fold back, the rest refuses, --force discards (docs/slidemind-cli-connect/16-manual-edit-reconcile.md)
180
203
  slidemind inspect-template-deck <deck> # normalized pack model for pack-owned validation; --pack <dir> --json
181
204
  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)
182
- slidemind push <deck> # render + push slides into a running PowerPoint (HOST REQUIRED)
205
+ slidemind push <deck> # render + push slides into a running PowerPoint (HOST REQUIRED); --replace reconciles the LIVE deck first (same doc), insert pushes never do
183
206
  slidemind ls engines # list the live engine catalog (image + slide; --type/--subtype) (pane-free on add-ins with engineRoutes; HOST REQUIRED on older ones)
184
207
  slidemind search "<query>" # search an engine for imagery or slides (--engine/--intent/--top/--deck) (pane-free on add-ins with engineRoutes; HOST REQUIRED on older ones)
185
208
  slidemind insert-slide [opts] # reuse found slide(s) live into a window (--search/--pick/--to/--at) (HOST REQUIRED)
186
209
  slidemind imagen "<prompt>" [opts] # generate ONE image via the add-in's image model into <deck>/assets/ (--deck/--out/--size/--quality/--brand-style; connected backend, pane-free)
187
- slidemind setup <claim> # one-command onboarding from the add-in's Connect CLI button: redeem the claim + npm install -g + skills + ready (--for/--no-install/--no-ready/--json)
210
+ slidemind setup <claim> # one-command onboarding from the add-in's Connect CLI button: redeem the claim + npm install -g + skills + best-effort Microsoft 365 sign-in (TTY only) + ready (--for/--no-install/--no-ready/--no-login/--json)
188
211
  slidemind add-slide [opts] # fold slide(s) into a deck as native .pptx — from an engine (--search/--pick/--deck/--to/--at/--json; pane-free on add-ins with engineRoutes) or a local file (--from <file.pptx>/--pick/--deck/--at; host-free, mutually exclusive with --search)
189
- slidemind whoami # list every connected environment (* = default)
212
+ slidemind adopt <file.pptx> # rebuild a deck workspace from a FILE's embedded source; foreign slides convert to editable jsx through the add-in's convert-pptx route where it is served (--deck/--brand/--no-convert) — live decks are add-in-only in v1 (docs/slidemind-cli-connect/14-foreign-slide-conversion.md)
213
+ slidemind login [--backend <url|env>] # sign in to Microsoft 365 (browser + PKCE); usually unnecessary — the first engine command runs this flow ITSELF and blocks until the human finishes (graph-auth `ensureSignedIn`, once per process). One sign-in mints Graph + SharePoint tokens from one refresh token; docs/slidemind-cli-connect/15-graph-delegated-cli.md
214
+ slidemind whoami # list every connected environment (* = default) + its Microsoft 365 sign-in state
190
215
  slidemind use <url|env> # set the default environment
191
- slidemind logout [url|env] # unpair one environment (--all unpairs every one)
216
+ slidemind logout [url|env] # unpair one environment (--all unpairs every one; also drops its Microsoft 365 sign-in)
192
217
  slidemind init --for <target> # install skill into claude | codex | opencode | all | auto (also clears the bundle cache); `setup` already does this
193
218
  slidemind uninstall --for <target> # remove the skill (also clears the bundle cache)
194
219
  ```
@@ -197,15 +222,19 @@ slidemind uninstall --for <target> # remove the skill (also clears the bundle
197
222
 
198
223
  **Multi-environment sessions.** The CLI stores one session **per backend** (`~/.config/slidemind/session.json`, v2 `{version, sessions[], default}`; the legacy single-record v1 shape is migrated on read). Connect once per environment (local / dev / prod) and every command **fans out** across all connected backends, tags each open window with its `env`, and **routes by the presentation's origin backend** automatically — no manual switching. Adding an environment is easiest with `slidemind setup <claim> --no-install --for none` — the claim carries its own backend, so nothing has to be typed or configured. There is no other way in: without a claim the CLI has no backend to talk to. Later commands may scope an existing session with `--backend <url|env>` / `SLIDEMIND_BACKEND`. A normal user connects exactly one backend.
199
224
 
225
+ **The env label is routing metadata, not narrative.** Because a normal user has exactly one backend, naming it on every line is noise that makes ordinary output read like a debugging session — so human-facing output shows an env label **only when more than one backend is connected**. `src/bridge/config.mjs` owns the rule (`isMultiEnv()`, `envTag()`, `envPhrase()`); call those rather than interpolating `[${env}]` by hand, and word the sentence so it still reads with the label absent. Three things are deliberately NOT gated: `--json` payloads (agents route on `env`), `doctor` and `whoami` (reporting environments is their whole job), and any label the user just typed (`--backend prod`, `logout dev`, `use dev`) — suppressing an echo of their own argument is confusing, not quiet. `setup` decides once per run, **before** it spends the claim, so its four mentions agree with each other rather than flipping when `upsertSession` changes the count.
226
+
227
+ **No credential may reach stdout or stderr.** The CLI holds four — the per-backend `sessionToken`, a Graph refresh token, a Graph access token, and the one-time `smc1_…` claim — and none is printed on purpose. The risk is transitive: a server-controlled **response body** used verbatim as an error detail (`setup-claim.mjs` redeem failures, the Entra token endpoint, `response.body.slice(0, 400)` on every non-2xx bridge call), plus `unknown command: <argv>` echoing a claim pasted without its verb. Patching those four sites would leave the fifth to whoever adds the next route, so containment lives at the output boundary instead: `src/bridge/secrets.mjs` holds the values this process knows are secret, and `bin/slidemind.mjs` wraps `process.stdout.write`/`process.stderr.write` **once, before any command runs**, so every line, `--json` payload and error message is scrubbed on its way out. Matching is by VALUE, not by pattern — exact, no false positives on ordinary prose — and the replacement is an opaque `«secret:4f9c»` tag (4 hex of sha256) that reveals nothing while still letting two occurrences of one credential be recognised as the same. Register a secret the moment it enters memory (`readSessions`, `getGraphRefreshToken`, `requestToken`, `decodeSetupClaim`, and argv); values under 8 characters are ignored on purpose, so **test fixtures must use realistic lengths** or the guard silently cannot fail. `test/secret-containment.test.mjs` drives the real bin against a relay that deliberately echoes the credential it was sent.
228
+
200
229
  **The design truth lives in the add-in, not the CLI.** The `.pptx` is rendered server-side over the local bridge (no local exporter), the corporate design is fetched as a bundle, brand imagery comes from the add-in's engine, and slide reuse (extract / insert / splice) executes in the add-in too. So `create`/`build-pptx`/`push`/`ls engines`/`search`/`insert-slide`/`add-slide` all need the **slidemind add-in with an active corporate design** behind them. The engine-search + slide-reuse wire contract is in `docs/slidemind-cli-connect/11-engine-search-slide-reuse.md`; see `docs/slidemind-cli-connect/` for the rest.
201
230
 
202
231
  **Engine ops are moving off the task pane.** An add-in that advertises `engineRoutes` in `/api/cli/capabilities` answers discovery, search, download and slide extraction on the CLI session itself (`GET /api/cli/engines`, `POST /api/cli/searches`, `GET /api/cli/searches/:id/results/:sel/{image,thumbnail,slide,source-presentation}`), so those commands need **no PowerPoint window at all** — the searchId is server-owned, `--to` becomes a no-op, and multi-environment routing goes by the deck's bound design (`.bundle-ref.json`), never by an open window's origin. Older add-ins keep relaying every one of them through the pane (`client.resolveEngineBackend()` returns null → `requirePaneHost()`), so **never remove the pane path and never raise `minCliVersion` for this**. Live `insert-slide` and reading an unsaved open deck stay pane-bound — that is Office.js physics, but even there the *extraction* backend is the one that **minted the searchId** (`client.resolveSearchBackend()`), never the target window's origin: with two environments connected those differ, and spending a server-owned id on the wrong one 404s as an "expired search" no re-search can clear.
203
232
 
204
- When the backend's permission snapshot ages out, corpus search (only) answers `403 permissions_stale`, rendered through `client.permissionsStaleBlocker(err)` as one user-actionable line — but the snapshot backs the **pane-free path only**, since a live pane signs every request with a browser token that is strictly fresher. So when a pane-ready window is open, `search` **relays through it** instead of blocking (one stderr note; the searchId is then the pane's, and every next step names that window), and the ops that cannot be replayed pane-side — anything bound to an opaque `searchId` — name that open pane as the route to a fresh search rather than telling the user to open an add-in that is already open. `slidemind ls engines` is the exception: `GET /api/cli/engines` is the catalog route on both add-in generations, so there is no second route to retry on and only the message widens. Plan: `docs/slidemind-cli-connect/13-pane-free-authoring.md`.
233
+ **Corpus identity is the user's own sign-in, resolved live.** The pane-free engine routes run as the USER: the CLI attaches the delegated Graph + SharePoint tokens `slidemind login` minted (body fields `graphApiToken`/`spApiToken` on POST, headers `x-slidemind-graph-token`/`x-slidemind-sp-token` on the byte GETs), and the backend resolves Entra + `sg:` site-group membership from them per request (cached ~10 min server-side). There is NO stored permission snapshot anymore — that Redis record, its capture points, and `permissions_stale` were removed on both sides. Without usable tokens the corpus routes answer `403 login_required` (403, not 401 — the CLI transport reserves 401 for a dead session), rendered through `client.loginRequiredBlocker(err)`; but before ever sending a token-less request the CLI attempts the sign-in ITSELF (`ensureSignedIn`: opens the browser, blocks up to 5 min, once per process), so the blocker only appears when the human refused or abandoned it. When a pane-ready window is open, `search` **relays through it** instead of blocking (one stderr note; the searchId is then the pane's, and every next step names that window), and the ops that cannot be replayed pane-side — anything bound to an opaque `searchId` — name that open pane as the route to a fresh search. Plan: `docs/slidemind-cli-connect/13-pane-free-authoring.md` + `15-graph-delegated-cli.md`.
205
234
 
206
235
  **The two ops that stay pane-bound self-heal a presId churn.** Every pane reconnect mints a NEW presId for the same window, so `push`'s insert and `insert-slide` re-list the registry and re-match the window before giving up: `waitForPaneReady` matches the same presId, then `fileName`+`brandId`, then — when a Save-As renamed the window mid-run — the **design fingerprint alone** (`presFP == brandId`), and only when exactly one connected window carries it (two windows on one design are indistinguishable, and pushing into the wrong one is worse than waiting). `engineOpWithPaneWait` takes that window identity plus an opt-in `retargetOnMissing`, which **only the live insert sets**: for an idempotent engine read a 404 is the search record talking, not the window, so waiting on the registry would just stall the agent (`isMissingPresentationError` keeps a coded 404 like `unknown_search` out of it). Both retries re-send the SAME `operationId`, so the pane-side dedup makes them safe; both retry exactly ONCE and then surface the wake-PowerPoint blocker — `push` by saving the rendered `.pptx` next to the deck rather than throwing the bytes away, `insert-slide` by naming the click and the window-free `add-slide` alternative.
207
236
 
208
- **The readiness ladder is two ladders.** `bin/slidemind.mjs` authors them once: `backendBlocker(pub)` (paired → every backend unreachable → CLI below the floor) is what render, bundle and the pane-free engine routes ride on, and `paneBlocker(pub)` (no window → not render-ready → pane suspended) governs only push's insert, `insert-slide` and reading an open presentation. `connectionBlocker = backendBlocker ?? paneBlocker` keeps every pre-existing message byte-identical, `doctor` reports the two separately (`authoring` / `host` in `--json`) so an agent stops reading "pane gone" as "cannot work", and `permissions_stale` is reported as a **degradation** next to the authoring ladder — never a rung, because it must not gate render or bundle. `doctor`'s engine-search line reads the same `capabilities.permissionsStale` flag off `/api/cli/readiness`, so `doctor`, `ls engines` and `search` name ONE cause for that one condition instead of three. `ready`'s ladder for a **bound** deck is session → bundle freshness → (informational) engine + pane reachability: with an empty registry it materializes straight over the brand route instead of sitting out the hydration wait, and when a window IS registered the open add-in stays the design source of truth. An unbound deck with no window resolves through `GET /api/cli/brands` (capability `brandsList`, `client.listBrands()`): one design binds automatically, several stop with `ambiguous_brand` naming `--brand <presFP>`. `client.engineReachability({ backends })` is the engine half of both reports — env labels only, never a URL.
237
+ **The readiness ladder is two ladders.** `bin/slidemind.mjs` authors them once: `backendBlocker(pub)` (paired → every backend unreachable → CLI below the floor) is what render, bundle and the pane-free engine routes ride on, and `paneBlocker(pub)` (no window → not render-ready → pane suspended) governs only push's insert, `insert-slide` and reading an open presentation. `connectionBlocker = backendBlocker ?? paneBlocker` keeps every pre-existing message byte-identical, `doctor` reports the two separately (`authoring` / `host` in `--json`) so an agent stops reading "pane gone" as "cannot work", and the Microsoft 365 sign-in state is reported next to the authoring ladder — never a rung, because it must not gate render or bundle (`ready`'s `m365.degradation.code: "login_required"`, computed LOCALLY from `hasGraphSession` — the backend advertises the contract via `cliLoginV1`, never per-user state). `ready`'s ladder for a **bound** deck is session → bundle freshness → (informational) engine + pane reachability: with an empty registry it materializes straight over the brand route instead of sitting out the hydration wait, and when a window IS registered the open add-in stays the design source of truth. An unbound deck with no window resolves through `GET /api/cli/brands` (capability `brandsList`, `client.listBrands()`): one design binds automatically, several stop with `ambiguous_brand` naming `--brand <presFP>`. `client.engineReachability({ backends })` is the engine half of both reports — env labels only, never a URL.
209
238
 
210
239
  ## External skill packs (this repo ships none)
211
240
 
package/README.md CHANGED
@@ -22,6 +22,7 @@ Both drive the same add-in bridge, so an agent and a human end up producing the
22
22
  - [Requirements](#requirements)
23
23
  - [Install](#install)
24
24
  - [One command (recommended)](#one-command-recommended)
25
+ - [No Node yet? (zero-prerequisite bootstrap)](#no-node-yet-zero-prerequisite-bootstrap)
25
26
  - [Manual install](#manual-install)
26
27
  - [Windows](#windows)
27
28
  - [macOS](#macos)
@@ -73,6 +74,24 @@ The copied command carries a one-time claim that works once and expires after ab
73
74
 
74
75
  Flags, if you need them: `--for <claude|codex|opencode|all|auto|none>` overrides harness detection (`auto` is the default; `none` skips skills entirely), `--no-install` skips the global npm install, `--no-ready` skips the closing readiness check.
75
76
 
77
+ ### No Node yet? (zero-prerequisite bootstrap)
78
+
79
+ If Node isn't installed — or an outdated Node (< 18) is — a bootstrap script can take care of that too: it checks for Node 18+, installs or upgrades it where a package manager is available (Homebrew on macOS, winget on Windows), then runs the same `npx … setup` handoff. It is served straight from the published npm package, so no repository access is needed:
80
+
81
+ macOS / Linux:
82
+
83
+ ```bash
84
+ curl -fsSL https://unpkg.com/slidemind/install.sh | sh -s -- smc1_…
85
+ ```
86
+
87
+ Windows (PowerShell):
88
+
89
+ ```powershell
90
+ powershell -NoProfile -ExecutionPolicy Bypass -Command "& ([scriptblock]::Create((irm https://unpkg.com/slidemind/install.ps1))) smc1_…"
91
+ ```
92
+
93
+ If Node cannot be installed automatically, the script aborts **before** the claim is used — the one-time code stays valid, so install Node from [nodejs.org](https://nodejs.org/en/download) and paste the same command again. (`https://cdn.jsdelivr.net/npm/slidemind/install.sh` is the equivalent fallback host.)
94
+
76
95
  ### Manual install
77
96
 
78
97
  If your organization distributes the Codex plugin through the admin console, the skills are already present and you only install the CLI binary:
@@ -298,15 +317,21 @@ slidemind add-slide [opts] Fold slide(s) into a deck as native .pptx — f
298
317
  --from <file.pptx> --pick <n[,n,...]> --deck <deck> [--at <N>] (local file; host-free; not with --search)
299
318
  slidemind download-result [opts] Download image/icon result(s) into a deck's assets/ (host required)
300
319
  --search <id> --pick <n[,n,...]> --deck <deck> --to <presId> [--icon-color <hex[,hex,...]>]
320
+ slidemind adopt <file.pptx> Rebuild a deck workspace from a .pptx — slidemind-authored slides come back
321
+ as their original jsx, foreign ones are converted to editable jsx where the
322
+ add-in offers it, the rest fold in native
323
+ --deck <slug> --brand <presFP> --no-convert
301
324
  slidemind download-bundle [deck] Materialize the active corporate design locally (host required; lower-level — `ready` covers this)
302
325
  slidemind presentations List PowerPoint windows registered with the add-in
303
326
  slidemind read [presId|file.pptx] Read an open presentation (presId; host required) or a local .pptx on disk (file path; host-free)
304
327
  --mode summary|raw (default raw) [--to <presId>] [--json]
305
328
  slidemind setup <claim> One-command onboarding: connect + global install + skills + ready
306
329
  --for <target> --no-install --no-ready --json
307
- slidemind whoami Show connected environment(s)
330
+ slidemind login [--backend <e>] Sign in to Microsoft 365 (opens your browser) so SharePoint-backed
331
+ downloads and slide extractions work without an open PowerPoint window
332
+ slidemind whoami Show connected environment(s) and their Microsoft 365 sign-in state
308
333
  slidemind use <url|env> Set the default environment
309
- slidemind logout [url|env] Unpair an environment (--all unpairs every one)
334
+ slidemind logout [url|env] Unpair an environment (--all unpairs every one; drops its Microsoft 365 sign-in too)
310
335
  slidemind init --for <target> Install the skill into a harness (clears the bundle cache)
311
336
  <target> = claude | codex | opencode | all | auto (every harness found here)
312
337
  Not needed after `setup` — only when adding a coding agent later
@@ -349,8 +374,13 @@ slidemind push my-deck --to <presId> # required if more than one window is
349
374
  slidemind push my-deck --at 0 # insert after slide 1 (0-based index)
350
375
  slidemind push my-deck --replace # delete every existing slide first (destructive — confirm!)
351
376
  slidemind push my-deck --slides 02,05,07 # push only a subset of source slides
377
+ slidemind push my-deck --replace --accept-conversions # replace even with unconfirmed converted slides
352
378
  ```
353
379
 
380
+ `--replace` refuses while an adopted slide's conversion is still unconfirmed —
381
+ that push is the first replacement of the original. Review the slide (the
382
+ warning names it), or override with `--accept-conversions`.
383
+
354
384
  ---
355
385
 
356
386
  ## Multiple environments (for slidemind developers)