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,177 @@
1
+ import { envPhrase } from "../bridge/config.mjs";
2
+ import { fail, loadBridgeModule } from "./common.mjs";
3
+
4
+ // The one line every "you are not connected" path speaks, so the remedy reads
5
+ // the same wherever a user meets it.
6
+ export const NOT_CONNECTED_LINE =
7
+ "Not connected. In PowerPoint open slidemind → Settings → General → Connect CLI, press the link button, and run the command it shows.";
8
+
9
+ // Match a `--backend`-style argument against a paired session the way logout and
10
+ // use do: exact URL, stored label, derived env label, or a substring of the URL.
11
+ export function matchesBackend(cfg, session, want) {
12
+ return (
13
+ session.backendURL === want ||
14
+ session.label === want ||
15
+ cfg.deriveEnvLabel(session.backendURL) === want ||
16
+ session.backendURL.includes(want)
17
+ );
18
+ }
19
+
20
+ // `slidemind login [--backend <url|env>]` — sign this CLI in to Microsoft 365 so
21
+ // the pane-free engine routes can fetch SharePoint-backed content with the
22
+ // user's OWN identity (docs/slidemind-cli-connect/15-graph-delegated-cli.md).
23
+ // Separate from the CLI session on purpose: that one is minted by the add-in for
24
+ // whoever pressed Connect CLI, this one is an Entra token for whoever completes
25
+ // the browser sign-in, and only the second may read a user's SharePoint content.
26
+ // Everything else — render, bundle, whoami — is unaffected either way.
27
+ export async function cmdLogin(rest) {
28
+ const stray = rest.find((a) => a.startsWith("-"));
29
+ if (stray) {
30
+ fail(`unexpected argument: ${stray}\nusage: slidemind login [--backend <url|env>]`);
31
+ }
32
+ const cfg = await loadBridgeModule("config");
33
+ const sessions = cfg.readSessions();
34
+ if (sessions.length === 0) fail(NOT_CONNECTED_LINE);
35
+ // A browser flow targets ONE environment — there is nothing to fan out. The
36
+ // scope flag picks it; otherwise the default environment does, and the closing
37
+ // line names the others so nothing looks silently skipped.
38
+ const want = (process.env.SLIDEMIND_BACKEND || "").trim();
39
+ let target;
40
+ if (want) {
41
+ target = sessions.find((s) => matchesBackend(cfg, s, want));
42
+ if (!target) {
43
+ fail(`not paired with '${want}'. Run 'slidemind whoami' to list environments.`);
44
+ }
45
+ } else {
46
+ const def = cfg.getDefaultBackend();
47
+ target = sessions.find((s) => s.backendURL === def) ?? sessions[0];
48
+ }
49
+ const graph = await loadBridgeModule("graph-auth");
50
+ // This command is routinely typed by an AI agent on the human's machine, and
51
+ // the browser is where the agent's reach ends — say the handoff out loud.
52
+ process.stderr.write(
53
+ `Opening your browser to sign in to Microsoft 365${envPhrase(target.label, "for")}…\n` +
54
+ "A HUMAN must complete this step (agents: ask your user to finish the " +
55
+ "sign-in in the browser window that just opened).\n",
56
+ );
57
+ const res = await graph.loginGraph({
58
+ backendURL: target.backendURL,
59
+ // Printed before the opener runs: on a machine where no opener works this
60
+ // link is the only way through, and it is useless after the wait times out.
61
+ onUrl: (url) =>
62
+ process.stderr.write(`If no browser opens, paste this link into one:\n ${url}\n`),
63
+ });
64
+ process.stdout.write(
65
+ `Signed in to Microsoft 365 as ${res.username ?? "your account"}${envPhrase(target.label, "for")}.\n` +
66
+ "SharePoint-backed slides and images now download with no PowerPoint window open.\n",
67
+ );
68
+ const others = sessions.filter((s) => s.backendURL !== target.backendURL);
69
+ if (others.length > 0) {
70
+ process.stdout.write(
71
+ `Still signed out: ${others.map((s) => s.label).join(", ")} ` +
72
+ "(each needs its own 'slidemind login --backend <env>').\n",
73
+ );
74
+ }
75
+ }
76
+
77
+ export async function cmdWhoami() {
78
+ const cfg = await loadBridgeModule("config");
79
+ const sessions = cfg.readSessions();
80
+ if (sessions.length === 0) {
81
+ process.stderr.write(`${NOT_CONNECTED_LINE}\n`);
82
+ return;
83
+ }
84
+ const { httpJson } = await loadBridgeModule("http-json");
85
+ const graph = await loadBridgeModule("graph-auth");
86
+ const def = cfg.getDefaultBackend();
87
+ const lines = await Promise.all(
88
+ sessions.map(async (s) => {
89
+ const marker = s.backendURL === def ? "*" : " ";
90
+ let detail;
91
+ try {
92
+ const res = await httpJson(
93
+ "GET",
94
+ `${s.backendURL}/api/cli/whoami`,
95
+ { token: s.sessionToken },
96
+ );
97
+ detail =
98
+ res.status === 200 && res.json
99
+ ? `user ${res.json.oid} · tenant ${res.json.tid ?? "(n/a)"}`
100
+ : `session invalid (${res.status}) — authorize again`;
101
+ } catch {
102
+ detail = "unreachable — check your connection or authorize again";
103
+ }
104
+ // Storage-only — no probe. The Graph sign-in is a SECOND identity, so it
105
+ // is reported next to the session rather than folded into it.
106
+ const graphLine = graph.hasGraphSession(s.backendURL)
107
+ ? "Microsoft 365: signed in"
108
+ : "Microsoft 365: not signed in — needed only for window-free SharePoint downloads (`slidemind login`)";
109
+ return `${marker} ${s.label}\n ${detail}\n ${graphLine}`;
110
+ }),
111
+ );
112
+ process.stdout.write(
113
+ `Connected environments (${sessions.length}):\n${lines.join("\n")}\n`,
114
+ );
115
+ }
116
+
117
+ export async function cmdLogout(rest) {
118
+ const cfg = await loadBridgeModule("config");
119
+ const all = rest.includes("--all");
120
+ const target = rest.find((a) => !a.startsWith("--")) ?? null;
121
+
122
+ // Which sessions to revoke + remove: all, one (by url/env/label), or the
123
+ // default (the bare `slidemind logout`).
124
+ const sessions = cfg.readSessions();
125
+ if (sessions.length === 0) {
126
+ process.stdout.write("Already logged out.\n");
127
+ return;
128
+ }
129
+ let toRemove;
130
+ if (all) {
131
+ toRemove = sessions;
132
+ } else if (target) {
133
+ toRemove = sessions.filter(
134
+ (s) =>
135
+ s.backendURL === target ||
136
+ s.label === target ||
137
+ cfg.deriveEnvLabel(s.backendURL) === target ||
138
+ s.backendURL.includes(target),
139
+ );
140
+ if (toRemove.length === 0) {
141
+ fail(`not paired with '${target}'. Run 'slidemind whoami' to list environments.`);
142
+ }
143
+ } else {
144
+ const def = cfg.getDefaultBackend();
145
+ toRemove = sessions.filter((s) => s.backendURL === def);
146
+ }
147
+
148
+ const { httpJson } = await loadBridgeModule("http-json");
149
+ // Unpairing takes the WHOLE record, the Microsoft 365 refresh token included —
150
+ // it was stored for this environment's session and must not outlive it. Said
151
+ // out loud because signing back in is a second command, not part of `setup`.
152
+ const droppedGraph = toRemove.filter((s) => s.msGraphRefreshToken);
153
+ for (const s of toRemove) {
154
+ try {
155
+ await httpJson("POST", `${s.backendURL}/api/cli/logout`, {
156
+ token: s.sessionToken,
157
+ });
158
+ } catch {
159
+ /* best-effort server-side revoke; we still drop the local session */
160
+ }
161
+ // Through the token setter, not just the record removal: the refresh token
162
+ // may live in the macOS keychain (config.mjs), which removing the session
163
+ // record alone would orphan.
164
+ cfg.setGraphRefreshToken(s.backendURL, null);
165
+ cfg.removeSession(s.backendURL);
166
+ }
167
+ const remaining = cfg.readSessions().length;
168
+ process.stdout.write(
169
+ `Logged out of ${toRemove.map((s) => s.label).join(", ")}.` +
170
+ (remaining > 0 ? ` ${remaining} environment(s) still connected.\n` : "\n") +
171
+ (droppedGraph.length > 0
172
+ ? `The Microsoft 365 sign-in for ${droppedGraph
173
+ .map((s) => s.label)
174
+ .join(", ")} was dropped too — run 'slidemind login' after reconnecting.\n`
175
+ : ""),
176
+ );
177
+ }
@@ -0,0 +1,282 @@
1
+ import { isClaudePluginInstall } from "../install-channel.mjs";
2
+ import { npmBin } from "../update-check.mjs";
3
+ import { readPackageJson, fail, loadBridgeModule, BIN_PATH } from "./common.mjs";
4
+ import { HARNESS_TARGETS, detectHarnesses, runInstaller, loadInstallerUi, spawnInherit, spawnStatus } from "./install.mjs";
5
+
6
+ //
7
+ // `npx --yes slidemind@latest setup <claim>` is the whole onboarding: the user
8
+ // clicks "Connect CLI" in the add-in, pastes ONE command, and ends up with a
9
+ // connected, globally-installed CLI. npx only runs the package from its cache —
10
+ // it never installs it — so this command performs the persistent
11
+ // `npm install -g` itself.
12
+ //
13
+ // Order matters: redeem FIRST. The claim is short-lived and one-time, while a
14
+ // global npm install can take a while or hit an EACCES prefix problem. Spending
15
+ // the claim up front means a failed install leaves a connected CLI the user can
16
+ // fix at their leisure, instead of forcing them back to PowerPoint for a fresh
17
+ // command.
18
+ export async function cmdSetup(rest) {
19
+ let claimArg = null;
20
+ // `auto` is the whole point: setup finishes onboarding by itself, so it
21
+ // deploys into the coding agents this machine actually has instead of asking
22
+ // the user to run `init` afterwards.
23
+ let target = "auto";
24
+ let doInstall = true;
25
+ let doReady = true;
26
+ let doLogin = true;
27
+ let asJson = false;
28
+ for (let i = 0; i < rest.length; i++) {
29
+ const a = rest[i];
30
+ if (a === "--for" || a === "-f") {
31
+ target = rest[++i];
32
+ if (!target) fail("--for expects a target");
33
+ if (![...HARNESS_TARGETS, "all", "auto", "none"].includes(target)) {
34
+ fail(`unknown install target: ${target}`);
35
+ }
36
+ } else if (a === "--no-install") {
37
+ doInstall = false;
38
+ } else if (a === "--no-ready") {
39
+ doReady = false;
40
+ } else if (a === "--no-login") {
41
+ doLogin = false;
42
+ } else if (a === "--json") {
43
+ asJson = true;
44
+ } else if (a.startsWith("-")) {
45
+ fail(`unknown flag: ${a}`);
46
+ } else if (claimArg === null) {
47
+ claimArg = a;
48
+ } else {
49
+ fail("setup takes exactly one setup claim");
50
+ }
51
+ }
52
+
53
+ if (!claimArg) {
54
+ fail(
55
+ "setup needs the connection command from the add-in.\n" +
56
+ " Open PowerPoint → slidemind → Settings → General → Connect CLI, copy the command, and paste it here.\n" +
57
+ " Already have the CLI? The panel's `CLI already installed?` toggle gives the short `slidemind setup …` form.",
58
+ );
59
+ }
60
+
61
+ const pkg = readPackageJson();
62
+ const claims = await loadBridgeModule("setup-claim");
63
+ // Decode before any UI so a mistyped/partial paste fails in one clean line.
64
+ const { backendURL, secret } = claims.decodeSetupClaim(claimArg);
65
+ const cfg = await loadBridgeModule("config");
66
+ const envLabel = cfg.deriveEnvLabel(backendURL);
67
+
68
+ // The installers paint their own progress rail. In --json mode stdout is
69
+ // reserved for the payload, so silence the whole UI rather than letting it
70
+ // corrupt what the caller parses.
71
+ const ui = await loadInstallerUi();
72
+ if (asJson) ui.silence();
73
+ ui.banner(pkg.version, "setup");
74
+
75
+ // --- 1. spend the claim -------------------------------------------------
76
+ // Whether to name the environment at all. Decided BEFORE the claim is spent
77
+ // so every line in this run agrees: a first connection has nothing to
78
+ // disambiguate against and stays quiet, while adding a second (or third)
79
+ // environment names each one. Computing it per-line would flip mid-run, since
80
+ // `upsertSession` below changes the count.
81
+ const showEnv = cfg.readSessions().length > 0;
82
+ await ui.section("connect", showEnv ? envLabel : "");
83
+ const { sessionToken, oid } = await claims.redeemSetupClaim({
84
+ backendURL,
85
+ secret,
86
+ cliVersion: pkg.version,
87
+ });
88
+ cfg.upsertSession({ backendURL, sessionToken, label: envLabel, oid });
89
+ ui.step({
90
+ label: "authorize",
91
+ value: showEnv ? envLabel : "connected",
92
+ note: "session saved",
93
+ });
94
+
95
+ // --- 2. make this CLI permanent ----------------------------------------
96
+ // Without this the whole thing evaporates with npx's cache: the user would be
97
+ // connected but have no `slidemind` on PATH.
98
+ let installed = false;
99
+ let installError = null;
100
+ const pluginInstall = isClaudePluginInstall();
101
+ if (doInstall && pluginInstall) {
102
+ ui.step({
103
+ status: "warn",
104
+ label: "install",
105
+ value: "skipped",
106
+ note: "this slidemind came from the Claude Code plugin — update it with /plugin",
107
+ });
108
+ } else if (doInstall) {
109
+ await ui.section("install", "npm global");
110
+ ui.step({ label: "install", value: "slidemind@latest", note: "global (npm)" });
111
+ try {
112
+ await spawnInherit(npmBin(), [
113
+ "install",
114
+ "-g",
115
+ "--loglevel=error",
116
+ "slidemind@latest",
117
+ ]);
118
+ installed = true;
119
+ } catch (err) {
120
+ installError = err?.message || String(err);
121
+ ui.step({
122
+ status: "warn",
123
+ label: "install",
124
+ value: "failed",
125
+ note: "the connection is saved — see the note below",
126
+ });
127
+ }
128
+ }
129
+
130
+ // --- 3. deploy the skills ----------------------------------------------
131
+ // In-process, from THIS package copy: the npx copy and the global install are
132
+ // the same published version, and going through PATH here would fail on a
133
+ // shell that has not rehashed yet.
134
+ //
135
+ // With the default `auto` this is what removes `slidemind init` from
136
+ // onboarding: the skills land in every coding agent present on the machine,
137
+ // and skip any whose plugin already supplies them (copying over a plugin's
138
+ // skills plants a second set that drifts on the next plugin update).
139
+ let skills = [];
140
+ let skippedPlugins = [];
141
+ if (target !== "none") {
142
+ if (target === "auto") {
143
+ const harnesses = await detectHarnesses();
144
+ skippedPlugins = harnesses
145
+ .filter((h) => h.present && h.pluginInstalled)
146
+ .map((h) => h.target);
147
+ }
148
+ await ui.section(
149
+ "skills",
150
+ target === "auto"
151
+ ? "detected harnesses"
152
+ : target === "all"
153
+ ? "all harnesses"
154
+ : target,
155
+ );
156
+ skills = await runInstaller(target, "install", []);
157
+ for (const t of skippedPlugins) {
158
+ ui.step({
159
+ status: "warn",
160
+ label: t,
161
+ value: "skipped",
162
+ note: "its slidemind plugin already supplies the skills",
163
+ });
164
+ }
165
+ if (skills.length === 0 && skippedPlugins.length === 0) {
166
+ ui.step({
167
+ status: "warn",
168
+ label: "harnesses",
169
+ value: "none detected",
170
+ note: "install a coding agent, then run `slidemind init --for auto`",
171
+ });
172
+ }
173
+ const bundleMod = await loadBridgeModule("bundle");
174
+ bundleMod.clearBundleCache();
175
+ }
176
+
177
+ // --- 4. Microsoft 365 sign-in (best-effort) -----------------------------
178
+ // The pasted-claim moment is the one time the user is reliably at the
179
+ // keyboard in consent headspace, so offer the browser sign-in NOW and no
180
+ // later command has to interrupt a task for it. Strictly best-effort and
181
+ // interactive-only: --json or no TTY (an agent ran setup — a browser tab it
182
+ // cannot see must never block onboarding), --no-login, a backend that does
183
+ // not honour the token, a decline or a timeout all skip WITHOUT failing
184
+ // setup — the closing `ready` and the failure-time remediations still name
185
+ // `slidemind login`.
186
+ if (doLogin && !asJson && process.stdout.isTTY) {
187
+ try {
188
+ const client = await loadBridgeModule("client");
189
+ if (await client.backendSupportsGraphDelegated(backendURL)) {
190
+ await ui.section("sign in", "Microsoft 365");
191
+ process.stderr.write(
192
+ "Opening your browser to sign in to Microsoft 365 — this lets SharePoint-backed\n" +
193
+ "content download with no PowerPoint window open.\n",
194
+ );
195
+ const graph = await loadBridgeModule("graph-auth");
196
+ const res = await graph.loginGraph({
197
+ backendURL,
198
+ // Shorter than `slidemind login`'s: the user is mid-setup, not
199
+ // parked on a sign-in they asked for — a walked-away browser tab
200
+ // should not stall onboarding for five minutes.
201
+ timeoutMs: 120_000,
202
+ onUrl: (url) =>
203
+ process.stderr.write(
204
+ `If no browser opens, paste this link into one:\n ${url}\n`,
205
+ ),
206
+ });
207
+ ui.step({
208
+ label: "Microsoft 365",
209
+ value: res.username ?? "signed in",
210
+ ...(showEnv ? { note: envLabel } : {}),
211
+ });
212
+ }
213
+ } catch {
214
+ ui.step({
215
+ status: "warn",
216
+ label: "Microsoft 365",
217
+ value: "skipped",
218
+ note: "run 'slidemind login' anytime for window-free SharePoint downloads",
219
+ });
220
+ }
221
+ }
222
+
223
+ if (asJson) {
224
+ const globalInstall =
225
+ doInstall && !pluginInstall
226
+ ? installed
227
+ ? "installed"
228
+ : "failed"
229
+ : "skipped";
230
+ process.stdout.write(
231
+ `${JSON.stringify(
232
+ {
233
+ ok: true,
234
+ connected: true,
235
+ env: envLabel,
236
+ globalInstall,
237
+ skills,
238
+ // Present-but-plugin-served harnesses, so an agent can tell "nothing
239
+ // deployed because nothing is installed" from "nothing needed".
240
+ skippedPlugins,
241
+ nextAction: "slidemind ready",
242
+ // Only on the unhappy path: the session IS saved, so `ok` stays true,
243
+ // but an agent needs to know the binary did not land.
244
+ ...(installError ? { installError } : {}),
245
+ },
246
+ null,
247
+ 2,
248
+ )}\n`,
249
+ );
250
+ return;
251
+ }
252
+
253
+ ui.done(
254
+ target === "none" || (skills.length === 0 && skippedPlugins.length === 0)
255
+ ? `connected${showEnv ? ` to ${envLabel}` : ""} — run 'slidemind ready' to start`
256
+ : `connected${showEnv ? ` to ${envLabel}` : ""} and ready to use`,
257
+ );
258
+
259
+ if (installError) {
260
+ process.stderr.write(
261
+ "\nslidemind is connected, but the global install could not write to npm's global package directory.\n" +
262
+ "This usually happens on macOS/Linux when Node was installed with a system-owned npm prefix.\n\n" +
263
+ "Avoid `sudo npm`. Use Homebrew/nvm, or move npm's global prefix into your home directory:\n\n" +
264
+ " mkdir -p ~/.npm-global\n" +
265
+ " npm config set prefix ~/.npm-global\n" +
266
+ " echo 'export PATH=\"$HOME/.npm-global/bin:$PATH\"' >> ~/.zshrc\n" +
267
+ " source ~/.zshrc\n" +
268
+ " npm install -g slidemind\n\n" +
269
+ `(details: ${installError})\n`,
270
+ );
271
+ return;
272
+ }
273
+
274
+ // --- 5. finish the session the user actually came for -------------------
275
+ // `ready` exits non-zero for every legitimate not-yet blocker (PowerPoint
276
+ // closed, no design activated). Setup already succeeded at that point, so its
277
+ // blocker is information, not a failure — run it for the message and the
278
+ // warmed bundle, and never inherit its exit code.
279
+ if (doReady) {
280
+ await spawnStatus(process.execPath, [BIN_PATH, "ready"]);
281
+ }
282
+ }