slidemind 0.3.12 → 0.3.14

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.
Files changed (49) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/AGENTS.md +86 -21
  4. package/README.md +14 -17
  5. package/bin/slidemind.mjs +65 -4969
  6. package/package.json +1 -1
  7. package/skills/create-slides/SKILL.md +113 -74
  8. package/skills/search/SKILL.md +25 -7
  9. package/src/bridge/build-and-push.mjs +108 -15
  10. package/src/bridge/bundle.mjs +69 -0
  11. package/src/bridge/capture.mjs +71 -8
  12. package/src/bridge/client/brands.mjs +622 -0
  13. package/src/bridge/client/engine-routing.mjs +237 -0
  14. package/src/bridge/client/engine-search.mjs +310 -0
  15. package/src/bridge/client/engine-slides.mjs +435 -0
  16. package/src/bridge/client/pane-ops.mjs +335 -0
  17. package/src/bridge/client/presentations.mjs +587 -0
  18. package/src/bridge/client/render.mjs +317 -0
  19. package/src/bridge/client/transport.mjs +695 -0
  20. package/src/bridge/client.mjs +80 -3452
  21. package/src/bridge/config.mjs +1 -1
  22. package/src/bridge/engine-download.mjs +15 -2
  23. package/src/builders/build.mjs +16 -669
  24. package/src/builders/draft-check.mjs +376 -0
  25. package/src/builders/lint.mjs +38 -2
  26. package/src/builders/preview.html +799 -0
  27. package/src/builders/review.mjs +23 -4
  28. package/src/builders/visual-families.mjs +6 -8
  29. package/src/cli/common.mjs +179 -0
  30. package/src/cli/concepts.mjs +310 -0
  31. package/src/cli/connection.mjs +147 -0
  32. package/src/cli/deck.mjs +608 -0
  33. package/src/cli/designs.mjs +113 -0
  34. package/src/cli/diagnose.mjs +141 -0
  35. package/src/cli/download-bundle.mjs +144 -0
  36. package/src/cli/engine-helpers.mjs +429 -0
  37. package/src/cli/engine-results.mjs +320 -0
  38. package/src/cli/help.mjs +201 -0
  39. package/src/cli/install.mjs +257 -0
  40. package/src/cli/read.mjs +120 -0
  41. package/src/cli/ready.mjs +805 -0
  42. package/src/cli/search.mjs +608 -0
  43. package/src/cli/session.mjs +177 -0
  44. package/src/cli/setup.mjs +282 -0
  45. package/src/cli/slide-reuse.mjs +334 -0
  46. package/src/cli/status.mjs +118 -0
  47. package/src/cli/workspace.mjs +302 -0
  48. package/src/installers/codex.mjs +1 -1
  49. package/src/update-check.mjs +231 -0
@@ -0,0 +1,335 @@
1
+ // Ops relayed through one PowerPoint window (insert / export / replace / ping)
2
+ // and the design-source + window pickers.
3
+
4
+ import { isMultiEnv } from "../config.mjs";
5
+ import { isRenderReadyPresentation, isPaneReadyPresentation, tieBreakMatches, resolveBackendForPres } from "./presentations.mjs";
6
+ import { READY_HINT_PICK, bridgeFetch } from "./transport.mjs";
7
+
8
+ // Ask the pane whether its host document is actually still alive (the
9
+ // `ping-document` op — one PowerPoint.run in the webview). This exists for
10
+ // `ready`'s zombie check: a webview whose document was closed keeps its socket
11
+ // (and thus its registry lease) alive, so only a host round-trip tells the
12
+ // truth. Deliberately NOT engineOpWithPaneWait — a probe must fail fast, never
13
+ // park in the 120s pane wait. Callers decide what a failure means; this just
14
+ // returns the ack or throws the transport error.
15
+ export async function pingPresentation({ presId, backendURL, timeoutMs = 12000 }) {
16
+ if (!presId) throw new Error("pingPresentation: presId is required");
17
+ const res = await bridgeFetch(
18
+ `/api/cli/presentations/${encodeURIComponent(presId)}/ping`,
19
+ {
20
+ method: "POST",
21
+ headers: { "content-type": "application/json" },
22
+ body: JSON.stringify({}),
23
+ timeoutMs,
24
+ },
25
+ backendURL,
26
+ );
27
+ return res.json(); // { ok, presId?, fileName? } | { ok: false, code, error }
28
+ }
29
+
30
+ export function ensurePaneAckOk(data, operation) {
31
+ if (!data || data.ok !== false) return data ?? {};
32
+ const detail = data.error || `${operation} failed`;
33
+ const err = new Error(detail);
34
+ err.code = data.code || `${operation}_failed`;
35
+ err.body = data;
36
+ throw err;
37
+ }
38
+
39
+ export async function insertSlides({
40
+ presId,
41
+ base64,
42
+ insertAt = null,
43
+ sourceSlideIds = null,
44
+ operationId = null,
45
+ sourceTagsBySlide = null,
46
+ backendURL = null,
47
+ }) {
48
+ if (!presId) throw new Error("insertSlides: presId is required");
49
+ if (!base64) throw new Error("insertSlides: base64 is required");
50
+ const be = backendURL || (await resolveBackendForPres(presId));
51
+ const body = { base64, insertAt, sourceSlideIds, operationId };
52
+ // Deck-embedded source payloads for the pane's post-insert tag write,
53
+ // aligned with the inserted slides in document order.
54
+ if (Array.isArray(sourceTagsBySlide) && sourceTagsBySlide.some(Boolean)) {
55
+ body.sourceTagsBySlide = sourceTagsBySlide;
56
+ }
57
+ const res = await bridgeFetch(
58
+ `/api/cli/presentations/${encodeURIComponent(presId)}/insert`,
59
+ {
60
+ method: "POST",
61
+ headers: { "content-type": "application/json" },
62
+ body: JSON.stringify(body),
63
+ timeoutMs:
64
+ parseInt(process.env.SLIDEMIND_BRIDGE_MUTATION_TIMEOUT_MS || "1805000", 10) ||
65
+ 1805000,
66
+ },
67
+ be,
68
+ );
69
+ return ensurePaneAckOk(await res.json().catch(() => ({})), "insert");
70
+ }
71
+
72
+ // Read-only sibling of the mutation ops: export the LIVE slides carrying the
73
+ // given GUIDs, so a `--replace` push can see what the user changed on screen
74
+ // BEFORE it wipes it (plan Phase C1). `guids` omitted ⇒ the whole deck.
75
+ //
76
+ // The ack's `tags` come from the Office.js tag API, NOT from the exported
77
+ // bytes: exportAsBase64 strips custom tags (office-js #6104), so a slide's
78
+ // drift BASELINE is recoverable only from `tags` — never from `base64`, which
79
+ // carries the CURRENT state and nothing else. `deck.unmatched` lists the live
80
+ // slides that are not in the requested set (what a whole-deck replace would
81
+ // delete); `truncated` names GUIDs that matched but did not fit the relay
82
+ // frame, to be re-requested.
83
+ export async function exportSlides({
84
+ presId,
85
+ guids = null,
86
+ presentationId = null,
87
+ backendURL = null,
88
+ }) {
89
+ if (!presId) throw new Error("exportSlides: presId is required");
90
+ const be = backendURL || (await resolveBackendForPres(presId));
91
+ const body = {};
92
+ if (Array.isArray(guids) && guids.length > 0) body.guids = guids;
93
+ if (presentationId) body.presentationId = presentationId;
94
+ const res = await bridgeFetch(
95
+ `/api/cli/presentations/${encodeURIComponent(presId)}/export-slides`,
96
+ {
97
+ method: "POST",
98
+ headers: { "content-type": "application/json" },
99
+ body: JSON.stringify(body),
100
+ // Same pane round trip as the mutations (a suspended pane has to wake),
101
+ // and the payload is slide bytes.
102
+ timeoutMs:
103
+ parseInt(process.env.SLIDEMIND_BRIDGE_MUTATION_TIMEOUT_MS || "1805000", 10) ||
104
+ 1805000,
105
+ },
106
+ be,
107
+ );
108
+ return ensurePaneAckOk(await res.json().catch(() => ({})), "export-slides");
109
+ }
110
+
111
+ export async function replaceSlides({
112
+ presId,
113
+ base64,
114
+ operationId = null,
115
+ sourceTagsBySlide = null,
116
+ backendURL = null,
117
+ }) {
118
+ if (!presId) throw new Error("replaceSlides: presId is required");
119
+ if (!base64) throw new Error("replaceSlides: base64 is required");
120
+ const be = backendURL || (await resolveBackendForPres(presId));
121
+ const replaceBody = { base64, operationId };
122
+ if (Array.isArray(sourceTagsBySlide) && sourceTagsBySlide.some(Boolean)) {
123
+ replaceBody.sourceTagsBySlide = sourceTagsBySlide;
124
+ }
125
+ const res = await bridgeFetch(
126
+ `/api/cli/presentations/${encodeURIComponent(presId)}/replace`,
127
+ {
128
+ method: "POST",
129
+ headers: { "content-type": "application/json" },
130
+ body: JSON.stringify(replaceBody),
131
+ timeoutMs:
132
+ parseInt(process.env.SLIDEMIND_BRIDGE_MUTATION_TIMEOUT_MS || "1805000", 10) ||
133
+ 1805000,
134
+ },
135
+ be,
136
+ );
137
+ return ensurePaneAckOk(await res.json().catch(() => ({})), "replace");
138
+ }
139
+
140
+ // Non-throwing, PURE (no network) sibling of pickPresentationEntry that answers
141
+ // the one question `slidemind ready` needs: which corporate design should this
142
+ // session bind to? Where pickPresentationEntry PROMPTS the user by throwing,
143
+ // resolveDesignSource returns a STRUCTURED verdict the CLI turns into a bound
144
+ // bundle, a single blocker, or a "which design?" question — no exceptions as
145
+ // control flow.
146
+ //
147
+ // The bar is RENDER-ready, never pane-ready: the add-in is the source of truth
148
+ // for the design, and a backgrounded (pane-suspended) window still serves its
149
+ // bundle and still renders — so it is a valid design source (the caller reports
150
+ // `paneSuspended` but stays exit 0). Pane readiness only matters later, when a
151
+ // push/search actually needs the live pane.
152
+ //
153
+ // The ONE user-facing ambiguity is ≥2 render-ready windows on DISTINCT designs
154
+ // (distinct `brandId`). We deliberately do NOT let the deck's own bundle ref
155
+ // (`deckPresFP`) break that tie — the open add-in wins; a deck never pins a
156
+ // design preference. The deck ref only:
157
+ // (a) tie-breaks ONE window mirrored across paired backends (same presId on
158
+ // dev+prod) via tieBreakMatches — the SAME rule pickPresentationEntry
159
+ // uses, and its only sanctioned use here; and
160
+ // (b) surfaces the matching candidate as `suggested`, so the agent can say
161
+ // "this deck was last built against X" WITHOUT the CLI silently deciding.
162
+ //
163
+ // Returns exactly one of:
164
+ // { status: "none" } — nothing render-ready is open.
165
+ // { status: "unknown_presid", presId } — an explicit --to names no
166
+ // render-ready window.
167
+ // { status: "resolved", entry } — the RAW registry entry (backendURL
168
+ // intact; the caller strips it before
169
+ // any user-facing output).
170
+ // { status: "ambiguous", candidates, suggested }
171
+ // — candidates deduped to ONE entry per
172
+ // distinct design; suggested is the
173
+ // presId of the deck-ref match, else
174
+ // null.
175
+ export function resolveDesignSource(
176
+ registry,
177
+ { explicitPresId = null, deckPresFP = null } = {},
178
+ ) {
179
+ const all = (Array.isArray(registry) ? registry : []).filter(
180
+ isRenderReadyPresentation,
181
+ );
182
+ if (all.length === 0) return { status: "none" };
183
+
184
+ // Group by DESIGN. A render-ready pane always carries a brandId, but be
185
+ // defensive: a missing brandId is treated as its OWN pseudo-design keyed by
186
+ // presId so two CD-less windows never collapse into one false match.
187
+ const designKey = (p) =>
188
+ p.brandId != null && p.brandId !== ""
189
+ ? `brand:${p.brandId}`
190
+ : `pres:${p.presId}`;
191
+
192
+ // Collapse a set of entries to one representative per distinct design,
193
+ // preferring a live (pane-ready) pane over a suspended mirror, then compute
194
+ // the deck-ref `suggested` presId over those representatives.
195
+ const dedupeCandidates = (entries) => {
196
+ const byKey = new Map();
197
+ for (const p of entries) {
198
+ const k = designKey(p);
199
+ const cur = byKey.get(k);
200
+ if (!cur || (!isPaneReadyPresentation(cur) && isPaneReadyPresentation(p))) {
201
+ byKey.set(k, p);
202
+ }
203
+ }
204
+ const candidates = [...byKey.values()];
205
+ const suggested =
206
+ (deckPresFP && candidates.find((c) => c.brandId === deckPresFP)?.presId) ||
207
+ null;
208
+ return { candidates, suggested };
209
+ };
210
+
211
+ if (explicitPresId) {
212
+ const matches = all.filter((p) => p.presId === explicitPresId);
213
+ if (matches.length === 0) {
214
+ return { status: "unknown_presid", presId: explicitPresId };
215
+ }
216
+ if (matches.length === 1) return { status: "resolved", entry: matches[0] };
217
+ // >1 render-ready match == ONE window mirrored across paired backends.
218
+ // Tie-break exactly as pickPresentationEntry; only a genuine tie is asked.
219
+ const winner = tieBreakMatches(matches, deckPresFP);
220
+ if (winner) return { status: "resolved", entry: winner };
221
+ return { status: "ambiguous", ...dedupeCandidates(matches) };
222
+ }
223
+
224
+ // A live pane outranks a suspended one for the DESIGN decision: a
225
+ // backgrounded — or closed and not yet purged — window must not turn the one
226
+ // design the user actually has in front of them into a question. Only when
227
+ // nothing is live do the suspended windows decide.
228
+ const live = all.filter(isPaneReadyPresentation);
229
+ const pool = live.length > 0 ? live : all;
230
+ const distinctDesigns = new Set(pool.map(designKey));
231
+ if (distinctDesigns.size === 1) {
232
+ // One design open (possibly one window mirrored across backends, or several
233
+ // windows on the SAME CD). Pick via the mirror tie-break, else a live pane,
234
+ // else the first — the design is unambiguous either way.
235
+ const entry =
236
+ tieBreakMatches(pool, deckPresFP) ??
237
+ pool.find(isPaneReadyPresentation) ??
238
+ pool[0];
239
+ return { status: "resolved", entry };
240
+ }
241
+ // ≥2 distinct designs open — the only case that asks the user (the deck ref
242
+ // does NOT auto-pick; it only annotates `suggested`).
243
+ return { status: "ambiguous", ...dedupeCandidates(pool) };
244
+ }
245
+
246
+ // Pick a target presentation given the (possibly multi-backend) registry and an
247
+ // optional explicit presId. Returns the matched registry ENTRY (which carries
248
+ // `backendURL`/`env` after a fan-out list), or throws with a helpful message if
249
+ // the choice is ambiguous. Callers that already hold the entry can pass its
250
+ // `backendURL` straight to the targeted ops, skipping a second fan-out.
251
+ // `preferBrandId` (the deck's bundle-ref presFP) tie-breaks a presId open on
252
+ // several backends — see tieBreakMatches.
253
+ export function pickPresentationEntry(
254
+ registry,
255
+ explicitPresId,
256
+ { preferBrandId = null } = {},
257
+ ) {
258
+ if (explicitPresId) {
259
+ const matches = registry.filter((p) => p.presId === explicitPresId);
260
+ if (matches.length === 0) {
261
+ const ids = registry.map((p) => p.presId).join(", ") || "(none registered)";
262
+ throw new Error(
263
+ `No open PowerPoint window registered with presId='${explicitPresId}'. Live: ${ids}`,
264
+ );
265
+ }
266
+ if (matches.length > 1) {
267
+ const winner = tieBreakMatches(matches, preferBrandId);
268
+ if (winner) return winner;
269
+ const envs = matches.map((m) => m.env || "?").join(", ");
270
+ throw new Error(
271
+ `Presentation '${explicitPresId}' is open on multiple environments: ${envs}, ` +
272
+ "and no single connected one has the deck's corporate design active. " +
273
+ "Disambiguate with --backend <env|label> (or SLIDEMIND_BACKEND).",
274
+ );
275
+ }
276
+ return matches[0];
277
+ }
278
+ if (registry.length === 0) {
279
+ throw new Error(
280
+ "No PowerPoint windows registered with the slidemind add-in. " +
281
+ "Open a presentation, ensure the add-in is loaded, and try again.",
282
+ );
283
+ }
284
+ if (registry.length === 1) return registry[0];
285
+ // ONE window mirrored across several paired backends is not a user choice —
286
+ // apply the same tie-break as the explicit-presId path. Several DIFFERENT
287
+ // windows stay a user decision.
288
+ if (new Set(registry.map((p) => p.presId)).size === 1) {
289
+ const winner = tieBreakMatches(registry, preferBrandId);
290
+ if (winner) return winner;
291
+ }
292
+ const list = registry
293
+ .map(
294
+ (p) =>
295
+ ` ${p.presId}\t${p.env && isMultiEnv() ? `[${p.env}]\t` : ""}${p.title || p.fileName || "(untitled)"}` +
296
+ (p.connected === false || p.suspendedAt ? "\t(suspended — PowerPoint in background)" : ""),
297
+ )
298
+ .join("\n");
299
+ throw new Error(
300
+ `Multiple PowerPoint windows are open. Pick one with --to <presId>:\n${list}\n${READY_HINT_PICK}`,
301
+ );
302
+ }
303
+
304
+ // Window-AGNOSTIC picker for engine ops (discovery / search / result view). The
305
+ // engine catalog is per-user and NOT bound to any window, so any connected pane
306
+ // can run the search — there is nothing for the caller to disambiguate. When no
307
+ // explicit presId is given we attach to one automatically (preferring a live,
308
+ // non-suspended pane so we don't have to wake a backgrounded one) instead of
309
+ // throwing the "multiple windows, pick --to" error pickPresentationEntry raises.
310
+ // An explicit presId still resolves exactly as pickPresentationEntry.
311
+ //
312
+ // CAVEAT: the searchId the add-in returns is bound to the pane that ran the
313
+ // search (each window keeps its own search-context store). So follow-up
314
+ // view/extract/insert must target the SAME entry this returns — surface its
315
+ // presId to the caller so it can thread it back.
316
+ export function pickAnyPresentationEntry(registry, explicitPresId, opts = {}) {
317
+ if (explicitPresId) return pickPresentationEntry(registry, explicitPresId, opts);
318
+ if (!Array.isArray(registry) || registry.length === 0) {
319
+ throw new Error(
320
+ "No PowerPoint windows registered with the slidemind add-in. " +
321
+ "Open a presentation, ensure the add-in is loaded, and try again.",
322
+ );
323
+ }
324
+ return (
325
+ registry.find(isPaneReadyPresentation) ??
326
+ registry.find((p) => p.connected !== false && !p.suspendedAt) ??
327
+ registry[0]
328
+ );
329
+ }
330
+
331
+ // Back-compat shim: returns just the presId string. Prefer
332
+ // pickPresentationEntry when you need the origin backend.
333
+ export function pickPresentation(registry, explicitPresId) {
334
+ return pickPresentationEntry(registry, explicitPresId).presId;
335
+ }