slidemind 0.3.4 → 0.3.12

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.
@@ -5,90 +5,243 @@ description: Discover, search, and reuse content from the active corporate desig
5
5
 
6
6
  # slidemind-search
7
7
 
8
- This skill is **thin workflow glue for the local harness**. The capability it drives — engine discovery, image/slide search, and slide reuse — runs on the **slidemind PowerPoint add-in**: the engines, the corporate design they belong to, and the search/extraction execution all live there and reach you over the add-in's cloud relay. The **craft** of using it well — how to discover engines, choose content-vs-visual search, pick a result, and reuse a slide responsibly — is authored once by the add-in and served on the materialized bundle as `workflow/search.md`, **byte-identical** to what the in-app design agent reads.
9
-
10
- So this file owns only what is genuinely CLI-specific: the **host precondition**, the **discover → search → pick → reuse** loop, the **reuse targets** (insert live vs fold into a deck), and the **command mapping** that translates `workflow/search.md`'s harness-neutral verbs into your `slidemind` CLI commands. **Read `workflow/search.md` from the bundle; never restate the craft here.**
11
-
12
- **slidemind is a CLI you drive directly.** Run `slidemind <command>` in your shell — the same way you'd run `git` or `npm`. There is no separate tool layer to go through; every step below is a real command you type.
8
+ This skill is **thin workflow glue for the local harness**. The capability it drives — engine
9
+ discovery, image/slide search, and slide reuse — runs on the **slidemind PowerPoint add-in**: the
10
+ engines, the corporate design they belong to, and the search/extraction execution all live there and
11
+ reach you over the add-in's cloud relay. The **craft** of using it well — how to discover engines,
12
+ choose content-vs-visual search, pick a result, and reuse a slide responsibly — is authored once by
13
+ the add-in and served on the materialized bundle as `workflow/search.md`, **byte-identical** to what
14
+ the in-app design agent reads.
15
+
16
+ So this file owns only what is genuinely CLI-specific: the **host precondition**, the **discover →
17
+ search → pick → reuse** loop, the **reuse targets** (insert live vs fold into a deck), and the
18
+ **command mapping** that translates `workflow/search.md`'s harness-neutral verbs into your
19
+ `slidemind` CLI commands. **Read `workflow/search.md` from the bundle; never restate the craft
20
+ here.**
21
+
22
+ **slidemind is a CLI you drive directly.** Run `slidemind <command>` in your shell — the same way
23
+ you'd run `git` or `npm`. There is no separate tool layer to go through; every step below is a real
24
+ command you type.
13
25
 
14
26
  ## Bootstrap (set yourself up — don't make the user do it)
15
27
 
16
- **The first step of a search session is `slidemind ready --connect-only`** (see *Precondition gate* below). It drives the `slidemind` CLI, which reaches PowerPoint through the add-in's **cloud relay** — no local server. Patch a missing CLI yourself; **connecting** needs the user once (they hold the add-in's identity):
17
-
18
- - **CLI missing** (`slidemind` not on `PATH`): `npm install -g slidemind` — that alone is the fix. Add `slidemind init --for <claude|codex|opencode|all>` ((re)deploys this skill) only when this skill did **not** come from a plugin: a `SKILL.md` under a harness plugin cache (`~/.claude/plugins/…`, `~/.codex/plugins/…`) is already deployed, and `init` would just plant a second, drifting copy.
19
- - **Not connected:** `ready --connect-only` exits with `not_connected` — the CLI cannot start a connection by itself, because the claim carrying the deployment's address is minted in the add-in. Ask the user to open PowerPoint → slidemind → **Settings → General → Connect CLI**, press the **link button**, and copy the command shown (**CLI already installed?** gives the short `slidemind setup …` form). Have them paste it to you and run it **verbatim**. `slidemind whoami` lists connected environments.
20
-
21
- This is the same connection as `slidemind-create-slides`, whose **Connecting the CLI** section carries the full step-by-step. If it was approved there, it is already available here.
28
+ **There is no preflight for a search session.** Every engine command checks the connection itself
29
+ and, on failure, names exactly one blocker and one remediation — so start directly with
30
+ `slidemind ls engines` (the first real-work step, see *Precondition gate* below) and act on what a
31
+ failing command prints. The CLI reaches PowerPoint through the add-in's **cloud relay** — no local
32
+ server. Patch a missing CLI yourself; **connecting** needs the user once (they hold the add-in's
33
+ identity):
34
+
35
+ - **CLI missing** (`slidemind` not on `PATH`): `npm install -g slidemind` — that alone is the fix.
36
+ Add `slidemind init --for <claude|codex|opencode|all>` ((re)deploys this skill) only when this
37
+ skill did **not** come from a plugin: a `SKILL.md` under a harness plugin cache
38
+ (`~/.claude/plugins/…`, `~/.codex/plugins/…`) is already deployed, and `init` would just plant a
39
+ second, drifting copy.
40
+ - **Not connected:** the command exits with `not_connected` — the CLI cannot start a connection by
41
+ itself, because the claim carrying the deployment's address is minted in the add-in. Ask the user
42
+ to open PowerPoint → slidemind → **Settings → General → Connect CLI**, press the **link button**,
43
+ and copy the command shown (**CLI already installed?** gives the short `slidemind setup …` form).
44
+ Have them paste it to you and run it **verbatim**. `slidemind whoami` lists connected
45
+ environments.
46
+
47
+ This is the same connection as `slidemind-create-slides`, whose **Connecting the CLI** section
48
+ carries the full step-by-step. If it was approved there, it is already available here.
22
49
 
23
50
  ## Precondition gate (check this first, every time)
24
51
 
25
- Search and reuse need a **connected** CLI. Whether they also need an open PowerPoint depends on the add-in: newer deployments answer engine discovery, search, previews, downloads, and slide extraction **off the connection itself**, so PowerPoint can be backgrounded or closed; older ones relay every one of those through the add-in's task pane and need a live window. The CLI detects which and picks the path — you only have to know which ops stay host-required either way.
26
-
27
- 1. Run `slidemind ready --connect-only`. It walks the connection ladder (a saved session → reachable backend → CLI version → whether the engines are reachable without a window) but does **not** resolve a design or download a bundle — search doesn't need one.
28
- - **Exit 0 → you're set.** Proceed to `slidemind ls engines` (the live catalog is authoritative — the first real-work step). The payload's `engines: { available, envs }` tells you which path you're on: `available: true` means every search step below runs with PowerPoint closed.
29
- - **Non-zero →** the CLI names exactly one blocker and one `nextAction`; relay it (copy the setup command / open PowerPoint / click the pane / run `slidemind update` / open the add-in once). The CLI owns the remediation text — don't restate it.
30
- 2. **Never pick a window for discovery.** The engine catalog is **per-user, not bound to a window**. On the pane-free path there is no window in the loop at all; on the relayed path search attaches to **any** connected window by itself. Either way, **never ask the user which window to search with** and don't pass `--to` to disambiguate. (A window matters in exactly two places: when you *insert a slide live*, and when the pane-free path refuses a download — see Reuse.)
31
-
32
- **Host-required, always:** `insert-slide` (it types into an open deck) and `slidemind read <presId>` (the live, possibly unsaved content of an open presentation). Everything else here — `ls engines`, `search`, `view-slide`, `read-source`, `download-result`, `add-slide` — is host-free wherever the add-in serves the pane-free routes, and self-waits for a pane where it doesn't: the CLI waits up to ~2 min, prints one *"click the slidemind pane"* line when user help is genuinely needed, and retries safely. Relay that line once and keep waiting — don't re-plan or babysit.
33
-
34
- **Microsoft 365 sign-in — automatic; just let it happen.** Every engine operation here — the catalog, `search`, previews, downloads, `add-slide` extraction — runs **as the user**. When no sign-in is stored yet, the first engine command opens the user's browser ITSELF, prints `Microsoft 365 sign-in needed — opening your browser…`, and **blocks until the human finishes**. When you see that line, tell the user to complete the sign-in in the browser window that just opened (you cannot do it) and keep waiting — don't kill or re-run the command. After that one sign-in, every command runs silently for ~90 days. Rendering, bundles, and already-downloaded assets never need it.
35
-
36
- **`login_required` → the sign-in was refused or abandoned.** If the automatic sign-in didn't finish (browser closed, 5-minute window elapsed), the command fails with one line naming `slidemind login`; relay it — the user runs it whenever they're ready — and don't work around it with a different engine. **Read the line before relaying it:** when a slidemind pane is already open, the CLI says so — a new `search` simply runs through that window instead (one `note:` on stderr, and the follow-up commands then carry its `--to`), and a command that can't be re-routed tells you to re-run `slidemind search` there. Follow whichever the CLI printed.
37
-
38
- **Add-in unreachable → block and name the tab.** When a host-required command can't reach the add-in (a connection blocker, a pane that never wakes, the *click the slidemind pane* line with no effect), **stop** — don't skip the step, work around it, or retry silently. Ask the user to go back into the PowerPoint tab of presentation **<title/fileName>** (name it from the command output or `slidemind presentations` — never a raw id), then re-run the failed command once they have.
52
+ Search and reuse need a **connected** CLI. Whether they also need an open PowerPoint depends on the
53
+ add-in: newer deployments answer engine discovery, search, previews, downloads, and slide extraction
54
+ **off the connection itself**, so PowerPoint can be backgrounded or closed; older ones relay every
55
+ one of those through the add-in's task pane and need a live window. The CLI detects which and picks
56
+ the path — you only have to know which ops stay host-required either way.
57
+
58
+ 1. Start with `slidemind ls engines` — the live catalog is authoritative, and the command doubles
59
+ as the connection check (no separate preflight; search needs no design bundle).
60
+ - **Exit 0 → you're set.** The catalog is in front of you; proceed to choose a target. Which
61
+ path you're on shows up in each command's own output: on the pane-free path every search step
62
+ below runs with PowerPoint closed, on the relayed path the follow-up commands name the window
63
+ they ran on.
64
+ - **Non-zero →** the CLI names exactly one blocker and one remediation; relay it (copy the setup
65
+ command / open PowerPoint / click the pane / run `slidemind update` / open the add-in once).
66
+ The CLI owns the remediation text — don't restate it.
67
+ 2. **Never pick a window for discovery.** The engine catalog is **per-user, not bound to a window**.
68
+ On the pane-free path there is no window in the loop at all; on the relayed path search attaches
69
+ to **any** connected window by itself. Either way, **never ask the user which window to search
70
+ with** and don't pass `--to` to disambiguate. (A window matters in exactly two places: when you
71
+ *insert a slide live*, and when the pane-free path refuses a download — see Reuse.)
72
+
73
+ **Host-required, always:** `insert-slide` (it types into an open deck) and `slidemind read <presId>`
74
+ (the live, possibly unsaved content of an open presentation). Everything else here — `ls engines`,
75
+ `search`, `view-slide`, `read-source`, `download-result`, `add-slide` — is host-free wherever the
76
+ add-in serves the pane-free routes, and self-waits for a pane where it doesn't: the CLI waits up to
77
+ ~2 min, prints one *"click the slidemind pane"* line when user help is genuinely needed, and retries
78
+ safely. Relay that line once and keep waiting — don't re-plan or babysit.
79
+
80
+ **Microsoft 365 sign-in — automatic; just let it happen.** Every engine operation here — the
81
+ catalog, `search`, previews, downloads, `add-slide` extraction — runs **as the user**. When no
82
+ sign-in is stored yet, the first engine command opens the user's browser ITSELF, prints `Microsoft
83
+ 365 sign-in needed — opening your browser…`, and **blocks until the human finishes**. When you see
84
+ that line, tell the user to complete the sign-in in the browser window that just opened (you cannot
85
+ do it) and keep waiting — don't kill or re-run the command. After that one sign-in, every command
86
+ runs silently for ~90 days. Rendering, bundles, and already-downloaded assets never need it.
87
+
88
+ **`login_required` → the sign-in was refused or abandoned.** If the automatic sign-in didn't finish
89
+ (browser closed, 5-minute window elapsed), the command fails with one line naming `slidemind login`;
90
+ relay it — the user runs it whenever they're ready — and don't work around it with a different
91
+ engine. **Read the line before relaying it:** when a slidemind pane is already open, the CLI says so
92
+ — a new `search` simply runs through that window instead (one `note:` on stderr, and the follow-up
93
+ commands then carry its `--to`), and a command that can't be re-routed tells you to re-run
94
+ `slidemind search` there. Follow whichever the CLI printed.
95
+
96
+ **Add-in unreachable → block and name the tab.** When a host-required command can't reach the add-in
97
+ (a connection blocker, a pane that never wakes, the *click the slidemind pane* line with no effect),
98
+ **stop** — don't skip the step, work around it, or retry silently. Ask the user to go back into the
99
+ PowerPoint tab of presentation **<title/fileName>** (name it from the command output or `slidemind
100
+ presentations` — never a raw id), then re-run the failed command once they have.
39
101
 
40
102
  ## Discover → search → pick → reuse
41
103
 
42
104
  Follow `workflow/search.md` for the discipline; this is the CLI shape of its loop.
43
105
 
44
- 1. **Discover targets — never guess an id.** Run `slidemind ls engines` and read the catalog before searching. It lists two kinds of target: **engines** (`--type collection|search`) and **scopes** (`--type scope` — `scope:all`, and `scope:sharepoint` once a site is connected), which search the corpus unfiltered. Filter with `--subtype <image|slide|template|content>` (e.g. `slidemind ls engines --type search --subtype slide`). The catalog is **per-user, not per-presentation** — there is no presId to scope; spanning every paired environment, each row is tagged with its `env`. (`--to` is accepted but never needed here.) Pick an **id** from the list — do not invent one. **An empty or thin engine list is not "nothing to search"** (see `workflow/search.md`): fall through to a scope. An empty list **under a `--type`/`--subtype` filter** says so explicitly — it reports how many engines and scopes exist unfiltered — so the next move there is to drop the filter, not to jump to a scope. A row marked *nothing indexed yet* / *indexing* exists but will return nothing right now; pick another target.
45
- 2. **Choose the target + intent.** Slide engines (`subtype slide`) and scopes support two intents: **content** (the slide's text is the signal — title, body, tables, notes) and **visual** (the slide image is the signal). Image/icon engines (`subtype image`) are **visual-only**. Match the intent to the ask: "a slide that says X" / "covers topic Y" → `--intent content`; "a slide that looks like X" / brand imagery → `--intent visual`. `--intent auto` lets the engine decide.
46
- 3. **Search — no window to choose.** `slidemind search "<query>" --engine <id> --intent <auto|content|visual> [--top N]` (leave `--to` off). A **scope** target additionally needs `--result-type slide|image` (it has no subtype of its own; the CLI defaults it to `slide`). Omit `--engine` entirely and the CLI picks for you: a slide intent searches `scope:all` for slides, anything else resolves a live image engine. It returns a `searchId` plus numbered results — **keep that `searchId`** (it also rides `--json`): every follow-up below is `--search <searchId> --pick <n>`, so an interrupted run resumes its picks without re-searching.
47
- - **How the follow-ups are addressed** is printed for you, and differs by path: *"searchId N is held by the add-in"* means the id lives on the connection — the follow-up commands carry **no `--to`** and work with PowerPoint closed; *"searchId N ran on window `<presId>`"* means the older relayed path, where the id is bound to that window — thread that `presId` in as `--to <presId>`. **Copy the next-step commands the search prints** rather than composing them from memory.
48
- - Connected to **several environments**? Pass `--deck <deck>` so the search routes by that deck's bound design instead of asking you to choose an environment.
49
- - **On `0 result(s)`, read the diagnostic the CLI prints and act on it** — it lists the causes that look identical from here (nothing connected, still indexing, the engine's folder filter excludes the content) and the next moves. Reuse from a scope works exactly like reuse from an engine, so widening costs you nothing.
50
- 4. **Read / view, then pick.** For **content** results, read the per-slide **text + score** the search prints — that is the on-the-wire signal; you usually don't need the picture. For **visual** results, scan the numbered thumbnail list; when the search prints a **contact sheet** path, Read that ONE file — every candidate is on it with its selection number burned in — and when it says there is none on this path, pull individual previews with `slidemind view-slide --search <searchId> --pick <n>` (it writes one result's preview to a file you can Read), or `slidemind search … --json` for the structured payload. Pick by **selection number(s)**.
51
- 5. **Read the source deck when the story matters.** A found slide came out of a real presentation, and often the answer the user needs is in the *rest* of it — how the slide was framed, what came before and after, whether that deck already covers more of the ask. `slidemind read-source --search <searchId> --pick <n>` prints the whole source presentation as Markdown, one section per slide. Skip it when you only need the one slide. Two things to know: it is the **last-synced** text, so very recent edits to the original may be missing; and a long deck comes back as a slide map instead, which you narrow with `--from-slide`/`--to-slide`. Only slide results have a source deck — image and collection results do not.
106
+ 1. **Discover targets — never guess an id.** Run `slidemind ls engines` and read the catalog before
107
+ searching. It lists two kinds of target: **engines** (`--type collection|search`) and **scopes**
108
+ (`--type scope` — `scope:all`, and `scope:sharepoint` once a site is connected), which search the
109
+ corpus unfiltered. Filter with `--subtype <image|slide|template|content>` (e.g. `slidemind ls
110
+ engines --type search --subtype slide`). The catalog is **per-user, not per-presentation** —
111
+ there is no presId to scope; spanning every paired environment, each row is tagged with its
112
+ `env`. (`--to` is accepted but never needed here.) Pick an **id** from the list — do not invent
113
+ one. **An empty or thin engine list is not "nothing to search"** (see `workflow/search.md`): fall
114
+ through to a scope. An empty list **under a `--type`/`--subtype` filter** says so explicitly — it
115
+ reports how many engines and scopes exist unfiltered — so the next move there is to drop the
116
+ filter, not to jump to a scope. A row marked *nothing indexed yet* / *indexing* exists but will
117
+ return nothing right now; pick another target.
118
+ 2. **Choose the target + intent.** Slide engines (`subtype slide`) and scopes support two intents:
119
+ **content** (the slide's text is the signal — title, body, tables, notes) and **visual** (the
120
+ slide image is the signal). Image/icon engines (`subtype image`) are **visual-only**. Match the
121
+ intent to the ask: "a slide that says X" / "covers topic Y" → `--intent content`; "a slide that
122
+ looks like X" / brand imagery → `--intent visual`. `--intent auto` lets the engine decide.
123
+ 3. **Search — no window to choose.** `slidemind search "<query>" --engine <id> --intent
124
+ <auto|content|visual> [--top N]` (leave `--to` off). A **scope** target additionally needs
125
+ `--result-type slide|image` (it has no subtype of its own; the CLI defaults it to `slide`). Omit
126
+ `--engine` entirely and the CLI picks for you: a slide intent searches `scope:all` for slides,
127
+ anything else resolves a live image engine. It returns a `searchId` plus numbered results —
128
+ **keep that `searchId`** (it also rides `--json`): every follow-up below is `--search <searchId>
129
+ --pick <n>`, so an interrupted run resumes its picks without re-searching.
130
+ - **How the follow-ups are addressed** is printed for you, and differs by path: *"searchId N is
131
+ held by the add-in"* means the id lives on the connection — the follow-up commands carry **no
132
+ `--to`** and work with PowerPoint closed; *"searchId N ran on window `<presId>`"* means the
133
+ older relayed path, where the id is bound to that window — thread that `presId` in as `--to
134
+ <presId>`. **Copy the next-step commands the search prints** rather than composing them from
135
+ memory.
136
+ - Connected to **several environments**? Pass `--deck <deck>` so the search routes by that deck's
137
+ bound design instead of asking you to choose an environment.
138
+ - **On `0 result(s)`, read the diagnostic the CLI prints and act on it** — it lists the causes
139
+ that look identical from here (nothing connected, still indexing, the engine's folder filter
140
+ excludes the content) and the next moves. Reuse from a scope works exactly like reuse from an
141
+ engine, so widening costs you nothing.
142
+ 4. **Read / view, then pick.** For **content** results, read the per-slide **text + score** the
143
+ search prints — that is the on-the-wire signal; you usually don't need the picture. For
144
+ **visual** results, scan the numbered thumbnail list; when the search prints a **contact sheet**
145
+ path, Read that ONE file — every candidate is on it with its selection number burned in. With no
146
+ contact sheet, previews depend on the result type: **slide-library results** support `slidemind
147
+ view-slide --search <searchId> --pick <n>` (writes one result's preview to a file you can Read);
148
+ **image/icon results have NO preview on this path** — `view-slide` answers "no image returned"
149
+ for them, so never call it on an image search, and never retry it hoping for a different answer.
150
+ For icons, pick by title + score, `download-result` the pick (downloads are cheap), Read the
151
+ saved file, and re-search once if it was wrong — two failed downloads for the same need means
152
+ mark it unresolved and move on. Pick by **selection number(s)**.
153
+ 5. **Read the source deck when the story matters.** A found slide came out of a real presentation,
154
+ and often the answer the user needs is in the *rest* of it — how the slide was framed, what came
155
+ before and after, whether that deck already covers more of the ask. `slidemind read-source
156
+ --search <searchId> --pick <n>` prints the whole source presentation as Markdown, one section per
157
+ slide. Skip it when you only need the one slide. Two things to know: it is the **last-synced**
158
+ text, so very recent edits to the original may be missing; and a long deck comes back as a slide
159
+ map instead, which you narrow with `--from-slide`/`--to-slide`. Only slide results have a source
160
+ deck — image and collection results do not.
52
161
  6. **Reuse.** Insert live or fold into a deck — see the next section.
53
162
 
54
163
  ## Reuse targets — insert live, or fold into a deck
55
164
 
56
- A found slide can be reused two ways. Both extract the picked result(s) as native PowerPoint slides — **reuse is JSX-free; reused slides are native `.pptx`, never JSX.**
57
-
58
- - **Insert LIVE into an open PowerPoint** — `slidemind insert-slide --search <searchId> --pick <n[,n,…]> --to <presId> [--at <N>]`. **This is the one host-required step:** the picked slides land in that open window (at position `--at`, else appended), so PowerPoint must be open with the slidemind pane. Name the target window from `slidemind presentations` (or, on the relayed path, use the window the search ran on). A window that reconnects mid-run gets a **new** id; the CLI re-finds it and retries once by itself, so a stale id is not your problem. If it still reports the pane asleep, **don't loop** — relay its line, or fold the same pick into a deck (below) and push it once the user is back in PowerPoint.
59
- - **Fold into a DECK on disk** — `slidemind add-slide --search <searchId> --pick <n[,n,…]> --deck <deck> [--at <N>]` (add `--to <searchWindow>` only on the relayed path, where the extraction must run on the window that holds the `searchId`). Each picked result is written as a native one-slide file into `decks/<slug>/slides/NN-reused-*.pptx`, in play order; no live window is changed, and nothing needs one. `--json` returns `{ searchId, deck, slides[] }`. Use this when the reused slide should become **part of a deck the user keeps and re-renders** — and prefer it when PowerPoint isn't open: fold now, `slidemind push <deck>` later.
165
+ A found slide can be reused two ways. Both extract the picked result(s) as native PowerPoint slides
166
+ — **reuse is JSX-free; reused slides are native `.pptx`, never JSX.**
167
+
168
+ - **Insert LIVE into an open PowerPoint** — `slidemind insert-slide --search <searchId> --pick
169
+ <n[,n,…]> --to <presId> [--at <N>]`. **This is the one host-required step:** the picked slides
170
+ land in that open window (at position `--at`, else appended), so PowerPoint must be open with the
171
+ slidemind pane. Name the target window from `slidemind presentations` (or, on the relayed path,
172
+ use the window the search ran on). A window that reconnects mid-run gets a **new** id; the CLI
173
+ re-finds it and retries once by itself, so a stale id is not your problem. If it still reports the
174
+ pane asleep, **don't loop** — relay its line, or fold the same pick into a deck (below) and push
175
+ it once the user is back in PowerPoint.
176
+ - **Fold into a DECK on disk** — `slidemind add-slide --search <searchId> --pick <n[,n,…]> --deck
177
+ <deck> [--at <N>]` (add `--to <searchWindow>` only on the relayed path, where the extraction must
178
+ run on the window that holds the `searchId`). Each picked result is written as a native one-slide
179
+ file into `decks/<slug>/slides/NN-reused-*.pptx`, in play order; no live window is changed, and
180
+ nothing needs one. `--json` returns `{ searchId, deck, slides[] }`. Use this when the reused slide
181
+ should become **part of a deck the user keeps and re-renders** — and prefer it when PowerPoint
182
+ isn't open: fold now, `slidemind push <deck>` later.
60
183
 
61
184
  ### A local `.pptx` file as the source (host-free — no engine, no search)
62
185
 
63
- When the slide to reuse already lives in a `.pptx` **on disk** (another presentation, an exported deck), skip discovery entirely — no host, no window, no engine:
64
-
65
- 1. **List the file's slides.** `slidemind read <file.pptx> --mode summary` prints one outline line per slide **in play order**; note the number(s) you want (1-based).
66
- 2. **Fold the picked slide(s) into a deck.** `slidemind add-slide --from <file.pptx> --pick <n[,n,…]> --deck <deck> [--at <N>]`. `--from` is the local-file alternative to `--search` (they are **mutually exclusive**), and it needs no `--to` — there is no window in the loop.
67
-
68
- This writes the **same** `decks/<slug>/slides/NN-reused-*.pptx` as the engine path, so the durable rule below applies unchanged (deck source-of-truth, spliced by play-order filename, survives every re-render). To deliver a local slide **live** into an open window there is no `insert-slide --from`: fold it into a deck as above, then `slidemind push <deck>`.
69
-
70
- > **The durable rule:** a slide folded into a deck with `add-slide` is **deck source-of-truth**. The render pipeline splices native `slides/*.pptx` into the deck **by play-order filename**, alongside any authored `.jsx`, so a reused slide **survives every `build-pptx` and `push` re-render** — it is never lost on re-push. Reorder it the same way you reorder JSX: rename its numeric prefix.
71
-
72
- > **Building a `.pptx` file from found slides → one combined deck, always.** When the user wants the reused slides as a **`.pptx` file** (rather than a live insert), collect **every** picked slide into **one** deck and render **one** file — never one `.pptx` per slide and never a deck per slide. Fold all picks into the same deck — pass every selection in a single `add-slide` call (`--pick n1,n2,…`), repeating only to cover picks that came from different `searchId`s (always the same `--deck`) — then run `slidemind build-pptx <deck>` once. The render splices every native `slides/*.pptx` into a **single `decks/<slug>/<slug>.pptx`** in play-order, so all the found slides land in one file. That combined `.pptx` is the deliverable; hand back its path. (Need new authored slides interleaved with the reused ones in that same file? That's still one deck — hand off to `slidemind-create-slides` to add the JSX, then `build-pptx` the whole deck once.)
73
-
74
- For brand **imagery** (icons/images, not whole slides), the reuse step is `slidemind download-result` — it writes the asset into the deck's `assets/`. That mechanism (including the icon-recolor descriptor and `--icon-color`) is documented where it's used, in `slidemind-create-slides`.
75
-
76
- Image results print their **intrinsic `width×height`** next to the title — the engine indexes it, so the aspect ratio is yours *before* you fetch anything. Carry it into the pick: a result whose shape doesn't suit the slot you have in mind is the wrong pick, whatever it depicts. (`download-result` re-states the size, measured from the delivered bytes.)
186
+ When the slide to reuse already lives in a `.pptx` **on disk** (another presentation, an exported
187
+ deck), skip discovery entirely — no host, no window, no engine:
188
+
189
+ 1. **List the file's slides.** `slidemind read <file.pptx> --mode summary` prints one outline line
190
+ per slide **in play order**; note the number(s) you want (1-based).
191
+ 2. **Fold the picked slide(s) into a deck.** `slidemind add-slide --from <file.pptx> --pick
192
+ <n[,n,…]> --deck <deck> [--at <N>]`. `--from` is the local-file alternative to `--search` (they
193
+ are **mutually exclusive**), and it needs no `--to` — there is no window in the loop.
194
+
195
+ This writes the **same** `decks/<slug>/slides/NN-reused-*.pptx` as the engine path, so the durable
196
+ rule below applies unchanged (deck source-of-truth, spliced by play-order filename, survives every
197
+ re-render). To deliver a local slide **live** into an open window there is no `insert-slide --from`:
198
+ fold it into a deck as above, then `slidemind push <deck>`.
199
+
200
+ > **The durable rule:** a slide folded into a deck with `add-slide` is **deck source-of-truth**. The
201
+ > render pipeline splices native `slides/*.pptx` into the deck **by play-order filename**, alongside
202
+ > any authored `.jsx`, so a reused slide **survives every `build-pptx` and `push` re-render** — it
203
+ > is never lost on re-push. Reorder it the same way you reorder JSX: rename its numeric prefix.
204
+
205
+ > **Building a `.pptx` file from found slides → one combined deck, always.** When the user wants the
206
+ > reused slides as a **`.pptx` file** (rather than a live insert), collect **every** picked slide
207
+ > into **one** deck and render **one** file — never one `.pptx` per slide and never a deck per
208
+ > slide. Fold all picks into the same deck — pass every selection in a single `add-slide` call
209
+ > (`--pick n1,n2,…`), repeating only to cover picks that came from different `searchId`s (always the
210
+ > same `--deck`) — then run `slidemind build-pptx <deck>` once. The render splices every native
211
+ > `slides/*.pptx` into a **single `decks/<slug>/<slug>.pptx`** in play-order, so all the found
212
+ > slides land in one file. That combined `.pptx` is the deliverable; hand back its path. (Need new
213
+ > authored slides interleaved with the reused ones in that same file? That's still one deck — hand
214
+ > off to `slidemind-create-slides` to add the JSX, then `build-pptx` the whole deck once.)
215
+
216
+ For brand **imagery** (icons/images, not whole slides), the reuse step is `slidemind
217
+ download-result` — it writes the asset into the deck's `assets/`. That mechanism (including the
218
+ icon-recolor descriptor and `--icon-color`) is documented where it's used, in
219
+ `slidemind-create-slides`.
220
+
221
+ Image results print their **intrinsic `width×height`** next to the title — the engine indexes it, so
222
+ the aspect ratio is yours *before* you fetch anything. Carry it into the pick: a result whose shape
223
+ doesn't suit the slot you have in mind is the wrong pick, whatever it depicts. (`download-result`
224
+ re-states the size, measured from the delivered bytes.)
77
225
 
78
226
  ## Working with slidemind-create-slides
79
227
 
80
- These two skills compose. When the user is **authoring** a deck (that's `slidemind-create-slides`) and wants an **existing** slide incorporated, reach for this skill's `add-slide`: it folds the existing slide into the same `decks/<slug>/slides/` directory as the authored JSX. The reused `.pptx` and the authored `.jsx` coexist in play order (numeric filename prefix) and the reused slide is **not lost on re-push**. Hand back to `slidemind-create-slides` for any new content authoring.
228
+ These two skills compose. When the user is **authoring** a deck (that's `slidemind-create-slides`)
229
+ and wants an **existing** slide incorporated, reach for this skill's `add-slide`: it folds the
230
+ existing slide into the same `decks/<slug>/slides/` directory as the authored JSX. The reused
231
+ `.pptx` and the authored `.jsx` coexist in play order (numeric filename prefix) and the reused slide
232
+ is **not lost on re-push**. Hand back to `slidemind-create-slides` for any new content authoring.
81
233
 
82
234
  ## Command mapping (the search vocabulary ⇄ your CLI)
83
235
 
84
- `workflow/search.md` speaks harness-neutral verbs (discover engines, search with intent, pick, insert live, fold into a deck, engine fetch). You are the local harness — this is your mapping:
236
+ `workflow/search.md` speaks harness-neutral verbs (discover engines, search with intent, pick,
237
+ insert live, fold into a deck, engine fetch). You are the local harness — this is your mapping:
85
238
 
86
239
  | Verb (`workflow/search.md`) | Your equivalent (local harness) |
87
240
  |---|---|
88
241
  | **discover engines** ("list what you can reach") | `slidemind ls engines [--type collection\|search\|scope] [--subtype …]` — returns the engine catalog **plus the search scopes** (pick an `id`; never guess one). The catalog is backend/user-scoped; `--to` is accepted but never needed. No window needed where the add-in serves the pane-free routes. |
89
242
  | **engine search** (with `intent`) | `slidemind search "<query>" --engine <id> --intent <auto\|content\|visual> [--top N] [--deck <deck>]` — leave `--to` off → a `searchId` + numbered results, plus the exact follow-up commands for the path it took (`--deck` routes by that deck's design when several environments are connected) |
90
243
  | **search a scope** (unfiltered corpus, no engine) | `slidemind search "<query>" --engine <scope:all\|scope:sharepoint> --result-type <slide\|image> [--intent …]` — `--result-type` is required for a scope (defaults to `slide`). Results reuse identically to engine results. |
91
- | **pull an on-demand thumbnail** for a stored result | `slidemind view-slide --search <searchId> --pick <n>` — writes one result's preview image to a file (add `--to <searchWindow>` only on the relayed path) · or `slidemind search … --json` for the structured payload. |
244
+ | **pull an on-demand thumbnail** for a stored result | `slidemind view-slide --search <searchId> --pick <n>` — SLIDE-library results only; image/icon results have no preview ("no image returned") — download and Read the file instead (add `--to <searchWindow>` only on the relayed path) · or `slidemind search … --json` for the structured payload. |
92
245
  | **read a result's SOURCE deck** (the presentation it came from) | `slidemind read-source --search <searchId> --pick <n> [--from-slide N] [--to-slide N] [--out <file.md>]` — Markdown of the whole source presentation, one section per slide (add `--to <searchWindow>` only on the relayed path). Indexed text, not the live file. Slide results only. |
93
246
  | **insert live** (reuse a slide → the open window) | `slidemind insert-slide --search <searchId> --pick <n[,n,…]> --to <presId> [--at N]` — **host-required**: the slide lands in that open window |
94
247
  | **fold into a deck** (reuse a slide → deck source) | `slidemind add-slide --search <searchId> --pick <n[,n,…]> --deck <deck> [--at N] [--json]` → writes `decks/<slug>/slides/NN-reused-*.pptx` (add `--to <searchWindow>` only on the relayed path, where extraction runs on the search's window) |
@@ -96,10 +249,17 @@ These two skills compose. When the user is **authoring** a deck (that's `slidemi
96
249
  | **fold a LOCAL slide into a deck** (`--from`, host-free) | `slidemind add-slide --from <file.pptx> --pick <n[,n,…]> --deck <deck> [--at N]` → same `decks/<slug>/slides/NN-reused-*.pptx` output as the engine path; `--from` replaces `--search`/`--to` (mutually exclusive with `--search`; no host). Live delivery = fold + `slidemind push <deck>` |
97
250
  | **engine fetch** (reuse an image asset) | `slidemind download-result --search <searchId> --pick <n[,n,…]> --deck <deck> [--icon-color <hex[,hex,…]>]` — writes into `decks/<slug>/assets/` (add `--to <searchWindow>` only on the relayed path; recolor descriptor + tinting are covered in `slidemind-create-slides`) |
98
251
 
99
- Everything else in `workflow/search.md` — the engine-discovery discipline, content-vs-visual judgement, the picking and reuse craft — applies to you unchanged.
252
+ Everything else in `workflow/search.md` — the engine-discovery discipline, content-vs-visual
253
+ judgement, the picking and reuse craft — applies to you unchanged.
100
254
 
101
255
  ## The engines and the workflow both live in the bundle, not here
102
256
 
103
- Do not restate which engines exist, what they contain, or any brand rule — the engine catalog comes from `slidemind ls engines` (live discovery — the bundle carries no engine list), and the search/reuse craft is in `workflow/search.md`. Read them and follow them. If the corporate design moves mid-task, re-list engines before searching again, since the catalog may have changed.
257
+ Do not restate which engines exist, what they contain, or any brand rule — the engine catalog comes
258
+ from `slidemind ls engines` (live discovery — the bundle carries no engine list), and the
259
+ search/reuse craft is in `workflow/search.md`. Read them and follow them. If the corporate design
260
+ moves mid-task, re-list engines before searching again, since the catalog may have changed.
104
261
 
105
- `workflow/search.md` also owns the judgement this skill deliberately does **not** restate: what an engine actually is (a saved filter over a shared corpus, not a library of its own), when to reach for a scope instead, and why zero results are not evidence that the content doesn't exist. Read it before concluding a search came up empty.
262
+ `workflow/search.md` also owns the judgement this skill deliberately does **not** restate: what an
263
+ engine actually is (a saved filter over a shared corpus, not a library of its own), when to reach
264
+ for a scope instead, and why zero results are not evidence that the content doesn't exist. Read it
265
+ before concluding a search came up empty.
@@ -273,10 +273,10 @@ export async function renderDeckToBase64(
273
273
  // server splices them back from. Keyed by slide file name, like `slides`.
274
274
  const conversions = loadConversions(deckDir);
275
275
  const hasConversions = Object.keys(conversions).length > 0;
276
- for (const file of conversionsMissingPreserveSource(conversions)) {
276
+ for (const file of conversionsMissingPreserveSource(conversions, deckDir)) {
277
277
  process.stderr.write(
278
- `note: ${file} has preserved native objects but no conversions/*.slide.pptx ` +
279
- `to splice them from — re-run 'slidemind adopt' for that deck\n`,
278
+ `note: ${file} has preserved native objects or a sourced chart but no ` +
279
+ `conversions/*.slide.pptx to read them from — re-run 'slidemind adopt' for that deck\n`,
280
280
  );
281
281
  }
282
282
 
@@ -30,7 +30,6 @@
30
30
  // ├── assets/brand-images.md (on-demand manifest of the reusable master images,
31
31
  // │ built from wire.extractedImages — design.md only
32
32
  // │ carries a sparse pointer + per-kind counts)
33
- // ├── samples/* (downloaded from wire.samples[].url — brand reference slides)
34
33
  // ├── fonts/* (downloaded from wire.fonts[].url, optional)
35
34
  // ├── master/empty-layout.pptx (downloaded from wire.master.url, optional)
36
35
  // ├── engines.json (wire.engines[] — legacy: new backends dropped the
@@ -87,6 +86,33 @@ export function readMaterializedBundleMeta(presFP) {
87
86
  }
88
87
  }
89
88
 
89
+ // Every design materialized on this machine: one entry per cache dir that
90
+ // carries a readable bundle.json. `ready` reports these so the agent can ask
91
+ // the user WHICH design to build against when more than one is at hand —
92
+ // no network, never throws.
93
+ export function listCachedBundles() {
94
+ const root = bundleCacheRoot();
95
+ let entries = [];
96
+ try {
97
+ entries = fs.readdirSync(root, { withFileTypes: true });
98
+ } catch {
99
+ return [];
100
+ }
101
+ const out = [];
102
+ for (const entry of entries) {
103
+ if (!entry.isDirectory()) continue;
104
+ const dir = path.join(root, entry.name);
105
+ try {
106
+ const meta = JSON.parse(fs.readFileSync(path.join(dir, "bundle.json"), "utf8"));
107
+ const presFP = meta?.presFP ?? entry.name;
108
+ out.push({ presFP, dir, meta });
109
+ } catch {
110
+ /* half-written or foreign dir — not a design */
111
+ }
112
+ }
113
+ return out;
114
+ }
115
+
90
116
  // Wipe the whole materialized bundle cache so the next build/push/download
91
117
  // re-fetches every design bundle fresh from the add-in. `slidemind init` calls
92
118
  // this: init is the deliberate (re)setup point — and `slidemind update`
@@ -164,22 +190,6 @@ async function downloadInto(targetPath, url) {
164
190
  fs.writeFileSync(targetPath, buf);
165
191
  }
166
192
 
167
- // File extension for a sample image, derived from its wire `contentType`. The
168
- // sample's stable `id` is the basename; samples carry no `filename` on the wire.
169
- function sampleExt(contentType) {
170
- switch (contentType) {
171
- case "image/jpeg":
172
- return ".jpg";
173
- case "image/webp":
174
- return ".webp";
175
- case "image/svg+xml":
176
- return ".svg";
177
- case "image/png":
178
- default:
179
- return ".png";
180
- }
181
- }
182
-
183
193
  const REUSABLE_KIND_ORDER = [
184
194
  "logo",
185
195
  "icon",
@@ -235,11 +245,11 @@ function buildReusableImagesManifest(extractedImages) {
235
245
 
236
246
  // Sync the bundle for `presId`. Idempotent: every call rewrites all inline
237
247
  // text from the freshly fetched wire (the wire is fetched on every call
238
- // anyway), downloads only binaries whose per-item blob etag changed (samples
239
- // are write-once by id), and prunes files the wire no longer serves. There is
248
+ // anyway), downloads only binaries whose per-item blob etag changed, and
249
+ // prunes files the wire no longer serves. There is
240
250
  // deliberately NO staleness token to compare — the materialized dir is always
241
251
  // a per-item projection of the last wire, so authored-layer edits that bypass
242
- // a bundle re-produce (engines, image-source guidelines, sample curation) and
252
+ // a bundle re-produce (engines, image-source guidelines) and
243
253
  // deployment-owned content (workflow refs, the lint engine) can never go
244
254
  // stale here. `force` re-downloads binaries even when their etags match.
245
255
  // Target: `presId` (pane registry), or `brandId` in options for the pane-free
@@ -376,7 +386,7 @@ export async function materializeBundle(
376
386
  return;
377
387
  }
378
388
  if (!force && exists && !etag) {
379
- return; // write-once binary (samples carry no etag) — presence is currency
389
+ return; // write-once binary without an etag — presence is currency
380
390
  }
381
391
  downloads.push({ rel, target, url, etag: etag ?? null, label });
382
392
  };
@@ -408,21 +418,6 @@ export async function materializeBundle(
408
418
  "master empty-layout.pptx",
409
419
  );
410
420
  }
411
- // Per-CD sample slides (the brand's own reference slides) — write-once
412
- // captures keyed by the sample's stable `id`; extension derived from
413
- // `contentType`.
414
- for (const sample of wire.samples ?? []) {
415
- if (!sample?.url || !sample?.id) continue;
416
- queueBinary(
417
- path.join(
418
- "samples",
419
- `${path.basename(sample.id)}${sampleExt(sample.contentType)}`,
420
- ),
421
- sample.url,
422
- null,
423
- `sample ${sample.id}`,
424
- );
425
- }
426
421
  // Reusable master images (logos, decorations, illustrations the add-in's
427
422
  // vision pass lifted from the brand's own master and flagged `reusable`).
428
423
  // Each carries a `workspacePath` (e.g. `assets/master-<sha8>.<ext>`) that the
@@ -522,6 +517,21 @@ export async function materializeBundle(
522
517
  role: f.role,
523
518
  family: f.family,
524
519
  filename: path.basename(f.filename),
520
+ // Face descriptors (multi-face families: Regular/Bold/Italic/…).
521
+ // Without them every face of a family registers descriptor-less and
522
+ // the last one wins for ALL weights — bold measured with the regular
523
+ // face. Older backends omit them; default to a plain 400/normal face.
524
+ weight:
525
+ typeof f.weight === "number" && f.weight >= 100 && f.weight <= 900
526
+ ? f.weight
527
+ : 400,
528
+ style: f.style === "italic" ? "italic" : "normal",
529
+ variable:
530
+ f.variable &&
531
+ typeof f.variable.minWeight === "number" &&
532
+ typeof f.variable.maxWeight === "number"
533
+ ? { minWeight: f.variable.minWeight, maxWeight: f.variable.maxWeight }
534
+ : null,
525
535
  })),
526
536
  binaries,
527
537
  fetchedAt: new Date().toISOString(),
@@ -529,9 +539,13 @@ export async function materializeBundle(
529
539
  };
530
540
  fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2), "utf8");
531
541
 
542
+ // Last, after every write AND the prune — the overlay must win over the wire.
543
+ const overlaid = applyBundleOverlay(dir);
544
+
532
545
  return {
533
546
  dir,
534
547
  presFP,
548
+ overlaid,
535
549
  etag: wire.etag ?? null,
536
550
  version: wire.version ?? null,
537
551
  // Which backend actually served the wire — meaningful on the brandId
@@ -545,6 +559,47 @@ export async function materializeBundle(
545
559
  };
546
560
  }
547
561
 
562
+ // Copy `$SLIDEMIND_BUNDLE_OVERLAY/**` over a materialized bundle. Experiment-only
563
+ // hook for the skill-optimization loop (LOOP_PLAN.md): it lets a run vary the
564
+ // add-in-owned workflow/design docs without a deploy. Deliberately undocumented
565
+ // in user-facing help. The wire re-syncs every text file on EVERY materialize
566
+ // (and the prune deletes anything it doesn't own), so this re-applies each time.
567
+ export function applyBundleOverlay(dir) {
568
+ const overlay = process.env.SLIDEMIND_BUNDLE_OVERLAY;
569
+ if (!overlay) return 0;
570
+ let copied = 0;
571
+ try {
572
+ if (!fs.statSync(overlay).isDirectory()) return 0;
573
+ fs.cpSync(overlay, dir, {
574
+ recursive: true,
575
+ force: true,
576
+ // Docs only (.md/.css): the bundle dir also holds lint-engine.mjs and
577
+ // capture-engine.mjs, which the CLI dynamic-imports — an overlay must
578
+ // never be able to swap executable code, and text files re-sync from the
579
+ // wire on every materialize so a doc overlay can't persist past the
580
+ // experiment (binaries would).
581
+ filter: (src) => {
582
+ let st;
583
+ try {
584
+ st = fs.lstatSync(src);
585
+ } catch {
586
+ return false; // dangling symlink — skip, never fail the materialize
587
+ }
588
+ if (st.isDirectory()) return true;
589
+ if (!/\.(md|css)$/i.test(src)) return false;
590
+ copied += 1;
591
+ return true;
592
+ },
593
+ });
594
+ } catch (err) {
595
+ // The overlay is an experiment convenience — a bad overlay must not break
596
+ // ready/build/push after an otherwise successful sync.
597
+ process.stderr.write(`bundle overlay skipped: ${err.message}\n`);
598
+ return 0;
599
+ }
600
+ return copied;
601
+ }
602
+
548
603
  // Delete files the current sync doesn't own and collapse empty directories.
549
604
  // `expected` holds dir-relative paths exactly as `path.join` built them, so
550
605
  // the comparison is platform-consistent.
@@ -114,6 +114,12 @@ export function buildCaptureInput(deckDir, bundleDir) {
114
114
  family: f.family,
115
115
  filename: path.basename(f.filename),
116
116
  bytesB64: fs.readFileSync(fontPath).toString("base64"),
117
+ // Face descriptors — the capture engine emits per-face @font-face
118
+ // weight/style rules from these (engine defaults 400/normal when a
119
+ // pre-descriptor meta.json omits them).
120
+ weight: f.weight,
121
+ style: f.style,
122
+ variable: f.variable ?? null,
117
123
  });
118
124
  }
119
125
 
@@ -214,3 +220,31 @@ export async function captureDeckLocally({ deckDir, bundleDir, onProgress }) {
214
220
  const input = buildCaptureInput(deckDir, bundleDir);
215
221
  return withBrowser((browser) => engine.captureDeck(browser, input, { onProgress }));
216
222
  }
223
+
224
+ /**
225
+ * The `<Chart>` stand-in runtime the add-in exports from the SAME artifact
226
+ * (`chartPreview: { scripts: [{ filename, source }], tokensCss }`), for the
227
+ * host-free HTML preview (`slidemind build`, and the review gate that serves
228
+ * it). The CLI ships no chart source: the page inlines these before any slide
229
+ * is evaluated, exactly where the add-in's own page loads them.
230
+ *
231
+ * null when the bundle has no engine or the engine predates charts. That is
232
+ * never a silent degradation: such a bundle's lint engine (same deployment)
233
+ * rejects `<Chart>` as an unknown component before a page is ever built.
234
+ */
235
+ export async function loadChartPreview(bundleDir) {
236
+ if (!bundleDir || !bundleHasCaptureEngine(bundleDir)) return null;
237
+ const enginePath = path.join(bundleDir, "capture-engine.mjs");
238
+ const version = readBundleMeta(bundleDir).captureEngineVersion ?? "0";
239
+ const engine = await import(
240
+ `${pathToFileURL(enginePath).href}?v=${encodeURIComponent(version)}`
241
+ );
242
+ const preview = engine.chartPreview;
243
+ if (!preview || !Array.isArray(preview.scripts)) return null;
244
+ return {
245
+ scripts: preview.scripts.filter(
246
+ (s) => s && typeof s.filename === "string" && typeof s.source === "string",
247
+ ),
248
+ tokensCss: typeof preview.tokensCss === "string" ? preview.tokensCss : "",
249
+ };
250
+ }