slidemind 0.3.13 → 0.3.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/AGENTS.md +86 -21
- package/README.md +14 -17
- package/bin/slidemind.mjs +65 -4969
- package/package.json +1 -1
- package/skills/create-slides/SKILL.md +113 -74
- package/skills/search/SKILL.md +25 -7
- package/src/bridge/build-and-push.mjs +108 -15
- package/src/bridge/bundle.mjs +69 -0
- package/src/bridge/capture.mjs +19 -0
- package/src/bridge/client/brands.mjs +622 -0
- package/src/bridge/client/engine-routing.mjs +237 -0
- package/src/bridge/client/engine-search.mjs +310 -0
- package/src/bridge/client/engine-slides.mjs +435 -0
- package/src/bridge/client/pane-ops.mjs +335 -0
- package/src/bridge/client/presentations.mjs +587 -0
- package/src/bridge/client/render.mjs +317 -0
- package/src/bridge/client/transport.mjs +695 -0
- package/src/bridge/client.mjs +80 -3452
- package/src/bridge/config.mjs +1 -1
- package/src/bridge/engine-download.mjs +15 -2
- package/src/builders/build.mjs +11 -804
- package/src/builders/draft-check.mjs +376 -0
- package/src/builders/lint.mjs +38 -2
- package/src/builders/preview.html +799 -0
- package/src/builders/review.mjs +23 -4
- package/src/builders/visual-families.mjs +6 -8
- package/src/cli/common.mjs +179 -0
- package/src/cli/concepts.mjs +310 -0
- package/src/cli/connection.mjs +147 -0
- package/src/cli/deck.mjs +608 -0
- package/src/cli/designs.mjs +113 -0
- package/src/cli/diagnose.mjs +141 -0
- package/src/cli/download-bundle.mjs +144 -0
- package/src/cli/engine-helpers.mjs +429 -0
- package/src/cli/engine-results.mjs +320 -0
- package/src/cli/help.mjs +201 -0
- package/src/cli/install.mjs +257 -0
- package/src/cli/read.mjs +120 -0
- package/src/cli/ready.mjs +805 -0
- package/src/cli/search.mjs +608 -0
- package/src/cli/session.mjs +177 -0
- package/src/cli/setup.mjs +282 -0
- package/src/cli/slide-reuse.mjs +334 -0
- package/src/cli/status.mjs +118 -0
- package/src/cli/workspace.mjs +302 -0
- package/src/installers/codex.mjs +1 -1
- package/src/update-check.mjs +231 -0
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
3
|
+
import { envTag, isMultiEnv } from "../bridge/config.mjs";
|
|
4
|
+
import { fail, resolveDeckArg, SRC_DIR } from "./common.mjs";
|
|
5
|
+
import { readDesignName, readThemeColors, designRows } from "./designs.mjs";
|
|
6
|
+
import { searchFailureHint, writeEmptySearchDiagnostic, writeSearchGrid, resolveEngineRouting, deckBrandId, failOnEngineBlocker, paneRelayForLoginRequired } from "./engine-helpers.mjs";
|
|
7
|
+
|
|
8
|
+
export async function cmdSearch(rest) {
|
|
9
|
+
let asJson = false;
|
|
10
|
+
let to = null;
|
|
11
|
+
let deck = null;
|
|
12
|
+
let engineId = null;
|
|
13
|
+
let resultType = null;
|
|
14
|
+
let intent = null;
|
|
15
|
+
let top = null;
|
|
16
|
+
const queryParts = [];
|
|
17
|
+
for (let i = 0; i < rest.length; i++) {
|
|
18
|
+
const a = rest[i];
|
|
19
|
+
if (a === "--json") {
|
|
20
|
+
asJson = true;
|
|
21
|
+
} else if (a === "--to") {
|
|
22
|
+
to = rest[++i];
|
|
23
|
+
if (!to) fail("--to expects a presId");
|
|
24
|
+
} else if (a === "--deck") {
|
|
25
|
+
deck = rest[++i];
|
|
26
|
+
if (!deck) fail("--deck expects a deck name or path");
|
|
27
|
+
} else if (a === "--engine" || a === "-e" || a === "--engineId") {
|
|
28
|
+
engineId = rest[++i];
|
|
29
|
+
if (!engineId) fail("--engine expects an engineId");
|
|
30
|
+
} else if (a === "--result-type" || a === "--resultType") {
|
|
31
|
+
resultType = rest[++i];
|
|
32
|
+
if (!resultType) fail("--result-type expects slide|image");
|
|
33
|
+
if (!["slide", "image"].includes(resultType)) {
|
|
34
|
+
fail(`--result-type expects slide|image, got '${resultType}'`);
|
|
35
|
+
}
|
|
36
|
+
} else if (a === "--intent") {
|
|
37
|
+
intent = rest[++i];
|
|
38
|
+
if (!intent) fail("--intent expects auto|content|visual");
|
|
39
|
+
if (!["auto", "content", "visual"].includes(intent)) {
|
|
40
|
+
fail(`--intent expects auto|content|visual, got '${intent}'`);
|
|
41
|
+
}
|
|
42
|
+
} else if (a === "--top") {
|
|
43
|
+
const next = rest[++i];
|
|
44
|
+
const n = parseInt(next, 10);
|
|
45
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
46
|
+
fail(`--top expects a positive integer, got '${next}'`);
|
|
47
|
+
}
|
|
48
|
+
top = n;
|
|
49
|
+
} else if (a.startsWith("-")) {
|
|
50
|
+
fail(`unknown flag: ${a}`);
|
|
51
|
+
} else {
|
|
52
|
+
queryParts.push(a);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const query = queryParts.join(" ").trim();
|
|
56
|
+
if (!query) {
|
|
57
|
+
fail(
|
|
58
|
+
`usage: slidemind search "<query>" [--engine <id|scope:all|scope:sharepoint>] [--result-type slide|image] [--intent auto|content|visual] [--top <N>] [--deck <deck>] [--to <presId>] [--json]`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Engine search runs against the add-in's live engine over the bridge —
|
|
63
|
+
// pane-free where the add-in serves the direct routes, relayed through an open
|
|
64
|
+
// window (host required) where it doesn't.
|
|
65
|
+
const client = await import(
|
|
66
|
+
pathToFileURL(path.join(SRC_DIR, "bridge", "client.mjs")).href
|
|
67
|
+
);
|
|
68
|
+
const bundle = await import(
|
|
69
|
+
pathToFileURL(path.join(SRC_DIR, "bridge", "bundle.mjs")).href
|
|
70
|
+
);
|
|
71
|
+
const { resolveAssetExt } = await import(
|
|
72
|
+
pathToFileURL(path.join(SRC_DIR, "bridge", "engine-download.mjs")).href
|
|
73
|
+
);
|
|
74
|
+
// The engine catalog is per-user, not window-bound. On the pane path search
|
|
75
|
+
// attaches to any connected window (preferring a live one) and the chosen
|
|
76
|
+
// window is printed below, because the searchId lives on that pane; on the
|
|
77
|
+
// direct path the searchId is the backend's and no window is involved.
|
|
78
|
+
const routing = await resolveEngineRouting(client, {
|
|
79
|
+
to,
|
|
80
|
+
brandId: await deckBrandId(deck ? resolveDeckArg(deck) : null),
|
|
81
|
+
pick: "any",
|
|
82
|
+
});
|
|
83
|
+
const presId = routing.presId;
|
|
84
|
+
const backendURL = routing.backendURL;
|
|
85
|
+
// Mutable, because a login_required refusal can push a direct search onto
|
|
86
|
+
// the pane relay below — and once it does, the searchId belongs to a WINDOW
|
|
87
|
+
// and every next-step hint has to name it.
|
|
88
|
+
let direct = routing.direct;
|
|
89
|
+
let registry = routing.registry;
|
|
90
|
+
let entry = routing.entry;
|
|
91
|
+
|
|
92
|
+
// An explicit content/visual intent means "search slides", not imagery.
|
|
93
|
+
const slideIntent = intent === "content" || intent === "visual";
|
|
94
|
+
// Thread the engine subtype on the wire only when the CLI actually KNOWS it:
|
|
95
|
+
// `--intent content` is slide-only by contract (image engines are
|
|
96
|
+
// visual-only), the pickImageEngine default branch is an image engine by
|
|
97
|
+
// construction, and a scope search names its index below. `--intent visual`
|
|
98
|
+
// asserts NOTHING — it is a valid intent for image AND slide engines, and
|
|
99
|
+
// asserting "slide" from it made the backend record an icon-engine search
|
|
100
|
+
// as slide results (the footer then taught add-slide instead of
|
|
101
|
+
// download-result). Omitted subtype → the backend derives it from what the
|
|
102
|
+
// results actually are.
|
|
103
|
+
let subtype = intent === "content" ? "slide" : null;
|
|
104
|
+
// `--engine Icons` — a display name straight off `ls engines` — would reach
|
|
105
|
+
// the backend verbatim and 404 as "Engine not found". Humans type names,
|
|
106
|
+
// agents paste ids; when the value cannot be an id (spaces, non-hex), look it
|
|
107
|
+
// up in the live catalog by label. Best-effort on purpose: no unique match
|
|
108
|
+
// passes the value through so the existing unknown-engine hint still fires.
|
|
109
|
+
if (
|
|
110
|
+
engineId &&
|
|
111
|
+
!bundle.isSearchScopeId(engineId) &&
|
|
112
|
+
!/^[0-9a-f-]{8,}$/i.test(engineId)
|
|
113
|
+
) {
|
|
114
|
+
const live = await client.listEngines({ backendURL }).catch(() => null);
|
|
115
|
+
const wanted = engineId.toLowerCase();
|
|
116
|
+
const ids = new Set(
|
|
117
|
+
(live?.engines ?? [])
|
|
118
|
+
.filter((e) => String(e.label ?? "").toLowerCase() === wanted)
|
|
119
|
+
.map((e) => e.id),
|
|
120
|
+
);
|
|
121
|
+
if (ids.size === 1) {
|
|
122
|
+
const [id] = ids;
|
|
123
|
+
process.stderr.write(`engine '${engineId}' resolved to ${id}\n`);
|
|
124
|
+
engineId = id;
|
|
125
|
+
} else if (ids.size > 1) {
|
|
126
|
+
fail(
|
|
127
|
+
`--engine '${engineId}' names ${ids.size} different engines across your connected environments — pass the id from 'slidemind ls engines', or scope with --backend.`,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (!engineId) {
|
|
132
|
+
if (slideIntent) {
|
|
133
|
+
// Slide search with no engine named: search the corpus UNFILTERED rather
|
|
134
|
+
// than refusing. An engine is a saved folder filter over the add-in's
|
|
135
|
+
// shared corpus, so a workspace can hold a full corpus and zero slide
|
|
136
|
+
// engines — demanding --engine here dead-ended exactly that case.
|
|
137
|
+
engineId = bundle.SCOPE_ALL;
|
|
138
|
+
resultType = resultType ?? "slide";
|
|
139
|
+
} else {
|
|
140
|
+
// Imagery: resolve a real image engine (a scope is not a brand image
|
|
141
|
+
// source). Ask the LIVE catalog first — the same one the add-in's own
|
|
142
|
+
// design agent reads — and fall back to the materialized bundle only when
|
|
143
|
+
// the host can't answer. Bundle-first would be stale: engine ids can be
|
|
144
|
+
// retired server-side, and a dead id surfaces as "Engine not found".
|
|
145
|
+
const live = await client
|
|
146
|
+
.listEngines({ subtype: "image", backendURL })
|
|
147
|
+
.catch(() => ({ engines: [] }));
|
|
148
|
+
let pick = bundle.pickImageEngine(live.engines);
|
|
149
|
+
if (!pick) {
|
|
150
|
+
// Best-effort: a bundle that can't be materialized (e.g. an incomplete
|
|
151
|
+
// one) must not replace the actionable "wire an image engine" message
|
|
152
|
+
// below with a bundle error — the live catalog is the real answer here.
|
|
153
|
+
// Pane path: fetch by the window's presId. Direct path: there is no
|
|
154
|
+
// window, so only a bound deck (--deck) can name the design; without one
|
|
155
|
+
// there is nothing to fall back to and the live catalog's answer stands.
|
|
156
|
+
const brandId = presId
|
|
157
|
+
? null
|
|
158
|
+
: await deckBrandId(deck ? resolveDeckArg(deck) : null);
|
|
159
|
+
const mat =
|
|
160
|
+
presId || brandId
|
|
161
|
+
? await bundle
|
|
162
|
+
.materializeBundle(presId, { backendURL, brandId })
|
|
163
|
+
.catch(() => null)
|
|
164
|
+
: null;
|
|
165
|
+
if (mat) pick = bundle.pickImageEngine(bundle.readEnginesFromDir(mat.dir));
|
|
166
|
+
}
|
|
167
|
+
if (!pick) {
|
|
168
|
+
fail(
|
|
169
|
+
"This corporate design has no image engines to search. Run 'slidemind ls engines' to see what exists, or ask the add-in owner to wire an image/icon engine.",
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
engineId = pick.id;
|
|
173
|
+
subtype = "image";
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// A scope has no subtype of its own, so the index must be named explicitly —
|
|
178
|
+
// the add-in rejects a scope search without it. Default to slides: reusing an
|
|
179
|
+
// existing slide is what an unfiltered corpus search is overwhelmingly for.
|
|
180
|
+
if (bundle.isSearchScopeId(engineId)) {
|
|
181
|
+
resultType = resultType ?? "slide";
|
|
182
|
+
subtype = resultType;
|
|
183
|
+
} else if (resultType) {
|
|
184
|
+
// Harmless but meaningless on an engine, whose own subtype is authoritative.
|
|
185
|
+
process.stderr.write(
|
|
186
|
+
`note: --result-type is ignored for engine '${engineId}' (it applies to scope:* targets); the engine's own type decides.\n`,
|
|
187
|
+
);
|
|
188
|
+
resultType = null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Direct: one pane-free call against the backend that owns the searchId.
|
|
192
|
+
// Pane: window-agnostic — tries the picked pane, then fails over to any other
|
|
193
|
+
// live window on the same backend if it's suspended/frozen. The answering
|
|
194
|
+
// window (res.presId) is what reuse must target there, since the searchId is
|
|
195
|
+
// bound to it.
|
|
196
|
+
const relaySearch = (reg) =>
|
|
197
|
+
client.engineSearchAnyPane({
|
|
198
|
+
registry: reg,
|
|
199
|
+
explicitPresId: to,
|
|
200
|
+
engineId,
|
|
201
|
+
query,
|
|
202
|
+
subtype,
|
|
203
|
+
resultType,
|
|
204
|
+
intent: intent ?? null,
|
|
205
|
+
top: top ?? null,
|
|
206
|
+
});
|
|
207
|
+
let res;
|
|
208
|
+
try {
|
|
209
|
+
res = direct
|
|
210
|
+
? await client.engineSearch({
|
|
211
|
+
engineId,
|
|
212
|
+
query,
|
|
213
|
+
subtype,
|
|
214
|
+
resultType,
|
|
215
|
+
intent: intent ?? null,
|
|
216
|
+
top: top ?? null,
|
|
217
|
+
backendURL,
|
|
218
|
+
})
|
|
219
|
+
: await relaySearch(routing.registry);
|
|
220
|
+
} catch (err) {
|
|
221
|
+
// login_required stops the PANE-FREE path only — an open pane signs each
|
|
222
|
+
// request with the browser's live token. Creating a new search is exactly
|
|
223
|
+
// the operation the pane can replay (it mints its own searchId), so relay
|
|
224
|
+
// it rather than parking the search on a sign-in the user hasn't finished.
|
|
225
|
+
const relay = direct ? await paneRelayForLoginRequired(client, err, { to }) : null;
|
|
226
|
+
if (!relay) await failOnEngineBlocker(client, err, { to });
|
|
227
|
+
process.stderr.write(
|
|
228
|
+
`note: this CLI is not signed in to Microsoft 365, so the search ran through the open ` +
|
|
229
|
+
`slidemind pane${envTag(relay.entry.env)} instead (it signs each request live).\n`,
|
|
230
|
+
);
|
|
231
|
+
direct = false;
|
|
232
|
+
registry = relay.registry;
|
|
233
|
+
entry = relay.entry;
|
|
234
|
+
try {
|
|
235
|
+
res = await relaySearch(registry);
|
|
236
|
+
} catch (relayErr) {
|
|
237
|
+
await failOnEngineBlocker(client, relayErr, { to });
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const ranPresId = res?.presId ?? entry?.presId ?? presId;
|
|
241
|
+
const ranEntry = direct
|
|
242
|
+
? { env: res?.env ?? null }
|
|
243
|
+
: (registry.find((p) => p.presId === ranPresId) ?? entry);
|
|
244
|
+
if (asJson) {
|
|
245
|
+
// The searchId rides the JSON so an interrupted agent can resume its picks
|
|
246
|
+
// without re-searching. The backendURL never does — env label only.
|
|
247
|
+
const { backendURL: _routingOnly, ...pub } = res ?? {};
|
|
248
|
+
process.stdout.write(`${JSON.stringify(pub, null, 2)}\n`);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (!res?.ok) {
|
|
252
|
+
process.stderr.write(`search failed: ${searchFailureHint(res?.error, engineId)}\n`);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
// Prefer the new `results` array (image + slide engines); fall back to the
|
|
256
|
+
// legacy `thumbnails` shape older add-ins still return for image engines.
|
|
257
|
+
const results = Array.isArray(res.results)
|
|
258
|
+
? res.results
|
|
259
|
+
: (res.thumbnails ?? []).map((t, i) => ({ selection: i + 1, title: t.title }));
|
|
260
|
+
const count = res.resultCount ?? results.length;
|
|
261
|
+
const targetLabel = bundle.isSearchScopeId(engineId)
|
|
262
|
+
? `scope ${engineId}`
|
|
263
|
+
: `engine ${engineId}`;
|
|
264
|
+
process.stdout.write(
|
|
265
|
+
`searchId ${res.searchId} — ${count} result(s) from ${targetLabel}` +
|
|
266
|
+
(res.intent ? ` (${res.intent})` : "") +
|
|
267
|
+
`\n`,
|
|
268
|
+
);
|
|
269
|
+
if (count === 0) {
|
|
270
|
+
writeEmptySearchDiagnostic(engineId, bundle);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const isContent = res.intent === "content";
|
|
274
|
+
// A SharePoint-indexed result carries the source file's browser link. Print
|
|
275
|
+
// it under the title so the user can open the original presentation — the
|
|
276
|
+
// add-in only stamps it where the index has one, so its absence is normal.
|
|
277
|
+
const writeSourceLink = (r) => {
|
|
278
|
+
if (typeof r.sourceUrl === "string" && r.sourceUrl) {
|
|
279
|
+
process.stdout.write(` source: ${r.sourceUrl}\n`);
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
results.forEach((r, i) => {
|
|
283
|
+
const sel = r.selection ?? i + 1;
|
|
284
|
+
if (isContent) {
|
|
285
|
+
// Content results: per-result header + the bare extracted text block.
|
|
286
|
+
const score = typeof r.score === "number" ? ` · ${r.score.toFixed(3)}` : "";
|
|
287
|
+
process.stdout.write(` ${sel}. ${r.title ?? "(untitled)"}${score}\n`);
|
|
288
|
+
writeSourceLink(r);
|
|
289
|
+
if (r.text) {
|
|
290
|
+
for (const line of String(r.text).split("\n")) {
|
|
291
|
+
process.stdout.write(` ${line}\n`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
} else {
|
|
295
|
+
// Visual / image results: the numbered list. An image result carries its
|
|
296
|
+
// intrinsic size from the engine index — show it, so the box can be sized
|
|
297
|
+
// to the real aspect ratio at authoring time rather than after the fetch.
|
|
298
|
+
const dims =
|
|
299
|
+
r.kind === "image" && Number.isFinite(r.width) && Number.isFinite(r.height)
|
|
300
|
+
? ` ${r.width}×${r.height}`
|
|
301
|
+
: "";
|
|
302
|
+
process.stdout.write(` ${sel}. ${r.title ?? "(untitled)"}${dims}\n`);
|
|
303
|
+
writeSourceLink(r);
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
// Tailor the next-step hint to the result kind: slide verbs (insert-slide /
|
|
307
|
+
// add-slide) for slide results, the image-download hint otherwise. The
|
|
308
|
+
// per-result `kind`s are authoritative — the top-level subtype echo has been
|
|
309
|
+
// observed WRONG on live backends (icon-engine searches echoing
|
|
310
|
+
// subtype:"slide" over kind:"image" results, which pointed agents at
|
|
311
|
+
// add-slide instead of download-result) — with the subtype as the fallback
|
|
312
|
+
// for add-ins that don't stamp per-result kinds.
|
|
313
|
+
const resultKinds = results.map((r) => r.kind).filter(Boolean);
|
|
314
|
+
const isSlideResults = resultKinds.length
|
|
315
|
+
? resultKinds.some((k) => k === "slide")
|
|
316
|
+
: res.subtype === "slide";
|
|
317
|
+
// Visual results: point at the ONE collage rather than making the caller pull
|
|
318
|
+
// each preview separately. The numbers burned into it match the list above.
|
|
319
|
+
if (!isContent) {
|
|
320
|
+
const gridPath = writeSearchGrid(res.gridDataUri, res.searchId, resolveAssetExt);
|
|
321
|
+
if (gridPath) {
|
|
322
|
+
process.stdout.write(
|
|
323
|
+
`Contact sheet of all ${count} result(s) — LOOK AT THIS FILE to choose, ` +
|
|
324
|
+
`don't fetch previews one by one: ${gridPath}\n`,
|
|
325
|
+
);
|
|
326
|
+
} else if (direct && isSlideResults) {
|
|
327
|
+
// The pane composes the collage in a DOM; the pane-free route may not
|
|
328
|
+
// have one to hand back (`gridUnavailable`, or simply no grid field on an
|
|
329
|
+
// add-in that hasn't grown it yet). Say where previews come from instead
|
|
330
|
+
// — otherwise an agent that has learned to expect a contact sheet quietly
|
|
331
|
+
// issues N thumbnail fetches to rebuild it.
|
|
332
|
+
process.stdout.write(
|
|
333
|
+
`No contact sheet on this path — preview candidates one at a time instead: ` +
|
|
334
|
+
`slidemind view-slide --search ${res.searchId} --pick <n>\n`,
|
|
335
|
+
);
|
|
336
|
+
} else if (direct) {
|
|
337
|
+
// Image/icon results carry NO preview on the pane-free path (view-slide
|
|
338
|
+
// answers "no image returned" for them). The download IS the preview:
|
|
339
|
+
// fetch the pick, look at the saved file, re-search once if wrong.
|
|
340
|
+
process.stdout.write(
|
|
341
|
+
`No preview for image results on this path — download the pick(s) and look at the saved file(s) ` +
|
|
342
|
+
`(cheap; re-search once if wrong; two misses = mark it (no result)).\n`,
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
const envLabel = envTag(ranEntry?.env);
|
|
347
|
+
if (direct) {
|
|
348
|
+
// The searchId belongs to the BACKEND here, not to a window: picks work with
|
|
349
|
+
// PowerPoint closed, so the follow-up commands carry no --to. Live insert is
|
|
350
|
+
// the one step that still names a window (it types into an open deck).
|
|
351
|
+
if (isSlideResults) {
|
|
352
|
+
process.stdout.write(
|
|
353
|
+
`searchId ${res.searchId}${envLabel} is held by the add-in — reuse it with no PowerPoint window:\n` +
|
|
354
|
+
` slidemind add-slide --search ${res.searchId} --pick <n> --deck <deck>\n` +
|
|
355
|
+
` slidemind insert-slide --search ${res.searchId} --pick <n> --to <presId> (live insert needs an open window)\n`,
|
|
356
|
+
);
|
|
357
|
+
} else {
|
|
358
|
+
process.stdout.write(
|
|
359
|
+
`searchId ${res.searchId}${envLabel} is held by the add-in — download image result(s) into a deck (no window needed):\n` +
|
|
360
|
+
` slidemind download-result --search ${res.searchId} --pick <n[,n,...]> --deck <deck>\n`,
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
} else if (isSlideResults) {
|
|
364
|
+
// The searchId is bound to the window the search ran on — reuse must target
|
|
365
|
+
// that same window, so spell out its presId in the next-step commands.
|
|
366
|
+
process.stdout.write(
|
|
367
|
+
`searchId ${res.searchId} ran on window ${ranPresId}${envLabel} — reuse from THIS window:\n` +
|
|
368
|
+
` slidemind insert-slide --search ${res.searchId} --pick <n> --to ${ranPresId}\n` +
|
|
369
|
+
` slidemind add-slide --search ${res.searchId} --pick <n> --deck <deck> --to ${ranPresId}\n`,
|
|
370
|
+
);
|
|
371
|
+
} else {
|
|
372
|
+
// The searchId is bound to the window the search ran on — download must
|
|
373
|
+
// target that same window, so spell out its presId in the next-step command.
|
|
374
|
+
process.stdout.write(
|
|
375
|
+
`searchId ${res.searchId} ran on window ${ranPresId}${envLabel} — download image result(s) into a deck:\n` +
|
|
376
|
+
` slidemind download-result --search ${res.searchId} --pick <n[,n,...]> --deck <deck> --to ${ranPresId}\n`,
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Every corporate design this machine can materialize a bundle for: the ones
|
|
382
|
+
// already cached here, plus the ones the connected backends list. This is the
|
|
383
|
+
// discovery half of bundle management — without it the only way to learn a
|
|
384
|
+
// presFP was to run `ready` and read its menu (or its ambiguous blocker), which
|
|
385
|
+
// also resolves and binds. Listing is read-only: no design is picked, no bundle
|
|
386
|
+
// is fetched, no deck is touched. Pair it with `download-bundle --brand <fp>`.
|
|
387
|
+
export async function cmdLsDesigns(rest) {
|
|
388
|
+
let asJson = false;
|
|
389
|
+
for (const a of rest) {
|
|
390
|
+
if (a === "--json") asJson = true;
|
|
391
|
+
else if (a.startsWith("-")) fail(`unknown flag: ${a}`);
|
|
392
|
+
else fail(`unexpected argument: ${a}`);
|
|
393
|
+
}
|
|
394
|
+
const bundle = await import(
|
|
395
|
+
pathToFileURL(path.join(SRC_DIR, "bridge", "bundle.mjs")).href
|
|
396
|
+
);
|
|
397
|
+
const rows = new Map();
|
|
398
|
+
for (const b of bundle.listCachedBundles()) {
|
|
399
|
+
rows.set(b.presFP, {
|
|
400
|
+
presFP: b.presFP,
|
|
401
|
+
name: readDesignName(b.dir),
|
|
402
|
+
colors: readThemeColors(b.dir),
|
|
403
|
+
cached: true,
|
|
404
|
+
etag: b.meta?.etag ?? null,
|
|
405
|
+
dir: b.dir,
|
|
406
|
+
envs: [],
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
// The backend list is a bonus, never a precondition: an unreachable or older
|
|
410
|
+
// add-in (no brandsList capability) still leaves every cached design listed.
|
|
411
|
+
let listed = [];
|
|
412
|
+
try {
|
|
413
|
+
const client = await import(
|
|
414
|
+
pathToFileURL(path.join(SRC_DIR, "bridge", "client.mjs")).href
|
|
415
|
+
);
|
|
416
|
+
client.setDiagnosticMode(true);
|
|
417
|
+
listed = await client.listBrands();
|
|
418
|
+
} catch {
|
|
419
|
+
listed = [];
|
|
420
|
+
}
|
|
421
|
+
for (const b of listed) {
|
|
422
|
+
if (!rows.has(b.presFP)) {
|
|
423
|
+
rows.set(b.presFP, {
|
|
424
|
+
presFP: b.presFP,
|
|
425
|
+
name: b.name ?? null,
|
|
426
|
+
colors: null,
|
|
427
|
+
cached: false,
|
|
428
|
+
etag: null,
|
|
429
|
+
dir: null,
|
|
430
|
+
envs: [],
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
const row = rows.get(b.presFP);
|
|
434
|
+
row.name ??= b.name ?? null;
|
|
435
|
+
if (b.env && !row.envs.includes(b.env)) row.envs.push(b.env);
|
|
436
|
+
}
|
|
437
|
+
const designs = [...rows.values()];
|
|
438
|
+
if (asJson) {
|
|
439
|
+
process.stdout.write(`${JSON.stringify(designs, null, 2)}\n`);
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
if (designs.length === 0) {
|
|
443
|
+
process.stderr.write(
|
|
444
|
+
"No corporate design is cached here and no connected add-in lists one. " +
|
|
445
|
+
"Run `slidemind ready` with PowerPoint open to materialize the active design.\n",
|
|
446
|
+
);
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
process.stdout.write(`${designRows(designs).join("\n")}\n`);
|
|
450
|
+
process.stdout.write(
|
|
451
|
+
"Materialize or refresh one with: slidemind download-bundle --brand <presFP>\n" +
|
|
452
|
+
"Refresh every cached design with: slidemind download-bundle --all\n",
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export async function cmdLs(rest) {
|
|
457
|
+
const [sub, ...subRest] = rest;
|
|
458
|
+
if (sub === "designs" || sub === "brands") {
|
|
459
|
+
await cmdLsDesigns(subRest);
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
if (sub !== "engines") {
|
|
463
|
+
fail(
|
|
464
|
+
"usage: slidemind ls engines [--type collection|search|scope] [--subtype image|slide|template|content] [--json]\n" +
|
|
465
|
+
" slidemind ls designs [--json]",
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
let type = null;
|
|
469
|
+
let subtype = null;
|
|
470
|
+
let to = null;
|
|
471
|
+
let all = false;
|
|
472
|
+
let asJson = false;
|
|
473
|
+
for (let i = 0; i < subRest.length; i++) {
|
|
474
|
+
const a = subRest[i];
|
|
475
|
+
if (a === "--type") {
|
|
476
|
+
type = subRest[++i];
|
|
477
|
+
if (!type) fail("--type expects collection|search|scope");
|
|
478
|
+
// "scope" narrows to the non-engine targets (scope:all / scope:sharepoint).
|
|
479
|
+
if (!["collection", "search", "scope"].includes(type)) {
|
|
480
|
+
fail(`--type expects collection|search|scope, got '${type}'`);
|
|
481
|
+
}
|
|
482
|
+
} else if (a === "--subtype") {
|
|
483
|
+
subtype = subRest[++i];
|
|
484
|
+
if (!subtype) fail("--subtype expects image|slide|template|content");
|
|
485
|
+
if (!["image", "slide", "template", "content"].includes(subtype)) {
|
|
486
|
+
fail(`--subtype expects image|slide|template|content, got '${subtype}'`);
|
|
487
|
+
}
|
|
488
|
+
} else if (a === "--to") {
|
|
489
|
+
to = subRest[++i];
|
|
490
|
+
if (!to) fail("--to expects a presId");
|
|
491
|
+
} else if (a === "--all") {
|
|
492
|
+
all = true;
|
|
493
|
+
} else if (a === "--json") {
|
|
494
|
+
asJson = true;
|
|
495
|
+
} else if (a.startsWith("-")) {
|
|
496
|
+
fail(`unknown flag: ${a}`);
|
|
497
|
+
} else {
|
|
498
|
+
fail(`unexpected argument: ${a}`);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
const client = await import(
|
|
502
|
+
pathToFileURL(path.join(SRC_DIR, "bridge", "client.mjs")).href
|
|
503
|
+
);
|
|
504
|
+
if (all && to) fail("--all cannot be combined with --to");
|
|
505
|
+
let backendURL = null;
|
|
506
|
+
if (!all) {
|
|
507
|
+
// Pane-free where the add-in serves the direct routes; otherwise the catalog
|
|
508
|
+
// is relayed through a window, so a live pane is still the precondition.
|
|
509
|
+
try {
|
|
510
|
+
backendURL = (await resolveEngineRouting(client, { to, pick: "any" }))
|
|
511
|
+
.backendURL;
|
|
512
|
+
} catch (err) {
|
|
513
|
+
// Listing is the one engine op that can simply show EVERY environment
|
|
514
|
+
// (each engine is tagged with its env), so an ambiguous multi-environment
|
|
515
|
+
// session fans out here rather than demanding --backend.
|
|
516
|
+
if (err?.code !== "engine_backend_ambiguous") throw err;
|
|
517
|
+
backendURL = null;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
let engines;
|
|
521
|
+
// The pane-free route's summary of the catalog BEFORE the type/subtype
|
|
522
|
+
// filter; null on older add-ins and on the pane-relay path.
|
|
523
|
+
let catalog = null;
|
|
524
|
+
// Which environments answered. The catalog is POOLED, and pooling dedupes the
|
|
525
|
+
// scopes every environment repeats — so a second environment can contribute
|
|
526
|
+
// no row of its own, and the surviving rows can no longer be counted to learn
|
|
527
|
+
// whether more than one answered.
|
|
528
|
+
let envs = [];
|
|
529
|
+
let failures = [];
|
|
530
|
+
try {
|
|
531
|
+
({ engines, catalog, envs = [], failures = [] } = await client.listEngines({
|
|
532
|
+
type,
|
|
533
|
+
subtype,
|
|
534
|
+
backendURL,
|
|
535
|
+
}));
|
|
536
|
+
} catch (err) {
|
|
537
|
+
// No pane fallback here: `GET /api/cli/engines` is the catalog route on both
|
|
538
|
+
// add-in generations (newer ones answer it directly, older ones relay it
|
|
539
|
+
// themselves), so there is no second route to retry on. The message does
|
|
540
|
+
// name the open pane, because searching THROUGH it still works.
|
|
541
|
+
await failOnEngineBlocker(client, err, { to });
|
|
542
|
+
}
|
|
543
|
+
if (asJson) {
|
|
544
|
+
process.stdout.write(`${JSON.stringify({ engines, catalog, envs, failures }, null, 2)}\n`);
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
// A fanned-out listing never opens the browser sign-in for an environment
|
|
548
|
+
// nobody asked about; an environment that refused for that reason is named
|
|
549
|
+
// here instead of silently missing from the pooled catalog.
|
|
550
|
+
for (const f of failures ?? []) {
|
|
551
|
+
process.stderr.write(` note: ${f.env} did not answer — ${f.message}\n`);
|
|
552
|
+
}
|
|
553
|
+
if (engines.length === 0) {
|
|
554
|
+
// A filter that matched nothing is NOT an empty deployment — the summary is
|
|
555
|
+
// the only thing that tells the two apart, so say which one this is and the
|
|
556
|
+
// next move follows (drop the filter vs. search the corpus unfiltered).
|
|
557
|
+
const unfiltered = catalog ? catalog.totalEngines + catalog.totalScopes : 0;
|
|
558
|
+
if (catalog?.filtered && unfiltered > 0) {
|
|
559
|
+
process.stderr.write(
|
|
560
|
+
`No engine or scope matched this filter, but ${catalog.totalEngines} engine(s) ` +
|
|
561
|
+
`and ${catalog.totalScopes} scope(s) exist. Re-run 'slidemind ls engines' ` +
|
|
562
|
+
"without --type/--subtype to see them.\n",
|
|
563
|
+
);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
// Deliberately not "nothing to search": an engine is a saved folder filter
|
|
567
|
+
// over the add-in's shared corpus, so an empty catalog says only that nobody
|
|
568
|
+
// saved a filter. An older add-in that predates scopes lands here too.
|
|
569
|
+
const { SCOPE_ALL } = await import(
|
|
570
|
+
pathToFileURL(path.join(SRC_DIR, "bridge", "bundle.mjs")).href
|
|
571
|
+
);
|
|
572
|
+
process.stderr.write(
|
|
573
|
+
"No engines or scopes matched. An engine is a saved folder filter over the corpus, " +
|
|
574
|
+
"so an empty list does NOT mean there is nothing to search — try " +
|
|
575
|
+
`'slidemind search "<query>" --engine ${SCOPE_ALL} --result-type slide' ` +
|
|
576
|
+
"to search it unfiltered.\n",
|
|
577
|
+
);
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
// Show the env tag only when more than one environment contributed. Two
|
|
581
|
+
// environments can carry identically-named engines, and without the tag the
|
|
582
|
+
// pooled list gives no way to tell them apart. Deduped scope rows carry no env
|
|
583
|
+
// of their own (they exist on every environment), so they stay untagged.
|
|
584
|
+
const multiEnv =
|
|
585
|
+
isMultiEnv() || envs.length > 1 || new Set(engines.map((e) => e.env)).size > 1;
|
|
586
|
+
for (const e of engines) {
|
|
587
|
+
const kind = [e.type, e.subtype].filter(Boolean).join("/");
|
|
588
|
+
const env = multiEnv && e.env ? `[${e.env}] ` : "";
|
|
589
|
+
const desc = e.description ? ` — ${e.description}` : "";
|
|
590
|
+
// A scope takes --result-type instead of having a subtype of its own; spell
|
|
591
|
+
// that out so the agent doesn't have to infer it from the missing subtype.
|
|
592
|
+
const hint =
|
|
593
|
+
e.type === "scope"
|
|
594
|
+
? ` (needs --result-type ${(e.capabilities?.resultTypes ?? ["slide", "image"]).join("|")})`
|
|
595
|
+
: "";
|
|
596
|
+
// An engine that exists but has nothing indexed yet returns zero hits for
|
|
597
|
+
// every query; say so here so an empty search isn't read as a bad engine id.
|
|
598
|
+
const state =
|
|
599
|
+
e.content?.state === "empty"
|
|
600
|
+
? " (nothing indexed yet)"
|
|
601
|
+
: e.content?.state === "indexing"
|
|
602
|
+
? " (indexing)"
|
|
603
|
+
: "";
|
|
604
|
+
process.stdout.write(
|
|
605
|
+
`${env}${e.id} · ${e.label ?? e.id} · ${kind}${hint}${state}${desc}\n`,
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
}
|