slidemind 0.1.5 → 0.1.6
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.md +6 -2
- package/README.md +13 -5
- package/bin/slidemind.mjs +311 -6
- package/package.json +1 -1
- package/skills/create-slides/SKILL.md +28 -4
- package/skills/search/SKILL.md +6 -3
- package/src/bridge/build-and-push.mjs +195 -19
- package/src/bridge/client.mjs +204 -5
- package/src/installers/codex.mjs +96 -28
- package/src/mcp/server.mjs +153 -5
package/src/installers/codex.mjs
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
|
|
24
24
|
import fs from "node:fs";
|
|
25
25
|
import path from "node:path";
|
|
26
|
+
import { spawnSync } from "node:child_process";
|
|
26
27
|
import {
|
|
27
28
|
expandHome,
|
|
28
29
|
installDir,
|
|
@@ -40,7 +41,10 @@ const TOML_MARK_BEGIN = "# slidemind:begin";
|
|
|
40
41
|
const TOML_MARK_END = "# slidemind:end";
|
|
41
42
|
|
|
42
43
|
function codexRoot() {
|
|
43
|
-
|
|
44
|
+
// Codex reads everything (skills, AGENTS.md, config.toml) from $CODEX_HOME
|
|
45
|
+
// when set, defaulting to ~/.codex — mirror that so our artifacts land where
|
|
46
|
+
// it actually looks.
|
|
47
|
+
return process.env.CODEX_HOME || expandHome("~/.codex");
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
function promptsDir() {
|
|
@@ -81,38 +85,102 @@ function configTomlFile() {
|
|
|
81
85
|
return path.join(codexRoot(), "config.toml");
|
|
82
86
|
}
|
|
83
87
|
|
|
84
|
-
|
|
88
|
+
// Register the slidemind MCP server. Prefer Codex's own `codex mcp add`, which
|
|
89
|
+
// is TOML-aware and idempotent: it sets `[mcp_servers.slidemind]` by key, so a
|
|
90
|
+
// re-install/upgrade — or a pre-existing entry added manually or by an earlier
|
|
91
|
+
// `codex mcp add` — is REPLACED in place rather than duplicated, while unrelated
|
|
92
|
+
// tables, top-level keys, and comments survive.
|
|
93
|
+
//
|
|
94
|
+
// The previous hand-rolled writer wrapped its block in `# slidemind:begin/end`
|
|
95
|
+
// comment markers and deduped by them; a `[mcp_servers.slidemind]` table that
|
|
96
|
+
// lacked those markers (manual edit, or a Codex-side config rewrite that dropped
|
|
97
|
+
// the comments) was invisible to it, so it appended a SECOND table — a
|
|
98
|
+
// duplicate-key TOML that broke config.toml. Delegating to Codex removes that
|
|
99
|
+
// whole failure mode.
|
|
100
|
+
//
|
|
101
|
+
// `codex mcp` landed around Codex 0.128. If the binary is missing or too old
|
|
102
|
+
// (spawn error / non-zero exit) we fall back to a minimal, non-destructive
|
|
103
|
+
// writer that never duplicates.
|
|
104
|
+
export function registerMcpServer(packageRoot) {
|
|
85
105
|
const mcpServer = path.join(packageRoot, "src", "mcp", "server.mjs");
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
106
|
+
const viaCli = spawnSync(
|
|
107
|
+
"codex",
|
|
108
|
+
["mcp", "add", NAMESPACE, "--", "node", mcpServer],
|
|
109
|
+
{ stdio: "ignore", shell: process.platform === "win32" },
|
|
110
|
+
);
|
|
111
|
+
if (!viaCli.error && viaCli.status === 0) {
|
|
112
|
+
sweepLegacyMarkers();
|
|
113
|
+
return "registered via codex mcp";
|
|
114
|
+
}
|
|
115
|
+
return fallbackWriteMcpServerBlock(mcpServer);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function unregisterMcpServer() {
|
|
119
|
+
const viaCli = spawnSync(
|
|
120
|
+
"codex",
|
|
121
|
+
["mcp", "remove", NAMESPACE],
|
|
122
|
+
{ stdio: "ignore", shell: process.platform === "win32" },
|
|
123
|
+
);
|
|
124
|
+
if (!viaCli.error && viaCli.status === 0) return "unregistered via codex mcp";
|
|
125
|
+
return fallbackClearMcpServerBlock();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function mcpServerBlock(mcpServer) {
|
|
129
|
+
return (
|
|
130
|
+
`[mcp_servers.${NAMESPACE}]\n` +
|
|
89
131
|
`command = "node"\n` +
|
|
90
|
-
`args = ["${mcpServer.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"]\n`
|
|
91
|
-
|
|
132
|
+
`args = ["${mcpServer.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"]\n`
|
|
133
|
+
);
|
|
134
|
+
}
|
|
92
135
|
|
|
136
|
+
// Fallback for when `codex mcp` is unavailable. Minimal and non-destructive:
|
|
137
|
+
// never append a second entry — if a `[mcp_servers.slidemind]` already exists
|
|
138
|
+
// (ours or the user's) we leave it untouched; otherwise we append our block.
|
|
139
|
+
export function fallbackWriteMcpServerBlock(mcpServer) {
|
|
93
140
|
const file = configTomlFile();
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
"g",
|
|
98
|
-
);
|
|
99
|
-
if (re.test(existing)) {
|
|
100
|
-
existing = existing.replace(re, "");
|
|
141
|
+
const existing = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
|
|
142
|
+
if (existing.includes(`[mcp_servers.${NAMESPACE}]`)) {
|
|
143
|
+
return "already registered (left as-is)";
|
|
101
144
|
}
|
|
102
|
-
const sep =
|
|
103
|
-
|
|
145
|
+
const sep =
|
|
146
|
+
existing.length && !existing.endsWith("\n") ? "\n\n" : existing.length ? "\n" : "";
|
|
147
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
148
|
+
fs.writeFileSync(file, existing + sep + mcpServerBlock(mcpServer));
|
|
149
|
+
return "registered (fallback)";
|
|
104
150
|
}
|
|
105
151
|
|
|
106
|
-
|
|
152
|
+
// Fallback uninstall: strip a legacy marker-wrapped block if one is present;
|
|
153
|
+
// otherwise point the user at `codex mcp remove` (we won't guess at the bounds
|
|
154
|
+
// of a marker-less table without Codex's TOML parser).
|
|
155
|
+
export function fallbackClearMcpServerBlock() {
|
|
156
|
+
const file = configTomlFile();
|
|
157
|
+
if (!fs.existsSync(file)) return "unregistered";
|
|
158
|
+
const existing = fs.readFileSync(file, "utf8");
|
|
159
|
+
const re = new RegExp(`${TOML_MARK_BEGIN}[\\s\\S]*?${TOML_MARK_END}\\n?`, "g");
|
|
160
|
+
const stripped = existing.replace(re, "");
|
|
161
|
+
if (stripped !== existing) {
|
|
162
|
+
fs.writeFileSync(file, stripped);
|
|
163
|
+
return "unregistered (fallback)";
|
|
164
|
+
}
|
|
165
|
+
return "run `codex mcp remove slidemind` to remove the entry";
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// After Codex rewrites the `[mcp_servers.slidemind]` table, an upgrader's old
|
|
169
|
+
// slidemind block can leave orphan `# slidemind:begin` / `# slidemind:end`
|
|
170
|
+
// comment lines behind. Drop just those literal lines (valid TOML either way —
|
|
171
|
+
// purely cosmetic).
|
|
172
|
+
function sweepLegacyMarkers() {
|
|
107
173
|
const file = configTomlFile();
|
|
108
174
|
if (!fs.existsSync(file)) return;
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
175
|
+
const before = fs.readFileSync(file, "utf8");
|
|
176
|
+
const after = before
|
|
177
|
+
.split("\n")
|
|
178
|
+
.filter(
|
|
179
|
+
(line) =>
|
|
180
|
+
line.trim() !== TOML_MARK_BEGIN && line.trim() !== TOML_MARK_END,
|
|
181
|
+
)
|
|
182
|
+
.join("\n");
|
|
183
|
+
if (after !== before) fs.writeFileSync(file, after);
|
|
116
184
|
}
|
|
117
185
|
|
|
118
186
|
function writeAgentsBlock() {
|
|
@@ -194,8 +262,8 @@ export async function install({ packageRoot, flags }) {
|
|
|
194
262
|
|
|
195
263
|
// Register the slidemind MCP server so agents can list/push into open
|
|
196
264
|
// PowerPoint windows via slidemind_list_open_presentations / slidemind_push_deck.
|
|
197
|
-
|
|
198
|
-
ui.step({ status: "ok", label: "mcp server", value:
|
|
265
|
+
const mcpNote = registerMcpServer(packageRoot);
|
|
266
|
+
ui.step({ status: "ok", label: "mcp server", value: mcpNote });
|
|
199
267
|
|
|
200
268
|
ui.note("restart Codex to pick up the skill + MCP server");
|
|
201
269
|
}
|
|
@@ -224,6 +292,6 @@ export async function uninstall() {
|
|
|
224
292
|
}
|
|
225
293
|
clearAgentsBlock();
|
|
226
294
|
ui.step({ status: "remove", label: "AGENTS.md", value: agentsFile() });
|
|
227
|
-
|
|
228
|
-
ui.step({ status: "info", label: "mcp server", value:
|
|
295
|
+
const mcpNote = unregisterMcpServer();
|
|
296
|
+
ui.step({ status: "info", label: "mcp server", value: mcpNote });
|
|
229
297
|
}
|
package/src/mcp/server.mjs
CHANGED
|
@@ -10,10 +10,20 @@
|
|
|
10
10
|
// Lists PowerPoint windows registered with the local bridge, including
|
|
11
11
|
// retained entries whose task-pane socket is temporarily disconnected.
|
|
12
12
|
//
|
|
13
|
+
// slidemind_read_presentation
|
|
14
|
+
// Pulls the current content of an open PowerPoint window — a per-slide
|
|
15
|
+
// outline (summary) or each slide's full plain text (raw) — so the agent can
|
|
16
|
+
// inspect the live deck before editing it.
|
|
17
|
+
//
|
|
13
18
|
// slidemind_push_deck
|
|
14
19
|
// Renders a deck server-side via the add-in and pushes its slides into one
|
|
15
20
|
// open PowerPoint window.
|
|
16
21
|
//
|
|
22
|
+
// slidemind_view_draft
|
|
23
|
+
// Renders the deck's authored slides to per-slide preview images locally and
|
|
24
|
+
// returns them WITHOUT pushing — the agent's "look at my own draft" check
|
|
25
|
+
// before push / the human review gate.
|
|
26
|
+
//
|
|
17
27
|
// slidemind_list_engines
|
|
18
28
|
// Lists the live engine catalog (image + slide) for the active corporate
|
|
19
29
|
// design, filterable by type/subtype.
|
|
@@ -151,6 +161,9 @@ const server = new McpServer(
|
|
|
151
161
|
"with an active corporate design is required, and all brand rules come from that " +
|
|
152
162
|
"design's bundle (design.md), never hardcoded here. " +
|
|
153
163
|
"Always call slidemind_list_open_presentations first to learn which presentation_id to target. " +
|
|
164
|
+
"If connection/readiness is unclear, call slidemind_connection_status before asking the user for help. " +
|
|
165
|
+
"To inspect what's already in a window before editing, call slidemind_read_presentation (summary outline or raw plain text). " +
|
|
166
|
+
"After authoring or editing slides, call slidemind_view_draft to LOOK at your own draft (per-slide images, nothing pushed) and self-correct before slidemind_push_deck. " +
|
|
154
167
|
"The CLI may be paired with several add-in environments (local/dev/prod) at once; the list spans " +
|
|
155
168
|
"all of them and each entry is tagged with its env, and routing to the right backend is automatic — " +
|
|
156
169
|
"just pass the presentation_id back. " +
|
|
@@ -183,6 +196,85 @@ server.registerTool(
|
|
|
183
196
|
},
|
|
184
197
|
);
|
|
185
198
|
|
|
199
|
+
server.registerTool(
|
|
200
|
+
"slidemind_connection_status",
|
|
201
|
+
{
|
|
202
|
+
title: "Diagnose CLI/add-in connection readiness",
|
|
203
|
+
description:
|
|
204
|
+
"Return paired backend reachability and per-window readiness. Use this when a slidemind tool fails or before asking the user for manual action.",
|
|
205
|
+
inputSchema: {},
|
|
206
|
+
},
|
|
207
|
+
async () => {
|
|
208
|
+
try {
|
|
209
|
+
const client = await loadBridgeClient();
|
|
210
|
+
const status = await client.connectionStatus();
|
|
211
|
+
return textResult({
|
|
212
|
+
paired: status.paired,
|
|
213
|
+
backendCount: status.backendCount,
|
|
214
|
+
backends: (status.backends ?? []).map((b) => ({
|
|
215
|
+
env: b.env,
|
|
216
|
+
ok: b.ok,
|
|
217
|
+
readinessV1: b.readinessV1 === true,
|
|
218
|
+
capabilities: b.capabilities ?? null,
|
|
219
|
+
error: b.error,
|
|
220
|
+
summary: b.summary ?? null,
|
|
221
|
+
presentations: (b.presentations ?? []).map(client.toPublicPresentation),
|
|
222
|
+
})),
|
|
223
|
+
presentations: (status.presentations ?? []).map(client.toPublicPresentation),
|
|
224
|
+
});
|
|
225
|
+
} catch (err) {
|
|
226
|
+
return errorResult(err);
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
server.registerTool(
|
|
232
|
+
"slidemind_read_presentation",
|
|
233
|
+
{
|
|
234
|
+
title: "Read the current content of an open presentation",
|
|
235
|
+
description:
|
|
236
|
+
"Pull the CURRENT content of an open PowerPoint window so you can inspect the live deck before editing it. " +
|
|
237
|
+
"mode='summary' returns a per-slide outline (index, layout, title, subtitle); mode='raw' (default) returns each slide's full plain text. " +
|
|
238
|
+
"This is plain text content, NOT editing markup. " +
|
|
239
|
+
"presentation_id is the presId from slidemind_list_open_presentations; it's optional when exactly one window is open, but required to disambiguate when several are open.",
|
|
240
|
+
inputSchema: {
|
|
241
|
+
presentation_id: z
|
|
242
|
+
.string()
|
|
243
|
+
.nullable()
|
|
244
|
+
.optional()
|
|
245
|
+
.describe("presId from slidemind_list_open_presentations. Optional when only one window is open; required to pick one when several are."),
|
|
246
|
+
mode: z
|
|
247
|
+
.enum(["summary", "raw"])
|
|
248
|
+
.nullable()
|
|
249
|
+
.optional()
|
|
250
|
+
.describe("'summary' = per-slide outline; 'raw' (default) = each slide's full plain text."),
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
async ({ presentation_id, mode }) => {
|
|
254
|
+
try {
|
|
255
|
+
const client = await loadBridgeClient();
|
|
256
|
+
const registry = await client.requirePaneHost();
|
|
257
|
+
const entry = client.pickPresentationEntry(registry, presentation_id ?? null);
|
|
258
|
+
const res = await client.getPresentationContent({
|
|
259
|
+
presId: entry.presId,
|
|
260
|
+
mode: mode ?? "raw",
|
|
261
|
+
backendURL: entry.backendURL ?? null,
|
|
262
|
+
});
|
|
263
|
+
if (!res?.ok) {
|
|
264
|
+
return errorResult(new Error(res?.error ?? "read presentation failed"));
|
|
265
|
+
}
|
|
266
|
+
return textResult({
|
|
267
|
+
presentation_id: entry.presId,
|
|
268
|
+
mode: res.mode,
|
|
269
|
+
slideCount: res.slideCount,
|
|
270
|
+
slides: res.slides ?? [],
|
|
271
|
+
});
|
|
272
|
+
} catch (err) {
|
|
273
|
+
return errorResult(err);
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
);
|
|
277
|
+
|
|
186
278
|
server.registerTool(
|
|
187
279
|
"slidemind_push_deck",
|
|
188
280
|
{
|
|
@@ -316,7 +408,7 @@ server.registerTool(
|
|
|
316
408
|
const client = await loadBridgeClient();
|
|
317
409
|
// All engine tools hard-require an open host (the truth lives in the
|
|
318
410
|
// add-in); surface the actionable "open PowerPoint…" message up front.
|
|
319
|
-
const registry = await client.
|
|
411
|
+
const registry = await client.requirePaneHost();
|
|
320
412
|
// The engine is per-user, not window-bound: attach to any connected window
|
|
321
413
|
// when the caller didn't pin one. The chosen presId is echoed back so the
|
|
322
414
|
// agent threads it to the follow-up tools (the searchId is bound to it).
|
|
@@ -412,6 +504,62 @@ server.registerTool(
|
|
|
412
504
|
},
|
|
413
505
|
);
|
|
414
506
|
|
|
507
|
+
server.registerTool(
|
|
508
|
+
"slidemind_view_draft",
|
|
509
|
+
{
|
|
510
|
+
title: "Look at a deck's draft slides (no push)",
|
|
511
|
+
description:
|
|
512
|
+
"Render the deck's authored slides/*.jsx to per-slide preview images and return them to you WITHOUT pushing anything — the 'look at my own draft before the user sees it' tool. " +
|
|
513
|
+
"Use this AFTER authoring/editing slides and BEFORE slidemind_push_deck (and before the human slidemind review gate): look at each screenshot, fix anything visibly wrong in the JSX, then view again until each slide is sound. " +
|
|
514
|
+
"Renders locally against the materialized bundle, so it needs no awake task pane (and no open host at all once the bundle has been downloaded once). " +
|
|
515
|
+
"Returns one image per freeform slide; purpose-built layout slides (covers/dividers) and reused native .pptx slides have no draft preview and report as a status line. Nothing is pushed, nothing is written to the deck.",
|
|
516
|
+
inputSchema: {
|
|
517
|
+
deck_path: z
|
|
518
|
+
.string()
|
|
519
|
+
.describe("Path to the deck folder (must contain slides/). Absolute or relative to CWD."),
|
|
520
|
+
presentation_id: z
|
|
521
|
+
.string()
|
|
522
|
+
.nullable()
|
|
523
|
+
.optional()
|
|
524
|
+
.describe("Optional presId — only used to fetch the corporate-design bundle if it isn't cached yet. Omit when the deck's bundle is already materialized (the normal case after authoring)."),
|
|
525
|
+
},
|
|
526
|
+
},
|
|
527
|
+
async ({ deck_path, presentation_id }) => {
|
|
528
|
+
try {
|
|
529
|
+
const { viewDraft } = await loadBuildAndPush();
|
|
530
|
+
const deckDir = path.resolve(process.cwd(), deck_path);
|
|
531
|
+
const result = await viewDraft(deckDir, {
|
|
532
|
+
presentationId: presentation_id ?? null,
|
|
533
|
+
});
|
|
534
|
+
const images = [];
|
|
535
|
+
const lines = [];
|
|
536
|
+
for (const slide of result.slides ?? []) {
|
|
537
|
+
const image = slide.dataUri ? imageContentFromDataUri(slide.dataUri) : null;
|
|
538
|
+
if (image) {
|
|
539
|
+
images.push(image);
|
|
540
|
+
lines.push(
|
|
541
|
+
`${slide.fileName} — preview attached (image ${images.length})` +
|
|
542
|
+
(slide.status === "degraded" && slide.note ? ` — ${slide.note}` : ""),
|
|
543
|
+
);
|
|
544
|
+
} else {
|
|
545
|
+
lines.push(`${slide.fileName} — ${slide.note ?? slide.status}`);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
const header =
|
|
549
|
+
images.length > 0
|
|
550
|
+
? `Rendered ${images.length} draft preview image(s) — NOT pushed to PowerPoint, nothing written to the deck. ` +
|
|
551
|
+
`Judge each screenshot with your eyes, fix what is visibly wrong in the slide JSX, view the draft again, ` +
|
|
552
|
+
`then push with slidemind_push_deck (or build-pptx) once each slide is visually sound.`
|
|
553
|
+
: `No draft preview images were produced — NOTHING was pushed.`;
|
|
554
|
+
const warnLines = (result.warnings ?? []).map((w) => ` warning: ${w}`);
|
|
555
|
+
const text = [header, ...lines, ...warnLines].join("\n");
|
|
556
|
+
return { content: [{ type: "text", text }, ...images] };
|
|
557
|
+
} catch (err) {
|
|
558
|
+
return errorResult(err);
|
|
559
|
+
}
|
|
560
|
+
},
|
|
561
|
+
);
|
|
562
|
+
|
|
415
563
|
server.registerTool(
|
|
416
564
|
"slidemind_list_engines",
|
|
417
565
|
{
|
|
@@ -479,7 +627,7 @@ server.registerTool(
|
|
|
479
627
|
async ({ presentation_id, search_id, selection }) => {
|
|
480
628
|
try {
|
|
481
629
|
const client = await loadBridgeClient();
|
|
482
|
-
await client.
|
|
630
|
+
await client.requirePaneHost();
|
|
483
631
|
const res = await client.viewSlide({
|
|
484
632
|
presId: presentation_id,
|
|
485
633
|
searchId: search_id,
|
|
@@ -532,7 +680,7 @@ server.registerTool(
|
|
|
532
680
|
async ({ presentation_id, search_id, selections, at }) => {
|
|
533
681
|
try {
|
|
534
682
|
const client = await loadBridgeClient();
|
|
535
|
-
await client.
|
|
683
|
+
await client.requirePaneHost();
|
|
536
684
|
const res = await client.insertSlideLive({
|
|
537
685
|
presId: presentation_id,
|
|
538
686
|
searchId: search_id,
|
|
@@ -590,7 +738,7 @@ server.registerTool(
|
|
|
590
738
|
async ({ presentation_id, deck_path, search_id, selections, at }) => {
|
|
591
739
|
try {
|
|
592
740
|
const client = await loadBridgeClient();
|
|
593
|
-
await client.
|
|
741
|
+
await client.requirePaneHost();
|
|
594
742
|
const deckDir = path.resolve(process.cwd(), deck_path);
|
|
595
743
|
const slidesDir = path.join(deckDir, "slides");
|
|
596
744
|
fs.mkdirSync(slidesDir, { recursive: true });
|
|
@@ -754,7 +902,7 @@ server.registerTool(
|
|
|
754
902
|
}
|
|
755
903
|
|
|
756
904
|
const client = await loadBridgeClient();
|
|
757
|
-
await client.
|
|
905
|
+
await client.requirePaneHost();
|
|
758
906
|
const deckDir = path.resolve(process.cwd(), deck_path);
|
|
759
907
|
const assetsDir = path.join(deckDir, "assets");
|
|
760
908
|
fs.mkdirSync(assetsDir, { recursive: true });
|