@j0hanz/superfetch 2.0.0 → 2.0.1
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/README.md +28 -17
- package/dist/config/index.js +11 -6
- package/dist/http/auth.js +161 -2
- package/dist/http/host-allowlist.d.ts +3 -0
- package/dist/http/host-allowlist.js +117 -0
- package/dist/http/mcp-routes.d.ts +8 -2
- package/dist/http/mcp-routes.js +101 -8
- package/dist/http/mcp-session-eviction.d.ts +3 -0
- package/dist/http/mcp-session-eviction.js +24 -0
- package/dist/http/mcp-session-init.d.ts +7 -0
- package/dist/http/mcp-session-init.js +94 -0
- package/dist/http/mcp-session-slots.d.ts +17 -0
- package/dist/http/mcp-session-slots.js +55 -0
- package/dist/http/mcp-session-transport-init.d.ts +7 -0
- package/dist/http/mcp-session-transport-init.js +41 -0
- package/dist/http/mcp-session-types.d.ts +5 -0
- package/dist/http/mcp-session-types.js +1 -0
- package/dist/http/mcp-session.d.ts +9 -9
- package/dist/http/mcp-session.js +5 -114
- package/dist/http/mcp-sessions.d.ts +43 -0
- package/dist/http/mcp-sessions.js +392 -0
- package/dist/http/rate-limit.js +2 -2
- package/dist/http/server-middleware.d.ts +6 -1
- package/dist/http/server-middleware.js +3 -117
- package/dist/http/server-shutdown.js +1 -1
- package/dist/http/server.d.ts +10 -0
- package/dist/http/server.js +508 -11
- package/dist/http/session-cleanup.js +8 -5
- package/dist/middleware/error-handler.d.ts +1 -1
- package/dist/middleware/error-handler.js +31 -30
- package/dist/resources/cached-content-params.d.ts +5 -0
- package/dist/resources/cached-content-params.js +36 -0
- package/dist/resources/cached-content.js +33 -33
- package/dist/server.js +1 -1
- package/dist/services/cache-events.d.ts +8 -0
- package/dist/services/cache-events.js +19 -0
- package/dist/services/cache.d.ts +5 -4
- package/dist/services/cache.js +49 -45
- package/dist/services/extractor.js +49 -38
- package/dist/services/fetcher/agents.js +1 -1
- package/dist/services/fetcher/dns-selection.js +1 -1
- package/dist/services/fetcher/interceptors.js +29 -60
- package/dist/services/fetcher/redirects.js +12 -4
- package/dist/services/fetcher/response.js +18 -8
- package/dist/services/fetcher.d.ts +21 -0
- package/dist/services/fetcher.js +532 -13
- package/dist/tools/handlers/fetch-single.shared.d.ts +11 -3
- package/dist/tools/handlers/fetch-single.shared.js +131 -2
- package/dist/tools/handlers/fetch-url.tool.d.ts +6 -0
- package/dist/tools/handlers/fetch-url.tool.js +48 -6
- package/dist/tools/utils/content-shaping.js +19 -4
- package/dist/tools/utils/content-transform.d.ts +4 -1
- package/dist/tools/utils/content-transform.js +110 -96
- package/dist/tools/utils/fetch-pipeline.js +47 -56
- package/dist/tools/utils/frontmatter.d.ts +3 -0
- package/dist/tools/utils/frontmatter.js +73 -0
- package/dist/tools/utils/markdown-heuristics.d.ts +1 -0
- package/dist/tools/utils/markdown-heuristics.js +19 -0
- package/dist/tools/utils/markdown-signals.d.ts +1 -0
- package/dist/tools/utils/markdown-signals.js +19 -0
- package/dist/tools/utils/raw-markdown-frontmatter.d.ts +3 -0
- package/dist/tools/utils/raw-markdown-frontmatter.js +73 -0
- package/dist/tools/utils/raw-markdown.d.ts +6 -0
- package/dist/tools/utils/raw-markdown.js +135 -0
- package/dist/transformers/markdown/fenced-code-rule.d.ts +2 -0
- package/dist/transformers/markdown/fenced-code-rule.js +38 -0
- package/dist/transformers/markdown/frontmatter.d.ts +2 -0
- package/dist/transformers/markdown/frontmatter.js +45 -0
- package/dist/transformers/markdown/noise-rule.d.ts +2 -0
- package/dist/transformers/markdown/noise-rule.js +80 -0
- package/dist/transformers/markdown/turndown-instance.d.ts +2 -0
- package/dist/transformers/markdown/turndown-instance.js +19 -0
- package/dist/transformers/markdown.d.ts +2 -0
- package/dist/transformers/markdown.js +185 -0
- package/dist/transformers/markdown.transformer.js +2 -189
- package/dist/utils/code-language-bash.d.ts +1 -0
- package/dist/utils/code-language-bash.js +48 -0
- package/dist/utils/code-language-core.d.ts +2 -0
- package/dist/utils/code-language-core.js +13 -0
- package/dist/utils/code-language-detectors.d.ts +5 -0
- package/dist/utils/code-language-detectors.js +142 -0
- package/dist/utils/code-language-helpers.d.ts +5 -0
- package/dist/utils/code-language-helpers.js +62 -0
- package/dist/utils/code-language-parsing.d.ts +5 -0
- package/dist/utils/code-language-parsing.js +62 -0
- package/dist/utils/code-language.d.ts +9 -0
- package/dist/utils/code-language.js +250 -46
- package/dist/utils/error-details.d.ts +3 -0
- package/dist/utils/error-details.js +12 -0
- package/dist/utils/filename-generator.js +14 -3
- package/dist/utils/ip-address.d.ts +4 -0
- package/dist/utils/ip-address.js +6 -0
- package/dist/utils/tool-error-handler.js +12 -17
- package/dist/utils/url-validator.js +33 -21
- package/package.json +7 -5
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export function containsJsxTag(code) {
|
|
2
|
+
for (let index = 0; index < code.length - 1; index += 1) {
|
|
3
|
+
if (code[index] !== '<')
|
|
4
|
+
continue;
|
|
5
|
+
const next = code[index + 1];
|
|
6
|
+
if (!next)
|
|
7
|
+
continue;
|
|
8
|
+
if (next >= 'A' && next <= 'Z')
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
export function containsWord(source, word) {
|
|
14
|
+
let startIndex = source.indexOf(word);
|
|
15
|
+
while (startIndex !== -1) {
|
|
16
|
+
const before = startIndex === 0 ? '' : source[startIndex - 1];
|
|
17
|
+
const afterIndex = startIndex + word.length;
|
|
18
|
+
const after = afterIndex >= source.length ? '' : source[afterIndex];
|
|
19
|
+
if (!isWordChar(before) && !isWordChar(after))
|
|
20
|
+
return true;
|
|
21
|
+
startIndex = source.indexOf(word, startIndex + word.length);
|
|
22
|
+
}
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
export function splitLines(content) {
|
|
26
|
+
return content.split('\n');
|
|
27
|
+
}
|
|
28
|
+
export function extractLanguageFromClassName(className) {
|
|
29
|
+
const tokens = className.match(/\S+/g);
|
|
30
|
+
if (!tokens)
|
|
31
|
+
return undefined;
|
|
32
|
+
for (const token of tokens) {
|
|
33
|
+
const lower = token.toLowerCase();
|
|
34
|
+
if (lower.startsWith('language-'))
|
|
35
|
+
return token.slice('language-'.length);
|
|
36
|
+
if (lower.startsWith('lang-'))
|
|
37
|
+
return token.slice('lang-'.length);
|
|
38
|
+
if (lower.startsWith('highlight-')) {
|
|
39
|
+
return token.slice('highlight-'.length);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
export function resolveLanguageFromDataAttribute(dataLang) {
|
|
45
|
+
const trimmed = dataLang.trim();
|
|
46
|
+
if (!trimmed)
|
|
47
|
+
return undefined;
|
|
48
|
+
for (const char of trimmed) {
|
|
49
|
+
if (!isWordChar(char))
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
return trimmed;
|
|
53
|
+
}
|
|
54
|
+
function isWordChar(char) {
|
|
55
|
+
if (!char)
|
|
56
|
+
return false;
|
|
57
|
+
const code = char.charCodeAt(0);
|
|
58
|
+
return ((code >= 48 && code <= 57) ||
|
|
59
|
+
(code >= 65 && code <= 90) ||
|
|
60
|
+
(code >= 97 && code <= 122) ||
|
|
61
|
+
char === '_');
|
|
62
|
+
}
|
|
@@ -1,2 +1,11 @@
|
|
|
1
|
+
export declare function containsJsxTag(code: string): boolean;
|
|
2
|
+
export declare function containsWord(source: string, word: string): boolean;
|
|
3
|
+
export declare function splitLines(content: string): string[];
|
|
4
|
+
export declare function extractLanguageFromClassName(className: string): string | undefined;
|
|
5
|
+
export declare function resolveLanguageFromDataAttribute(dataLang: string): string | undefined;
|
|
6
|
+
export interface CodeDetector {
|
|
7
|
+
language: string;
|
|
8
|
+
detect: (code: string) => boolean;
|
|
9
|
+
}
|
|
1
10
|
export declare function detectLanguageFromCode(code: string): string | undefined;
|
|
2
11
|
export declare function resolveLanguageFromAttributes(className: string, dataLang: string): string | undefined;
|
|
@@ -1,56 +1,260 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
'
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
'
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
1
|
+
export function containsJsxTag(code) {
|
|
2
|
+
for (let index = 0; index < code.length - 1; index += 1) {
|
|
3
|
+
if (code[index] !== '<')
|
|
4
|
+
continue;
|
|
5
|
+
const next = code[index + 1];
|
|
6
|
+
if (!next)
|
|
7
|
+
continue;
|
|
8
|
+
if (next >= 'A' && next <= 'Z')
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
export function containsWord(source, word) {
|
|
14
|
+
let startIndex = source.indexOf(word);
|
|
15
|
+
while (startIndex !== -1) {
|
|
16
|
+
const before = startIndex === 0 ? '' : source[startIndex - 1];
|
|
17
|
+
const afterIndex = startIndex + word.length;
|
|
18
|
+
const after = afterIndex >= source.length ? '' : source[afterIndex];
|
|
19
|
+
if (!isWordChar(before) && !isWordChar(after))
|
|
20
|
+
return true;
|
|
21
|
+
startIndex = source.indexOf(word, startIndex + word.length);
|
|
22
|
+
}
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
export function splitLines(content) {
|
|
26
|
+
return content.split('\n');
|
|
27
|
+
}
|
|
28
|
+
export function extractLanguageFromClassName(className) {
|
|
29
|
+
const tokens = className.match(/\S+/g);
|
|
30
|
+
if (!tokens)
|
|
31
|
+
return undefined;
|
|
32
|
+
for (const token of tokens) {
|
|
33
|
+
const lower = token.toLowerCase();
|
|
34
|
+
if (lower.startsWith('language-'))
|
|
35
|
+
return token.slice('language-'.length);
|
|
36
|
+
if (lower.startsWith('lang-'))
|
|
37
|
+
return token.slice('lang-'.length);
|
|
38
|
+
if (lower.startsWith('highlight-')) {
|
|
39
|
+
return token.slice('highlight-'.length);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
export function resolveLanguageFromDataAttribute(dataLang) {
|
|
45
|
+
const trimmed = dataLang.trim();
|
|
46
|
+
if (!trimmed)
|
|
47
|
+
return undefined;
|
|
48
|
+
for (const char of trimmed) {
|
|
49
|
+
if (!isWordChar(char))
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
return trimmed;
|
|
53
|
+
}
|
|
54
|
+
function isWordChar(char) {
|
|
55
|
+
if (!char)
|
|
56
|
+
return false;
|
|
57
|
+
const code = char.charCodeAt(0);
|
|
58
|
+
return ((code >= 48 && code <= 57) ||
|
|
59
|
+
(code >= 65 && code <= 90) ||
|
|
60
|
+
(code >= 97 && code <= 122) ||
|
|
61
|
+
char === '_');
|
|
62
|
+
}
|
|
63
|
+
const BASH_PACKAGE_MANAGERS = [
|
|
64
|
+
'npm',
|
|
65
|
+
'yarn',
|
|
66
|
+
'pnpm',
|
|
67
|
+
'npx',
|
|
68
|
+
'brew',
|
|
69
|
+
'apt',
|
|
70
|
+
'pip',
|
|
71
|
+
'cargo',
|
|
72
|
+
'go',
|
|
73
|
+
];
|
|
74
|
+
const BASH_VERBS = ['install', 'add', 'run', 'build', 'start'];
|
|
75
|
+
const BASH_COMMANDS = ['sudo', 'chmod', 'mkdir', 'cd', 'ls', 'cat', 'echo'];
|
|
76
|
+
function detectBash(code) {
|
|
77
|
+
const lines = splitLines(code);
|
|
78
|
+
for (const line of lines) {
|
|
79
|
+
const trimmed = line.trimStart();
|
|
80
|
+
if (!trimmed)
|
|
81
|
+
continue;
|
|
82
|
+
if (isBashIndicator(trimmed))
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
function startsWithCommand(line, commands) {
|
|
88
|
+
return commands.some((command) => line === command || line.startsWith(`${command} `));
|
|
89
|
+
}
|
|
90
|
+
function isBashIndicator(line) {
|
|
91
|
+
return (isShebang(line) ||
|
|
92
|
+
isPromptLine(line) ||
|
|
93
|
+
startsWithCommand(line, BASH_COMMANDS) ||
|
|
94
|
+
startsWithPackageManagerCommand(line));
|
|
95
|
+
}
|
|
96
|
+
function isShebang(line) {
|
|
97
|
+
return line.startsWith('#!');
|
|
98
|
+
}
|
|
99
|
+
function isPromptLine(line) {
|
|
100
|
+
return line.startsWith('$ ') || line.startsWith('# ');
|
|
101
|
+
}
|
|
102
|
+
function startsWithPackageManagerCommand(line) {
|
|
103
|
+
return BASH_PACKAGE_MANAGERS.some((manager) => {
|
|
104
|
+
if (!line.startsWith(`${manager} `))
|
|
105
|
+
return false;
|
|
106
|
+
const rest = line.slice(manager.length + 1);
|
|
107
|
+
return BASH_VERBS.some((verb) => rest === verb || rest.startsWith(`${verb} `));
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
const TYPE_HINTS = [
|
|
111
|
+
'string',
|
|
112
|
+
'number',
|
|
113
|
+
'boolean',
|
|
114
|
+
'void',
|
|
115
|
+
'any',
|
|
116
|
+
'unknown',
|
|
117
|
+
'never',
|
|
118
|
+
];
|
|
119
|
+
const HTML_TAGS = [
|
|
120
|
+
'<!doctype',
|
|
121
|
+
'<html',
|
|
122
|
+
'<head',
|
|
123
|
+
'<body',
|
|
124
|
+
'<div',
|
|
125
|
+
'<span',
|
|
126
|
+
'<p',
|
|
127
|
+
'<a',
|
|
128
|
+
'<script',
|
|
129
|
+
'<style',
|
|
130
|
+
];
|
|
131
|
+
const SQL_KEYWORDS = [
|
|
132
|
+
'select',
|
|
133
|
+
'insert',
|
|
134
|
+
'update',
|
|
135
|
+
'delete',
|
|
136
|
+
'create',
|
|
137
|
+
'alter',
|
|
138
|
+
'drop',
|
|
27
139
|
];
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
140
|
+
const JS_WORD_REGEX = /\b(?:const|let|var|function|class|async|await|export|import)\b/;
|
|
141
|
+
const PYTHON_WORD_REGEX = /\b(?:def|class|import|from)\b/;
|
|
142
|
+
const RUST_WORD_REGEX = /\b(?:fn|impl|struct|enum)\b/;
|
|
143
|
+
const CSS_DIRECTIVE_REGEX = /@media|@import|@keyframes/;
|
|
144
|
+
const CODE_DETECTORS = [
|
|
145
|
+
{ language: 'jsx', detect: detectJsx },
|
|
146
|
+
{ language: 'typescript', detect: detectTypescript },
|
|
147
|
+
{ language: 'rust', detect: detectRust },
|
|
148
|
+
{ language: 'javascript', detect: detectJavascript },
|
|
149
|
+
{ language: 'python', detect: detectPython },
|
|
150
|
+
{ language: 'bash', detect: detectBash },
|
|
151
|
+
{ language: 'css', detect: detectCss },
|
|
152
|
+
{ language: 'html', detect: detectHtml },
|
|
153
|
+
{ language: 'json', detect: detectJson },
|
|
154
|
+
{ language: 'yaml', detect: detectYaml },
|
|
155
|
+
{ language: 'sql', detect: detectSql },
|
|
156
|
+
{ language: 'go', detect: detectGo },
|
|
32
157
|
];
|
|
158
|
+
function detectJsx(code) {
|
|
159
|
+
const lower = code.toLowerCase();
|
|
160
|
+
if (lower.includes('classname='))
|
|
161
|
+
return true;
|
|
162
|
+
if (lower.includes('jsx:'))
|
|
163
|
+
return true;
|
|
164
|
+
if (lower.includes("from 'react'") || lower.includes('from "react"')) {
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
return containsJsxTag(code);
|
|
168
|
+
}
|
|
169
|
+
function detectTypescript(code) {
|
|
170
|
+
const lower = code.toLowerCase();
|
|
171
|
+
if (containsWord(lower, 'interface'))
|
|
172
|
+
return true;
|
|
173
|
+
if (containsWord(lower, 'type'))
|
|
174
|
+
return true;
|
|
175
|
+
return TYPE_HINTS.some((hint) => lower.includes(`: ${hint}`) || lower.includes(`:${hint}`));
|
|
176
|
+
}
|
|
177
|
+
function detectRust(code) {
|
|
178
|
+
const lower = code.toLowerCase();
|
|
179
|
+
return (RUST_WORD_REGEX.test(lower) ||
|
|
180
|
+
lower.includes('let mut') ||
|
|
181
|
+
(lower.includes('use ') && lower.includes('::')));
|
|
182
|
+
}
|
|
183
|
+
function detectJavascript(code) {
|
|
184
|
+
const lower = code.toLowerCase();
|
|
185
|
+
return JS_WORD_REGEX.test(lower);
|
|
186
|
+
}
|
|
187
|
+
function detectPython(code) {
|
|
188
|
+
const lower = code.toLowerCase();
|
|
189
|
+
return (PYTHON_WORD_REGEX.test(lower) ||
|
|
190
|
+
lower.includes('print(') ||
|
|
191
|
+
lower.includes('__name__'));
|
|
192
|
+
}
|
|
193
|
+
function detectCss(code) {
|
|
194
|
+
const lower = code.toLowerCase();
|
|
195
|
+
if (CSS_DIRECTIVE_REGEX.test(lower))
|
|
196
|
+
return true;
|
|
197
|
+
const lines = splitLines(code);
|
|
198
|
+
for (const line of lines) {
|
|
199
|
+
const trimmed = line.trimStart();
|
|
200
|
+
if (!trimmed)
|
|
201
|
+
continue;
|
|
202
|
+
if (isCssSelectorLine(trimmed) || isCssPropertyLine(trimmed))
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
function detectHtml(code) {
|
|
208
|
+
const lower = code.toLowerCase();
|
|
209
|
+
return HTML_TAGS.some((tag) => lower.includes(tag));
|
|
210
|
+
}
|
|
211
|
+
function detectJson(code) {
|
|
212
|
+
const trimmed = code.trimStart();
|
|
213
|
+
if (!trimmed)
|
|
214
|
+
return false;
|
|
215
|
+
return trimmed.startsWith('{') || trimmed.startsWith('[');
|
|
216
|
+
}
|
|
217
|
+
function detectYaml(code) {
|
|
218
|
+
const lines = splitLines(code);
|
|
219
|
+
for (const line of lines) {
|
|
220
|
+
const trimmed = line.trim();
|
|
221
|
+
if (!trimmed)
|
|
222
|
+
continue;
|
|
223
|
+
const colonIndex = trimmed.indexOf(':');
|
|
224
|
+
if (colonIndex <= 0)
|
|
225
|
+
continue;
|
|
226
|
+
const after = trimmed[colonIndex + 1];
|
|
227
|
+
if (after === ' ' || after === '\t')
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
function detectSql(code) {
|
|
233
|
+
const lower = code.toLowerCase();
|
|
234
|
+
return SQL_KEYWORDS.some((keyword) => containsWord(lower, keyword));
|
|
235
|
+
}
|
|
236
|
+
function detectGo(code) {
|
|
237
|
+
const lower = code.toLowerCase();
|
|
238
|
+
return (containsWord(lower, 'package') ||
|
|
239
|
+
containsWord(lower, 'func') ||
|
|
240
|
+
lower.includes('import "'));
|
|
241
|
+
}
|
|
242
|
+
function isCssSelectorLine(line) {
|
|
243
|
+
if (!line.startsWith('.') && !line.startsWith('#'))
|
|
244
|
+
return false;
|
|
245
|
+
return line.includes('{');
|
|
246
|
+
}
|
|
247
|
+
function isCssPropertyLine(line) {
|
|
248
|
+
return line.includes(':') && line.includes(';');
|
|
249
|
+
}
|
|
33
250
|
export function detectLanguageFromCode(code) {
|
|
34
|
-
for (const
|
|
35
|
-
if (
|
|
251
|
+
for (const { language, detect } of CODE_DETECTORS) {
|
|
252
|
+
if (detect(code))
|
|
36
253
|
return language;
|
|
37
|
-
}
|
|
38
254
|
}
|
|
39
255
|
return undefined;
|
|
40
256
|
}
|
|
41
257
|
export function resolveLanguageFromAttributes(className, dataLang) {
|
|
42
|
-
const classMatch =
|
|
258
|
+
const classMatch = extractLanguageFromClassName(className);
|
|
43
259
|
return classMatch ?? resolveLanguageFromDataAttribute(dataLang);
|
|
44
260
|
}
|
|
45
|
-
function matchFirstCapture(value, patterns) {
|
|
46
|
-
for (const pattern of patterns) {
|
|
47
|
-
const match = pattern.exec(value);
|
|
48
|
-
if (match?.[1])
|
|
49
|
-
return match[1];
|
|
50
|
-
}
|
|
51
|
-
return undefined;
|
|
52
|
-
}
|
|
53
|
-
function resolveLanguageFromDataAttribute(dataLang) {
|
|
54
|
-
const match = /^(\w+)$/.exec(dataLang);
|
|
55
|
-
return match?.[1];
|
|
56
|
-
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function getErrorMessage(error) {
|
|
2
|
+
return error instanceof Error ? error.message : 'Unknown error';
|
|
3
|
+
}
|
|
4
|
+
export function createErrorWithCode(message, code) {
|
|
5
|
+
const error = new Error(message);
|
|
6
|
+
return Object.assign(error, { code });
|
|
7
|
+
}
|
|
8
|
+
export function isSystemError(error) {
|
|
9
|
+
return (error instanceof Error &&
|
|
10
|
+
'code' in error &&
|
|
11
|
+
typeof Reflect.get(error, 'code') === 'string');
|
|
12
|
+
}
|
|
@@ -2,6 +2,17 @@ const MAX_FILENAME_LENGTH = 200;
|
|
|
2
2
|
const UNSAFE_CHARS_REGEX = /[<>:"/\\|?*]|\p{C}/gu;
|
|
3
3
|
const WHITESPACE_REGEX = /\s+/g;
|
|
4
4
|
const DEFAULT_EXTENSION = '.md';
|
|
5
|
+
function trimHyphens(value) {
|
|
6
|
+
let start = 0;
|
|
7
|
+
let end = value.length;
|
|
8
|
+
while (start < end && value[start] === '-') {
|
|
9
|
+
start += 1;
|
|
10
|
+
}
|
|
11
|
+
while (end > start && value[end - 1] === '-') {
|
|
12
|
+
end -= 1;
|
|
13
|
+
}
|
|
14
|
+
return value.slice(start, end);
|
|
15
|
+
}
|
|
5
16
|
export function generateSafeFilename(url, title, hashFallback, extension = DEFAULT_EXTENSION) {
|
|
6
17
|
const fromUrl = extractFilenameFromUrl(url);
|
|
7
18
|
if (fromUrl)
|
|
@@ -52,9 +63,9 @@ function slugifyTitle(title) {
|
|
|
52
63
|
.trim()
|
|
53
64
|
.replace(UNSAFE_CHARS_REGEX, '')
|
|
54
65
|
.replace(WHITESPACE_REGEX, '-')
|
|
55
|
-
.replace(/-+/g, '-')
|
|
56
|
-
|
|
57
|
-
return
|
|
66
|
+
.replace(/-+/g, '-');
|
|
67
|
+
const trimmed = trimHyphens(slug);
|
|
68
|
+
return trimmed || null;
|
|
58
69
|
}
|
|
59
70
|
function sanitizeFilename(name, extension) {
|
|
60
71
|
let sanitized = name
|
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
import { FetchError } from '../errors/app-error.js';
|
|
2
|
-
import { isSystemError } from './error-
|
|
3
|
-
function createFallbackErrorResponse(fallbackMessage, url, error) {
|
|
4
|
-
return createToolErrorResponse(`${fallbackMessage}: ${error.message}`, url);
|
|
5
|
-
}
|
|
6
|
-
function createUnknownErrorResponse(fallbackMessage, url) {
|
|
7
|
-
return createToolErrorResponse(`${fallbackMessage}: Unknown error`, url);
|
|
8
|
-
}
|
|
2
|
+
import { isSystemError } from './error-details.js';
|
|
9
3
|
export function createToolErrorResponse(message, url) {
|
|
10
4
|
const structuredContent = {
|
|
11
5
|
error: message,
|
|
@@ -18,19 +12,20 @@ export function createToolErrorResponse(message, url) {
|
|
|
18
12
|
};
|
|
19
13
|
}
|
|
20
14
|
export function handleToolError(error, url, fallbackMessage = 'Operation failed') {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
if (error instanceof FetchError) {
|
|
25
|
-
return createToolErrorResponse(error.message, url);
|
|
26
|
-
}
|
|
27
|
-
if (error instanceof Error) {
|
|
28
|
-
return createFallbackErrorResponse(fallbackMessage, url, error);
|
|
29
|
-
}
|
|
30
|
-
return createUnknownErrorResponse(fallbackMessage, url);
|
|
15
|
+
const message = resolveToolErrorMessage(error, fallbackMessage);
|
|
16
|
+
return createToolErrorResponse(message, url);
|
|
31
17
|
}
|
|
32
18
|
function isValidationError(error) {
|
|
33
19
|
return (error instanceof Error &&
|
|
34
20
|
isSystemError(error) &&
|
|
35
21
|
error.code === 'VALIDATION_ERROR');
|
|
36
22
|
}
|
|
23
|
+
function resolveToolErrorMessage(error, fallbackMessage) {
|
|
24
|
+
if (isValidationError(error) || error instanceof FetchError) {
|
|
25
|
+
return error.message;
|
|
26
|
+
}
|
|
27
|
+
if (error instanceof Error) {
|
|
28
|
+
return `${fallbackMessage}: ${error.message}`;
|
|
29
|
+
}
|
|
30
|
+
return `${fallbackMessage}: Unknown error`;
|
|
31
|
+
}
|
|
@@ -1,28 +1,38 @@
|
|
|
1
1
|
import { BlockList, isIP } from 'node:net';
|
|
2
2
|
import { config } from '../config/index.js';
|
|
3
|
-
import { createErrorWithCode } from './error-
|
|
3
|
+
import { createErrorWithCode } from './error-details.js';
|
|
4
|
+
import { buildIpv4, buildIpv6 } from './ip-address.js';
|
|
4
5
|
const BLOCK_LIST = new BlockList();
|
|
6
|
+
const IPV6_ZERO = buildIpv6([0, 0, 0, 0, 0, 0, 0, 0]);
|
|
7
|
+
const IPV6_LOOPBACK = buildIpv6([0, 0, 0, 0, 0, 0, 0, 1]);
|
|
8
|
+
const IPV6_64_FF9B = buildIpv6(['64', 'ff9b', 0, 0, 0, 0, 0, 0]);
|
|
9
|
+
const IPV6_64_FF9B_1 = buildIpv6(['64', 'ff9b', 1, 0, 0, 0, 0, 0]);
|
|
10
|
+
const IPV6_2001 = buildIpv6(['2001', 0, 0, 0, 0, 0, 0, 0]);
|
|
11
|
+
const IPV6_2002 = buildIpv6(['2002', 0, 0, 0, 0, 0, 0, 0]);
|
|
12
|
+
const IPV6_FC00 = buildIpv6(['fc00', 0, 0, 0, 0, 0, 0, 0]);
|
|
13
|
+
const IPV6_FE80 = buildIpv6(['fe80', 0, 0, 0, 0, 0, 0, 0]);
|
|
14
|
+
const IPV6_FF00 = buildIpv6(['ff00', 0, 0, 0, 0, 0, 0, 0]);
|
|
5
15
|
const BLOCKED_IPV4_SUBNETS = [
|
|
6
|
-
{ subnet:
|
|
7
|
-
{ subnet:
|
|
8
|
-
{ subnet:
|
|
9
|
-
{ subnet:
|
|
10
|
-
{ subnet:
|
|
11
|
-
{ subnet:
|
|
12
|
-
{ subnet:
|
|
13
|
-
{ subnet:
|
|
14
|
-
{ subnet:
|
|
16
|
+
{ subnet: buildIpv4([0, 0, 0, 0]), prefix: 8 },
|
|
17
|
+
{ subnet: buildIpv4([10, 0, 0, 0]), prefix: 8 },
|
|
18
|
+
{ subnet: buildIpv4([100, 64, 0, 0]), prefix: 10 },
|
|
19
|
+
{ subnet: buildIpv4([127, 0, 0, 0]), prefix: 8 },
|
|
20
|
+
{ subnet: buildIpv4([169, 254, 0, 0]), prefix: 16 },
|
|
21
|
+
{ subnet: buildIpv4([172, 16, 0, 0]), prefix: 12 },
|
|
22
|
+
{ subnet: buildIpv4([192, 168, 0, 0]), prefix: 16 },
|
|
23
|
+
{ subnet: buildIpv4([224, 0, 0, 0]), prefix: 4 },
|
|
24
|
+
{ subnet: buildIpv4([240, 0, 0, 0]), prefix: 4 },
|
|
15
25
|
];
|
|
16
26
|
const BLOCKED_IPV6_SUBNETS = [
|
|
17
|
-
{ subnet:
|
|
18
|
-
{ subnet:
|
|
19
|
-
{ subnet:
|
|
20
|
-
{ subnet:
|
|
21
|
-
{ subnet:
|
|
22
|
-
{ subnet:
|
|
23
|
-
{ subnet:
|
|
24
|
-
{ subnet:
|
|
25
|
-
{ subnet:
|
|
27
|
+
{ subnet: IPV6_ZERO, prefix: 128 },
|
|
28
|
+
{ subnet: IPV6_LOOPBACK, prefix: 128 },
|
|
29
|
+
{ subnet: IPV6_64_FF9B, prefix: 96 },
|
|
30
|
+
{ subnet: IPV6_64_FF9B_1, prefix: 48 },
|
|
31
|
+
{ subnet: IPV6_2001, prefix: 32 },
|
|
32
|
+
{ subnet: IPV6_2002, prefix: 16 },
|
|
33
|
+
{ subnet: IPV6_FC00, prefix: 7 },
|
|
34
|
+
{ subnet: IPV6_FE80, prefix: 10 },
|
|
35
|
+
{ subnet: IPV6_FF00, prefix: 8 },
|
|
26
36
|
];
|
|
27
37
|
for (const entry of BLOCKED_IPV4_SUBNETS) {
|
|
28
38
|
BLOCK_LIST.addSubnet(entry.subnet, entry.prefix, 'ipv4');
|
|
@@ -93,10 +103,12 @@ function assertUrlLength(url) {
|
|
|
93
103
|
throw createValidationError(`URL exceeds maximum length of ${config.constants.maxUrlLength} characters`);
|
|
94
104
|
}
|
|
95
105
|
function parseUrl(urlString) {
|
|
96
|
-
|
|
106
|
+
try {
|
|
107
|
+
return new URL(urlString);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
97
110
|
throw createValidationError('Invalid URL format');
|
|
98
111
|
}
|
|
99
|
-
return new URL(urlString);
|
|
100
112
|
}
|
|
101
113
|
function assertHttpProtocol(url) {
|
|
102
114
|
if (url.protocol === 'http:' || url.protocol === 'https:')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@j0hanz/superfetch",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"mcpName": "io.github.j0hanz/superfetch",
|
|
5
5
|
"description": "Intelligent web content fetcher MCP server that converts HTML to clean, AI-readable Markdown",
|
|
6
6
|
"type": "module",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"prepublishOnly": "npm run lint && npm run type-check && npm run build"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@modelcontextprotocol/sdk": "^1.25.
|
|
53
|
+
"@modelcontextprotocol/sdk": "^1.25.2",
|
|
54
54
|
"@mozilla/readability": "^0.6.0",
|
|
55
55
|
"express": "^5.2.1",
|
|
56
56
|
"linkedom": "^0.18.12",
|
|
@@ -60,16 +60,18 @@
|
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@eslint/js": "^9.39.2",
|
|
63
|
-
"@trivago/prettier-plugin-sort-imports": "^6.0.
|
|
63
|
+
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
|
|
64
64
|
"@types/express": "^5.0.6",
|
|
65
65
|
"@types/node": "^22.19.3",
|
|
66
66
|
"@types/turndown": "^5.0.6",
|
|
67
67
|
"eslint": "^9.23.2",
|
|
68
68
|
"eslint-config-prettier": "^10.1.8",
|
|
69
|
+
"eslint-plugin-de-morgan": "^2.0.0",
|
|
70
|
+
"eslint-plugin-depend": "^1.4.0",
|
|
71
|
+
"eslint-plugin-sonarjs": "^3.0.5",
|
|
69
72
|
"eslint-plugin-unused-imports": "^4.3.0",
|
|
70
|
-
"knip": "^5.80.
|
|
73
|
+
"knip": "^5.80.1",
|
|
71
74
|
"prettier": "^3.7.4",
|
|
72
|
-
"shx": "^0.4.0",
|
|
73
75
|
"tsx": "^4.21.0",
|
|
74
76
|
"typescript": "^5.9.3",
|
|
75
77
|
"typescript-eslint": "^8.52.0"
|