slidemind 0.3.1 → 0.3.3

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.3.1",
3
+ "version": "0.3.3",
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.3.1",
3
+ "version": "0.3.3",
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
@@ -230,7 +230,7 @@ slidemind uninstall --for <target> # remove the skill (also clears the bundle
230
230
 
231
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.
232
232
 
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`.
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 30 min — nothing expires while the tab waits — 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`.
234
234
 
235
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.
236
236
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slidemind",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
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": {
@@ -2885,14 +2885,24 @@ export async function insertSlidesFromSearch({
2885
2885
  : [];
2886
2886
  insertedCount += landed.length || 1;
2887
2887
  insertedSlideIds.push(...landed);
2888
- fs.rmSync(slidePath, { force: true });
2888
+ bestEffortRm(slidePath);
2889
2889
  }
2890
2890
  return { ok: true, insertedCount, insertedSlideIds, presId: livePresId };
2891
2891
  } finally {
2892
- fs.rmSync(dir, { recursive: true, force: true });
2892
+ bestEffortRm(dir);
2893
2893
  }
2894
2894
  }
2895
2895
 
2896
+ // Remove a temp file/dir that bytes were JUST streamed into. Windows may still
2897
+ // hold a handle on it at cleanup time (antivirus scanning the fresh write), so
2898
+ // plain rmSync races into EBUSY/ENOTEMPTY there — retry, and swallow what's
2899
+ // left: cleanup failure must never mask an operation that succeeded.
2900
+ function bestEffortRm(p) {
2901
+ try {
2902
+ fs.rmSync(p, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
2903
+ } catch {}
2904
+ }
2905
+
2896
2906
  // Fetch a chosen slide-search result's thumbnail/preview as a data URI for
2897
2907
  // on-demand viewing (the agent doesn't have to extract the whole .pptx just to
2898
2908
  // look at a slide). Relayed through the pane.
@@ -2942,7 +2952,7 @@ export async function viewSlide({
2942
2952
  dataUri: `data:${mimeType};base64,${fs.readFileSync(tmp).toString("base64")}`,
2943
2953
  };
2944
2954
  } finally {
2945
- fs.rmSync(dir, { recursive: true, force: true });
2955
+ bestEffortRm(dir);
2946
2956
  }
2947
2957
  }
2948
2958
  const be = backendURL || (await resolveBackendForPres(presId));
@@ -63,10 +63,14 @@ function sharePointScope(resource) {
63
63
 
64
64
  const DEFAULT_AUTHORITY = "https://login.microsoftonline.com";
65
65
 
66
- // How long the loopback listener waits for the browser to come back. Long
67
- // enough for an MFA prompt and a password manager; short enough that a login
68
- // the user abandoned doesn't hold a terminal forever.
69
- const LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
66
+ // How long the loopback listener waits for the browser to come back. Generous
67
+ // on purpose: the user may be away from the machine when the tab opens (MFA
68
+ // phone in another room, a coffee), and NOTHING here expires meanwhile — the
69
+ // authorize page keeps, the auth code is only minted when they finish, and the
70
+ // listener is a local ephemeral port. The cap only stops an abandoned login
71
+ // from holding a terminal forever. (`setup` passes its own much shorter cap —
72
+ // mid-onboarding is not a parked sign-in.)
73
+ const LOGIN_TIMEOUT_MS = 30 * 60 * 1000;
70
74
 
71
75
  // Refresh this long before the access token actually expires, so a token handed
72
76
  // to a long download doesn't die mid-transfer.