skrypt-ai 0.6.1 → 0.8.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 (180) hide show
  1. package/dist/audit/doc-parser.d.ts +5 -0
  2. package/dist/audit/doc-parser.js +106 -0
  3. package/dist/audit/index.d.ts +4 -0
  4. package/dist/audit/index.js +4 -0
  5. package/dist/audit/matcher.d.ts +6 -0
  6. package/dist/audit/matcher.js +94 -0
  7. package/dist/audit/reporter.d.ts +9 -0
  8. package/dist/audit/reporter.js +106 -0
  9. package/dist/audit/types.d.ts +37 -0
  10. package/dist/auth/index.js +6 -4
  11. package/dist/cli.js +12 -2
  12. package/dist/commands/audit.d.ts +2 -0
  13. package/dist/commands/audit.js +59 -0
  14. package/dist/commands/config.d.ts +2 -0
  15. package/dist/commands/config.js +73 -0
  16. package/dist/commands/{generate.d.ts → generate/index.d.ts} +1 -0
  17. package/dist/commands/generate/index.js +393 -0
  18. package/dist/commands/generate/scan.d.ts +41 -0
  19. package/dist/commands/generate/scan.js +256 -0
  20. package/dist/commands/generate/verify.d.ts +14 -0
  21. package/dist/commands/generate/verify.js +122 -0
  22. package/dist/commands/generate/write.d.ts +25 -0
  23. package/dist/commands/generate/write.js +120 -0
  24. package/dist/commands/import.js +4 -1
  25. package/dist/commands/llms-txt.js +6 -4
  26. package/dist/commands/refresh.d.ts +2 -0
  27. package/dist/commands/refresh.js +158 -0
  28. package/dist/commands/review.d.ts +2 -0
  29. package/dist/commands/review.js +110 -0
  30. package/dist/commands/test.js +177 -236
  31. package/dist/commands/watch.js +29 -20
  32. package/dist/config/loader.d.ts +6 -2
  33. package/dist/config/loader.js +39 -3
  34. package/dist/config/types.d.ts +7 -0
  35. package/dist/generator/agents-md.d.ts +25 -0
  36. package/dist/generator/agents-md.js +122 -0
  37. package/dist/generator/generator.js +2 -1
  38. package/dist/generator/index.d.ts +2 -0
  39. package/dist/generator/index.js +2 -0
  40. package/dist/generator/mdx-serializer.d.ts +11 -0
  41. package/dist/generator/mdx-serializer.js +135 -0
  42. package/dist/generator/organizer.d.ts +1 -16
  43. package/dist/generator/organizer.js +0 -38
  44. package/dist/generator/types.d.ts +3 -0
  45. package/dist/generator/writer.js +65 -32
  46. package/dist/github/org-discovery.d.ts +17 -0
  47. package/dist/github/org-discovery.js +93 -0
  48. package/dist/llm/index.d.ts +2 -0
  49. package/dist/llm/index.js +8 -2
  50. package/dist/llm/proxy-client.d.ts +32 -0
  51. package/dist/llm/proxy-client.js +103 -0
  52. package/dist/next-actions/actions.d.ts +2 -0
  53. package/dist/next-actions/actions.js +190 -0
  54. package/dist/next-actions/index.d.ts +6 -0
  55. package/dist/next-actions/index.js +39 -0
  56. package/dist/next-actions/setup.d.ts +2 -0
  57. package/dist/next-actions/setup.js +72 -0
  58. package/dist/next-actions/state.d.ts +7 -0
  59. package/dist/next-actions/state.js +68 -0
  60. package/dist/next-actions/suggest.d.ts +3 -0
  61. package/dist/next-actions/suggest.js +47 -0
  62. package/dist/next-actions/types.d.ts +26 -0
  63. package/dist/refresh/differ.d.ts +9 -0
  64. package/dist/refresh/differ.js +67 -0
  65. package/dist/refresh/index.d.ts +4 -0
  66. package/dist/refresh/index.js +4 -0
  67. package/dist/refresh/manifest.d.ts +18 -0
  68. package/dist/refresh/manifest.js +71 -0
  69. package/dist/refresh/splicer.d.ts +9 -0
  70. package/dist/refresh/splicer.js +50 -0
  71. package/dist/refresh/types.d.ts +37 -0
  72. package/dist/review/index.d.ts +8 -0
  73. package/dist/review/index.js +94 -0
  74. package/dist/review/parser.d.ts +16 -0
  75. package/dist/review/parser.js +95 -0
  76. package/dist/review/types.d.ts +18 -0
  77. package/dist/scanner/csharp.d.ts +0 -4
  78. package/dist/scanner/csharp.js +9 -49
  79. package/dist/scanner/go.d.ts +0 -3
  80. package/dist/scanner/go.js +8 -35
  81. package/dist/scanner/java.d.ts +0 -4
  82. package/dist/scanner/java.js +9 -49
  83. package/dist/scanner/kotlin.d.ts +0 -3
  84. package/dist/scanner/kotlin.js +6 -33
  85. package/dist/scanner/php.d.ts +0 -10
  86. package/dist/scanner/php.js +11 -55
  87. package/dist/scanner/ruby.d.ts +0 -3
  88. package/dist/scanner/ruby.js +8 -38
  89. package/dist/scanner/rust.d.ts +0 -3
  90. package/dist/scanner/rust.js +10 -37
  91. package/dist/scanner/swift.d.ts +0 -3
  92. package/dist/scanner/swift.js +8 -35
  93. package/dist/scanner/types.d.ts +2 -0
  94. package/dist/scanner/utils.d.ts +41 -0
  95. package/dist/scanner/utils.js +97 -0
  96. package/dist/structure/index.d.ts +19 -0
  97. package/dist/structure/index.js +92 -0
  98. package/dist/structure/planner.d.ts +8 -0
  99. package/dist/structure/planner.js +180 -0
  100. package/dist/structure/topology.d.ts +16 -0
  101. package/dist/structure/topology.js +49 -0
  102. package/dist/structure/types.d.ts +26 -0
  103. package/dist/template/docs.json +5 -2
  104. package/dist/template/next.config.mjs +31 -0
  105. package/dist/template/package.json +5 -3
  106. package/dist/template/src/app/layout.tsx +13 -13
  107. package/dist/template/src/app/llms-full.md/route.ts +29 -0
  108. package/dist/template/src/app/llms.txt/route.ts +29 -0
  109. package/dist/template/src/app/md/[...slug]/route.ts +174 -0
  110. package/dist/template/src/app/reference/route.ts +22 -18
  111. package/dist/template/src/app/sitemap.ts +1 -1
  112. package/dist/template/src/components/ai-chat-impl.tsx +206 -0
  113. package/dist/template/src/components/ai-chat.tsx +20 -193
  114. package/dist/template/src/components/mdx/index.tsx +27 -4
  115. package/dist/template/src/lib/fonts.ts +135 -0
  116. package/dist/template/src/middleware.ts +101 -0
  117. package/dist/template/src/styles/globals.css +28 -20
  118. package/dist/testing/comparator.d.ts +7 -0
  119. package/dist/testing/comparator.js +77 -0
  120. package/dist/testing/docker.d.ts +21 -0
  121. package/dist/testing/docker.js +234 -0
  122. package/dist/testing/env.d.ts +16 -0
  123. package/dist/testing/env.js +58 -0
  124. package/dist/testing/extractor.d.ts +9 -0
  125. package/dist/testing/extractor.js +195 -0
  126. package/dist/testing/index.d.ts +6 -0
  127. package/dist/testing/index.js +6 -0
  128. package/dist/testing/runner.d.ts +5 -0
  129. package/dist/testing/runner.js +225 -0
  130. package/dist/testing/types.d.ts +58 -0
  131. package/dist/utils/files.d.ts +0 -8
  132. package/dist/utils/files.js +0 -33
  133. package/package.json +1 -1
  134. package/dist/autofix/autofix.test.js +0 -487
  135. package/dist/commands/generate.js +0 -445
  136. package/dist/generator/generator.test.js +0 -259
  137. package/dist/generator/writer.test.js +0 -411
  138. package/dist/llm/llm.manual-test.js +0 -112
  139. package/dist/llm/llm.mock-test.d.ts +0 -4
  140. package/dist/llm/llm.mock-test.js +0 -79
  141. package/dist/plugins/index.d.ts +0 -47
  142. package/dist/plugins/index.js +0 -181
  143. package/dist/scanner/content-type.test.js +0 -231
  144. package/dist/scanner/integration.test.d.ts +0 -4
  145. package/dist/scanner/integration.test.js +0 -180
  146. package/dist/scanner/scanner.test.js +0 -210
  147. package/dist/scanner/typescript.manual-test.d.ts +0 -1
  148. package/dist/scanner/typescript.manual-test.js +0 -112
  149. package/dist/template/src/app/docs/auth/page.mdx +0 -589
  150. package/dist/template/src/app/docs/autofix/page.mdx +0 -624
  151. package/dist/template/src/app/docs/cli/page.mdx +0 -217
  152. package/dist/template/src/app/docs/config/page.mdx +0 -428
  153. package/dist/template/src/app/docs/configuration/page.mdx +0 -86
  154. package/dist/template/src/app/docs/deployment/page.mdx +0 -112
  155. package/dist/template/src/app/docs/generator/generator.md +0 -504
  156. package/dist/template/src/app/docs/generator/organizer.md +0 -779
  157. package/dist/template/src/app/docs/generator/page.mdx +0 -613
  158. package/dist/template/src/app/docs/github/page.mdx +0 -502
  159. package/dist/template/src/app/docs/llm/anthropic-client.md +0 -549
  160. package/dist/template/src/app/docs/llm/index.md +0 -471
  161. package/dist/template/src/app/docs/llm/page.mdx +0 -428
  162. package/dist/template/src/app/docs/plugins/page.mdx +0 -1793
  163. package/dist/template/src/app/docs/pro/page.mdx +0 -121
  164. package/dist/template/src/app/docs/quickstart/page.mdx +0 -93
  165. package/dist/template/src/app/docs/scanner/content-type.md +0 -599
  166. package/dist/template/src/app/docs/scanner/index.md +0 -212
  167. package/dist/template/src/app/docs/scanner/page.mdx +0 -307
  168. package/dist/template/src/app/docs/scanner/python.md +0 -469
  169. package/dist/template/src/app/docs/scanner/python_parser.md +0 -1056
  170. package/dist/template/src/app/docs/scanner/rust.md +0 -325
  171. package/dist/template/src/app/docs/scanner/typescript.md +0 -201
  172. package/dist/template/src/app/icon.tsx +0 -29
  173. package/dist/utils/validation.d.ts +0 -1
  174. package/dist/utils/validation.js +0 -12
  175. /package/dist/{autofix/autofix.test.d.ts → audit/types.js} +0 -0
  176. /package/dist/{generator/generator.test.d.ts → next-actions/types.js} +0 -0
  177. /package/dist/{generator/writer.test.d.ts → refresh/types.js} +0 -0
  178. /package/dist/{llm/llm.manual-test.d.ts → review/types.js} +0 -0
  179. /package/dist/{scanner/content-type.test.d.ts → structure/types.js} +0 -0
  180. /package/dist/{scanner/scanner.test.d.ts → testing/types.js} +0 -0
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Use LLM to plan a smart documentation structure
3
+ */
4
+ export async function planStructure(elements, topology, client) {
5
+ // Build a summary of the API surface for the LLM
6
+ const summary = buildAPISummary(elements, topology);
7
+ const response = await client.complete({
8
+ messages: [
9
+ {
10
+ role: 'system',
11
+ content: STRUCTURE_PLANNER_PROMPT,
12
+ },
13
+ {
14
+ role: 'user',
15
+ content: summary,
16
+ },
17
+ ],
18
+ temperature: 0,
19
+ maxTokens: 4096,
20
+ });
21
+ return parseStructurePlan(response.content, elements);
22
+ }
23
+ const STRUCTURE_PLANNER_PROMPT = `You are an expert documentation architect. Given a list of API elements, plan a documentation structure organized by user journey — NOT by file.
24
+
25
+ ## Output Structure
26
+
27
+ Return a JSON object with this shape:
28
+ \`\`\`json
29
+ {
30
+ "pages": [
31
+ {
32
+ "slug": "quickstart",
33
+ "title": "Quickstart",
34
+ "category": "quickstart",
35
+ "description": "Get from zero to first API call in 5 minutes",
36
+ "elementNames": ["createClient", "configure"]
37
+ },
38
+ {
39
+ "slug": "authentication",
40
+ "title": "Authentication",
41
+ "category": "concepts",
42
+ "description": "How auth works — API keys, OAuth, sessions",
43
+ "elementNames": ["authenticate", "refreshToken", "SessionManager"]
44
+ }
45
+ ]
46
+ }
47
+ \`\`\`
48
+
49
+ ## Rules
50
+ 1. ALWAYS start with a "quickstart" page covering entry points
51
+ 2. Group by CONCEPT (auth, data model, errors), not by file
52
+ 3. Put classes and their methods together on one page
53
+ 4. "api" category pages are per-class or per-module API reference
54
+ 5. "guides" are task-oriented: "Setting up webhooks", "Testing your integration"
55
+ 6. Keep slugs lowercase-kebab-case
56
+ 7. Every element must appear in exactly one page
57
+ 8. Return ONLY the JSON, no markdown fences, no explanation`;
58
+ function buildAPISummary(elements, topology) {
59
+ let summary = `## API Surface (${elements.length} elements)\n\n`;
60
+ if (topology.entryPoints.length > 0) {
61
+ summary += `### Entry Points\n`;
62
+ for (const ep of topology.entryPoints) {
63
+ summary += `- ${ep.kind} \`${ep.name}\` (${ep.filePath})\n`;
64
+ }
65
+ summary += '\n';
66
+ }
67
+ summary += `### Public API (${topology.publicAPI.length} elements)\n`;
68
+ for (const el of topology.publicAPI.slice(0, 100)) {
69
+ const parent = el.parentClass ? ` (${el.parentClass})` : '';
70
+ summary += `- ${el.kind} \`${el.name}\`${parent}: ${el.signature.slice(0, 80)}\n`;
71
+ }
72
+ if (topology.publicAPI.length > 100) {
73
+ summary += `... and ${topology.publicAPI.length - 100} more\n`;
74
+ }
75
+ if (topology.classClusters.size > 0) {
76
+ summary += `\n### Classes (${topology.classClusters.size})\n`;
77
+ for (const [className, methods] of topology.classClusters) {
78
+ summary += `- \`${className}\` with methods: ${methods.map(m => m.name).join(', ')}\n`;
79
+ }
80
+ }
81
+ return summary;
82
+ }
83
+ /**
84
+ * Parse LLM response into DocStructure
85
+ */
86
+ function parseStructurePlan(content, elements) {
87
+ // Extract JSON from response
88
+ let jsonStr = content.trim();
89
+ // Remove markdown fences if present
90
+ jsonStr = jsonStr.replace(/^```(?:json)?\s*\n?/, '').replace(/\n?```\s*$/, '');
91
+ let parsed;
92
+ try {
93
+ parsed = JSON.parse(jsonStr);
94
+ }
95
+ catch {
96
+ // Fallback: create a simple structure
97
+ return createFallbackStructure(elements);
98
+ }
99
+ // Map element names to actual elements (case-insensitive for LLM tolerance)
100
+ const elementByName = new Map();
101
+ const elementByNameLower = new Map();
102
+ for (const el of elements) {
103
+ elementByName.set(el.name, el);
104
+ elementByNameLower.set(el.name.toLowerCase(), el);
105
+ }
106
+ const pages = [];
107
+ const assignedElements = new Set();
108
+ for (const page of parsed.pages) {
109
+ const pageElements = [];
110
+ for (const name of page.elementNames || []) {
111
+ // Try exact match first, then case-insensitive
112
+ let el = elementByName.get(name);
113
+ if (!el) {
114
+ el = elementByNameLower.get(name.toLowerCase());
115
+ if (el) {
116
+ console.log(` Note: LLM returned "${name}", matched to "${el.name}" (case-insensitive)`);
117
+ }
118
+ }
119
+ if (el) {
120
+ pageElements.push(el);
121
+ assignedElements.add(el.name);
122
+ }
123
+ else {
124
+ console.log(` Warning: LLM referenced unknown element "${name}" — skipping`);
125
+ }
126
+ }
127
+ const category = (['quickstart', 'concepts', 'guides', 'api'].includes(page.category)
128
+ ? page.category
129
+ : 'api');
130
+ pages.push({
131
+ slug: page.slug,
132
+ title: page.title,
133
+ category,
134
+ description: page.description,
135
+ elements: pageElements,
136
+ });
137
+ }
138
+ // Add unassigned elements to an "API Reference" catch-all
139
+ const unassigned = elements.filter(el => !assignedElements.has(el.name));
140
+ if (unassigned.length > 0) {
141
+ pages.push({
142
+ slug: 'api-reference',
143
+ title: 'API Reference',
144
+ category: 'api',
145
+ description: 'Complete API reference',
146
+ elements: unassigned,
147
+ });
148
+ }
149
+ // Build navigation
150
+ const navigation = buildNavigation(pages);
151
+ return { pages, navigation };
152
+ }
153
+ function createFallbackStructure(elements) {
154
+ const pages = [{
155
+ slug: 'api-reference',
156
+ title: 'API Reference',
157
+ category: 'api',
158
+ description: 'Complete API reference',
159
+ elements,
160
+ }];
161
+ return {
162
+ pages,
163
+ navigation: [{ title: 'API Reference', slug: 'api-reference' }],
164
+ };
165
+ }
166
+ function buildNavigation(pages) {
167
+ const categories = {
168
+ quickstart: { title: 'Getting Started', slug: '', children: [] },
169
+ concepts: { title: 'Concepts', slug: '', children: [] },
170
+ guides: { title: 'Guides', slug: '', children: [] },
171
+ api: { title: 'API Reference', slug: '', children: [] },
172
+ };
173
+ for (const page of pages) {
174
+ const cat = categories[page.category];
175
+ if (cat) {
176
+ cat.children.push({ title: page.title, slug: page.slug });
177
+ }
178
+ }
179
+ return Object.values(categories).filter(c => c.children.length > 0);
180
+ }
@@ -0,0 +1,16 @@
1
+ import { APIElement } from '../scanner/types.js';
2
+ /**
3
+ * Analysis of a codebase's API surface
4
+ */
5
+ export interface TopologyAnalysis {
6
+ entryPoints: APIElement[];
7
+ publicAPI: APIElement[];
8
+ internalElements: APIElement[];
9
+ classClusters: Map<string, APIElement[]>;
10
+ fileClusters: Map<string, APIElement[]>;
11
+ }
12
+ /**
13
+ * Analyze the topology of a set of API elements.
14
+ * Identifies entry points, public surface, class groupings, and file clusters.
15
+ */
16
+ export declare function analyzeTopology(elements: APIElement[]): TopologyAnalysis;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Analyze the topology of a set of API elements.
3
+ * Identifies entry points, public surface, class groupings, and file clusters.
4
+ */
5
+ export function analyzeTopology(elements) {
6
+ const entryPoints = [];
7
+ const publicAPI = [];
8
+ const internalElements = [];
9
+ const classClusters = new Map();
10
+ const fileClusters = new Map();
11
+ for (const el of elements) {
12
+ // Classify by visibility
13
+ if (el.isExported || el.isPublic) {
14
+ publicAPI.push(el);
15
+ }
16
+ else {
17
+ internalElements.push(el);
18
+ }
19
+ // Identify likely entry points (exported top-level functions/classes)
20
+ if ((el.isExported || el.isPublic) && !el.parentClass && (el.kind === 'function' || el.kind === 'class')) {
21
+ // Heuristic: entry points are often in index.ts, main.ts, or have names like init, create, setup
22
+ const isEntryFile = /(?:index|main|app|server|client)\.\w+$/.test(el.filePath);
23
+ const isEntryName = /^(?:create|init|setup|configure|connect|start|build)/i.test(el.name);
24
+ if (isEntryFile || isEntryName) {
25
+ entryPoints.push(el);
26
+ }
27
+ }
28
+ // Group by parent class
29
+ if (el.parentClass) {
30
+ if (!classClusters.has(el.parentClass)) {
31
+ classClusters.set(el.parentClass, []);
32
+ }
33
+ classClusters.get(el.parentClass).push(el);
34
+ }
35
+ // Group by source file
36
+ const fileKey = el.filePath.replace(/\.(ts|js|py|go|rs|java|cs|php|kt|swift|rb)x?$/, '');
37
+ if (!fileClusters.has(fileKey)) {
38
+ fileClusters.set(fileKey, []);
39
+ }
40
+ fileClusters.get(fileKey).push(el);
41
+ }
42
+ return {
43
+ entryPoints,
44
+ publicAPI,
45
+ internalElements,
46
+ classClusters,
47
+ fileClusters,
48
+ };
49
+ }
@@ -0,0 +1,26 @@
1
+ import { APIElement } from '../scanner/types.js';
2
+ /**
3
+ * A planned page in the smart-structured docs
4
+ */
5
+ export interface PagePlan {
6
+ slug: string;
7
+ title: string;
8
+ category: 'quickstart' | 'concepts' | 'guides' | 'api';
9
+ description: string;
10
+ elements: APIElement[];
11
+ }
12
+ /**
13
+ * Full documentation structure plan
14
+ */
15
+ export interface DocStructure {
16
+ pages: PagePlan[];
17
+ navigation: NavigationNode[];
18
+ }
19
+ /**
20
+ * Navigation tree node
21
+ */
22
+ export interface NavigationNode {
23
+ title: string;
24
+ slug: string;
25
+ children?: NavigationNode[];
26
+ }
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "name": "Skrypt",
3
3
  "description": "AI-powered documentation generation",
4
+ "fonts": {
5
+ "sans": "Inter",
6
+ "mono": "JetBrains Mono"
7
+ },
4
8
  "theme": {
5
9
  "primaryColor": "#3b82f6",
6
- "accentColor": "#8b5cf6",
7
- "font": "Inter"
10
+ "accentColor": "#8b5cf6"
8
11
  },
9
12
  "navigation": [
10
13
  {
@@ -14,6 +14,37 @@ const withMDX = createMDX({
14
14
  const nextConfig = {
15
15
  pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'],
16
16
  transpilePackages: ['@scalar/api-reference-react', '@scalar/api-reference'],
17
+ async headers() {
18
+ return [{
19
+ source: '/(.*)',
20
+ headers: [
21
+ {
22
+ key: 'Content-Security-Policy',
23
+ value: "default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; font-src 'self' data: https: https://fonts.gstatic.com; connect-src 'self' https:; frame-src 'self' https://codesandbox.io"
24
+ },
25
+ {
26
+ key: 'X-Frame-Options',
27
+ value: 'DENY'
28
+ },
29
+ {
30
+ key: 'X-Content-Type-Options',
31
+ value: 'nosniff'
32
+ },
33
+ {
34
+ key: 'Referrer-Policy',
35
+ value: 'strict-origin-when-cross-origin'
36
+ },
37
+ {
38
+ key: 'Strict-Transport-Security',
39
+ value: 'max-age=31536000; includeSubDomains'
40
+ },
41
+ {
42
+ key: 'Permissions-Policy',
43
+ value: 'camera=(), microphone=(), geolocation=()'
44
+ }
45
+ ]
46
+ }]
47
+ },
17
48
  }
18
49
 
19
50
  export default withMDX(nextConfig)
@@ -13,16 +13,13 @@
13
13
  "@ai-sdk/anthropic": "^1.0.0",
14
14
  "@ai-sdk/openai": "^1.0.0",
15
15
  "@ai-sdk/react": "^1.0.0",
16
- "@codesandbox/sandpack-react": "^2.20.0",
17
16
  "@mdx-js/loader": "^3.1.0",
18
17
  "@mdx-js/react": "^3.1.0",
19
18
  "@next/mdx": "^15.3.0",
20
19
  "@orama/orama": "^3.1.0",
21
- "@scalar/nextjs-api-reference": "^0.4.0",
22
20
  "clsx": "^2.1.0",
23
21
  "gray-matter": "^4.0.3",
24
22
  "lucide-react": "^0.500.0",
25
- "mermaid": "^11.13.0",
26
23
  "next": "^15.3.0",
27
24
  "next-mdx-remote": "^6.0.0",
28
25
  "react": "^19.0.0",
@@ -35,6 +32,11 @@
35
32
  "tailwind-merge": "^3.0.0",
36
33
  "yaml": "^2.8.2"
37
34
  },
35
+ "optionalDependencies": {
36
+ "@codesandbox/sandpack-react": "^2.20.0",
37
+ "mermaid": "^11.13.0",
38
+ "@scalar/nextjs-api-reference": "^0.4.0"
39
+ },
38
40
  "devDependencies": {
39
41
  "@tailwindcss/postcss": "^4.1.0",
40
42
  "@types/mdx": "^2.0.0",
@@ -5,12 +5,17 @@ import { SyntaxThemeProvider } from '@/contexts/syntax-theme'
5
5
  import { readFileSync } from 'fs'
6
6
  import { join } from 'path'
7
7
  import { derivePrimaryColors } from '@/lib/theme-utils'
8
+ import { resolveFonts, buildGoogleFontsUrl } from '@/lib/fonts'
8
9
 
9
10
  const inter = Inter({ subsets: ['latin'] })
10
11
 
11
12
  function getDocsConfig() {
12
13
  const configPath = join(process.cwd(), 'docs.json')
13
- return JSON.parse(readFileSync(configPath, 'utf-8'))
14
+ try {
15
+ return JSON.parse(readFileSync(configPath, 'utf-8'))
16
+ } catch {
17
+ return {}
18
+ }
14
19
  }
15
20
 
16
21
  const docsConfig = getDocsConfig()
@@ -19,19 +24,13 @@ const siteName = docsConfig.name || 'Documentation'
19
24
  const siteDescription = docsConfig.description || 'Documentation'
20
25
 
21
26
  // Theme: derive primary color variants from docs.json
22
- const primaryHex = docsConfig.theme?.primaryColor || '#3b82f6'
27
+ const rawColor = docsConfig.theme?.primaryColor
28
+ const primaryHex = typeof rawColor === 'string' && /^#[0-9a-fA-F]{3}(?:[0-9a-fA-F]{3})?$/.test(rawColor) ? rawColor : '#3b82f6'
23
29
  const { primary, primaryDark, primaryLight, primaryForeground } = derivePrimaryColors(primaryHex)
24
30
 
25
- // Font: read from docs.json, default to Inter
26
- const customFont = docsConfig.theme?.font || 'Inter'
27
- const safeFont = customFont ? customFont.replace(/[^a-zA-Z0-9\s-]/g, '') : null
28
- const isCustomFont = safeFont !== 'Inter'
29
- const googleFontsUrl = isCustomFont
30
- ? `https://fonts.googleapis.com/css2?family=${encodeURIComponent(safeFont!)}:wght@400;500;600;700;800&display=swap`
31
- : null
32
- const googleFontsPreloadUrl = isCustomFont
33
- ? `https://fonts.googleapis.com/css2?family=${encodeURIComponent(safeFont!)}:wght@400;600;700&display=swap`
34
- : null
31
+ // Fonts: read from docs.json (supports both new `fonts` key and legacy `theme.font`)
32
+ const { sans, mono, needsGoogleFonts } = resolveFonts(docsConfig)
33
+ const googleFontsUrl = needsGoogleFonts ? buildGoogleFontsUrl(sans, mono) : null
35
34
 
36
35
  // Build the CSS custom property overrides
37
36
  const themeStyles = `
@@ -40,7 +39,8 @@ const themeStyles = `
40
39
  --color-primary-dark: ${primaryDark};
41
40
  --color-primary-light: ${primaryLight};
42
41
  --color-primary-foreground: ${primaryForeground};
43
- --font-body: "${safeFont}", "Inter", ui-sans-serif, system-ui, sans-serif;
42
+ --font-body: "${sans.name}", "Inter", ui-sans-serif, system-ui, sans-serif;
43
+ --font-mono: "${mono.name}", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
44
44
  }
45
45
  `
46
46
 
@@ -0,0 +1,29 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { readFile } from 'fs/promises'
3
+ import { join } from 'path'
4
+
5
+ export async function GET() {
6
+ const paths = [
7
+ join(process.cwd(), 'content', 'docs', 'llms-full.md'),
8
+ join(process.cwd(), 'public', 'llms-full.md'),
9
+ ]
10
+
11
+ for (const path of paths) {
12
+ try {
13
+ const content = await readFile(path, 'utf-8')
14
+ return new NextResponse(content, {
15
+ headers: {
16
+ 'Content-Type': 'text/markdown; charset=utf-8',
17
+ 'Cache-Control': 'public, max-age=3600, s-maxage=86400',
18
+ },
19
+ })
20
+ } catch {
21
+ continue
22
+ }
23
+ }
24
+
25
+ return new NextResponse('# No llms-full.md found\n\nRun `skrypt generate` to create one.\n', {
26
+ status: 404,
27
+ headers: { 'Content-Type': 'text/markdown; charset=utf-8' },
28
+ })
29
+ }
@@ -0,0 +1,29 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { readFile } from 'fs/promises'
3
+ import { join } from 'path'
4
+
5
+ export async function GET() {
6
+ const paths = [
7
+ join(process.cwd(), 'content', 'docs', 'llms.txt'),
8
+ join(process.cwd(), 'public', 'llms.txt'),
9
+ ]
10
+
11
+ for (const path of paths) {
12
+ try {
13
+ const content = await readFile(path, 'utf-8')
14
+ return new NextResponse(content, {
15
+ headers: {
16
+ 'Content-Type': 'text/plain; charset=utf-8',
17
+ 'Cache-Control': 'public, max-age=3600, s-maxage=86400',
18
+ },
19
+ })
20
+ } catch {
21
+ continue
22
+ }
23
+ }
24
+
25
+ return new NextResponse('# No llms.txt found\n\nRun `skrypt generate` to create one.\n', {
26
+ status: 404,
27
+ headers: { 'Content-Type': 'text/plain; charset=utf-8' },
28
+ })
29
+ }
@@ -0,0 +1,174 @@
1
+ import { NextRequest, NextResponse } from 'next/server'
2
+ import { readFile } from 'fs/promises'
3
+ import { join, resolve } from 'path'
4
+
5
+ /**
6
+ * Markdown route handler for content negotiation.
7
+ *
8
+ * Serves raw MDX/Markdown content with JSX components stripped,
9
+ * returning clean markdown that AI agents can consume directly.
10
+ */
11
+
12
+ interface RouteParams {
13
+ params: Promise<{ slug: string[] }>
14
+ }
15
+
16
+ export async function GET(request: NextRequest, { params }: RouteParams) {
17
+ const { slug } = await params
18
+ const contentDir = resolve(process.cwd(), 'content', 'docs')
19
+
20
+ // Reject path traversal: slug segments must not contain .. or absolute paths
21
+ if (slug.some(s => s === '..' || s === '.' || s.includes('/') || s.includes('\\'))) {
22
+ return new NextResponse('# 400 Bad Request\n', {
23
+ status: 400,
24
+ headers: { 'Content-Type': 'text/markdown; charset=utf-8' },
25
+ })
26
+ }
27
+
28
+ // Try to find the MDX/MD source file
29
+ const filePath = join(contentDir, ...slug)
30
+
31
+ // Double-check resolved path stays within content directory
32
+ const resolved = resolve(filePath)
33
+ if (!resolved.startsWith(contentDir)) {
34
+ return new NextResponse('# 400 Bad Request\n', {
35
+ status: 400,
36
+ headers: { 'Content-Type': 'text/markdown; charset=utf-8' },
37
+ })
38
+ }
39
+ let content: string | null = null
40
+ let resolvedPath = ''
41
+
42
+ for (const ext of ['.mdx', '.md', '/index.mdx', '/index.md']) {
43
+ try {
44
+ resolvedPath = filePath + ext
45
+ content = await readFile(resolvedPath, 'utf-8')
46
+ break
47
+ } catch {
48
+ continue
49
+ }
50
+ }
51
+
52
+ if (!content) {
53
+ return new NextResponse('# 404 Not Found\n\nThis documentation page does not exist.\n', {
54
+ status: 404,
55
+ headers: { 'Content-Type': 'text/markdown; charset=utf-8' },
56
+ })
57
+ }
58
+
59
+ // Strip JSX components for clean markdown output
60
+ const markdown = stripMdxToMarkdown(content)
61
+
62
+ return new NextResponse(markdown, {
63
+ status: 200,
64
+ headers: {
65
+ 'Content-Type': 'text/markdown; charset=utf-8',
66
+ 'Cache-Control': 'public, max-age=3600, s-maxage=86400',
67
+ 'X-Content-Source': 'mdx',
68
+ },
69
+ })
70
+ }
71
+
72
+ /**
73
+ * Lightweight MDX-to-markdown stripping for the route handler.
74
+ * Runs at request time so must be fast — uses regex, not a full parser.
75
+ */
76
+ function stripMdxToMarkdown(mdx: string): string {
77
+ let out = mdx
78
+
79
+ // Remove import/export statements
80
+ out = out.replace(/^import\s+.*$/gm, '')
81
+ out = out.replace(/^export\s+(?!default\s).*$/gm, '')
82
+
83
+ // CodeGroup → flatten (keep code blocks)
84
+ out = out.replace(/<CodeGroup>\s*([\s\S]*?)\s*<\/CodeGroup>/g, (_m, inner: string) => inner.trim())
85
+
86
+ // Callout variants → blockquotes
87
+ for (const tag of ['Callout', 'Info', 'Warning', 'Success', 'Error', 'Tip', 'Note']) {
88
+ out = out.replace(
89
+ new RegExp(`<${tag}\\s+type=["']([^"']+)["'][^>]*>\\s*([\\s\\S]*?)\\s*<\\/${tag}>`, 'g'),
90
+ (_m, type: string, content: string) => {
91
+ const label = type.charAt(0).toUpperCase() + type.slice(1)
92
+ return `> **${label}:** ${content.trim()}\n`
93
+ }
94
+ )
95
+ out = out.replace(
96
+ new RegExp(`<${tag}(?:\\s[^>]*)?>\\s*([\\s\\S]*?)\\s*<\\/${tag}>`, 'g'),
97
+ (_m, content: string) => {
98
+ const label = tag === 'Callout' ? 'Note' : tag
99
+ return `> **${label}:** ${content.trim()}\n`
100
+ }
101
+ )
102
+ }
103
+
104
+ // CardGroup → unwrap
105
+ out = out.replace(/<CardGroup[^>]*>\s*/g, '')
106
+ out = out.replace(/\s*<\/CardGroup>/g, '')
107
+
108
+ // Card with href
109
+ out = out.replace(
110
+ /<Card\s+title=["']([^"']+)["']\s+(?:[^>]*?href=["']([^"']+)["'][^>]*?)>\s*([\s\S]*?)\s*<\/Card>/g,
111
+ (_m, title: string, href: string, desc: string) => {
112
+ const d = desc.trim().replace(/<[^>]+>/g, '')
113
+ return d ? `- **[${title}](${href})** — ${d}` : `- **[${title}](${href})**`
114
+ }
115
+ )
116
+
117
+ // Card without href
118
+ out = out.replace(
119
+ /<Card\s+title=["']([^"']+)["'][^>]*>\s*([\s\S]*?)\s*<\/Card>/g,
120
+ (_m, title: string, desc: string) => {
121
+ const d = desc.trim().replace(/<[^>]+>/g, '')
122
+ return d ? `- **${title}** — ${d}` : `- **${title}**`
123
+ }
124
+ )
125
+
126
+ // Tabs
127
+ out = out.replace(/<TabList>\s*([\s\S]*?)\s*<\/TabList>/g, '')
128
+ out = out.replace(/<Tab\s+label=["']([^"']+)["'][^>]*>\s*([\s\S]*?)\s*<\/Tab>/g,
129
+ (_m, label: string, content: string) => `#### ${label}\n\n${content.trim()}\n`
130
+ )
131
+ out = out.replace(/<TabPanel[^>]*>\s*([\s\S]*?)\s*<\/TabPanel>/g, (_m, c: string) => c.trim())
132
+ out = out.replace(/<Tabs[^>]*>\s*/g, '')
133
+ out = out.replace(/\s*<\/Tabs>/g, '')
134
+
135
+ // Steps
136
+ out = out.replace(/<Steps>\s*([\s\S]*?)\s*<\/Steps>/g, (_match, inner: string) => {
137
+ let n = 0
138
+ return inner.replace(
139
+ /<Step\s*(?:title=["']([^"']+)["'][^>]*)?>?\s*([\s\S]*?)\s*<\/Step>/g,
140
+ (_m, title: string | undefined, content: string) => {
141
+ n++
142
+ return `${n}. ${title ? `**${title}** ` : ''}${content.trim()}\n`
143
+ }
144
+ )
145
+ })
146
+
147
+ // Accordion
148
+ out = out.replace(/<AccordionGroup[^>]*>\s*/g, '')
149
+ out = out.replace(/\s*<\/AccordionGroup>/g, '')
150
+ out = out.replace(/<Accordion\s+title=["']([^"']+)["'][^>]*>\s*([\s\S]*?)\s*<\/Accordion>/g,
151
+ (_m, title: string, content: string) => `**${title}**\n\n${content.trim()}\n`
152
+ )
153
+
154
+ // Screenshot → image
155
+ out = out.replace(/<Screenshot\s+(?:[^>]*?src=["']([^"']+)["'][^>]*?)(?:\s+alt=["']([^"']+)["'])?[^>]*\s*\/?>/g,
156
+ (_m, src: string, alt?: string) => `![${alt || 'Screenshot'}](${src})`
157
+ )
158
+ out = out.replace(/<Screenshot\s+(?:[^>]*?src=["']([^"']+)["'][^>]*?)>[\s\S]*?<\/Screenshot>/g,
159
+ (_m, src: string) => `![Screenshot](${src})`
160
+ )
161
+
162
+ // Frame → unwrap
163
+ out = out.replace(/<Frame[^>]*>\s*/g, '')
164
+ out = out.replace(/\s*<\/Frame>/g, '')
165
+
166
+ // Catch-all: remove remaining JSX
167
+ out = out.replace(/<[A-Z][A-Za-z]*\s[^>]*\/>/g, '')
168
+ out = out.replace(/<[A-Z][A-Za-z]*(?:\s[^>]*)?>/g, '')
169
+ out = out.replace(/<\/[A-Z][A-Za-z]*>/g, '')
170
+
171
+ // Clean up
172
+ out = out.replace(/\n{4,}/g, '\n\n\n')
173
+ return out.trim() + '\n'
174
+ }