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.
- package/.agents/plugins/marketplace.json +2 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/AGENTS.md +5 -5
- package/bin/slidemind.mjs +535 -134
- package/package.json +1 -1
- package/skills/create-slides/SKILL.md +718 -127
- package/skills/search/SKILL.md +217 -57
- package/src/bridge/build-and-push.mjs +3 -3
- package/src/bridge/bundle.mjs +91 -36
- package/src/bridge/capture.mjs +34 -0
- package/src/bridge/client.mjs +81 -23
- package/src/bridge/config.mjs +21 -2
- package/src/bridge/engine-download.mjs +26 -7
- package/src/bridge/recolor.mjs +46 -0
- package/src/bridge/setup-claim.mjs +7 -1
- package/src/builders/build.mjs +179 -5
- package/src/builders/gate-server.mjs +25 -36
- package/src/builders/gate-session.js +28 -48
- package/src/builders/lint-fix.mjs +88 -0
- package/src/builders/plan-overlay.js +507 -136
- package/src/builders/plan.mjs +337 -60
- package/src/builders/review-overlay.js +126 -109
- package/src/builders/review.mjs +118 -64
- package/src/builders/storyboard-markdown.mjs +15 -0
- package/src/builders/storyboard-schema.mjs +23 -6
- package/src/builders/visual-families.mjs +20 -4
- package/src/lib/adopt.mjs +10 -6
- package/src/lib/conversions.mjs +26 -6
- package/src/lib/overwrite-guard.mjs +14 -1
- package/src/lib/reconcile.mjs +15 -4
- package/src/pptx/tags.mjs +49 -0
package/skills/search/SKILL.md
CHANGED
|
@@ -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
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
**
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
- **
|
|
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
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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`)
|
|
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,
|
|
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>` —
|
|
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
|
|
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
|
|
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
|
|
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
|
|
279
|
-
`to
|
|
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
|
|
package/src/bridge/bundle.mjs
CHANGED
|
@@ -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
|
|
239
|
-
//
|
|
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
|
|
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
|
|
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.
|
package/src/bridge/capture.mjs
CHANGED
|
@@ -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
|
+
}
|