slidemind 0.1.9 → 0.2.1
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/marketplace.json +25 -0
- package/.claude-plugin/plugin.json +6 -3
- package/.codex-plugin/plugin.json +29 -0
- package/CLAUDE.md +25 -5
- package/README.md +24 -0
- package/bin/slidemind +7 -0
- package/bin/slidemind.cmd +2 -0
- package/bin/slidemind.mjs +480 -60
- package/package.json +2 -1
- package/skills/create-slides/SKILL.md +6 -5
- package/skills/search/SKILL.md +10 -4
- package/src/bridge/bundle.mjs +23 -4
- package/src/bridge/client.mjs +414 -7
- package/src/bridge/engine-download.mjs +229 -5
- package/src/bridge/image-dimensions.mjs +181 -0
- package/src/builders/resolve-skill.mjs +45 -1
- package/src/builders/review.mjs +4 -1
- package/src/install-channel.mjs +61 -0
- package/src/installers/claude.mjs +22 -0
- package/src/pptx/fill/engine.mjs +1273 -0
- package/src/pptx/fill/freeform.mjs +156 -0
- package/src/pptx/fill/parse-slides.mjs +154 -0
- package/src/pptx/fill/xml.mjs +486 -0
- package/src/pptx/ooxml.mjs +10 -3
package/bin/slidemind.mjs
CHANGED
|
@@ -26,6 +26,13 @@ import os from "node:os";
|
|
|
26
26
|
import path from "node:path";
|
|
27
27
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
28
28
|
|
|
29
|
+
// The only eager `src/` import: node-builtins only, and the version-floor
|
|
30
|
+
// ladder below is sync, so it cannot be lazily loaded like the rest.
|
|
31
|
+
import {
|
|
32
|
+
isClaudePluginInstall,
|
|
33
|
+
updateCliInstruction,
|
|
34
|
+
} from "../src/install-channel.mjs";
|
|
35
|
+
|
|
29
36
|
const __filename = fileURLToPath(import.meta.url);
|
|
30
37
|
const __dirname = path.dirname(__filename);
|
|
31
38
|
const PACKAGE_ROOT = path.resolve(__dirname, "..");
|
|
@@ -68,7 +75,7 @@ function printHelp() {
|
|
|
68
75
|
` slidemind review <deck> Open a spatial preview review; blocks until the user\n` +
|
|
69
76
|
` approves or leaves comments (host-free)\n` +
|
|
70
77
|
` --port <n> bind a fixed port (default: ephemeral)\n` +
|
|
71
|
-
` slidemind build-pptx <deck> Export native .pptx via the add-in (
|
|
78
|
+
` slidemind build-pptx <deck> Export native .pptx — via the add-in for compose decks (host), LOCALLY for template packs (host-free)\n` +
|
|
72
79
|
` --to <presId> target a specific window (required if >1 open)\n` +
|
|
73
80
|
` --out <path> output path (default <deck>/<deck>.pptx)\n` +
|
|
74
81
|
` slidemind view-draft <deck> Render the deck's slides to per-slide PNGs (no push, no .pptx)\n` +
|
|
@@ -77,8 +84,10 @@ function printHelp() {
|
|
|
77
84
|
` --to <presId> window to fetch the bundle from (only if not cached)\n` +
|
|
78
85
|
` --out <dir> output dir (default <deck>/.draft-preview/)\n` +
|
|
79
86
|
` --open open each rendered PNG\n` +
|
|
80
|
-
` slidemind search "<query>" Search an engine for images or slides (requires an open host)\n` +
|
|
81
|
-
` --engine <id> engine
|
|
87
|
+
` slidemind search "<query>" Search an engine or scope for images or slides (requires an open host)\n` +
|
|
88
|
+
` --engine <id> engine, or scope:all | scope:sharepoint to search unfiltered\n` +
|
|
89
|
+
` (default: first image engine; scope:all for slide intents)\n` +
|
|
90
|
+
` --result-type <t> slide | image — required for a scope:* target\n` +
|
|
82
91
|
` --intent <i> auto | content | visual (slide engines)\n` +
|
|
83
92
|
` --top <N> max results\n` +
|
|
84
93
|
` --to <presId> target a specific window (required if >1 open)\n` +
|
|
@@ -88,8 +97,8 @@ function printHelp() {
|
|
|
88
97
|
` --pick <n> 1-based selection number to view\n` +
|
|
89
98
|
` --to <presId> the window the search ran on (required if >1 open)\n` +
|
|
90
99
|
` --out <dir> output dir (default: a slidemind temp dir)\n` +
|
|
91
|
-
` slidemind ls engines List the live engine catalog for an active pane\n` +
|
|
92
|
-
` --type <t> collection | search\n` +
|
|
100
|
+
` slidemind ls engines List the live engine catalog + search scopes for an active pane\n` +
|
|
101
|
+
` --type <t> collection | search | scope\n` +
|
|
93
102
|
` --subtype <s> image | slide | template | content\n` +
|
|
94
103
|
` --to <presId> target a specific window/backend\n` +
|
|
95
104
|
` --all include every paired backend\n` +
|
|
@@ -112,6 +121,12 @@ function printHelp() {
|
|
|
112
121
|
` --deck <deck> deck to write the asset(s) into\n` +
|
|
113
122
|
` --to <presId> the window the search ran on (required if >1 open)\n` +
|
|
114
123
|
` --icon-color <hex[,hex,..]> bake a colour into SVG result(s) (one for all, or one per --pick)\n` +
|
|
124
|
+
` slidemind imagen "<prompt>" Generate ONE image with the add-in's image model into a deck's assets/ (paired backend, pane-free)\n` +
|
|
125
|
+
` --deck <deck> bound deck to write the image into (required)\n` +
|
|
126
|
+
` --out <name> file name under assets/ (default imagen-N.png)\n` +
|
|
127
|
+
` --size <s> auto|1024x1024|1536x1024|1024x1536 (default auto)\n` +
|
|
128
|
+
` --quality <q> low|medium|high|auto\n` +
|
|
129
|
+
` --brand-style condition on the CD's reference samples\n` +
|
|
115
130
|
` slidemind download-bundle [deck] Materialize the active corporate design's bundle locally\n` +
|
|
116
131
|
` --to <presId> target a specific window\n` +
|
|
117
132
|
` --force re-download even if the cached bundle is current\n` +
|
|
@@ -306,6 +321,103 @@ function renderProgressPrinter() {
|
|
|
306
321
|
};
|
|
307
322
|
}
|
|
308
323
|
|
|
324
|
+
// Generate one regular image via the paired add-in's image model and write it
|
|
325
|
+
// into the deck's assets/. Pane-free: routes by the deck's bound design
|
|
326
|
+
// (.bundle-ref.json), so it works with PowerPoint backgrounded or closed.
|
|
327
|
+
async function cmdImagen(rest) {
|
|
328
|
+
let prompt = null;
|
|
329
|
+
let deck = null;
|
|
330
|
+
let outName = null;
|
|
331
|
+
let size = null;
|
|
332
|
+
let quality = null;
|
|
333
|
+
let brandStyle = false;
|
|
334
|
+
for (let i = 0; i < rest.length; i++) {
|
|
335
|
+
const a = rest[i];
|
|
336
|
+
if (a === "--deck") {
|
|
337
|
+
deck = rest[++i];
|
|
338
|
+
if (!deck) fail("--deck expects a deck");
|
|
339
|
+
} else if (a === "--out" || a === "-o") {
|
|
340
|
+
outName = rest[++i];
|
|
341
|
+
if (!outName) fail("--out expects a file name");
|
|
342
|
+
} else if (a === "--size") {
|
|
343
|
+
size = rest[++i];
|
|
344
|
+
if (!["auto", "1024x1024", "1536x1024", "1024x1536"].includes(size ?? "")) {
|
|
345
|
+
fail("--size expects auto|1024x1024|1536x1024|1024x1536");
|
|
346
|
+
}
|
|
347
|
+
} else if (a === "--quality") {
|
|
348
|
+
quality = rest[++i];
|
|
349
|
+
if (!["low", "medium", "high", "auto"].includes(quality ?? "")) {
|
|
350
|
+
fail("--quality expects low|medium|high|auto");
|
|
351
|
+
}
|
|
352
|
+
} else if (a === "--brand-style") {
|
|
353
|
+
brandStyle = true;
|
|
354
|
+
} else if (a.startsWith("-")) {
|
|
355
|
+
fail(`unknown flag: ${a}`);
|
|
356
|
+
} else if (prompt === null) {
|
|
357
|
+
prompt = a;
|
|
358
|
+
} else {
|
|
359
|
+
fail(`unexpected argument: ${a} (quote the prompt as one argument)`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
if (!prompt || !prompt.trim() || !deck) {
|
|
363
|
+
fail(
|
|
364
|
+
'usage: slidemind imagen "<prompt>" --deck <deck> [--out <name.png>] ' +
|
|
365
|
+
"[--size auto|1024x1024|1536x1024|1024x1536] [--quality low|medium|high|auto] [--brand-style]",
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
const deckDir = resolveDeckArg(deck);
|
|
369
|
+
const bundleMod = await import(
|
|
370
|
+
pathToFileURL(path.join(SRC_DIR, "bridge", "bundle.mjs")).href
|
|
371
|
+
);
|
|
372
|
+
const ref = bundleMod.readDeckBundleRef(deckDir);
|
|
373
|
+
if (!ref?.presFP) {
|
|
374
|
+
fail(
|
|
375
|
+
`deck ${path.basename(deckDir)} is not bound to a design yet — run ` +
|
|
376
|
+
`'slidemind ready ${deck}' first (imagen routes by the deck's bound design).`,
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
const client = await import(
|
|
380
|
+
pathToFileURL(path.join(SRC_DIR, "bridge", "client.mjs")).href
|
|
381
|
+
);
|
|
382
|
+
process.stdout.write(" generating image (this can take a minute)…\n");
|
|
383
|
+
let result;
|
|
384
|
+
try {
|
|
385
|
+
result = await client.generateImageForBrand(ref.presFP, {
|
|
386
|
+
prompt,
|
|
387
|
+
size,
|
|
388
|
+
quality,
|
|
389
|
+
brandStyle,
|
|
390
|
+
});
|
|
391
|
+
} catch (err) {
|
|
392
|
+
if (err?.code === "imagen_route_unavailable") {
|
|
393
|
+
fail(
|
|
394
|
+
`${err.message}\n The paired add-in does not offer image generation yet — ` +
|
|
395
|
+
`it ships with the next add-in update; until then add pictures to assets/ by hand ` +
|
|
396
|
+
`or via 'slidemind download-result'.`,
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
throw err;
|
|
400
|
+
}
|
|
401
|
+
const m = /^data:image\/(png|jpe?g);base64,(.+)$/.exec(result?.image ?? "");
|
|
402
|
+
if (!m) fail("backend returned no image data");
|
|
403
|
+
const ext = m[1] === "png" ? "png" : "jpeg";
|
|
404
|
+
const assetsDir = path.join(deckDir, "assets");
|
|
405
|
+
fs.mkdirSync(assetsDir, { recursive: true });
|
|
406
|
+
let file = outName;
|
|
407
|
+
if (!file) {
|
|
408
|
+
let n = 1;
|
|
409
|
+
while (fs.existsSync(path.join(assetsDir, `imagen-${n}.${ext}`))) n++;
|
|
410
|
+
file = `imagen-${n}.${ext}`;
|
|
411
|
+
}
|
|
412
|
+
const outPath = path.join(assetsDir, file);
|
|
413
|
+
fs.writeFileSync(outPath, Buffer.from(m[2], "base64"));
|
|
414
|
+
const styled = result.styleReferences > 0 ? ` (styled on ${result.styleReferences} brand sample(s))` : "";
|
|
415
|
+
process.stdout.write(
|
|
416
|
+
`Wrote ${path.relative(process.cwd(), outPath)}${styled}\n` +
|
|
417
|
+
` Reference it from slide props as "assets/${file}" (e.g. summaryImage, illustration).\n`,
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
|
|
309
421
|
async function cmdBuildPptx(rest) {
|
|
310
422
|
const { deck, passthrough } = splitDeckArg(rest, "build-pptx");
|
|
311
423
|
let to = null;
|
|
@@ -328,6 +440,57 @@ async function cmdBuildPptx(rest) {
|
|
|
328
440
|
if (!fs.existsSync(path.join(deckDir, "slides"))) {
|
|
329
441
|
fail(`expected 'slides/' folder in ${deckDir}`);
|
|
330
442
|
}
|
|
443
|
+
|
|
444
|
+
// Template packs (mode: fill-template) build LOCALLY: the deck's slides are
|
|
445
|
+
// declarative fills over a template .pptx that ships in the pack, so no
|
|
446
|
+
// add-in render is involved and no PowerPoint host is needed. Compose decks
|
|
447
|
+
// take the bridge path below UNTOUCHED — the mode probe never throws, so a
|
|
448
|
+
// compose deck with an unmaterialized bundle still self-heals in the render.
|
|
449
|
+
const resolveMod = await import(
|
|
450
|
+
pathToFileURL(path.join(SRC_DIR, "builders", "resolve-skill.mjs")).href
|
|
451
|
+
);
|
|
452
|
+
if (resolveMod.resolveDeckMode(deckDir).mode === "fill-template") {
|
|
453
|
+
const skill = resolveMod.resolveSkillForDeck(deckDir);
|
|
454
|
+
if (to) {
|
|
455
|
+
fail(
|
|
456
|
+
"--to targets a PowerPoint window, but fill-template decks build " +
|
|
457
|
+
"locally from the pack's template — drop --to (use 'slidemind push' " +
|
|
458
|
+
"to get slides into a window).",
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
// In-deck freeform slides (create-slides JSX next to the master-backed
|
|
462
|
+
// ones) render to sibling NN.pptx artifacts first — the only step that
|
|
463
|
+
// talks to the add-in; unchanged freeform slides keep the build offline.
|
|
464
|
+
const freeform = await import(
|
|
465
|
+
pathToFileURL(path.join(SRC_DIR, "pptx", "fill", "freeform.mjs")).href
|
|
466
|
+
);
|
|
467
|
+
const ff = await freeform.ensureFreeformRenders({
|
|
468
|
+
deckDir,
|
|
469
|
+
locale: skill.locale ?? "en",
|
|
470
|
+
srcDir: SRC_DIR,
|
|
471
|
+
onLog: (line) => process.stdout.write(` ${line}\n`),
|
|
472
|
+
});
|
|
473
|
+
for (const w of ff.warnings) process.stderr.write(` warning: ${w}\n`);
|
|
474
|
+
|
|
475
|
+
const engine = await import(
|
|
476
|
+
pathToFileURL(path.join(SRC_DIR, "pptx", "fill", "engine.mjs")).href
|
|
477
|
+
);
|
|
478
|
+
const result = await engine.buildFillTemplateDeck({
|
|
479
|
+
deckDir,
|
|
480
|
+
skill,
|
|
481
|
+
outPath,
|
|
482
|
+
onLog: (line) => process.stdout.write(` ${line}\n`),
|
|
483
|
+
});
|
|
484
|
+
process.stdout.write(
|
|
485
|
+
`Wrote ${result.outPath} (${result.slideCount} slides) from template ` +
|
|
486
|
+
`'${skill.templateKey}' — host-free\n`,
|
|
487
|
+
);
|
|
488
|
+
for (const w of result.warnings ?? []) {
|
|
489
|
+
process.stderr.write(` warning: ${w}\n`);
|
|
490
|
+
}
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
|
|
331
494
|
// The .pptx is rendered server-side by the add-in (one exporter, one truth).
|
|
332
495
|
// A bound deck renders pane-free over the brand routes — no PowerPoint
|
|
333
496
|
// window needed; unbound decks (or --to) resolve through the pane registry.
|
|
@@ -433,10 +596,96 @@ async function cmdViewDraft(rest) {
|
|
|
433
596
|
}
|
|
434
597
|
}
|
|
435
598
|
|
|
599
|
+
// Turn the add-in's deliberately terse search failures into something actionable.
|
|
600
|
+
// It withholds detail on purpose: the raw message used to carry the built OData
|
|
601
|
+
// filter and physical index names, and "Engine not found" doubles as a permission
|
|
602
|
+
// denial so engine existence stays undisclosed. So the CLI can't diagnose — it can
|
|
603
|
+
// only name the next move.
|
|
604
|
+
function searchFailureHint(error, engineId) {
|
|
605
|
+
const raw = error ?? "unknown error";
|
|
606
|
+
if (/engine not found/i.test(raw)) {
|
|
607
|
+
return (
|
|
608
|
+
`${raw} — '${engineId}' is unknown or not visible to you. ` +
|
|
609
|
+
`Re-run 'slidemind ls engines' (ids can be retired) and pick from the live list, ` +
|
|
610
|
+
`or search 'scope:all' to skip engines entirely.`
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
if (/^search failed$/i.test(raw)) {
|
|
614
|
+
return (
|
|
615
|
+
`${raw} — the add-in withholds the reason (it can leak index internals); ` +
|
|
616
|
+
`the detail is in the add-in's logs. Retry once, then try 'scope:all'.`
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
return raw;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Zero results is now a routine, ambiguous outcome: the add-in answers "success,
|
|
623
|
+
// nothing" for a corpus that isn't connected, one that is still embedding, and a
|
|
624
|
+
// folder filter that matches nothing — all indistinguishable on the wire. Say that
|
|
625
|
+
// instead of letting an empty list read as "this content does not exist".
|
|
626
|
+
function writeEmptySearchDiagnostic(engineId, bundleMod) {
|
|
627
|
+
const isScope = bundleMod.isSearchScopeId(engineId);
|
|
628
|
+
const lines = [
|
|
629
|
+
` No results — this does NOT mean the content doesn't exist. Any of these looks identical here:`,
|
|
630
|
+
` · nothing is connected yet, so there is no corpus to search`,
|
|
631
|
+
` · the corpus is still indexing (content appears progressively)`,
|
|
632
|
+
];
|
|
633
|
+
if (!isScope) {
|
|
634
|
+
lines.push(
|
|
635
|
+
` · this engine is a saved FOLDER FILTER and the content sits outside it`,
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
lines.push(` Next:`);
|
|
639
|
+
if (!isScope) {
|
|
640
|
+
lines.push(
|
|
641
|
+
` · widen to the unfiltered corpus: slidemind search "<query>" --engine ${bundleMod.SCOPE_ALL} --result-type slide`,
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
lines.push(
|
|
645
|
+
` · re-word the query (content search matches phrasing, so a synonym can decide a hit)`,
|
|
646
|
+
` · slidemind ls engines — see every engine and scope you can reach`,
|
|
647
|
+
);
|
|
648
|
+
if (isScope) {
|
|
649
|
+
lines.push(
|
|
650
|
+
` · if the unfiltered corpus is also empty, report it as "not found" and name what you searched — don't author a replacement as if reuse had been tried`,
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
process.stdout.write(`${lines.join("\n")}\n`);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Write the search's numbered collage to a file and tell the agent to look at it.
|
|
657
|
+
//
|
|
658
|
+
// The add-in composes ONE bounded 800x600 image showing every candidate with its
|
|
659
|
+
// selection number burned in — the same picture the in-app agent sees. That is the
|
|
660
|
+
// intended way to choose a visual result: one look at one file, rather than N
|
|
661
|
+
// per-result previews (which is also why the relay ack carries no per-result bytes
|
|
662
|
+
// — a corpus image is multi-megabyte and N of them can't cross the relay).
|
|
663
|
+
//
|
|
664
|
+
// Best-effort by design: a missing/…unparseable grid must never fail a search that
|
|
665
|
+
// otherwise returned usable results. Content-intent searches have no grid at all —
|
|
666
|
+
// their signal is the printed text.
|
|
667
|
+
function writeSearchGrid(gridDataUri, searchId, resolveExt) {
|
|
668
|
+
if (typeof gridDataUri !== "string" || !gridDataUri.startsWith("data:")) {
|
|
669
|
+
return null;
|
|
670
|
+
}
|
|
671
|
+
try {
|
|
672
|
+
const m = /^data:([^;]+);base64,(.+)$/.exec(gridDataUri);
|
|
673
|
+
if (!m) return null;
|
|
674
|
+
const bytes = Buffer.from(m[2], "base64");
|
|
675
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "slidemind-grid-"));
|
|
676
|
+
const target = path.join(dir, `grid-${searchId}.${resolveExt(m[1], bytes)}`);
|
|
677
|
+
fs.writeFileSync(target, bytes);
|
|
678
|
+
return target;
|
|
679
|
+
} catch {
|
|
680
|
+
return null;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
436
684
|
async function cmdSearch(rest) {
|
|
437
685
|
let asJson = false;
|
|
438
686
|
let to = null;
|
|
439
687
|
let engineId = null;
|
|
688
|
+
let resultType = null;
|
|
440
689
|
let intent = null;
|
|
441
690
|
let top = null;
|
|
442
691
|
const queryParts = [];
|
|
@@ -450,6 +699,12 @@ async function cmdSearch(rest) {
|
|
|
450
699
|
} else if (a === "--engine" || a === "-e" || a === "--engineId") {
|
|
451
700
|
engineId = rest[++i];
|
|
452
701
|
if (!engineId) fail("--engine expects an engineId");
|
|
702
|
+
} else if (a === "--result-type" || a === "--resultType") {
|
|
703
|
+
resultType = rest[++i];
|
|
704
|
+
if (!resultType) fail("--result-type expects slide|image");
|
|
705
|
+
if (!["slide", "image"].includes(resultType)) {
|
|
706
|
+
fail(`--result-type expects slide|image, got '${resultType}'`);
|
|
707
|
+
}
|
|
453
708
|
} else if (a === "--intent") {
|
|
454
709
|
intent = rest[++i];
|
|
455
710
|
if (!intent) fail("--intent expects auto|content|visual");
|
|
@@ -472,7 +727,7 @@ async function cmdSearch(rest) {
|
|
|
472
727
|
const query = queryParts.join(" ").trim();
|
|
473
728
|
if (!query) {
|
|
474
729
|
fail(
|
|
475
|
-
`usage: slidemind search "<query>" [--engine <id>] [--intent auto|content|visual] [--top <N>] [--to <presId>] [--json]`,
|
|
730
|
+
`usage: slidemind search "<query>" [--engine <id|scope:all|scope:sharepoint>] [--result-type slide|image] [--intent auto|content|visual] [--top <N>] [--to <presId>] [--json]`,
|
|
476
731
|
);
|
|
477
732
|
}
|
|
478
733
|
|
|
@@ -484,6 +739,9 @@ async function cmdSearch(rest) {
|
|
|
484
739
|
const bundle = await import(
|
|
485
740
|
pathToFileURL(path.join(SRC_DIR, "bridge", "bundle.mjs")).href
|
|
486
741
|
);
|
|
742
|
+
const { resolveAssetExt } = await import(
|
|
743
|
+
pathToFileURL(path.join(SRC_DIR, "bridge", "engine-download.mjs")).href
|
|
744
|
+
);
|
|
487
745
|
const registry = await client.requirePaneHost();
|
|
488
746
|
// The engine is per-user, not window-bound — so search just attaches to any
|
|
489
747
|
// connected window (preferring a live one). No "which window?" prompt; pass
|
|
@@ -493,42 +751,64 @@ async function cmdSearch(rest) {
|
|
|
493
751
|
const presId = entry.presId;
|
|
494
752
|
const backendURL = entry.backendURL ?? null;
|
|
495
753
|
|
|
496
|
-
//
|
|
497
|
-
// only applies to image/visual intent. content/visual on a slide engine is
|
|
498
|
-
// the caller's choice via --engine.
|
|
754
|
+
// An explicit content/visual intent means "search slides", not imagery.
|
|
499
755
|
const slideIntent = intent === "content" || intent === "visual";
|
|
500
756
|
// Thread the engine subtype on the wire whenever the CLI can determine it:
|
|
501
757
|
// - the pickImageEngine default branch is, by construction, an image engine;
|
|
502
|
-
// - an explicit slide intent (--intent content|visual) targets
|
|
503
|
-
// engine.
|
|
758
|
+
// - an explicit slide intent (--intent content|visual) targets slides.
|
|
504
759
|
// This is belt-and-suspenders for an add-in regression where a missing
|
|
505
760
|
// subtype no longer defaulted to "image"; when the subtype is unknown we omit
|
|
506
761
|
// it and let the add-in resolve it from the snapshot.
|
|
507
762
|
let subtype = slideIntent ? "slide" : null;
|
|
508
763
|
if (!engineId) {
|
|
509
764
|
if (slideIntent) {
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
//
|
|
518
|
-
//
|
|
519
|
-
//
|
|
765
|
+
// Slide search with no engine named: search the corpus UNFILTERED rather
|
|
766
|
+
// than refusing. An engine is a saved folder filter over the add-in's
|
|
767
|
+
// shared corpus, so a workspace can hold a full corpus and zero slide
|
|
768
|
+
// engines — demanding --engine here dead-ended exactly that case.
|
|
769
|
+
engineId = bundle.SCOPE_ALL;
|
|
770
|
+
resultType = resultType ?? "slide";
|
|
771
|
+
} else {
|
|
772
|
+
// Imagery: resolve a real image engine (a scope is not a brand image
|
|
773
|
+
// source). Ask the LIVE catalog first — the same one the add-in's own
|
|
774
|
+
// design agent reads — and fall back to the materialized bundle only when
|
|
775
|
+
// the host can't answer. Bundle-first would be stale: engine ids can be
|
|
776
|
+
// retired server-side, and a dead id surfaces as "Engine not found".
|
|
520
777
|
const live = await client
|
|
521
778
|
.listEngines({ subtype: "image", backendURL })
|
|
522
779
|
.catch(() => ({ engines: [] }));
|
|
523
|
-
pick = bundle.pickImageEngine(live.engines);
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
780
|
+
let pick = bundle.pickImageEngine(live.engines);
|
|
781
|
+
if (!pick) {
|
|
782
|
+
// Best-effort: a bundle that can't be materialized (e.g. an incomplete
|
|
783
|
+
// one) must not replace the actionable "wire an image engine" message
|
|
784
|
+
// below with a bundle error — the live catalog is the real answer here.
|
|
785
|
+
const mat = await bundle
|
|
786
|
+
.materializeBundle(presId, { backendURL })
|
|
787
|
+
.catch(() => null);
|
|
788
|
+
if (mat) pick = bundle.pickImageEngine(bundle.readEnginesFromDir(mat.dir));
|
|
789
|
+
}
|
|
790
|
+
if (!pick) {
|
|
791
|
+
fail(
|
|
792
|
+
"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.",
|
|
793
|
+
);
|
|
794
|
+
}
|
|
795
|
+
engineId = pick.id;
|
|
796
|
+
subtype = "image";
|
|
529
797
|
}
|
|
530
|
-
|
|
531
|
-
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// A scope has no subtype of its own, so the index must be named explicitly —
|
|
801
|
+
// the add-in rejects a scope search without it. Default to slides: reusing an
|
|
802
|
+
// existing slide is what an unfiltered corpus search is overwhelmingly for.
|
|
803
|
+
if (bundle.isSearchScopeId(engineId)) {
|
|
804
|
+
resultType = resultType ?? "slide";
|
|
805
|
+
subtype = resultType;
|
|
806
|
+
} else if (resultType) {
|
|
807
|
+
// Harmless but meaningless on an engine, whose own subtype is authoritative.
|
|
808
|
+
process.stderr.write(
|
|
809
|
+
`note: --result-type is ignored for engine '${engineId}' (it applies to scope:* targets); the engine's own type decides.\n`,
|
|
810
|
+
);
|
|
811
|
+
resultType = null;
|
|
532
812
|
}
|
|
533
813
|
|
|
534
814
|
// Window-agnostic: tries the picked pane, then fails over to any other live
|
|
@@ -540,6 +820,7 @@ async function cmdSearch(rest) {
|
|
|
540
820
|
engineId,
|
|
541
821
|
query,
|
|
542
822
|
subtype,
|
|
823
|
+
resultType,
|
|
543
824
|
intent: intent ?? null,
|
|
544
825
|
top: top ?? null,
|
|
545
826
|
});
|
|
@@ -550,7 +831,7 @@ async function cmdSearch(rest) {
|
|
|
550
831
|
return;
|
|
551
832
|
}
|
|
552
833
|
if (!res?.ok) {
|
|
553
|
-
process.stderr.write(`search failed: ${res?.error
|
|
834
|
+
process.stderr.write(`search failed: ${searchFailureHint(res?.error, engineId)}\n`);
|
|
554
835
|
return;
|
|
555
836
|
}
|
|
556
837
|
// Prefer the new `results` array (image + slide engines); fall back to the
|
|
@@ -559,11 +840,18 @@ async function cmdSearch(rest) {
|
|
|
559
840
|
? res.results
|
|
560
841
|
: (res.thumbnails ?? []).map((t, i) => ({ selection: i + 1, title: t.title }));
|
|
561
842
|
const count = res.resultCount ?? results.length;
|
|
843
|
+
const targetLabel = bundle.isSearchScopeId(engineId)
|
|
844
|
+
? `scope ${engineId}`
|
|
845
|
+
: `engine ${engineId}`;
|
|
562
846
|
process.stdout.write(
|
|
563
|
-
`searchId ${res.searchId} — ${count} result(s) from
|
|
847
|
+
`searchId ${res.searchId} — ${count} result(s) from ${targetLabel}` +
|
|
564
848
|
(res.intent ? ` (${res.intent})` : "") +
|
|
565
849
|
`\n`,
|
|
566
850
|
);
|
|
851
|
+
if (count === 0) {
|
|
852
|
+
writeEmptySearchDiagnostic(engineId, bundle);
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
567
855
|
const isContent = res.intent === "content";
|
|
568
856
|
results.forEach((r, i) => {
|
|
569
857
|
const sel = r.selection ?? i + 1;
|
|
@@ -577,10 +865,27 @@ async function cmdSearch(rest) {
|
|
|
577
865
|
}
|
|
578
866
|
}
|
|
579
867
|
} else {
|
|
580
|
-
// Visual / image results: the numbered list.
|
|
581
|
-
|
|
868
|
+
// Visual / image results: the numbered list. An image result carries its
|
|
869
|
+
// intrinsic size from the engine index — show it, so the box can be sized
|
|
870
|
+
// to the real aspect ratio at authoring time rather than after the fetch.
|
|
871
|
+
const dims =
|
|
872
|
+
r.kind === "image" && Number.isFinite(r.width) && Number.isFinite(r.height)
|
|
873
|
+
? ` ${r.width}×${r.height}`
|
|
874
|
+
: "";
|
|
875
|
+
process.stdout.write(` ${sel}. ${r.title ?? "(untitled)"}${dims}\n`);
|
|
582
876
|
}
|
|
583
877
|
});
|
|
878
|
+
// Visual results: point at the ONE collage rather than making the caller pull
|
|
879
|
+
// each preview separately. The numbers burned into it match the list above.
|
|
880
|
+
if (!isContent) {
|
|
881
|
+
const gridPath = writeSearchGrid(res.gridDataUri, res.searchId, resolveAssetExt);
|
|
882
|
+
if (gridPath) {
|
|
883
|
+
process.stdout.write(
|
|
884
|
+
`Contact sheet of all ${count} result(s) — LOOK AT THIS FILE to choose, ` +
|
|
885
|
+
`don't fetch previews one by one: ${gridPath}\n`,
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
584
889
|
// Tailor the next-step hint to the result kind: slide verbs (insert-slide /
|
|
585
890
|
// add-slide) for slide results, the image-download hint otherwise. The result
|
|
586
891
|
// subtype is authoritative; fall back to a per-result `kind` for older
|
|
@@ -609,7 +914,7 @@ async function cmdSearch(rest) {
|
|
|
609
914
|
async function cmdLs(rest) {
|
|
610
915
|
const [sub, ...subRest] = rest;
|
|
611
916
|
if (sub !== "engines") {
|
|
612
|
-
fail("usage: slidemind ls engines [--type collection|search] [--subtype image|slide|template|content] [--json]");
|
|
917
|
+
fail("usage: slidemind ls engines [--type collection|search|scope] [--subtype image|slide|template|content] [--json]");
|
|
613
918
|
}
|
|
614
919
|
let type = null;
|
|
615
920
|
let subtype = null;
|
|
@@ -620,9 +925,10 @@ async function cmdLs(rest) {
|
|
|
620
925
|
const a = subRest[i];
|
|
621
926
|
if (a === "--type") {
|
|
622
927
|
type = subRest[++i];
|
|
623
|
-
if (!type) fail("--type expects collection|search");
|
|
624
|
-
|
|
625
|
-
|
|
928
|
+
if (!type) fail("--type expects collection|search|scope");
|
|
929
|
+
// "scope" narrows to the non-engine targets (scope:all / scope:sharepoint).
|
|
930
|
+
if (!["collection", "search", "scope"].includes(type)) {
|
|
931
|
+
fail(`--type expects collection|search|scope, got '${type}'`);
|
|
626
932
|
}
|
|
627
933
|
} else if (a === "--subtype") {
|
|
628
934
|
subtype = subRest[++i];
|
|
@@ -659,7 +965,18 @@ async function cmdLs(rest) {
|
|
|
659
965
|
return;
|
|
660
966
|
}
|
|
661
967
|
if (engines.length === 0) {
|
|
662
|
-
|
|
968
|
+
// Deliberately not "nothing to search": an engine is a saved folder filter
|
|
969
|
+
// over the add-in's shared corpus, so an empty catalog says only that nobody
|
|
970
|
+
// saved a filter. An older add-in that predates scopes lands here too.
|
|
971
|
+
const { SCOPE_ALL } = await import(
|
|
972
|
+
pathToFileURL(path.join(SRC_DIR, "bridge", "bundle.mjs")).href
|
|
973
|
+
);
|
|
974
|
+
process.stderr.write(
|
|
975
|
+
"No engines or scopes matched. An engine is a saved folder filter over the corpus, " +
|
|
976
|
+
"so an empty list does NOT mean there is nothing to search — try " +
|
|
977
|
+
`'slidemind search "<query>" --engine ${SCOPE_ALL} --result-type slide' ` +
|
|
978
|
+
"to search it unfiltered.\n",
|
|
979
|
+
);
|
|
663
980
|
return;
|
|
664
981
|
}
|
|
665
982
|
// Show the env tag only when more than one environment contributed engines.
|
|
@@ -668,7 +985,15 @@ async function cmdLs(rest) {
|
|
|
668
985
|
const kind = [e.type, e.subtype].filter(Boolean).join("/");
|
|
669
986
|
const env = multiEnv && e.env ? `[${e.env}] ` : "";
|
|
670
987
|
const desc = e.description ? ` — ${e.description}` : "";
|
|
671
|
-
|
|
988
|
+
// A scope takes --result-type instead of having a subtype of its own; spell
|
|
989
|
+
// that out so the agent doesn't have to infer it from the missing subtype.
|
|
990
|
+
const hint =
|
|
991
|
+
e.type === "scope"
|
|
992
|
+
? ` (needs --result-type ${(e.capabilities?.resultTypes ?? ["slide", "image"]).join("|")})`
|
|
993
|
+
: "";
|
|
994
|
+
process.stdout.write(
|
|
995
|
+
`${env}${e.id} · ${e.label ?? e.id} · ${kind}${hint}${desc}\n`,
|
|
996
|
+
);
|
|
672
997
|
}
|
|
673
998
|
}
|
|
674
999
|
|
|
@@ -867,18 +1192,61 @@ async function cmdAddSlide(rest) {
|
|
|
867
1192
|
const reused = await import(
|
|
868
1193
|
pathToFileURL(path.join(SRC_DIR, "lib", "reused-slides.mjs")).href
|
|
869
1194
|
);
|
|
1195
|
+
const { createTransferProgress } = await import(
|
|
1196
|
+
pathToFileURL(path.join(SRC_DIR, "bridge", "engine-download.mjs")).href
|
|
1197
|
+
);
|
|
1198
|
+
// Stream each pick to a provisional file: the play-order filename can only be
|
|
1199
|
+
// computed once every pick is in hand, and buffering a multi-megabyte slide in
|
|
1200
|
+
// memory just to name it would defeat the streaming path.
|
|
870
1201
|
const extracted = [];
|
|
871
|
-
|
|
872
|
-
const
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
1202
|
+
const cleanupTemps = () => {
|
|
1203
|
+
for (const s of extracted) {
|
|
1204
|
+
if (s.tempPath) fs.rmSync(s.tempPath, { force: true });
|
|
1205
|
+
}
|
|
1206
|
+
};
|
|
1207
|
+
try {
|
|
1208
|
+
for (const pick of picks) {
|
|
1209
|
+
const tempPath = path.join(slidesDir, `.reused-${searchId}-${pick}.part`);
|
|
1210
|
+
if (typeof client.extractSlideToFile === "function") {
|
|
1211
|
+
try {
|
|
1212
|
+
const res = await client.extractSlideToFile({
|
|
1213
|
+
presId,
|
|
1214
|
+
searchId,
|
|
1215
|
+
selection: pick,
|
|
1216
|
+
destPath: tempPath,
|
|
1217
|
+
backendURL,
|
|
1218
|
+
onProgress: createTransferProgress(`extracting slide ${pick}`),
|
|
1219
|
+
});
|
|
1220
|
+
if (!res?.bytesWritten) throw new Error("stream produced no bytes");
|
|
1221
|
+
extracted.push({ selection: pick, slideId: res.slideId ?? null, tempPath });
|
|
1222
|
+
continue;
|
|
1223
|
+
} catch (err) {
|
|
1224
|
+
fs.rmSync(tempPath, { force: true });
|
|
1225
|
+
// Route absent (bodyless 404 / 501) → this add-in predates the streaming
|
|
1226
|
+
// route; fall through to the capped base64 ack path below.
|
|
1227
|
+
const routeMissing =
|
|
1228
|
+
(err?.status === 404 && !err?.code) || err?.status === 501;
|
|
1229
|
+
if (!routeMissing) {
|
|
1230
|
+
cleanupTemps();
|
|
1231
|
+
fail(`add-slide failed for selection ${pick}: ${err.message}`);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
const res = await client.extractSlide({
|
|
1236
|
+
presId,
|
|
1237
|
+
searchId,
|
|
1238
|
+
selection: pick,
|
|
1239
|
+
backendURL,
|
|
1240
|
+
});
|
|
1241
|
+
if (!res?.ok || !res.base64) {
|
|
1242
|
+
cleanupTemps();
|
|
1243
|
+
fail(`add-slide failed for selection ${pick}: ${res?.error ?? "no slide bytes returned"}`);
|
|
1244
|
+
}
|
|
1245
|
+
extracted.push({ selection: pick, slideId: res.slideId ?? null, base64: res.base64 });
|
|
880
1246
|
}
|
|
881
|
-
|
|
1247
|
+
} catch (err) {
|
|
1248
|
+
cleanupTemps();
|
|
1249
|
+
throw err;
|
|
882
1250
|
}
|
|
883
1251
|
const existing = fs
|
|
884
1252
|
.readdirSync(slidesDir)
|
|
@@ -887,7 +1255,11 @@ async function cmdAddSlide(rest) {
|
|
|
887
1255
|
const written = [];
|
|
888
1256
|
extracted.forEach((slide, i) => {
|
|
889
1257
|
const target = path.join(slidesDir, targets[i].filename);
|
|
890
|
-
|
|
1258
|
+
if (slide.tempPath) {
|
|
1259
|
+
fs.renameSync(slide.tempPath, target);
|
|
1260
|
+
} else {
|
|
1261
|
+
fs.writeFileSync(target, Buffer.from(slide.base64, "base64"));
|
|
1262
|
+
}
|
|
891
1263
|
written.push(path.relative(process.cwd(), target));
|
|
892
1264
|
});
|
|
893
1265
|
process.stdout.write(
|
|
@@ -957,7 +1329,7 @@ async function cmdViewSlide(rest) {
|
|
|
957
1329
|
const client = await import(
|
|
958
1330
|
pathToFileURL(path.join(SRC_DIR, "bridge", "client.mjs")).href
|
|
959
1331
|
);
|
|
960
|
-
const {
|
|
1332
|
+
const { resolveAssetExt } = await import(
|
|
961
1333
|
pathToFileURL(path.join(SRC_DIR, "bridge", "engine-download.mjs")).href
|
|
962
1334
|
);
|
|
963
1335
|
const registry = await client.requirePaneHost();
|
|
@@ -979,8 +1351,15 @@ async function cmdViewSlide(rest) {
|
|
|
979
1351
|
? path.resolve(process.cwd(), outDir)
|
|
980
1352
|
: fs.mkdtempSync(path.join(os.tmpdir(), "slidemind-view-"));
|
|
981
1353
|
fs.mkdirSync(dir, { recursive: true });
|
|
982
|
-
|
|
983
|
-
|
|
1354
|
+
// Extension from the BYTES, not the declared MIME — engines inherit a
|
|
1355
|
+
// Content-Type from upstream and it is demonstrably wrong for some corpus
|
|
1356
|
+
// images (PNG served as image/jpeg).
|
|
1357
|
+
const bytes = Buffer.from(m[2], "base64");
|
|
1358
|
+
const target = path.join(
|
|
1359
|
+
dir,
|
|
1360
|
+
`slide-${searchId}-${pick}.${resolveAssetExt(m[1], bytes)}`,
|
|
1361
|
+
);
|
|
1362
|
+
fs.writeFileSync(target, bytes);
|
|
984
1363
|
process.stdout.write(
|
|
985
1364
|
`Wrote preview of result ${pick} (searchId ${searchId}) to ${target}\n`,
|
|
986
1365
|
);
|
|
@@ -1058,7 +1437,11 @@ async function cmdDownloadResult(rest) {
|
|
|
1058
1437
|
? r.value
|
|
1059
1438
|
: { selection: picks[i], error: r.reason?.message ?? String(r.reason) },
|
|
1060
1439
|
);
|
|
1440
|
+
const { formatSize } = await import(
|
|
1441
|
+
pathToFileURL(path.join(SRC_DIR, "bridge", "image-dimensions.mjs")).href
|
|
1442
|
+
);
|
|
1061
1443
|
let ok = 0;
|
|
1444
|
+
let anySized = false;
|
|
1062
1445
|
const lines = [];
|
|
1063
1446
|
for (const r of results) {
|
|
1064
1447
|
if (r.error) {
|
|
@@ -1067,12 +1450,21 @@ async function cmdDownloadResult(rest) {
|
|
|
1067
1450
|
}
|
|
1068
1451
|
ok++;
|
|
1069
1452
|
const note = r.recolor?.note ? ` — ${r.recolor.note}` : "";
|
|
1070
|
-
|
|
1453
|
+
// The intrinsic size is the agent's only handle on the asset's aspect
|
|
1454
|
+
// ratio — without it the <img> box is a guess and the render stretches.
|
|
1455
|
+
const dims = formatSize(r.size);
|
|
1456
|
+
anySized ||= Boolean(dims);
|
|
1457
|
+
const size = dims ? ` ${dims}` : " (size unknown)";
|
|
1458
|
+
lines.push(` ${r.reference}${size} (${r.recolor?.mode ?? "?"})${note}`);
|
|
1071
1459
|
}
|
|
1072
1460
|
process.stdout.write(
|
|
1073
1461
|
`Downloaded ${ok}/${results.length} result(s) into ${path.relative(process.cwd(), assetsDir)}:\n` +
|
|
1074
1462
|
lines.join("\n") +
|
|
1075
|
-
`\nReference each from slide JSX as <img src="./assets/...">.\n
|
|
1463
|
+
`\nReference each from slide JSX as <img src="./assets/...">.\n` +
|
|
1464
|
+
(anySized
|
|
1465
|
+
? `Sizes above are the assets' intrinsic width×height — give each <img> a box ` +
|
|
1466
|
+
`with that aspect ratio, or the render stretches it to whatever box you wrote.\n`
|
|
1467
|
+
: ""),
|
|
1076
1468
|
);
|
|
1077
1469
|
if (ok === 0) process.exit(1);
|
|
1078
1470
|
}
|
|
@@ -1392,9 +1784,13 @@ function publicConnectionStatus(status, client) {
|
|
|
1392
1784
|
// unmet precondition, or null when the connection is fully ready. `code` is the
|
|
1393
1785
|
// machine-copeable reason an agent switches on; `actor` says who can clear it —
|
|
1394
1786
|
// only `cli_update_required` is the AGENT's to fix (it can run `slidemind
|
|
1395
|
-
// update`), every other rung needs a human at the add-in. The
|
|
1396
|
-
//
|
|
1397
|
-
//
|
|
1787
|
+
// update`), every other rung needs a human at the add-in. The one exception is
|
|
1788
|
+
// a plugin-installed CLI: `/plugin` is the only updater that replaces the
|
|
1789
|
+
// binary on the session's PATH, and the agent cannot drive it, so that rung
|
|
1790
|
+
// hands back to the user (see src/install-channel.mjs). The `message` strings
|
|
1791
|
+
// are otherwise BYTE-IDENTICAL to the old connectionNextAction ladder so
|
|
1792
|
+
// doctor's printed output never churns (connectionNextAction is now a thin
|
|
1793
|
+
// projection over this).
|
|
1398
1794
|
// `slidemind ready` layers `ambiguous_design` on top and treats `pane_suspended`
|
|
1399
1795
|
// as non-blocking (a render-ready-but-suspended window is still a design source).
|
|
1400
1796
|
function connectionBlocker(pub) {
|
|
@@ -1420,11 +1816,11 @@ function connectionBlocker(pub) {
|
|
|
1420
1816
|
if (outdated) {
|
|
1421
1817
|
return {
|
|
1422
1818
|
code: "cli_update_required",
|
|
1423
|
-
actor: "agent",
|
|
1819
|
+
actor: isClaudePluginInstall() ? "user" : "agent",
|
|
1424
1820
|
message:
|
|
1425
1821
|
`This CLI is older than the minimum the '${outdated.env}' add-in supports` +
|
|
1426
1822
|
`${outdated.minCliVersion ? ` (${outdated.minCliVersion})` : ""}. ` +
|
|
1427
|
-
|
|
1823
|
+
updateCliInstruction(),
|
|
1428
1824
|
};
|
|
1429
1825
|
}
|
|
1430
1826
|
if ((pub.presentations ?? []).length === 0) {
|
|
@@ -1561,6 +1957,10 @@ async function cmdReady(rest) {
|
|
|
1561
1957
|
const client = await import(
|
|
1562
1958
|
pathToFileURL(path.join(SRC_DIR, "bridge", "client.mjs")).href
|
|
1563
1959
|
);
|
|
1960
|
+
// Same opt-out as doctor: `ready` OWNS the cli_update_required blocker (it is
|
|
1961
|
+
// terminal, `actor: "agent"`, with the update command as its nextAction), so
|
|
1962
|
+
// the raw header throw would only replace a structured answer with a worse one.
|
|
1963
|
+
client.setDiagnosticMode(true);
|
|
1564
1964
|
|
|
1565
1965
|
// --- Network (one parallel round): readiness ladder + the merged registry. ---
|
|
1566
1966
|
const [statusRaw, detailed] = await Promise.all([
|
|
@@ -1878,6 +2278,9 @@ async function cmdDoctor(rest) {
|
|
|
1878
2278
|
const client = await import(
|
|
1879
2279
|
pathToFileURL(path.join(SRC_DIR, "bridge", "client.mjs")).href
|
|
1880
2280
|
);
|
|
2281
|
+
// doctor diagnoses skew — it must never be killed BY skew. The version floor
|
|
2282
|
+
// still reaches the reader, as the `cli_update_required` rung of the ladder.
|
|
2283
|
+
client.setDiagnosticMode(true);
|
|
1881
2284
|
if (sessions.length === 0) {
|
|
1882
2285
|
const pub = {
|
|
1883
2286
|
paired: false,
|
|
@@ -2534,6 +2937,20 @@ async function cmdUpdate(rest) {
|
|
|
2534
2937
|
const ui = await loadInstallerUi();
|
|
2535
2938
|
ui.banner(pkg.version, "update");
|
|
2536
2939
|
|
|
2940
|
+
// A plugin-installed CLI cannot update itself this way: npm would drop a
|
|
2941
|
+
// second copy into the global prefix while the plugin session keeps running
|
|
2942
|
+
// its own. Say so instead of letting the user believe they are now current —
|
|
2943
|
+
// the global install is still worth doing (it is what a plain terminal
|
|
2944
|
+
// outside Claude Code picks up), so this warns and continues.
|
|
2945
|
+
if (isClaudePluginInstall()) {
|
|
2946
|
+
ui.step({
|
|
2947
|
+
status: "warn",
|
|
2948
|
+
label: "plugin",
|
|
2949
|
+
value:
|
|
2950
|
+
"this slidemind came from the Claude Code plugin — update it with `/plugin` → slidemind → update; the global install below does NOT replace it inside plugin sessions",
|
|
2951
|
+
});
|
|
2952
|
+
}
|
|
2953
|
+
|
|
2537
2954
|
// Upgrade the global install straight from public npm. `@latest` (or a pinned
|
|
2538
2955
|
// version) lets npm resolve + verify the tarball — no SAS, no checksum dance.
|
|
2539
2956
|
const spec = ref
|
|
@@ -2610,6 +3027,9 @@ async function main(rawArgv) {
|
|
|
2610
3027
|
case "review":
|
|
2611
3028
|
await cmdReview(rest);
|
|
2612
3029
|
return 0;
|
|
3030
|
+
case "imagen":
|
|
3031
|
+
await cmdImagen(rest);
|
|
3032
|
+
break;
|
|
2613
3033
|
case "build-pptx":
|
|
2614
3034
|
await cmdBuildPptx(rest);
|
|
2615
3035
|
return 0;
|