docus 5.8.1 → 5.10.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.
Files changed (75) hide show
  1. package/README.md +1 -1
  2. package/app/app.config.ts +13 -0
  3. package/app/app.vue +5 -1
  4. package/app/components/OgImage/Docs.takumi.vue +43 -0
  5. package/app/components/OgImage/Landing.takumi.vue +67 -0
  6. package/app/components/app/AppFooterRight.vue +4 -1
  7. package/app/components/app/AppHeader.vue +6 -7
  8. package/app/components/app/AppHeaderBody.vue +6 -2
  9. package/app/components/app/AppHeaderBottom.vue +6 -2
  10. package/app/components/app/AppHeaderLeft.vue +16 -0
  11. package/app/components/docs/DocsAsideLeftBody.vue +6 -1
  12. package/app/components/docs/DocsAsideMobileBar.vue +11 -1
  13. package/app/components/docs/DocsAsideRight.vue +6 -1
  14. package/app/composables/useDocusColorMode.ts +7 -0
  15. package/app/composables/useUIConfig.ts +30 -0
  16. package/app/error.vue +3 -0
  17. package/app/middleware/colorMode.global.ts +8 -0
  18. package/app/pages/[[lang]]/[...slug].vue +16 -12
  19. package/app/templates/landing.vue +3 -3
  20. package/app/utils/ogImage.ts +23 -0
  21. package/i18n/locales/ar.json +27 -0
  22. package/i18n/locales/be.json +27 -0
  23. package/i18n/locales/bg.json +27 -0
  24. package/i18n/locales/bn.json +27 -0
  25. package/i18n/locales/ca.json +27 -0
  26. package/i18n/locales/ckb.json +32 -1
  27. package/i18n/locales/cs.json +27 -0
  28. package/i18n/locales/da.json +27 -0
  29. package/i18n/locales/de.json +27 -0
  30. package/i18n/locales/el.json +27 -0
  31. package/i18n/locales/es.json +27 -0
  32. package/i18n/locales/et.json +27 -0
  33. package/i18n/locales/fi.json +27 -0
  34. package/i18n/locales/he.json +27 -0
  35. package/i18n/locales/hi.json +27 -0
  36. package/i18n/locales/hy.json +27 -0
  37. package/i18n/locales/id.json +27 -0
  38. package/i18n/locales/it.json +27 -0
  39. package/i18n/locales/ja.json +27 -0
  40. package/i18n/locales/kk.json +27 -0
  41. package/i18n/locales/km.json +27 -0
  42. package/i18n/locales/ko.json +27 -0
  43. package/i18n/locales/ky.json +27 -0
  44. package/i18n/locales/lb.json +27 -0
  45. package/i18n/locales/ms.json +27 -0
  46. package/i18n/locales/nb.json +27 -0
  47. package/i18n/locales/nl.json +27 -0
  48. package/i18n/locales/pl.json +27 -0
  49. package/i18n/locales/pt-BR.json +27 -0
  50. package/i18n/locales/ro.json +27 -0
  51. package/i18n/locales/ru.json +27 -0
  52. package/i18n/locales/si.json +27 -0
  53. package/i18n/locales/sl.json +27 -0
  54. package/i18n/locales/sv.json +27 -0
  55. package/i18n/locales/tr.json +27 -0
  56. package/i18n/locales/uk.json +27 -0
  57. package/i18n/locales/ur.json +27 -0
  58. package/i18n/locales/vi.json +27 -0
  59. package/i18n/locales/zh-CN.json +27 -0
  60. package/index.d.ts +33 -0
  61. package/modules/assistant/README.md +17 -8
  62. package/modules/assistant/index.ts +26 -16
  63. package/modules/assistant/runtime/server/api/search.ts +5 -0
  64. package/modules/config.ts +7 -2
  65. package/modules/css.ts +12 -0
  66. package/modules/skills/index.ts +158 -0
  67. package/modules/skills/runtime/server/routes/skills-files.ts +49 -0
  68. package/modules/skills/runtime/server/routes/skills-index.ts +8 -0
  69. package/nuxt.config.ts +8 -2
  70. package/nuxt.schema.ts +22 -0
  71. package/package.json +24 -21
  72. package/server/mcp/tools/get-page.ts +16 -5
  73. package/server/mcp/tools/list-pages.ts +13 -3
  74. package/app/components/OgImage/OgImageDocs.vue +0 -76
  75. package/app/components/OgImage/OgImageLanding.vue +0 -98
@@ -1,4 +1,5 @@
1
1
  import { addComponent, addImports, addServerHandler, createResolver, defineNuxtModule, logger } from '@nuxt/kit'
2
+ import { defu } from 'defu'
2
3
 
3
4
  export interface AssistantModuleOptions {
4
5
  /**
@@ -22,24 +23,33 @@ export interface AssistantModuleOptions {
22
23
 
23
24
  const log = logger.withTag('Docus')
24
25
 
26
+ const defaults: Required<AssistantModuleOptions> = {
27
+ apiPath: '/__docus__/assistant',
28
+ mcpServer: '/mcp',
29
+ model: 'google/gemini-3-flash',
30
+ }
31
+
25
32
  export default defineNuxtModule<AssistantModuleOptions>({
26
33
  meta: {
27
34
  name: 'assistant',
28
- configKey: 'assistant',
29
35
  },
30
- defaults: {
31
- apiPath: '/__docus__/assistant',
32
- mcpServer: '/mcp',
33
- model: 'google/gemini-3-flash',
34
- },
35
- setup(options, nuxt) {
36
- const hasApiKey = !!process.env.AI_GATEWAY_API_KEY
36
+ setup(_options, nuxt) {
37
+ const legacyOptions = nuxt.options.assistant
38
+ if (legacyOptions && Object.keys(legacyOptions).length > 0) {
39
+ log.warn('`assistant` top-level config is deprecated. Move it under `docus.assistant` in nuxt.config.ts')
40
+ }
41
+
42
+ const options = defu(nuxt.options.docus?.assistant, legacyOptions, defaults) as Required<AssistantModuleOptions>
43
+
44
+ const hasAiGatewayAuth = !!(
45
+ process.env.AI_GATEWAY_API_KEY || process.env.VERCEL_OIDC_TOKEN
46
+ )
37
47
 
38
48
  const { resolve } = createResolver(import.meta.url)
39
49
 
40
50
  nuxt.options.runtimeConfig.public.assistant = {
41
- enabled: hasApiKey,
42
- apiPath: options.apiPath!,
51
+ enabled: hasAiGatewayAuth,
52
+ apiPath: options.apiPath,
43
53
  }
44
54
 
45
55
  addImports([
@@ -60,23 +70,23 @@ export default defineNuxtModule<AssistantModuleOptions>({
60
70
  components.forEach(name =>
61
71
  addComponent({
62
72
  name,
63
- filePath: hasApiKey
73
+ filePath: hasAiGatewayAuth
64
74
  ? resolve(`./runtime/components/${name}.vue`)
65
75
  : resolve('./runtime/components/AssistantChatDisabled.vue'),
66
76
  }),
67
77
  )
68
78
 
69
- if (!hasApiKey) {
70
- log.warn('AI assistant disabled: AI_GATEWAY_API_KEY not found')
79
+ if (!hasAiGatewayAuth) {
80
+ log.warn('AI assistant disabled: neither AI_GATEWAY_API_KEY nor VERCEL_OIDC_TOKEN found')
71
81
  return
72
82
  }
73
83
 
74
84
  nuxt.options.runtimeConfig.assistant = {
75
- mcpServer: options.mcpServer!,
76
- model: options.model!,
85
+ mcpServer: options.mcpServer,
86
+ model: options.model,
77
87
  }
78
88
 
79
- const routePath = options.apiPath!.replace(/^\//, '')
89
+ const routePath = options.apiPath.replace(/^\//, '')
80
90
  addServerHandler({
81
91
  route: `/${routePath}`,
82
92
  handler: resolve('./runtime/server/api/search'),
@@ -37,6 +37,11 @@ function getSystemPrompt(siteName: string) {
37
37
  - Be concise, helpful, and direct
38
38
  - Guide users like a friendly expert would
39
39
 
40
+ **Links and exploration:**
41
+ - Tool results include a \`url\` for each page — prefer markdown links \`[label](url)\` so users can open the doc in one click
42
+ - When it helps, add extra links (related pages, “read more”, side topics) — make the answer easy to dig into, not a wall of text
43
+ - Stick to URLs from tool results (\`url\` / \`path\`) so links stay valid
44
+
40
45
  **FORMATTING RULES (CRITICAL):**
41
46
  - NEVER use markdown headings (#, ##, ###, etc.)
42
47
  - Use **bold text** for emphasis and section labels
package/modules/config.ts CHANGED
@@ -58,13 +58,18 @@ export default defineNuxtModule({
58
58
  branch: getGitBranch(),
59
59
  })
60
60
 
61
+ const forcedColorMode = (nuxt.options.appConfig.docus as Record<string, unknown>)?.colorMode as string | undefined
62
+ if (forcedColorMode === 'light' || forcedColorMode === 'dark') {
63
+ nuxt.options.colorMode = defu({ preference: forcedColorMode, fallback: forcedColorMode }, nuxt.options.colorMode || {}) as typeof nuxt.options.colorMode
64
+ }
65
+
61
66
  /*
62
67
  ** I18N
63
68
  */
64
- const typedNuxtOptions = nuxt.options as typeof nuxt.options & { i18n?: DocusI18nOptions }
69
+ const typedNuxtOptions = nuxt.options as typeof nuxt.options & { i18n?: false | DocusI18nOptions }
65
70
  const i18nOptions = typedNuxtOptions.i18n
66
71
 
67
- if (i18nOptions?.locales) {
72
+ if (i18nOptions && typeof i18nOptions === 'object' && i18nOptions.locales) {
68
73
  const { resolve } = createResolver(import.meta.url)
69
74
 
70
75
  // Filter locales to only include existing ones
package/modules/css.ts CHANGED
@@ -32,5 +32,17 @@ export default defineNuxtModule({
32
32
  if (Array.isArray(nuxt.options.css)) {
33
33
  nuxt.options.css.unshift(cssTemplate.dst)
34
34
  }
35
+
36
+ // Noisy Vite warnings
37
+ const sourcemapWarnIgnore = ['@tailwindcss/vite:generate:build', 'nuxt:module-preload-polyfill']
38
+ nuxt.hook('vite:extendConfig', (config) => {
39
+ const logger = config.customLogger
40
+ if (!logger) return
41
+ const originalWarn = logger.warn.bind(logger)
42
+ logger.warn = (msg, options) => {
43
+ if (sourcemapWarnIgnore.some(p => msg.includes(p))) return
44
+ originalWarn(msg, options)
45
+ }
46
+ })
35
47
  },
36
48
  })
@@ -0,0 +1,158 @@
1
+ import { addPrerenderRoutes, addServerHandler, createResolver, defineNuxtModule, logger } from '@nuxt/kit'
2
+ import { defu } from 'defu'
3
+ import { existsSync } from 'node:fs'
4
+ import { readdir, readFile } from 'node:fs/promises'
5
+ import { join } from 'node:path'
6
+ import type { NitroConfig } from 'nitropack'
7
+ import { parse as parseYaml } from 'yaml'
8
+
9
+ interface SkillEntry {
10
+ name: string
11
+ description: string
12
+ files: string[]
13
+ }
14
+
15
+ export interface SkillsModuleOptions {
16
+ dir?: string
17
+ }
18
+
19
+ const SKILL_NAME_REGEX = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/
20
+ const MAX_NAME_LENGTH = 64
21
+
22
+ const log = logger.withTag('Docus')
23
+
24
+ const defaults: Required<SkillsModuleOptions> = {
25
+ dir: 'skills',
26
+ }
27
+
28
+ export default defineNuxtModule<SkillsModuleOptions>({
29
+ meta: {
30
+ name: 'skills',
31
+ },
32
+ async setup(_inlineOptions, nuxt) {
33
+ const options = defu(nuxt.options.docus?.skills, defaults) as Required<SkillsModuleOptions>
34
+
35
+ const skillsDir = join(nuxt.options.rootDir, options.dir)
36
+ if (!existsSync(skillsDir)) return
37
+
38
+ const catalog = await scanSkills(skillsDir)
39
+ if (!catalog.length) return
40
+
41
+ log.info(`Found ${catalog.length} agent skill${catalog.length > 1 ? 's' : ''}: ${catalog.map(s => s.name).join(', ')}`)
42
+
43
+ nuxt.options.runtimeConfig.skills = { catalog }
44
+
45
+ const { resolve } = createResolver(import.meta.url)
46
+
47
+ const onNitroConfig = nuxt.hook as (name: 'nitro:config', cb: (nitroConfig: NitroConfig) => void) => void
48
+ onNitroConfig('nitro:config', (nitroConfig) => {
49
+ nitroConfig.serverAssets ||= []
50
+ nitroConfig.serverAssets.push({ baseName: 'skills', dir: skillsDir })
51
+ })
52
+
53
+ const prerenderRoutes = ['/.well-known/skills/index.json']
54
+ for (const skill of catalog) {
55
+ for (const file of skill.files) {
56
+ prerenderRoutes.push(`/.well-known/skills/${skill.name}/${file}`)
57
+ }
58
+ }
59
+ addPrerenderRoutes(prerenderRoutes)
60
+
61
+ addServerHandler({
62
+ route: '/.well-known/skills/index.json',
63
+ handler: resolve('./runtime/server/routes/skills-index'),
64
+ })
65
+
66
+ addServerHandler({
67
+ route: '/.well-known/skills/**',
68
+ handler: resolve('./runtime/server/routes/skills-files'),
69
+ })
70
+ },
71
+ })
72
+
73
+ function parseFrontmatter(content: string): { name?: string, description?: string } | null {
74
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/)
75
+ if (!match?.[1]) return null
76
+ try {
77
+ return parseYaml(match[1])
78
+ }
79
+ catch {
80
+ return null
81
+ }
82
+ }
83
+
84
+ function validateSkillName(name: string, dirName: string): boolean {
85
+ if (name.length > MAX_NAME_LENGTH) {
86
+ log.warn(`Skill "${name}" exceeds ${MAX_NAME_LENGTH} character limit`)
87
+ return false
88
+ }
89
+ if (!SKILL_NAME_REGEX.test(name) || name.includes('--')) {
90
+ log.warn(`Skill name "${name}" does not match the Agent Skills naming spec`)
91
+ return false
92
+ }
93
+ if (name !== dirName) {
94
+ log.warn(`Skill name "${name}" does not match directory name "${dirName}"`)
95
+ return false
96
+ }
97
+ return true
98
+ }
99
+
100
+ async function listFilesRecursively(dir: string, base: string = ''): Promise<string[]> {
101
+ const files: string[] = []
102
+ const entries = await readdir(dir, { withFileTypes: true })
103
+ for (const entry of entries) {
104
+ const relPath = base ? `${base}/${entry.name}` : entry.name
105
+ if (entry.isDirectory()) {
106
+ files.push(...await listFilesRecursively(join(dir, entry.name), relPath))
107
+ }
108
+ else {
109
+ files.push(relPath)
110
+ }
111
+ }
112
+ return files
113
+ }
114
+
115
+ async function scanSkills(skillsDir: string): Promise<SkillEntry[]> {
116
+ const catalog: SkillEntry[] = []
117
+ const entries = await readdir(skillsDir, { withFileTypes: true })
118
+
119
+ for (const entry of entries) {
120
+ if (!entry.isDirectory()) continue
121
+
122
+ const skillDir = join(skillsDir, entry.name)
123
+ const skillMdPath = join(skillDir, 'SKILL.md')
124
+
125
+ if (!existsSync(skillMdPath)) continue
126
+
127
+ const content = await readFile(skillMdPath, 'utf-8')
128
+ const frontmatter = parseFrontmatter(content)
129
+
130
+ if (!frontmatter?.description) {
131
+ log.warn(`Skipping skill "${entry.name}": missing description in SKILL.md frontmatter`)
132
+ continue
133
+ }
134
+
135
+ const name = frontmatter.name || entry.name
136
+ if (!validateSkillName(name, entry.name)) continue
137
+
138
+ const allFiles = await listFilesRecursively(skillDir)
139
+ const files = allFiles.filter(f => !f.split('/').some(s => s.startsWith('.')))
140
+ const sortedFiles = ['SKILL.md', ...files.filter(f => f !== 'SKILL.md')]
141
+
142
+ catalog.push({
143
+ name,
144
+ description: frontmatter.description,
145
+ files: sortedFiles,
146
+ })
147
+ }
148
+
149
+ return catalog
150
+ }
151
+
152
+ declare module 'nuxt/schema' {
153
+ interface RuntimeConfig {
154
+ skills: {
155
+ catalog: SkillEntry[]
156
+ }
157
+ }
158
+ }
@@ -0,0 +1,49 @@
1
+ const CONTENT_TYPES: Record<string, string> = {
2
+ '.md': 'text/markdown; charset=utf-8',
3
+ '.json': 'application/json; charset=utf-8',
4
+ '.yaml': 'text/yaml; charset=utf-8',
5
+ '.yml': 'text/yaml; charset=utf-8',
6
+ '.txt': 'text/plain; charset=utf-8',
7
+ '.py': 'text/plain; charset=utf-8',
8
+ '.sh': 'text/plain; charset=utf-8',
9
+ '.js': 'text/javascript; charset=utf-8',
10
+ '.ts': 'text/plain; charset=utf-8',
11
+ }
12
+
13
+ function getContentType(path: string): string {
14
+ const ext = path.slice(path.lastIndexOf('.'))
15
+ return CONTENT_TYPES[ext] || 'application/octet-stream'
16
+ }
17
+
18
+ export default defineEventHandler(async (event) => {
19
+ const url = getRequestURL(event)
20
+ const prefix = '/.well-known/skills/'
21
+ const idx = url.pathname.indexOf(prefix)
22
+ if (idx === -1) {
23
+ throw createError({ statusCode: 404, statusMessage: 'Not Found' })
24
+ }
25
+
26
+ const filePath = decodeURIComponent(url.pathname.slice(idx + prefix.length))
27
+
28
+ if (!filePath || filePath.includes('..')) {
29
+ throw createError({ statusCode: 400, statusMessage: 'Bad Request' })
30
+ }
31
+
32
+ const { skills } = useRuntimeConfig(event)
33
+ const skillName = filePath.split('/')[0]
34
+ if (!skills.catalog.some((s: { name: string }) => s.name === skillName)) {
35
+ throw createError({ statusCode: 404, statusMessage: 'Not Found' })
36
+ }
37
+
38
+ const storage = useStorage('assets:skills')
39
+ const content = await storage.getItemRaw<string>(filePath)
40
+
41
+ if (!content) {
42
+ throw createError({ statusCode: 404, statusMessage: 'Not Found' })
43
+ }
44
+
45
+ setResponseHeader(event, 'content-type', getContentType(filePath))
46
+ setResponseHeader(event, 'cache-control', 'public, max-age=3600')
47
+
48
+ return content
49
+ })
@@ -0,0 +1,8 @@
1
+ export default defineEventHandler((event) => {
2
+ const { skills } = useRuntimeConfig(event)
3
+
4
+ setResponseHeader(event, 'content-type', 'application/json')
5
+ setResponseHeader(event, 'cache-control', 'public, max-age=3600')
6
+
7
+ return { skills: skills.catalog }
8
+ })
package/nuxt.config.ts CHANGED
@@ -10,6 +10,7 @@ export default defineNuxtConfig({
10
10
  resolve('./modules/config'),
11
11
  resolve('./modules/routing'),
12
12
  resolve('./modules/markdown-rewrite'),
13
+ resolve('./modules/skills'),
13
14
  resolve('./modules/css'),
14
15
  () => {
15
16
  const nuxt = useNuxt()
@@ -38,9 +39,11 @@ export default defineNuxtConfig({
38
39
 
39
40
  // Fix @vercel/oidc ESM export issue (transitive dep of @ai-sdk/gateway)
40
41
  // Only needed when AI assistant is enabled.
41
- if (process.env.AI_GATEWAY_API_KEY) {
42
+ if (process.env.AI_GATEWAY_API_KEY || process.env.VERCEL_OIDC_TOKEN) {
42
43
  config.optimizeDeps.include.push('@vercel/oidc')
43
- config.optimizeDeps.include.map(id => id.replace(/^@vercel\/oidc$/, 'docus > @vercel/oidc'))
44
+ config.optimizeDeps.include = config.optimizeDeps.include.map(id =>
45
+ id.replace(/^@vercel\/oidc$/, 'docus > @vercel/oidc'),
46
+ )
44
47
  }
45
48
  })
46
49
  },
@@ -118,6 +121,9 @@ export default defineNuxtConfig({
118
121
  },
119
122
  provider: 'iconify',
120
123
  },
124
+ ogImage: {
125
+ zeroRuntime: true,
126
+ },
121
127
  robots: {
122
128
  groups: [
123
129
  {
package/nuxt.schema.ts CHANGED
@@ -277,6 +277,28 @@ export default defineNuxtSchema({
277
277
  }),
278
278
  },
279
279
  }),
280
+ docus: group({
281
+ title: 'Docus',
282
+ description: 'Docus configuration.',
283
+ icon: 'i-lucide-settings',
284
+ fields: {
285
+ locale: field({
286
+ type: 'string',
287
+ title: 'Locale',
288
+ description: 'Default locale for single-language documentation.',
289
+ icon: 'i-lucide-languages',
290
+ default: 'en',
291
+ }),
292
+ colorMode: field({
293
+ type: 'string',
294
+ title: 'Color Mode',
295
+ description: 'Force a specific color mode. Leave empty for system preference with toggle.',
296
+ icon: 'i-lucide-monitor',
297
+ default: '',
298
+ required: ['', 'light', 'dark'],
299
+ }),
300
+ },
301
+ }),
280
302
  assistant: group({
281
303
  title: 'Assistant',
282
304
  description: 'Assistant configuration.',
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.8.1",
4
+ "version": "5.10.0",
5
5
  "type": "module",
6
6
  "main": "./nuxt.config.ts",
7
7
  "repository": {
@@ -14,6 +14,7 @@
14
14
  "app",
15
15
  "i18n",
16
16
  "content.config.ts",
17
+ "index.d.ts",
17
18
  "modules",
18
19
  "nuxt.config.ts",
19
20
  "nuxt.schema.ts",
@@ -22,39 +23,41 @@
22
23
  "README.md"
23
24
  ],
24
25
  "dependencies": {
25
- "@ai-sdk/gateway": "^3.0.66",
26
- "@ai-sdk/mcp": "^1.0.25",
27
- "@ai-sdk/vue": "3.0.116",
28
- "@iconify-json/lucide": "^1.2.96",
29
- "@iconify-json/simple-icons": "^1.2.73",
26
+ "@ai-sdk/gateway": "^3.0.83",
27
+ "@ai-sdk/mcp": "^1.0.30",
28
+ "@ai-sdk/vue": "3.0.141",
29
+ "@iconify-json/lucide": "^1.2.100",
30
+ "@iconify-json/simple-icons": "^1.2.75",
30
31
  "@iconify-json/vscode-icons": "^1.2.45",
31
32
  "@nuxt/content": "^3.12.0",
32
33
  "@nuxt/image": "^2.0.0",
33
- "@nuxt/kit": "^4.3.1",
34
- "@nuxt/ui": "^4.5.1",
35
- "@nuxtjs/i18n": "^10.2.3",
36
- "@nuxtjs/mcp-toolkit": "^0.7.0",
37
- "@nuxtjs/mdc": "^0.20.2",
38
- "@nuxtjs/robots": "^5.7.1",
34
+ "@nuxt/kit": "^4.4.2",
35
+ "@nuxt/ui": "^4.6.0",
36
+ "@nuxtjs/i18n": "^10.2.4",
37
+ "@nuxtjs/mcp-toolkit": "^0.13.2",
38
+ "@nuxtjs/mdc": "^0.21.0",
39
+ "@nuxtjs/robots": "^6.0.6",
40
+ "@shikijs/core": "^4.0.2",
41
+ "@shikijs/engine-javascript": "^4.0.2",
42
+ "@shikijs/langs": "^4.0.2",
43
+ "@shikijs/themes": "^4.0.2",
44
+ "@takumi-rs/core": "^0.73.1",
39
45
  "@vueuse/core": "^14.2.1",
40
- "ai": "6.0.116",
46
+ "ai": "6.0.141",
41
47
  "defu": "^6.1.4",
42
48
  "exsolve": "^1.0.8",
43
49
  "git-url-parse": "^16.1.0",
44
- "motion-v": "^1.10.3",
50
+ "motion-v": "^2.2.0",
45
51
  "nuxt-llms": "^0.2.0",
46
- "nuxt-og-image": "^5.1.13",
52
+ "nuxt-og-image": "^6.3.1",
47
53
  "pkg-types": "^2.3.0",
48
54
  "scule": "^1.3.0",
49
- "@shikijs/core": "^3.22.0",
50
- "@shikijs/engine-javascript": "^3.22.0",
51
- "@shikijs/langs": "^3.22.0",
52
- "@shikijs/themes": "^3.22.0",
53
55
  "shiki-stream": "^0.1.4",
54
- "tailwindcss": "^4.2.1",
56
+ "tailwindcss": "^4.2.2",
55
57
  "ufo": "^1.6.3",
58
+ "yaml": "^2.7.1",
56
59
  "zod": "^4.3.6",
57
- "zod-to-json-schema": "^3.25.1"
60
+ "zod-to-json-schema": "^3.25.2"
58
61
  },
59
62
  "peerDependencies": {
60
63
  "better-sqlite3": "12.x",
@@ -17,9 +17,19 @@ WHEN NOT TO USE: If you don't know the exact path and need to search/explore, us
17
17
 
18
18
  WORKFLOW: This tool returns the complete page content including title, description, and full markdown. Use this when you need to provide detailed answers or code examples from specific documentation pages.
19
19
  `,
20
+ annotations: {
21
+ readOnlyHint: true,
22
+ destructiveHint: false,
23
+ idempotentHint: true,
24
+ openWorldHint: false,
25
+ },
20
26
  inputSchema: {
21
27
  path: z.string().describe('The page path from list-pages or provided by the user (e.g., /en/getting-started/installation)'),
22
28
  },
29
+ inputExamples: [
30
+ { path: '/en/getting-started/installation' },
31
+ { path: '/getting-started/introduction' },
32
+ ],
23
33
  cache: '1h',
24
34
  handler: async ({ path }) => {
25
35
  const event = useEvent()
@@ -38,21 +48,22 @@ WORKFLOW: This tool returns the complete page content including title, descripti
38
48
  .first()
39
49
 
40
50
  if (!page) {
41
- return errorResult('Page not found')
51
+ throw createError({ statusCode: 404, message: 'Page not found' })
42
52
  }
43
53
 
44
54
  const content = await event.$fetch<string>(`/raw${path}.md`)
45
55
 
46
- return jsonResult({
56
+ return {
47
57
  title: page.title,
48
58
  path: page.path,
49
59
  description: page.description,
50
60
  content,
51
61
  url: `${siteUrl}${page.path}`,
52
- })
62
+ }
53
63
  }
54
- catch {
55
- return errorResult('Failed to get page')
64
+ catch (error) {
65
+ if ((error as { statusCode?: number }).statusCode === 404) throw error
66
+ throw createError({ statusCode: 500, message: 'Failed to get page' })
56
67
  }
57
68
  },
58
69
  })
@@ -23,9 +23,19 @@ OUTPUT: Returns a structured list with:
23
23
  - path: Exact path for use with get-page
24
24
  - description: Brief summary of page content
25
25
  - url: Full URL for reference`,
26
+ annotations: {
27
+ readOnlyHint: true,
28
+ destructiveHint: false,
29
+ idempotentHint: true,
30
+ openWorldHint: false,
31
+ },
26
32
  inputSchema: {
27
- locale: z.string().optional().describe('The locale to filter pages by'),
33
+ locale: z.string().optional().describe('The locale to filter pages by (e.g., "en", "fr")'),
28
34
  },
35
+ inputExamples: [
36
+ { locale: 'en' },
37
+ {},
38
+ ],
29
39
  cache: '1h',
30
40
  handler: async ({ locale }) => {
31
41
  const event = useEvent()
@@ -52,10 +62,10 @@ OUTPUT: Returns a structured list with:
52
62
  }),
53
63
  )
54
64
 
55
- return jsonResult(allPages.flat())
65
+ return allPages.flat()
56
66
  }
57
67
  catch {
58
- return errorResult('Failed to list pages')
68
+ throw createError({ statusCode: 500, message: 'Failed to list pages' })
59
69
  }
60
70
  },
61
71
  })
@@ -1,76 +0,0 @@
1
- <script lang="ts" setup>
2
- const props = withDefaults(defineProps<{ title?: string, description?: string, headline?: string }>(), {
3
- title: 'title',
4
- description: 'description',
5
- })
6
-
7
- const title = (props.title || '').slice(0, 60)
8
- const description = (props.description || '').slice(0, 200)
9
- </script>
10
-
11
- <template>
12
- <div class="w-full h-full flex flex-col justify-center bg-neutral-900">
13
- <svg
14
- class="absolute right-0 top-0 opacity-50"
15
- width="629"
16
- height="593"
17
- viewBox="0 0 629 593"
18
- fill="none"
19
- xmlns="http://www.w3.org/2000/svg"
20
- >
21
- <g filter="url(#filter0_f_199_94966)">
22
- <path
23
- d="M628.5 -578L639.334 -94.4223L806.598 -548.281L659.827 -87.387L965.396 -462.344L676.925 -74.0787L1087.69 -329.501L688.776 -55.9396L1160.22 -164.149L694.095 -34.9354L1175.13 15.7948L692.306 -13.3422L1130.8 190.83L683.602 6.50012L1032.04 341.989L668.927 22.4412L889.557 452.891L649.872 32.7537L718.78 511.519L628.5 36.32L538.22 511.519L607.128 32.7537L367.443 452.891L588.073 22.4412L224.955 341.989L573.398 6.50012L126.198 190.83L564.694 -13.3422L81.8734 15.7948L562.905 -34.9354L96.7839 -164.149L568.224 -55.9396L169.314 -329.501L580.075 -74.0787L291.604 -462.344L597.173 -87.387L450.402 -548.281L617.666 -94.4223L628.5 -578Z"
24
- fill="white"
25
- />
26
- </g>
27
- <defs>
28
- <filter
29
- id="filter0_f_199_94966"
30
- x="0.873535"
31
- y="-659"
32
- width="1255.25"
33
- height="1251.52"
34
- filterUnits="userSpaceOnUse"
35
- color-interpolation-filters="sRGB"
36
- >
37
- <feFlood
38
- flood-opacity="0"
39
- result="BackgroundImageFix"
40
- />
41
- <feBlend
42
- mode="normal"
43
- in="SourceGraphic"
44
- in2="BackgroundImageFix"
45
- result="shape"
46
- />
47
- <feGaussianBlur
48
- stdDeviation="40.5"
49
- result="effect1_foregroundBlur_199_94966"
50
- />
51
- </filter>
52
- </defs>
53
- </svg>
54
-
55
- <div class="pl-[100px]">
56
- <p
57
- v-if="headline"
58
- class="uppercase text-[24px] text-emerald-500 mb-4 font-semibold"
59
- >
60
- {{ headline }}
61
- </p>
62
- <h1
63
- v-if="title"
64
- class="m-0 text-[75px] font-semibold mb-4 text-white flex items-center"
65
- >
66
- <span>{{ title }}</span>
67
- </h1>
68
- <p
69
- v-if="description"
70
- class="text-[32px] text-neutral-300 leading-tight w-[700px]"
71
- >
72
- {{ description }}
73
- </p>
74
- </div>
75
- </div>
76
- </template>