docorbit 0.1.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 (144) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +660 -0
  3. package/apps/cli/bin/docorbit.js +8 -0
  4. package/apps/cli/src/commands/add.ts +44 -0
  5. package/apps/cli/src/commands/api.ts +38 -0
  6. package/apps/cli/src/commands/context.ts +47 -0
  7. package/apps/cli/src/commands/dashboard.ts +55 -0
  8. package/apps/cli/src/commands/diff.ts +30 -0
  9. package/apps/cli/src/commands/evaluate.ts +133 -0
  10. package/apps/cli/src/commands/examples.ts +39 -0
  11. package/apps/cli/src/commands/export.ts +89 -0
  12. package/apps/cli/src/commands/impact.ts +31 -0
  13. package/apps/cli/src/commands/init.ts +69 -0
  14. package/apps/cli/src/commands/inspect.ts +30 -0
  15. package/apps/cli/src/commands/mcp.ts +72 -0
  16. package/apps/cli/src/commands/pitfalls.ts +38 -0
  17. package/apps/cli/src/commands/recipes.ts +35 -0
  18. package/apps/cli/src/commands/search.ts +48 -0
  19. package/apps/cli/src/commands/update.ts +73 -0
  20. package/apps/cli/src/commands/verify.ts +48 -0
  21. package/apps/cli/src/formatters/colors.ts +23 -0
  22. package/apps/cli/src/formatters/inspection.ts +102 -0
  23. package/apps/cli/src/formatters/knowledge.ts +272 -0
  24. package/apps/cli/src/formatters/retrieval.ts +74 -0
  25. package/apps/cli/src/formatters/terminal.ts +6 -0
  26. package/apps/cli/src/formatters/verification.ts +126 -0
  27. package/apps/cli/src/index.ts +409 -0
  28. package/bin/docorbit.js +8 -0
  29. package/package.json +46 -0
  30. package/packages/core/src/dashboard/server.ts +314 -0
  31. package/packages/core/src/dashboard/ui.ts +586 -0
  32. package/packages/core/src/implementation-service.ts +451 -0
  33. package/packages/core/src/index.ts +7 -0
  34. package/packages/core/src/inspector.ts +71 -0
  35. package/packages/core/src/pipeline.ts +331 -0
  36. package/packages/crawler/src/config.ts +12 -0
  37. package/packages/crawler/src/fetcher.ts +185 -0
  38. package/packages/crawler/src/index.ts +2 -0
  39. package/packages/discovery/src/index.ts +31 -0
  40. package/packages/discovery/src/provider.ts +47 -0
  41. package/packages/discovery/src/providers/generic.ts +98 -0
  42. package/packages/discovery/src/providers/github.ts +61 -0
  43. package/packages/discovery/src/providers/llms-txt.ts +73 -0
  44. package/packages/discovery/src/providers/markdown.ts +48 -0
  45. package/packages/discovery/src/providers/openapi.ts +91 -0
  46. package/packages/discovery/src/providers/sitemap.ts +62 -0
  47. package/packages/discovery/src/providers/skill.ts +54 -0
  48. package/packages/discovery/src/ranker.ts +123 -0
  49. package/packages/evaluation/src/dataset.ts +963 -0
  50. package/packages/evaluation/src/index.ts +8 -0
  51. package/packages/evaluation/src/runner.ts +241 -0
  52. package/packages/evaluation/src/strategies/context7-runner.ts +269 -0
  53. package/packages/evaluation/src/strategies/docorbit-runner.ts +228 -0
  54. package/packages/evaluation/src/strategies/firecrawl-runner.ts +172 -0
  55. package/packages/evaluation/src/strategies/web-search-runner.ts +194 -0
  56. package/packages/evaluation/src/types.ts +34 -0
  57. package/packages/evaluation/src/version-matcher.ts +73 -0
  58. package/packages/export/src/agents-md.ts +200 -0
  59. package/packages/export/src/claude-md.ts +141 -0
  60. package/packages/export/src/docs-map.ts +150 -0
  61. package/packages/export/src/index.ts +6 -0
  62. package/packages/export/src/llms-txt.ts +96 -0
  63. package/packages/export/src/service.ts +250 -0
  64. package/packages/export/src/skill-md.ts +128 -0
  65. package/packages/mcp/src/index.ts +46 -0
  66. package/packages/mcp/src/resources/index.ts +189 -0
  67. package/packages/mcp/src/server.ts +278 -0
  68. package/packages/mcp/src/tools/analyze-impact.ts +74 -0
  69. package/packages/mcp/src/tools/check-api.ts +86 -0
  70. package/packages/mcp/src/tools/diff-docs.ts +68 -0
  71. package/packages/mcp/src/tools/export-context.ts +73 -0
  72. package/packages/mcp/src/tools/find-api.ts +99 -0
  73. package/packages/mcp/src/tools/find-example.ts +100 -0
  74. package/packages/mcp/src/tools/find-pitfall.ts +94 -0
  75. package/packages/mcp/src/tools/find-recipe.ts +98 -0
  76. package/packages/mcp/src/tools/get-doc.ts +130 -0
  77. package/packages/mcp/src/tools/get-docs-map.ts +64 -0
  78. package/packages/mcp/src/tools/get-version.ts +118 -0
  79. package/packages/mcp/src/tools/implementation-context.ts +88 -0
  80. package/packages/mcp/src/tools/index.ts +59 -0
  81. package/packages/mcp/src/tools/list-sources.ts +85 -0
  82. package/packages/mcp/src/tools/search-docs.ts +123 -0
  83. package/packages/mcp/src/tools/types.ts +28 -0
  84. package/packages/mcp/src/transports/http.ts +256 -0
  85. package/packages/mcp/src/transports/stdio.ts +105 -0
  86. package/packages/mcp/src/transports/types.ts +6 -0
  87. package/packages/mcp/src/types.ts +102 -0
  88. package/packages/normalizer/src/example-indexer.ts +240 -0
  89. package/packages/normalizer/src/html.ts +253 -0
  90. package/packages/normalizer/src/index.ts +8 -0
  91. package/packages/normalizer/src/llms.ts +83 -0
  92. package/packages/normalizer/src/openapi/endpoint-parser.ts +406 -0
  93. package/packages/normalizer/src/openapi/schema-resolver.ts +111 -0
  94. package/packages/normalizer/src/openapi.ts +2 -0
  95. package/packages/normalizer/src/page.ts +184 -0
  96. package/packages/normalizer/src/pitfall-extractor.ts +190 -0
  97. package/packages/normalizer/src/slicer.ts +455 -0
  98. package/packages/retrieval/src/engine.ts +120 -0
  99. package/packages/retrieval/src/index.ts +7 -0
  100. package/packages/retrieval/src/intent.ts +43 -0
  101. package/packages/retrieval/src/packer.ts +145 -0
  102. package/packages/retrieval/src/recipe-engine.ts +313 -0
  103. package/packages/retrieval/src/scorer.ts +139 -0
  104. package/packages/retrieval/src/weights.ts +31 -0
  105. package/packages/security/src/annotations.ts +112 -0
  106. package/packages/security/src/index.ts +2 -0
  107. package/packages/security/src/ssrf.ts +153 -0
  108. package/packages/shared/src/errors.ts +53 -0
  109. package/packages/shared/src/hashing.ts +23 -0
  110. package/packages/shared/src/index.ts +3 -0
  111. package/packages/shared/src/types.ts +881 -0
  112. package/packages/storage/src/db.ts +72 -0
  113. package/packages/storage/src/index.ts +11 -0
  114. package/packages/storage/src/interfaces.ts +115 -0
  115. package/packages/storage/src/repositories/api-repository.ts +219 -0
  116. package/packages/storage/src/repositories/chunk-repository.ts +316 -0
  117. package/packages/storage/src/repositories/example-repository.ts +206 -0
  118. package/packages/storage/src/repositories/page-repository.ts +205 -0
  119. package/packages/storage/src/repositories/pitfall-repository.ts +188 -0
  120. package/packages/storage/src/repositories/source-repository.ts +205 -0
  121. package/packages/storage/src/repository.ts +256 -0
  122. package/packages/storage/src/schema.ts +269 -0
  123. package/packages/storage/src/search-tokens.ts +28 -0
  124. package/packages/verification/src/diff-engine.ts +258 -0
  125. package/packages/verification/src/extractor.ts +339 -0
  126. package/packages/verification/src/impact-scanner.ts +203 -0
  127. package/packages/verification/src/index.ts +5 -0
  128. package/packages/verification/src/services.ts +238 -0
  129. package/packages/verification/src/verifier.ts +375 -0
  130. package/packages/workspace/src/detector.ts +143 -0
  131. package/packages/workspace/src/ecosystems/cargo.ts +84 -0
  132. package/packages/workspace/src/ecosystems/composer.ts +42 -0
  133. package/packages/workspace/src/ecosystems/go.ts +54 -0
  134. package/packages/workspace/src/ecosystems/index.ts +34 -0
  135. package/packages/workspace/src/ecosystems/maven.ts +34 -0
  136. package/packages/workspace/src/ecosystems/npm.ts +83 -0
  137. package/packages/workspace/src/ecosystems/pub.ts +40 -0
  138. package/packages/workspace/src/ecosystems/pypi.ts +100 -0
  139. package/packages/workspace/src/ecosystems/rubygems.ts +30 -0
  140. package/packages/workspace/src/ecosystems/types.ts +18 -0
  141. package/packages/workspace/src/index.ts +5 -0
  142. package/packages/workspace/src/lockfile.ts +194 -0
  143. package/packages/workspace/src/resolver.ts +234 -0
  144. package/packages/workspace/src/semver.ts +259 -0
@@ -0,0 +1,102 @@
1
+ export interface JsonRpcRequest {
2
+ jsonrpc: '2.0';
3
+ id?: string | number | null;
4
+ method: string;
5
+ params?: Record<string, unknown>;
6
+ }
7
+
8
+ export interface JsonRpcResponse {
9
+ jsonrpc: '2.0';
10
+ id: string | number | null;
11
+ result?: unknown;
12
+ error?: JsonRpcErrorObject;
13
+ }
14
+
15
+ export interface JsonRpcNotification {
16
+ jsonrpc: '2.0';
17
+ method: string;
18
+ params?: Record<string, unknown>;
19
+ }
20
+
21
+ export interface JsonRpcErrorObject {
22
+ code: number;
23
+ message: string;
24
+ data?: unknown;
25
+ }
26
+
27
+ export const JSONRPC_ERRORS = {
28
+ PARSE_ERROR: -32700,
29
+ INVALID_REQUEST: -32600,
30
+ METHOD_NOT_FOUND: -32601,
31
+ INVALID_PARAMS: -32602,
32
+ INTERNAL_ERROR: -32603,
33
+ } as const;
34
+
35
+ export interface ToolInputSchema {
36
+ type: 'object';
37
+ properties: Record<string, {
38
+ type: string;
39
+ description: string;
40
+ items?: Record<string, unknown>;
41
+ enum?: string[];
42
+ default?: unknown;
43
+ }>;
44
+ required?: string[];
45
+ }
46
+
47
+ export interface McpTool {
48
+ name: string;
49
+ description: string;
50
+ inputSchema: ToolInputSchema;
51
+ }
52
+
53
+ export interface ToolContentItem {
54
+ type: 'text';
55
+ text: string;
56
+ }
57
+
58
+ export interface CallToolResult {
59
+ content: ToolContentItem[];
60
+ isError?: boolean;
61
+ }
62
+
63
+ export interface McpResource {
64
+ uri: string;
65
+ name: string;
66
+ description?: string;
67
+ mimeType?: string;
68
+ }
69
+
70
+ export interface ResourceContent {
71
+ uri: string;
72
+ mimeType?: string;
73
+ text?: string;
74
+ blob?: string;
75
+ }
76
+
77
+ export interface ReadResourceResult {
78
+ contents: ResourceContent[];
79
+ }
80
+
81
+ export interface ServerCapabilities {
82
+ tools?: {
83
+ listChanged?: boolean;
84
+ };
85
+ resources?: {
86
+ subscribe?: boolean;
87
+ listChanged?: boolean;
88
+ };
89
+ prompts?: {
90
+ listChanged?: boolean;
91
+ };
92
+ logging?: Record<string, unknown>;
93
+ }
94
+
95
+ export interface InitializeResult {
96
+ protocolVersion: string;
97
+ serverInfo: {
98
+ name: string;
99
+ version: string;
100
+ };
101
+ capabilities: ServerCapabilities;
102
+ }
@@ -0,0 +1,240 @@
1
+ import { createHash } from 'node:crypto';
2
+ import type {
3
+ NormalizedPage,
4
+ DocumentChunk,
5
+ IndexedExample,
6
+ SourceAuthority,
7
+ Provenance,
8
+ } from '../../shared/src/index.ts';
9
+
10
+ const FRAMEWORK_PATTERNS: Array<{ framework: string; pattern: RegExp }> = [
11
+ { framework: 'next', pattern: /(?:from\s+['"]next(?:\/.*)?['"]|require\(['"]next(?:\/.*)?['"]\)|'use client'|'use server')/i },
12
+ { framework: 'react', pattern: /(?:from\s+['"]react['"]|require\(['"]react['"]\)|useState|useEffect|useMemo|useCallback)/ },
13
+ { framework: 'express', pattern: /(?:from\s+['"]express['"]|require\(['"]express['"]\)|express\(\))/i },
14
+ { framework: 'fastify', pattern: /(?:from\s+['"]fastify['"]|require\(['"]fastify['"]\))/i },
15
+ { framework: 'hono', pattern: /(?:from\s+['"]hono['"]|require\(['"]hono['"]\))/i },
16
+ { framework: 'vue', pattern: /(?:from\s+['"]vue['"]|createApp\(|defineComponent)/i },
17
+ { framework: 'svelte', pattern: /(?:from\s+['"]svelte['"]|<script.*lang=["']ts["']>)/i },
18
+ { framework: 'fastapi', pattern: /(?:from\s+fastapi\s+import|FastAPI\()/i },
19
+ { framework: 'flask', pattern: /(?:from\s+flask\s+import|Flask\(__name__\))/i },
20
+ { framework: 'django', pattern: /(?:from\s+django|django\.)/i },
21
+ { framework: 'spring', pattern: /(?:@SpringBootApplication|@RestController|@GetMapping|@PostMapping)/i },
22
+ { framework: 'gin', pattern: /(?:github\.com\/gin-gonic\/gin|gin\.Default\(\))/i },
23
+ ];
24
+
25
+ const API_METHOD_PATH_REGEX = /\b(GET|POST|PUT|DELETE|PATCH)\s+([/\w\-_{}]+)/i;
26
+
27
+ /**
28
+ * Detects framework from code snippet content.
29
+ */
30
+ function detectFramework(code: string): string | undefined {
31
+ for (const { framework, pattern } of FRAMEWORK_PATTERNS) {
32
+ if (pattern.test(code)) {
33
+ return framework;
34
+ }
35
+ }
36
+ return undefined;
37
+ }
38
+
39
+ /**
40
+ * Detects an API endpoint reference from code or task context.
41
+ */
42
+ function detectRelatedApi(code: string, context: string): string | undefined {
43
+ // Check context first (e.g. "POST /v1/webhook_endpoints")
44
+ const contextMatch = context.match(API_METHOD_PATH_REGEX);
45
+ if (contextMatch) {
46
+ return `${contextMatch[1].toUpperCase()} ${contextMatch[2]}`;
47
+ }
48
+
49
+ // Check code
50
+ const codeMatch = code.match(API_METHOD_PATH_REGEX);
51
+ if (codeMatch) {
52
+ return `${codeMatch[1].toUpperCase()} ${codeMatch[2]}`;
53
+ }
54
+
55
+ // Common client library calls (e.g. fetch('/api/v1/foo', { method: 'POST' }))
56
+ const fetchMatch = code.match(/fetch\(\s*['"`]([/\w\-_{}]+)['"`](?:,\s*\{[^}]*method:\s*['"](\w+)['"])?/i);
57
+ if (fetchMatch) {
58
+ const method = (fetchMatch[2] || 'GET').toUpperCase();
59
+ return `${method} ${fetchMatch[1]}`;
60
+ }
61
+
62
+ // Framework router calls: app.post('/api/webhook', ...) or router.get('/users', ...) or @app.post('/webhook')
63
+ const routeMatch = code.match(/(?:app|router)\.(get|post|put|delete|patch)\(\s*['"`]([/\w\-_{}]+)['"`]/i);
64
+ if (routeMatch) {
65
+ return `${routeMatch[1].toUpperCase()} ${routeMatch[2]}`;
66
+ }
67
+
68
+ // Python decorators: @app.post("/webhook")
69
+ const pyRouteMatch = code.match(/@(?:app|router)\.(get|post|put|delete|patch)\(\s*['"`]([/\w\-_{}]+)['"`]/i);
70
+ if (pyRouteMatch) {
71
+ return `${pyRouteMatch[1].toUpperCase()} ${pyRouteMatch[2]}`;
72
+ }
73
+
74
+ return undefined;
75
+ }
76
+
77
+ /**
78
+ * Detects a primary symbol or function call from code.
79
+ */
80
+ function detectRelatedSymbol(code: string): string | undefined {
81
+ // Check function declarations
82
+ const funcMatch = code.match(/(?:function\s+([a-zA-Z0-9_$]+)|const\s+([a-zA-Z0-9_$]+)\s*=\s*(?:async\s*)?\()/);
83
+ if (funcMatch) {
84
+ return funcMatch[1] || funcMatch[2];
85
+ }
86
+
87
+ // Check class declarations
88
+ const classMatch = code.match(/class\s+([a-zA-Z0-9_$]+)/);
89
+ if (classMatch) {
90
+ return classMatch[1];
91
+ }
92
+
93
+ // Check prominent method invocations, ignoring routing boilerplate like app.post or res.json
94
+ const boilerplateCalls = new Set([
95
+ 'app.get', 'app.post', 'app.put', 'app.delete', 'app.use',
96
+ 'router.get', 'router.post', 'router.use',
97
+ 'express.raw', 'express.json', 'express.urlencoded',
98
+ 'res.json', 'res.send', 'res.status', 'console.log', 'console.error',
99
+ ]);
100
+ const allCalls = Array.from(code.matchAll(/([a-zA-Z0-9_$]+(?:\.[a-zA-Z0-9_$]+)+)\s*\(/g));
101
+ for (const match of allCalls) {
102
+ if (!boilerplateCalls.has(match[1])) {
103
+ return match[1];
104
+ }
105
+ }
106
+ if (allCalls.length > 0) {
107
+ return allCalls[0][1];
108
+ }
109
+
110
+ return undefined;
111
+ }
112
+
113
+ /**
114
+ * Extracts and indexes first-class code examples from normalized pages and chunks.
115
+ */
116
+ export function extractIndexedExamples(
117
+ page: NormalizedPage,
118
+ chunks: DocumentChunk[],
119
+ authority: SourceAuthority,
120
+ docVersion?: string,
121
+ snapshotId?: string
122
+ ): IndexedExample[] {
123
+ const examples: IndexedExample[] = [];
124
+ const seenCodeHashes = new Set<string>();
125
+ const nowIso = new Date().toISOString();
126
+
127
+ // Map each code example in the page to its enclosing chunk
128
+ for (const codeExample of page.codeExamples) {
129
+ const rawCode = codeExample.code.trim();
130
+ if (!rawCode || rawCode.length < 15) continue; // Skip trivial snippets
131
+
132
+ const codeHash = createHash('sha256').update(rawCode).digest('hex');
133
+ if (seenCodeHashes.has(codeHash)) continue;
134
+ seenCodeHashes.add(codeHash);
135
+
136
+ // Find the chunk that contains this code snippet
137
+ const matchingChunk = chunks.find(c =>
138
+ c.chunkType === 'code' && c.content.includes(rawCode)
139
+ ) || chunks.find(c => c.content.includes(rawCode));
140
+
141
+ // Determine task context from breadcrumbs or headings
142
+ let task = '';
143
+ if (matchingChunk && matchingChunk.sectionPath.length > 0) {
144
+ task = matchingChunk.sectionPath.join(' > ');
145
+ } else if (matchingChunk && matchingChunk.title) {
146
+ task = matchingChunk.title;
147
+ } else {
148
+ task = page.title;
149
+ }
150
+
151
+ const language = (codeExample.language || (matchingChunk?.language) || 'typescript').toLowerCase();
152
+ const framework = detectFramework(rawCode);
153
+ const relatedApi = detectRelatedApi(rawCode, task);
154
+ const relatedSymbol = detectRelatedSymbol(rawCode);
155
+
156
+ const exampleId = createHash('sha256')
157
+ .update(`${page.id}:${codeHash}`)
158
+ .digest('hex')
159
+ .substring(0, 16);
160
+
161
+ const provenance: Provenance = {
162
+ sourceUrl: page.url,
163
+ retrievedAt: page.metadata?.extractedAt || nowIso,
164
+ sourceAuthority: authority,
165
+ };
166
+
167
+ const effectiveSnapshotId = snapshotId || matchingChunk?.snapshotId || (page as any).snapshotId || 'snap_default';
168
+
169
+ examples.push({
170
+ id: exampleId,
171
+ chunkId: matchingChunk?.id,
172
+ pageId: page.id,
173
+ snapshotId: effectiveSnapshotId,
174
+ language,
175
+ framework,
176
+ task,
177
+ code: rawCode,
178
+ sourceUrl: page.url,
179
+ sourceAuthority: authority,
180
+ relatedApi,
181
+ relatedSymbol,
182
+ docVersion: docVersion || page.docVersion,
183
+ provenance,
184
+ createdAt: nowIso,
185
+ });
186
+ }
187
+
188
+ // Also check chunks of type 'code' that might not have been caught in page.codeExamples
189
+ for (const chunk of chunks) {
190
+ if (chunk.chunkType !== 'code') continue;
191
+ const rawCode = chunk.content.trim();
192
+ if (!rawCode || rawCode.length < 15) continue;
193
+
194
+ const codeHash = createHash('sha256').update(rawCode).digest('hex');
195
+ if (seenCodeHashes.has(codeHash)) continue;
196
+ seenCodeHashes.add(codeHash);
197
+
198
+ const task = chunk.sectionPath.length > 0
199
+ ? chunk.sectionPath.join(' > ')
200
+ : (chunk.title || page.title);
201
+
202
+ const language = (chunk.language || 'typescript').toLowerCase();
203
+ const framework = detectFramework(rawCode);
204
+ const relatedApi = detectRelatedApi(rawCode, task);
205
+ const relatedSymbol = detectRelatedSymbol(rawCode);
206
+
207
+ const exampleId = createHash('sha256')
208
+ .update(`${page.id}:${codeHash}`)
209
+ .digest('hex')
210
+ .substring(0, 16);
211
+
212
+ const provenance: Provenance = {
213
+ sourceUrl: page.url,
214
+ retrievedAt: page.metadata?.extractedAt || nowIso,
215
+ sourceAuthority: authority,
216
+ };
217
+
218
+ const effectiveSnapshotId = snapshotId || chunk.snapshotId || (page as any).snapshotId || 'snap_default';
219
+
220
+ examples.push({
221
+ id: exampleId,
222
+ chunkId: chunk.id,
223
+ pageId: page.id,
224
+ snapshotId: effectiveSnapshotId,
225
+ language,
226
+ framework,
227
+ task,
228
+ code: rawCode,
229
+ sourceUrl: page.url,
230
+ sourceAuthority: authority,
231
+ relatedApi,
232
+ relatedSymbol,
233
+ docVersion: docVersion || chunk.docVersion || page.docVersion,
234
+ provenance,
235
+ createdAt: nowIso,
236
+ });
237
+ }
238
+
239
+ return examples;
240
+ }
@@ -0,0 +1,253 @@
1
+ import type { Heading, Link, CodeExample } from '../../shared/src/index.ts';
2
+
3
+ export interface ExtractedHtmlDoc {
4
+ title: string;
5
+ markdown: string;
6
+ headings: Heading[];
7
+ links: Link[];
8
+ codeExamples: CodeExample[];
9
+ }
10
+
11
+ function unescapeHtml(text: string): string {
12
+ return text
13
+ .replace(/&amp;/g, '&')
14
+ .replace(/&lt;/g, '<')
15
+ .replace(/&gt;/g, '>')
16
+ .replace(/&quot;/g, '"')
17
+ .replace(/&#39;/g, "'")
18
+ .replace(/&apos;/g, "'")
19
+ .replace(/&nbsp;/g, ' ')
20
+ .replace(/&#(\d+);/g, (_, code) => String.fromCharCode(Number(code)))
21
+ .replace(/&#x([0-9a-fA-F]+);/g, (_, code) => String.fromCharCode(parseInt(code, 16)));
22
+ }
23
+
24
+ function slugify(text: string): string {
25
+ return text
26
+ .toLowerCase()
27
+ .trim()
28
+ .replace(/[^\w\s-]/g, '')
29
+ .replace(/[\s_-]+/g, '-')
30
+ .replace(/^-+|-+$/g, '');
31
+ }
32
+
33
+ /**
34
+ * Deterministically normalizes HTML documentation into clean Markdown,
35
+ * preserving code fences, language tags, headings, tables, callouts, and links.
36
+ */
37
+ export function normalizeHtmlToMarkdown(html: string, baseUrl: string): ExtractedHtmlDoc {
38
+ const headings: Heading[] = [];
39
+ const links: Link[] = [];
40
+ const codeExamples: CodeExample[] = [];
41
+
42
+ let title = '';
43
+
44
+ // 1. Extract title from <title> tag if present
45
+ const titleMatch = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
46
+ if (titleMatch) {
47
+ title = unescapeHtml(titleMatch[1]).replace(/\s+/g, ' ').trim();
48
+ title = title.split(/\s+[|–—-]\s+/)[0].trim();
49
+ }
50
+
51
+ // 2. Remove non-content / boilerplate tags
52
+ let cleaned = html
53
+ .replace(/<!--[\s\S]*?-->/g, '')
54
+ .replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, '')
55
+ .replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, '')
56
+ .replace(/<noscript\b[^>]*>[\s\S]*?<\/noscript>/gi, '')
57
+ .replace(/<svg\b[^>]*>[\s\S]*?<\/svg>/gi, '')
58
+ .replace(/<iframe\b[^>]*>[\s\S]*?<\/iframe>/gi, '')
59
+ .replace(/<nav\b[^>]*>[\s\S]*?<\/nav>/gi, '')
60
+ .replace(/<header\b[^>]*>[\s\S]*?<\/header>/gi, '')
61
+ .replace(/<footer\b[^>]*>[\s\S]*?<\/footer>/gi, '')
62
+ .replace(/<div\b[^>]*(?:class|id)=["'][^"']*\b(?:sidebar|nav|menu|navbar|footer|cookie-banner|breadcrumbs)\b[^"']*["'][^>]*>[\s\S]*?<\/div>/gi, '');
63
+
64
+ // 3. Extract and preserve code blocks
65
+ const codeBlockPlaceholders: string[] = [];
66
+ cleaned = cleaned.replace(/<pre\b[^>]*>(?:<code\b([^>]*)>)?([\s\S]*?)(?:<\/code>)?<\/pre>/gi, (_, codeAttrs, codeBody) => {
67
+ let lang = '';
68
+ if (codeAttrs) {
69
+ const langMatch = codeAttrs.match(/\b(?:class|data-language)=["'][^"']*\b(?:language-|lang-)?([a-zA-Z0-9_-]+)\b[^"']*["']/i);
70
+ if (langMatch && !['hljs', 'code'].includes(langMatch[1].toLowerCase())) {
71
+ lang = langMatch[1].toLowerCase();
72
+ }
73
+ }
74
+
75
+ const rawCode = unescapeHtml(codeBody.replace(/<[^>]+>/g, '')).trim();
76
+ if (!rawCode) return '';
77
+
78
+ const exampleId = `code_${codeExamples.length + 1}`;
79
+ codeExamples.push({
80
+ id: exampleId,
81
+ language: lang || 'text',
82
+ code: rawCode,
83
+ });
84
+
85
+ const placeholder = `__DOCORBIT_CODE_BLOCK_${codeBlockPlaceholders.length}__`;
86
+ codeBlockPlaceholders.push(`\n\`\`\`${lang}\n${rawCode}\n\`\`\`\n`);
87
+ return placeholder;
88
+ });
89
+
90
+ // 4. Extract Headings and build anchors
91
+ cleaned = cleaned.replace(/<h([1-6])\b([^>]*)>([\s\S]*?)<\/h\1>/gi, (_, levelStr, attrs, innerText) => {
92
+ const level = parseInt(levelStr, 10);
93
+ const plainText = unescapeHtml(innerText.replace(/<[^>]+>/g, '')).replace(/\s+/g, ' ').trim();
94
+ if (!plainText) return '';
95
+
96
+ let anchor = '';
97
+ const idMatch = attrs.match(/\bid=["']([^"']+)["']/i);
98
+ if (idMatch) {
99
+ anchor = idMatch[1];
100
+ } else {
101
+ anchor = slugify(plainText);
102
+ }
103
+
104
+ headings.push({ level, text: plainText, anchor });
105
+ const hashes = '#'.repeat(level);
106
+ return `\n\n${hashes} ${plainText}\n\n`;
107
+ });
108
+
109
+ // Prefer first H1 as title, otherwise fallback to HTML title, then first heading
110
+ const h1 = headings.find(h => h.level === 1);
111
+ if (h1) {
112
+ title = h1.text;
113
+ } else if (!title && headings.length > 0) {
114
+ title = headings[0].text;
115
+ }
116
+ if (!title) {
117
+ title = 'Documentation Page';
118
+ }
119
+
120
+ // 5. Extract and normalize Links
121
+ cleaned = cleaned.replace(/<a\b[^>]*\bhref=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi, (_, href, linkText) => {
122
+ const text = unescapeHtml(linkText.replace(/<[^>]+>/g, '')).replace(/\s+/g, ' ').trim();
123
+ if (!text || href.startsWith('#') || href.startsWith('javascript:')) return text;
124
+
125
+ let absoluteUrl = href;
126
+ let isExternal = false;
127
+ try {
128
+ const resolved = new URL(href, baseUrl);
129
+ absoluteUrl = resolved.href;
130
+ const baseHost = new URL(baseUrl).hostname;
131
+ isExternal = resolved.hostname !== baseHost;
132
+ } catch {
133
+ // Keep raw href
134
+ }
135
+
136
+ links.push({
137
+ text,
138
+ url: absoluteUrl,
139
+ isExternal,
140
+ });
141
+
142
+ return `[${text}](${absoluteUrl})`;
143
+ });
144
+
145
+ // 6. Handle Callouts & Blockquotes
146
+ cleaned = cleaned.replace(/<blockquote\b[^>]*>([\s\S]*?)<\/blockquote>/gi, (_, inner) => {
147
+ const blockText = inner
148
+ .replace(/<p\b[^>]*>/gi, '')
149
+ .replace(/<\/p>/gi, '\n')
150
+ .replace(/<br\s*\/?>/gi, '\n')
151
+ .replace(/<[^>]+>/g, '');
152
+ const cleanLines = unescapeHtml(blockText)
153
+ .split('\n')
154
+ .map(l => l.trim())
155
+ .filter(Boolean)
156
+ .map(l => `> ${l}`)
157
+ .join('\n');
158
+ return `\n\n${cleanLines}\n\n`;
159
+ });
160
+
161
+ cleaned = cleaned.replace(/<div\b[^>]*\bclass=["'][^"']*\b(?:alert|callout|admonition|tip|warning|note|info)\b[^"']*["'][^>]*>([\s\S]*?)<\/div>/gi, (_, inner) => {
162
+ const alertText = unescapeHtml(inner.replace(/<[^>]+>/g, ' ')).replace(/\s+/g, ' ').trim();
163
+ if (!alertText) return '';
164
+ return `\n\n> [!NOTE]\n> ${alertText}\n\n`;
165
+ });
166
+
167
+ // 7. Handle Tables
168
+ cleaned = cleaned.replace(/<table\b[^>]*>([\s\S]*?)<\/table>/gi, (_, tableContent) => {
169
+ const rows: string[][] = [];
170
+ const rowMatches = tableContent.matchAll(/<tr\b[^>]*>([\s\S]*?)<\/tr>/gi);
171
+
172
+ for (const rMatch of rowMatches) {
173
+ const rowContent = rMatch[1];
174
+ const cells: string[] = [];
175
+ const cellMatches = rowContent.matchAll(/<t[hd]\b[^>]*>([\s\S]*?)<\/t[hd]>/gi);
176
+ for (const cMatch of cellMatches) {
177
+ const cellText = unescapeHtml(cMatch[1].replace(/<[^>]+>/g, '')).replace(/\s+/g, ' ').trim();
178
+ cells.push(cellText);
179
+ }
180
+ if (cells.length > 0) {
181
+ rows.push(cells);
182
+ }
183
+ }
184
+
185
+ if (rows.length === 0) return '';
186
+
187
+ const colCount = Math.max(...rows.map(r => r.length));
188
+ const headerRow = rows[0].map(c => c || '-');
189
+ while (headerRow.length < colCount) headerRow.push('-');
190
+
191
+ const delimiterRow = Array(colCount).fill('---');
192
+ const tableLines = [
193
+ `| ${headerRow.join(' | ')} |`,
194
+ `| ${delimiterRow.join(' | ')} |`,
195
+ ];
196
+
197
+ for (let i = 1; i < rows.length; i++) {
198
+ const dataRow = rows[i].map(c => c || '');
199
+ while (dataRow.length < colCount) dataRow.push('');
200
+ tableLines.push(`| ${dataRow.join(' | ')} |`);
201
+ }
202
+
203
+ return `\n\n${tableLines.join('\n')}\n\n`;
204
+ });
205
+
206
+ // 8. Handle Lists
207
+ cleaned = cleaned
208
+ .replace(/<li\b[^>]*>([\s\S]*?)<\/li>/gi, (_, inner) => {
209
+ const text = unescapeHtml(inner.replace(/<[^>]+>/g, '')).replace(/\s+/g, ' ').trim();
210
+ return `\n- ${text}`;
211
+ })
212
+ .replace(/<\/?(?:ul|ol)\b[^>]*>/gi, '\n');
213
+
214
+ // 9. Handle Inline elements
215
+ cleaned = cleaned
216
+ .replace(/<code\b[^>]*>([\s\S]*?)<\/code>/gi, (_, inner) => {
217
+ const text = unescapeHtml(inner.replace(/<[^>]+>/g, '')).trim();
218
+ return text ? `\`${text}\`` : '';
219
+ })
220
+ .replace(/<(?:strong|b)\b[^>]*>([\s\S]*?)<\/(?:strong|b)>/gi, (_, inner) => {
221
+ const text = inner.trim();
222
+ return text ? `**${text}**` : '';
223
+ })
224
+ .replace(/<(?:em|i)\b[^>]*>([\s\S]*?)<\/(?:em|i)>/gi, (_, inner) => {
225
+ const text = inner.trim();
226
+ return text ? `*${text}*` : '';
227
+ })
228
+ .replace(/<br\s*\/?>/gi, '\n')
229
+ .replace(/<\/?(?:p|div|section|article|main)\b[^>]*>/gi, '\n\n');
230
+
231
+ // 10. Strip all remaining HTML tags
232
+ cleaned = cleaned.replace(/<[^>]+>/g, '');
233
+
234
+ // 11. Restore preserved code blocks
235
+ for (let i = 0; i < codeBlockPlaceholders.length; i++) {
236
+ cleaned = cleaned.replace(`__DOCORBIT_CODE_BLOCK_${i}__`, codeBlockPlaceholders[i]);
237
+ }
238
+
239
+ // 12. Normalize whitespace
240
+ cleaned = unescapeHtml(cleaned)
241
+ .replace(/[ \t]+/g, ' ')
242
+ .replace(/\n\s+\n/g, '\n\n')
243
+ .replace(/\n{3,}/g, '\n\n')
244
+ .trim();
245
+
246
+ return {
247
+ title,
248
+ markdown: cleaned,
249
+ headings,
250
+ links,
251
+ codeExamples,
252
+ };
253
+ }
@@ -0,0 +1,8 @@
1
+ export * from './html.ts';
2
+ export * from './llms.ts';
3
+ export * from './openapi.ts';
4
+ export * from './page.ts';
5
+ export * from './slicer.ts';
6
+ export * from './example-indexer.ts';
7
+ export * from './pitfall-extractor.ts';
8
+
@@ -0,0 +1,83 @@
1
+ import type { LlmsDocument, LlmsSection, LlmsLink } from '../../shared/src/index.ts';
2
+
3
+ export function parseLlmsTxt(content: string, baseUrl: string): LlmsDocument {
4
+ const lines = content.split('\n');
5
+ let title: string | undefined;
6
+ let summary: string | undefined;
7
+ const sections: LlmsSection[] = [];
8
+ let currentSection: LlmsSection = { name: 'Overview', links: [] };
9
+
10
+ const summaryLines: string[] = [];
11
+
12
+ for (let i = 0; i < lines.length; i++) {
13
+ const rawLine = lines[i];
14
+ const line = rawLine.trim();
15
+
16
+ if (!line) continue;
17
+
18
+ if (line.startsWith('# ') && !title) {
19
+ title = line.slice(2).trim();
20
+ continue;
21
+ }
22
+
23
+ if (line.startsWith('>')) {
24
+ const text = line.replace(/^>\s*/, '').trim();
25
+ if (text) summaryLines.push(text);
26
+ continue;
27
+ }
28
+
29
+ if (line.startsWith('## ')) {
30
+ if (currentSection.links.length > 0 || currentSection.name !== 'Overview') {
31
+ sections.push(currentSection);
32
+ }
33
+ currentSection = {
34
+ name: line.slice(3).trim(),
35
+ links: [],
36
+ };
37
+ continue;
38
+ }
39
+
40
+ const linkMatch = line.match(/^[-*]\s+\[([^\]]+)\]\(([^)]+)\)(?::\s*(.*))?$/);
41
+ if (linkMatch) {
42
+ const linkTitle = linkMatch[1].trim();
43
+ let rawHref = linkMatch[2].trim();
44
+ const description = linkMatch[3] ? linkMatch[3].trim() : undefined;
45
+
46
+ try {
47
+ rawHref = new URL(rawHref, baseUrl).href;
48
+ } catch {
49
+ // Keep raw
50
+ }
51
+
52
+ const linkItem: LlmsLink = {
53
+ title: linkTitle,
54
+ url: rawHref,
55
+ ...(description ? { description } : {}),
56
+ };
57
+
58
+ currentSection.links.push(linkItem);
59
+ }
60
+ }
61
+
62
+ if (currentSection.links.length > 0 || sections.length === 0) {
63
+ sections.push(currentSection);
64
+ }
65
+
66
+ if (summaryLines.length > 0) {
67
+ summary = summaryLines.join(' ');
68
+ }
69
+
70
+ return {
71
+ title,
72
+ summary,
73
+ sections,
74
+ rawContent: content,
75
+ };
76
+ }
77
+
78
+ export function isValidLlmsTxt(content: string): boolean {
79
+ if (!content || typeof content !== 'string') return false;
80
+ const hasH1 = /^#\s+.+/m.test(content);
81
+ const hasLink = /^[-*]\s+\[.+\]\(.+\)/m.test(content);
82
+ return hasH1 && hasLink;
83
+ }