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,320 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ import { fail, resolveDeckArg, SRC_DIR } from "./common.mjs";
6
+ import { resolveEngineRouting, deckBrandId, loginRequiredMessage, failOnEngineBlocker, describeEngineByteFailure, parseSearchIdArg, parsePicks, parseIconColors, describeReadSourceFailure } from "./engine-helpers.mjs";
7
+
8
+ export async function cmdViewSlide(rest) {
9
+ let searchId = null;
10
+ let pick = null;
11
+ let to = null;
12
+ let outDir = null;
13
+ for (let i = 0; i < rest.length; i++) {
14
+ const a = rest[i];
15
+ if (a === "--search") {
16
+ searchId = parseSearchIdArg(rest[++i]);
17
+ } else if (a === "--pick") {
18
+ const next = rest[++i];
19
+ const n = parseInt(next, 10);
20
+ if (!Number.isFinite(n) || n <= 0) {
21
+ fail(`--pick expects a positive selection number, got '${next}'`);
22
+ }
23
+ pick = n;
24
+ } else if (a === "--to") {
25
+ to = rest[++i];
26
+ if (!to) fail("--to expects a presId");
27
+ } else if (a === "--out" || a === "-o") {
28
+ outDir = rest[++i];
29
+ if (!outDir) fail("--out expects a directory");
30
+ } else if (a.startsWith("-")) {
31
+ fail(`unknown flag: ${a}`);
32
+ } else {
33
+ fail(`unexpected argument: ${a}`);
34
+ }
35
+ }
36
+ if (searchId === null || pick === null) {
37
+ fail("usage: slidemind view-slide --search <id> --pick <n> [--to <presId>] [--out <dir>]");
38
+ }
39
+ const client = await import(
40
+ pathToFileURL(path.join(SRC_DIR, "bridge", "client.mjs")).href
41
+ );
42
+ const { resolveAssetExt } = await import(
43
+ pathToFileURL(path.join(SRC_DIR, "bridge", "engine-download.mjs")).href
44
+ );
45
+ const routing = await resolveEngineRouting(client, { to, searchId });
46
+ let res;
47
+ try {
48
+ res = await client.viewSlide({
49
+ presId: routing.presId,
50
+ searchId,
51
+ selection: pick,
52
+ backendURL: routing.backendURL,
53
+ });
54
+ } catch (err) {
55
+ await failOnEngineBlocker(client, err, { to });
56
+ }
57
+ if (!res?.ok || !res.dataUri) {
58
+ fail(`view-slide failed: ${res?.error ?? "no image returned"}`);
59
+ }
60
+ const m = /^data:([^;]+);base64,(.+)$/.exec(res.dataUri);
61
+ if (!m) fail("view-slide returned an unparseable data URI");
62
+ const dir = outDir
63
+ ? path.resolve(process.cwd(), outDir)
64
+ : fs.mkdtempSync(path.join(os.tmpdir(), "slidemind-view-"));
65
+ fs.mkdirSync(dir, { recursive: true });
66
+ // Extension from the BYTES, not the declared MIME — engines inherit a
67
+ // Content-Type from upstream and it is demonstrably wrong for some corpus
68
+ // images (PNG served as image/jpeg).
69
+ const bytes = Buffer.from(m[2], "base64");
70
+ const target = path.join(
71
+ dir,
72
+ `slide-${searchId}-${pick}.${resolveAssetExt(m[1], bytes)}`,
73
+ );
74
+ fs.writeFileSync(target, bytes);
75
+ process.stdout.write(
76
+ `Wrote preview of result ${pick} (searchId ${searchId}) to ${target}\n`,
77
+ );
78
+ }
79
+
80
+ export async function cmdReadSource(rest) {
81
+ let searchId = null;
82
+ let pick = null;
83
+ let fromSlide = null;
84
+ let toSlide = null;
85
+ let to = null;
86
+ let outFile = null;
87
+ for (let i = 0; i < rest.length; i++) {
88
+ const a = rest[i];
89
+ if (a === "--search") {
90
+ searchId = parseSearchIdArg(rest[++i]);
91
+ } else if (a === "--pick") {
92
+ const next = rest[++i];
93
+ const n = parseInt(next, 10);
94
+ if (!Number.isFinite(n) || n <= 0) {
95
+ fail(`--pick expects a positive selection number, got '${next}'`);
96
+ }
97
+ pick = n;
98
+ } else if (a === "--from-slide") {
99
+ const next = rest[++i];
100
+ const n = parseInt(next, 10);
101
+ if (!Number.isFinite(n) || n <= 0) {
102
+ fail(`--from-slide expects a positive slide number, got '${next}'`);
103
+ }
104
+ fromSlide = n;
105
+ } else if (a === "--to-slide") {
106
+ const next = rest[++i];
107
+ const n = parseInt(next, 10);
108
+ if (!Number.isFinite(n) || n <= 0) {
109
+ fail(`--to-slide expects a positive slide number, got '${next}'`);
110
+ }
111
+ toSlide = n;
112
+ } else if (a === "--to") {
113
+ // Deliberately the WINDOW, matching every other search-scoped command.
114
+ // The slide window is --from-slide/--to-slide.
115
+ to = rest[++i];
116
+ if (!to) fail("--to expects a presId");
117
+ } else if (a === "--out" || a === "-o") {
118
+ outFile = rest[++i];
119
+ if (!outFile) fail("--out expects a file path");
120
+ } else if (a.startsWith("-")) {
121
+ fail(`unknown flag: ${a}`);
122
+ } else {
123
+ fail(`unexpected argument: ${a}`);
124
+ }
125
+ }
126
+ if (searchId === null || pick === null) {
127
+ fail(
128
+ "usage: slidemind read-source --search <id> --pick <n> [--from-slide <n>] [--to-slide <n>] [--to <presId>] [--out <file>]",
129
+ );
130
+ }
131
+ const client = await import(
132
+ pathToFileURL(path.join(SRC_DIR, "bridge", "client.mjs")).href
133
+ );
134
+ // Pane path: the searchId is presentation-scoped, so --to targets the window
135
+ // the search ran on — same rule as view-slide. Direct path: the record is the
136
+ // backend's, so no window is involved.
137
+ const routing = await resolveEngineRouting(client, { to, searchId });
138
+ let res;
139
+ try {
140
+ res = await client.readSourcePresentation({
141
+ presId: routing.presId,
142
+ searchId,
143
+ selection: pick,
144
+ fromSlide,
145
+ toSlide,
146
+ backendURL: routing.backendURL,
147
+ });
148
+ } catch (err) {
149
+ // The login_required line is authored once and knows about an open pane;
150
+ // everything else is this command's own vocabulary.
151
+ fail(
152
+ (await loginRequiredMessage(client, err, { to })) ??
153
+ describeReadSourceFailure(err, searchId, pick),
154
+ );
155
+ }
156
+ if (!res?.markdown) {
157
+ fail("read-source returned no content. Try the search again, then re-pick.");
158
+ }
159
+ if (outFile) {
160
+ const target = path.resolve(process.cwd(), outFile);
161
+ fs.mkdirSync(path.dirname(target), { recursive: true });
162
+ fs.writeFileSync(target, res.markdown, "utf8");
163
+ process.stdout.write(
164
+ `Wrote the source presentation of result ${pick} (searchId ${searchId}) to ${target}\n`,
165
+ );
166
+ } else {
167
+ process.stdout.write(res.markdown.endsWith("\n") ? res.markdown : `${res.markdown}\n`);
168
+ }
169
+ // Truncation is the one thing a reader must not miss: the body they just got
170
+ // is a slide map, not the deck.
171
+ if (res.truncated) {
172
+ process.stderr.write(
173
+ `note: this deck was too long to return in full — the output is its slide map. ` +
174
+ `Re-run with --from-slide/--to-slide to read a range.\n`,
175
+ );
176
+ } else if (res.totalSlideCount && res.slideCount < res.totalSlideCount) {
177
+ process.stderr.write(
178
+ `note: showing ${res.slideCount} of ${res.totalSlideCount} slides.\n`,
179
+ );
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Turn a source-presentation failure into the action that actually fixes it.
185
+ * A login_required refusal is handled one level up, in loginRequiredMessage —
186
+ * it is the one blocker only the USER can clear and it reads identically
187
+ * across every command.
188
+ */
189
+
190
+ export async function cmdDownloadResult(rest) {
191
+ let searchId = null;
192
+ let picks = null;
193
+ let deck = null;
194
+ let to = null;
195
+ let asJson = false;
196
+ let iconColorsRaw = null;
197
+ for (let i = 0; i < rest.length; i++) {
198
+ const a = rest[i];
199
+ if (a === "--search") {
200
+ searchId = parseSearchIdArg(rest[++i]);
201
+ } else if (a === "--pick") {
202
+ picks = parsePicks(rest[++i]);
203
+ } else if (a === "--deck") {
204
+ deck = rest[++i];
205
+ if (!deck) fail("--deck expects a deck name or path");
206
+ } else if (a === "--to") {
207
+ to = rest[++i];
208
+ if (!to) fail("--to expects a presId");
209
+ } else if (a === "--json") {
210
+ asJson = true;
211
+ } else if (a === "--icon-color" || a === "--icon-colour") {
212
+ iconColorsRaw = rest[++i];
213
+ if (!iconColorsRaw) fail("--icon-color expects a hex colour (or comma-separated list aligned to --pick)");
214
+ } else if (a.startsWith("-")) {
215
+ fail(`unknown flag: ${a}`);
216
+ } else {
217
+ fail(`unexpected argument: ${a}`);
218
+ }
219
+ }
220
+ if (searchId === null || !picks || !deck) {
221
+ fail(
222
+ "usage: slidemind download-result --search <id> --pick <n[,n,...]> --deck <deck> [--to <presId>] [--icon-color <hex[,hex,...]>] [--json]",
223
+ );
224
+ }
225
+ const iconColors = parseIconColors(iconColorsRaw, picks.length);
226
+ const deckDir = resolveDeckArg(deck);
227
+ const assetsDir = path.join(deckDir, "assets");
228
+ fs.mkdirSync(assetsDir, { recursive: true });
229
+
230
+ const client = await import(
231
+ pathToFileURL(path.join(SRC_DIR, "bridge", "client.mjs")).href
232
+ );
233
+ // Shared image/icon download logic (src/bridge/engine-download.mjs).
234
+ const { downloadOneResult } = await import(
235
+ pathToFileURL(path.join(SRC_DIR, "bridge", "engine-download.mjs")).href
236
+ );
237
+ // Direct path: an opaque searchId routes to the record's own environment,
238
+ // else the deck's bound design decides; no window needed. Pane path: the
239
+ // searchId is presentation-scoped, so --to targets the window the search ran
240
+ // on (without it, >1 open window is a dead-end).
241
+ const routing = await resolveEngineRouting(client, {
242
+ to,
243
+ brandId: await deckBrandId(deckDir),
244
+ searchId,
245
+ });
246
+
247
+ // Concurrent — each download is its own bridge round-trip; allSettled so one
248
+ // bad selection doesn't sink the rest of the batch.
249
+ const settled = await Promise.allSettled(
250
+ picks.map((selection, i) =>
251
+ downloadOneResult({
252
+ client,
253
+ presId: routing.presId,
254
+ backendURL: routing.backendURL,
255
+ searchId,
256
+ assetsDir,
257
+ selection,
258
+ iconColor: iconColors[i],
259
+ }),
260
+ ),
261
+ );
262
+ // One user-clearable blocker sinks the whole batch identically — every pick
263
+ // would fail the same way, and the fix is one action, not N error lines.
264
+ const stale = settled.find(
265
+ (r) => r.status === "rejected" && client.loginRequiredBlocker?.(r.reason),
266
+ );
267
+ if (stale) fail(await loginRequiredMessage(client, stale.reason, { to }));
268
+ const results = await Promise.all(
269
+ settled.map(async (r, i) =>
270
+ r.status === "fulfilled"
271
+ ? r.value
272
+ : {
273
+ selection: picks[i],
274
+ error:
275
+ (await describeEngineByteFailure(client, r.reason, {
276
+ backendURL: routing.backendURL,
277
+ })) ??
278
+ r.reason?.message ??
279
+ String(r.reason),
280
+ },
281
+ ),
282
+ );
283
+ const { formatSize } = await import(
284
+ pathToFileURL(path.join(SRC_DIR, "bridge", "image-dimensions.mjs")).href
285
+ );
286
+ let ok = 0;
287
+ let anySized = false;
288
+ const lines = [];
289
+ for (const r of results) {
290
+ if (r.error) {
291
+ lines.push(` ✗ selection ${r.selection}: ${r.error}`);
292
+ continue;
293
+ }
294
+ ok++;
295
+ const note = r.recolor?.note ? ` — ${r.recolor.note}` : "";
296
+ // The intrinsic size is the agent's only handle on the asset's aspect
297
+ // ratio — without it the <img> box is a guess and the render stretches.
298
+ const dims = formatSize(r.size);
299
+ anySized ||= Boolean(dims);
300
+ const size = dims ? ` ${dims}` : " (size unknown)";
301
+ lines.push(` ${r.reference}${size} (${r.recolor?.mode ?? "?"})${note}`);
302
+ }
303
+ if (asJson) {
304
+ process.stdout.write(
305
+ `${JSON.stringify({ searchId, assetsDir, downloaded: ok, results: results.map((r) => (r.error ? { selection: r.selection, error: r.error } : { selection: r.selection, file: r.reference, size: r.size ?? null, recolor: r.recolor ?? null })) }, null, 2)}\n`,
306
+ );
307
+ if (ok === 0) process.exit(1);
308
+ return;
309
+ }
310
+ process.stdout.write(
311
+ `Downloaded ${ok}/${results.length} result(s) into ${path.relative(process.cwd(), assetsDir)}:\n` +
312
+ lines.join("\n") +
313
+ `\nReference each from slide JSX as <img src="./assets/...">.\n` +
314
+ (anySized
315
+ ? `Sizes above are the assets' intrinsic width×height — give each <img> a box ` +
316
+ `with that aspect ratio, or the render stretches it to whatever box you wrote.\n`
317
+ : ""),
318
+ );
319
+ if (ok === 0) process.exit(1);
320
+ }
@@ -0,0 +1,201 @@
1
+ import path from "node:path";
2
+ import { readPackageJson } from "./common.mjs";
3
+
4
+ // `printHelp("view-draft")` prints only that command's block — every command
5
+ // accepts `--help` (agents reach for it before the full 15KB listing).
6
+ export function printHelp(cmd = null) {
7
+ const pkg = readPackageJson();
8
+ const text =
9
+ `slidemind ${pkg.version} — drive the slidemind PowerPoint add-in to build native, on-brand decks\n` +
10
+ `\n` +
11
+ `Usage:\n` +
12
+ ` slidemind setup <claim> One-command onboarding. Copy the command from PowerPoint →\n` +
13
+ ` slidemind → Settings → General → Connect CLI and paste it:\n` +
14
+ ` it connects this machine, installs slidemind globally, deploys\n` +
15
+ ` the skills, and runs 'ready'. Designed for 'npx slidemind@latest'.\n` +
16
+ ` --for <target> harness to deploy skills into (default: auto — every\n` +
17
+ ` coding agent found on this machine; none to skip)\n` +
18
+ ` --no-install skip the global npm install (already installed)\n` +
19
+ ` --no-ready skip the closing readiness check\n` +
20
+ ` --no-login skip the Microsoft 365 browser sign-in offer\n` +
21
+ ` --json machine-readable result\n` +
22
+ ` slidemind ready [deck] One-command session setup — run this FIRST each session.\n` +
23
+ ` Verifies authorization/connection/CLI version, resolves the open\n` +
24
+ ` design, materializes its bundle, and (with a deck) binds it.\n` +
25
+ ` Exit 0 = ready; non-zero prints one actionable blocker.\n` +
26
+ ` A deck already bound to a design readies with PowerPoint closed.\n` +
27
+ ` --to <presId> bind to a specific open window\n` +
28
+ ` --brand <id> bind to a design by id (presFP) — no window needed\n` +
29
+ ` --force re-download the bundle even if current\n` +
30
+ ` --connect-only check the connection only (no design/bundle)\n` +
31
+ ` --diagnose read-only report instead: both ladders (backend\n` +
32
+ ` authoring / live PowerPoint), every paired backend,\n` +
33
+ ` Microsoft 365 state and every registered window\n` +
34
+ ` --json machine-readable readiness payload\n` +
35
+ ` With a deck, also prints its stage (plan / concepts / assets / slides /\n` +
36
+ ` draft check / review) and the next command — read from disk.\n` +
37
+ ` slidemind new <name> Scaffold ./decks/<name>/ (auto-binds to the open design)\n` +
38
+ ` --brand <presFP> bind to a chosen (cached) design instead\n` +
39
+ ` --to <presId> bind to a specific open window\n` +
40
+ ` --no-bind skip the auto-bind\n` +
41
+ ` slidemind plan <deck> Plan sign-off gate on <deck>/plan.md: opens it in the browser\n` +
42
+ ` as a markdown editor + live preview + overall feedback box,\n` +
43
+ ` blocks until the user decides, prints DECISION: approved | comments\n` +
44
+ ` --port <n> bind a fixed port (default: ephemeral)\n` +
45
+ ` slidemind concepts <deck> After plan sign-off, generate one concept mockup PNG per planned\n` +
46
+ ` slide into <deck>/concepts/concept-slide-NN.png (bound deck,\n` +
47
+ ` pane-free — no PowerPoint window needed).\n` +
48
+ ` Read each as a design target, then rebuild it as slide JSX.\n` +
49
+ ` Optional aid — skips cleanly (exit 0) if unavailable.\n` +
50
+ ` --concept <image>[@N,M] the user's own sketch/mockup IS the\n` +
51
+ ` mockup for slides N,M (default: all)\n` +
52
+ ` --style <image>[@N,M] the user's look-and-feel reference,\n` +
53
+ ` leads the generation (repeatable)\n` +
54
+ ` --json machine-readable result (paths, missing, status)\n` +
55
+ ` Always ends on one line: CONCEPTS: <n>/<planned> <status>\n` +
56
+ ` slidemind review <deck> Open a spatial preview review; blocks until the user\n` +
57
+ ` approves or leaves comments (host-free)\n` +
58
+ ` --port <n> bind a fixed port (default: ephemeral)\n` +
59
+ ` --allow-draft-defects open even while the last view-draft's measured defects stand\n` +
60
+ ` slidemind build-pptx <deck> Export native .pptx — via the add-in for compose decks (pane-free: the deck's design, else the one design cached/listed; a window only with --to), LOCALLY for template packs (host-free)\n` +
61
+ ` --pack <dir> explicit template-pack directory (plugin integrations)\n` +
62
+ ` --to <presId> target a specific window (required if >1 open)\n` +
63
+ ` --out <path> output path (default <deck>/<deck>.pptx)\n` +
64
+ ` --force discard manual PowerPoint edits instead of reconciling them\n` +
65
+ ` --json machine-readable result\n` +
66
+ ` slidemind inspect-template-deck <deck> Inspect a template deck as JSON for pack-owned validation\n` +
67
+ ` --pack <dir> explicit template-pack directory (required)\n` +
68
+ ` --allow-unrendered-freeform include JSX inserts without .pptx artifacts\n` +
69
+ ` --json print the normalized deck model\n` +
70
+ ` slidemind view-draft <deck> Lint + build the HTML preview, then render the deck's slides to\n` +
71
+ ` per-slide PNGs (no push, no .pptx)\n` +
72
+ ` so the agent can look before review; host-free once the\n` +
73
+ ` bundle is materialized. Prints the measured defects ([fill:warn], [inventory:warn])\n` +
74
+ ` Also runs the server's export gate on the captured slides (slot-fit,\n` +
75
+ ` diagram geometry, connector endpoints) so build-pptx's 422s surface here.\n` +
76
+ ` --only <file|N>[,…] render just these slides (a per-slide builder's own check)\n` +
77
+ ` --fix apply the deterministic lint autofixes first\n` +
78
+ ` --lint-only lint + HTML build, no render (fast fix loop)\n` +
79
+ ` --no-translate skip the export gate (purely local check)\n` +
80
+ ` --translate insist on the export gate (fail instead of skipping when the backend is unreachable)\n` +
81
+ ` --json machine-readable result (slides, defects, reviewCanOpen)\n` +
82
+ ` --to <presId> window to fetch the bundle from (only if not cached)\n` +
83
+ ` --out <dir> output dir (default <deck>/.draft-preview/)\n` +
84
+ ` --open open each rendered PNG\n` +
85
+ ` slidemind search "<query>" Search an engine or scope for images or slides (no window needed on\n` +
86
+ ` add-ins that serve the pane-free engine routes; older ones relay\n` +
87
+ ` through an open host)\n` +
88
+ ` --engine <id> engine, or scope:all | scope:sharepoint to search unfiltered\n` +
89
+ ` (default: first image engine; scope:all for slide intents)\n` +
90
+ ` --result-type <t> slide | image — required for a scope:* target\n` +
91
+ ` --intent <i> auto | content | visual (slide engines)\n` +
92
+ ` --top <N> max results\n` +
93
+ ` --deck <deck> route by this deck's bound design (multi-environment)\n` +
94
+ ` --to <presId> pin a window on the relayed path (ignored pane-free)\n` +
95
+ ` --json print the raw result payload (carries the searchId)\n` +
96
+ ` slidemind view-slide [opts] Fetch a slide search result's preview image to a file\n` +
97
+ ` --search <id> searchId from 'slidemind search'\n` +
98
+ ` --pick <n> 1-based selection number to view\n` +
99
+ ` --to <presId> the window the search ran on (relayed path only)\n` +
100
+ ` --out <dir> output dir (default: a slidemind temp dir)\n` +
101
+ ` slidemind read-source [opts] Read the ORIGINAL deck a slide search result came from\n` +
102
+ ` --search <id> searchId from 'slidemind search'\n` +
103
+ ` --pick <n> 1-based selection number\n` +
104
+ ` --from-slide <n> first slide to read (1-based)\n` +
105
+ ` --to-slide <n> last slide to read (1-based)\n` +
106
+ ` --to <presId> the window the search ran on (relayed path only)\n` +
107
+ ` --out <file> write the Markdown to a file instead of stdout\n` +
108
+ ` slidemind ls designs List every corporate design at hand: cached here + listed by the\n` +
109
+ ` add-in (presFP, name, theme colours) — read-only, picks nothing\n` +
110
+ ` --json print the raw design list\n` +
111
+ ` slidemind ls engines List the live engine catalog + search scopes\n` +
112
+ ` --type <t> collection | search | scope\n` +
113
+ ` --subtype <s> image | slide | template | content\n` +
114
+ ` --to <presId> target a specific window/backend (relayed path only)\n` +
115
+ ` --all include every paired backend\n` +
116
+ ` --json print the raw engine list\n` +
117
+ ` slidemind insert-slide [opts] Insert reused slide search results live into a window (requires a host)\n` +
118
+ ` --search <id> searchId from 'slidemind search'\n` +
119
+ ` --pick <n,..> 1-based selection number(s)\n` +
120
+ ` --to <presId> target window (required if >1 open)\n` +
121
+ ` --at <N> insert after slide N (0-based); default = append\n` +
122
+ ` slidemind add-slide [opts] Fold reused slide(s) into a deck as native .pptx\n` +
123
+ ` --search <id> searchId from 'slidemind search'\n` +
124
+ ` --from <file.pptx> fold slide(s) from a local .pptx instead (host-free)\n` +
125
+ ` --pick <n,..> 1-based selection number(s)\n` +
126
+ ` --deck <deck> deck to add the slides to\n` +
127
+ ` --to <presId> the window the search ran on (relayed path only; ignored with --from)\n` +
128
+ ` --at <N> numeric prefix to start at (default: append)\n` +
129
+ ` --json machine-readable result (carries the searchId)\n` +
130
+ ` slidemind adopt <file.pptx> Rebuild a deck workspace from a file's embedded source; foreign\n` +
131
+ ` slides are converted to editable jsx where the add-in offers it\n` +
132
+ ` --deck <slug> deck folder to write (default: the file's name)\n` +
133
+ ` --brand <presFP> design to convert against (when the file names none)\n` +
134
+ ` --no-convert fold every untagged slide as native .pptx instead\n` +
135
+ ` slidemind download-result [opts] Download image/icon search result(s) into a deck's assets/\n` +
136
+ ` --search <id> searchId from 'slidemind search'\n` +
137
+ ` --pick <n,..> 1-based selection number(s)\n` +
138
+ ` --deck <deck> deck to write the asset(s) into\n` +
139
+ ` --to <presId> the window the search ran on (relayed path only)\n` +
140
+ ` --icon-color <hex[,hex,..]> bake a colour into SVG result(s) (one for all, or one per --pick)\n` +
141
+ ` --json machine-readable result (file, size, recolor per pick)\n` +
142
+ ` slidemind imagen "<prompt>" Generate ONE image with the add-in's image model into a deck's assets/ (paired backend, pane-free)\n` +
143
+ ` --deck <deck> bound deck to write the image into (required)\n` +
144
+ ` --out <name> file name under assets/ (default imagen-N.png)\n` +
145
+ ` --size <s> auto|1024x1024|1536x1024|1024x1536 (default auto)\n` +
146
+ ` --quality <q> low|medium|high|auto\n` +
147
+ ` slidemind download-bundle [deck] Materialize or refresh a corporate design's bundle locally\n` +
148
+ ` (pane-free for a bound deck, --brand or --all — no PowerPoint needed)\n` +
149
+ ` --to <presId> target a specific window (forces the pane route)\n` +
150
+ ` --brand <presFP> materialize a named design without a window\n` +
151
+ ` --all refresh every design already cached on this machine\n` +
152
+ ` --force re-download even if the cached bundle is current\n` +
153
+
154
+ ` slidemind read [presId|file.pptx] Pull the content of an open presentation (requires a host),\n` +
155
+ ` or of a local .pptx path (read host-free from disk)\n` +
156
+ ` --mode <m> summary | raw (default: raw)\n` +
157
+ ` --to <presId> target a specific window (required if >1 open)\n` +
158
+ ` --json print the raw content payload\n` +
159
+ ` slidemind push <deck> [opts] Build deck and push slides into a running PowerPoint\n` +
160
+ ` --to <presId> target a specific window (required if >1 open)\n` +
161
+ ` --at <index> insert after slide N (0-based); default = append\n` +
162
+ ` --replace delete existing slides first (destructive)\n` +
163
+ ` --slides <ids> comma-separated source slide ids to push\n` +
164
+ ` --accept-conversions replace even when a converted slide is unconfirmed\n` +
165
+ ` --force discard manual PowerPoint edits instead of reconciling them\n` +
166
+ ` slidemind login Sign in to Microsoft 365 in your browser so SharePoint-backed\n` +
167
+ ` slides and images download without an open PowerPoint window\n` +
168
+ ` --backend <url|env> environment to sign in for (default: the default one)\n` +
169
+ ` slidemind whoami Show every connected environment (* = default)\n` +
170
+ ` slidemind logout [url|env] Unpair one environment (--all to unpair every one)\n` +
171
+ ` slidemind init --for <t> Set up slidemind in a harness (deploys the skill; clears the bundle cache)\n` +
172
+ ` <t> = claude | codex | opencode | all | auto (every harness found here)\n` +
173
+ ` 'setup' already does this — only needed after installing a new agent\n` +
174
+ ` slidemind uninstall --for <t> Remove a previously-installed skill (clears the bundle cache)\n` +
175
+ ` slidemind update [--for <t>] Fetch the latest release and redeploy\n` +
176
+ ` --for <t> harness to redeploy into (default: all)\n` +
177
+ ` --ref <ver> pin to a release version (e.g. v0.1.0)\n` +
178
+ ` slidemind <command> --help Show one command's usage\n` +
179
+ ` slidemind --help Show this help\n` +
180
+ ` slidemind --version Print version\n` +
181
+ `\n` +
182
+ `Install targets: claude | codex | opencode | all\n` +
183
+ `Decks live in ./decks/ relative to your current directory.\n` +
184
+ `Override with env: SLIDEMIND_DECKS=/abs/path/to/decks\n` +
185
+ `Global: --backend <url|env> scope a command to one paired environment (also SLIDEMIND_BACKEND)\n`;
186
+ process.stdout.write(cmd ? commandBlock(text, cmd) : text);
187
+ }
188
+
189
+ // The usage block of one command: its ` slidemind <cmd>` line and the indented
190
+ // flag lines under it, up to the next command line. Falls back to the full text
191
+ // for a name the listing does not carry.
192
+ export function commandBlock(text, cmd) {
193
+ const head = new RegExp(`^ slidemind ${cmd.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}( |$)`);
194
+ const out = [];
195
+ let inBlock = false;
196
+ for (const line of text.split("\n")) {
197
+ if (/^ slidemind /.test(line) || line === "") inBlock = head.test(line);
198
+ if (inBlock) out.push(line);
199
+ }
200
+ return out.length ? out.join("\n") + "\n" : text;
201
+ }