slidev-addon-agent 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +54 -0
- package/agent/constants.ts +119 -0
- package/agent/deck-context.ts +67 -0
- package/agent/index.ts +201 -0
- package/agent/middleware.ts +163 -0
- package/agent/skills/slidev/README.md +61 -0
- package/agent/skills/slidev/SKILL.md +189 -0
- package/agent/skills/slidev/references/animation-click-marker.md +37 -0
- package/agent/skills/slidev/references/animation-drawing.md +68 -0
- package/agent/skills/slidev/references/animation-rough-marker.md +53 -0
- package/agent/skills/slidev/references/api-slide-hooks.md +37 -0
- package/agent/skills/slidev/references/build-og-image.md +36 -0
- package/agent/skills/slidev/references/build-pdf.md +40 -0
- package/agent/skills/slidev/references/build-remote-assets.md +34 -0
- package/agent/skills/slidev/references/build-seo-meta.md +43 -0
- package/agent/skills/slidev/references/code-groups.md +64 -0
- package/agent/skills/slidev/references/code-import-snippet.md +55 -0
- package/agent/skills/slidev/references/code-line-highlighting.md +50 -0
- package/agent/skills/slidev/references/code-line-numbers.md +46 -0
- package/agent/skills/slidev/references/code-magic-move.md +57 -0
- package/agent/skills/slidev/references/code-max-height.md +37 -0
- package/agent/skills/slidev/references/code-twoslash.md +42 -0
- package/agent/skills/slidev/references/core-animations.md +196 -0
- package/agent/skills/slidev/references/core-cli.md +140 -0
- package/agent/skills/slidev/references/core-components.md +197 -0
- package/agent/skills/slidev/references/core-exporting.md +148 -0
- package/agent/skills/slidev/references/core-frontmatter.md +195 -0
- package/agent/skills/slidev/references/core-global-context.md +155 -0
- package/agent/skills/slidev/references/core-headmatter.md +188 -0
- package/agent/skills/slidev/references/core-hosting.md +152 -0
- package/agent/skills/slidev/references/core-layouts.md +286 -0
- package/agent/skills/slidev/references/core-syntax.md +155 -0
- package/agent/skills/slidev/references/diagram-latex.md +55 -0
- package/agent/skills/slidev/references/diagram-mermaid.md +44 -0
- package/agent/skills/slidev/references/diagram-plantuml.md +45 -0
- package/agent/skills/slidev/references/editor-monaco-run.md +44 -0
- package/agent/skills/slidev/references/editor-monaco-write.md +24 -0
- package/agent/skills/slidev/references/editor-monaco.md +50 -0
- package/agent/skills/slidev/references/editor-prettier.md +40 -0
- package/agent/skills/slidev/references/editor-side.md +23 -0
- package/agent/skills/slidev/references/editor-vscode.md +55 -0
- package/agent/skills/slidev/references/layout-canvas-size.md +25 -0
- package/agent/skills/slidev/references/layout-draggable.md +57 -0
- package/agent/skills/slidev/references/layout-global-layers.md +50 -0
- package/agent/skills/slidev/references/layout-slots.md +75 -0
- package/agent/skills/slidev/references/layout-transform.md +33 -0
- package/agent/skills/slidev/references/layout-zoom.md +39 -0
- package/agent/skills/slidev/references/presenter-notes-ruby.md +35 -0
- package/agent/skills/slidev/references/presenter-recording.md +30 -0
- package/agent/skills/slidev/references/presenter-remote.md +40 -0
- package/agent/skills/slidev/references/presenter-timer.md +34 -0
- package/agent/skills/slidev/references/style-direction.md +34 -0
- package/agent/skills/slidev/references/style-icons.md +46 -0
- package/agent/skills/slidev/references/style-scoped.md +50 -0
- package/agent/skills/slidev/references/syntax-block-frontmatter.md +39 -0
- package/agent/skills/slidev/references/syntax-frontmatter-merging.md +49 -0
- package/agent/skills/slidev/references/syntax-importing-slides.md +60 -0
- package/agent/skills/slidev/references/syntax-mdc.md +51 -0
- package/agent/skills/slidev/references/tool-eject-theme.md +27 -0
- package/agent/tools/export-tool.ts +216 -0
- package/agent/tools/review-tool.ts +136 -0
- package/app/index.ts +124 -0
- package/components/MessageItem.vue +231 -0
- package/components/SlidevAgentNavButton.vue +48 -0
- package/components/SlidevAgentSidebar.vue +766 -0
- package/components/SubagentCard.vue +184 -0
- package/components/TypingDots.vue +62 -0
- package/dist/agent/constants.js +117 -0
- package/dist/agent/deck-context.js +47 -0
- package/dist/agent/index.js +167 -0
- package/dist/agent/middleware.js +134 -0
- package/dist/agent/slide-preview-tool.js +257 -0
- package/dist/agent/tools/export-tool.js +167 -0
- package/dist/agent/tools/review-tool.js +111 -0
- package/dist/app/index.js +101 -0
- package/dist/bin/slidev-agent.js +155 -0
- package/dist/lib/bridge.js +151 -0
- package/dist/lib/env.js +17 -0
- package/dist/lib/headless-tools.js +10 -0
- package/dist/lib/langgraph-init.js +59 -0
- package/dist/lib/review-tool.js +98 -0
- package/lib/bridge.ts +212 -0
- package/lib/config.ts +79 -0
- package/lib/env.ts +38 -0
- package/lib/headless-tool-impl.ts +26 -0
- package/lib/headless-tools.ts +11 -0
- package/lib/langgraph-init.ts +79 -0
- package/lib/messages.ts +169 -0
- package/lib/render-chat-markdown.ts +19 -0
- package/lib/sidebar.ts +573 -0
- package/lib/state.ts +44 -0
- package/package.json +65 -0
- package/public/deepagents.svg +12 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { readFile, stat } from "node:fs/promises";
|
|
3
|
+
import { Hono } from "hono";
|
|
4
|
+
import { env } from "../lib/env.js";
|
|
5
|
+
function resolveRootDir() {
|
|
6
|
+
return path.resolve(env(process.env, "SLIDEV_AGENT_ROOT_DIR") || process.cwd());
|
|
7
|
+
}
|
|
8
|
+
function normalizeBasePath(basePath) {
|
|
9
|
+
if (!basePath.startsWith("/"))
|
|
10
|
+
return `/${basePath}`;
|
|
11
|
+
return basePath.replace(/\/+$/, "");
|
|
12
|
+
}
|
|
13
|
+
function getMimeType(filePath) {
|
|
14
|
+
const extension = path.extname(filePath).toLowerCase();
|
|
15
|
+
switch (extension) {
|
|
16
|
+
case ".html":
|
|
17
|
+
return "text/html; charset=utf-8";
|
|
18
|
+
case ".js":
|
|
19
|
+
return "application/javascript; charset=utf-8";
|
|
20
|
+
case ".mjs":
|
|
21
|
+
return "application/javascript; charset=utf-8";
|
|
22
|
+
case ".css":
|
|
23
|
+
return "text/css; charset=utf-8";
|
|
24
|
+
case ".json":
|
|
25
|
+
return "application/json; charset=utf-8";
|
|
26
|
+
case ".svg":
|
|
27
|
+
return "image/svg+xml";
|
|
28
|
+
case ".png":
|
|
29
|
+
return "image/png";
|
|
30
|
+
case ".jpg":
|
|
31
|
+
case ".jpeg":
|
|
32
|
+
return "image/jpeg";
|
|
33
|
+
case ".gif":
|
|
34
|
+
return "image/gif";
|
|
35
|
+
case ".webp":
|
|
36
|
+
return "image/webp";
|
|
37
|
+
case ".woff":
|
|
38
|
+
return "font/woff";
|
|
39
|
+
case ".woff2":
|
|
40
|
+
return "font/woff2";
|
|
41
|
+
default:
|
|
42
|
+
return "application/octet-stream";
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function exists(filePath) {
|
|
46
|
+
try {
|
|
47
|
+
await stat(filePath);
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async function proxyToDevServer(requestUrl, assetPath) {
|
|
55
|
+
const devServerUrl = new URL(env(process.env, "SLIDEV_AGENT_DEV_URL", "http://localhost:3030"));
|
|
56
|
+
const targetUrl = new URL(assetPath || "/", devServerUrl);
|
|
57
|
+
targetUrl.search = requestUrl.search;
|
|
58
|
+
const response = await fetch(targetUrl);
|
|
59
|
+
const body = await response.arrayBuffer();
|
|
60
|
+
return new Response(body, {
|
|
61
|
+
status: response.status,
|
|
62
|
+
headers: response.headers,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
const app = new Hono();
|
|
66
|
+
const basePath = normalizeBasePath(env(process.env, "SLIDEV_AGENT_APP_BASE_PATH", "/slidev-agent"));
|
|
67
|
+
app.get("/", c => c.redirect(`${basePath}/`, 302));
|
|
68
|
+
app.get(basePath, c => c.redirect(`${basePath}/`, 302));
|
|
69
|
+
app.get(`${basePath}/*`, async (c) => {
|
|
70
|
+
const requestUrl = new URL(c.req.url);
|
|
71
|
+
const assetPath = requestUrl.pathname.slice(basePath.length) || "/";
|
|
72
|
+
const distRoot = path.join(resolveRootDir(), "dist");
|
|
73
|
+
if (await exists(path.join(distRoot, "index.html"))) {
|
|
74
|
+
const candidatePath = path.normalize(path.join(distRoot, assetPath));
|
|
75
|
+
const safeRoot = `${distRoot}${path.sep}`;
|
|
76
|
+
const safeCandidate = candidatePath === distRoot || candidatePath.startsWith(safeRoot);
|
|
77
|
+
if (!safeCandidate)
|
|
78
|
+
return c.text("Invalid asset path", 400);
|
|
79
|
+
const isSpaRoute = !path.extname(assetPath);
|
|
80
|
+
const filePath = await exists(candidatePath)
|
|
81
|
+
? candidatePath
|
|
82
|
+
: isSpaRoute
|
|
83
|
+
? path.join(distRoot, "index.html")
|
|
84
|
+
: "";
|
|
85
|
+
if (!filePath)
|
|
86
|
+
return c.text("Asset not found", 404);
|
|
87
|
+
const content = await readFile(filePath);
|
|
88
|
+
return new Response(content, {
|
|
89
|
+
headers: {
|
|
90
|
+
"content-type": getMimeType(filePath),
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
return await proxyToDevServer(requestUrl, assetPath);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return c.text(`Slidev app is not available yet. Start \`slidev-agent dev\` or build the deck so LangGraph can serve it from ${basePath}/.`, 503);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
export { app };
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { stat } from "node:fs/promises";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { writeLanggraphJsonIfMissing } from "../lib/langgraph-init.js";
|
|
8
|
+
import { pullRemoteSlides, pushRemoteSlides, resolveSlideEntry } from "../lib/bridge.js";
|
|
9
|
+
const [, , command = "dev", ...rest] = process.argv;
|
|
10
|
+
const require = createRequire(import.meta.url);
|
|
11
|
+
function getNpxCommand() {
|
|
12
|
+
return process.platform === "win32" ? "npx.cmd" : "npx";
|
|
13
|
+
}
|
|
14
|
+
function spawnCommand(commandName, args, options = {}) {
|
|
15
|
+
return spawn(commandName, args, {
|
|
16
|
+
cwd: process.cwd(),
|
|
17
|
+
stdio: "inherit",
|
|
18
|
+
...options,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
function resolvePackageBin(packageName, preferredBinName) {
|
|
22
|
+
const packageJsonPath = require.resolve(`${packageName}/package.json`);
|
|
23
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
24
|
+
const packageRoot = path.dirname(packageJsonPath);
|
|
25
|
+
if (typeof packageJson.bin === "string")
|
|
26
|
+
return path.resolve(packageRoot, packageJson.bin);
|
|
27
|
+
if (packageJson.bin && typeof packageJson.bin === "object") {
|
|
28
|
+
const selectedBin = preferredBinName
|
|
29
|
+
? packageJson.bin[preferredBinName]
|
|
30
|
+
: Object.values(packageJson.bin)[0];
|
|
31
|
+
if (typeof selectedBin === "string")
|
|
32
|
+
return path.resolve(packageRoot, selectedBin);
|
|
33
|
+
}
|
|
34
|
+
throw new Error(`Could not resolve a binary for ${packageName}`);
|
|
35
|
+
}
|
|
36
|
+
async function fileExists(filePath) {
|
|
37
|
+
try {
|
|
38
|
+
await stat(filePath);
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function ensureLanggraphJsonForDev(cwd) {
|
|
46
|
+
if (process.env.SLIDEV_AGENT_DISABLE_LANGGRAPH === "1")
|
|
47
|
+
return;
|
|
48
|
+
try {
|
|
49
|
+
writeLanggraphJsonIfMissing(cwd);
|
|
50
|
+
}
|
|
51
|
+
catch (e) {
|
|
52
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
53
|
+
console.warn(`slidev-agent: could not create langgraph.json (${msg}). LangGraph dev will not start.`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function startLangGraphDev() {
|
|
57
|
+
if (process.env.SLIDEV_AGENT_DISABLE_LANGGRAPH === "1")
|
|
58
|
+
return null;
|
|
59
|
+
const configPath = path.join(process.cwd(), "langgraph.json");
|
|
60
|
+
if (!await fileExists(configPath))
|
|
61
|
+
return null;
|
|
62
|
+
const langgraphBin = resolvePackageBin("@langchain/langgraph-cli", "langgraphjs");
|
|
63
|
+
return spawnCommand(process.execPath, [
|
|
64
|
+
langgraphBin,
|
|
65
|
+
"dev",
|
|
66
|
+
"--config",
|
|
67
|
+
"langgraph.json",
|
|
68
|
+
"--no-browser",
|
|
69
|
+
]);
|
|
70
|
+
}
|
|
71
|
+
/** Resolves when `child` has exited (immediately if it already has). */
|
|
72
|
+
function onceChildExit(child) {
|
|
73
|
+
return new Promise((resolve) => {
|
|
74
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
75
|
+
resolve();
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
child.once("exit", () => resolve());
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
const CHILD_SHUTDOWN_TIMEOUT_MS = 10_000;
|
|
82
|
+
async function waitForChildrenThenExit(slidevChild, langgraphChild, exitCode) {
|
|
83
|
+
const waitFor = [onceChildExit(slidevChild)];
|
|
84
|
+
if (langgraphChild)
|
|
85
|
+
waitFor.push(onceChildExit(langgraphChild));
|
|
86
|
+
const timeout = setTimeout(() => {
|
|
87
|
+
langgraphChild?.kill("SIGKILL");
|
|
88
|
+
slidevChild.kill("SIGKILL");
|
|
89
|
+
}, CHILD_SHUTDOWN_TIMEOUT_MS);
|
|
90
|
+
try {
|
|
91
|
+
await Promise.all(waitFor);
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
clearTimeout(timeout);
|
|
95
|
+
}
|
|
96
|
+
process.exit(exitCode);
|
|
97
|
+
}
|
|
98
|
+
async function run() {
|
|
99
|
+
if (command === "sync") {
|
|
100
|
+
const subcommand = rest[0] || "pull";
|
|
101
|
+
if (subcommand === "pull") {
|
|
102
|
+
const result = await pullRemoteSlides(process.cwd());
|
|
103
|
+
if (result.mode === "local")
|
|
104
|
+
console.log(`No remote bridge configured, using local entry at ${result.entry}`);
|
|
105
|
+
else
|
|
106
|
+
console.log(`Pulled ${result.manifest.files.length} files into ${result.manifest.generatedRoot}`);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (subcommand === "push") {
|
|
110
|
+
await pushRemoteSlides(process.cwd());
|
|
111
|
+
console.log("Pushed generated slides back to the bridge.");
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
throw new Error(`Unknown sync subcommand: ${subcommand}`);
|
|
115
|
+
}
|
|
116
|
+
const { entry } = await resolveSlideEntry(process.cwd());
|
|
117
|
+
const slidevArgs = command === "dev"
|
|
118
|
+
? [entry, ...rest]
|
|
119
|
+
: [command, entry, ...rest];
|
|
120
|
+
if (command === "dev")
|
|
121
|
+
ensureLanggraphJsonForDev(process.cwd());
|
|
122
|
+
const langgraphChild = command === "dev" ? await startLangGraphDev() : null;
|
|
123
|
+
const slidevChild = spawnCommand(getNpxCommand(), ["slidev", ...slidevArgs]);
|
|
124
|
+
let isShuttingDown = false;
|
|
125
|
+
function shutdown(exitCode = 0) {
|
|
126
|
+
if (isShuttingDown) {
|
|
127
|
+
// Second Ctrl+C (or SIGTERM) while waiting for children: force quit.
|
|
128
|
+
langgraphChild?.kill("SIGKILL");
|
|
129
|
+
slidevChild.kill("SIGKILL");
|
|
130
|
+
process.exit(exitCode);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
isShuttingDown = true;
|
|
134
|
+
langgraphChild?.kill("SIGTERM");
|
|
135
|
+
slidevChild.kill("SIGTERM");
|
|
136
|
+
void waitForChildrenThenExit(slidevChild, langgraphChild, exitCode);
|
|
137
|
+
}
|
|
138
|
+
process.on("SIGINT", () => shutdown(0));
|
|
139
|
+
process.on("SIGTERM", () => shutdown(0));
|
|
140
|
+
langgraphChild?.on("exit", (code) => {
|
|
141
|
+
if (isShuttingDown)
|
|
142
|
+
return;
|
|
143
|
+
if ((code ?? 0) !== 0)
|
|
144
|
+
shutdown(code ?? 1);
|
|
145
|
+
});
|
|
146
|
+
slidevChild.on("exit", (code) => {
|
|
147
|
+
if (isShuttingDown)
|
|
148
|
+
return;
|
|
149
|
+
shutdown(code ?? 1);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
run().catch((error) => {
|
|
153
|
+
console.error(error instanceof Error ? error.message : error);
|
|
154
|
+
process.exit(1);
|
|
155
|
+
});
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { env } from "./env.js";
|
|
4
|
+
const GENERATED_ROOT = ".slidev-agent/generated";
|
|
5
|
+
const MANIFEST_PATH = ".slidev-agent/manifest.json";
|
|
6
|
+
function resolveBridgeConfig(cwd) {
|
|
7
|
+
return {
|
|
8
|
+
entry: env(process.env, "SLIDEV_AGENT_ENTRY", "slides.md"),
|
|
9
|
+
routePrefix: env(process.env, "SLIDEV_AGENT_ROUTE_PREFIX", "/slidev-agent"),
|
|
10
|
+
cwd,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
function isRemoteBridgeEnabled(config) {
|
|
14
|
+
return Boolean(config.apiUrl && config.deckId);
|
|
15
|
+
}
|
|
16
|
+
function normalizeRoutePrefix(routePrefix) {
|
|
17
|
+
if (!routePrefix.startsWith("/"))
|
|
18
|
+
return `/${routePrefix}`;
|
|
19
|
+
return routePrefix.replace(/\/$/, "");
|
|
20
|
+
}
|
|
21
|
+
function createDeckUrl(config) {
|
|
22
|
+
if (!config.apiUrl || !config.deckId)
|
|
23
|
+
return undefined;
|
|
24
|
+
const prefix = normalizeRoutePrefix(config.routePrefix);
|
|
25
|
+
const url = new URL(`${prefix}/decks/${encodeURIComponent(config.deckId)}`, config.apiUrl);
|
|
26
|
+
if (config.namespace)
|
|
27
|
+
url.searchParams.set("namespace", config.namespace);
|
|
28
|
+
return url;
|
|
29
|
+
}
|
|
30
|
+
function normalizePayload(payload, fallbackEntry) {
|
|
31
|
+
const entry = typeof payload.entry === "string" && payload.entry
|
|
32
|
+
? payload.entry
|
|
33
|
+
: fallbackEntry;
|
|
34
|
+
if (Array.isArray(payload.files)) {
|
|
35
|
+
return {
|
|
36
|
+
entry,
|
|
37
|
+
files: payload.files.map((file) => ({
|
|
38
|
+
path: file.path,
|
|
39
|
+
content: file.content,
|
|
40
|
+
})),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
if (payload.slides && typeof payload.slides === "object") {
|
|
44
|
+
return {
|
|
45
|
+
entry,
|
|
46
|
+
files: Object.entries(payload.slides).map(([filePath, content]) => ({
|
|
47
|
+
path: filePath,
|
|
48
|
+
content: String(content),
|
|
49
|
+
})),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
throw new Error("Unsupported response shape. Expected `files` or `slides` in the bridge response.");
|
|
53
|
+
}
|
|
54
|
+
async function ensureParentDirectory(targetFile) {
|
|
55
|
+
await mkdir(path.dirname(targetFile), { recursive: true });
|
|
56
|
+
}
|
|
57
|
+
async function writeManifest(cwd, manifest) {
|
|
58
|
+
const manifestFile = path.join(cwd, MANIFEST_PATH);
|
|
59
|
+
await ensureParentDirectory(manifestFile);
|
|
60
|
+
await writeFile(manifestFile, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
61
|
+
}
|
|
62
|
+
async function readManifest(cwd) {
|
|
63
|
+
const manifestFile = path.join(cwd, MANIFEST_PATH);
|
|
64
|
+
const content = await readFile(manifestFile, "utf8");
|
|
65
|
+
return JSON.parse(content);
|
|
66
|
+
}
|
|
67
|
+
export async function pullRemoteSlides(cwd = process.cwd()) {
|
|
68
|
+
const config = resolveBridgeConfig(cwd);
|
|
69
|
+
if (!isRemoteBridgeEnabled(config)) {
|
|
70
|
+
return {
|
|
71
|
+
mode: "local",
|
|
72
|
+
entry: path.join(cwd, config.entry),
|
|
73
|
+
manifest: null,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const deckUrl = createDeckUrl(config);
|
|
77
|
+
if (!deckUrl) {
|
|
78
|
+
throw new Error("Remote sync is not configured. Set SLIDEV_AGENT_API_URL and SLIDEV_AGENT_DECK_ID.");
|
|
79
|
+
}
|
|
80
|
+
const response = await fetch(deckUrl, {
|
|
81
|
+
headers: {
|
|
82
|
+
accept: "application/json",
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
if (!response.ok)
|
|
86
|
+
throw new Error(`Bridge pull failed with ${response.status} ${response.statusText}`);
|
|
87
|
+
const payload = normalizePayload(await response.json(), config.entry);
|
|
88
|
+
const generatedRoot = path.join(cwd, GENERATED_ROOT);
|
|
89
|
+
await rm(generatedRoot, { recursive: true, force: true });
|
|
90
|
+
await mkdir(generatedRoot, { recursive: true });
|
|
91
|
+
for (const file of payload.files) {
|
|
92
|
+
const outputFile = path.join(generatedRoot, file.path);
|
|
93
|
+
await ensureParentDirectory(outputFile);
|
|
94
|
+
await writeFile(outputFile, file.content, "utf8");
|
|
95
|
+
}
|
|
96
|
+
const manifest = {
|
|
97
|
+
entry: payload.entry,
|
|
98
|
+
generatedRoot: GENERATED_ROOT,
|
|
99
|
+
files: payload.files.map(file => file.path),
|
|
100
|
+
};
|
|
101
|
+
await writeManifest(cwd, manifest);
|
|
102
|
+
return {
|
|
103
|
+
mode: "remote",
|
|
104
|
+
entry: path.join(generatedRoot, payload.entry),
|
|
105
|
+
manifest,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
export async function pushRemoteSlides(cwd = process.cwd()) {
|
|
109
|
+
const config = resolveBridgeConfig(cwd);
|
|
110
|
+
if (!isRemoteBridgeEnabled(config))
|
|
111
|
+
throw new Error("Remote sync is not configured. Set SLIDEV_AGENT_API_URL and SLIDEV_AGENT_DECK_ID.");
|
|
112
|
+
const manifest = await readManifest(cwd);
|
|
113
|
+
const generatedRoot = path.join(cwd, manifest.generatedRoot);
|
|
114
|
+
const files = [];
|
|
115
|
+
for (const filePath of manifest.files) {
|
|
116
|
+
const absoluteFile = path.join(generatedRoot, filePath);
|
|
117
|
+
files.push({
|
|
118
|
+
path: filePath,
|
|
119
|
+
content: await readFile(absoluteFile, "utf8"),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
const deckUrl = createDeckUrl(config);
|
|
123
|
+
if (!deckUrl) {
|
|
124
|
+
throw new Error("Remote sync is not configured. Set SLIDEV_AGENT_API_URL and SLIDEV_AGENT_DECK_ID.");
|
|
125
|
+
}
|
|
126
|
+
const response = await fetch(deckUrl, {
|
|
127
|
+
method: "PUT",
|
|
128
|
+
headers: {
|
|
129
|
+
"content-type": "application/json",
|
|
130
|
+
accept: "application/json",
|
|
131
|
+
},
|
|
132
|
+
body: JSON.stringify({
|
|
133
|
+
entry: manifest.entry,
|
|
134
|
+
files,
|
|
135
|
+
}),
|
|
136
|
+
});
|
|
137
|
+
if (!response.ok)
|
|
138
|
+
throw new Error(`Bridge push failed with ${response.status} ${response.statusText}`);
|
|
139
|
+
}
|
|
140
|
+
export async function resolveSlideEntry(cwd = process.cwd()) {
|
|
141
|
+
const config = resolveBridgeConfig(cwd);
|
|
142
|
+
if (isRemoteBridgeEnabled(config))
|
|
143
|
+
return pullRemoteSlides(cwd);
|
|
144
|
+
const entryFile = path.join(cwd, config.entry);
|
|
145
|
+
await stat(entryFile);
|
|
146
|
+
return {
|
|
147
|
+
mode: "local",
|
|
148
|
+
entry: entryFile,
|
|
149
|
+
manifest: null,
|
|
150
|
+
};
|
|
151
|
+
}
|
package/dist/lib/env.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function env(source, name, fallback) {
|
|
2
|
+
const value = source[name];
|
|
3
|
+
return typeof value === "string" && value.trim() ? value.trim() : fallback;
|
|
4
|
+
}
|
|
5
|
+
const globalEnv = "process" in globalThis
|
|
6
|
+
? globalThis.process.env
|
|
7
|
+
: import.meta.env ?? {};
|
|
8
|
+
const anthropicEnv = env(globalEnv, "ANTHROPIC_API_KEY");
|
|
9
|
+
const googleEnv = env(globalEnv, "GOOGLE_API_KEY");
|
|
10
|
+
const openaiEnv = env(globalEnv, "OPENAI_API_KEY");
|
|
11
|
+
export const model = env(globalEnv, "SLIDEV_AGENT_MODEL") ?? (anthropicEnv
|
|
12
|
+
? "anthropic:claude-sonnet-4-6"
|
|
13
|
+
: googleEnv
|
|
14
|
+
? "google:gemini-2.5-flash"
|
|
15
|
+
: openaiEnv
|
|
16
|
+
? "openai:gpt-5.4"
|
|
17
|
+
: undefined);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { tool } from "langchain";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
export const slidevGoToSlide = tool({
|
|
4
|
+
name: "slidev_go_to_slide",
|
|
5
|
+
description: "Navigate the active Slidev presentation in the user's browser to a specific 1-based slide number. Use this after creating a new slide when you know its final index.",
|
|
6
|
+
schema: z.object({
|
|
7
|
+
page: z.number().int().positive().describe("The 1-based Slidev page number to open."),
|
|
8
|
+
reason: z.string().optional().describe("Optional short explanation for the navigation."),
|
|
9
|
+
}),
|
|
10
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { writeFileSync } from "node:fs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
function posixRelativeToCwd(cwd, absoluteFile) {
|
|
6
|
+
const rel = path.relative(cwd, absoluteFile).replace(/\\/g, "/");
|
|
7
|
+
if (!rel)
|
|
8
|
+
return ".";
|
|
9
|
+
return rel.startsWith(".") ? rel : `./${rel}`;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Resolves installed `slidev-addon-agent` entry files and returns graph spec strings
|
|
13
|
+
* for langgraph.json (path + export name).
|
|
14
|
+
*/
|
|
15
|
+
export function resolveSlidevAddonGraphSpecs(cwd) {
|
|
16
|
+
const packageJsonPath = path.join(cwd, "package.json");
|
|
17
|
+
if (!existsSync(packageJsonPath)) {
|
|
18
|
+
throw new Error(`No package.json in ${cwd}. Run slidev-agent dev from your Slidev project root (where package.json lives).`);
|
|
19
|
+
}
|
|
20
|
+
const require = createRequire(packageJsonPath);
|
|
21
|
+
let pkgRoot;
|
|
22
|
+
try {
|
|
23
|
+
pkgRoot = path.dirname(require.resolve("slidev-addon-agent"));
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
throw new Error("Could not resolve \"slidev-addon-agent\". Install it in this project, e.g. pnpm add slidev-addon-agent");
|
|
27
|
+
}
|
|
28
|
+
const agentFile = path.join(pkgRoot, "agent", "index.ts");
|
|
29
|
+
const appFile = path.join(pkgRoot, "app", "index.ts");
|
|
30
|
+
return {
|
|
31
|
+
agent: `${posixRelativeToCwd(cwd, agentFile)}:agent`,
|
|
32
|
+
app: `${posixRelativeToCwd(cwd, appFile)}:app`,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export function buildLanggraphJson(cwd) {
|
|
36
|
+
const { agent, app } = resolveSlidevAddonGraphSpecs(cwd);
|
|
37
|
+
return {
|
|
38
|
+
node_version: "20",
|
|
39
|
+
dependencies: ["."],
|
|
40
|
+
graphs: {
|
|
41
|
+
agent,
|
|
42
|
+
},
|
|
43
|
+
http: {
|
|
44
|
+
app,
|
|
45
|
+
},
|
|
46
|
+
env: ".env",
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Writes `langgraph.json` when it is missing (first `slidev-agent dev`).
|
|
51
|
+
* Does not overwrite an existing file.
|
|
52
|
+
*/
|
|
53
|
+
export function writeLanggraphJsonIfMissing(cwd) {
|
|
54
|
+
const outPath = path.join(cwd, "langgraph.json");
|
|
55
|
+
if (existsSync(outPath))
|
|
56
|
+
return;
|
|
57
|
+
const config = buildLanggraphJson(cwd);
|
|
58
|
+
writeFileSync(outPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
59
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { readFile, stat } from "node:fs/promises";
|
|
3
|
+
import { initChatModel, tool } from "langchain";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { readTrimmedEnv } from "./env.js";
|
|
6
|
+
const slidevReviewScreenshotSchema = z.object({
|
|
7
|
+
imagePath: z.string().min(1).describe("Path to the exported screenshot image. Relative to the project root or an absolute path within it."),
|
|
8
|
+
slideIndex: z.number().int().positive().optional().describe("Optional 1-based slide index represented by the screenshot."),
|
|
9
|
+
focus: z.string().optional().describe("Optional short note about what to scrutinize most closely."),
|
|
10
|
+
});
|
|
11
|
+
const slidevReviewScreenshotResultSchema = z.object({
|
|
12
|
+
pass: z.boolean().describe("Whether the slide looks presentation-ready in the screenshot."),
|
|
13
|
+
summary: z.string().describe("Short visual verdict."),
|
|
14
|
+
issues: z.array(z.string()).describe("Concrete visual problems found in the screenshot."),
|
|
15
|
+
suggestions: z.array(z.string()).describe("Targeted improvements to address the issues."),
|
|
16
|
+
});
|
|
17
|
+
const supportedImageMimeTypes = {
|
|
18
|
+
".gif": "image/gif",
|
|
19
|
+
".jpeg": "image/jpeg",
|
|
20
|
+
".jpg": "image/jpeg",
|
|
21
|
+
".png": "image/png",
|
|
22
|
+
".webp": "image/webp",
|
|
23
|
+
};
|
|
24
|
+
function resolveImagePath(rootDir, imagePath) {
|
|
25
|
+
const absoluteRoot = path.resolve(rootDir);
|
|
26
|
+
const resolved = path.isAbsolute(imagePath)
|
|
27
|
+
? path.resolve(imagePath)
|
|
28
|
+
: path.resolve(absoluteRoot, imagePath);
|
|
29
|
+
if (resolved === absoluteRoot)
|
|
30
|
+
throw new Error("Screenshot path must point to an image file, not the project root.");
|
|
31
|
+
if (!resolved.startsWith(`${absoluteRoot}${path.sep}`))
|
|
32
|
+
throw new Error("Screenshot path must stay within the project root.");
|
|
33
|
+
return resolved;
|
|
34
|
+
}
|
|
35
|
+
function getImageMimeType(filePath) {
|
|
36
|
+
return supportedImageMimeTypes[path.extname(filePath).toLowerCase()] || "";
|
|
37
|
+
}
|
|
38
|
+
export function createSlidevReviewScreenshotTool(options) {
|
|
39
|
+
const rootDir = path.resolve(options.rootDir);
|
|
40
|
+
const reviewModelName = readTrimmedEnv(process.env, "SLIDEV_AGENT_REVIEW_MODEL")
|
|
41
|
+
|| options.model
|
|
42
|
+
|| readTrimmedEnv(process.env, "SLIDEV_AGENT_MODEL");
|
|
43
|
+
let reviewModelPromise = null;
|
|
44
|
+
async function getReviewModel() {
|
|
45
|
+
if (!reviewModelName) {
|
|
46
|
+
throw new Error("No screenshot review model is configured. Set `SLIDEV_AGENT_MODEL` or `SLIDEV_AGENT_REVIEW_MODEL` to a vision-capable chat model.");
|
|
47
|
+
}
|
|
48
|
+
reviewModelPromise ??= initChatModel(reviewModelName, {
|
|
49
|
+
temperature: 0,
|
|
50
|
+
});
|
|
51
|
+
return reviewModelPromise;
|
|
52
|
+
}
|
|
53
|
+
return tool(async ({ imagePath, slideIndex, focus }) => {
|
|
54
|
+
const resolvedImagePath = resolveImagePath(rootDir, imagePath);
|
|
55
|
+
const fileInfo = await stat(resolvedImagePath).catch(() => null);
|
|
56
|
+
if (!fileInfo?.isFile())
|
|
57
|
+
throw new Error(`Screenshot not found: ${imagePath}`);
|
|
58
|
+
const mimeType = getImageMimeType(resolvedImagePath);
|
|
59
|
+
if (!mimeType)
|
|
60
|
+
throw new Error(`Unsupported screenshot format for review: ${path.extname(resolvedImagePath) || "unknown"}`);
|
|
61
|
+
const imageData = await readFile(resolvedImagePath, "base64");
|
|
62
|
+
const reviewModel = await getReviewModel();
|
|
63
|
+
const structuredReviewer = reviewModel.withStructuredOutput(slidevReviewScreenshotResultSchema);
|
|
64
|
+
const review = await structuredReviewer.invoke([
|
|
65
|
+
{
|
|
66
|
+
role: "user",
|
|
67
|
+
content: [
|
|
68
|
+
{
|
|
69
|
+
type: "text",
|
|
70
|
+
text: [
|
|
71
|
+
`Review this rendered Slidev slide screenshot${slideIndex ? ` for slide ${slideIndex}` : ""}.`,
|
|
72
|
+
"Judge only what is visible in the image. Do not guess about hidden or source markdown content.",
|
|
73
|
+
"Mark the slide as failing if you notice clipped or off-screen content, overflow, cramped layout, collisions, unreadable or tiny text, broken wrapping, poor contrast, or obvious alignment problems.",
|
|
74
|
+
"Only pass the slide if it looks presentation-ready.",
|
|
75
|
+
focus ? `Pay extra attention to: ${focus}` : "",
|
|
76
|
+
"Return concise structured output.",
|
|
77
|
+
].filter(Boolean).join("\n"),
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
type: "image",
|
|
81
|
+
source_type: "base64",
|
|
82
|
+
data: imageData,
|
|
83
|
+
mimeType,
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
]);
|
|
88
|
+
return {
|
|
89
|
+
...review,
|
|
90
|
+
imagePath: path.relative(rootDir, resolvedImagePath) || path.basename(resolvedImagePath),
|
|
91
|
+
slideIndex: slideIndex || null,
|
|
92
|
+
};
|
|
93
|
+
}, {
|
|
94
|
+
name: "slidev_review_screenshot",
|
|
95
|
+
description: "Review an exported slide screenshot with a multimodal model and report visual issues such as clipping, overflow, collisions, poor contrast, and unreadable text.",
|
|
96
|
+
schema: slidevReviewScreenshotSchema,
|
|
97
|
+
});
|
|
98
|
+
}
|