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.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/AGENTS.md +86 -21
- package/README.md +14 -17
- package/bin/slidemind.mjs +65 -4969
- package/package.json +1 -1
- package/skills/create-slides/SKILL.md +113 -74
- package/skills/search/SKILL.md +25 -7
- package/src/bridge/build-and-push.mjs +108 -15
- package/src/bridge/bundle.mjs +69 -0
- package/src/bridge/capture.mjs +71 -8
- package/src/bridge/client/brands.mjs +622 -0
- package/src/bridge/client/engine-routing.mjs +237 -0
- package/src/bridge/client/engine-search.mjs +310 -0
- package/src/bridge/client/engine-slides.mjs +435 -0
- package/src/bridge/client/pane-ops.mjs +335 -0
- package/src/bridge/client/presentations.mjs +587 -0
- package/src/bridge/client/render.mjs +317 -0
- package/src/bridge/client/transport.mjs +695 -0
- package/src/bridge/client.mjs +80 -3452
- package/src/bridge/config.mjs +1 -1
- package/src/bridge/engine-download.mjs +15 -2
- package/src/builders/build.mjs +16 -669
- package/src/builders/draft-check.mjs +376 -0
- package/src/builders/lint.mjs +38 -2
- package/src/builders/preview.html +799 -0
- package/src/builders/review.mjs +23 -4
- package/src/builders/visual-families.mjs +6 -8
- package/src/cli/common.mjs +179 -0
- package/src/cli/concepts.mjs +310 -0
- package/src/cli/connection.mjs +147 -0
- package/src/cli/deck.mjs +608 -0
- package/src/cli/designs.mjs +113 -0
- package/src/cli/diagnose.mjs +141 -0
- package/src/cli/download-bundle.mjs +144 -0
- package/src/cli/engine-helpers.mjs +429 -0
- package/src/cli/engine-results.mjs +320 -0
- package/src/cli/help.mjs +201 -0
- package/src/cli/install.mjs +257 -0
- package/src/cli/read.mjs +120 -0
- package/src/cli/ready.mjs +805 -0
- package/src/cli/search.mjs +608 -0
- package/src/cli/session.mjs +177 -0
- package/src/cli/setup.mjs +282 -0
- package/src/cli/slide-reuse.mjs +334 -0
- package/src/cli/status.mjs +118 -0
- package/src/cli/workspace.mjs +302 -0
- package/src/installers/codex.mjs +1 -1
- 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
|
+
}
|