docus 5.10.1 → 5.12.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/app/app.config.ts +4 -1
- package/app/app.vue +22 -27
- package/app/components/app/AppSearch.vue +54 -0
- package/app/components/docs/DocsAsideRight.vue +1 -2
- package/app/components/docs/DocsAsideRightBottom.vue +2 -2
- package/app/components/docs/DocsPageHeaderLinks.vue +11 -6
- package/app/composables/useDocusShortcuts.ts +24 -0
- package/app/composables/useUIConfig.ts +1 -1
- package/app/error.vue +1 -10
- package/app/pages/[[lang]]/[...slug].vue +6 -4
- package/app/plugins/i18n.ts +1 -1
- package/app/types/index.d.ts +22 -0
- package/i18n/locales/pl.json +2 -2
- package/modules/assistant/index.ts +13 -5
- package/modules/assistant/runtime/components/AssistantChat.vue +5 -2
- package/modules/assistant/runtime/components/AssistantComark.ts +9 -0
- package/modules/assistant/runtime/components/AssistantFloatingInput.vue +9 -10
- package/modules/assistant/runtime/components/AssistantIndicator.vue +116 -0
- package/modules/assistant/runtime/components/AssistantPanel.vue +268 -258
- package/modules/assistant/runtime/components/AssistantPreStream.vue +1 -1
- package/modules/assistant/runtime/composables/useAssistant.ts +34 -38
- package/modules/assistant/runtime/server/api/search.ts +22 -42
- package/modules/config.ts +12 -1
- package/modules/css.ts +35 -6
- package/modules/markdown-rewrite.ts +1 -1
- package/modules/skills/index.ts +4 -2
- package/nuxt.schema.ts +28 -0
- package/package.json +47 -28
- package/server/mcp/tools/get-page.ts +17 -8
- package/server/mcp/tools/list-pages.ts +10 -6
- package/server/routes/sitemap.xml.ts +7 -4
- package/server/utils/content.ts +4 -0
- package/modules/assistant/runtime/components/AssistantLoading.vue +0 -164
- package/modules/assistant/runtime/components/AssistantMatrix.vue +0 -92
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { UIMessage } from 'ai'
|
|
2
|
-
import { useAppConfig, useRuntimeConfig
|
|
3
|
-
import {
|
|
4
|
-
import { computed } from 'vue'
|
|
2
|
+
import { useAppConfig, useRuntimeConfig } from '#imports'
|
|
3
|
+
import { createSharedComposable, useLocalStorage } from '@vueuse/core'
|
|
4
|
+
import { computed, ref, watch } from 'vue'
|
|
5
5
|
import type { FaqCategory, FaqQuestions, LocalizedFaqQuestions } from '../types'
|
|
6
6
|
|
|
7
7
|
function normalizeFaqQuestions(questions: FaqQuestions): FaqCategory[] {
|
|
@@ -19,10 +19,7 @@ function normalizeFaqQuestions(questions: FaqQuestions): FaqCategory[] {
|
|
|
19
19
|
return questions as FaqCategory[]
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
const
|
|
23
|
-
const PANEL_WIDTH_EXPANDED = 520
|
|
24
|
-
|
|
25
|
-
export function useAssistant() {
|
|
22
|
+
export const useAssistant = createSharedComposable(() => {
|
|
26
23
|
const config = useRuntimeConfig()
|
|
27
24
|
const appConfig = useAppConfig()
|
|
28
25
|
const assistantRuntimeConfig = config.public.assistant as { enabled?: boolean } | undefined
|
|
@@ -30,26 +27,37 @@ export function useAssistant() {
|
|
|
30
27
|
const docusRuntimeConfig = appConfig.docus as { locale?: string } | undefined
|
|
31
28
|
const isEnabled = computed(() => assistantRuntimeConfig?.enabled ?? false)
|
|
32
29
|
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
const
|
|
30
|
+
const storageOpen = useLocalStorage('assistant-open', false)
|
|
31
|
+
const messages = useLocalStorage<UIMessage[]>('assistant-messages', [])
|
|
32
|
+
|
|
33
|
+
const isOpen = ref(false)
|
|
34
|
+
const isStudioExpanded = ref(false)
|
|
37
35
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
onNuxtReady(() => {
|
|
37
|
+
nextTick(() => {
|
|
38
|
+
isOpen.value = storageOpen.value
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
isStudioExpanded.value = document.body.hasAttribute('data-expand-sidebar')
|
|
42
|
+
const observer = new MutationObserver(() => {
|
|
43
|
+
isStudioExpanded.value = document.body.hasAttribute('data-expand-sidebar')
|
|
44
|
+
})
|
|
45
|
+
observer.observe(document.body, { attributes: true, attributeFilter: ['data-expand-sidebar'] })
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
watch(isOpen, (value) => {
|
|
49
|
+
storageOpen.value = value
|
|
50
|
+
})
|
|
41
51
|
|
|
42
52
|
const faqQuestions = computed<FaqCategory[]>(() => {
|
|
43
53
|
const faqConfig = assistantConfig?.faqQuestions
|
|
44
54
|
if (!faqConfig) return []
|
|
45
55
|
|
|
46
|
-
// Check if it's a localized object (has locale keys like 'en', 'fr')
|
|
47
56
|
if (!Array.isArray(faqConfig)) {
|
|
48
57
|
const localizedConfig = faqConfig as LocalizedFaqQuestions
|
|
49
58
|
const currentLocale = docusRuntimeConfig?.locale || 'en'
|
|
50
59
|
const defaultLocale = config.public.i18n?.defaultLocale || 'en'
|
|
51
60
|
|
|
52
|
-
// Try current locale, then default locale, then first available
|
|
53
61
|
const questions = localizedConfig[currentLocale]
|
|
54
62
|
|| localizedConfig[defaultLocale]
|
|
55
63
|
|| Object.values(localizedConfig)[0]
|
|
@@ -61,51 +69,39 @@ export function useAssistant() {
|
|
|
61
69
|
})
|
|
62
70
|
|
|
63
71
|
function open(initialMessage?: string, clearPrevious = false) {
|
|
72
|
+
if (isStudioExpanded.value) return
|
|
73
|
+
|
|
64
74
|
if (clearPrevious) {
|
|
65
75
|
messages.value = []
|
|
66
76
|
}
|
|
67
77
|
|
|
68
78
|
if (initialMessage) {
|
|
69
|
-
|
|
79
|
+
messages.value = [...messages.value, {
|
|
80
|
+
id: String(Date.now()),
|
|
81
|
+
role: 'user' as const,
|
|
82
|
+
parts: [{ type: 'text' as const, text: initialMessage }],
|
|
83
|
+
}]
|
|
70
84
|
}
|
|
71
85
|
isOpen.value = true
|
|
72
86
|
}
|
|
73
87
|
|
|
74
|
-
function clearPending() {
|
|
75
|
-
pendingMessage.value = undefined
|
|
76
|
-
}
|
|
77
|
-
|
|
78
88
|
function close() {
|
|
79
89
|
isOpen.value = false
|
|
80
90
|
}
|
|
81
91
|
|
|
82
92
|
function toggle() {
|
|
93
|
+
if (isStudioExpanded.value) return
|
|
83
94
|
isOpen.value = !isOpen.value
|
|
84
95
|
}
|
|
85
96
|
|
|
86
|
-
function clearMessages() {
|
|
87
|
-
messages.value = []
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function toggleExpanded() {
|
|
91
|
-
isExpanded.value = !isExpanded.value
|
|
92
|
-
}
|
|
93
|
-
|
|
94
97
|
return {
|
|
95
98
|
isEnabled,
|
|
96
99
|
isOpen,
|
|
97
|
-
|
|
98
|
-
isMobile,
|
|
99
|
-
panelWidth,
|
|
100
|
-
shouldPushContent,
|
|
100
|
+
isStudioExpanded,
|
|
101
101
|
messages,
|
|
102
|
-
pendingMessage,
|
|
103
102
|
faqQuestions,
|
|
104
103
|
open,
|
|
105
|
-
clearPending,
|
|
106
104
|
close,
|
|
107
105
|
toggle,
|
|
108
|
-
toggleExpanded,
|
|
109
|
-
clearMessages,
|
|
110
106
|
}
|
|
111
|
-
}
|
|
107
|
+
})
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { streamText, convertToModelMessages
|
|
2
|
-
import type {
|
|
1
|
+
import { streamText, convertToModelMessages } from 'ai'
|
|
2
|
+
import type { ToolSet } from 'ai'
|
|
3
3
|
import { createMCPClient } from '@ai-sdk/mcp'
|
|
4
4
|
import type { H3Event } from 'h3'
|
|
5
5
|
|
|
@@ -23,11 +23,10 @@ function createLocalFetch(event: H3Event): typeof fetch {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
26
|
-
function stopWhenResponseComplete({ steps }: { steps:
|
|
26
|
+
function stopWhenResponseComplete({ steps }: { steps: { text?: string, toolCalls?: unknown[] }[] }): boolean {
|
|
27
27
|
const lastStep = steps.at(-1)
|
|
28
28
|
if (!lastStep) return false
|
|
29
29
|
|
|
30
|
-
// Primary condition: stop when model gives a text response without tool calls
|
|
31
30
|
const hasText = Boolean(lastStep.text && lastStep.text.trim().length > 0)
|
|
32
31
|
const hasNoToolCalls = !lastStep.toolCalls || lastStep.toolCalls.length === 0
|
|
33
32
|
|
|
@@ -57,7 +56,7 @@ function getSystemPrompt(siteName: string) {
|
|
|
57
56
|
|
|
58
57
|
**Links and exploration:**
|
|
59
58
|
- Tool results include a \`url\` for each page — prefer markdown links \`[label](url)\` so users can open the doc in one click
|
|
60
|
-
- When it helps, add extra links (related pages,
|
|
59
|
+
- When it helps, add extra links (related pages, "read more", side topics) — make the answer easy to dig into, not a wall of text
|
|
61
60
|
- Stick to URLs from tool results (\`url\` / \`path\`) so links stay valid
|
|
62
61
|
|
|
63
62
|
**FORMATTING RULES (CRITICAL):**
|
|
@@ -85,6 +84,9 @@ export default defineEventHandler(async (event) => {
|
|
|
85
84
|
const isExternalUrl = mcpServer.startsWith('http://') || mcpServer.startsWith('https://')
|
|
86
85
|
const baseURL = config.app?.baseURL?.replace(/\/$/, '') || ''
|
|
87
86
|
|
|
87
|
+
const abortController = new AbortController()
|
|
88
|
+
event.node.req.on('close', () => abortController.abort())
|
|
89
|
+
|
|
88
90
|
let transport: Parameters<typeof createMCPClient>[0]['transport']
|
|
89
91
|
if (isExternalUrl) {
|
|
90
92
|
transport = {
|
|
@@ -109,41 +111,19 @@ export default defineEventHandler(async (event) => {
|
|
|
109
111
|
const httpClient = await createMCPClient({ transport })
|
|
110
112
|
const mcpTools = await httpClient.tools()
|
|
111
113
|
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
type: 'data-tool-calls',
|
|
128
|
-
data: {
|
|
129
|
-
tools: toolCalls.map((tc: ToolCallPart) => {
|
|
130
|
-
const args = 'args' in tc ? tc.args : 'input' in tc ? tc.input : {}
|
|
131
|
-
return {
|
|
132
|
-
toolName: tc.toolName,
|
|
133
|
-
toolCallId: tc.toolCallId,
|
|
134
|
-
args,
|
|
135
|
-
}
|
|
136
|
-
}),
|
|
137
|
-
},
|
|
138
|
-
})
|
|
139
|
-
},
|
|
140
|
-
})
|
|
141
|
-
writer.merge(result.toUIMessageStream())
|
|
142
|
-
},
|
|
143
|
-
onFinish: async () => {
|
|
144
|
-
await httpClient.close()
|
|
145
|
-
},
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
return createUIMessageStreamResponse({ stream })
|
|
114
|
+
const closeMcp = () => event.waitUntil(httpClient.close())
|
|
115
|
+
|
|
116
|
+
return streamText({
|
|
117
|
+
model: config.assistant.model,
|
|
118
|
+
maxOutputTokens: 4000,
|
|
119
|
+
maxRetries: 2,
|
|
120
|
+
abortSignal: abortController.signal,
|
|
121
|
+
stopWhen: stopWhenResponseComplete,
|
|
122
|
+
system: getSystemPrompt(siteName),
|
|
123
|
+
messages: await convertToModelMessages(messages),
|
|
124
|
+
tools: mcpTools as ToolSet,
|
|
125
|
+
onFinish: closeMcp,
|
|
126
|
+
onAbort: closeMcp,
|
|
127
|
+
onError: closeMcp,
|
|
128
|
+
}).toUIMessageStreamResponse()
|
|
149
129
|
})
|
package/modules/config.ts
CHANGED
|
@@ -5,10 +5,11 @@ import { join } from 'node:path'
|
|
|
5
5
|
import { inferSiteURL, getPackageJsonMetadata } from '../utils/meta'
|
|
6
6
|
import { getGitBranch, getGitEnv, getLocalGitInfo } from '../utils/git'
|
|
7
7
|
|
|
8
|
-
const log = logger.withTag('
|
|
8
|
+
const log = logger.withTag('docus')
|
|
9
9
|
|
|
10
10
|
type I18nLocale = string | { code: string, name?: string }
|
|
11
11
|
type DocusI18nOptions = { locales?: I18nLocale[], strategy?: string }
|
|
12
|
+
type DocusMcpOptions = { route?: string, enabled?: boolean }
|
|
12
13
|
type RegisterModuleOptions = {
|
|
13
14
|
langDir: string
|
|
14
15
|
locales: Array<{ code: string, name: string, file: string }>
|
|
@@ -58,6 +59,16 @@ export default defineNuxtModule({
|
|
|
58
59
|
branch: getGitBranch(),
|
|
59
60
|
})
|
|
60
61
|
|
|
62
|
+
/*
|
|
63
|
+
** MCP route (expose to client so the page header dropdown stays in sync
|
|
64
|
+
** with the user-configured `mcp.route` from @nuxtjs/mcp-toolkit)
|
|
65
|
+
*/
|
|
66
|
+
const mcpOptions = (nuxt.options as typeof nuxt.options & { mcp?: DocusMcpOptions }).mcp
|
|
67
|
+
nuxt.options.runtimeConfig.public.mcp = defu(
|
|
68
|
+
nuxt.options.runtimeConfig.public.mcp as DocusMcpOptions | undefined,
|
|
69
|
+
{ route: mcpOptions?.route || '/mcp' },
|
|
70
|
+
)
|
|
71
|
+
|
|
61
72
|
const forcedColorMode = (nuxt.options.appConfig.docus as Record<string, unknown>)?.colorMode as string | undefined
|
|
62
73
|
if (forcedColorMode === 'light' || forcedColorMode === 'dark') {
|
|
63
74
|
nuxt.options.colorMode = defu({ preference: forcedColorMode, fallback: forcedColorMode }, nuxt.options.colorMode || {}) as typeof nuxt.options.colorMode
|
package/modules/css.ts
CHANGED
|
@@ -1,21 +1,37 @@
|
|
|
1
|
-
import { defineNuxtModule, addTemplate, createResolver } from '@nuxt/kit'
|
|
2
|
-
import {
|
|
1
|
+
import { defineNuxtModule, addTemplate, createResolver, logger } from '@nuxt/kit'
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
|
+
import { readFile } from 'node:fs/promises'
|
|
4
|
+
import { resolve } from 'pathe'
|
|
3
5
|
import { resolveModulePath } from 'exsolve'
|
|
4
6
|
|
|
7
|
+
const log = logger.withTag('docus')
|
|
8
|
+
|
|
5
9
|
export default defineNuxtModule({
|
|
6
10
|
meta: {
|
|
7
|
-
name: 'css',
|
|
11
|
+
name: 'docus-css',
|
|
8
12
|
},
|
|
9
13
|
async setup(_options, nuxt) {
|
|
10
|
-
const dir = nuxt.options.rootDir
|
|
11
14
|
const resolver = createResolver(import.meta.url)
|
|
12
15
|
|
|
13
|
-
const contentDir =
|
|
16
|
+
const contentDir = resolve(nuxt.options.rootDir, 'content')
|
|
14
17
|
const uiPath = resolveModulePath('@nuxt/ui', { from: import.meta.url, conditions: ['style'] })
|
|
15
18
|
const tailwindPath = resolveModulePath('tailwindcss', { from: import.meta.url, conditions: ['style'] })
|
|
16
19
|
const layerDir = resolver.resolve('../app')
|
|
17
20
|
const assistantDir = resolver.resolve('../modules/assistant')
|
|
18
21
|
|
|
22
|
+
let userDocusPath: string | null = resolve(nuxt.options.srcDir, 'app.css')
|
|
23
|
+
if (existsSync(userDocusPath)) {
|
|
24
|
+
const userDocusCss = await readFile(userDocusPath, 'utf-8')
|
|
25
|
+
if (userDocusCss.includes('@import "tailwindcss"')) {
|
|
26
|
+
nuxt.hook('modules:done', () => {
|
|
27
|
+
log.warn('`app.css` contains `@import "tailwindcss";` consider removing it to avoid duplicate css.')
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
userDocusPath = null
|
|
33
|
+
}
|
|
34
|
+
|
|
19
35
|
const cssTemplate = addTemplate({
|
|
20
36
|
filename: 'docus.css',
|
|
21
37
|
getContents: () => {
|
|
@@ -25,7 +41,20 @@ export default defineNuxtModule({
|
|
|
25
41
|
@source "${contentDir.replace(/\\/g, '/')}/**/*";
|
|
26
42
|
@source "${layerDir.replace(/\\/g, '/')}/**/*";
|
|
27
43
|
@source "../../app.config.ts";
|
|
28
|
-
@source "${assistantDir.replace(/\\/g, '/')}/**/*"
|
|
44
|
+
@source "${assistantDir.replace(/\\/g, '/')}/**/*";
|
|
45
|
+
|
|
46
|
+
html.dark .shiki span {
|
|
47
|
+
color: var(--shiki-dark) !important;
|
|
48
|
+
background-color: var(--shiki-dark-bg) !important;
|
|
49
|
+
font-style: var(--shiki-dark-font-style) !important;
|
|
50
|
+
font-weight: var(--shiki-dark-font-weight) !important;
|
|
51
|
+
text-decoration: var(--shiki-dark-text-decoration) !important;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
:root {
|
|
55
|
+
--ui-container: 90rem;
|
|
56
|
+
}
|
|
57
|
+
` + (userDocusPath ? `\n@import ${JSON.stringify(userDocusPath)};` : '')
|
|
29
58
|
},
|
|
30
59
|
})
|
|
31
60
|
|
|
@@ -2,7 +2,7 @@ import { defineNuxtModule, logger } from '@nuxt/kit'
|
|
|
2
2
|
import { resolve } from 'node:path'
|
|
3
3
|
import { readFile, writeFile } from 'node:fs/promises'
|
|
4
4
|
|
|
5
|
-
const log = logger.withTag('
|
|
5
|
+
const log = logger.withTag('docus')
|
|
6
6
|
|
|
7
7
|
type I18nLocale = string | { code: string }
|
|
8
8
|
type DocusI18nOptions = { locales?: I18nLocale[] }
|
package/modules/skills/index.ts
CHANGED
|
@@ -19,7 +19,7 @@ export interface SkillsModuleOptions {
|
|
|
19
19
|
const SKILL_NAME_REGEX = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/
|
|
20
20
|
const MAX_NAME_LENGTH = 64
|
|
21
21
|
|
|
22
|
-
const log = logger.withTag('
|
|
22
|
+
const log = logger.withTag('docus')
|
|
23
23
|
|
|
24
24
|
const defaults: Required<SkillsModuleOptions> = {
|
|
25
25
|
dir: 'skills',
|
|
@@ -38,7 +38,9 @@ export default defineNuxtModule<SkillsModuleOptions>({
|
|
|
38
38
|
const catalog = await scanSkills(skillsDir)
|
|
39
39
|
if (!catalog.length) return
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
nuxt.hook('modules:done', () => {
|
|
42
|
+
log.info(`Found ${catalog.length} agent skill${catalog.length > 1 ? 's' : ''}: ${catalog.map(s => s.name).join(', ')}`)
|
|
43
|
+
})
|
|
42
44
|
|
|
43
45
|
nuxt.options.runtimeConfig.skills = { catalog }
|
|
44
46
|
|
package/nuxt.schema.ts
CHANGED
|
@@ -297,6 +297,34 @@ export default defineNuxtSchema({
|
|
|
297
297
|
default: '',
|
|
298
298
|
required: ['', 'light', 'dark'],
|
|
299
299
|
}),
|
|
300
|
+
shortcuts: group({
|
|
301
|
+
title: 'Shortcuts',
|
|
302
|
+
description: 'Keyboard shortcuts configuration.',
|
|
303
|
+
icon: 'i-lucide-keyboard',
|
|
304
|
+
fields: {
|
|
305
|
+
toggleColorMode: field({
|
|
306
|
+
type: 'string',
|
|
307
|
+
title: 'Toggle Color Mode',
|
|
308
|
+
description: 'Shortcut to toggle light and dark mode (e.g., d, meta_d). Leave empty to disable.',
|
|
309
|
+
icon: 'i-lucide-keyboard',
|
|
310
|
+
default: 'd',
|
|
311
|
+
}),
|
|
312
|
+
},
|
|
313
|
+
}),
|
|
314
|
+
},
|
|
315
|
+
}),
|
|
316
|
+
search: group({
|
|
317
|
+
title: 'Search',
|
|
318
|
+
description: 'Search configuration.',
|
|
319
|
+
icon: 'i-lucide-search',
|
|
320
|
+
fields: {
|
|
321
|
+
fts: field({
|
|
322
|
+
type: 'boolean',
|
|
323
|
+
title: 'Full-Text Search',
|
|
324
|
+
description: 'Use SQLite FTS5 full-text search instead of Fuse.js. Requires @nuxt/content v3.14+.',
|
|
325
|
+
icon: 'i-lucide-database',
|
|
326
|
+
default: false,
|
|
327
|
+
}),
|
|
300
328
|
},
|
|
301
329
|
}),
|
|
302
330
|
assistant: group({
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "docus",
|
|
3
3
|
"description": "Nuxt layer for Docus documentation theme",
|
|
4
|
-
"version": "5.
|
|
4
|
+
"version": "5.12.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./nuxt.config.ts",
|
|
7
7
|
"repository": {
|
|
@@ -23,44 +23,63 @@
|
|
|
23
23
|
"README.md"
|
|
24
24
|
],
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@
|
|
27
|
-
"@
|
|
28
|
-
"@
|
|
29
|
-
"@
|
|
30
|
-
"@iconify-json/simple-icons": "^1.2.79",
|
|
31
|
-
"@iconify-json/vscode-icons": "^1.2.45",
|
|
32
|
-
"@nuxt/content": "^3.13.0",
|
|
26
|
+
"@iconify-json/lucide": "^1.2.111",
|
|
27
|
+
"@iconify-json/simple-icons": "^1.2.85",
|
|
28
|
+
"@iconify-json/vscode-icons": "^1.2.53",
|
|
29
|
+
"@nuxt/content": "^3.14.0",
|
|
33
30
|
"@nuxt/image": "^2.0.0",
|
|
34
|
-
"@nuxt/kit": "^4.4.
|
|
35
|
-
"@nuxt/ui": "^4.
|
|
36
|
-
"@nuxtjs/i18n": "^10.
|
|
37
|
-
"@nuxtjs/mcp-toolkit": "^0.
|
|
38
|
-
"@nuxtjs/mdc": "^0.
|
|
39
|
-
"@nuxtjs/robots": "^6.0.
|
|
40
|
-
"@shikijs/core": "^4.0
|
|
41
|
-
"@shikijs/engine-javascript": "^4.0
|
|
42
|
-
"@shikijs/langs": "^4.0
|
|
43
|
-
"@shikijs/
|
|
44
|
-
"@
|
|
45
|
-
"@
|
|
46
|
-
"
|
|
31
|
+
"@nuxt/kit": "^4.4.7",
|
|
32
|
+
"@nuxt/ui": "^4.8.1",
|
|
33
|
+
"@nuxtjs/i18n": "^10.4.0",
|
|
34
|
+
"@nuxtjs/mcp-toolkit": "^0.17.2",
|
|
35
|
+
"@nuxtjs/mdc": "^0.22.0",
|
|
36
|
+
"@nuxtjs/robots": "^6.0.9",
|
|
37
|
+
"@shikijs/core": "^4.2.0",
|
|
38
|
+
"@shikijs/engine-javascript": "^4.2.0",
|
|
39
|
+
"@shikijs/langs": "^4.2.0",
|
|
40
|
+
"@shikijs/stream": "^4.2.0",
|
|
41
|
+
"@shikijs/themes": "^4.2.0",
|
|
42
|
+
"@takumi-rs/core": "^1.6.0",
|
|
43
|
+
"@vueuse/core": "^14.3.0",
|
|
47
44
|
"defu": "^6.1.7",
|
|
48
45
|
"exsolve": "^1.0.8",
|
|
49
46
|
"git-url-parse": "^16.1.0",
|
|
50
47
|
"motion-v": "^2.2.1",
|
|
51
48
|
"nuxt-llms": "^0.2.0",
|
|
52
|
-
"nuxt-og-image": "^6.
|
|
53
|
-
"
|
|
49
|
+
"nuxt-og-image": "^6.5.1",
|
|
50
|
+
"pathe": "^2.0.3",
|
|
51
|
+
"pkg-types": "^2.3.1",
|
|
54
52
|
"scule": "^1.3.0",
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"zod": "^4.3.6",
|
|
53
|
+
"tailwindcss": "^4.3.0",
|
|
54
|
+
"ufo": "^1.6.4",
|
|
55
|
+
"yaml": "^2.9.0",
|
|
56
|
+
"zod": "^4.4.3",
|
|
60
57
|
"zod-to-json-schema": "^3.25.2"
|
|
61
58
|
},
|
|
62
59
|
"peerDependencies": {
|
|
60
|
+
"@ai-sdk/gateway": "^3.0.120",
|
|
61
|
+
"@ai-sdk/mcp": "^1.0.43",
|
|
62
|
+
"@ai-sdk/vue": "^3.0.191",
|
|
63
|
+
"@comark/nuxt": "^0.3.1",
|
|
64
|
+
"ai": "^6.0.191",
|
|
63
65
|
"better-sqlite3": "12.x",
|
|
64
66
|
"nuxt": "4.x"
|
|
67
|
+
},
|
|
68
|
+
"peerDependenciesMeta": {
|
|
69
|
+
"@ai-sdk/gateway": {
|
|
70
|
+
"optional": true
|
|
71
|
+
},
|
|
72
|
+
"@ai-sdk/mcp": {
|
|
73
|
+
"optional": true
|
|
74
|
+
},
|
|
75
|
+
"@ai-sdk/vue": {
|
|
76
|
+
"optional": true
|
|
77
|
+
},
|
|
78
|
+
"@comark/nuxt": {
|
|
79
|
+
"optional": true
|
|
80
|
+
},
|
|
81
|
+
"ai": {
|
|
82
|
+
"optional": true
|
|
83
|
+
}
|
|
65
84
|
}
|
|
66
85
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { z } from 'zod'
|
|
2
|
-
import { queryCollection } from '@nuxt/content/server'
|
|
3
1
|
import type { Collections } from '@nuxt/content'
|
|
4
|
-
import {
|
|
2
|
+
import { queryCollection } from '@nuxt/content/server'
|
|
3
|
+
import { joinURL } from 'ufo'
|
|
4
|
+
import { z } from 'zod'
|
|
5
5
|
import { inferSiteURL } from '../../../utils/meta'
|
|
6
|
+
import { getAvailableLocales, getCollectionFromPath, isNavigationPath } from '../../utils/content'
|
|
6
7
|
|
|
7
8
|
export default defineMcpTool({
|
|
8
9
|
description: `Retrieves the full content and details of a specific documentation page.
|
|
@@ -24,7 +25,9 @@ WORKFLOW: This tool returns the complete page content including title, descripti
|
|
|
24
25
|
openWorldHint: false,
|
|
25
26
|
},
|
|
26
27
|
inputSchema: {
|
|
27
|
-
path: z.string().describe(
|
|
28
|
+
path: z.string().describe(
|
|
29
|
+
'The page path from list-pages or provided by the user (e.g., /en/getting-started/installation)',
|
|
30
|
+
),
|
|
28
31
|
},
|
|
29
32
|
inputExamples: [
|
|
30
33
|
{ path: '/en/getting-started/installation' },
|
|
@@ -32,12 +35,18 @@ WORKFLOW: This tool returns the complete page content including title, descripti
|
|
|
32
35
|
],
|
|
33
36
|
cache: '1h',
|
|
34
37
|
handler: async ({ path }) => {
|
|
38
|
+
if (isNavigationPath(path)) {
|
|
39
|
+
throw createError({ statusCode: 404, message: 'Page not found' })
|
|
40
|
+
}
|
|
41
|
+
|
|
35
42
|
const event = useEvent()
|
|
36
|
-
const config = useRuntimeConfig(event)
|
|
43
|
+
const config = useRuntimeConfig(event)
|
|
44
|
+
const publicConfig = config.public
|
|
37
45
|
const siteUrl = getRequestURL(event).origin || inferSiteURL()
|
|
46
|
+
const baseURL = config.app?.baseURL || '/'
|
|
38
47
|
|
|
39
|
-
const availableLocales = getAvailableLocales(
|
|
40
|
-
const collectionName =
|
|
48
|
+
const availableLocales = getAvailableLocales(publicConfig)
|
|
49
|
+
const collectionName = publicConfig.i18n?.locales
|
|
41
50
|
? getCollectionFromPath(path, availableLocales)
|
|
42
51
|
: 'docs'
|
|
43
52
|
|
|
@@ -58,7 +67,7 @@ WORKFLOW: This tool returns the complete page content including title, descripti
|
|
|
58
67
|
path: page.path,
|
|
59
68
|
description: page.description,
|
|
60
69
|
content,
|
|
61
|
-
url:
|
|
70
|
+
url: siteUrl ? joinURL(siteUrl, baseURL, page.path) : joinURL(baseURL, page.path),
|
|
62
71
|
}
|
|
63
72
|
}
|
|
64
73
|
catch (error) {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { z } from 'zod'
|
|
2
|
-
import { queryCollection } from '@nuxt/content/server'
|
|
3
1
|
import type { Collections } from '@nuxt/content'
|
|
4
|
-
import {
|
|
2
|
+
import { queryCollection } from '@nuxt/content/server'
|
|
3
|
+
import { joinURL } from 'ufo'
|
|
4
|
+
import { z } from 'zod'
|
|
5
5
|
import { inferSiteURL } from '../../../utils/meta'
|
|
6
|
+
import { getAvailableLocales, getCollectionsToQuery } from '../../utils/content'
|
|
6
7
|
|
|
7
8
|
export default defineMcpTool({
|
|
8
9
|
description: `Lists all available documentation pages with their categories and basic information.
|
|
@@ -39,16 +40,19 @@ OUTPUT: Returns a structured list with:
|
|
|
39
40
|
cache: '1h',
|
|
40
41
|
handler: async ({ locale }) => {
|
|
41
42
|
const event = useEvent()
|
|
42
|
-
const config = useRuntimeConfig(event)
|
|
43
|
+
const config = useRuntimeConfig(event)
|
|
44
|
+
const publicConfig = config.public
|
|
43
45
|
|
|
44
46
|
const siteUrl = getRequestURL(event).origin || inferSiteURL()
|
|
45
|
-
const
|
|
47
|
+
const baseURL = config.app?.baseURL || '/'
|
|
48
|
+
const availableLocales = getAvailableLocales(publicConfig)
|
|
46
49
|
const collections = getCollectionsToQuery(locale, availableLocales)
|
|
47
50
|
|
|
48
51
|
try {
|
|
49
52
|
const allPages = await Promise.all(
|
|
50
53
|
collections.map(async (collectionName) => {
|
|
51
54
|
const pages = await queryCollection(event, collectionName as keyof Collections)
|
|
55
|
+
.where('path', 'NOT LIKE', '%.navigation')
|
|
52
56
|
.select('title', 'path', 'description')
|
|
53
57
|
.all()
|
|
54
58
|
|
|
@@ -57,7 +61,7 @@ OUTPUT: Returns a structured list with:
|
|
|
57
61
|
path: page.path,
|
|
58
62
|
description: page.description,
|
|
59
63
|
locale: collectionName.replace('docs_', ''),
|
|
60
|
-
url:
|
|
64
|
+
url: siteUrl ? joinURL(siteUrl, baseURL, page.path) : joinURL(baseURL, page.path),
|
|
61
65
|
}))
|
|
62
66
|
}),
|
|
63
67
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { queryCollection } from '@nuxt/content/server'
|
|
2
|
-
import { getAvailableLocales, getCollectionsToQuery } from '../utils/content'
|
|
3
2
|
import { inferSiteURL } from '../../utils/meta'
|
|
3
|
+
import { getAvailableLocales, getCollectionsToQuery, isNavigationPath } from '../utils/content'
|
|
4
4
|
|
|
5
5
|
interface SitemapUrl {
|
|
6
6
|
loc: string
|
|
@@ -27,17 +27,20 @@ export default defineEventHandler(async (event) => {
|
|
|
27
27
|
|
|
28
28
|
for (const collection of collections) {
|
|
29
29
|
try {
|
|
30
|
-
const pages = await (queryCollection as unknown as (
|
|
30
|
+
const pages = await (queryCollection as unknown as (
|
|
31
|
+
event: unknown,
|
|
32
|
+
collection: string,
|
|
33
|
+
) => { all: () => Promise<Array<Record<string, unknown> & { path?: string }>> })(event, collection).all()
|
|
31
34
|
|
|
32
35
|
for (const page of pages) {
|
|
33
|
-
const meta = page as Record<string, unknown>
|
|
36
|
+
const meta = page.meta as Record<string, unknown>
|
|
34
37
|
const pagePath = page.path || '/'
|
|
35
38
|
|
|
36
39
|
// Skip pages with sitemap: false in frontmatter
|
|
37
40
|
if (meta.sitemap === false) continue
|
|
38
41
|
|
|
39
42
|
// Skip .navigation files (used for navigation configuration)
|
|
40
|
-
if (
|
|
43
|
+
if (isNavigationPath(pagePath)) continue
|
|
41
44
|
|
|
42
45
|
const urlEntry: SitemapUrl = {
|
|
43
46
|
loc: pagePath,
|
package/server/utils/content.ts
CHANGED
|
@@ -25,6 +25,10 @@ export function getCollectionsToQuery(locale: string | undefined, availableLocal
|
|
|
25
25
|
: ['docs']
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
export function isNavigationPath(path: string): boolean {
|
|
29
|
+
return path.endsWith('.navigation') || path.includes('/.navigation/')
|
|
30
|
+
}
|
|
31
|
+
|
|
28
32
|
export function getCollectionFromPath(path: string, availableLocales: string[]): string {
|
|
29
33
|
const pathSegments = path.split('/').filter(Boolean)
|
|
30
34
|
const firstSegment = pathSegments[0]
|