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
package/lib/bridge.ts
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
|
|
4
|
+
import { env } from "./env.js"
|
|
5
|
+
|
|
6
|
+
const GENERATED_ROOT = ".slidev-agent/generated"
|
|
7
|
+
const MANIFEST_PATH = ".slidev-agent/manifest.json"
|
|
8
|
+
|
|
9
|
+
type BridgeConfig = {
|
|
10
|
+
apiUrl?: string
|
|
11
|
+
deckId?: string
|
|
12
|
+
namespace?: string
|
|
13
|
+
entry: string
|
|
14
|
+
routePrefix: string
|
|
15
|
+
cwd: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type DeckFile = {
|
|
19
|
+
path: string
|
|
20
|
+
content: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type NormalizedPayload = {
|
|
24
|
+
entry: string
|
|
25
|
+
files: DeckFile[]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type BridgeManifest = {
|
|
29
|
+
entry: string
|
|
30
|
+
generatedRoot: string
|
|
31
|
+
files: string[]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function resolveBridgeConfig(cwd: string): BridgeConfig {
|
|
35
|
+
return {
|
|
36
|
+
entry: env(process.env, "SLIDEV_AGENT_ENTRY", "slides.md"),
|
|
37
|
+
routePrefix: env(process.env, "SLIDEV_AGENT_ROUTE_PREFIX", "/slidev-agent"),
|
|
38
|
+
cwd,
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isRemoteBridgeEnabled(config: BridgeConfig) {
|
|
43
|
+
return Boolean(config.apiUrl && config.deckId)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function normalizeRoutePrefix(routePrefix: string) {
|
|
47
|
+
if (!routePrefix.startsWith("/"))
|
|
48
|
+
return `/${routePrefix}`
|
|
49
|
+
|
|
50
|
+
return routePrefix.replace(/\/$/, "")
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function createDeckUrl(config: BridgeConfig) {
|
|
54
|
+
if (!config.apiUrl || !config.deckId)
|
|
55
|
+
return undefined
|
|
56
|
+
|
|
57
|
+
const prefix = normalizeRoutePrefix(config.routePrefix)
|
|
58
|
+
const url = new URL(`${prefix}/decks/${encodeURIComponent(config.deckId)}`, config.apiUrl)
|
|
59
|
+
|
|
60
|
+
if (config.namespace)
|
|
61
|
+
url.searchParams.set("namespace", config.namespace)
|
|
62
|
+
|
|
63
|
+
return url
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function normalizePayload(payload: any, fallbackEntry: string): NormalizedPayload {
|
|
67
|
+
const entry = typeof payload.entry === "string" && payload.entry
|
|
68
|
+
? payload.entry
|
|
69
|
+
: fallbackEntry
|
|
70
|
+
|
|
71
|
+
if (Array.isArray(payload.files)) {
|
|
72
|
+
return {
|
|
73
|
+
entry,
|
|
74
|
+
files: payload.files.map((file: any) => ({
|
|
75
|
+
path: file.path,
|
|
76
|
+
content: file.content,
|
|
77
|
+
})),
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (payload.slides && typeof payload.slides === "object") {
|
|
82
|
+
return {
|
|
83
|
+
entry,
|
|
84
|
+
files: Object.entries(payload.slides).map(([filePath, content]) => ({
|
|
85
|
+
path: filePath,
|
|
86
|
+
content: String(content),
|
|
87
|
+
})),
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
throw new Error("Unsupported response shape. Expected `files` or `slides` in the bridge response.")
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function ensureParentDirectory(targetFile: string) {
|
|
95
|
+
await mkdir(path.dirname(targetFile), { recursive: true })
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function writeManifest(cwd: string, manifest: BridgeManifest) {
|
|
99
|
+
const manifestFile = path.join(cwd, MANIFEST_PATH)
|
|
100
|
+
await ensureParentDirectory(manifestFile)
|
|
101
|
+
await writeFile(manifestFile, `${JSON.stringify(manifest, null, 2)}\n`, "utf8")
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function readManifest(cwd: string): Promise<BridgeManifest> {
|
|
105
|
+
const manifestFile = path.join(cwd, MANIFEST_PATH)
|
|
106
|
+
const content = await readFile(manifestFile, "utf8")
|
|
107
|
+
return JSON.parse(content) as BridgeManifest
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function pullRemoteSlides(cwd = process.cwd()) {
|
|
111
|
+
const config = resolveBridgeConfig(cwd)
|
|
112
|
+
if (!isRemoteBridgeEnabled(config)) {
|
|
113
|
+
return {
|
|
114
|
+
mode: "local" as const,
|
|
115
|
+
entry: path.join(cwd, config.entry),
|
|
116
|
+
manifest: null,
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const deckUrl = createDeckUrl(config)
|
|
121
|
+
if (!deckUrl) {
|
|
122
|
+
throw new Error("Remote sync is not configured. Set SLIDEV_AGENT_API_URL and SLIDEV_AGENT_DECK_ID.")
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const response = await fetch(deckUrl, {
|
|
126
|
+
headers: {
|
|
127
|
+
accept: "application/json",
|
|
128
|
+
},
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
if (!response.ok)
|
|
132
|
+
throw new Error(`Bridge pull failed with ${response.status} ${response.statusText}`)
|
|
133
|
+
|
|
134
|
+
const payload = normalizePayload(await response.json(), config.entry)
|
|
135
|
+
const generatedRoot = path.join(cwd, GENERATED_ROOT)
|
|
136
|
+
|
|
137
|
+
await rm(generatedRoot, { recursive: true, force: true })
|
|
138
|
+
await mkdir(generatedRoot, { recursive: true })
|
|
139
|
+
|
|
140
|
+
for (const file of payload.files) {
|
|
141
|
+
const outputFile = path.join(generatedRoot, file.path)
|
|
142
|
+
await ensureParentDirectory(outputFile)
|
|
143
|
+
await writeFile(outputFile, file.content, "utf8")
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const manifest: BridgeManifest = {
|
|
147
|
+
entry: payload.entry,
|
|
148
|
+
generatedRoot: GENERATED_ROOT,
|
|
149
|
+
files: payload.files.map(file => file.path),
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
await writeManifest(cwd, manifest)
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
mode: "remote" as const,
|
|
156
|
+
entry: path.join(generatedRoot, payload.entry),
|
|
157
|
+
manifest,
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export async function pushRemoteSlides(cwd = process.cwd()) {
|
|
162
|
+
const config = resolveBridgeConfig(cwd)
|
|
163
|
+
if (!isRemoteBridgeEnabled(config))
|
|
164
|
+
throw new Error("Remote sync is not configured. Set SLIDEV_AGENT_API_URL and SLIDEV_AGENT_DECK_ID.")
|
|
165
|
+
|
|
166
|
+
const manifest = await readManifest(cwd)
|
|
167
|
+
const generatedRoot = path.join(cwd, manifest.generatedRoot)
|
|
168
|
+
|
|
169
|
+
const files: DeckFile[] = []
|
|
170
|
+
for (const filePath of manifest.files) {
|
|
171
|
+
const absoluteFile = path.join(generatedRoot, filePath)
|
|
172
|
+
files.push({
|
|
173
|
+
path: filePath,
|
|
174
|
+
content: await readFile(absoluteFile, "utf8"),
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const deckUrl = createDeckUrl(config)
|
|
179
|
+
if (!deckUrl) {
|
|
180
|
+
throw new Error("Remote sync is not configured. Set SLIDEV_AGENT_API_URL and SLIDEV_AGENT_DECK_ID.")
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const response = await fetch(deckUrl, {
|
|
184
|
+
method: "PUT",
|
|
185
|
+
headers: {
|
|
186
|
+
"content-type": "application/json",
|
|
187
|
+
accept: "application/json",
|
|
188
|
+
},
|
|
189
|
+
body: JSON.stringify({
|
|
190
|
+
entry: manifest.entry,
|
|
191
|
+
files,
|
|
192
|
+
}),
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
if (!response.ok)
|
|
196
|
+
throw new Error(`Bridge push failed with ${response.status} ${response.statusText}`)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export async function resolveSlideEntry(cwd = process.cwd()) {
|
|
200
|
+
const config = resolveBridgeConfig(cwd)
|
|
201
|
+
if (isRemoteBridgeEnabled(config))
|
|
202
|
+
return pullRemoteSlides(cwd)
|
|
203
|
+
|
|
204
|
+
const entryFile = path.join(cwd, config.entry)
|
|
205
|
+
await stat(entryFile)
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
mode: "local" as const,
|
|
209
|
+
entry: entryFile,
|
|
210
|
+
manifest: null,
|
|
211
|
+
}
|
|
212
|
+
}
|
package/lib/config.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
export interface SlidevAgentRuntimeConfig {
|
|
2
|
+
apiUrl: string
|
|
3
|
+
assistantId: string
|
|
4
|
+
deckId: string
|
|
5
|
+
threadId: string | null
|
|
6
|
+
inputPlaceholder: string
|
|
7
|
+
enabled: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
import { env } from "./env"
|
|
11
|
+
|
|
12
|
+
interface SlidevAgentInjectedConfig {
|
|
13
|
+
apiUrl?: string
|
|
14
|
+
assistantId?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function readInjectedConfig(): SlidevAgentInjectedConfig {
|
|
18
|
+
if (typeof window === "undefined")
|
|
19
|
+
return {}
|
|
20
|
+
|
|
21
|
+
const injectedConfig = Reflect.get(window, "__SLIDEV_AGENT_CONFIG__")
|
|
22
|
+
if (!injectedConfig || typeof injectedConfig !== "object")
|
|
23
|
+
return {}
|
|
24
|
+
|
|
25
|
+
return injectedConfig as SlidevAgentInjectedConfig
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getThreadStorageKey(assistantId: string, deckId: string) {
|
|
29
|
+
return `slidev-agent:thread:${assistantId || "default"}:${deckId || "default"}`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readStoredThreadId(assistantId: string, deckId: string) {
|
|
33
|
+
if (typeof window === "undefined")
|
|
34
|
+
return null
|
|
35
|
+
|
|
36
|
+
const storageKey = getThreadStorageKey(assistantId, deckId)
|
|
37
|
+
const current = window.sessionStorage.getItem(storageKey)
|
|
38
|
+
const legacyCurrent = window.localStorage.getItem(storageKey)
|
|
39
|
+
|
|
40
|
+
if (legacyCurrent) {
|
|
41
|
+
window.localStorage.removeItem(storageKey)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return current?.trim() || null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function persistSlidevAgentThreadId(assistantId: string, deckId: string, threadId: string) {
|
|
48
|
+
if (typeof window === "undefined" || !threadId.trim())
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
window.sessionStorage.setItem(getThreadStorageKey(assistantId, deckId), threadId)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function clearSlidevAgentThreadId(assistantId: string, deckId: string) {
|
|
55
|
+
if (typeof window === "undefined")
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
const storageKey = getThreadStorageKey(assistantId, deckId)
|
|
59
|
+
window.sessionStorage.removeItem(storageKey)
|
|
60
|
+
window.localStorage.removeItem(storageKey)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function resolveSlidevAgentRuntimeConfig(): SlidevAgentRuntimeConfig {
|
|
64
|
+
const injectedConfig = readInjectedConfig()
|
|
65
|
+
const apiUrl = env(import.meta.env, "VITE_LANGGRAPH_API_URL") || injectedConfig.apiUrl || "http://localhost:2024"
|
|
66
|
+
const assistantId = env(import.meta.env, "VITE_LANGGRAPH_ASSISTANT_ID") || injectedConfig.assistantId || ""
|
|
67
|
+
const deckId = env(import.meta.env, "VITE_SLIDEV_AGENT_DECK_ID") || "default-deck"
|
|
68
|
+
const inputPlaceholder = env(import.meta.env, "VITE_SLIDEV_AGENT_PLACEHOLDER") || "Ask the agent to create or revise slides..."
|
|
69
|
+
const threadId = env(import.meta.env, "VITE_LANGGRAPH_THREAD_ID") || readStoredThreadId(assistantId, deckId)
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
apiUrl,
|
|
73
|
+
assistantId,
|
|
74
|
+
deckId,
|
|
75
|
+
threadId,
|
|
76
|
+
inputPlaceholder,
|
|
77
|
+
enabled: Boolean(apiUrl && assistantId),
|
|
78
|
+
}
|
|
79
|
+
}
|
package/lib/env.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export function env(
|
|
2
|
+
source: Record<string, unknown>,
|
|
3
|
+
name: string,
|
|
4
|
+
): string | undefined
|
|
5
|
+
export function env<Fallback extends string>(
|
|
6
|
+
source: Record<string, unknown>,
|
|
7
|
+
name: string,
|
|
8
|
+
fallback: Fallback,
|
|
9
|
+
): string | Fallback
|
|
10
|
+
export function env(
|
|
11
|
+
source: Record<string, unknown>,
|
|
12
|
+
name: string,
|
|
13
|
+
fallback?: string,
|
|
14
|
+
): string | undefined {
|
|
15
|
+
const value = source[name]
|
|
16
|
+
return typeof value === "string" && value.trim() ? value.trim() : fallback
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type ImportMetaEnv = ImportMeta & {
|
|
20
|
+
env?: Record<string, string | undefined>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const globalEnv = "process" in globalThis
|
|
24
|
+
? globalThis.process.env
|
|
25
|
+
: (import.meta as ImportMetaEnv).env ?? {}
|
|
26
|
+
const anthropicEnv = env(globalEnv, "ANTHROPIC_API_KEY")
|
|
27
|
+
const googleEnv = env(globalEnv, "GOOGLE_API_KEY")
|
|
28
|
+
const openaiEnv = env(globalEnv, "OPENAI_API_KEY")
|
|
29
|
+
|
|
30
|
+
export const model = env(globalEnv, "SLIDEV_AGENT_MODEL") ?? (
|
|
31
|
+
anthropicEnv
|
|
32
|
+
? "anthropic:claude-sonnet-4-6"
|
|
33
|
+
: googleEnv
|
|
34
|
+
? "google:gemini-2.5-flash"
|
|
35
|
+
: openaiEnv
|
|
36
|
+
? "openai:gpt-5.4"
|
|
37
|
+
: undefined
|
|
38
|
+
)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { nextTick, unref } from "vue"
|
|
2
|
+
|
|
3
|
+
import { slidevGoToSlide as slidevGoToSlideDefinition } from "./headless-tools"
|
|
4
|
+
|
|
5
|
+
type SlidevNavAdapter = {
|
|
6
|
+
go: (page: number) => void | Promise<void>
|
|
7
|
+
currentPage: unknown
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function createSlidevHeadlessTools(nav: SlidevNavAdapter) {
|
|
11
|
+
const slidevGoToSlide = slidevGoToSlideDefinition.implement(async ({ page, reason }) => {
|
|
12
|
+
await Promise.resolve(nav.go(page))
|
|
13
|
+
await nextTick()
|
|
14
|
+
|
|
15
|
+
const currentPage = Number(unref(nav.currentPage)) || page
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
success: currentPage === page,
|
|
19
|
+
page,
|
|
20
|
+
currentPage,
|
|
21
|
+
reason: reason || undefined,
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
return [slidevGoToSlide]
|
|
26
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { tool } from "langchain"
|
|
2
|
+
import { z } from "zod"
|
|
3
|
+
|
|
4
|
+
export const slidevGoToSlide = tool({
|
|
5
|
+
name: "slidev_go_to_slide",
|
|
6
|
+
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.",
|
|
7
|
+
schema: z.object({
|
|
8
|
+
page: z.number().int().positive().describe("The 1-based Slidev page number to open."),
|
|
9
|
+
reason: z.string().optional().describe("Optional short explanation for the navigation."),
|
|
10
|
+
}),
|
|
11
|
+
})
|
|
@@ -0,0 +1,79 @@
|
|
|
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
|
+
|
|
6
|
+
export interface LanggraphJsonShape {
|
|
7
|
+
node_version: string
|
|
8
|
+
dependencies: string[]
|
|
9
|
+
graphs: Record<string, string>
|
|
10
|
+
http: { app: string }
|
|
11
|
+
env: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function posixRelativeToCwd(cwd: string, absoluteFile: string): string {
|
|
15
|
+
const rel = path.relative(cwd, absoluteFile).replace(/\\/g, "/")
|
|
16
|
+
if (!rel)
|
|
17
|
+
return "."
|
|
18
|
+
return rel.startsWith(".") ? rel : `./${rel}`
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Resolves installed `slidev-addon-agent` entry files and returns graph spec strings
|
|
23
|
+
* for langgraph.json (path + export name).
|
|
24
|
+
*/
|
|
25
|
+
export function resolveSlidevAddonGraphSpecs(cwd: string): { agent: string; app: string } {
|
|
26
|
+
const packageJsonPath = path.join(cwd, "package.json")
|
|
27
|
+
if (!existsSync(packageJsonPath)) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`No package.json in ${cwd}. Run slidev-agent dev from your Slidev project root (where package.json lives).`,
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const require = createRequire(packageJsonPath)
|
|
34
|
+
let pkgRoot: string
|
|
35
|
+
try {
|
|
36
|
+
pkgRoot = path.dirname(require.resolve("slidev-addon-agent"))
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
throw new Error(
|
|
40
|
+
"Could not resolve \"slidev-addon-agent\". Install it in this project, e.g. pnpm add slidev-addon-agent",
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const agentFile = path.join(pkgRoot, "agent", "index.ts")
|
|
45
|
+
const appFile = path.join(pkgRoot, "app", "index.ts")
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
agent: `${posixRelativeToCwd(cwd, agentFile)}:agent`,
|
|
49
|
+
app: `${posixRelativeToCwd(cwd, appFile)}:app`,
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function buildLanggraphJson(cwd: string): LanggraphJsonShape {
|
|
54
|
+
const { agent, app } = resolveSlidevAddonGraphSpecs(cwd)
|
|
55
|
+
return {
|
|
56
|
+
node_version: "20",
|
|
57
|
+
dependencies: ["."],
|
|
58
|
+
graphs: {
|
|
59
|
+
agent,
|
|
60
|
+
},
|
|
61
|
+
http: {
|
|
62
|
+
app,
|
|
63
|
+
},
|
|
64
|
+
env: ".env",
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Writes `langgraph.json` when it is missing (first `slidev-agent dev`).
|
|
70
|
+
* Does not overwrite an existing file.
|
|
71
|
+
*/
|
|
72
|
+
export function writeLanggraphJsonIfMissing(cwd: string): void {
|
|
73
|
+
const outPath = path.join(cwd, "langgraph.json")
|
|
74
|
+
if (existsSync(outPath))
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
const config = buildLanggraphJson(cwd)
|
|
78
|
+
writeFileSync(outPath, `${JSON.stringify(config, null, 2)}\n`, "utf8")
|
|
79
|
+
}
|
package/lib/messages.ts
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
function stringifyContentPart(part: unknown): string {
|
|
2
|
+
if (typeof part === "string")
|
|
3
|
+
return part
|
|
4
|
+
|
|
5
|
+
if (part && typeof part === "object") {
|
|
6
|
+
const maybeText = Reflect.get(part, "text")
|
|
7
|
+
if (typeof maybeText === "string")
|
|
8
|
+
return maybeText
|
|
9
|
+
|
|
10
|
+
const maybeType = Reflect.get(part, "type")
|
|
11
|
+
if (maybeType === "tool_call" || maybeType === "tool_result")
|
|
12
|
+
return ""
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return ""
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type MessageToolCall = {
|
|
19
|
+
id: string
|
|
20
|
+
name: string
|
|
21
|
+
args?: unknown
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function parseToolCallLike(value: unknown): MessageToolCall | null {
|
|
25
|
+
if (!value || typeof value !== "object")
|
|
26
|
+
return null
|
|
27
|
+
|
|
28
|
+
const directId = Reflect.get(value, "id")
|
|
29
|
+
const nestedFunction = Reflect.get(value, "function")
|
|
30
|
+
const nestedId = Reflect.get(value, "tool_call_id")
|
|
31
|
+
const directName = Reflect.get(value, "name")
|
|
32
|
+
const nestedName = nestedFunction && typeof nestedFunction === "object"
|
|
33
|
+
? Reflect.get(nestedFunction, "name")
|
|
34
|
+
: undefined
|
|
35
|
+
const directArgs = Reflect.get(value, "args")
|
|
36
|
+
const nestedArgs = nestedFunction && typeof nestedFunction === "object"
|
|
37
|
+
? Reflect.get(nestedFunction, "arguments")
|
|
38
|
+
: undefined
|
|
39
|
+
|
|
40
|
+
const id = typeof directId === "string"
|
|
41
|
+
? directId
|
|
42
|
+
: typeof nestedId === "string"
|
|
43
|
+
? nestedId
|
|
44
|
+
: ""
|
|
45
|
+
const name = typeof directName === "string"
|
|
46
|
+
? directName
|
|
47
|
+
: typeof nestedName === "string"
|
|
48
|
+
? nestedName
|
|
49
|
+
: ""
|
|
50
|
+
const args = directArgs ?? nestedArgs
|
|
51
|
+
|
|
52
|
+
if (!id && !name)
|
|
53
|
+
return null
|
|
54
|
+
|
|
55
|
+
return { id, name, args }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function getMessageRole(message: unknown): string {
|
|
59
|
+
if (message && typeof message === "object") {
|
|
60
|
+
const getType = Reflect.get(message, "getType")
|
|
61
|
+
if (typeof getType === "function")
|
|
62
|
+
return String(getType.call(message))
|
|
63
|
+
|
|
64
|
+
const type = Reflect.get(message, "type")
|
|
65
|
+
if (typeof type === "string")
|
|
66
|
+
return type
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return "assistant"
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function getMessageContent(message: unknown): string {
|
|
73
|
+
if (!message || typeof message !== "object")
|
|
74
|
+
return ""
|
|
75
|
+
|
|
76
|
+
const content = Reflect.get(message, "content")
|
|
77
|
+
if (typeof content === "string")
|
|
78
|
+
return content
|
|
79
|
+
|
|
80
|
+
if (Array.isArray(content))
|
|
81
|
+
return content.map(stringifyContentPart).filter(Boolean).join("\n\n")
|
|
82
|
+
|
|
83
|
+
return ""
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function getMessageToolCalls(message: unknown): MessageToolCall[] {
|
|
87
|
+
if (!message || typeof message !== "object")
|
|
88
|
+
return []
|
|
89
|
+
|
|
90
|
+
const toolCalls: MessageToolCall[] = []
|
|
91
|
+
const seen = new Set<string>()
|
|
92
|
+
const content = Reflect.get(message, "content")
|
|
93
|
+
|
|
94
|
+
if (Array.isArray(content)) {
|
|
95
|
+
content.forEach((part) => {
|
|
96
|
+
if (!part || typeof part !== "object" || Reflect.get(part, "type") !== "tool_call")
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
const toolCall = parseToolCallLike(part)
|
|
100
|
+
if (!toolCall)
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
const key = toolCall.id || `${toolCall.name}:${JSON.stringify(toolCall.args ?? "")}`
|
|
104
|
+
if (seen.has(key))
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
seen.add(key)
|
|
108
|
+
toolCalls.push(toolCall)
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const kwargs = Reflect.get(message, "additional_kwargs")
|
|
113
|
+
const nestedToolCalls = kwargs && typeof kwargs === "object"
|
|
114
|
+
? Reflect.get(kwargs, "tool_calls")
|
|
115
|
+
: undefined
|
|
116
|
+
|
|
117
|
+
if (Array.isArray(nestedToolCalls)) {
|
|
118
|
+
nestedToolCalls.forEach((entry) => {
|
|
119
|
+
const toolCall = parseToolCallLike(entry)
|
|
120
|
+
if (!toolCall)
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
const key = toolCall.id || `${toolCall.name}:${JSON.stringify(toolCall.args ?? "")}`
|
|
124
|
+
if (seen.has(key))
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
seen.add(key)
|
|
128
|
+
toolCalls.push(toolCall)
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return toolCalls
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function getMessageToolName(message: unknown): string {
|
|
136
|
+
if (!message || typeof message !== "object")
|
|
137
|
+
return ""
|
|
138
|
+
|
|
139
|
+
const directName = Reflect.get(message, "name")
|
|
140
|
+
if (typeof directName === "string")
|
|
141
|
+
return directName
|
|
142
|
+
|
|
143
|
+
const kwargs = Reflect.get(message, "additional_kwargs")
|
|
144
|
+
if (kwargs && typeof kwargs === "object") {
|
|
145
|
+
const nestedName = Reflect.get(kwargs, "name")
|
|
146
|
+
if (typeof nestedName === "string")
|
|
147
|
+
return nestedName
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return ""
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function getMessageToolCallId(message: unknown): string {
|
|
154
|
+
if (!message || typeof message !== "object")
|
|
155
|
+
return ""
|
|
156
|
+
|
|
157
|
+
const directId = Reflect.get(message, "tool_call_id")
|
|
158
|
+
if (typeof directId === "string")
|
|
159
|
+
return directId
|
|
160
|
+
|
|
161
|
+
const kwargs = Reflect.get(message, "additional_kwargs")
|
|
162
|
+
if (kwargs && typeof kwargs === "object") {
|
|
163
|
+
const nestedId = Reflect.get(kwargs, "tool_call_id")
|
|
164
|
+
if (typeof nestedId === "string")
|
|
165
|
+
return nestedId
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return ""
|
|
169
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import DOMPurify from "dompurify"
|
|
2
|
+
import { marked } from "marked"
|
|
3
|
+
|
|
4
|
+
marked.setOptions({
|
|
5
|
+
gfm: true,
|
|
6
|
+
breaks: true,
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
DOMPurify.addHook("afterSanitizeAttributes", (node) => {
|
|
10
|
+
if (node.tagName === "A") {
|
|
11
|
+
node.setAttribute("target", "_blank")
|
|
12
|
+
node.setAttribute("rel", "noopener noreferrer")
|
|
13
|
+
}
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
export function renderChatMarkdown(source: string): string {
|
|
17
|
+
const html = marked.parse(source, { async: false }) as string
|
|
18
|
+
return DOMPurify.sanitize(html)
|
|
19
|
+
}
|