openclaw-smart-fetch 0.1.3

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Thinkscape
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # openclaw-smart-fetch
2
+
3
+ OpenClaw plugin package for browser-fingerprinted fetching via `wreq-js` plus readable extraction via Defuddle.
4
+
5
+ ## What it registers
6
+
7
+ - `defuddle_fetch`
8
+
9
+ ## Install
10
+
11
+ From npm:
12
+
13
+ ```bash
14
+ openclaw plugins install openclaw-smart-fetch
15
+ ```
16
+
17
+ From a local checkout:
18
+
19
+ ```bash
20
+ openclaw plugins install -l /absolute/path/to/agent-smart-fetch/packages/openclaw-smart-fetch
21
+ ```
22
+
23
+ ## Tool parameters
24
+
25
+ Supported request parameters:
26
+ - `url`
27
+ - `browser`
28
+ - `os`
29
+ - `headers`
30
+ - `maxChars`
31
+ - `format`
32
+ - `removeImages`
33
+ - `includeReplies`
34
+ - `proxy`
35
+
36
+ ## OpenClaw config
37
+
38
+ See `openclaw.plugin.json` for the plugin config schema and defaults.
@@ -0,0 +1,47 @@
1
+ type IncludeRepliesOption = boolean | "extractors";
2
+ interface FetchToolConfig {
3
+ maxChars?: number;
4
+ timeoutMs?: number;
5
+ browser?: string;
6
+ os?: string;
7
+ removeImages?: boolean;
8
+ includeReplies?: IncludeRepliesOption;
9
+ }
10
+ interface FetchToolDefaults {
11
+ maxChars: number;
12
+ timeoutMs: number;
13
+ browser: string;
14
+ os: string;
15
+ removeImages: boolean;
16
+ includeReplies: IncludeRepliesOption;
17
+ }
18
+
19
+ type PluginConfig = FetchToolConfig;
20
+ interface ToolRegistrationApi {
21
+ pluginConfig?: PluginConfig;
22
+ registerTool(definition: {
23
+ name: string;
24
+ description: string;
25
+ parameters: unknown;
26
+ execute(toolCallId: string, params: Record<string, unknown>): Promise<{
27
+ content: Array<{
28
+ type: "text";
29
+ text: string;
30
+ }>;
31
+ isError?: boolean;
32
+ }>;
33
+ }): void;
34
+ logger: {
35
+ info(message: string): void;
36
+ };
37
+ }
38
+
39
+ declare const resolvePluginDefaults: (pluginConfig?: PluginConfig) => FetchToolDefaults;
40
+ declare const plugin: {
41
+ id: string;
42
+ name: string;
43
+ description: string;
44
+ register(api: ToolRegistrationApi): void;
45
+ };
46
+
47
+ export { plugin as default, resolvePluginDefaults };
package/dist/index.js ADDED
@@ -0,0 +1,286 @@
1
+ import { Type } from '@sinclair/typebox';
2
+ import { Defuddle } from 'defuddle/node';
3
+ import { getProfiles, fetch } from 'wreq-js';
4
+ import { parseHTML } from 'linkedom';
5
+
6
+ // src/index.ts
7
+
8
+ // ../core/src/constants.ts
9
+ var DEFAULT_BROWSER = "chrome_145";
10
+ var DEFAULT_OS = "windows";
11
+ var DEFAULT_MAX_CHARS = 5e4;
12
+ var DEFAULT_TIMEOUT_MS = 15e3;
13
+ var DEFAULT_INCLUDE_REPLIES = "extractors";
14
+ var DEFAULT_ACCEPT_HEADER = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8";
15
+ var DEFAULT_ACCEPT_LANGUAGE_HEADER = "en-US,en;q=0.9";
16
+ var runtimeDependencies = {
17
+ fetch: fetch,
18
+ defuddle: Defuddle,
19
+ getProfiles
20
+ };
21
+ function parseLinkedomHTML(html, url) {
22
+ const { document } = parseHTML(html);
23
+ const doc = document;
24
+ const defaultView = doc.defaultView;
25
+ if (!doc.styleSheets) {
26
+ doc.styleSheets = [];
27
+ }
28
+ if (defaultView && !defaultView.getComputedStyle) {
29
+ defaultView.getComputedStyle = (() => ({
30
+ display: ""
31
+ }));
32
+ }
33
+ if (url) {
34
+ doc.URL = url;
35
+ }
36
+ return document;
37
+ }
38
+
39
+ // ../core/src/format.ts
40
+ function buildHeader(parts) {
41
+ return parts.filter(([, value]) => value !== void 0 && value !== "").map(([label, value]) => `> ${label}: ${value}`).join("\n");
42
+ }
43
+ function markdownToText(markdown) {
44
+ return markdown.replace(/^#{1,6}\s+/gm, "").replace(/\*\*([^*]+)\*\*/g, "$1").replace(/\*([^*]+)\*/g, "$1").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/!\[[^\]]*\]\([^)]+\)/g, "").replace(/^>\s+/gm, "").replace(/^[-*+]\s+/gm, "\u2022 ").replace(/`([^`]+)`/g, "$1");
45
+ }
46
+ function truncateContent(content, maxChars) {
47
+ if (content.length <= maxChars) return content;
48
+ return `${content.slice(0, maxChars)}
49
+
50
+ [... truncated]`;
51
+ }
52
+ function buildCompactMetadataHeader(result) {
53
+ return buildHeader([
54
+ ["URL", result.finalUrl],
55
+ ["Title", result.title],
56
+ ["Author", result.author],
57
+ ["Published", result.published]
58
+ ]);
59
+ }
60
+ function buildMetadataHeader(result) {
61
+ return buildHeader([
62
+ ["URL", result.finalUrl],
63
+ ["Title", result.title],
64
+ ["Author", result.author],
65
+ ["Published", result.published],
66
+ ["Site", result.site],
67
+ ["Language", result.language],
68
+ ["Words", result.wordCount],
69
+ ["Browser", `${result.browser}/${result.os}`]
70
+ ]);
71
+ }
72
+ function buildFetchResponseText(result, options = {}) {
73
+ const header = options.verbose ? buildMetadataHeader(result) : buildCompactMetadataHeader(result);
74
+ return header ? `${header}
75
+
76
+ ${result.content}` : result.content;
77
+ }
78
+
79
+ // ../core/src/extract.ts
80
+ var HTML_CONTENT_TYPES = [
81
+ "text/html",
82
+ "application/xhtml+xml",
83
+ "text/plain",
84
+ "text/markdown"
85
+ ];
86
+ function createDefuddleFetch(dependencies = runtimeDependencies) {
87
+ return async function defuddleFetch2(opts) {
88
+ const browser = opts.browser ?? DEFAULT_BROWSER;
89
+ const os = opts.os ?? DEFAULT_OS;
90
+ const format = opts.format ?? "markdown";
91
+ const maxChars = opts.maxChars ?? DEFAULT_MAX_CHARS;
92
+ const removeImages = opts.removeImages ?? false;
93
+ const includeReplies = opts.includeReplies ?? DEFAULT_INCLUDE_REPLIES;
94
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
95
+ let parsed;
96
+ try {
97
+ parsed = new URL(opts.url);
98
+ } catch {
99
+ return { error: `Invalid URL: ${opts.url}` };
100
+ }
101
+ if (!["http:", "https:"].includes(parsed.protocol)) {
102
+ return {
103
+ error: `Only http/https URLs supported, got ${parsed.protocol}`
104
+ };
105
+ }
106
+ const fetchOptions = {
107
+ browser,
108
+ os,
109
+ headers: {
110
+ Accept: DEFAULT_ACCEPT_HEADER,
111
+ "Accept-Language": DEFAULT_ACCEPT_LANGUAGE_HEADER,
112
+ ...opts.headers
113
+ },
114
+ redirect: "follow",
115
+ timeout: timeoutMs
116
+ };
117
+ if (opts.proxy) {
118
+ fetchOptions.proxy = opts.proxy;
119
+ }
120
+ const response = await dependencies.fetch(opts.url, fetchOptions);
121
+ if (!response.ok) {
122
+ return {
123
+ error: `HTTP ${response.status} ${response.statusText} for ${opts.url}`
124
+ };
125
+ }
126
+ const finalUrl = response.url ?? opts.url;
127
+ const contentType = response.headers.get("content-type") ?? "";
128
+ if (!HTML_CONTENT_TYPES.some((value) => contentType.includes(value))) {
129
+ return { error: `Not an HTML page (content-type: ${contentType})` };
130
+ }
131
+ const html = await response.text();
132
+ const document = parseLinkedomHTML(html, finalUrl);
133
+ const extracted = await dependencies.defuddle(document, finalUrl, {
134
+ markdown: format !== "html",
135
+ removeImages,
136
+ includeReplies
137
+ });
138
+ if (!extracted.content || extracted.wordCount === 0) {
139
+ return {
140
+ error: `No content extracted from ${opts.url}. May need JS rendering or is blocked.`
141
+ };
142
+ }
143
+ const normalizedContent = format === "text" ? markdownToText(extracted.content) : extracted.content;
144
+ return {
145
+ url: opts.url,
146
+ finalUrl,
147
+ title: extracted.title ?? "",
148
+ author: extracted.author ?? "",
149
+ published: extracted.published ?? "",
150
+ site: extracted.site ?? "",
151
+ language: extracted.language ?? "",
152
+ wordCount: extracted.wordCount,
153
+ content: truncateContent(normalizedContent, maxChars),
154
+ browser,
155
+ os
156
+ };
157
+ };
158
+ }
159
+ var defuddleFetch = createDefuddleFetch();
160
+ function isError(result) {
161
+ return "error" in result;
162
+ }
163
+ function resolveFetchToolDefaults(config = {}) {
164
+ return {
165
+ maxChars: config.maxChars ?? DEFAULT_MAX_CHARS,
166
+ timeoutMs: config.timeoutMs ?? DEFAULT_TIMEOUT_MS,
167
+ browser: config.browser ?? DEFAULT_BROWSER,
168
+ os: config.os ?? DEFAULT_OS,
169
+ removeImages: config.removeImages ?? false,
170
+ includeReplies: config.includeReplies ?? DEFAULT_INCLUDE_REPLIES
171
+ };
172
+ }
173
+ function createBaseFetchToolParameterProperties(defaults) {
174
+ return {
175
+ url: Type.String({ description: "URL to fetch (http/https only)" }),
176
+ browser: Type.Optional(
177
+ Type.String({
178
+ description: `Browser profile for TLS fingerprinting. Default: "${defaults.browser}". Examples: chrome_145, firefox_147, safari_26, edge_145, opera_127`
179
+ })
180
+ ),
181
+ os: Type.Optional(
182
+ Type.String({
183
+ description: `OS profile for fingerprinting. Default: "${defaults.os}". Options: windows, macos, linux, android, ios`
184
+ })
185
+ ),
186
+ headers: Type.Optional(
187
+ Type.Record(Type.String(), Type.String(), {
188
+ description: "Custom HTTP headers to send. By default, Accept and Accept-Language are set automatically."
189
+ })
190
+ ),
191
+ maxChars: Type.Optional(
192
+ Type.Number({
193
+ description: `Maximum characters to return. Default: ${defaults.maxChars}`
194
+ })
195
+ ),
196
+ format: Type.Optional(
197
+ Type.Union(
198
+ [Type.Literal("markdown"), Type.Literal("html"), Type.Literal("text")],
199
+ {
200
+ description: 'Output format. "markdown" (default), "html" (cleaned HTML), or "text" (plain text, no formatting)'
201
+ }
202
+ )
203
+ ),
204
+ removeImages: Type.Optional(
205
+ Type.Boolean({
206
+ description: "Strip image references from output. Default: false"
207
+ })
208
+ ),
209
+ includeReplies: Type.Optional(
210
+ Type.Union([Type.Boolean(), Type.Literal("extractors")], {
211
+ description: "Include replies/comments: 'extractors' for site-specific only (default), true for all, false for none"
212
+ })
213
+ ),
214
+ proxy: Type.Optional(
215
+ Type.String({
216
+ description: "Proxy URL (http://user:pass@host:port or socks5://host:port)"
217
+ })
218
+ )
219
+ };
220
+ }
221
+ async function executeFetchToolCall(params, defaults) {
222
+ return defuddleFetch({
223
+ url: params.url,
224
+ browser: params.browser ?? defaults.browser,
225
+ os: params.os ?? defaults.os,
226
+ headers: params.headers,
227
+ maxChars: params.maxChars ?? defaults.maxChars,
228
+ format: params.format ?? "markdown",
229
+ removeImages: params.removeImages ?? defaults.removeImages,
230
+ includeReplies: params.includeReplies ?? defaults.includeReplies,
231
+ proxy: params.proxy,
232
+ timeoutMs: defaults.timeoutMs
233
+ });
234
+ }
235
+
236
+ // src/index.ts
237
+ var resolvePluginDefaults = (pluginConfig = {}) => resolveFetchToolDefaults(pluginConfig);
238
+ function renderToolResponse(result) {
239
+ return {
240
+ content: [
241
+ {
242
+ type: "text",
243
+ text: buildFetchResponseText(result, { verbose: true })
244
+ }
245
+ ]
246
+ };
247
+ }
248
+ var plugin = {
249
+ id: "smart-fetch",
250
+ name: "Smart Fetch",
251
+ description: "Clean web content extraction with TLS fingerprinting. Uses wreq-js (Rust native bindings) for browser-grade TLS and Defuddle for extraction.",
252
+ register(api) {
253
+ const defaults = resolvePluginDefaults(api.pluginConfig);
254
+ api.registerTool({
255
+ name: "defuddle_fetch",
256
+ description: [
257
+ "Fetch a URL with browser-grade TLS fingerprinting and extract clean, readable content.",
258
+ "Uses Rust native bindings to impersonate real browsers at the TLS/HTTP2 level (JA3/JA4 match).",
259
+ "Returns markdown with rich metadata (author, publish date, schema.org data).",
260
+ "Better noise removal and anti-bot bypass than web_fetch.",
261
+ "Does NOT execute JavaScript \u2014 use the browser tool for JS-heavy SPAs."
262
+ ].join(" "),
263
+ parameters: Type.Object(createBaseFetchToolParameterProperties(defaults)),
264
+ async execute(_toolCallId, params) {
265
+ const result = await executeFetchToolCall(params, defaults);
266
+ if (isError(result)) {
267
+ return {
268
+ content: [
269
+ { type: "text", text: `Error: ${result.error}` }
270
+ ],
271
+ isError: true
272
+ };
273
+ }
274
+ return renderToolResponse(result);
275
+ }
276
+ });
277
+ api.logger.info(
278
+ `defuddle_fetch tool registered (default: ${defaults.browser}/${defaults.os})`
279
+ );
280
+ }
281
+ };
282
+ var index_default = plugin;
283
+
284
+ export { index_default as default, resolvePluginDefaults };
285
+ //# sourceMappingURL=index.js.map
286
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../core/src/constants.ts","../../core/src/dependencies.ts","../../core/src/dom.ts","../../core/src/format.ts","../../core/src/extract.ts","../../core/src/tool.ts","../src/index.ts"],"names":["wreqFetch","defuddleFetch","Type"],"mappings":"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","file":"index.js","sourcesContent":["import type { FingerprintOs } from \"./types\";\n\nexport const DEFAULT_BROWSER = \"chrome_145\";\nexport const DEFAULT_OS: FingerprintOs = \"windows\";\nexport const DEFAULT_MAX_CHARS = 50_000;\nexport const DEFAULT_TIMEOUT_MS = 15_000;\nexport const DEFAULT_INCLUDE_REPLIES = \"extractors\" as const;\nexport const DEFAULT_ACCEPT_HEADER =\n \"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\";\nexport const DEFAULT_ACCEPT_LANGUAGE_HEADER = \"en-US,en;q=0.9\";\n","import { Defuddle } from \"defuddle/node\";\nimport { getProfiles, fetch as wreqFetch } from \"wreq-js\";\nimport type { FetchDependencies } from \"./types\";\n\nexport const runtimeDependencies: FetchDependencies = {\n fetch: wreqFetch,\n defuddle: Defuddle,\n getProfiles,\n};\n","import { parseHTML } from \"linkedom\";\n\n/** Apply linkedom polyfills that Defuddle expects (getComputedStyle, styleSheets). */\nexport function parseLinkedomHTML(html: string, url?: string): Document {\n const { document } = parseHTML(html);\n const doc = document as Document & Record<string, unknown>;\n const defaultView = doc.defaultView as\n | (Window & {\n getComputedStyle?: (\n elt: Element,\n pseudoElt?: string | null,\n ) => CSSStyleDeclaration;\n })\n | undefined;\n\n if (!(doc as { styleSheets?: unknown }).styleSheets) {\n (doc as { styleSheets?: unknown }).styleSheets =\n [] as unknown as StyleSheetList;\n }\n\n if (defaultView && !defaultView.getComputedStyle) {\n defaultView.getComputedStyle = (() => ({\n display: \"\",\n })) as unknown as typeof defaultView.getComputedStyle;\n }\n\n if (url) {\n (doc as { URL?: string }).URL = url;\n }\n\n return document;\n}\n","import type { FetchResult } from \"./types\";\n\nfunction buildHeader(\n parts: Array<[label: string, value: string | number | undefined]>,\n) {\n return parts\n .filter(([, value]) => value !== undefined && value !== \"\")\n .map(([label, value]) => `> ${label}: ${value}`)\n .join(\"\\n\");\n}\n\nexport function markdownToText(markdown: string): string {\n return markdown\n .replace(/^#{1,6}\\s+/gm, \"\")\n .replace(/\\*\\*([^*]+)\\*\\*/g, \"$1\")\n .replace(/\\*([^*]+)\\*/g, \"$1\")\n .replace(/\\[([^\\]]+)\\]\\([^)]+\\)/g, \"$1\")\n .replace(/!\\[[^\\]]*\\]\\([^)]+\\)/g, \"\")\n .replace(/^>\\s+/gm, \"\")\n .replace(/^[-*+]\\s+/gm, \"• \")\n .replace(/`([^`]+)`/g, \"$1\");\n}\n\nexport function truncateContent(content: string, maxChars: number): string {\n if (content.length <= maxChars) return content;\n return `${content.slice(0, maxChars)}\\n\\n[... truncated]`;\n}\n\nexport function buildCompactMetadataHeader(result: FetchResult): string {\n return buildHeader([\n [\"URL\", result.finalUrl],\n [\"Title\", result.title],\n [\"Author\", result.author],\n [\"Published\", result.published],\n ]);\n}\n\nexport function buildMetadataHeader(result: FetchResult): string {\n return buildHeader([\n [\"URL\", result.finalUrl],\n [\"Title\", result.title],\n [\"Author\", result.author],\n [\"Published\", result.published],\n [\"Site\", result.site],\n [\"Language\", result.language],\n [\"Words\", result.wordCount],\n [\"Browser\", `${result.browser}/${result.os}`],\n ]);\n}\n\nexport function buildFetchResponseText(\n result: FetchResult,\n options: { verbose?: boolean } = {},\n): string {\n const header = options.verbose\n ? buildMetadataHeader(result)\n : buildCompactMetadataHeader(result);\n\n return header ? `${header}\\n\\n${result.content}` : result.content;\n}\n","/**\n * Core extraction pipeline: fetch with TLS fingerprinting → parse → Defuddle extract.\n * Separated from the plugin entry so it can be tested independently.\n */\n\nimport {\n DEFAULT_ACCEPT_HEADER,\n DEFAULT_ACCEPT_LANGUAGE_HEADER,\n DEFAULT_BROWSER,\n DEFAULT_INCLUDE_REPLIES,\n DEFAULT_MAX_CHARS,\n DEFAULT_OS,\n DEFAULT_TIMEOUT_MS,\n} from \"./constants\";\nimport { runtimeDependencies } from \"./dependencies\";\nimport { parseLinkedomHTML } from \"./dom\";\nimport { markdownToText, truncateContent } from \"./format\";\nimport { getLatestChromeProfile as getLatestChromeProfileFrom } from \"./profiles\";\nimport type {\n FetchDependencies,\n FetchError,\n FetchOptions,\n FetchResult,\n OutputFormat,\n} from \"./types\";\n\nexport {\n DEFAULT_BROWSER,\n DEFAULT_INCLUDE_REPLIES,\n DEFAULT_MAX_CHARS,\n DEFAULT_OS,\n DEFAULT_TIMEOUT_MS,\n} from \"./constants\";\nexport type {\n FetchError,\n FetchOptions,\n FetchResult,\n OutputFormat,\n} from \"./types\";\n\nconst HTML_CONTENT_TYPES = [\n \"text/html\",\n \"application/xhtml+xml\",\n \"text/plain\",\n \"text/markdown\",\n];\n\nexport function getLatestChromeProfile(): string {\n return getLatestChromeProfileFrom(runtimeDependencies.getProfiles);\n}\n\nexport function createDefuddleFetch(\n dependencies: FetchDependencies = runtimeDependencies,\n) {\n return async function defuddleFetch(\n opts: FetchOptions,\n ): Promise<FetchResult | FetchError> {\n const browser = opts.browser ?? DEFAULT_BROWSER;\n const os = opts.os ?? DEFAULT_OS;\n const format: OutputFormat = opts.format ?? \"markdown\";\n const maxChars = opts.maxChars ?? DEFAULT_MAX_CHARS;\n const removeImages = opts.removeImages ?? false;\n const includeReplies = opts.includeReplies ?? DEFAULT_INCLUDE_REPLIES;\n const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n\n let parsed: URL;\n try {\n parsed = new URL(opts.url);\n } catch {\n return { error: `Invalid URL: ${opts.url}` };\n }\n\n if (![\"http:\", \"https:\"].includes(parsed.protocol)) {\n return {\n error: `Only http/https URLs supported, got ${parsed.protocol}`,\n };\n }\n\n const fetchOptions: Record<string, unknown> = {\n browser,\n os,\n headers: {\n Accept: DEFAULT_ACCEPT_HEADER,\n \"Accept-Language\": DEFAULT_ACCEPT_LANGUAGE_HEADER,\n ...opts.headers,\n },\n redirect: \"follow\",\n timeout: timeoutMs,\n };\n\n if (opts.proxy) {\n fetchOptions.proxy = opts.proxy;\n }\n\n const response = await dependencies.fetch(opts.url, fetchOptions);\n\n if (!response.ok) {\n return {\n error: `HTTP ${response.status} ${response.statusText} for ${opts.url}`,\n };\n }\n\n const finalUrl = response.url ?? opts.url;\n const contentType = response.headers.get(\"content-type\") ?? \"\";\n\n if (!HTML_CONTENT_TYPES.some((value) => contentType.includes(value))) {\n return { error: `Not an HTML page (content-type: ${contentType})` };\n }\n\n const html = await response.text();\n const document = parseLinkedomHTML(html, finalUrl);\n const extracted = await dependencies.defuddle(document, finalUrl, {\n markdown: format !== \"html\",\n removeImages,\n includeReplies,\n });\n\n if (!extracted.content || extracted.wordCount === 0) {\n return {\n error: `No content extracted from ${opts.url}. May need JS rendering or is blocked.`,\n };\n }\n\n const normalizedContent =\n format === \"text\" ? markdownToText(extracted.content) : extracted.content;\n\n return {\n url: opts.url,\n finalUrl,\n title: extracted.title ?? \"\",\n author: extracted.author ?? \"\",\n published: extracted.published ?? \"\",\n site: extracted.site ?? \"\",\n language: extracted.language ?? \"\",\n wordCount: extracted.wordCount,\n content: truncateContent(normalizedContent, maxChars),\n browser,\n os,\n };\n };\n}\n\nexport const defuddleFetch = createDefuddleFetch();\n\n/** Type guard: check if result is an error. */\nexport function isError(\n result: FetchResult | FetchError,\n): result is FetchError {\n return \"error\" in result;\n}\n","import { type TSchema, Type } from \"@sinclair/typebox\";\nimport {\n DEFAULT_BROWSER,\n DEFAULT_INCLUDE_REPLIES,\n DEFAULT_MAX_CHARS,\n DEFAULT_OS,\n DEFAULT_TIMEOUT_MS,\n} from \"./constants\";\nimport { defuddleFetch } from \"./extract\";\nimport type {\n FetchError,\n FetchResult,\n FetchToolConfig,\n FetchToolDefaults,\n} from \"./types\";\n\nexport function resolveFetchToolDefaults(\n config: FetchToolConfig = {},\n): FetchToolDefaults {\n return {\n maxChars: config.maxChars ?? DEFAULT_MAX_CHARS,\n timeoutMs: config.timeoutMs ?? DEFAULT_TIMEOUT_MS,\n browser: config.browser ?? DEFAULT_BROWSER,\n os: config.os ?? DEFAULT_OS,\n removeImages: config.removeImages ?? false,\n includeReplies: config.includeReplies ?? DEFAULT_INCLUDE_REPLIES,\n };\n}\n\nexport function createBaseFetchToolParameterProperties(\n defaults: FetchToolDefaults,\n): Record<string, TSchema> {\n return {\n url: Type.String({ description: \"URL to fetch (http/https only)\" }),\n browser: Type.Optional(\n Type.String({\n description: `Browser profile for TLS fingerprinting. Default: \"${defaults.browser}\". Examples: chrome_145, firefox_147, safari_26, edge_145, opera_127`,\n }),\n ),\n os: Type.Optional(\n Type.String({\n description: `OS profile for fingerprinting. Default: \"${defaults.os}\". Options: windows, macos, linux, android, ios`,\n }),\n ),\n headers: Type.Optional(\n Type.Record(Type.String(), Type.String(), {\n description:\n \"Custom HTTP headers to send. By default, Accept and Accept-Language are set automatically.\",\n }),\n ),\n maxChars: Type.Optional(\n Type.Number({\n description: `Maximum characters to return. Default: ${defaults.maxChars}`,\n }),\n ),\n format: Type.Optional(\n Type.Union(\n [Type.Literal(\"markdown\"), Type.Literal(\"html\"), Type.Literal(\"text\")],\n {\n description:\n 'Output format. \"markdown\" (default), \"html\" (cleaned HTML), or \"text\" (plain text, no formatting)',\n },\n ),\n ),\n removeImages: Type.Optional(\n Type.Boolean({\n description: \"Strip image references from output. Default: false\",\n }),\n ),\n includeReplies: Type.Optional(\n Type.Union([Type.Boolean(), Type.Literal(\"extractors\")], {\n description:\n \"Include replies/comments: 'extractors' for site-specific only (default), true for all, false for none\",\n }),\n ),\n proxy: Type.Optional(\n Type.String({\n description:\n \"Proxy URL (http://user:pass@host:port or socks5://host:port)\",\n }),\n ),\n };\n}\n\nexport async function executeFetchToolCall(\n params: Record<string, unknown>,\n defaults: FetchToolDefaults,\n): Promise<FetchResult | FetchError> {\n return defuddleFetch({\n url: params.url as string,\n browser: (params.browser as string) ?? defaults.browser,\n os: (params.os as string) ?? defaults.os,\n headers: params.headers as Record<string, string> | undefined,\n maxChars: (params.maxChars as number) ?? defaults.maxChars,\n format: (params.format as \"markdown\" | \"html\" | \"text\") ?? \"markdown\",\n removeImages: (params.removeImages as boolean) ?? defaults.removeImages,\n includeReplies:\n (params.includeReplies as boolean | \"extractors\") ??\n defaults.includeReplies,\n proxy: params.proxy as string | undefined,\n timeoutMs: defaults.timeoutMs,\n });\n}\n","import { Type } from \"@sinclair/typebox\";\nimport {\n buildFetchResponseText,\n createBaseFetchToolParameterProperties,\n executeFetchToolCall,\n type FetchResult,\n isError,\n resolveFetchToolDefaults,\n} from \"smart-fetch-core\";\nimport type { PluginConfig, ToolRegistrationApi } from \"./types\";\n\nexport const resolvePluginDefaults = (pluginConfig: PluginConfig = {}) =>\n resolveFetchToolDefaults(pluginConfig);\n\nfunction renderToolResponse(result: FetchResult) {\n return {\n content: [\n {\n type: \"text\" as const,\n text: buildFetchResponseText(result, { verbose: true }),\n },\n ],\n };\n}\n\nconst plugin = {\n id: \"smart-fetch\",\n name: \"Smart Fetch\",\n description:\n \"Clean web content extraction with TLS fingerprinting. Uses wreq-js (Rust native bindings) for browser-grade TLS and Defuddle for extraction.\",\n\n register(api: ToolRegistrationApi) {\n const defaults = resolvePluginDefaults(api.pluginConfig);\n\n api.registerTool({\n name: \"defuddle_fetch\",\n description: [\n \"Fetch a URL with browser-grade TLS fingerprinting and extract clean, readable content.\",\n \"Uses Rust native bindings to impersonate real browsers at the TLS/HTTP2 level (JA3/JA4 match).\",\n \"Returns markdown with rich metadata (author, publish date, schema.org data).\",\n \"Better noise removal and anti-bot bypass than web_fetch.\",\n \"Does NOT execute JavaScript — use the browser tool for JS-heavy SPAs.\",\n ].join(\" \"),\n parameters: Type.Object(createBaseFetchToolParameterProperties(defaults)),\n\n async execute(_toolCallId: string, params: Record<string, unknown>) {\n const result = await executeFetchToolCall(params, defaults);\n\n if (isError(result)) {\n return {\n content: [\n { type: \"text\" as const, text: `Error: ${result.error}` },\n ],\n isError: true,\n };\n }\n\n return renderToolResponse(result);\n },\n });\n\n api.logger.info(\n `defuddle_fetch tool registered (default: ${defaults.browser}/${defaults.os})`,\n );\n },\n};\n\nexport default plugin;\n"]}
@@ -0,0 +1,43 @@
1
+ {
2
+ "id": "smart-fetch",
3
+ "name": "Smart Fetch",
4
+ "version": "0.1.3",
5
+ "description": "Clean web content extraction with browser-grade TLS fingerprinting. Uses wreq-js (Rust native bindings) for anti-bot bypass and Defuddle for superior content extraction.",
6
+ "configSchema": {
7
+ "type": "object",
8
+ "properties": {
9
+ "maxChars": {
10
+ "type": "number",
11
+ "description": "Maximum characters to return (default: 50000)",
12
+ "default": 50000
13
+ },
14
+ "timeoutMs": {
15
+ "type": "number",
16
+ "description": "Fetch timeout in milliseconds (default: 15000)",
17
+ "default": 15000
18
+ },
19
+ "browser": {
20
+ "type": "string",
21
+ "description": "Default browser profile for TLS fingerprinting (default: chrome_145)",
22
+ "default": "chrome_145"
23
+ },
24
+ "os": {
25
+ "type": "string",
26
+ "description": "Default OS profile for fingerprinting (default: windows)",
27
+ "default": "windows",
28
+ "enum": ["windows", "macos", "linux", "android", "ios"]
29
+ },
30
+ "removeImages": {
31
+ "type": "boolean",
32
+ "description": "Strip image references from output (default: false)",
33
+ "default": false
34
+ },
35
+ "includeReplies": {
36
+ "type": ["boolean", "string"],
37
+ "description": "Include replies/comments: 'extractors' (default), true, or false",
38
+ "default": "extractors"
39
+ }
40
+ },
41
+ "additionalProperties": false
42
+ }
43
+ }
package/package.json ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "openclaw-smart-fetch",
3
+ "version": "0.1.3",
4
+ "type": "module",
5
+ "description": "OpenClaw smart fetch plugin with browser-grade TLS fingerprinting and Defuddle extraction.",
6
+ "license": "MIT",
7
+ "author": "Thinkscape",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/Thinkscape/agent-smart-fetch.git"
11
+ },
12
+ "homepage": "https://github.com/Thinkscape/agent-smart-fetch#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/Thinkscape/agent-smart-fetch/issues"
15
+ },
16
+ "keywords": [
17
+ "openclaw",
18
+ "plugin",
19
+ "smart-fetch",
20
+ "defuddle",
21
+ "wreq-js",
22
+ "content-extraction",
23
+ "web-fetch",
24
+ "tls-fingerprinting"
25
+ ],
26
+ "main": "./dist/index.js",
27
+ "types": "./dist/index.d.ts",
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/index.d.ts",
31
+ "import": "./dist/index.js"
32
+ }
33
+ },
34
+ "files": [
35
+ "dist/",
36
+ "openclaw.plugin.json",
37
+ "README.md",
38
+ "LICENSE"
39
+ ],
40
+ "engines": {
41
+ "bun": ">=1.3.0",
42
+ "node": ">=22"
43
+ },
44
+ "publishConfig": {
45
+ "access": "public"
46
+ },
47
+ "openclaw": {
48
+ "extensions": [
49
+ "./dist/index.js"
50
+ ],
51
+ "compat": {
52
+ "pluginApi": ">=2026.3.2",
53
+ "minGatewayVersion": "2026.3.2"
54
+ },
55
+ "build": {
56
+ "openclawVersion": "2026.3.2",
57
+ "pluginSdkVersion": "2026.3.2"
58
+ }
59
+ },
60
+ "scripts": {
61
+ "clean": "rm -rf dist",
62
+ "build": "bun run clean && bunx tsup --config tsup.config.ts",
63
+ "test": "bun test test",
64
+ "typecheck": "bunx tsc -p tsconfig.json",
65
+ "check": "bun run test && bun run build && bun run typecheck",
66
+ "pack:dry-run": "bun run build && npm pack --dry-run"
67
+ },
68
+ "dependencies": {
69
+ "@sinclair/typebox": "^0.34.49",
70
+ "defuddle": "^0.14.0",
71
+ "linkedom": "^0.18.12",
72
+ "wreq-js": "^2.2.2"
73
+ }
74
+ }