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.
- package/LICENSE +21 -0
- package/README.md +660 -0
- package/apps/cli/bin/docorbit.js +8 -0
- package/apps/cli/src/commands/add.ts +44 -0
- package/apps/cli/src/commands/api.ts +38 -0
- package/apps/cli/src/commands/context.ts +47 -0
- package/apps/cli/src/commands/dashboard.ts +55 -0
- package/apps/cli/src/commands/diff.ts +30 -0
- package/apps/cli/src/commands/evaluate.ts +133 -0
- package/apps/cli/src/commands/examples.ts +39 -0
- package/apps/cli/src/commands/export.ts +89 -0
- package/apps/cli/src/commands/impact.ts +31 -0
- package/apps/cli/src/commands/init.ts +69 -0
- package/apps/cli/src/commands/inspect.ts +30 -0
- package/apps/cli/src/commands/mcp.ts +72 -0
- package/apps/cli/src/commands/pitfalls.ts +38 -0
- package/apps/cli/src/commands/recipes.ts +35 -0
- package/apps/cli/src/commands/search.ts +48 -0
- package/apps/cli/src/commands/update.ts +73 -0
- package/apps/cli/src/commands/verify.ts +48 -0
- package/apps/cli/src/formatters/colors.ts +23 -0
- package/apps/cli/src/formatters/inspection.ts +102 -0
- package/apps/cli/src/formatters/knowledge.ts +272 -0
- package/apps/cli/src/formatters/retrieval.ts +74 -0
- package/apps/cli/src/formatters/terminal.ts +6 -0
- package/apps/cli/src/formatters/verification.ts +126 -0
- package/apps/cli/src/index.ts +409 -0
- package/bin/docorbit.js +8 -0
- package/package.json +46 -0
- package/packages/core/src/dashboard/server.ts +314 -0
- package/packages/core/src/dashboard/ui.ts +586 -0
- package/packages/core/src/implementation-service.ts +451 -0
- package/packages/core/src/index.ts +7 -0
- package/packages/core/src/inspector.ts +71 -0
- package/packages/core/src/pipeline.ts +331 -0
- package/packages/crawler/src/config.ts +12 -0
- package/packages/crawler/src/fetcher.ts +185 -0
- package/packages/crawler/src/index.ts +2 -0
- package/packages/discovery/src/index.ts +31 -0
- package/packages/discovery/src/provider.ts +47 -0
- package/packages/discovery/src/providers/generic.ts +98 -0
- package/packages/discovery/src/providers/github.ts +61 -0
- package/packages/discovery/src/providers/llms-txt.ts +73 -0
- package/packages/discovery/src/providers/markdown.ts +48 -0
- package/packages/discovery/src/providers/openapi.ts +91 -0
- package/packages/discovery/src/providers/sitemap.ts +62 -0
- package/packages/discovery/src/providers/skill.ts +54 -0
- package/packages/discovery/src/ranker.ts +123 -0
- package/packages/evaluation/src/dataset.ts +963 -0
- package/packages/evaluation/src/index.ts +8 -0
- package/packages/evaluation/src/runner.ts +241 -0
- package/packages/evaluation/src/strategies/context7-runner.ts +269 -0
- package/packages/evaluation/src/strategies/docorbit-runner.ts +228 -0
- package/packages/evaluation/src/strategies/firecrawl-runner.ts +172 -0
- package/packages/evaluation/src/strategies/web-search-runner.ts +194 -0
- package/packages/evaluation/src/types.ts +34 -0
- package/packages/evaluation/src/version-matcher.ts +73 -0
- package/packages/export/src/agents-md.ts +200 -0
- package/packages/export/src/claude-md.ts +141 -0
- package/packages/export/src/docs-map.ts +150 -0
- package/packages/export/src/index.ts +6 -0
- package/packages/export/src/llms-txt.ts +96 -0
- package/packages/export/src/service.ts +250 -0
- package/packages/export/src/skill-md.ts +128 -0
- package/packages/mcp/src/index.ts +46 -0
- package/packages/mcp/src/resources/index.ts +189 -0
- package/packages/mcp/src/server.ts +278 -0
- package/packages/mcp/src/tools/analyze-impact.ts +74 -0
- package/packages/mcp/src/tools/check-api.ts +86 -0
- package/packages/mcp/src/tools/diff-docs.ts +68 -0
- package/packages/mcp/src/tools/export-context.ts +73 -0
- package/packages/mcp/src/tools/find-api.ts +99 -0
- package/packages/mcp/src/tools/find-example.ts +100 -0
- package/packages/mcp/src/tools/find-pitfall.ts +94 -0
- package/packages/mcp/src/tools/find-recipe.ts +98 -0
- package/packages/mcp/src/tools/get-doc.ts +130 -0
- package/packages/mcp/src/tools/get-docs-map.ts +64 -0
- package/packages/mcp/src/tools/get-version.ts +118 -0
- package/packages/mcp/src/tools/implementation-context.ts +88 -0
- package/packages/mcp/src/tools/index.ts +59 -0
- package/packages/mcp/src/tools/list-sources.ts +85 -0
- package/packages/mcp/src/tools/search-docs.ts +123 -0
- package/packages/mcp/src/tools/types.ts +28 -0
- package/packages/mcp/src/transports/http.ts +256 -0
- package/packages/mcp/src/transports/stdio.ts +105 -0
- package/packages/mcp/src/transports/types.ts +6 -0
- package/packages/mcp/src/types.ts +102 -0
- package/packages/normalizer/src/example-indexer.ts +240 -0
- package/packages/normalizer/src/html.ts +253 -0
- package/packages/normalizer/src/index.ts +8 -0
- package/packages/normalizer/src/llms.ts +83 -0
- package/packages/normalizer/src/openapi/endpoint-parser.ts +406 -0
- package/packages/normalizer/src/openapi/schema-resolver.ts +111 -0
- package/packages/normalizer/src/openapi.ts +2 -0
- package/packages/normalizer/src/page.ts +184 -0
- package/packages/normalizer/src/pitfall-extractor.ts +190 -0
- package/packages/normalizer/src/slicer.ts +455 -0
- package/packages/retrieval/src/engine.ts +120 -0
- package/packages/retrieval/src/index.ts +7 -0
- package/packages/retrieval/src/intent.ts +43 -0
- package/packages/retrieval/src/packer.ts +145 -0
- package/packages/retrieval/src/recipe-engine.ts +313 -0
- package/packages/retrieval/src/scorer.ts +139 -0
- package/packages/retrieval/src/weights.ts +31 -0
- package/packages/security/src/annotations.ts +112 -0
- package/packages/security/src/index.ts +2 -0
- package/packages/security/src/ssrf.ts +153 -0
- package/packages/shared/src/errors.ts +53 -0
- package/packages/shared/src/hashing.ts +23 -0
- package/packages/shared/src/index.ts +3 -0
- package/packages/shared/src/types.ts +881 -0
- package/packages/storage/src/db.ts +72 -0
- package/packages/storage/src/index.ts +11 -0
- package/packages/storage/src/interfaces.ts +115 -0
- package/packages/storage/src/repositories/api-repository.ts +219 -0
- package/packages/storage/src/repositories/chunk-repository.ts +316 -0
- package/packages/storage/src/repositories/example-repository.ts +206 -0
- package/packages/storage/src/repositories/page-repository.ts +205 -0
- package/packages/storage/src/repositories/pitfall-repository.ts +188 -0
- package/packages/storage/src/repositories/source-repository.ts +205 -0
- package/packages/storage/src/repository.ts +256 -0
- package/packages/storage/src/schema.ts +269 -0
- package/packages/storage/src/search-tokens.ts +28 -0
- package/packages/verification/src/diff-engine.ts +258 -0
- package/packages/verification/src/extractor.ts +339 -0
- package/packages/verification/src/impact-scanner.ts +203 -0
- package/packages/verification/src/index.ts +5 -0
- package/packages/verification/src/services.ts +238 -0
- package/packages/verification/src/verifier.ts +375 -0
- package/packages/workspace/src/detector.ts +143 -0
- package/packages/workspace/src/ecosystems/cargo.ts +84 -0
- package/packages/workspace/src/ecosystems/composer.ts +42 -0
- package/packages/workspace/src/ecosystems/go.ts +54 -0
- package/packages/workspace/src/ecosystems/index.ts +34 -0
- package/packages/workspace/src/ecosystems/maven.ts +34 -0
- package/packages/workspace/src/ecosystems/npm.ts +83 -0
- package/packages/workspace/src/ecosystems/pub.ts +40 -0
- package/packages/workspace/src/ecosystems/pypi.ts +100 -0
- package/packages/workspace/src/ecosystems/rubygems.ts +30 -0
- package/packages/workspace/src/ecosystems/types.ts +18 -0
- package/packages/workspace/src/index.ts +5 -0
- package/packages/workspace/src/lockfile.ts +194 -0
- package/packages/workspace/src/resolver.ts +234 -0
- 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
|
+
}
|