@slidev/cli 52.16.0 → 52.17.0
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/README.md +1 -1
- package/dist/{build-BtXN8hyb.mjs → build-Cszi5hDI.mjs} +4 -4
- package/dist/cli.mjs +16 -7
- package/dist/{export-BHxAO7pf.mjs → export-CU2GL9dm.mjs} +2 -2
- package/dist/index.d.mts +0 -1
- package/dist/index.mjs +1 -1
- package/dist/{resolver-CZx9LFrt.mjs → resolver-BBT539iz.mjs} +4 -2
- package/dist/{serve-bnzg299W.mjs → serve-CgOXLPUI.mjs} +529 -86
- package/dist/stdio-LsewwOTO.mjs +24 -0
- package/package.json +26 -23
- package/skills/slidev/SKILL.md +1 -0
- package/skills/slidev/references/core-cli.md +8 -0
- package/skills/slidev/references/tool-mcp.md +48 -0
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ Presentation <b>slide</b>s for <b>dev</b>elopers 🧑💻👩💻👨
|
|
|
19
19
|
</p>
|
|
20
20
|
|
|
21
21
|
<p align="center">
|
|
22
|
-
<a href="https://
|
|
22
|
+
<a href="https://x.com/antfu7/status/1389604687502995457">Video Preview</a> | <a href="https://sli.dev">Documentation</a>
|
|
23
23
|
</p>
|
|
24
24
|
|
|
25
25
|
<div align="center">
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as resolveViteConfigs } from "./serve-
|
|
1
|
+
import { n as resolveViteConfigs } from "./serve-CgOXLPUI.mjs";
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
3
|
import { build as build$1 } from "vite";
|
|
4
4
|
import { existsSync } from "node:fs";
|
|
@@ -51,7 +51,7 @@ async function build(options, viteConfig = {}, args) {
|
|
|
51
51
|
dev: true
|
|
52
52
|
}));
|
|
53
53
|
server.listen(port);
|
|
54
|
-
const { exportSlides } = await import("./export-
|
|
54
|
+
const { exportSlides } = await import("./export-CU2GL9dm.mjs");
|
|
55
55
|
const tempDir = resolve(outDir, "temp");
|
|
56
56
|
await fs$1.mkdir(tempDir, { recursive: true });
|
|
57
57
|
await exportSlides({
|
|
@@ -64,7 +64,7 @@ async function build(options, viteConfig = {}, args) {
|
|
|
64
64
|
range: "1",
|
|
65
65
|
width: options.data.config.canvasWidth,
|
|
66
66
|
height: Math.round(options.data.config.canvasWidth / options.data.config.aspectRatio),
|
|
67
|
-
routerMode: options.data.config.routerMode,
|
|
67
|
+
routerMode: options.data.config.routerMode === "memory" ? "history" : options.data.config.routerMode,
|
|
68
68
|
waitUntil: "networkidle",
|
|
69
69
|
timeout: args.timeout || 3e4,
|
|
70
70
|
perSlide: true,
|
|
@@ -92,7 +92,7 @@ async function build(options, viteConfig = {}, args) {
|
|
|
92
92
|
"true",
|
|
93
93
|
"auto"
|
|
94
94
|
].includes(options.data.config.download)) {
|
|
95
|
-
const { exportSlides, getExportOptions } = await import("./export-
|
|
95
|
+
const { exportSlides, getExportOptions } = await import("./export-CU2GL9dm.mjs");
|
|
96
96
|
const port = 12445;
|
|
97
97
|
const app = connect();
|
|
98
98
|
const server = http.createServer(app);
|
package/dist/cli.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { i as resolveEntry, r as isInstalledGlobally } from "./resolver-
|
|
2
|
-
import {
|
|
1
|
+
import { i as resolveEntry, r as isInstalledGlobally } from "./resolver-BBT539iz.mjs";
|
|
2
|
+
import { c as getThemeMeta, d as loadSetups, f as updateFrontmatterPatch, l as resolveTheme, o as resolveOptions, s as parser, t as createServer, u as version } from "./serve-CgOXLPUI.mjs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import process from "node:process";
|
|
5
5
|
import fs from "node:fs/promises";
|
|
@@ -288,15 +288,19 @@ cli.command("build [entry..]", "Build hostable SPA", (args) => exportOptions(com
|
|
|
288
288
|
describe: "exclude speaker notes from the built output"
|
|
289
289
|
}).option("router-mode", {
|
|
290
290
|
type: "string",
|
|
291
|
-
choices: [
|
|
292
|
-
|
|
291
|
+
choices: [
|
|
292
|
+
"hash",
|
|
293
|
+
"history",
|
|
294
|
+
"memory"
|
|
295
|
+
],
|
|
296
|
+
describe: "override routerMode in the built output (hash for subdirectory deploys like GitHub Pages; memory keeps the slide number out of the URL, for kiosk/follower decks)"
|
|
293
297
|
}).option("inspect", {
|
|
294
298
|
default: false,
|
|
295
299
|
type: "boolean",
|
|
296
300
|
describe: "enable the inspect plugin for debugging"
|
|
297
301
|
}).strict().help(), async (args) => {
|
|
298
302
|
const { entry, theme, base, download, out, inspect, "without-notes": withoutNotes, "router-mode": routerMode } = args;
|
|
299
|
-
const { build } = await import("./build-
|
|
303
|
+
const { build } = await import("./build-Cszi5hDI.mjs");
|
|
300
304
|
for (const entryFile of entry) {
|
|
301
305
|
const options = await resolveOptions({
|
|
302
306
|
entry: entryFile,
|
|
@@ -324,6 +328,10 @@ cli.command("format [entry..]", "Format the markdown file", (args) => commonOpti
|
|
|
324
328
|
await parser.save(md);
|
|
325
329
|
}
|
|
326
330
|
});
|
|
331
|
+
cli.command("mcp [entry]", "Start an MCP (Model Context Protocol) server over stdio for AI agents to inspect and edit the slides", (args) => commonOptions(args).strict().help(), async ({ entry }) => {
|
|
332
|
+
const { startMcpStdioServer } = await import("./stdio-LsewwOTO.mjs");
|
|
333
|
+
await startMcpStdioServer(await resolveEntry(entry));
|
|
334
|
+
});
|
|
327
335
|
cli.command("theme [subcommand]", "Theme related operations", (command) => {
|
|
328
336
|
return command.command("eject", "Eject current theme into local file system", (args) => commonOptions(args).option("dir", {
|
|
329
337
|
type: "string",
|
|
@@ -361,7 +369,7 @@ cli.command("theme [subcommand]", "Theme related operations", (command) => {
|
|
|
361
369
|
});
|
|
362
370
|
cli.command("export [entry..]", "Export slides to PDF", (args) => exportOptions(commonOptions(args)).strict().help(), async (args) => {
|
|
363
371
|
const { entry, theme } = args;
|
|
364
|
-
const { exportSlides, getExportOptions } = await import("./export-
|
|
372
|
+
const { exportSlides, getExportOptions } = await import("./export-CU2GL9dm.mjs");
|
|
365
373
|
const candidatePort = await getPort(12445);
|
|
366
374
|
let warned = false;
|
|
367
375
|
for (const entryFile of entry) {
|
|
@@ -413,7 +421,7 @@ cli.command("export-notes [entry..]", "Export slide notes to PDF", (args) => arg
|
|
|
413
421
|
type: "number",
|
|
414
422
|
describe: "wait for the specified ms before exporting"
|
|
415
423
|
}).strict().help(), async ({ entry, output, timeout, wait }) => {
|
|
416
|
-
const { exportNotes } = await import("./export-
|
|
424
|
+
const { exportNotes } = await import("./export-CU2GL9dm.mjs");
|
|
417
425
|
const candidatePort = await getPort(12445);
|
|
418
426
|
for (const entryFile of entry) {
|
|
419
427
|
const options = await resolveOptions({ entry: entryFile }, "export");
|
|
@@ -539,6 +547,7 @@ function printInfo(options, port, base, remote, tunnelUrl, publicIp) {
|
|
|
539
547
|
if (options.utils.define.__SLIDEV_FEATURE_PRESENTER__) console.log(`${dim(" presenter mode ")} > ${blue(`${baseUrl}${presenterPath}`)}`);
|
|
540
548
|
console.log(`${dim(" slides overview ")} > ${blue(`${baseUrl}${overviewPath}`)}`);
|
|
541
549
|
if (options.utils.define.__SLIDEV_FEATURE_BROWSER_EXPORTER__) console.log(`${dim(" export slides")} > ${blue(`${baseUrl}/export/`)}`);
|
|
550
|
+
if (options.mode === "dev" && options.data.config.mcp !== false) console.log(`${dim(" mcp server (ai)")} > ${blue(`http://localhost:${bold(port)}/__mcp`)}`);
|
|
542
551
|
if (options.inspect) console.log(`${dim(" vite inspector")} > ${yellow(`${baseUrl}/__inspect/`)}`);
|
|
543
552
|
let lastRemoteUrl = "";
|
|
544
553
|
if (remote !== void 0) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as getRoots } from "./resolver-
|
|
1
|
+
import { n as getRoots } from "./resolver-BBT539iz.mjs";
|
|
2
2
|
import path, { dirname, relative } from "node:path";
|
|
3
3
|
import process from "node:process";
|
|
4
4
|
import fs from "node:fs/promises";
|
|
@@ -387,7 +387,7 @@ function getExportOptions(args, options, outFilename) {
|
|
|
387
387
|
wait: wait ?? 0,
|
|
388
388
|
waitUntil: waitUntil === "none" ? void 0 : waitUntil ?? "networkidle",
|
|
389
389
|
dark: dark || options.data.config.colorSchema === "dark",
|
|
390
|
-
routerMode: options.data.config.routerMode,
|
|
390
|
+
routerMode: options.data.config.routerMode === "memory" ? "history" : options.data.config.routerMode,
|
|
391
391
|
width: options.data.config.canvasWidth,
|
|
392
392
|
height: Math.round(options.data.config.canvasWidth / options.data.config.aspectRatio),
|
|
393
393
|
withClicks: withClicks ?? format === "pptx",
|
package/dist/index.d.mts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { InlineConfig, PluginOption } from "vite";
|
|
2
2
|
import * as parser from "@slidev/parser/fs";
|
|
3
3
|
import { ResolvedSlidevOptions, ResolvedSlidevUtils, SlidevEntryOptions, SlidevPluginOptions, SlidevServerOptions } from "@slidev/types";
|
|
4
|
-
|
|
5
4
|
//#region node/commands/serve.d.ts
|
|
6
5
|
declare function createServer(options: ResolvedSlidevOptions, viteConfig?: InlineConfig, serverOptions?: SlidevServerOptions): Promise<import("vite").ViteDevServer>;
|
|
7
6
|
//#endregion
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as createDataUtils, o as resolveOptions, r as ViteSlidevPlugin, s as parser, t as createServer } from "./serve-CgOXLPUI.mjs";
|
|
2
2
|
export { ViteSlidevPlugin, createDataUtils, createServer, parser, resolveOptions };
|
|
@@ -229,12 +229,14 @@ async function getRoots(entry) {
|
|
|
229
229
|
isInstalledGlobally.value = slash(relative(userRoot, process.argv[1])).includes("/.pnpm/") || invocationNodeModules != null && slash(relative(userRoot, invocationNodeModules)).startsWith("..") || (await import("is-installed-globally")).default;
|
|
230
230
|
const clientRoot = await findPkgRoot("@slidev/client", cliRoot, true);
|
|
231
231
|
const closestPkgRoot = dirname(await findClosestPkgJsonPath(userRoot) || userRoot);
|
|
232
|
+
const userPkgJson = await getUserPkgJson(closestPkgRoot);
|
|
233
|
+
const userWorkspaceRoot = await searchForWorkspaceRoot(closestPkgRoot);
|
|
232
234
|
rootsInfo = {
|
|
233
235
|
cliRoot,
|
|
234
236
|
clientRoot,
|
|
235
237
|
userRoot,
|
|
236
|
-
userPkgJson
|
|
237
|
-
userWorkspaceRoot
|
|
238
|
+
userPkgJson,
|
|
239
|
+
userWorkspaceRoot
|
|
238
240
|
};
|
|
239
241
|
return rootsInfo;
|
|
240
242
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as resolveImportPath, c as toAtFS, i as resolveEntry, n as getRoots, o as resolveImportUrl, r as isInstalledGlobally, s as resolveSourceFiles, t as createResolver } from "./resolver-
|
|
1
|
+
import { a as resolveImportPath, c as toAtFS, i as resolveEntry, n as getRoots, o as resolveImportUrl, r as isInstalledGlobally, s as resolveSourceFiles, t as createResolver } from "./resolver-BBT539iz.mjs";
|
|
2
2
|
import { builtinModules } from "node:module";
|
|
3
3
|
import path, { basename, dirname, join, relative, resolve, win32 } from "node:path";
|
|
4
4
|
import process from "node:process";
|
|
@@ -40,7 +40,11 @@ import lz from "lz-string";
|
|
|
40
40
|
import { encode } from "plantuml-encoder";
|
|
41
41
|
import { SourceMapConsumer } from "source-map-js";
|
|
42
42
|
import { fromAsyncCodeToHtml } from "@shikijs/markdown-it/async";
|
|
43
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
44
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
45
|
+
import { z } from "zod";
|
|
43
46
|
import ServerRef from "vite-plugin-vue-server-ref";
|
|
47
|
+
import { VitePWA } from "vite-plugin-pwa";
|
|
44
48
|
import UnoCSS from "unocss/vite";
|
|
45
49
|
import { mergeConfigs, presetIcons } from "unocss";
|
|
46
50
|
import Vue from "@vitejs/plugin-vue";
|
|
@@ -307,6 +311,7 @@ const RE_SLIDEV_CLIENT_SUBPATH = /^@slidev\/client\/(.*)/;
|
|
|
307
311
|
const RE_SLIDEV_VIRTUAL = /^#slidev\/(.*)/;
|
|
308
312
|
const RE_MONACO_EDITOR = /\/monaco-editor(?:-core)?\//;
|
|
309
313
|
const INCLUDE_GLOBAL = [
|
|
314
|
+
"@fix-webm-duration/fix",
|
|
310
315
|
"@typescript/ata",
|
|
311
316
|
"file-saver",
|
|
312
317
|
"lz-string",
|
|
@@ -528,7 +533,7 @@ const templateTitleRenderer = {
|
|
|
528
533
|
};
|
|
529
534
|
//#endregion
|
|
530
535
|
//#region node/vite/importGuard.ts
|
|
531
|
-
const virtualSlideMarkdownIds = new Set([templateTitleRendererMd.id, templateLegacyTitles.id]);
|
|
536
|
+
const virtualSlideMarkdownIds = /* @__PURE__ */ new Set([templateTitleRendererMd.id, templateLegacyTitles.id]);
|
|
532
537
|
function createSlideImportGuardPlugin() {
|
|
533
538
|
let config;
|
|
534
539
|
let allowRoots = [];
|
|
@@ -685,7 +690,7 @@ function createLayoutWrapperPlugin({ data, utils }) {
|
|
|
685
690
|
}
|
|
686
691
|
//#endregion
|
|
687
692
|
//#region package.json
|
|
688
|
-
var version = "52.
|
|
693
|
+
var version = "52.17.0";
|
|
689
694
|
//#endregion
|
|
690
695
|
//#region node/integrations/addons.ts
|
|
691
696
|
async function resolveAddons(addonsInConfig) {
|
|
@@ -901,7 +906,7 @@ function resolveShikiOptions(options) {
|
|
|
901
906
|
if (mergedOptions.themes) {
|
|
902
907
|
for (const theme of Object.values(mergedOptions.themes)) if (typeof theme === "object" && theme?.name) themeInput[theme.name] = theme;
|
|
903
908
|
}
|
|
904
|
-
const languageNames = new Set([
|
|
909
|
+
const languageNames = /* @__PURE__ */ new Set([
|
|
905
910
|
"markdown",
|
|
906
911
|
"vue",
|
|
907
912
|
"javascript",
|
|
@@ -1071,6 +1076,7 @@ function getDefine(options) {
|
|
|
1071
1076
|
__DEV__: options.mode === "dev",
|
|
1072
1077
|
__SLIDEV_CLIENT_ROOT__: toAtFS(options.clientRoot),
|
|
1073
1078
|
__SLIDEV_HASH_ROUTE__: options.data.config.routerMode === "hash",
|
|
1079
|
+
__SLIDEV_MEMORY_ROUTE__: options.data.config.routerMode === "memory" && options.mode !== "export",
|
|
1074
1080
|
__SLIDEV_FEATURE_DRAWINGS__: matchMode(options.data.config.drawings.enabled),
|
|
1075
1081
|
__SLIDEV_FEATURE_EDITOR__: options.mode === "dev" && options.data.config.editor !== false,
|
|
1076
1082
|
__SLIDEV_FEATURE_DRAWINGS_PERSIST__: !!options.data.config.drawings.persist,
|
|
@@ -1083,6 +1089,7 @@ function getDefine(options) {
|
|
|
1083
1089
|
].includes(options.data.config.download),
|
|
1084
1090
|
__SLIDEV_FEATURE_BROWSER_EXPORTER__: matchMode(options.data.config.browserExporter),
|
|
1085
1091
|
__SLIDEV_FEATURE_WAKE_LOCK__: matchMode(options.data.config.wakeLock),
|
|
1092
|
+
__SLIDEV_FEATURE_PWA__: matchMode(options.data.config.pwa),
|
|
1086
1093
|
__SLIDEV_HAS_SERVER__: options.mode !== "build"
|
|
1087
1094
|
}, (v, k) => [v, JSON.stringify(k)]);
|
|
1088
1095
|
}
|
|
@@ -1658,6 +1665,7 @@ function createSlidesLoader(options, serverOptions) {
|
|
|
1658
1665
|
frontmatter: void 0,
|
|
1659
1666
|
source: void 0,
|
|
1660
1667
|
importChain: void 0,
|
|
1668
|
+
images: slide.images ?? slide.source?.images,
|
|
1661
1669
|
...mode === "build" ? {
|
|
1662
1670
|
raw: "",
|
|
1663
1671
|
content: "",
|
|
@@ -2579,6 +2587,491 @@ async function createMarkdownPlugin(options, { markdown: mdOptions }) {
|
|
|
2579
2587
|
});
|
|
2580
2588
|
}
|
|
2581
2589
|
//#endregion
|
|
2590
|
+
//#region node/mcp/operations.ts
|
|
2591
|
+
/**
|
|
2592
|
+
* Resolve a rendered slide (1-based, as displayed in the presentation) or
|
|
2593
|
+
* throw a descriptive error.
|
|
2594
|
+
*/
|
|
2595
|
+
function resolveSlide(data, no) {
|
|
2596
|
+
const slide = data.slides[no - 1];
|
|
2597
|
+
if (!slide) throw new Error(`Slide ${no} does not exist. The deck has ${data.slides.length} slides (1-${data.slides.length}).`);
|
|
2598
|
+
return slide;
|
|
2599
|
+
}
|
|
2600
|
+
function getMarkdown(data, source) {
|
|
2601
|
+
const md = data.markdownFiles[source.filepath];
|
|
2602
|
+
if (!md) throw new Error(`Markdown file not loaded: ${source.filepath}`);
|
|
2603
|
+
return md;
|
|
2604
|
+
}
|
|
2605
|
+
function assertNotEntryHeadmatter(data, source, action) {
|
|
2606
|
+
if (source.filepath === data.entry.filepath && data.entry.slides.indexOf(source) === 0) throw new Error(`Cannot ${action} the first slide of the entry file: its frontmatter is the deck headmatter (global configuration). Edit its content with the update tool instead, or operate on the following slides.`);
|
|
2607
|
+
}
|
|
2608
|
+
/**
|
|
2609
|
+
* Apply a `SlidePatch` to the slide source and save the markdown file.
|
|
2610
|
+
*
|
|
2611
|
+
* Note this only mutates the *source* slide (`slide.source`), not the
|
|
2612
|
+
* rendered `SlideInfo`, so a running dev server will pick up the change from
|
|
2613
|
+
* disk like an external edit and push HMR updates to connected clients.
|
|
2614
|
+
*/
|
|
2615
|
+
async function applySlidePatch(data, no, patch) {
|
|
2616
|
+
const slide = resolveSlide(data, no);
|
|
2617
|
+
const source = slide.source;
|
|
2618
|
+
if (patch.content != null) source.content = patch.content;
|
|
2619
|
+
if (patch.frontmatterRaw != null) if (patch.frontmatterRaw.trim() === "") source.frontmatterDoc = source.frontmatterStyle = void 0;
|
|
2620
|
+
else {
|
|
2621
|
+
const parsed = YAML.parseDocument(patch.frontmatterRaw);
|
|
2622
|
+
if (parsed.errors.length) throw new Error(`Invalid YAML frontmatter: ${parsed.errors.map((e) => e.message).join("; ")}`);
|
|
2623
|
+
source.frontmatterDoc = parsed;
|
|
2624
|
+
}
|
|
2625
|
+
if (patch.note != null) source.note = patch.note;
|
|
2626
|
+
if (patch.frontmatter) updateFrontmatterPatch(source, patch.frontmatter);
|
|
2627
|
+
parser.prettifySlide(source);
|
|
2628
|
+
return {
|
|
2629
|
+
slide,
|
|
2630
|
+
fileContent: await parser.save(getMarkdown(data, source))
|
|
2631
|
+
};
|
|
2632
|
+
}
|
|
2633
|
+
/**
|
|
2634
|
+
* Insert a new slide after an existing one, in the same markdown file.
|
|
2635
|
+
*/
|
|
2636
|
+
async function insertSlide(data, options) {
|
|
2637
|
+
const anchor = resolveSlide(data, options.after);
|
|
2638
|
+
const md = getMarkdown(data, anchor.source);
|
|
2639
|
+
const frontmatter = options.frontmatter ?? {};
|
|
2640
|
+
const doc = Object.keys(frontmatter).length > 0 ? new YAML.Document(frontmatter) : void 0;
|
|
2641
|
+
const source = {
|
|
2642
|
+
filepath: anchor.source.filepath,
|
|
2643
|
+
index: 0,
|
|
2644
|
+
start: 0,
|
|
2645
|
+
contentStart: 0,
|
|
2646
|
+
end: 0,
|
|
2647
|
+
raw: "",
|
|
2648
|
+
revision: "",
|
|
2649
|
+
content: options.content,
|
|
2650
|
+
contentRaw: options.content,
|
|
2651
|
+
frontmatter,
|
|
2652
|
+
frontmatterDoc: doc,
|
|
2653
|
+
frontmatterStyle: doc ? "frontmatter" : void 0,
|
|
2654
|
+
frontmatterRaw: doc?.toString(),
|
|
2655
|
+
note: options.note?.trim() || void 0
|
|
2656
|
+
};
|
|
2657
|
+
parser.prettifySlide(source);
|
|
2658
|
+
const anchorIdx = md.slides.indexOf(anchor.source);
|
|
2659
|
+
md.slides.splice(anchorIdx + 1, 0, source);
|
|
2660
|
+
const fileContent = await parser.save(md);
|
|
2661
|
+
return {
|
|
2662
|
+
source,
|
|
2663
|
+
filepath: md.filepath,
|
|
2664
|
+
fileContent
|
|
2665
|
+
};
|
|
2666
|
+
}
|
|
2667
|
+
/**
|
|
2668
|
+
* Remove a slide from its source markdown file.
|
|
2669
|
+
*/
|
|
2670
|
+
async function removeSlide(data, no) {
|
|
2671
|
+
const slide = resolveSlide(data, no);
|
|
2672
|
+
assertNotEntryHeadmatter(data, slide.source, "remove");
|
|
2673
|
+
const md = getMarkdown(data, slide.source);
|
|
2674
|
+
const idx = md.slides.indexOf(slide.source);
|
|
2675
|
+
if (idx < 0) throw new Error(`Slide ${no} is out of sync with its source file. Try again.`);
|
|
2676
|
+
md.slides.splice(idx, 1);
|
|
2677
|
+
const fileContent = await parser.save(md);
|
|
2678
|
+
return {
|
|
2679
|
+
removed: slide,
|
|
2680
|
+
filepath: md.filepath,
|
|
2681
|
+
fileContent
|
|
2682
|
+
};
|
|
2683
|
+
}
|
|
2684
|
+
/**
|
|
2685
|
+
* Move a slide before or after another slide within the same markdown file.
|
|
2686
|
+
*/
|
|
2687
|
+
async function moveSlide(data, options) {
|
|
2688
|
+
const { from, before, after } = options;
|
|
2689
|
+
if (before == null === (after == null)) throw new Error("Specify exactly one of `before` or `after`.");
|
|
2690
|
+
const anchorNo = before ?? after;
|
|
2691
|
+
if (anchorNo === from) throw new Error("The `before`/`after` anchor must be a different slide than `from`.");
|
|
2692
|
+
const slide = resolveSlide(data, from);
|
|
2693
|
+
const anchor = resolveSlide(data, anchorNo);
|
|
2694
|
+
if (slide.source.filepath !== anchor.source.filepath) throw new Error(`Cannot move a slide across markdown files: slide ${from} is in "${slide.source.filepath}" but slide ${anchorNo} is in "${anchor.source.filepath}" (imported with \`src:\`). Move it within its own file, or edit the \`src:\` imports in the entry file manually.`);
|
|
2695
|
+
assertNotEntryHeadmatter(data, slide.source, "move");
|
|
2696
|
+
if (before != null) assertNotEntryHeadmatter(data, anchor.source, "insert a slide before");
|
|
2697
|
+
const md = getMarkdown(data, slide.source);
|
|
2698
|
+
const fromIdx = md.slides.indexOf(slide.source);
|
|
2699
|
+
if (fromIdx < 0) throw new Error(`Slide ${from} is out of sync with its source file. Try again.`);
|
|
2700
|
+
md.slides.splice(fromIdx, 1);
|
|
2701
|
+
const anchorIdx = md.slides.indexOf(anchor.source);
|
|
2702
|
+
if (anchorIdx < 0) throw new Error(`Slide ${anchorNo} is out of sync with its source file. Try again.`);
|
|
2703
|
+
md.slides.splice(before != null ? anchorIdx : anchorIdx + 1, 0, slide.source);
|
|
2704
|
+
const fileContent = await parser.save(md);
|
|
2705
|
+
return {
|
|
2706
|
+
moved: slide,
|
|
2707
|
+
anchor,
|
|
2708
|
+
filepath: md.filepath,
|
|
2709
|
+
fileContent
|
|
2710
|
+
};
|
|
2711
|
+
}
|
|
2712
|
+
//#endregion
|
|
2713
|
+
//#region node/mcp/server.ts
|
|
2714
|
+
function result(data) {
|
|
2715
|
+
return { content: [{
|
|
2716
|
+
type: "text",
|
|
2717
|
+
text: typeof data === "string" ? data : JSON.stringify(data, null, 2)
|
|
2718
|
+
}] };
|
|
2719
|
+
}
|
|
2720
|
+
function slideSummary(slide) {
|
|
2721
|
+
return {
|
|
2722
|
+
no: slide.index + 1,
|
|
2723
|
+
title: slide.title ?? null,
|
|
2724
|
+
...slide.frontmatter.layout ? { layout: slide.frontmatter.layout } : {},
|
|
2725
|
+
file: slide.source.filepath,
|
|
2726
|
+
hasNote: !!slide.source.note,
|
|
2727
|
+
...slide.importChain?.length ? { importedBySrcDirective: true } : {}
|
|
2728
|
+
};
|
|
2729
|
+
}
|
|
2730
|
+
const noSchema = z.number().int().min(1).describe("Slide number (1-based, as displayed in the presentation)");
|
|
2731
|
+
const frontmatterSchema = z.record(z.any()).optional().describe("Slide frontmatter (YAML headmatter of the slide) as an object, e.g. { \"layout\": \"two-cols\" }");
|
|
2732
|
+
/**
|
|
2733
|
+
* Create the Slidev MCP server exposing tools for agents to inspect, edit,
|
|
2734
|
+
* and (with a running dev server) navigate a slides deck.
|
|
2735
|
+
*
|
|
2736
|
+
* The same tool set is served over the dev server HTTP endpoint (`/__mcp`)
|
|
2737
|
+
* and the `slidev mcp` stdio command.
|
|
2738
|
+
*/
|
|
2739
|
+
function createSlidevMcpServer(ctx) {
|
|
2740
|
+
const server = new McpServer({
|
|
2741
|
+
name: "slidev",
|
|
2742
|
+
version: ctx.version
|
|
2743
|
+
}, { instructions: [
|
|
2744
|
+
"Tools for working with a Slidev (https://sli.dev) slides deck.",
|
|
2745
|
+
"A deck is a Markdown file where slides are separated by `---`; each slide can have YAML frontmatter, Markdown/Vue content, and a speaker note (trailing HTML comment).",
|
|
2746
|
+
"Slides are addressed by their rendered 1-based number, matching the slide numbers shown in the presentation.",
|
|
2747
|
+
"After editing tools run, a running dev server hot-reloads the presentation automatically."
|
|
2748
|
+
].join("\n") });
|
|
2749
|
+
server.registerTool("slidev-get-info", {
|
|
2750
|
+
title: "Get deck info",
|
|
2751
|
+
description: "Get an overview of the Slidev deck: entry file, title, slide count, markdown files, and (when a dev server is running) the server URL and current position of the live presentation.",
|
|
2752
|
+
annotations: { readOnlyHint: true }
|
|
2753
|
+
}, async () => {
|
|
2754
|
+
const data = await ctx.getData();
|
|
2755
|
+
const nav = ctx.nav?.getState();
|
|
2756
|
+
return result({
|
|
2757
|
+
slidevVersion: ctx.version,
|
|
2758
|
+
entry: ctx.entry,
|
|
2759
|
+
title: data.headmatter.title ?? data.slides[0]?.title ?? null,
|
|
2760
|
+
theme: data.config?.theme ?? data.headmatter.theme ?? null,
|
|
2761
|
+
totalSlides: data.slides.length,
|
|
2762
|
+
markdownFiles: Object.keys(data.markdownFiles),
|
|
2763
|
+
...ctx.getServerUrl?.() ? { server: {
|
|
2764
|
+
url: ctx.getServerUrl(),
|
|
2765
|
+
currentPage: nav?.page || null,
|
|
2766
|
+
currentClicks: nav?.page ? nav.clicks : null
|
|
2767
|
+
} } : {}
|
|
2768
|
+
});
|
|
2769
|
+
});
|
|
2770
|
+
server.registerTool("slidev-list-slides", {
|
|
2771
|
+
title: "List slides",
|
|
2772
|
+
description: "List all slides of the deck with their number, title, layout, and source file. Slides hidden with `hide`/`disabled` frontmatter are not included.",
|
|
2773
|
+
annotations: { readOnlyHint: true }
|
|
2774
|
+
}, async () => {
|
|
2775
|
+
return result((await ctx.getData()).slides.map(slideSummary));
|
|
2776
|
+
});
|
|
2777
|
+
server.registerTool("slidev-get-slide", {
|
|
2778
|
+
title: "Get slide",
|
|
2779
|
+
description: "Get the full source of one slide: frontmatter, Markdown content, and speaker note.",
|
|
2780
|
+
inputSchema: { no: noSchema },
|
|
2781
|
+
annotations: { readOnlyHint: true }
|
|
2782
|
+
}, async ({ no }) => {
|
|
2783
|
+
const slide = resolveSlide(await ctx.getData(), no);
|
|
2784
|
+
return result({
|
|
2785
|
+
...slideSummary(slide),
|
|
2786
|
+
frontmatter: slide.source.frontmatter,
|
|
2787
|
+
content: slide.source.content.trim(),
|
|
2788
|
+
note: slide.source.note ?? null,
|
|
2789
|
+
...slide.importChain?.length ? { importedBy: slide.importChain.map((s) => `${s.filepath}#${s.index + 1}`) } : {}
|
|
2790
|
+
});
|
|
2791
|
+
});
|
|
2792
|
+
server.registerTool("slidev-update-slide", {
|
|
2793
|
+
title: "Update slide",
|
|
2794
|
+
description: "Update the content, speaker note, and/or frontmatter of a slide. Only the provided fields are changed. Pass an empty string to clear the content or note. In `frontmatter`, only the given keys are patched; pass `null` as a value to delete that key.",
|
|
2795
|
+
inputSchema: {
|
|
2796
|
+
no: noSchema,
|
|
2797
|
+
content: z.string().optional().describe("New Markdown content of the slide (without frontmatter and note)"),
|
|
2798
|
+
note: z.string().optional().describe("New speaker note (Markdown, stored as a trailing HTML comment)"),
|
|
2799
|
+
frontmatter: frontmatterSchema
|
|
2800
|
+
}
|
|
2801
|
+
}, async ({ no, content, note, frontmatter }) => {
|
|
2802
|
+
if (content == null && note == null && frontmatter == null) throw new Error("Nothing to update: provide at least one of `content`, `note`, or `frontmatter`.");
|
|
2803
|
+
const { slide } = await applySlidePatch(await ctx.getData(), no, {
|
|
2804
|
+
content,
|
|
2805
|
+
note,
|
|
2806
|
+
frontmatter
|
|
2807
|
+
});
|
|
2808
|
+
return result(`Updated slide ${no} in ${slide.source.filepath}.`);
|
|
2809
|
+
});
|
|
2810
|
+
server.registerTool("slidev-insert-slide", {
|
|
2811
|
+
title: "Insert slide",
|
|
2812
|
+
description: "Insert a new slide after an existing slide (into the same markdown file). To add a slide at the very end, pass the last slide number.",
|
|
2813
|
+
inputSchema: {
|
|
2814
|
+
after: z.number().int().min(1).describe("Slide number (1-based) after which the new slide is inserted"),
|
|
2815
|
+
content: z.string().describe("Markdown content of the new slide"),
|
|
2816
|
+
frontmatter: frontmatterSchema,
|
|
2817
|
+
note: z.string().optional().describe("Speaker note of the new slide")
|
|
2818
|
+
}
|
|
2819
|
+
}, async ({ after, content, frontmatter, note }) => {
|
|
2820
|
+
const { filepath } = await insertSlide(await ctx.getData(), {
|
|
2821
|
+
after,
|
|
2822
|
+
content,
|
|
2823
|
+
frontmatter,
|
|
2824
|
+
note
|
|
2825
|
+
});
|
|
2826
|
+
return result(`Inserted a new slide after slide ${after} in ${filepath}. Slide numbers after it have shifted; list the slides again if needed.`);
|
|
2827
|
+
});
|
|
2828
|
+
server.registerTool("slidev-remove-slide", {
|
|
2829
|
+
title: "Remove slide",
|
|
2830
|
+
description: "Remove a slide from the deck (deletes it from its source markdown file).",
|
|
2831
|
+
inputSchema: { no: noSchema },
|
|
2832
|
+
annotations: { destructiveHint: true }
|
|
2833
|
+
}, async ({ no }) => {
|
|
2834
|
+
const { removed, filepath } = await removeSlide(await ctx.getData(), no);
|
|
2835
|
+
return result(`Removed slide ${no}${removed.title ? ` ("${removed.title}")` : ""} from ${filepath}. Slide numbers after it have shifted; list the slides again if needed.`);
|
|
2836
|
+
});
|
|
2837
|
+
server.registerTool("slidev-move-slide", {
|
|
2838
|
+
title: "Move slide",
|
|
2839
|
+
description: "Move a slide before or after another slide to reorder the deck. Both slides must be in the same markdown file. To swap two adjacent slides, move one after the other.",
|
|
2840
|
+
inputSchema: {
|
|
2841
|
+
from: z.number().int().min(1).describe("Slide number (1-based) of the slide to move"),
|
|
2842
|
+
before: z.number().int().min(1).optional().describe("Move the slide right before this slide number"),
|
|
2843
|
+
after: z.number().int().min(1).optional().describe("Move the slide right after this slide number")
|
|
2844
|
+
}
|
|
2845
|
+
}, async ({ from, before, after }) => {
|
|
2846
|
+
const { filepath } = await moveSlide(await ctx.getData(), {
|
|
2847
|
+
from,
|
|
2848
|
+
before,
|
|
2849
|
+
after
|
|
2850
|
+
});
|
|
2851
|
+
return result(`Moved slide ${from} ${before != null ? `before slide ${before}` : `after slide ${after}`} in ${filepath}. Slide numbers have shifted; list the slides again if needed.`);
|
|
2852
|
+
});
|
|
2853
|
+
if (ctx.nav) {
|
|
2854
|
+
const nav = ctx.nav;
|
|
2855
|
+
server.registerTool("slidev-goto-slide", {
|
|
2856
|
+
title: "Go to slide",
|
|
2857
|
+
description: "Navigate the live presentation (all connected browsers) to a given slide, e.g. to visually verify a slide after editing it.",
|
|
2858
|
+
inputSchema: {
|
|
2859
|
+
no: noSchema,
|
|
2860
|
+
clicks: z.number().int().min(0).optional().describe("Click animation step to reveal (defaults to 0)")
|
|
2861
|
+
},
|
|
2862
|
+
annotations: { idempotentHint: true }
|
|
2863
|
+
}, async ({ no, clicks }) => {
|
|
2864
|
+
resolveSlide(await ctx.getData(), no);
|
|
2865
|
+
nav.go(no, clicks ?? 0);
|
|
2866
|
+
return result(`Navigated the presentation to slide ${no}${clicks ? ` (click ${clicks})` : ""}.`);
|
|
2867
|
+
});
|
|
2868
|
+
}
|
|
2869
|
+
return server;
|
|
2870
|
+
}
|
|
2871
|
+
//#endregion
|
|
2872
|
+
//#region node/integrations/drawings.ts
|
|
2873
|
+
function resolveDrawingsDir(options) {
|
|
2874
|
+
return options.data.config.drawings.persist ? resolve(dirname(options.entry), options.data.config.drawings.persist) : void 0;
|
|
2875
|
+
}
|
|
2876
|
+
async function loadDrawings(options) {
|
|
2877
|
+
const dir = resolveDrawingsDir(options);
|
|
2878
|
+
if (!dir || !existsSync(dir)) return {};
|
|
2879
|
+
const files = await fg("*.svg", {
|
|
2880
|
+
onlyFiles: true,
|
|
2881
|
+
cwd: dir,
|
|
2882
|
+
absolute: true,
|
|
2883
|
+
suppressErrors: true
|
|
2884
|
+
});
|
|
2885
|
+
const obj = {};
|
|
2886
|
+
await Promise.all(files.map(async (path) => {
|
|
2887
|
+
const num = +basename(path, ".svg");
|
|
2888
|
+
if (Number.isNaN(num)) return;
|
|
2889
|
+
const lines = (await fs$1.readFile(path, "utf8")).split(/\n/g);
|
|
2890
|
+
obj[num.toString()] = lines.slice(1, -1).join("\n");
|
|
2891
|
+
}));
|
|
2892
|
+
return obj;
|
|
2893
|
+
}
|
|
2894
|
+
async function writeDrawings(options, drawing) {
|
|
2895
|
+
const dir = resolveDrawingsDir(options);
|
|
2896
|
+
if (!dir) return;
|
|
2897
|
+
const width = options.data.config.canvasWidth;
|
|
2898
|
+
const SVG_HEAD = `<svg width="${width}" height="${Math.round(width / options.data.config.aspectRatio)}" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">`;
|
|
2899
|
+
await fs$1.mkdir(dir, { recursive: true });
|
|
2900
|
+
return Promise.all(Object.entries(drawing).map(async ([key, value]) => {
|
|
2901
|
+
if (!value) return;
|
|
2902
|
+
const svg = `${SVG_HEAD}\n${value}\n</svg>`;
|
|
2903
|
+
await fs$1.writeFile(join(dir, `${key}.svg`), svg, "utf-8");
|
|
2904
|
+
}));
|
|
2905
|
+
}
|
|
2906
|
+
//#endregion
|
|
2907
|
+
//#region node/integrations/snapshots.ts
|
|
2908
|
+
function resolveSnapshotsDir(options) {
|
|
2909
|
+
return resolve(dirname(options.entry), ".slidev/snapshots");
|
|
2910
|
+
}
|
|
2911
|
+
async function loadSnapshots(options) {
|
|
2912
|
+
const dir = resolveSnapshotsDir(options);
|
|
2913
|
+
const file = join(dir, "snapshots.json");
|
|
2914
|
+
if (!dir || !existsSync(file)) return {};
|
|
2915
|
+
return JSON.parse(await fs$1.readFile(file, "utf8"));
|
|
2916
|
+
}
|
|
2917
|
+
async function writeSnapshots(options, data) {
|
|
2918
|
+
const dir = resolveSnapshotsDir(options);
|
|
2919
|
+
if (!dir) return;
|
|
2920
|
+
await fs$1.mkdir(dir, { recursive: true });
|
|
2921
|
+
await fs$1.writeFile(join(dir, "snapshots.json"), JSON.stringify(data, null, 2), "utf-8");
|
|
2922
|
+
}
|
|
2923
|
+
//#endregion
|
|
2924
|
+
//#region node/vite/serverRef.ts
|
|
2925
|
+
const serverRefStates = /* @__PURE__ */ new WeakMap();
|
|
2926
|
+
/**
|
|
2927
|
+
* Access the server-side state shared with the clients via
|
|
2928
|
+
* `vite-plugin-vue-server-ref` (e.g. `nav` holds the current position of the
|
|
2929
|
+
* live presentation).
|
|
2930
|
+
*/
|
|
2931
|
+
function getServerRefState(options) {
|
|
2932
|
+
return serverRefStates.get(options);
|
|
2933
|
+
}
|
|
2934
|
+
async function createServerRefPlugin(options, pluginOptions) {
|
|
2935
|
+
const state = {
|
|
2936
|
+
sync: false,
|
|
2937
|
+
nav: {
|
|
2938
|
+
page: 0,
|
|
2939
|
+
clicks: 0,
|
|
2940
|
+
timer: {
|
|
2941
|
+
status: "stopped",
|
|
2942
|
+
slides: {},
|
|
2943
|
+
startedAt: 0,
|
|
2944
|
+
pausedAt: 0
|
|
2945
|
+
}
|
|
2946
|
+
},
|
|
2947
|
+
drawings: await loadDrawings(options),
|
|
2948
|
+
snapshots: await loadSnapshots(options),
|
|
2949
|
+
...pluginOptions.serverRef?.state
|
|
2950
|
+
};
|
|
2951
|
+
serverRefStates.set(options, state);
|
|
2952
|
+
return ServerRef({
|
|
2953
|
+
debug: false,
|
|
2954
|
+
state,
|
|
2955
|
+
onChanged(key, data, patch, timestamp) {
|
|
2956
|
+
pluginOptions.serverRef?.onChanged?.(key, data, patch, timestamp);
|
|
2957
|
+
if (options.data.config.drawings.persist && key === "drawings") writeDrawings(options, patch ?? data);
|
|
2958
|
+
if (key === "snapshots") writeSnapshots(options, data);
|
|
2959
|
+
}
|
|
2960
|
+
});
|
|
2961
|
+
}
|
|
2962
|
+
const SERVER_REF_WS_EVENT = "vue-server-ref";
|
|
2963
|
+
const SERVER_REF_NAV_MODULE_IDS = ["/@server-reactive/nav", "/@server-ref/nav"];
|
|
2964
|
+
/**
|
|
2965
|
+
* Serves the Slidev MCP (Model Context Protocol) server on the dev server at
|
|
2966
|
+
* `/__mcp` (streamable HTTP transport), so AI agents can inspect, edit, and
|
|
2967
|
+
* navigate the slides. Can be disabled with `mcp: false` in the headmatter.
|
|
2968
|
+
*/
|
|
2969
|
+
function createMcpPlugin(options) {
|
|
2970
|
+
return {
|
|
2971
|
+
name: "slidev:mcp",
|
|
2972
|
+
apply: "serve",
|
|
2973
|
+
configureServer(server) {
|
|
2974
|
+
if (options.mode !== "dev") return;
|
|
2975
|
+
const ctx = {
|
|
2976
|
+
version,
|
|
2977
|
+
entry: options.entry,
|
|
2978
|
+
getData: () => options.data,
|
|
2979
|
+
getServerUrl: () => server.resolvedUrls?.local[0],
|
|
2980
|
+
nav: {
|
|
2981
|
+
getState: () => {
|
|
2982
|
+
const nav = getServerRefState(options)?.nav;
|
|
2983
|
+
return {
|
|
2984
|
+
page: nav?.page ?? 0,
|
|
2985
|
+
clicks: nav?.clicks ?? 0
|
|
2986
|
+
};
|
|
2987
|
+
},
|
|
2988
|
+
go: (page, clicks) => navigateClients(server, options, page, clicks)
|
|
2989
|
+
}
|
|
2990
|
+
};
|
|
2991
|
+
server.middlewares.use(async (req, res, next) => {
|
|
2992
|
+
if (req.url?.split("?")[0]?.replace(/\/$/, "") !== "/__mcp") return next();
|
|
2993
|
+
if (options.data.config.mcp === false) {
|
|
2994
|
+
res.statusCode = 403;
|
|
2995
|
+
res.setHeader("Content-Type", "application/json");
|
|
2996
|
+
res.end(JSON.stringify({
|
|
2997
|
+
jsonrpc: "2.0",
|
|
2998
|
+
error: {
|
|
2999
|
+
code: -32e3,
|
|
3000
|
+
message: "The Slidev MCP server is disabled (`mcp: false` in the headmatter)"
|
|
3001
|
+
},
|
|
3002
|
+
id: null
|
|
3003
|
+
}));
|
|
3004
|
+
return;
|
|
3005
|
+
}
|
|
3006
|
+
try {
|
|
3007
|
+
const mcp = createSlidevMcpServer(ctx);
|
|
3008
|
+
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
|
|
3009
|
+
res.on("close", () => {
|
|
3010
|
+
transport.close();
|
|
3011
|
+
mcp.close();
|
|
3012
|
+
});
|
|
3013
|
+
await mcp.connect(transport);
|
|
3014
|
+
await transport.handleRequest(req, res);
|
|
3015
|
+
} catch (e) {
|
|
3016
|
+
console.error("[slidev] Error handling MCP request:", e);
|
|
3017
|
+
if (!res.headersSent) {
|
|
3018
|
+
res.statusCode = 500;
|
|
3019
|
+
res.setHeader("Content-Type", "application/json");
|
|
3020
|
+
res.end(JSON.stringify({
|
|
3021
|
+
jsonrpc: "2.0",
|
|
3022
|
+
error: {
|
|
3023
|
+
code: -32603,
|
|
3024
|
+
message: "Internal server error"
|
|
3025
|
+
},
|
|
3026
|
+
id: null
|
|
3027
|
+
}));
|
|
3028
|
+
}
|
|
3029
|
+
}
|
|
3030
|
+
});
|
|
3031
|
+
}
|
|
3032
|
+
};
|
|
3033
|
+
}
|
|
3034
|
+
/**
|
|
3035
|
+
* Drive all connected clients to the given slide by patching the shared
|
|
3036
|
+
* `nav` state, mimicking what `vite-plugin-vue-server-ref` does when a
|
|
3037
|
+
* presenter or remote-control client navigates.
|
|
3038
|
+
*/
|
|
3039
|
+
function navigateClients(server, options, page, clicks) {
|
|
3040
|
+
const time = Date.now();
|
|
3041
|
+
const nav = getServerRefState(options)?.nav;
|
|
3042
|
+
if (nav) {
|
|
3043
|
+
nav.page = page;
|
|
3044
|
+
nav.clicks = clicks;
|
|
3045
|
+
nav.lastUpdate = {
|
|
3046
|
+
id: "slidev-mcp",
|
|
3047
|
+
type: "presenter",
|
|
3048
|
+
time
|
|
3049
|
+
};
|
|
3050
|
+
}
|
|
3051
|
+
for (const id of SERVER_REF_NAV_MODULE_IDS) {
|
|
3052
|
+
const module = server.moduleGraph.getModuleById(id);
|
|
3053
|
+
if (module) server.moduleGraph.invalidateModule(module);
|
|
3054
|
+
}
|
|
3055
|
+
for (const type of ["presenter", "viewer"]) server.hot.send({
|
|
3056
|
+
type: "custom",
|
|
3057
|
+
event: SERVER_REF_WS_EVENT,
|
|
3058
|
+
data: {
|
|
3059
|
+
key: "nav",
|
|
3060
|
+
source: "slidev-mcp",
|
|
3061
|
+
patch: {
|
|
3062
|
+
page,
|
|
3063
|
+
clicks,
|
|
3064
|
+
lastUpdate: {
|
|
3065
|
+
id: "slidev-mcp",
|
|
3066
|
+
type,
|
|
3067
|
+
time
|
|
3068
|
+
}
|
|
3069
|
+
},
|
|
3070
|
+
timestamp: time
|
|
3071
|
+
}
|
|
3072
|
+
});
|
|
3073
|
+
}
|
|
3074
|
+
//#endregion
|
|
2582
3075
|
//#region node/vite/monacoTypes.ts
|
|
2583
3076
|
function createMonacoTypesLoader({ userRoot, utils }) {
|
|
2584
3077
|
return {
|
|
@@ -2648,6 +3141,34 @@ function createPatchMonacoSourceMapPlugin() {
|
|
|
2648
3141
|
};
|
|
2649
3142
|
}
|
|
2650
3143
|
//#endregion
|
|
3144
|
+
//#region node/vite/pwa.ts
|
|
3145
|
+
function createPWAPlugin(options) {
|
|
3146
|
+
const matchMode = (mode) => mode === true || mode === options.mode;
|
|
3147
|
+
const enabled = matchMode(options.data.config.pwa);
|
|
3148
|
+
const base = options.base ?? "/";
|
|
3149
|
+
const title = options.data.config.title || "Slidev";
|
|
3150
|
+
return VitePWA({
|
|
3151
|
+
disable: !enabled,
|
|
3152
|
+
registerType: "autoUpdate",
|
|
3153
|
+
injectRegister: null,
|
|
3154
|
+
base,
|
|
3155
|
+
workbox: {
|
|
3156
|
+
maximumFileSizeToCacheInBytes: 100 * 1024 * 1024,
|
|
3157
|
+
globPatterns: ["**/*.{js,css,html,woff,woff2,ttf,png,jpg,jpeg,svg,gif,webp,avif,ico,mp4,webm,ogv,mov,m4v,mp3,wav,ogg,oga,m4a,aac,flac,opus,weba}"],
|
|
3158
|
+
navigateFallback: "index.html",
|
|
3159
|
+
navigateFallbackDenylist: [/^\/(presenter|notes|overview|print|export)/]
|
|
3160
|
+
},
|
|
3161
|
+
manifest: {
|
|
3162
|
+
name: title,
|
|
3163
|
+
short_name: "Slidev",
|
|
3164
|
+
start_url: base,
|
|
3165
|
+
display: "fullscreen",
|
|
3166
|
+
theme_color: "#121212",
|
|
3167
|
+
background_color: "#121212"
|
|
3168
|
+
}
|
|
3169
|
+
});
|
|
3170
|
+
}
|
|
3171
|
+
//#endregion
|
|
2651
3172
|
//#region node/vite/remoteAssets.ts
|
|
2652
3173
|
async function createRemoteAssetsPlugin({ data: { config }, mode }, pluginOptions) {
|
|
2653
3174
|
if (!(config.remoteAssets === true || config.remoteAssets === mode)) return;
|
|
@@ -2667,86 +3188,6 @@ async function createRemoteAssetsPlugin({ data: { config }, mode }, pluginOption
|
|
|
2667
3188
|
});
|
|
2668
3189
|
}
|
|
2669
3190
|
//#endregion
|
|
2670
|
-
//#region node/integrations/drawings.ts
|
|
2671
|
-
function resolveDrawingsDir(options) {
|
|
2672
|
-
return options.data.config.drawings.persist ? resolve(dirname(options.entry), options.data.config.drawings.persist) : void 0;
|
|
2673
|
-
}
|
|
2674
|
-
async function loadDrawings(options) {
|
|
2675
|
-
const dir = resolveDrawingsDir(options);
|
|
2676
|
-
if (!dir || !existsSync(dir)) return {};
|
|
2677
|
-
const files = await fg("*.svg", {
|
|
2678
|
-
onlyFiles: true,
|
|
2679
|
-
cwd: dir,
|
|
2680
|
-
absolute: true,
|
|
2681
|
-
suppressErrors: true
|
|
2682
|
-
});
|
|
2683
|
-
const obj = {};
|
|
2684
|
-
await Promise.all(files.map(async (path) => {
|
|
2685
|
-
const num = +basename(path, ".svg");
|
|
2686
|
-
if (Number.isNaN(num)) return;
|
|
2687
|
-
const lines = (await fs$1.readFile(path, "utf8")).split(/\n/g);
|
|
2688
|
-
obj[num.toString()] = lines.slice(1, -1).join("\n");
|
|
2689
|
-
}));
|
|
2690
|
-
return obj;
|
|
2691
|
-
}
|
|
2692
|
-
async function writeDrawings(options, drawing) {
|
|
2693
|
-
const dir = resolveDrawingsDir(options);
|
|
2694
|
-
if (!dir) return;
|
|
2695
|
-
const width = options.data.config.canvasWidth;
|
|
2696
|
-
const SVG_HEAD = `<svg width="${width}" height="${Math.round(width / options.data.config.aspectRatio)}" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">`;
|
|
2697
|
-
await fs$1.mkdir(dir, { recursive: true });
|
|
2698
|
-
return Promise.all(Object.entries(drawing).map(async ([key, value]) => {
|
|
2699
|
-
if (!value) return;
|
|
2700
|
-
const svg = `${SVG_HEAD}\n${value}\n</svg>`;
|
|
2701
|
-
await fs$1.writeFile(join(dir, `${key}.svg`), svg, "utf-8");
|
|
2702
|
-
}));
|
|
2703
|
-
}
|
|
2704
|
-
//#endregion
|
|
2705
|
-
//#region node/integrations/snapshots.ts
|
|
2706
|
-
function resolveSnapshotsDir(options) {
|
|
2707
|
-
return resolve(dirname(options.entry), ".slidev/snapshots");
|
|
2708
|
-
}
|
|
2709
|
-
async function loadSnapshots(options) {
|
|
2710
|
-
const dir = resolveSnapshotsDir(options);
|
|
2711
|
-
const file = join(dir, "snapshots.json");
|
|
2712
|
-
if (!dir || !existsSync(file)) return {};
|
|
2713
|
-
return JSON.parse(await fs$1.readFile(file, "utf8"));
|
|
2714
|
-
}
|
|
2715
|
-
async function writeSnapshots(options, data) {
|
|
2716
|
-
const dir = resolveSnapshotsDir(options);
|
|
2717
|
-
if (!dir) return;
|
|
2718
|
-
await fs$1.mkdir(dir, { recursive: true });
|
|
2719
|
-
await fs$1.writeFile(join(dir, "snapshots.json"), JSON.stringify(data, null, 2), "utf-8");
|
|
2720
|
-
}
|
|
2721
|
-
//#endregion
|
|
2722
|
-
//#region node/vite/serverRef.ts
|
|
2723
|
-
async function createServerRefPlugin(options, pluginOptions) {
|
|
2724
|
-
return ServerRef({
|
|
2725
|
-
debug: false,
|
|
2726
|
-
state: {
|
|
2727
|
-
sync: false,
|
|
2728
|
-
nav: {
|
|
2729
|
-
page: 0,
|
|
2730
|
-
clicks: 0,
|
|
2731
|
-
timer: {
|
|
2732
|
-
status: "stopped",
|
|
2733
|
-
slides: {},
|
|
2734
|
-
startedAt: 0,
|
|
2735
|
-
pausedAt: 0
|
|
2736
|
-
}
|
|
2737
|
-
},
|
|
2738
|
-
drawings: await loadDrawings(options),
|
|
2739
|
-
snapshots: await loadSnapshots(options),
|
|
2740
|
-
...pluginOptions.serverRef?.state
|
|
2741
|
-
},
|
|
2742
|
-
onChanged(key, data, patch, timestamp) {
|
|
2743
|
-
pluginOptions.serverRef?.onChanged?.(key, data, patch, timestamp);
|
|
2744
|
-
if (options.data.config.drawings.persist && key === "drawings") writeDrawings(options, patch ?? data);
|
|
2745
|
-
if (key === "snapshots") writeSnapshots(options, data);
|
|
2746
|
-
}
|
|
2747
|
-
});
|
|
2748
|
-
}
|
|
2749
|
-
//#endregion
|
|
2750
3191
|
//#region node/vite/staticCopy.ts
|
|
2751
3192
|
async function createStaticCopyPlugin({ themeRoots, addonRoots }, pluginOptions) {
|
|
2752
3193
|
const publicDirs = [...themeRoots, ...addonRoots].map((i) => join(i, "public")).filter(existsSync);
|
|
@@ -2805,7 +3246,7 @@ const RE_VUE_QUERY_VUE = /\.vue\?vue/;
|
|
|
2805
3246
|
const RE_VUE_QUERY_V = /\.vue\?v=/;
|
|
2806
3247
|
const RE_MD_EXT = /\.md$/;
|
|
2807
3248
|
const RE_MD_QUERY_VUE = /\.md\?vue/;
|
|
2808
|
-
const customElements = new Set([
|
|
3249
|
+
const customElements = /* @__PURE__ */ new Set([
|
|
2809
3250
|
"annotation",
|
|
2810
3251
|
"math",
|
|
2811
3252
|
"menclose",
|
|
@@ -2863,6 +3304,7 @@ async function createVuePlugin(_options, pluginOptions) {
|
|
|
2863
3304
|
function ViteSlidevPlugin(options, pluginOptions = {}, serverOptions = {}) {
|
|
2864
3305
|
return Promise.all([
|
|
2865
3306
|
createSlidesLoader(options, serverOptions),
|
|
3307
|
+
createMcpPlugin(options),
|
|
2866
3308
|
createMarkdownPlugin(options, pluginOptions),
|
|
2867
3309
|
createLayoutWrapperPlugin(options),
|
|
2868
3310
|
createContextInjectionPlugin(),
|
|
@@ -2881,6 +3323,7 @@ function ViteSlidevPlugin(options, pluginOptions = {}, serverOptions = {}) {
|
|
|
2881
3323
|
createStaticCopyPlugin(options, pluginOptions),
|
|
2882
3324
|
createInspectPlugin(options, pluginOptions),
|
|
2883
3325
|
createPatchMonacoSourceMapPlugin(),
|
|
3326
|
+
createPWAPlugin(options),
|
|
2884
3327
|
setupVitePlugins(options)
|
|
2885
3328
|
]);
|
|
2886
3329
|
}
|
|
@@ -2920,4 +3363,4 @@ async function createServer$1(options, viteConfig = {}, serverOptions) {
|
|
|
2920
3363
|
return await createServer(await resolveViteConfigs(options, { optimizeDeps: { entries: [join(options.clientRoot, "main.ts")] } }, viteConfig, "serve", serverOptions));
|
|
2921
3364
|
}
|
|
2922
3365
|
//#endregion
|
|
2923
|
-
export {
|
|
3366
|
+
export { createDataUtils as a, getThemeMeta as c, loadSetups as d, updateFrontmatterPatch as f, createSlidevMcpServer as i, resolveTheme as l, resolveViteConfigs as n, resolveOptions as o, ViteSlidevPlugin as r, parser as s, createServer$1 as t, version as u };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { n as getRoots } from "./resolver-BBT539iz.mjs";
|
|
2
|
+
import { i as createSlidevMcpServer, s as parser, u as version } from "./serve-CgOXLPUI.mjs";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
//#region node/mcp/stdio.ts
|
|
5
|
+
/**
|
|
6
|
+
* Start a Slidev MCP server over stdio, operating on the markdown files
|
|
7
|
+
* directly (no dev server required, so no live-navigation tool).
|
|
8
|
+
*
|
|
9
|
+
* Note: in stdio mode, stdout is reserved for the MCP protocol — nothing
|
|
10
|
+
* else may be printed to it.
|
|
11
|
+
*/
|
|
12
|
+
async function startMcpStdioServer(entry) {
|
|
13
|
+
const { userRoot } = await getRoots(entry);
|
|
14
|
+
await createSlidevMcpServer({
|
|
15
|
+
version,
|
|
16
|
+
entry,
|
|
17
|
+
getData: () => parser.load({
|
|
18
|
+
userRoot,
|
|
19
|
+
roots: [userRoot]
|
|
20
|
+
}, entry)
|
|
21
|
+
}).connect(new StdioServerTransport());
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
export { startMcpStdioServer };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@slidev/cli",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "52.
|
|
4
|
+
"version": "52.17.0",
|
|
5
5
|
"description": "Presentation slides for developers",
|
|
6
6
|
"author": "Anthony Fu <anthonyfu117@hotmail.com>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -43,21 +43,22 @@
|
|
|
43
43
|
}
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@antfu/ni": "^30.
|
|
46
|
+
"@antfu/ni": "^30.2.0",
|
|
47
47
|
"@antfu/utils": "^9.3.0",
|
|
48
48
|
"@comark/markdown-it": "^0.3.4",
|
|
49
|
-
"@iconify-json/carbon": "^1.2.
|
|
49
|
+
"@iconify-json/carbon": "^1.2.24",
|
|
50
50
|
"@iconify-json/ph": "^1.2.2",
|
|
51
51
|
"@iconify-json/svg-spinners": "^1.2.4",
|
|
52
52
|
"@lillallol/outline-pdf": "^4.0.0",
|
|
53
|
-
"@
|
|
54
|
-
"@shikijs/
|
|
55
|
-
"@shikijs/
|
|
56
|
-
"@shikijs/
|
|
57
|
-
"@
|
|
58
|
-
"@unocss/
|
|
53
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
54
|
+
"@shikijs/magic-move": "^4.3.1",
|
|
55
|
+
"@shikijs/markdown-it": "^4.3.1",
|
|
56
|
+
"@shikijs/twoslash": "^4.3.1",
|
|
57
|
+
"@shikijs/vitepress-twoslash": "^4.3.1",
|
|
58
|
+
"@unocss/extractor-mdc": "^66.7.5",
|
|
59
|
+
"@unocss/reset": "^66.7.5",
|
|
59
60
|
"@vitejs/plugin-vue": "^6.0.7",
|
|
60
|
-
"@vitejs/plugin-vue-jsx": "^5.1.
|
|
61
|
+
"@vitejs/plugin-vue-jsx": "^5.1.6",
|
|
61
62
|
"ansis": "^4.3.1",
|
|
62
63
|
"chokidar": "^5.0.0",
|
|
63
64
|
"cli-progress": "^3.12.0",
|
|
@@ -74,15 +75,15 @@
|
|
|
74
75
|
"lz-string": "^1.5.0",
|
|
75
76
|
"magic-string": "^0.30.21",
|
|
76
77
|
"magic-string-stack": "^1.1.0",
|
|
77
|
-
"markdown-exit": "^1.
|
|
78
|
+
"markdown-exit": "^1.1.0-beta.2",
|
|
78
79
|
"markdown-it-footnote": "^4.0.0",
|
|
79
80
|
"markdown-it-github-alerts": "^1.0.1",
|
|
80
81
|
"mlly": "^1.8.2",
|
|
81
82
|
"monaco-editor": "^0.55.1",
|
|
82
|
-
"obug": "^2.1.
|
|
83
|
+
"obug": "^2.1.3",
|
|
83
84
|
"open": "^11.0.0",
|
|
84
85
|
"pdf-lib": "^1.17.1",
|
|
85
|
-
"picomatch": "^4.0.
|
|
86
|
+
"picomatch": "^4.0.5",
|
|
86
87
|
"plantuml-encoder": "^1.4.0",
|
|
87
88
|
"postcss-nested": "^7.0.2",
|
|
88
89
|
"pptxgenjs": "^4.0.1",
|
|
@@ -90,30 +91,32 @@
|
|
|
90
91
|
"public-ip": "^8.0.0",
|
|
91
92
|
"resolve-from": "^5.0.0",
|
|
92
93
|
"resolve-global": "^2.0.0",
|
|
93
|
-
"semver": "^7.8.
|
|
94
|
-
"shiki": "^4.
|
|
94
|
+
"semver": "^7.8.5",
|
|
95
|
+
"shiki": "^4.3.1",
|
|
95
96
|
"sirv": "^3.0.2",
|
|
96
97
|
"source-map-js": "^1.2.1",
|
|
97
98
|
"typescript": "^6.0.3",
|
|
98
|
-
"unhead": "^3.1.
|
|
99
|
-
"unocss": "^66.7.
|
|
99
|
+
"unhead": "^3.1.7",
|
|
100
|
+
"unocss": "^66.7.5",
|
|
100
101
|
"unplugin-icons": "^23.0.1",
|
|
101
102
|
"unplugin-vue-components": "^32.1.0",
|
|
102
103
|
"unplugin-vue-markdown": "^32.0.0",
|
|
103
104
|
"untun": "^0.1.3",
|
|
104
105
|
"uqr": "^0.1.3",
|
|
105
|
-
"vite": "^8.
|
|
106
|
+
"vite": "^8.1.3",
|
|
106
107
|
"vite-plugin-inspect": "^11.4.1",
|
|
108
|
+
"vite-plugin-pwa": "^1.3.0",
|
|
107
109
|
"vite-plugin-remote-assets": "^2.1.0",
|
|
108
|
-
"vite-plugin-static-copy": "^4.1.
|
|
110
|
+
"vite-plugin-static-copy": "^4.1.1",
|
|
109
111
|
"vite-plugin-vue-server-ref": "^1.0.0",
|
|
110
112
|
"vitefu": "^1.1.3",
|
|
111
|
-
"vue": "^3.5.
|
|
113
|
+
"vue": "^3.5.39",
|
|
112
114
|
"yaml": "^2.9.0",
|
|
113
115
|
"yargs": "^18.0.0",
|
|
114
|
-
"
|
|
115
|
-
"@slidev/
|
|
116
|
-
"@slidev/
|
|
116
|
+
"zod": "^3.25.76",
|
|
117
|
+
"@slidev/parser": "52.17.0",
|
|
118
|
+
"@slidev/client": "52.17.0",
|
|
119
|
+
"@slidev/types": "52.17.0"
|
|
117
120
|
},
|
|
118
121
|
"devDependencies": {
|
|
119
122
|
"@hedgedoc/markdown-it-plugins": "^2.1.4",
|
package/skills/slidev/SKILL.md
CHANGED
|
@@ -158,6 +158,7 @@ Presenter notes go here
|
|
|
158
158
|
| VS Code extension | Install `antfu.slidev` | [editor-vscode](references/editor-vscode.md) |
|
|
159
159
|
| Prettier | `prettier-plugin-slidev` | [editor-prettier](references/editor-prettier.md) |
|
|
160
160
|
| Eject theme | `slidev theme eject` | [tool-eject-theme](references/tool-eject-theme.md) |
|
|
161
|
+
| MCP server (AI agents) | `http://localhost:<port>/__mcp` or `slidev mcp` | [tool-mcp](references/tool-mcp.md) |
|
|
161
162
|
|
|
162
163
|
### Lifecycle & API
|
|
163
164
|
|
|
@@ -93,6 +93,14 @@ slidev format [entry]
|
|
|
93
93
|
|
|
94
94
|
Formats the slides markdown file.
|
|
95
95
|
|
|
96
|
+
## MCP Server
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
slidev mcp [entry]
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Starts an MCP (Model Context Protocol) server over stdio for AI agents to inspect and edit the slides. When a dev server is running, an HTTP MCP endpoint is also available at `http://localhost:<port>/__mcp`. See [tool-mcp](tool-mcp.md).
|
|
103
|
+
|
|
96
104
|
## Theme Eject
|
|
97
105
|
|
|
98
106
|
```bash
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mcp
|
|
3
|
+
description: Built-in MCP server for AI agents to inspect, edit, reorder, and navigate slides
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# MCP Server
|
|
7
|
+
|
|
8
|
+
Slidev ships a built-in MCP (Model Context Protocol) server exposing structured tools to read, edit, reorder, and navigate a deck. Prefer these tools over raw text edits for slide-level operations (update/insert/remove/move) — they handle Slidev's compound separators correctly and hot-reload the presentation.
|
|
9
|
+
|
|
10
|
+
## Usage
|
|
11
|
+
|
|
12
|
+
With a running dev server (streamable HTTP):
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
http://localhost:<port>/__mcp
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# e.g. for Claude Code
|
|
20
|
+
claude mcp add --transport http slidev http://localhost:3030/__mcp
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Standalone over stdio (no dev server, operates on files directly):
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
slidev mcp [entry]
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Tools
|
|
30
|
+
|
|
31
|
+
| Tool | Description |
|
|
32
|
+
| --- | --- |
|
|
33
|
+
| `slidev-get-info` | Deck overview: entry, title, slide count, markdown files, server URL, current position |
|
|
34
|
+
| `slidev-list-slides` | All slides with number, title, layout, source file |
|
|
35
|
+
| `slidev-get-slide` | Full source of one slide: frontmatter, content, note |
|
|
36
|
+
| `slidev-update-slide` | Update content, note, and/or frontmatter of a slide |
|
|
37
|
+
| `slidev-insert-slide` | Insert a new slide after an existing one |
|
|
38
|
+
| `slidev-remove-slide` | Remove a slide |
|
|
39
|
+
| `slidev-move-slide` | Move a slide before/after another to reorder the deck |
|
|
40
|
+
| `slidev-goto-slide` | Navigate the live presentation to a slide (dev server only) |
|
|
41
|
+
|
|
42
|
+
## Behavior
|
|
43
|
+
|
|
44
|
+
- Slides are addressed by rendered 1-based numbers (as shown in the presentation)
|
|
45
|
+
- Edits are saved to the markdown files; a running dev server hot-reloads instantly
|
|
46
|
+
- The first slide of the entry file cannot be removed/moved (its frontmatter is the deck headmatter)
|
|
47
|
+
- Slides imported via `src:` are edited in their own file; moves cannot cross files
|
|
48
|
+
- Disable with `mcp: false` in the headmatter
|