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,98 @@
1
+ import type { DiscoveredSource } from '../../../shared/src/index.ts';
2
+ import { SecureFetcher } from '../../../crawler/src/index.ts';
3
+ import type { DiscoveryProvider } from '../provider.ts';
4
+
5
+ export class GenericWebProvider implements DiscoveryProvider {
6
+ name = 'generic_web';
7
+
8
+ async discover(targetUrl: string, fetcher: SecureFetcher): Promise<DiscoveredSource[]> {
9
+ const results: DiscoveredSource[] = [];
10
+
11
+ try {
12
+ const res = await fetcher.fetch(targetUrl, {
13
+ timeoutMs: 5000,
14
+ });
15
+
16
+ if (res.status >= 200 && res.status < 300) {
17
+ let framework = 'unknown';
18
+ const bodyLower = res.body.toLowerCase();
19
+
20
+ // Check generator meta tags or distinctive DOM anchors first
21
+ const metaGenerator = res.body.match(/<meta\b[^>]*\bname=["']generator["'][^>]*\bcontent=["']([^"']+)["']/i);
22
+ const generatorVal = metaGenerator ? metaGenerator[1].toLowerCase() : '';
23
+
24
+ if (generatorVal.includes('docusaurus') || res.body.includes('__docusaurus') || res.body.includes('docusaurus-plugin')) {
25
+ framework = 'docusaurus';
26
+ } else if (generatorVal.includes('mintlify') || res.body.includes('_mintlify') || res.body.includes('mintlify.css') || res.body.includes('mintlify.com')) {
27
+ framework = 'mintlify';
28
+ } else if (generatorVal.includes('nextra') || res.body.includes('__nextra')) {
29
+ framework = 'nextra';
30
+ } else if (generatorVal.includes('gitbook') || res.body.includes('gitbook-root') || res.body.includes('data-gitbook')) {
31
+ framework = 'gitbook';
32
+ } else if (generatorVal.includes('mkdocs') || res.body.includes('mkdocs')) {
33
+ framework = 'mkdocs';
34
+ } else if (generatorVal.includes('sphinx') || res.body.includes('sphinxsidebar')) {
35
+ framework = 'sphinx';
36
+ }
37
+
38
+ // Section 18: Detect candidate doc sections without blindly crawling
39
+ const candidateSections: string[] = [];
40
+ const sectionRegex = /href=["']([^"'#?]+(?:\/docs|\/api|\/reference|\/guides|\/tutorials|\/changelog)[^"'#?]*)["']/gi;
41
+ let sMatch: RegExpExecArray | null;
42
+ while ((sMatch = sectionRegex.exec(res.body)) !== null) {
43
+ try {
44
+ const secUrl = new URL(sMatch[1], targetUrl).href;
45
+ if (new URL(secUrl).origin === new URL(targetUrl).origin && !candidateSections.includes(secUrl)) {
46
+ candidateSections.push(secUrl);
47
+ if (candidateSections.length >= 10) break;
48
+ }
49
+ } catch {
50
+ // Ignore
51
+ }
52
+ }
53
+
54
+ const linkHeader = res.headers['link'];
55
+ if (linkHeader) {
56
+ const llmsMatch = linkHeader.match(/<([^>]+)>;\s*rel=["']?(?:llms-txt|alternate)["']?/i);
57
+ if (llmsMatch) {
58
+ try {
59
+ const fullLlmsUrl = new URL(llmsMatch[1], targetUrl).href;
60
+ results.push({
61
+ url: fullLlmsUrl,
62
+ type: 'llms_txt',
63
+ discoveredBy: `${this.name}:link-header`,
64
+ status: 'valid',
65
+ confidence: 0.99,
66
+ authority: 'official',
67
+ machineReadable: true,
68
+ });
69
+ } catch {
70
+ // Ignore
71
+ }
72
+ }
73
+ }
74
+
75
+ results.push({
76
+ url: res.finalUrl || targetUrl,
77
+ type: 'web',
78
+ discoveredBy: this.name,
79
+ status: 'valid',
80
+ contentType: res.contentType,
81
+ confidence: 0.85,
82
+ authority: 'official',
83
+ machineReadable: false,
84
+ metadata: {
85
+ framework,
86
+ status: res.status,
87
+ bytes: res.bytesRead,
88
+ candidateSections,
89
+ },
90
+ });
91
+ }
92
+ } catch {
93
+ // Continue
94
+ }
95
+
96
+ return results;
97
+ }
98
+ }
@@ -0,0 +1,61 @@
1
+ import type { DiscoveredSource } from '../../../shared/src/index.ts';
2
+ import { SecureFetcher } from '../../../crawler/src/index.ts';
3
+ import type { DiscoveryProvider } from '../provider.ts';
4
+
5
+ export class GithubProvider implements DiscoveryProvider {
6
+ name = 'github';
7
+
8
+ async discover(targetUrl: string, fetcher: SecureFetcher): Promise<DiscoveredSource[]> {
9
+ const results: DiscoveredSource[] = [];
10
+
11
+ const isDirectGithub = /^https?:\/\/(?:www\.)?github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+/i.test(targetUrl);
12
+ if (isDirectGithub) {
13
+ results.push({
14
+ url: targetUrl,
15
+ type: 'github',
16
+ discoveredBy: this.name,
17
+ status: 'valid',
18
+ confidence: 0.99,
19
+ authority: 'official',
20
+ machineReadable: false,
21
+ });
22
+ return results;
23
+ }
24
+
25
+ try {
26
+ const res = await fetcher.fetch(targetUrl, {
27
+ timeoutMs: 4000,
28
+ });
29
+
30
+ if (res.status >= 200 && res.status < 300 && res.body) {
31
+ const githubMatches = res.body.matchAll(/href=["'](https?:\/\/(?:www\.)?github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+(?:\/)?)(?:["']|[/?#])/gi);
32
+ const seenRepos = new Set<string>();
33
+
34
+ for (const m of githubMatches) {
35
+ const repoUrl = m[1].replace(/\/$/, '');
36
+ if (repoUrl.includes('/topics') || repoUrl.includes('/features') || repoUrl.includes('/pricing')) continue;
37
+
38
+ if (!seenRepos.has(repoUrl)) {
39
+ seenRepos.add(repoUrl);
40
+ results.push({
41
+ url: repoUrl,
42
+ type: 'github',
43
+ discoveredBy: this.name,
44
+ status: 'valid',
45
+ confidence: 0.85,
46
+ authority: 'official',
47
+ machineReadable: false,
48
+ metadata: {
49
+ foundOn: targetUrl,
50
+ },
51
+ });
52
+ }
53
+ }
54
+ }
55
+ } catch {
56
+ // Continue
57
+ }
58
+
59
+ return results;
60
+ }
61
+ }
@@ -0,0 +1,73 @@
1
+ import type { DiscoveredSource } from '../../../shared/src/index.ts';
2
+ import { SecureFetcher } from '../../../crawler/src/index.ts';
3
+ import { isValidLlmsTxt } from '../../../normalizer/src/index.ts';
4
+ import type { DiscoveryProvider } from '../provider.ts';
5
+
6
+ export class LlmsTxtProvider implements DiscoveryProvider {
7
+ name = 'llms_txt';
8
+
9
+ async discover(targetUrl: string, fetcher: SecureFetcher): Promise<DiscoveredSource[]> {
10
+ const results: DiscoveredSource[] = [];
11
+ let baseUrl: URL;
12
+ try {
13
+ baseUrl = new URL(targetUrl);
14
+ } catch {
15
+ return results;
16
+ }
17
+
18
+ const cleanPath = baseUrl.pathname.replace(/\/$/, '');
19
+
20
+ // Probe both subpath-relative and origin-root paths
21
+ const candidatePaths: Array<{ path: string; type: 'llms_txt' | 'llms_full_txt' }> = [
22
+ { path: '/llms.txt', type: 'llms_txt' },
23
+ { path: '/llms-full.txt', type: 'llms_full_txt' },
24
+ { path: '/.well-known/llms.txt', type: 'llms_txt' },
25
+ ];
26
+
27
+ if (cleanPath && cleanPath !== '') {
28
+ candidatePaths.unshift(
29
+ { path: `${cleanPath}/llms.txt`, type: 'llms_txt' },
30
+ { path: `${cleanPath}/llms-full.txt`, type: 'llms_full_txt' }
31
+ );
32
+ }
33
+
34
+ const seenUrls = new Set<string>();
35
+
36
+ for (const item of candidatePaths) {
37
+ const probeUrl = new URL(item.path, baseUrl.origin).href;
38
+ if (seenUrls.has(probeUrl)) continue;
39
+ seenUrls.add(probeUrl);
40
+
41
+ try {
42
+ const res = await fetcher.fetch(probeUrl, {
43
+ timeoutMs: 4000,
44
+ });
45
+
46
+ if (res.status >= 200 && res.status < 300) {
47
+ const isValid = isValidLlmsTxt(res.body);
48
+ const isNotHtml = !res.contentType.includes('text/html') && !res.body.includes('<html');
49
+ if (isValid || (item.type === 'llms_full_txt' && isNotHtml)) {
50
+ results.push({
51
+ url: probeUrl,
52
+ type: item.type,
53
+ discoveredBy: this.name,
54
+ status: 'valid',
55
+ contentType: res.contentType,
56
+ confidence: 0.98,
57
+ authority: 'official',
58
+ machineReadable: true,
59
+ metadata: {
60
+ bytes: res.bytesRead,
61
+ path: item.path,
62
+ },
63
+ });
64
+ }
65
+ }
66
+ } catch {
67
+ // Continue
68
+ }
69
+ }
70
+
71
+ return results;
72
+ }
73
+ }
@@ -0,0 +1,48 @@
1
+ import type { DiscoveredSource } from '../../../shared/src/index.ts';
2
+ import { SecureFetcher } from '../../../crawler/src/index.ts';
3
+ import type { DiscoveryProvider } from '../provider.ts';
4
+
5
+ export class MarkdownProvider implements DiscoveryProvider {
6
+ name = 'markdown';
7
+
8
+ async discover(targetUrl: string, fetcher: SecureFetcher): Promise<DiscoveredSource[]> {
9
+ const results: DiscoveredSource[] = [];
10
+
11
+ const mdProbeUrl = targetUrl.endsWith('.md')
12
+ ? targetUrl
13
+ : targetUrl.replace(/\/$/, '') + '.md';
14
+
15
+ try {
16
+ const res = await fetcher.fetch(mdProbeUrl, {
17
+ timeoutMs: 4000,
18
+ });
19
+
20
+ if (res.status >= 200 && res.status < 300) {
21
+ const isMarkdown =
22
+ res.contentType.includes('text/markdown') ||
23
+ res.contentType.includes('text/plain') ||
24
+ res.body.startsWith('#');
25
+
26
+ if (isMarkdown && !res.body.includes('<html')) {
27
+ results.push({
28
+ url: mdProbeUrl,
29
+ type: 'markdown',
30
+ discoveredBy: this.name,
31
+ status: 'valid',
32
+ contentType: res.contentType,
33
+ confidence: 0.95,
34
+ authority: 'official',
35
+ machineReadable: true,
36
+ metadata: {
37
+ bytes: res.bytesRead,
38
+ },
39
+ });
40
+ }
41
+ }
42
+ } catch {
43
+ // Continue
44
+ }
45
+
46
+ return results;
47
+ }
48
+ }
@@ -0,0 +1,91 @@
1
+ import type { DiscoveredSource } from '../../../shared/src/index.ts';
2
+ import { SecureFetcher } from '../../../crawler/src/index.ts';
3
+ import { detectOpenApiSpec } from '../../../normalizer/src/index.ts';
4
+ import type { DiscoveryProvider } from '../provider.ts';
5
+
6
+ export class OpenApiProvider implements DiscoveryProvider {
7
+ name = 'openapi';
8
+
9
+ async discover(targetUrl: string, fetcher: SecureFetcher): Promise<DiscoveredSource[]> {
10
+ const results: DiscoveredSource[] = [];
11
+ let baseUrl: URL;
12
+ try {
13
+ baseUrl = new URL(targetUrl);
14
+ } catch {
15
+ return results;
16
+ }
17
+
18
+ const cleanPath = baseUrl.pathname.replace(/\/$/, '');
19
+
20
+ const candidatePaths = [
21
+ '/openapi.json',
22
+ '/openapi.yaml',
23
+ '/swagger.json',
24
+ '/v3/api-docs',
25
+ '/api-docs',
26
+ ];
27
+
28
+ if (cleanPath && cleanPath !== '') {
29
+ candidatePaths.unshift(
30
+ `${cleanPath}/openapi.json`,
31
+ `${cleanPath}/openapi.yaml`,
32
+ `${cleanPath}/swagger.json`
33
+ );
34
+ }
35
+
36
+ // Also probe targetUrl HTML for <link rel="service-doc">
37
+ try {
38
+ const pageRes = await fetcher.fetch(targetUrl, { timeoutMs: 4000 });
39
+ if (pageRes.status >= 200 && pageRes.status < 300 && pageRes.body) {
40
+ const linkMatch = pageRes.body.match(/<link\b[^>]*\brel=["'](?:service-doc|openapi|swagger)["'][^>]*\bhref=["']([^"']+)["']/i);
41
+ if (linkMatch) {
42
+ const docHref = new URL(linkMatch[1], targetUrl).href;
43
+ candidatePaths.unshift(new URL(docHref).pathname);
44
+ }
45
+ }
46
+ } catch {
47
+ // Continue
48
+ }
49
+
50
+ const seenUrls = new Set<string>();
51
+
52
+ for (const path of candidatePaths) {
53
+ const probeUrl = new URL(path, baseUrl.origin).href;
54
+ if (seenUrls.has(probeUrl)) continue;
55
+ seenUrls.add(probeUrl);
56
+
57
+ try {
58
+ const res = await fetcher.fetch(probeUrl, {
59
+ timeoutMs: 4000,
60
+ });
61
+
62
+ if (res.status >= 200 && res.status < 300) {
63
+ const spec = detectOpenApiSpec(res.body, probeUrl);
64
+ if (spec) {
65
+ results.push({
66
+ url: probeUrl,
67
+ type: 'openapi',
68
+ discoveredBy: this.name,
69
+ status: 'valid',
70
+ contentType: res.contentType,
71
+ confidence: 0.99,
72
+ authority: 'official',
73
+ machineReadable: true,
74
+ metadata: {
75
+ specVersion: spec.specVersion,
76
+ title: spec.title,
77
+ pathCount: spec.pathCount,
78
+ servers: spec.servers,
79
+ },
80
+ });
81
+ break;
82
+ }
83
+ }
84
+ } catch {
85
+ // Continue
86
+ }
87
+ }
88
+
89
+ return results;
90
+ }
91
+ }
@@ -0,0 +1,62 @@
1
+ import type { DiscoveredSource } from '../../../shared/src/index.ts';
2
+ import { SecureFetcher } from '../../../crawler/src/index.ts';
3
+ import type { DiscoveryProvider } from '../provider.ts';
4
+
5
+ export class SitemapProvider implements DiscoveryProvider {
6
+ name = 'sitemap';
7
+
8
+ async discover(targetUrl: string, fetcher: SecureFetcher): Promise<DiscoveredSource[]> {
9
+ const results: DiscoveredSource[] = [];
10
+ let baseUrl: URL;
11
+ try {
12
+ baseUrl = new URL(targetUrl);
13
+ } catch {
14
+ return results;
15
+ }
16
+
17
+ const cleanPath = baseUrl.pathname.replace(/\/$/, '');
18
+
19
+ const candidatePaths = ['/sitemap.xml', '/sitemap_index.xml'];
20
+ if (cleanPath && cleanPath !== '') {
21
+ candidatePaths.unshift(`${cleanPath}/sitemap.xml`);
22
+ }
23
+
24
+ const seenUrls = new Set<string>();
25
+
26
+ for (const path of candidatePaths) {
27
+ const probeUrl = new URL(path, baseUrl.origin).href;
28
+ if (seenUrls.has(probeUrl)) continue;
29
+ seenUrls.add(probeUrl);
30
+
31
+ try {
32
+ const res = await fetcher.fetch(probeUrl, {
33
+ timeoutMs: 4000,
34
+ });
35
+
36
+ if (res.status >= 200 && res.status < 300) {
37
+ const isXml = res.contentType.includes('xml') || res.body.includes('<urlset') || res.body.includes('<sitemapindex');
38
+ if (isXml) {
39
+ results.push({
40
+ url: probeUrl,
41
+ type: 'sitemap',
42
+ discoveredBy: this.name,
43
+ status: 'valid',
44
+ contentType: res.contentType,
45
+ confidence: 0.9,
46
+ authority: 'official',
47
+ machineReadable: true,
48
+ metadata: {
49
+ bytes: res.bytesRead,
50
+ },
51
+ });
52
+ break;
53
+ }
54
+ }
55
+ } catch {
56
+ // Continue
57
+ }
58
+ }
59
+
60
+ return results;
61
+ }
62
+ }
@@ -0,0 +1,54 @@
1
+ import type { DiscoveredSource } from '../../../shared/src/index.ts';
2
+ import { SecureFetcher } from '../../../crawler/src/index.ts';
3
+ import type { DiscoveryProvider } from '../provider.ts';
4
+
5
+ export class SkillProvider implements DiscoveryProvider {
6
+ name = 'skill';
7
+
8
+ async discover(targetUrl: string, fetcher: SecureFetcher): Promise<DiscoveredSource[]> {
9
+ const results: DiscoveredSource[] = [];
10
+ let baseUrl: URL;
11
+ try {
12
+ baseUrl = new URL(targetUrl);
13
+ } catch {
14
+ return results;
15
+ }
16
+
17
+ const candidatePaths = ['/skill.md', '/.well-known/skills/skill.md'];
18
+
19
+ for (const path of candidatePaths) {
20
+ const probeUrl = new URL(path, baseUrl.origin).href;
21
+ try {
22
+ const res = await fetcher.fetch(probeUrl, {
23
+ timeoutMs: 4000,
24
+ });
25
+
26
+ if (res.status >= 200 && res.status < 300) {
27
+ const hasYamlHeader = /^---\s*\n([\s\S]*?)\n---/m.test(res.body);
28
+ const hasName = /\bname:\s*.+/i.test(res.body);
29
+
30
+ if (hasYamlHeader && hasName) {
31
+ results.push({
32
+ url: probeUrl,
33
+ type: 'skill',
34
+ discoveredBy: this.name,
35
+ status: 'valid',
36
+ contentType: res.contentType,
37
+ confidence: 0.95,
38
+ authority: 'official',
39
+ machineReadable: true,
40
+ metadata: {
41
+ bytes: res.bytesRead,
42
+ },
43
+ });
44
+ break;
45
+ }
46
+ }
47
+ } catch {
48
+ // Continue
49
+ }
50
+ }
51
+
52
+ return results;
53
+ }
54
+ }
@@ -0,0 +1,123 @@
1
+ import type {
2
+ DiscoveredSource,
3
+ SourcePurpose,
4
+ SourceRankResult,
5
+ SourceType,
6
+ } from '../../shared/src/index.ts';
7
+
8
+ const PURPOSE_TYPE_WEIGHTS: Record<SourcePurpose, Record<SourceType, number>> = {
9
+ navigation: {
10
+ llms_txt: 1.0,
11
+ sitemap: 0.9,
12
+ web: 0.7,
13
+ llms_full_txt: 0.6,
14
+ markdown: 0.5,
15
+ skill: 0.45,
16
+ openapi: 0.4,
17
+ github: 0.3,
18
+ },
19
+ conceptual: {
20
+ markdown: 1.0,
21
+ llms_full_txt: 0.95,
22
+ llms_txt: 0.85,
23
+ web: 0.75,
24
+ skill: 0.6,
25
+ openapi: 0.5,
26
+ github: 0.45,
27
+ sitemap: 0.3,
28
+ },
29
+ api: {
30
+ openapi: 1.0,
31
+ markdown: 0.7,
32
+ llms_full_txt: 0.65,
33
+ web: 0.5,
34
+ llms_txt: 0.4,
35
+ github: 0.35,
36
+ skill: 0.3,
37
+ sitemap: 0.2,
38
+ },
39
+ examples: {
40
+ github: 1.0,
41
+ markdown: 0.85,
42
+ llms_full_txt: 0.8,
43
+ web: 0.6,
44
+ skill: 0.5,
45
+ llms_txt: 0.45,
46
+ openapi: 0.4,
47
+ sitemap: 0.2,
48
+ },
49
+ implementation: {
50
+ skill: 1.0,
51
+ markdown: 0.9,
52
+ llms_txt: 0.85,
53
+ llms_full_txt: 0.8,
54
+ web: 0.65,
55
+ openapi: 0.6,
56
+ github: 0.55,
57
+ sitemap: 0.2,
58
+ },
59
+ };
60
+
61
+ const AUTHORITY_WEIGHTS = {
62
+ official: 1.0,
63
+ community: 0.8,
64
+ third_party: 0.6,
65
+ };
66
+
67
+ function scoreSource(source: DiscoveredSource, purpose: SourcePurpose): number {
68
+ if (source.status === 'unreachable') return 0;
69
+ if (source.status === 'invalid') return 0.05;
70
+
71
+ const typeWeights = PURPOSE_TYPE_WEIGHTS[purpose] || PURPOSE_TYPE_WEIGHTS.conceptual;
72
+ const baseWeight = typeWeights[source.type] || 0.4;
73
+ const authorityWeight = AUTHORITY_WEIGHTS[source.authority] || 0.6;
74
+ const machineBonus = source.machineReadable ? 0.05 : 0.0;
75
+
76
+ const score = (baseWeight * authorityWeight + machineBonus) * source.confidence;
77
+ return Math.min(1.0, Math.max(0.0, score));
78
+ }
79
+
80
+ /**
81
+ * Ranks discovered sources based on the intended purpose:
82
+ * navigation, conceptual, api, examples, or implementation.
83
+ */
84
+ export function rankSources(
85
+ sources: DiscoveredSource[],
86
+ purpose: SourcePurpose = 'conceptual'
87
+ ): SourceRankResult {
88
+ if (!sources || sources.length === 0) {
89
+ return {
90
+ purpose,
91
+ recommended: null,
92
+ ranked: [],
93
+ rationale: `No sources available to rank for purpose "${purpose}".`,
94
+ };
95
+ }
96
+
97
+ const scored = sources.map(s => ({
98
+ source: s,
99
+ score: scoreSource(s, purpose),
100
+ }));
101
+
102
+ scored.sort((a, b) => {
103
+ if (b.score !== a.score) return b.score - a.score;
104
+ return a.source.url.localeCompare(b.source.url);
105
+ });
106
+
107
+ const ranked = scored.map(item => item.source);
108
+ const recommended = ranked.find(s => s.status === 'valid') || null;
109
+
110
+ let rationale = `Ranked ${ranked.length} source(s) for purpose "${purpose}".`;
111
+ if (recommended) {
112
+ rationale += ` Recommended "${recommended.url}" (${recommended.type}, authority: ${recommended.authority}, machine-readable: ${recommended.machineReadable}).`;
113
+ } else {
114
+ rationale += ' No valid candidate sources found.';
115
+ }
116
+
117
+ return {
118
+ purpose,
119
+ recommended,
120
+ ranked,
121
+ rationale,
122
+ };
123
+ }