@openclaw/duckduckgo-plugin 0.0.0 → 2026.8.1-beta.2

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 CHANGED
@@ -1,3 +1,12 @@
1
- # @openclaw/duckduckgo-plugin
1
+ # OpenClaw DuckDuckGo Plugin
2
2
 
3
- Reserved placeholder for the official OpenClaw plugin package. Use a published release version instead.
3
+ Official OpenClaw plugin for DuckDuckGo web search.
4
+
5
+ Install from OpenClaw:
6
+
7
+ ```bash
8
+ openclaw plugins install @openclaw/duckduckgo-plugin
9
+ openclaw gateway restart
10
+ ```
11
+
12
+ See <https://docs.openclaw.ai/tools/duckduckgo-search> for setup and configuration.
@@ -0,0 +1,152 @@
1
+ import { decodeHtmlEntities } from "openclaw/plugin-sdk/html-entity-runtime";
2
+ import { readProviderTextResponse } from "openclaw/plugin-sdk/provider-http";
3
+ import { DEFAULT_CACHE_TTL_MINUTES, DEFAULT_SEARCH_COUNT, normalizeCacheKey, readCache, readResponseText, resolveCacheTtlMs, resolveSearchCount, resolveSiteName, resolveTimeoutSeconds, withTrustedWebSearchEndpoint, wrapWebContent, writeCache } from "openclaw/plugin-sdk/provider-web-search";
4
+ import { normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
5
+ //#region extensions/duckduckgo/src/config.ts
6
+ const DEFAULT_DDG_SAFE_SEARCH = "moderate";
7
+ function resolveDdgWebSearchConfig(config) {
8
+ const webSearch = (config?.plugins?.entries?.duckduckgo?.config)?.webSearch;
9
+ if (webSearch && typeof webSearch === "object" && !Array.isArray(webSearch)) return webSearch;
10
+ }
11
+ function resolveDdgRegion(config) {
12
+ return normalizeOptionalString(resolveDdgWebSearchConfig(config)?.region);
13
+ }
14
+ function resolveDdgSafeSearch(config) {
15
+ const safeSearch = resolveDdgWebSearchConfig(config)?.safeSearch;
16
+ const normalized = normalizeLowercaseStringOrEmpty(safeSearch);
17
+ if (normalized === "strict" || normalized === "off") return normalized;
18
+ return DEFAULT_DDG_SAFE_SEARCH;
19
+ }
20
+ //#endregion
21
+ //#region extensions/duckduckgo/src/ddg-client.ts
22
+ const DDG_HTML_ENDPOINT = "https://html.duckduckgo.com/html";
23
+ const DEFAULT_TIMEOUT_SECONDS = 20;
24
+ const DDG_HTML_ENTITY_RE = /&(?:lt|gt|quot|apos|#39|#x27|#x2F|nbsp|ndash|mdash|hellip|amp|#\d+|#x[0-9a-f]+);/gi;
25
+ const DDG_ENTITY_TEXT = {
26
+ "\xA0": " ",
27
+ "–": "-",
28
+ "—": "--",
29
+ "…": "..."
30
+ };
31
+ const DDG_SAFE_SEARCH_PARAM = {
32
+ strict: "1",
33
+ moderate: "-1",
34
+ off: "-2"
35
+ };
36
+ const DDG_SEARCH_CACHE = /* @__PURE__ */ new Map();
37
+ function decodeHtmlEntities$1(text) {
38
+ return text.replace(DDG_HTML_ENTITY_RE, (entity) => {
39
+ const decoded = decodeHtmlEntities(entity.startsWith("&#") ? entity : entity.toLowerCase());
40
+ return DDG_ENTITY_TEXT[decoded] ?? decoded;
41
+ });
42
+ }
43
+ function stripHtml(html) {
44
+ return html.replace(/<\/?b\b[^>]*>/gi, "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
45
+ }
46
+ function decodeDuckDuckGoUrl(rawUrl) {
47
+ try {
48
+ const normalized = rawUrl.startsWith("//") ? `https:${rawUrl}` : rawUrl;
49
+ const uddg = new URL(normalized).searchParams.get("uddg");
50
+ if (uddg) return uddg;
51
+ } catch {}
52
+ return rawUrl;
53
+ }
54
+ function readHrefAttribute(tagAttributes) {
55
+ return /\bhref="([^"]*)"/i.exec(tagAttributes)?.[1] ?? "";
56
+ }
57
+ function isBotChallenge(html) {
58
+ if (/class="[^"]*\bresult__a\b[^"]*"/i.test(html)) return false;
59
+ return /g-recaptcha|are you a human|id="challenge-form"|name="challenge"/i.test(html);
60
+ }
61
+ async function readDuckDuckGoHtmlResponse(response) {
62
+ return await readProviderTextResponse(response, "DuckDuckGo search");
63
+ }
64
+ function parseDuckDuckGoHtml(html) {
65
+ const results = [];
66
+ const resultRegex = /<a\b(?=[^>]*\bclass="[^"]*\bresult__a\b[^"]*")([^>]*)>([\s\S]*?)<\/a>/gi;
67
+ const nextResultRegex = /<a\b(?=[^>]*\bclass="[^"]*\bresult__a\b[^"]*")[^>]*>/i;
68
+ const snippetRegex = /<a\b(?=[^>]*\bclass="[^"]*\bresult__snippet\b[^"]*")[^>]*>([\s\S]*?)<\/a>/i;
69
+ for (const match of html.matchAll(resultRegex)) {
70
+ const rawAttributes = match[1] ?? "";
71
+ const rawTitle = match[2] ?? "";
72
+ const rawUrl = readHrefAttribute(rawAttributes);
73
+ const matchEnd = (match.index ?? 0) + match[0].length;
74
+ const trailingHtml = html.slice(matchEnd);
75
+ const nextResultIndex = trailingHtml.search(nextResultRegex);
76
+ const scopedTrailingHtml = nextResultIndex >= 0 ? trailingHtml.slice(0, nextResultIndex) : trailingHtml;
77
+ const rawSnippet = snippetRegex.exec(scopedTrailingHtml)?.[1] ?? "";
78
+ const title = decodeHtmlEntities$1(stripHtml(rawTitle));
79
+ const url = decodeDuckDuckGoUrl(decodeHtmlEntities$1(rawUrl));
80
+ const snippet = decodeHtmlEntities$1(stripHtml(rawSnippet));
81
+ if (title && url) results.push({
82
+ title,
83
+ url,
84
+ snippet
85
+ });
86
+ }
87
+ return results;
88
+ }
89
+ async function runDuckDuckGoSearch(params) {
90
+ const count = resolveSearchCount(params.count, DEFAULT_SEARCH_COUNT);
91
+ const region = params.region ?? resolveDdgRegion(params.config);
92
+ const safeSearch = params.safeSearch === "strict" || params.safeSearch === "moderate" || params.safeSearch === "off" ? params.safeSearch : resolveDdgSafeSearch(params.config);
93
+ const timeoutSeconds = resolveTimeoutSeconds(params.timeoutSeconds, DEFAULT_TIMEOUT_SECONDS);
94
+ const cacheTtlMs = resolveCacheTtlMs(params.cacheTtlMinutes, DEFAULT_CACHE_TTL_MINUTES);
95
+ const cacheKey = normalizeCacheKey(JSON.stringify({
96
+ provider: "duckduckgo",
97
+ query: params.query,
98
+ count,
99
+ region: region ?? "",
100
+ safeSearch
101
+ }));
102
+ const cached = readCache(DDG_SEARCH_CACHE, cacheKey);
103
+ if (cached) return {
104
+ ...cached.value,
105
+ cached: true
106
+ };
107
+ const url = new URL(DDG_HTML_ENDPOINT);
108
+ url.searchParams.set("q", params.query);
109
+ if (region) url.searchParams.set("kl", region);
110
+ url.searchParams.set("kp", DDG_SAFE_SEARCH_PARAM[safeSearch]);
111
+ const startedAt = Date.now();
112
+ const results = await withTrustedWebSearchEndpoint({
113
+ url: url.toString(),
114
+ timeoutSeconds,
115
+ signal: params.signal,
116
+ init: {
117
+ method: "GET",
118
+ headers: { "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36" }
119
+ }
120
+ }, async (response) => {
121
+ if (!response.ok) {
122
+ const detail = (await readResponseText(response, { maxBytes: 64e3 })).text;
123
+ throw new Error(`DuckDuckGo search error (${response.status}): ${detail || response.statusText}`);
124
+ }
125
+ const html = await readDuckDuckGoHtmlResponse(response);
126
+ if (isBotChallenge(html)) throw new Error("DuckDuckGo returned a bot-detection challenge.");
127
+ return parseDuckDuckGoHtml(html).slice(0, count);
128
+ });
129
+ params.signal?.throwIfAborted();
130
+ const payload = {
131
+ query: params.query,
132
+ provider: "duckduckgo",
133
+ count: results.length,
134
+ tookMs: Date.now() - startedAt,
135
+ externalContent: {
136
+ untrusted: true,
137
+ source: "web_search",
138
+ provider: "duckduckgo",
139
+ wrapped: true
140
+ },
141
+ results: results.map((result) => ({
142
+ title: wrapWebContent(result.title, "web_search"),
143
+ url: result.url,
144
+ snippet: result.snippet ? wrapWebContent(result.snippet, "web_search") : "",
145
+ siteName: resolveSiteName(result.url) || void 0
146
+ }))
147
+ };
148
+ writeCache(DDG_SEARCH_CACHE, cacheKey, payload, cacheTtlMs);
149
+ return payload;
150
+ }
151
+ //#endregion
152
+ export { runDuckDuckGoSearch };
@@ -0,0 +1,55 @@
1
+ import { t as createDuckDuckGoWebSearchProviderBase } from "./ddg-search-provider.shared-B2Y6U1WN.js";
2
+ import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
3
+ import { readPositiveIntegerParam, readStringParam } from "openclaw/plugin-sdk/param-readers";
4
+ //#region extensions/duckduckgo/src/ddg-search-provider.ts
5
+ const loadDuckDuckGoClientModule = createLazyRuntimeModule(() => import("./ddg-client-CdJu902j.js"));
6
+ const DuckDuckGoSearchSchema = {
7
+ type: "object",
8
+ properties: {
9
+ query: {
10
+ type: "string",
11
+ description: "Search query string."
12
+ },
13
+ count: {
14
+ type: "integer",
15
+ description: "Number of results to return (1-10).",
16
+ minimum: 1,
17
+ maximum: 10
18
+ },
19
+ region: {
20
+ type: "string",
21
+ description: "Optional DuckDuckGo region code such as us-en, uk-en, or de-de."
22
+ },
23
+ safeSearch: {
24
+ type: "string",
25
+ description: "SafeSearch level: strict, moderate, or off."
26
+ }
27
+ },
28
+ additionalProperties: false
29
+ };
30
+ function createDuckDuckGoWebSearchProvider() {
31
+ return {
32
+ ...createDuckDuckGoWebSearchProviderBase(),
33
+ createTool: (ctx) => ({
34
+ description: "Search the web using DuckDuckGo. Returns titles, URLs, and snippets with no API key required.",
35
+ parameters: DuckDuckGoSearchSchema,
36
+ execute: async (args, context) => {
37
+ context?.signal?.throwIfAborted();
38
+ const { runDuckDuckGoSearch } = await loadDuckDuckGoClientModule();
39
+ return await runDuckDuckGoSearch({
40
+ config: ctx.config,
41
+ query: readStringParam(args, "query", { required: true }),
42
+ count: readPositiveIntegerParam(args, "count", {
43
+ max: 10,
44
+ message: "count must be an integer from 1 to 10."
45
+ }),
46
+ region: readStringParam(args, "region"),
47
+ safeSearch: readStringParam(args, "safeSearch"),
48
+ ...context?.signal ? { signal: context.signal } : {}
49
+ });
50
+ }
51
+ })
52
+ };
53
+ }
54
+ //#endregion
55
+ export { createDuckDuckGoWebSearchProvider as t };
@@ -0,0 +1,28 @@
1
+ import { createWebSearchProviderContractFields } from "openclaw/plugin-sdk/provider-web-search-contract";
2
+ //#region extensions/duckduckgo/src/ddg-search-provider.shared.ts
3
+ const DUCKDUCKGO_ONBOARDING_SCOPES = ["text-inference"];
4
+ function createDuckDuckGoWebSearchProviderBase() {
5
+ return {
6
+ id: "duckduckgo",
7
+ label: "DuckDuckGo Search (experimental)",
8
+ hint: "Free web search fallback with no API key required",
9
+ onboardingScopes: [...DUCKDUCKGO_ONBOARDING_SCOPES],
10
+ requiresCredential: false,
11
+ envVars: [],
12
+ placeholder: "(no key needed)",
13
+ signupUrl: "https://duckduckgo.com/",
14
+ docsUrl: "https://docs.openclaw.ai/tools/web",
15
+ autoDetectOrder: 100,
16
+ credentialPath: "",
17
+ ...createWebSearchProviderContractFields({
18
+ credentialPath: "",
19
+ searchCredential: {
20
+ type: "scoped",
21
+ scopeId: "duckduckgo"
22
+ },
23
+ selectionPluginId: "duckduckgo"
24
+ })
25
+ };
26
+ }
27
+ //#endregion
28
+ export { createDuckDuckGoWebSearchProviderBase as t };
package/dist/index.js ADDED
@@ -0,0 +1,13 @@
1
+ import { t as createDuckDuckGoWebSearchProvider } from "./ddg-search-provider-gy4EJrPB.js";
2
+ import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
3
+ //#region extensions/duckduckgo/index.ts
4
+ var duckduckgo_default = definePluginEntry({
5
+ id: "duckduckgo",
6
+ name: "DuckDuckGo Plugin",
7
+ description: "Official DuckDuckGo web search plugin",
8
+ register(api) {
9
+ api.registerWebSearchProvider(createDuckDuckGoWebSearchProvider());
10
+ }
11
+ });
12
+ //#endregion
13
+ export { duckduckgo_default as default };
@@ -0,0 +1,10 @@
1
+ import { t as createDuckDuckGoWebSearchProviderBase } from "./ddg-search-provider.shared-B2Y6U1WN.js";
2
+ //#region extensions/duckduckgo/web-search-contract-api.ts
3
+ function createDuckDuckGoWebSearchProvider() {
4
+ return {
5
+ ...createDuckDuckGoWebSearchProviderBase(),
6
+ createTool: () => null
7
+ };
8
+ }
9
+ //#endregion
10
+ export { createDuckDuckGoWebSearchProvider };
@@ -0,0 +1,2 @@
1
+ import { t as createDuckDuckGoWebSearchProvider } from "./ddg-search-provider-gy4EJrPB.js";
2
+ export { createDuckDuckGoWebSearchProvider };
@@ -0,0 +1,39 @@
1
+ {
2
+ "id": "duckduckgo",
3
+ "icon": "https://cdn.simpleicons.org/duckduckgo",
4
+ "activation": {
5
+ "onStartup": false
6
+ },
7
+ "uiHints": {
8
+ "webSearch.region": {
9
+ "label": "DuckDuckGo Region",
10
+ "help": "Optional DuckDuckGo region code such as us-en, uk-en, or de-de."
11
+ },
12
+ "webSearch.safeSearch": {
13
+ "label": "DuckDuckGo SafeSearch",
14
+ "help": "SafeSearch level for DuckDuckGo results."
15
+ }
16
+ },
17
+ "contracts": {
18
+ "webSearchProviders": ["duckduckgo"]
19
+ },
20
+ "configSchema": {
21
+ "type": "object",
22
+ "additionalProperties": false,
23
+ "properties": {
24
+ "webSearch": {
25
+ "type": "object",
26
+ "additionalProperties": false,
27
+ "properties": {
28
+ "region": {
29
+ "type": "string"
30
+ },
31
+ "safeSearch": {
32
+ "type": "string",
33
+ "enum": ["strict", "moderate", "off"]
34
+ }
35
+ }
36
+ }
37
+ }
38
+ }
39
+ }
package/package.json CHANGED
@@ -1,14 +1,49 @@
1
1
  {
2
2
  "name": "@openclaw/duckduckgo-plugin",
3
- "version": "0.0.0",
4
- "description": "Reserved package name for an official OpenClaw plugin.",
5
- "license": "MIT",
3
+ "version": "2026.8.1-beta.2",
4
+ "description": "OpenClaw DuckDuckGo plugin.",
6
5
  "repository": {
7
6
  "type": "git",
8
- "url": "git+https://github.com/openclaw/openclaw.git"
7
+ "url": "https://github.com/openclaw/openclaw"
9
8
  },
10
- "publishConfig": {
11
- "access": "public",
12
- "tag": "placeholder"
13
- }
9
+ "type": "module",
10
+ "openclaw": {
11
+ "extensions": [
12
+ "./index.ts"
13
+ ],
14
+ "install": {
15
+ "clawhubSpec": "clawhub:@openclaw/duckduckgo-plugin",
16
+ "npmSpec": "@openclaw/duckduckgo-plugin",
17
+ "defaultChoice": "npm",
18
+ "minHostVersion": ">=2026.7.2"
19
+ },
20
+ "compat": {
21
+ "pluginApi": ">=2026.8.1-beta.2"
22
+ },
23
+ "build": {
24
+ "openclawVersion": "2026.8.1-beta.2",
25
+ "bundledDist": false
26
+ },
27
+ "release": {
28
+ "publishToClawHub": true,
29
+ "publishToNpm": true
30
+ },
31
+ "runtimeExtensions": [
32
+ "./dist/index.js"
33
+ ]
34
+ },
35
+ "files": [
36
+ "dist/**",
37
+ "openclaw.plugin.json",
38
+ "README.md"
39
+ ],
40
+ "peerDependencies": {
41
+ "openclaw": ">=2026.8.1-beta.2"
42
+ },
43
+ "peerDependenciesMeta": {
44
+ "openclaw": {
45
+ "optional": true
46
+ }
47
+ },
48
+ "bundledDependencies": []
14
49
  }