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.
Files changed (49) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/AGENTS.md +86 -21
  4. package/README.md +14 -17
  5. package/bin/slidemind.mjs +65 -4969
  6. package/package.json +1 -1
  7. package/skills/create-slides/SKILL.md +113 -74
  8. package/skills/search/SKILL.md +25 -7
  9. package/src/bridge/build-and-push.mjs +108 -15
  10. package/src/bridge/bundle.mjs +69 -0
  11. package/src/bridge/capture.mjs +19 -0
  12. package/src/bridge/client/brands.mjs +622 -0
  13. package/src/bridge/client/engine-routing.mjs +237 -0
  14. package/src/bridge/client/engine-search.mjs +310 -0
  15. package/src/bridge/client/engine-slides.mjs +435 -0
  16. package/src/bridge/client/pane-ops.mjs +335 -0
  17. package/src/bridge/client/presentations.mjs +587 -0
  18. package/src/bridge/client/render.mjs +317 -0
  19. package/src/bridge/client/transport.mjs +695 -0
  20. package/src/bridge/client.mjs +80 -3452
  21. package/src/bridge/config.mjs +1 -1
  22. package/src/bridge/engine-download.mjs +15 -2
  23. package/src/builders/build.mjs +11 -804
  24. package/src/builders/draft-check.mjs +376 -0
  25. package/src/builders/lint.mjs +38 -2
  26. package/src/builders/preview.html +799 -0
  27. package/src/builders/review.mjs +23 -4
  28. package/src/builders/visual-families.mjs +6 -8
  29. package/src/cli/common.mjs +179 -0
  30. package/src/cli/concepts.mjs +310 -0
  31. package/src/cli/connection.mjs +147 -0
  32. package/src/cli/deck.mjs +608 -0
  33. package/src/cli/designs.mjs +113 -0
  34. package/src/cli/diagnose.mjs +141 -0
  35. package/src/cli/download-bundle.mjs +144 -0
  36. package/src/cli/engine-helpers.mjs +429 -0
  37. package/src/cli/engine-results.mjs +320 -0
  38. package/src/cli/help.mjs +201 -0
  39. package/src/cli/install.mjs +257 -0
  40. package/src/cli/read.mjs +120 -0
  41. package/src/cli/ready.mjs +805 -0
  42. package/src/cli/search.mjs +608 -0
  43. package/src/cli/session.mjs +177 -0
  44. package/src/cli/setup.mjs +282 -0
  45. package/src/cli/slide-reuse.mjs +334 -0
  46. package/src/cli/status.mjs +118 -0
  47. package/src/cli/workspace.mjs +302 -0
  48. package/src/installers/codex.mjs +1 -1
  49. package/src/update-check.mjs +231 -0
@@ -0,0 +1,257 @@
1
+ import { spawn } from "node:child_process";
2
+ import path from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+ import { isClaudePluginInstall } from "../install-channel.mjs";
5
+ import { npmBin } from "../update-check.mjs";
6
+ import { readPackageJson, fail, PACKAGE_ROOT, SRC_DIR, INSTALLERS_DIR } from "./common.mjs";
7
+
8
+ export const HARNESS_TARGETS = ["claude", "codex", "opencode"];
9
+
10
+ export function parseTargetFlag(rest, { allowAuto = false } = {}) {
11
+ const idx = rest.findIndex((a) => a === "--for" || a === "-f");
12
+ if (idx < 0 || !rest[idx + 1]) {
13
+ fail(
14
+ `missing --for <claude|codex|opencode|all${allowAuto ? "|auto" : ""}>`,
15
+ );
16
+ }
17
+ const target = rest[idx + 1];
18
+ const allowed = allowAuto
19
+ ? [...HARNESS_TARGETS, "all", "auto"]
20
+ : [...HARNESS_TARGETS, "all"];
21
+ if (!allowed.includes(target)) {
22
+ fail(`unknown install target: ${target}`);
23
+ }
24
+ return target;
25
+ }
26
+
27
+ // Which coding agents are actually on this machine, and which of them already
28
+ // get their skills from a plugin. This is what lets `slidemind setup` finish the
29
+ // job on its own: it deploys where a harness exists and stays out of the way
30
+ // where a plugin already supplies the skills, so a separate `init` is no longer
31
+ // part of onboarding.
32
+ export async function detectHarnesses() {
33
+ const found = [];
34
+ for (const target of HARNESS_TARGETS) {
35
+ const mod = await import(
36
+ pathToFileURL(path.join(INSTALLERS_DIR, `${target}.mjs`)).href
37
+ );
38
+ const info =
39
+ typeof mod.detect === "function"
40
+ ? mod.detect()
41
+ : { present: false, pluginInstalled: false, root: null };
42
+ found.push({ target, ...info });
43
+ }
44
+ return found;
45
+ }
46
+
47
+ /** Expand a `--for` value into the concrete harness list to act on. */
48
+
49
+ export async function resolveInstallTargets(target) {
50
+ if (target === "all") return [...HARNESS_TARGETS];
51
+ if (target !== "auto") return [target];
52
+ return (await detectHarnesses())
53
+ .filter((h) => h.present && !h.pluginInstalled)
54
+ .map((h) => h.target);
55
+ }
56
+
57
+ export async function runInstaller(target, action, rest) {
58
+ const targets = await resolveInstallTargets(target);
59
+ for (const t of targets) {
60
+ const mod = await import(
61
+ pathToFileURL(path.join(INSTALLERS_DIR, `${t}.mjs`)).href
62
+ );
63
+ const fn = mod[action];
64
+ if (typeof fn !== "function") {
65
+ fail(`installer for '${t}' does not implement '${action}'`);
66
+ }
67
+ await fn({
68
+ packageRoot: PACKAGE_ROOT,
69
+ flags: rest,
70
+ });
71
+ }
72
+ return targets;
73
+ }
74
+
75
+ export async function loadInstallerUi() {
76
+ const mod = await import(
77
+ pathToFileURL(path.join(INSTALLERS_DIR, "lib.mjs")).href
78
+ );
79
+ return mod.ui;
80
+ }
81
+
82
+ export async function cmdInstall(rest) {
83
+ const target = parseTargetFlag(rest, { allowAuto: true });
84
+ const ui = await loadInstallerUi();
85
+ ui.banner(readPackageJson().version, "init");
86
+ const deployed = await runInstaller(target, "install", rest);
87
+ if (deployed.length === 0) {
88
+ ui.step({
89
+ status: "warn",
90
+ label: "harnesses",
91
+ value: "none detected",
92
+ note: "install a coding agent, then rerun with --for <claude|codex|opencode>",
93
+ });
94
+ }
95
+
96
+ // Force every design bundle to re-download from the add-in on the next
97
+ // build/push/download by wiping the materialized cache. init is the
98
+ // deliberate refresh point (and `slidemind update` redeploys through it), so
99
+ // a stale binary or an orphaned per-presentation dir never outlives a re-init.
100
+ const bundle = await import(
101
+ pathToFileURL(path.join(SRC_DIR, "bridge", "bundle.mjs")).href
102
+ );
103
+ const cleared = bundle.clearBundleCache();
104
+ await ui.section("cache", "design bundles");
105
+ ui.step({
106
+ label: "clear",
107
+ value: cleared ? `${cleared} bundle${cleared === 1 ? "" : "s"}` : "empty",
108
+ note: "re-downloads on next build",
109
+ });
110
+
111
+ ui.done(
112
+ deployed.length === 0
113
+ ? "no coding agent found — the CLI is installed, skills are not"
114
+ : deployed.length === HARNESS_TARGETS.length
115
+ ? "ready — ask your agent to plan, create, or edit a deck"
116
+ : `ready — ${deployed.join(", ")} can now plan, create, or edit decks`,
117
+ );
118
+ }
119
+
120
+ export async function cmdUninstall(rest) {
121
+ const target = parseTargetFlag(rest);
122
+ const ui = await loadInstallerUi();
123
+ ui.banner(readPackageJson().version, "uninstall");
124
+ await runInstaller(target, "uninstall", rest);
125
+
126
+ // Drop the materialized bundle cache too — uninstall is a teardown, so leaving
127
+ // a per-presentation dir (with brand assets) behind would be stale residue.
128
+ const bundle = await import(
129
+ pathToFileURL(path.join(SRC_DIR, "bridge", "bundle.mjs")).href
130
+ );
131
+ const cleared = bundle.clearBundleCache();
132
+ await ui.section("cache", "design bundles");
133
+ ui.step({
134
+ status: "remove",
135
+ label: "clear",
136
+ value: cleared ? `${cleared} bundle${cleared === 1 ? "" : "s"}` : "empty",
137
+ });
138
+
139
+ ui.done(target === "all" ? "removed from all harnesses" : `removed from ${target}`);
140
+ }
141
+
142
+ // On Windows we must spawn through the shell (Node refuses to exec .cmd
143
+ // shims directly since CVE-2024-27980), and `shell: true` joins file + args
144
+ // into one cmd.exe command string WITHOUT quoting — so an unquoted
145
+ // `C:\Program Files\nodejs\npm.cmd` splits at the space and cmd tries to run
146
+ // `C:\Program`. Quote anything containing whitespace before handing it over.
147
+ export function cmdQuote(s) {
148
+ return /\s/.test(s) ? `"${s}"` : s;
149
+ }
150
+
151
+ export function spawnInherit(file, args) {
152
+ const useShell = process.platform === "win32";
153
+ return new Promise((resolve, reject) => {
154
+ const child = spawn(
155
+ useShell ? cmdQuote(file) : file,
156
+ useShell ? args.map(cmdQuote) : args,
157
+ {
158
+ stdio: "inherit",
159
+ env: process.env,
160
+ shell: useShell,
161
+ },
162
+ );
163
+ child.on("error", reject);
164
+ child.on("exit", (code) => {
165
+ if (code === 0) resolve();
166
+ else reject(new Error(`${file} ${args.join(" ")} exited with code ${code}`));
167
+ });
168
+ });
169
+ }
170
+
171
+ // Like spawnInherit, but resolves with the child's exit code instead of
172
+ // rejecting — for a step whose failure is a message to the user rather than a
173
+ // failure of the command that ran it (see `setup` → `ready`).
174
+ export function spawnStatus(file, args) {
175
+ return spawnInherit(file, args).then(
176
+ () => 0,
177
+ () => 1,
178
+ );
179
+ }
180
+
181
+ //
182
+ // slidemind is published to public npm as `slidemind`. First install is
183
+ // `npm install -g slidemind` (then `slidemind init` to deploy the skill;
184
+ // `install` stays as a back-compat alias); `slidemind update` is a thin
185
+ // wrapper over `npm install -g slidemind@latest`.
186
+
187
+ export async function cmdUpdate(rest) {
188
+ let target = "all";
189
+ let ref = null;
190
+ for (let i = 0; i < rest.length; i++) {
191
+ const a = rest[i];
192
+ if (a === "--for" || a === "-f") {
193
+ target = rest[++i];
194
+ if (!target) fail("--for expects a target");
195
+ if (!["claude", "codex", "opencode", "all", "none"].includes(target)) {
196
+ fail(`unknown install target: ${target}`);
197
+ }
198
+ } else if (a === "--ref") {
199
+ ref = rest[++i];
200
+ if (!ref) fail("--ref expects a version tag (e.g. v0.1.0)");
201
+ } else {
202
+ fail(`unknown flag: ${a}`);
203
+ }
204
+ }
205
+
206
+ const pkg = readPackageJson();
207
+ const ui = await loadInstallerUi();
208
+ ui.banner(pkg.version, "update");
209
+
210
+ // A plugin-installed CLI cannot update itself this way: npm would drop a
211
+ // second copy into the global prefix while the plugin session keeps running
212
+ // its own. Say so instead of letting the user believe they are now current —
213
+ // the global install is still worth doing (it is what a plain terminal
214
+ // outside Claude Code picks up), so this warns and continues.
215
+ if (isClaudePluginInstall()) {
216
+ ui.step({
217
+ status: "warn",
218
+ label: "plugin",
219
+ value:
220
+ "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",
221
+ });
222
+ }
223
+
224
+ // Upgrade the global install straight from public npm. `@latest` (or a pinned
225
+ // version) lets npm resolve + verify the tarball — no SAS, no checksum dance.
226
+ const spec = ref
227
+ ? `slidemind@${ref.replace(/^v/, "")}`
228
+ : "slidemind@latest";
229
+ await ui.section("release", ref ? `pinned ${spec}` : "latest");
230
+ ui.step({ label: "install", value: spec, note: "global (npm)" });
231
+ try {
232
+ await spawnInherit(npmBin(), ["install", "-g", "--loglevel=error", spec]);
233
+ } catch (err) {
234
+ console.error(
235
+ "\nslidemind update could not write to npm's global package directory.\n" +
236
+ "This usually happens on macOS/Linux when Node was installed with a system-owned npm prefix.\n\n" +
237
+ "Avoid `sudo npm`. Use Homebrew/nvm, or move npm's global prefix into your home directory:\n\n" +
238
+ " mkdir -p ~/.npm-global\n" +
239
+ " npm config set prefix ~/.npm-global\n" +
240
+ " echo 'export PATH=\"$HOME/.npm-global/bin:$PATH\"' >> ~/.zshrc\n" +
241
+ " source ~/.zshrc\n" +
242
+ " npm install -g slidemind\n\n" +
243
+ "Then run `slidemind init --for all`.\n",
244
+ );
245
+ throw err;
246
+ }
247
+
248
+ if (target !== "none") {
249
+ await spawnInherit("slidemind", ["init", "--for", target]);
250
+ }
251
+
252
+ ui.done(
253
+ target === "none"
254
+ ? "updated — skill files unchanged (run 'slidemind init --for all' when ready)"
255
+ : `updated and redeployed into ${target === "all" ? "all harnesses" : target}`,
256
+ );
257
+ }
@@ -0,0 +1,120 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+ import { fail, LOCAL_PPTX_RE, SRC_DIR } from "./common.mjs";
5
+
6
+ // Print a read result — the `{ ok, mode, slideCount, slides[] }` shape, whether
7
+ // it came from the add-in (open presentation) or a local .pptx. Shared by both
8
+ // `cmdRead` branches so their output is byte-for-byte identical.
9
+ export function printReadResult(res, asJson) {
10
+ if (asJson) {
11
+ process.stdout.write(JSON.stringify(res, null, 2) + "\n");
12
+ return;
13
+ }
14
+ const slides = Array.isArray(res.slides) ? res.slides : [];
15
+ if (slides.length === 0) {
16
+ process.stderr.write("(presentation has no slides)\n");
17
+ return;
18
+ }
19
+ if (res.mode === "summary") {
20
+ for (const s of slides) {
21
+ const label = s.title || "(untitled)";
22
+ const sub = s.subtitle ? ` — ${s.subtitle}` : "";
23
+ const layout = s.layout ? ` [${s.layout}]` : "";
24
+ process.stdout.write(
25
+ `${String(s.index).padStart(3)}.${layout} ${label}${sub}\n`,
26
+ );
27
+ }
28
+ } else {
29
+ slides.forEach((s, i) => {
30
+ if (i > 0) process.stdout.write("\n");
31
+ const layout = s.layout ? ` (${s.layout})` : "";
32
+ process.stdout.write(`── Slide ${s.index}${layout} ──\n`);
33
+ process.stdout.write(`${s.text && s.text.length ? s.text : "(no text)"}\n`);
34
+ });
35
+ }
36
+ }
37
+
38
+ // Pull the current content of an open presentation. Content is window-bound, so
39
+ // pick exactly one window (positional presId or --to; pickPresentationEntry errors
40
+ // listing the windows if several are open and none was named). `--mode summary`
41
+ // prints a per-slide outline; default `raw` prints each slide's plain text.
42
+ //
43
+ // A positional arg with a local PowerPoint extension (LOCAL_PPTX_RE: .pptx and
44
+ // its .pptm/.potx/.potm/.ppsx/.ppsm siblings) AND resolving to an existing
45
+ // regular file is read host-free from disk instead (no bridge). The gate is
46
+ // strict: a nonexistent path or a real presId falls through to the host path
47
+ // unchanged, and it is skipped when --to is given (--to targets an open window).
48
+ export async function cmdRead(rest) {
49
+ let presId = null;
50
+ let mode = "raw";
51
+ let asJson = false;
52
+ let toGiven = false;
53
+ let fileArg = null; // the positional (a candidate local .pptx path)
54
+ for (let i = 0; i < rest.length; i++) {
55
+ const a = rest[i];
56
+ if (a === "--mode") {
57
+ const next = rest[++i];
58
+ if (next !== "summary" && next !== "raw") {
59
+ fail(`--mode expects 'summary' or 'raw', got '${next ?? ""}'`);
60
+ }
61
+ mode = next;
62
+ } else if (a === "--to") {
63
+ presId = rest[++i];
64
+ if (!presId) fail("--to expects a presId");
65
+ toGiven = true;
66
+ } else if (a === "--json") {
67
+ asJson = true;
68
+ } else if (a.startsWith("-")) {
69
+ fail(`unknown flag: ${a}`);
70
+ } else if (!presId) {
71
+ presId = a; // positional presId (or local .pptx path)
72
+ fileArg = a;
73
+ } else {
74
+ fail(`unexpected argument: ${a}`);
75
+ }
76
+ }
77
+
78
+ // Local-file gate: applies ONLY to the positional, and never when --to is set.
79
+ // The positional must end in .pptx and resolve to an existing regular file;
80
+ // anything else (missing path, real presId) falls through to the host path.
81
+ if (fileArg && !toGiven && LOCAL_PPTX_RE.test(fileArg)) {
82
+ const p = path.isAbsolute(fileArg)
83
+ ? fileArg
84
+ : path.resolve(process.cwd(), fileArg);
85
+ let st = null;
86
+ try {
87
+ st = fs.statSync(p);
88
+ } catch {
89
+ st = null;
90
+ }
91
+ if (st && st.isFile()) {
92
+ const { readLocalPptx } = await import(
93
+ pathToFileURL(path.join(SRC_DIR, "pptx", "read-local.mjs")).href
94
+ );
95
+ let res;
96
+ try {
97
+ res = readLocalPptx(fs.readFileSync(p), { mode });
98
+ } catch (err) {
99
+ fail(`read failed: ${err.message}`);
100
+ }
101
+ printReadResult(res, asJson);
102
+ return;
103
+ }
104
+ }
105
+
106
+ const client = await import(
107
+ pathToFileURL(path.join(SRC_DIR, "bridge", "client.mjs")).href
108
+ );
109
+ const registry = await client.requirePaneHost();
110
+ const entry = client.pickPresentationEntry(registry, presId);
111
+ const res = await client.getPresentationContent({
112
+ presId: entry.presId,
113
+ mode,
114
+ backendURL: entry.backendURL ?? null,
115
+ });
116
+ if (!res?.ok) {
117
+ fail(`read failed: ${res?.error ?? "unknown error"}`);
118
+ }
119
+ printReadResult(res, asJson);
120
+ }