svedocs 0.1.0-beta.2

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 (192) hide show
  1. package/LICENSE +5 -0
  2. package/README.md +38 -0
  3. package/dist/ai/openai-compatible.d.ts +13 -0
  4. package/dist/ai/openai-compatible.d.ts.map +1 -0
  5. package/dist/ai/openai-compatible.js +92 -0
  6. package/dist/ai/providers.d.ts +19 -0
  7. package/dist/ai/providers.d.ts.map +1 -0
  8. package/dist/ai/providers.js +185 -0
  9. package/dist/ai/rate-limit.d.ts +18 -0
  10. package/dist/ai/rate-limit.d.ts.map +1 -0
  11. package/dist/ai/rate-limit.js +52 -0
  12. package/dist/ai/response.d.ts +4 -0
  13. package/dist/ai/response.d.ts.map +1 -0
  14. package/dist/ai/response.js +151 -0
  15. package/dist/ai/runtime.d.ts +30 -0
  16. package/dist/ai/runtime.d.ts.map +1 -0
  17. package/dist/ai/runtime.js +105 -0
  18. package/dist/ai/types.d.ts +93 -0
  19. package/dist/ai/types.d.ts.map +1 -0
  20. package/dist/ai/types.js +1 -0
  21. package/dist/ai.d.ts +7 -0
  22. package/dist/ai.d.ts.map +1 -0
  23. package/dist/ai.js +6 -0
  24. package/dist/ambient.d.ts +5 -0
  25. package/dist/cloudflare.d.ts +27 -0
  26. package/dist/cloudflare.d.ts.map +1 -0
  27. package/dist/cloudflare.js +146 -0
  28. package/dist/config.d.ts +311 -0
  29. package/dist/config.d.ts.map +1 -0
  30. package/dist/config.js +243 -0
  31. package/dist/core/checks.d.ts +4 -0
  32. package/dist/core/checks.d.ts.map +1 -0
  33. package/dist/core/checks.js +252 -0
  34. package/dist/core/config.d.ts +5 -0
  35. package/dist/core/config.d.ts.map +1 -0
  36. package/dist/core/config.js +182 -0
  37. package/dist/core/content.d.ts +7 -0
  38. package/dist/core/content.d.ts.map +1 -0
  39. package/dist/core/content.js +211 -0
  40. package/dist/core/links.d.ts +8 -0
  41. package/dist/core/links.d.ts.map +1 -0
  42. package/dist/core/links.js +94 -0
  43. package/dist/core/navigation.d.ts +5 -0
  44. package/dist/core/navigation.d.ts.map +1 -0
  45. package/dist/core/navigation.js +144 -0
  46. package/dist/core/search.d.ts +5 -0
  47. package/dist/core/search.d.ts.map +1 -0
  48. package/dist/core/search.js +41 -0
  49. package/dist/core/types.d.ts +269 -0
  50. package/dist/core/types.d.ts.map +1 -0
  51. package/dist/core/types.js +4 -0
  52. package/dist/core/utils.d.ts +12 -0
  53. package/dist/core/utils.d.ts.map +1 -0
  54. package/dist/core/utils.js +52 -0
  55. package/dist/core.d.ts +8 -0
  56. package/dist/core.d.ts.map +1 -0
  57. package/dist/core.js +7 -0
  58. package/dist/index.d.ts +6 -0
  59. package/dist/index.d.ts.map +1 -0
  60. package/dist/index.js +3 -0
  61. package/dist/mdx/ast.d.ts +32 -0
  62. package/dist/mdx/ast.d.ts.map +1 -0
  63. package/dist/mdx/ast.js +148 -0
  64. package/dist/mdx/code.d.ts +14 -0
  65. package/dist/mdx/code.d.ts.map +1 -0
  66. package/dist/mdx/code.js +315 -0
  67. package/dist/mdx/compile.d.ts +26 -0
  68. package/dist/mdx/compile.d.ts.map +1 -0
  69. package/dist/mdx/compile.js +63 -0
  70. package/dist/mdx/diff.d.ts +5 -0
  71. package/dist/mdx/diff.d.ts.map +1 -0
  72. package/dist/mdx/diff.js +160 -0
  73. package/dist/mdx/utils.d.ts +10 -0
  74. package/dist/mdx/utils.d.ts.map +1 -0
  75. package/dist/mdx/utils.js +64 -0
  76. package/dist/og/image.d.ts +21 -0
  77. package/dist/og/image.d.ts.map +1 -0
  78. package/dist/og/image.js +216 -0
  79. package/dist/og/metadata.d.ts +7 -0
  80. package/dist/og/metadata.d.ts.map +1 -0
  81. package/dist/og/metadata.js +160 -0
  82. package/dist/og/types.d.ts +59 -0
  83. package/dist/og/types.d.ts.map +1 -0
  84. package/dist/og/types.js +1 -0
  85. package/dist/og.d.ts +4 -0
  86. package/dist/og.d.ts.map +1 -0
  87. package/dist/og.js +3 -0
  88. package/dist/search/algolia.d.ts +10 -0
  89. package/dist/search/algolia.d.ts.map +1 -0
  90. package/dist/search/algolia.js +82 -0
  91. package/dist/search/cloudflare-sync.d.ts +5 -0
  92. package/dist/search/cloudflare-sync.d.ts.map +1 -0
  93. package/dist/search/cloudflare-sync.js +169 -0
  94. package/dist/search/cloudflare.d.ts +9 -0
  95. package/dist/search/cloudflare.d.ts.map +1 -0
  96. package/dist/search/cloudflare.js +115 -0
  97. package/dist/search/local.d.ts +9 -0
  98. package/dist/search/local.d.ts.map +1 -0
  99. package/dist/search/local.js +122 -0
  100. package/dist/search/runtime.d.ts +41 -0
  101. package/dist/search/runtime.d.ts.map +1 -0
  102. package/dist/search/runtime.js +151 -0
  103. package/dist/search/types.d.ts +172 -0
  104. package/dist/search/types.d.ts.map +1 -0
  105. package/dist/search/types.js +1 -0
  106. package/dist/search/typesense.d.ts +11 -0
  107. package/dist/search/typesense.d.ts.map +1 -0
  108. package/dist/search/typesense.js +91 -0
  109. package/dist/search/utils.d.ts +10 -0
  110. package/dist/search/utils.d.ts.map +1 -0
  111. package/dist/search/utils.js +55 -0
  112. package/dist/search.d.ts +8 -0
  113. package/dist/search.d.ts.map +1 -0
  114. package/dist/search.js +7 -0
  115. package/dist/svelte.d.ts +24 -0
  116. package/dist/svelte.d.ts.map +1 -0
  117. package/dist/svelte.js +51 -0
  118. package/dist/testing.d.ts +3 -0
  119. package/dist/testing.d.ts.map +1 -0
  120. package/dist/testing.js +23 -0
  121. package/dist/theme/AskAiPanel.svelte +433 -0
  122. package/dist/theme/AskAiPanel.svelte.d.ts +27 -0
  123. package/dist/theme/AskAiPanel.svelte.d.ts.map +1 -0
  124. package/dist/theme/Button.svelte +34 -0
  125. package/dist/theme/Button.svelte.d.ts +41 -0
  126. package/dist/theme/Button.svelte.d.ts.map +1 -0
  127. package/dist/theme/Checkbox.svelte +63 -0
  128. package/dist/theme/Checkbox.svelte.d.ts +45 -0
  129. package/dist/theme/Checkbox.svelte.d.ts.map +1 -0
  130. package/dist/theme/CodeBlock.svelte +6 -0
  131. package/dist/theme/CodeBlock.svelte.d.ts +22 -0
  132. package/dist/theme/CodeBlock.svelte.d.ts.map +1 -0
  133. package/dist/theme/DocPage.svelte +121 -0
  134. package/dist/theme/DocPage.svelte.d.ts +33 -0
  135. package/dist/theme/DocPage.svelte.d.ts.map +1 -0
  136. package/dist/theme/DocsApp.svelte +78 -0
  137. package/dist/theme/DocsApp.svelte.d.ts +63 -0
  138. package/dist/theme/DocsApp.svelte.d.ts.map +1 -0
  139. package/dist/theme/DocsLayout.svelte +167 -0
  140. package/dist/theme/DocsLayout.svelte.d.ts +40 -0
  141. package/dist/theme/DocsLayout.svelte.d.ts.map +1 -0
  142. package/dist/theme/FloatingToolbar.svelte +73 -0
  143. package/dist/theme/FloatingToolbar.svelte.d.ts +22 -0
  144. package/dist/theme/FloatingToolbar.svelte.d.ts.map +1 -0
  145. package/dist/theme/FormField.svelte +41 -0
  146. package/dist/theme/FormField.svelte.d.ts +35 -0
  147. package/dist/theme/FormField.svelte.d.ts.map +1 -0
  148. package/dist/theme/HomePage.svelte +228 -0
  149. package/dist/theme/HomePage.svelte.d.ts +58 -0
  150. package/dist/theme/HomePage.svelte.d.ts.map +1 -0
  151. package/dist/theme/Input.svelte +36 -0
  152. package/dist/theme/Input.svelte.d.ts +34 -0
  153. package/dist/theme/Input.svelte.d.ts.map +1 -0
  154. package/dist/theme/PageLayout.svelte +36 -0
  155. package/dist/theme/PageLayout.svelte.d.ts +31 -0
  156. package/dist/theme/PageLayout.svelte.d.ts.map +1 -0
  157. package/dist/theme/RootLayout.svelte +330 -0
  158. package/dist/theme/RootLayout.svelte.d.ts +40 -0
  159. package/dist/theme/RootLayout.svelte.d.ts.map +1 -0
  160. package/dist/theme/ScopeSwitcher.svelte +103 -0
  161. package/dist/theme/ScopeSwitcher.svelte.d.ts +24 -0
  162. package/dist/theme/ScopeSwitcher.svelte.d.ts.map +1 -0
  163. package/dist/theme/SearchDialog.svelte +253 -0
  164. package/dist/theme/SearchDialog.svelte.d.ts +28 -0
  165. package/dist/theme/SearchDialog.svelte.d.ts.map +1 -0
  166. package/dist/theme/Select.svelte +37 -0
  167. package/dist/theme/Select.svelte.d.ts +42 -0
  168. package/dist/theme/Select.svelte.d.ts.map +1 -0
  169. package/dist/theme/SidebarTree.svelte +136 -0
  170. package/dist/theme/SidebarTree.svelte.d.ts +24 -0
  171. package/dist/theme/SidebarTree.svelte.d.ts.map +1 -0
  172. package/dist/theme/Textarea.svelte +33 -0
  173. package/dist/theme/Textarea.svelte.d.ts +33 -0
  174. package/dist/theme/Textarea.svelte.d.ts.map +1 -0
  175. package/dist/theme/ThemeToggle.svelte +39 -0
  176. package/dist/theme/ThemeToggle.svelte.d.ts +21 -0
  177. package/dist/theme/ThemeToggle.svelte.d.ts.map +1 -0
  178. package/dist/theme/forms.d.ts +4 -0
  179. package/dist/theme/forms.d.ts.map +1 -0
  180. package/dist/theme/forms.js +3 -0
  181. package/dist/theme/index.d.ts +21 -0
  182. package/dist/theme/index.d.ts.map +1 -0
  183. package/dist/theme/index.js +19 -0
  184. package/dist/theme/portal.d.ts +5 -0
  185. package/dist/theme/portal.d.ts.map +1 -0
  186. package/dist/theme/portal.js +19 -0
  187. package/dist/theme/styles.css +3697 -0
  188. package/dist/virtual.d.ts +59 -0
  189. package/dist/vite.d.ts +10 -0
  190. package/dist/vite.d.ts.map +1 -0
  191. package/dist/vite.js +304 -0
  192. package/package.json +118 -0
package/LICENSE ADDED
@@ -0,0 +1,5 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 svedocs contributors
4
+
5
+ See the repository root license for the full text.
package/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # svedocs
2
+
3
+ Integrated SvelteKit documentation framework package.
4
+
5
+ ## Exports
6
+
7
+ - `svedocs/config`: config schema and `defineConfig`.
8
+ - `svedocs/core`: content manifest, scoped navigation, checks, search records, and shared types.
9
+ - `svedocs/vite`: virtual modules and content refresh Vite plugin.
10
+ - `svedocs/theme`: default Svelte theme components and Tailwind CSS v4 styles.
11
+ - `svedocs/svelte`: `mdsvex`-based Svelte-compatible authoring helpers.
12
+ - `svedocs/search`: weighted local search, scope filters, Cloudflare AI Search provider, and indexing sync.
13
+ - `svedocs/ai`: Ask AI providers, SSE responses, and rate limiting helpers.
14
+ - `svedocs/og`: SEO metadata, sitemap/robots, SVG/PNG/Satori OG generation.
15
+ - `svedocs/cloudflare`: build presets, wrangler config, and binding type helpers.
16
+
17
+ All rendering, theme, search, AI, SEO, OG, and Cloudflare capabilities are intentionally kept inside this package.
18
+
19
+ ## MDX/SVX Components
20
+
21
+ The Vite plugin loads `svedocs.config.ts` by default. You can also pass a config object explicitly and register shared authoring components or layouts:
22
+
23
+ ```ts
24
+ import { svedocs } from 'svedocs/vite';
25
+ import svedocsConfig from './svedocs.config';
26
+
27
+ svedocs({
28
+ config: svedocsConfig,
29
+ components: {
30
+ Callout: '$lib/Callout.svelte'
31
+ },
32
+ layouts: {
33
+ feature: '$lib/FeatureLayout.svelte'
34
+ }
35
+ })
36
+ ```
37
+
38
+ `.svx` and `.mdx` files can then use `<Callout />` without local imports.
@@ -0,0 +1,13 @@
1
+ import type { AiProvider } from './types.js';
2
+ export interface OpenAiCompatibleProviderOptions {
3
+ apiKey: string;
4
+ model: string;
5
+ baseUrl?: string;
6
+ systemPrompt?: string;
7
+ citationLimit?: number;
8
+ temperature?: number;
9
+ headers?: Record<string, string>;
10
+ fetch?: typeof fetch;
11
+ }
12
+ export declare function createOpenAiCompatibleProvider(options: OpenAiCompatibleProviderOptions): AiProvider;
13
+ //# sourceMappingURL=openai-compatible.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openai-compatible.d.ts","sourceRoot":"","sources":["../../src/ai/openai-compatible.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAY,MAAM,YAAY,CAAC;AAEvD,MAAM,WAAW,+BAA+B;IAC9C,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;CACtB;AAYD,wBAAgB,8BAA8B,CAAC,OAAO,EAAE,+BAA+B,GAAG,UAAU,CAgDnG"}
@@ -0,0 +1,92 @@
1
+ import { filterSearchRecords, searchRecords } from '../search.js';
2
+ export function createOpenAiCompatibleProvider(options) {
3
+ const citationLimit = options.citationLimit ?? 5;
4
+ const systemPrompt = options.systemPrompt ?? [
5
+ 'Answer from the documentation sources provided by svedocs.',
6
+ 'Cite relevant pages by title and URL.',
7
+ 'If the sources are insufficient, say what is missing.'
8
+ ].join(' ');
9
+ return {
10
+ name: 'openai-compatible',
11
+ async ask(input) {
12
+ const ranked = createOpenAiContextRecords(input, citationLimit);
13
+ const citations = ranked.map((result) => ({
14
+ title: result.title,
15
+ url: result.url,
16
+ ...(result.section ? { section: result.section } : {})
17
+ }));
18
+ const history = (input.messages ?? []).map((message) => ({ role: message.role, content: message.content }));
19
+ const lastIsCurrent = history.length > 0
20
+ && history[history.length - 1]?.role === 'user'
21
+ && history[history.length - 1]?.content === input.question;
22
+ const trailing = history.length > 0 && lastIsCurrent ? history.slice(0, -1) : history;
23
+ const response = await (options.fetch ?? fetch)(createChatCompletionsEndpoint(options.baseUrl), {
24
+ method: 'POST',
25
+ headers: {
26
+ 'content-type': 'application/json',
27
+ authorization: `Bearer ${options.apiKey}`,
28
+ ...(options.headers ?? {})
29
+ },
30
+ body: JSON.stringify({
31
+ model: options.model,
32
+ ...(typeof options.temperature === 'number' ? { temperature: options.temperature } : {}),
33
+ messages: [
34
+ { role: 'system', content: systemPrompt },
35
+ ...trailing,
36
+ { role: 'user', content: createPrompt(input, ranked) }
37
+ ]
38
+ })
39
+ });
40
+ if (!response.ok) {
41
+ throw new Error(`OpenAI-compatible provider returned ${response.status}: ${(await response.text()).slice(0, 500)}`);
42
+ }
43
+ const payload = await response.json();
44
+ return {
45
+ answer: readChatCompletionAnswer(payload),
46
+ citations
47
+ };
48
+ }
49
+ };
50
+ }
51
+ function createOpenAiContextRecords(input, citationLimit) {
52
+ const ranked = searchRecords(input.records ?? [], {
53
+ query: input.question,
54
+ limit: citationLimit,
55
+ ...(input.scope ?? {})
56
+ });
57
+ if (ranked.length > 0)
58
+ return ranked;
59
+ return filterSearchRecords(input.records ?? [], input.scope).slice(0, citationLimit).map((record, index) => ({
60
+ id: record.id,
61
+ title: record.title,
62
+ url: record.url,
63
+ ...(record.section ? { section: record.section } : {}),
64
+ excerpt: record.content.slice(0, 220),
65
+ score: 1 / (index + 1),
66
+ metadata: record.metadata
67
+ }));
68
+ }
69
+ function createChatCompletionsEndpoint(baseUrl = 'https://api.openai.com/v1') {
70
+ return `${baseUrl.replace(/\/$/, '')}/chat/completions`;
71
+ }
72
+ function createPrompt(input, sources) {
73
+ const context = sources.length > 0
74
+ ? sources.map((source, index) => [
75
+ `[${index + 1}] ${source.section ? `${source.title}: ${source.section}` : source.title}`,
76
+ `URL: ${source.url}`,
77
+ source.excerpt
78
+ ].join('\n')).join('\n\n')
79
+ : 'No local documentation source matched.';
80
+ return [
81
+ `Question: ${input.question}`,
82
+ '',
83
+ 'Sources:',
84
+ context
85
+ ].join('\n');
86
+ }
87
+ function readChatCompletionAnswer(payload) {
88
+ return payload.output_text
89
+ ?? payload.choices?.[0]?.message?.content
90
+ ?? payload.choices?.[0]?.text
91
+ ?? 'No answer was returned.';
92
+ }
@@ -0,0 +1,19 @@
1
+ import type { SvedocsSearchRecord } from '../core.js';
2
+ import type { AiProvider, AiSearchBinding, AskCitation, CloudflareWorkersAiBinding } from './types.js';
3
+ export declare function createMockAiProvider(): AiProvider;
4
+ export declare function createCloudflareAiSearchAiProvider(input: {
5
+ binding: AiSearchBinding;
6
+ instanceName?: string;
7
+ systemPrompt?: string;
8
+ model?: string;
9
+ maxResults?: number;
10
+ }): AiProvider;
11
+ export declare function createWorkersAiProvider(input: {
12
+ ai: CloudflareWorkersAiBinding;
13
+ model?: string;
14
+ systemPrompt?: string;
15
+ citationLimit?: number;
16
+ }): AiProvider;
17
+ export declare function rankAskCitations(records: SvedocsSearchRecord[], question: string, limit?: number): AskCitation[];
18
+ export declare const mockAiProvider: AiProvider;
19
+ //# sourceMappingURL=providers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"providers.d.ts","sourceRoot":"","sources":["../../src/ai/providers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAItD,OAAO,KAAK,EACV,UAAU,EACV,eAAe,EACf,WAAW,EAGX,0BAA0B,EAC3B,MAAM,YAAY,CAAC;AAEpB,wBAAgB,oBAAoB,IAAI,UAAU,CAajD;AAED,wBAAgB,kCAAkC,CAAC,KAAK,EAAE;IACxD,OAAO,EAAE,eAAe,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GAAG,UAAU,CAsFb;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE;IAC7C,EAAE,EAAE,0BAA0B,CAAC;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,GAAG,UAAU,CA2Bb;AAED,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,mBAAmB,EAAE,EAC9B,QAAQ,EAAE,MAAM,EAChB,KAAK,SAAI,GACR,WAAW,EAAE,CAMf;AAED,eAAO,MAAM,cAAc,YAAyB,CAAC"}
@@ -0,0 +1,185 @@
1
+ import { normalizeCloudflareAiSearchResults } from '../search/cloudflare.js';
2
+ import { searchRecords } from '../search.js';
3
+ export function createMockAiProvider() {
4
+ return {
5
+ name: 'mock',
6
+ ask(input) {
7
+ const citations = rankAskCitations(input.records ?? [], input.question, 3);
8
+ return {
9
+ answer: citations.length > 0
10
+ ? `I found ${citations.length} relevant source${citations.length === 1 ? '' : 's'} in this documentation. Connect Cloudflare AI Search to generate a hosted answer.`
11
+ : `Ask AI is configured. Connect Cloudflare AI Search to answer: ${input.question}`,
12
+ citations
13
+ };
14
+ }
15
+ };
16
+ }
17
+ export function createCloudflareAiSearchAiProvider(input) {
18
+ const maxResults = input.maxResults ?? 5;
19
+ const systemPrompt = input.systemPrompt ?? 'Answer from the documentation. Cite the pages you used.';
20
+ async function ask(question) {
21
+ const conversation = buildChatMessages(systemPrompt, question);
22
+ const instance = resolveAiChatInstance(input.binding, input.instanceName);
23
+ if (!instance.chatCompletions) {
24
+ const result = await instance.search({
25
+ messages: conversation,
26
+ ai_search_options: {
27
+ retrieval: {
28
+ retrieval_type: 'hybrid',
29
+ max_num_results: maxResults
30
+ }
31
+ }
32
+ });
33
+ return {
34
+ answer: result.response ?? result.answer ?? 'No answer was returned.',
35
+ citations: normalizeCloudflareAiSearchResults(result, question.question)
36
+ .slice(0, maxResults)
37
+ .map((result) => ({
38
+ title: result.title,
39
+ url: result.url,
40
+ ...(result.section ? { section: result.section } : {})
41
+ }))
42
+ };
43
+ }
44
+ const result = await instance.chatCompletions({
45
+ messages: conversation,
46
+ ...(input.model ? { model: input.model } : {}),
47
+ ai_search_options: {
48
+ retrieval: {
49
+ retrieval_type: 'hybrid',
50
+ max_num_results: maxResults
51
+ }
52
+ }
53
+ });
54
+ return {
55
+ answer: readAiSearchAnswer(result),
56
+ citations: normalizeCitations(result.citations).length > 0
57
+ ? normalizeCitations(result.citations)
58
+ : normalizeCloudflareAiSearchResults(result, question.question)
59
+ .slice(0, maxResults)
60
+ .map((result) => ({
61
+ title: result.title,
62
+ url: result.url,
63
+ ...(result.section ? { section: result.section } : {})
64
+ }))
65
+ };
66
+ }
67
+ return {
68
+ name: 'cloudflare-ai-search',
69
+ ask,
70
+ async stream(question) {
71
+ const instance = resolveAiChatInstance(input.binding, input.instanceName);
72
+ if (!instance.chatCompletions) {
73
+ return createSvedocsAnswerStream(await ask(question));
74
+ }
75
+ const result = await instance.chatCompletions({
76
+ messages: buildChatMessages(systemPrompt, question),
77
+ ...(input.model ? { model: input.model } : {}),
78
+ stream: true,
79
+ ai_search_options: {
80
+ retrieval: {
81
+ retrieval_type: 'hybrid',
82
+ max_num_results: maxResults
83
+ }
84
+ }
85
+ });
86
+ if (result instanceof Response || result instanceof ReadableStream)
87
+ return result;
88
+ return createSvedocsAnswerStream({
89
+ answer: readAiSearchAnswer(result),
90
+ citations: normalizeCitations(result.citations).length > 0
91
+ ? normalizeCitations(result.citations)
92
+ : normalizeCloudflareAiSearchResults(result, question.question)
93
+ .slice(0, maxResults)
94
+ .map((result) => ({
95
+ title: result.title,
96
+ url: result.url,
97
+ ...(result.section ? { section: result.section } : {})
98
+ }))
99
+ });
100
+ }
101
+ };
102
+ }
103
+ export function createWorkersAiProvider(input) {
104
+ const systemPrompt = input.systemPrompt ?? 'Answer from the documentation context. Keep answers concise and cite relevant pages.';
105
+ return {
106
+ name: 'cloudflare-workers-ai',
107
+ async ask(question) {
108
+ const citations = rankAskCitations(question.records ?? [], question.question, input.citationLimit ?? 5);
109
+ const context = citations.map((citation) => `- ${citation.section ?? citation.title}: ${citation.url}`).join('\n');
110
+ const history = (question.messages ?? []).map((message) => ({ role: message.role, content: message.content }));
111
+ const userContent = [
112
+ `Question: ${question.question}`,
113
+ '',
114
+ 'Sources:',
115
+ context || 'No local source matched.'
116
+ ].join('\n');
117
+ const result = await input.ai.run(input.model ?? '@cf/meta/llama-3.1-8b-instruct', {
118
+ messages: [
119
+ { role: 'system', content: systemPrompt },
120
+ ...history,
121
+ { role: 'user', content: userContent }
122
+ ]
123
+ });
124
+ return {
125
+ answer: readWorkersAiAnswer(result),
126
+ citations
127
+ };
128
+ }
129
+ };
130
+ }
131
+ export function rankAskCitations(records, question, limit = 3) {
132
+ return searchRecords(records, { query: question, limit }).map((result) => ({
133
+ title: result.title,
134
+ url: result.url,
135
+ ...(result.section ? { section: result.section } : {})
136
+ }));
137
+ }
138
+ export const mockAiProvider = createMockAiProvider();
139
+ function createSvedocsAnswerStream(result) {
140
+ const encoder = new TextEncoder();
141
+ return new ReadableStream({
142
+ start(controller) {
143
+ controller.enqueue(encoder.encode(`event: answer\ndata: ${JSON.stringify({ answer: result.answer })}\n\n`));
144
+ controller.enqueue(encoder.encode(`event: citations\ndata: ${JSON.stringify({ citations: result.citations })}\n\n`));
145
+ controller.enqueue(encoder.encode('event: done\ndata: {}\n\n'));
146
+ controller.close();
147
+ }
148
+ });
149
+ }
150
+ function resolveAiChatInstance(binding, instanceName = 'svedocs') {
151
+ return ('get' in binding ? binding.get(instanceName) : binding);
152
+ }
153
+ function normalizeCitations(citations) {
154
+ return (citations ?? []).map((citation) => ({
155
+ title: citation.title ?? 'Source',
156
+ url: citation.url ?? '#',
157
+ ...(citation.section ? { section: citation.section } : {})
158
+ }));
159
+ }
160
+ function readAiSearchAnswer(result) {
161
+ return (result.response ??
162
+ result.answer ??
163
+ result.choices?.[0]?.message?.content ??
164
+ result.choices?.[0]?.text ??
165
+ 'No answer was returned.');
166
+ }
167
+ function readWorkersAiAnswer(result) {
168
+ if (typeof result === 'string')
169
+ return result;
170
+ return result.response ?? result.answer ?? result.result ?? 'No answer was returned.';
171
+ }
172
+ function buildChatMessages(systemPrompt, input) {
173
+ const history = (input.messages ?? []).map((message) => ({ role: message.role, content: message.content }));
174
+ const lastIsCurrentQuestion = history.length > 0 &&
175
+ history[history.length - 1]?.role === 'user' &&
176
+ history[history.length - 1]?.content === input.question;
177
+ const messages = [
178
+ { role: 'system', content: systemPrompt },
179
+ ...history
180
+ ];
181
+ if (!lastIsCurrentQuestion) {
182
+ messages.push({ role: 'user', content: input.question });
183
+ }
184
+ return messages;
185
+ }
@@ -0,0 +1,18 @@
1
+ import type { AiRateLimiter, AiRateLimitStore, CloudflareKvNamespace } from './types.js';
2
+ export declare function createMemoryRateLimiter(input: {
3
+ windowMs: number;
4
+ max: number;
5
+ }): AiRateLimiter;
6
+ export declare function createStoreRateLimiter(input: {
7
+ windowMs: number;
8
+ max: number;
9
+ store: AiRateLimitStore;
10
+ prefix?: string;
11
+ }): AiRateLimiter;
12
+ export declare function createCloudflareKvRateLimiter(input: {
13
+ namespace: CloudflareKvNamespace;
14
+ windowMs: number;
15
+ max: number;
16
+ prefix?: string;
17
+ }): AiRateLimiter;
18
+ //# sourceMappingURL=rate-limit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../../src/ai/rate-limit.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAEzF,wBAAgB,uBAAuB,CAAC,KAAK,EAAE;IAC7C,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;CACb,GAAG,aAAa,CAchB;AAED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE;IAC5C,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,gBAAgB,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,GAAG,aAAa,CAqBhB;AAED,wBAAgB,6BAA6B,CAAC,KAAK,EAAE;IACnD,SAAS,EAAE,qBAAqB,CAAC;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,GAAG,aAAa,CAchB"}
@@ -0,0 +1,52 @@
1
+ export function createMemoryRateLimiter(input) {
2
+ const buckets = new Map();
3
+ return createStoreRateLimiter({
4
+ windowMs: input.windowMs,
5
+ max: input.max,
6
+ store: {
7
+ async get(key) {
8
+ return buckets.get(key);
9
+ },
10
+ async put(key, value) {
11
+ buckets.set(key, value);
12
+ }
13
+ }
14
+ });
15
+ }
16
+ export function createStoreRateLimiter(input) {
17
+ return {
18
+ async check({ key }) {
19
+ const now = Date.now();
20
+ const storeKey = `${input.prefix ?? 'svedocs:ai'}:${key}`;
21
+ const existing = await input.store.get(storeKey);
22
+ if (!existing || existing.resetAt <= now) {
23
+ await input.store.put(storeKey, { count: 1, resetAt: now + input.windowMs }, Math.ceil(input.windowMs / 1000));
24
+ return { allowed: true };
25
+ }
26
+ const next = { count: existing.count + 1, resetAt: existing.resetAt };
27
+ await input.store.put(storeKey, next, Math.ceil((existing.resetAt - now) / 1000));
28
+ if (next.count > input.max) {
29
+ return {
30
+ allowed: false,
31
+ retryAfter: Math.ceil((existing.resetAt - now) / 1000)
32
+ };
33
+ }
34
+ return { allowed: true };
35
+ }
36
+ };
37
+ }
38
+ export function createCloudflareKvRateLimiter(input) {
39
+ return createStoreRateLimiter({
40
+ windowMs: input.windowMs,
41
+ max: input.max,
42
+ ...(input.prefix ? { prefix: input.prefix } : {}),
43
+ store: {
44
+ async get(key) {
45
+ return input.namespace.get(key, { type: 'json' }).then((value) => value ?? undefined);
46
+ },
47
+ async put(key, value, ttlSeconds) {
48
+ await input.namespace.put(key, JSON.stringify(value), { expirationTtl: ttlSeconds });
49
+ }
50
+ }
51
+ });
52
+ }
@@ -0,0 +1,4 @@
1
+ import type { SvedocsSearchRecord } from '../core.js';
2
+ import type { AiProvider, CreateAskResponseOptions } from './types.js';
3
+ export declare function createAskResponse(provider: AiProvider, request: Request, recordsOrOptions?: SvedocsSearchRecord[] | CreateAskResponseOptions): Promise<Response>;
4
+ //# sourceMappingURL=response.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"response.d.ts","sourceRoot":"","sources":["../../src/ai/response.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAGtD,OAAO,KAAK,EAAE,UAAU,EAA0B,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAE/F,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,UAAU,EACpB,OAAO,EAAE,OAAO,EAChB,gBAAgB,GAAE,mBAAmB,EAAE,GAAG,wBAA6B,GACtE,OAAO,CAAC,QAAQ,CAAC,CA2CnB"}
@@ -0,0 +1,151 @@
1
+ import { filterSearchRecords } from '../search.js';
2
+ export async function createAskResponse(provider, request, recordsOrOptions = []) {
3
+ const options = Array.isArray(recordsOrOptions) ? { records: recordsOrOptions } : recordsOrOptions;
4
+ const stream = options.stream ?? wantsStream(request);
5
+ try {
6
+ const body = await request.json().catch(() => ({}));
7
+ const messages = sanitizeMessages(body.messages);
8
+ const lastUserMessage = [...messages].reverse().find((message) => message.role === 'user');
9
+ const question = (body.question?.trim() ?? lastUserMessage?.content.trim() ?? '');
10
+ if (!question) {
11
+ return askErrorResponse('Question is required.', 400, stream);
12
+ }
13
+ if (options.rateLimiter) {
14
+ const limit = await options.rateLimiter.check({
15
+ key: createRateLimitKey(request, options.rateLimitKey),
16
+ request
17
+ });
18
+ if (!limit.allowed) {
19
+ return askErrorResponse('Ask AI rate limit exceeded.', 429, stream, limit.retryAfter);
20
+ }
21
+ }
22
+ const scope = createRequestScope(request, body, options.scope);
23
+ const records = filterSearchRecords(options.records ?? [], scope);
24
+ const askInput = {
25
+ question,
26
+ records,
27
+ scope,
28
+ ...(messages.length > 0 ? { messages } : {}),
29
+ ...(options.systemPrompt ? { systemPrompt: options.systemPrompt } : {}),
30
+ ...(options.maxResults ? { maxResults: options.maxResults } : {})
31
+ };
32
+ if (stream && provider.stream) {
33
+ return streamProviderResponse(await provider.stream(askInput));
34
+ }
35
+ const result = await provider.ask(askInput);
36
+ return stream ? streamAskResult(result) : jsonResponse(result);
37
+ }
38
+ catch (error) {
39
+ return askErrorResponse(error instanceof Error ? error.message : 'Ask AI failed.', 500, stream);
40
+ }
41
+ }
42
+ function sanitizeMessages(messages) {
43
+ if (!Array.isArray(messages))
44
+ return [];
45
+ return messages
46
+ .filter((message) => Boolean(message)
47
+ && (message.role === 'user' || message.role === 'assistant')
48
+ && typeof message.content === 'string'
49
+ && message.content.trim().length > 0)
50
+ .map((message) => ({ role: message.role, content: message.content }));
51
+ }
52
+ function createRequestScope(request, body, configured) {
53
+ const url = new URL(request.url);
54
+ const base = typeof configured === 'function' ? configured(request) : configured;
55
+ const locale = readScopeParam(url, 'locale');
56
+ const kind = readScopeParam(url, 'kind');
57
+ return cleanScope({
58
+ ...(base ?? {}),
59
+ ...(locale ? { locale } : {}),
60
+ ...(kind ? { kind } : {}),
61
+ ...(body.locale ? { locale: body.locale } : {}),
62
+ ...(body.kind ? { kind: body.kind } : {})
63
+ });
64
+ }
65
+ function cleanScope(scope) {
66
+ return {
67
+ ...(scope.locale?.trim() ? { locale: scope.locale.trim() } : {}),
68
+ ...(scope.kind?.trim() ? { kind: scope.kind.trim() } : {})
69
+ };
70
+ }
71
+ function readScopeParam(url, key) {
72
+ return url.searchParams.get(key)?.trim() || undefined;
73
+ }
74
+ function wantsStream(request) {
75
+ const url = new URL(request.url);
76
+ return url.searchParams.get('stream') === '1' || request.headers.get('accept')?.includes('text/event-stream') === true;
77
+ }
78
+ function createRateLimitKey(request, key) {
79
+ if (typeof key === 'function')
80
+ return key(request);
81
+ if (typeof key === 'string')
82
+ return key;
83
+ return request.headers.get('cf-connecting-ip')
84
+ ?? request.headers.get('x-forwarded-for')?.split(',')[0]?.trim()
85
+ ?? 'anonymous';
86
+ }
87
+ function streamProviderResponse(result) {
88
+ if (result instanceof Response) {
89
+ return new Response(result.body, {
90
+ status: result.status,
91
+ headers: {
92
+ 'content-type': result.headers.get('content-type') ?? 'text/event-stream; charset=utf-8',
93
+ 'cache-control': result.headers.get('cache-control') ?? 'no-cache, no-transform'
94
+ }
95
+ });
96
+ }
97
+ return new Response(result, {
98
+ headers: {
99
+ 'content-type': 'text/event-stream; charset=utf-8',
100
+ 'cache-control': 'no-cache, no-transform'
101
+ }
102
+ });
103
+ }
104
+ function streamAskResult(result) {
105
+ return eventStreamResponse((send) => {
106
+ send('answer', { answer: result.answer });
107
+ send('citations', { citations: result.citations });
108
+ send('done', {});
109
+ });
110
+ }
111
+ function askErrorResponse(message, status, stream, retryAfter) {
112
+ if (stream) {
113
+ return eventStreamResponse((send) => {
114
+ send('error', { error: message, status, ...(retryAfter ? { retryAfter } : {}) });
115
+ send('done', {});
116
+ }, status, retryAfter);
117
+ }
118
+ return jsonResponse({
119
+ error: message,
120
+ ...(retryAfter ? { retryAfter } : {})
121
+ }, status, retryAfter);
122
+ }
123
+ function eventStreamResponse(write, status = 200, retryAfter) {
124
+ const encoder = new TextEncoder();
125
+ const stream = new ReadableStream({
126
+ start(controller) {
127
+ const send = (event, value) => {
128
+ controller.enqueue(encoder.encode(`event: ${event}\ndata: ${JSON.stringify(value)}\n\n`));
129
+ };
130
+ write(send);
131
+ controller.close();
132
+ }
133
+ });
134
+ return new Response(stream, {
135
+ status,
136
+ headers: {
137
+ 'content-type': 'text/event-stream; charset=utf-8',
138
+ 'cache-control': 'no-cache, no-transform',
139
+ ...(retryAfter ? { 'retry-after': String(retryAfter) } : {})
140
+ }
141
+ });
142
+ }
143
+ function jsonResponse(value, status = 200, retryAfter) {
144
+ return new Response(JSON.stringify(value), {
145
+ status,
146
+ headers: {
147
+ 'content-type': 'application/json; charset=utf-8',
148
+ ...(retryAfter ? { 'retry-after': String(retryAfter) } : {})
149
+ }
150
+ });
151
+ }
@@ -0,0 +1,30 @@
1
+ import type { SvedocsResolvedConfig, SvedocsSearchRecord } from '../core.js';
2
+ import type { CloudflareAiSearchInstance, CloudflareAiSearchNamespace } from '../search.js';
3
+ import type { AiProvider, CloudflareWorkersAiBinding, CreateAskResponseOptions } from './types.js';
4
+ export interface SvedocsAiRuntimeEnv {
5
+ SVEDOCS_AI_SEARCH?: CloudflareAiSearchInstance | CloudflareAiSearchNamespace | undefined;
6
+ AI?: CloudflareWorkersAiBinding | undefined;
7
+ OPENAI_COMPATIBLE_API_KEY?: string | undefined;
8
+ OPENAI_COMPATIBLE_BASE_URL?: string | undefined;
9
+ OPENAI_COMPATIBLE_MODEL?: string | undefined;
10
+ OPENAI_COMPATIBLE_TEMPERATURE?: string | undefined;
11
+ SVEDOCS_OPENAI_COMPATIBLE_API_KEY?: string | undefined;
12
+ SVEDOCS_OPENAI_COMPATIBLE_BASE_URL?: string | undefined;
13
+ SVEDOCS_OPENAI_COMPATIBLE_MODEL?: string | undefined;
14
+ SVEDOCS_OPENAI_COMPATIBLE_TEMPERATURE?: string | undefined;
15
+ [key: string]: unknown;
16
+ }
17
+ export interface CreateConfiguredAiProviderOptions {
18
+ config: Pick<SvedocsResolvedConfig, 'ai' | 'cloudflare'>;
19
+ env?: SvedocsAiRuntimeEnv;
20
+ provider?: string;
21
+ fetch?: typeof fetch;
22
+ }
23
+ export interface CreateConfiguredAskResponseOptions extends Omit<CreateAskResponseOptions, 'records'> {
24
+ env?: SvedocsAiRuntimeEnv;
25
+ provider?: string;
26
+ fetch?: typeof fetch;
27
+ }
28
+ export declare function createConfiguredAiProvider(options: CreateConfiguredAiProviderOptions): AiProvider;
29
+ export declare function createConfiguredAskResponse(config: Pick<SvedocsResolvedConfig, 'ai' | 'cloudflare'>, records: SvedocsSearchRecord[], request: Request, options?: CreateConfiguredAskResponseOptions): Promise<Response>;
30
+ //# sourceMappingURL=runtime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/ai/runtime.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAC7E,OAAO,KAAK,EAAE,0BAA0B,EAAE,2BAA2B,EAAE,MAAM,cAAc,CAAC;AAG5F,OAAO,KAAK,EAAE,UAAU,EAAE,0BAA0B,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAGnG,MAAM,WAAW,mBAAmB;IAClC,iBAAiB,CAAC,EAAE,0BAA0B,GAAG,2BAA2B,GAAG,SAAS,CAAC;IACzF,EAAE,CAAC,EAAE,0BAA0B,GAAG,SAAS,CAAC;IAC5C,yBAAyB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/C,0BAA0B,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAChD,uBAAuB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7C,6BAA6B,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACnD,iCAAiC,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACvD,kCAAkC,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACxD,+BAA+B,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACrD,qCAAqC,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3D,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,iCAAiC;IAChD,MAAM,EAAE,IAAI,CAAC,qBAAqB,EAAE,IAAI,GAAG,YAAY,CAAC,CAAC;IACzD,GAAG,CAAC,EAAE,mBAAmB,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;CACtB;AAED,MAAM,WAAW,kCAAmC,SAAQ,IAAI,CAAC,wBAAwB,EAAE,SAAS,CAAC;IACnG,GAAG,CAAC,EAAE,mBAAmB,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;CACtB;AAED,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,iCAAiC,GAAG,UAAU,CA4CjG;AAED,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,IAAI,CAAC,qBAAqB,EAAE,IAAI,GAAG,YAAY,CAAC,EACxD,OAAO,EAAE,mBAAmB,EAAE,EAC9B,OAAO,EAAE,OAAO,EAChB,OAAO,GAAE,kCAAuC,GAC/C,OAAO,CAAC,QAAQ,CAAC,CAcnB"}