@openclaw/searxng-plugin 0.0.0 → 2026.6.11

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,13 @@
1
- # @openclaw/searxng-plugin
1
+ # SearXNG OpenClaw plugin
2
2
 
3
- Bootstrap reservation for the OpenClaw SearXNG plugin package.
3
+ Official OpenClaw plugin for SearXNG.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ openclaw plugins install @openclaw/searxng-plugin
9
+ ```
10
+
11
+ ## Docs
12
+
13
+ See `docs/tools/searxng-search.md` in the OpenClaw repository, or the published docs at `https://docs.openclaw.ai/tools/searxng-search`.
package/dist/index.js ADDED
@@ -0,0 +1,13 @@
1
+ import { t as createSearxngWebSearchProvider } from "./searxng-search-provider-Da4Go6Vh.js";
2
+ import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
3
+ //#region extensions/searxng/index.ts
4
+ var searxng_default = definePluginEntry({
5
+ id: "searxng",
6
+ name: "SearXNG Plugin",
7
+ description: "Bundled SearXNG web search plugin",
8
+ register(api) {
9
+ api.registerWebSearchProvider(createSearxngWebSearchProvider());
10
+ }
11
+ });
12
+ //#endregion
13
+ export { searxng_default as default };
@@ -0,0 +1,219 @@
1
+ import { DEFAULT_CACHE_TTL_MINUTES, DEFAULT_SEARCH_COUNT, normalizeCacheKey, readCache, readResponseText, resolveCacheTtlMs, resolveSearchCount, resolveSiteName, resolveTimeoutSeconds, withSelfHostedWebSearchEndpoint, withTrustedWebSearchEndpoint, wrapWebContent, writeCache } from "openclaw/plugin-sdk/provider-web-search";
2
+ import { assertHttpUrlTargetsPrivateNetwork, isBlockedHostnameOrIp, isPrivateIpAddress, resolvePinnedHostnameWithPolicy } from "openclaw/plugin-sdk/ssrf-runtime";
3
+ import { normalizeResolvedSecretInputString, normalizeSecretInput } from "openclaw/plugin-sdk/secret-input";
4
+ //#region extensions/searxng/src/config.ts
5
+ function normalizeConfiguredString(value, path) {
6
+ try {
7
+ return normalizeSecretInput(normalizeResolvedSecretInputString({
8
+ value,
9
+ path
10
+ }));
11
+ } catch {
12
+ return;
13
+ }
14
+ }
15
+ function readInlineEnvSecretRefValue(value, env) {
16
+ if (!value || typeof value !== "object" || Array.isArray(value)) return;
17
+ const record = value;
18
+ if (record.source !== "env" || typeof record.id !== "string") return;
19
+ return normalizeSecretInput(env[record.id]);
20
+ }
21
+ function normalizeTrimmedString(value) {
22
+ if (typeof value !== "string") return;
23
+ return value.trim() || void 0;
24
+ }
25
+ function normalizeBaseUrl(value) {
26
+ return value?.replace(/\/+$/u, "") || void 0;
27
+ }
28
+ function resolveSearxngWebSearchConfig(config) {
29
+ const webSearch = (config?.plugins?.entries?.searxng?.config)?.webSearch;
30
+ if (webSearch && typeof webSearch === "object" && !Array.isArray(webSearch)) return webSearch;
31
+ }
32
+ function resolveSearxngBaseUrl(config, env = process.env) {
33
+ const webSearch = resolveSearxngWebSearchConfig(config);
34
+ return normalizeBaseUrl(normalizeConfiguredString(webSearch?.baseUrl, "plugins.entries.searxng.config.webSearch.baseUrl")) ?? normalizeBaseUrl(readInlineEnvSecretRefValue(webSearch?.baseUrl, env)) ?? normalizeBaseUrl(normalizeSecretInput(env.SEARXNG_BASE_URL));
35
+ }
36
+ function resolveSearxngCategories(config) {
37
+ return normalizeTrimmedString(resolveSearxngWebSearchConfig(config)?.categories);
38
+ }
39
+ function resolveSearxngLanguage(config) {
40
+ return normalizeTrimmedString(resolveSearxngWebSearchConfig(config)?.language);
41
+ }
42
+ //#endregion
43
+ //#region extensions/searxng/src/searxng-client.ts
44
+ const DEFAULT_TIMEOUT_SECONDS = 20;
45
+ const MAX_RESPONSE_BYTES = 1e6;
46
+ const SEARXNG_SEARCH_CACHE = /* @__PURE__ */ new Map();
47
+ function normalizeSearxngResult(value) {
48
+ if (!value || typeof value !== "object") return null;
49
+ const candidate = value;
50
+ if (typeof candidate.url !== "string" || typeof candidate.title !== "string") return null;
51
+ return {
52
+ url: candidate.url,
53
+ title: candidate.title,
54
+ content: typeof candidate.content === "string" ? candidate.content : void 0,
55
+ img_src: typeof candidate.img_src === "string" ? candidate.img_src : void 0
56
+ };
57
+ }
58
+ function buildSearxngSearchUrl(params) {
59
+ const url = new URL(params.baseUrl);
60
+ url.pathname = url.pathname.endsWith("/") ? `${url.pathname}search` : `${url.pathname}/search`;
61
+ url.search = "";
62
+ url.searchParams.set("q", params.query);
63
+ url.searchParams.set("format", "json");
64
+ if (params.categories) url.searchParams.set("categories", params.categories);
65
+ if (params.language) url.searchParams.set("language", params.language);
66
+ return url.toString();
67
+ }
68
+ function shouldRetryEmptyCategorySearchWithGeneral(categories) {
69
+ if (!categories) return false;
70
+ const normalized = categories.split(",").map((category) => category.trim().toLowerCase()).filter((category) => category.length > 0);
71
+ return normalized.length > 0 && !normalized.includes("general");
72
+ }
73
+ async function searxngEndpointTargetsPrivateNetwork(url, lookupFn) {
74
+ if (isBlockedHostnameOrIp(url.hostname)) return true;
75
+ try {
76
+ return (await resolvePinnedHostnameWithPolicy(url.hostname, {
77
+ lookupFn,
78
+ policy: {
79
+ allowPrivateNetwork: true,
80
+ allowRfc2544BenchmarkRange: true
81
+ }
82
+ })).addresses.every((address) => isPrivateIpAddress(address));
83
+ } catch {
84
+ return false;
85
+ }
86
+ }
87
+ async function validateSearxngBaseUrl(baseUrl, lookupFn) {
88
+ let parsed;
89
+ try {
90
+ parsed = new URL(baseUrl);
91
+ } catch {
92
+ throw new Error("SearXNG base URL must be a valid http:// or https:// URL.");
93
+ }
94
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error("SearXNG base URL must use http:// or https://.");
95
+ if (parsed.protocol === "http:") {
96
+ await assertHttpUrlTargetsPrivateNetwork(parsed.toString(), {
97
+ dangerouslyAllowPrivateNetwork: true,
98
+ lookupFn,
99
+ errorMessage: "SearXNG HTTP base URL must target a trusted private or loopback host. Use https:// for public hosts."
100
+ });
101
+ return "selfHosted";
102
+ }
103
+ return await searxngEndpointTargetsPrivateNetwork(parsed, lookupFn) ? "selfHosted" : "strict";
104
+ }
105
+ function parseSearxngResponseText(text, count) {
106
+ let parsed;
107
+ try {
108
+ parsed = JSON.parse(text);
109
+ } catch {
110
+ throw new Error("SearXNG returned invalid JSON.");
111
+ }
112
+ if (!parsed || typeof parsed !== "object") return [];
113
+ const response = parsed;
114
+ const rawResults = Array.isArray(response.results) ? response.results : [];
115
+ const results = [];
116
+ for (const rawResult of rawResults) {
117
+ const result = normalizeSearxngResult(rawResult);
118
+ if (result) results.push(result);
119
+ if (results.length >= count) break;
120
+ }
121
+ return results;
122
+ }
123
+ async function fetchSearxngResults(params) {
124
+ const url = buildSearxngSearchUrl({
125
+ baseUrl: params.baseUrl,
126
+ query: params.query,
127
+ categories: params.categories,
128
+ language: params.language
129
+ });
130
+ return await (params.endpointMode === "selfHosted" ? withSelfHostedWebSearchEndpoint : withTrustedWebSearchEndpoint)({
131
+ url,
132
+ timeoutSeconds: params.timeoutSeconds,
133
+ init: {
134
+ method: "GET",
135
+ headers: { Accept: "application/json" }
136
+ }
137
+ }, async (response) => {
138
+ if (!response.ok) {
139
+ const detail = (await readResponseText(response, { maxBytes: 64e3 })).text;
140
+ throw new Error(`SearXNG search error (${response.status}): ${detail || response.statusText}`);
141
+ }
142
+ const body = await readResponseText(response, { maxBytes: MAX_RESPONSE_BYTES });
143
+ if (body.truncated) throw new Error("SearXNG response too large.");
144
+ return parseSearxngResponseText(body.text, params.count);
145
+ });
146
+ }
147
+ async function runSearxngSearch(params) {
148
+ const count = resolveSearchCount(params.count, DEFAULT_SEARCH_COUNT);
149
+ const categories = params.categories ?? resolveSearxngCategories(params.config);
150
+ const language = params.language ?? resolveSearxngLanguage(params.config);
151
+ const baseUrl = params.baseUrl ?? resolveSearxngBaseUrl(params.config);
152
+ const timeoutSeconds = resolveTimeoutSeconds(params.timeoutSeconds, DEFAULT_TIMEOUT_SECONDS);
153
+ const cacheTtlMs = resolveCacheTtlMs(params.cacheTtlMinutes, DEFAULT_CACHE_TTL_MINUTES);
154
+ if (!baseUrl) throw new Error("SearXNG base URL is not configured. Set SEARXNG_BASE_URL or configure plugins.entries.searxng.config.webSearch.baseUrl.");
155
+ const endpointMode = await validateSearxngBaseUrl(baseUrl);
156
+ const cacheKey = normalizeCacheKey(JSON.stringify({
157
+ provider: "searxng",
158
+ query: params.query,
159
+ count,
160
+ categories: categories ?? "",
161
+ language: language ?? "",
162
+ baseUrl
163
+ }));
164
+ const cached = readCache(SEARXNG_SEARCH_CACHE, cacheKey);
165
+ if (cached) return {
166
+ ...cached.value,
167
+ cached: true
168
+ };
169
+ const startedAt = Date.now();
170
+ let results = await fetchSearxngResults({
171
+ baseUrl,
172
+ query: params.query,
173
+ categories,
174
+ language,
175
+ timeoutSeconds,
176
+ count,
177
+ endpointMode
178
+ });
179
+ if (results.length === 0 && shouldRetryEmptyCategorySearchWithGeneral(categories)) results = await fetchSearxngResults({
180
+ baseUrl,
181
+ query: params.query,
182
+ categories: "general",
183
+ language,
184
+ timeoutSeconds,
185
+ count,
186
+ endpointMode
187
+ });
188
+ const payload = {
189
+ query: params.query,
190
+ provider: "searxng",
191
+ count: results.length,
192
+ tookMs: Date.now() - startedAt,
193
+ externalContent: {
194
+ untrusted: true,
195
+ source: "web_search",
196
+ provider: "searxng",
197
+ wrapped: true
198
+ },
199
+ results: results.map((result) => ({
200
+ title: wrapWebContent(result.title, "web_search"),
201
+ url: result.url,
202
+ snippet: result.content ? wrapWebContent(result.content, "web_search") : "",
203
+ siteName: resolveSiteName(result.url) || void 0,
204
+ img_src: result.img_src || void 0
205
+ }))
206
+ };
207
+ writeCache(SEARXNG_SEARCH_CACHE, cacheKey, payload, cacheTtlMs);
208
+ return payload;
209
+ }
210
+ const testing = {
211
+ buildSearxngSearchUrl,
212
+ normalizeSearxngResult,
213
+ parseSearxngResponseText,
214
+ shouldRetryEmptyCategorySearchWithGeneral,
215
+ validateSearxngBaseUrl,
216
+ SEARXNG_SEARCH_CACHE
217
+ };
218
+ //#endregion
219
+ export { testing as __testing, testing, runSearxngSearch };
@@ -0,0 +1,80 @@
1
+ import { readPositiveIntegerParam, readStringParam } from "openclaw/plugin-sdk/param-readers";
2
+ import { createWebSearchProviderContractFields } from "openclaw/plugin-sdk/provider-web-search-contract";
3
+ //#region extensions/searxng/src/searxng-search-provider.ts
4
+ const SEARXNG_CREDENTIAL_PATH = "plugins.entries.searxng.config.webSearch.baseUrl";
5
+ let searxngClientModulePromise;
6
+ function loadSearxngClientModule() {
7
+ searxngClientModulePromise ??= import("./searxng-client-BFVWmp9D.js");
8
+ return searxngClientModulePromise;
9
+ }
10
+ const SearxngSearchSchema = {
11
+ type: "object",
12
+ properties: {
13
+ query: {
14
+ type: "string",
15
+ description: "Search query string."
16
+ },
17
+ count: {
18
+ type: "integer",
19
+ description: "Number of results to return (1-10).",
20
+ minimum: 1,
21
+ maximum: 10
22
+ },
23
+ categories: {
24
+ type: "string",
25
+ description: "Optional comma-separated search categories such as general, news, or science."
26
+ },
27
+ language: {
28
+ type: "string",
29
+ description: "Optional language code for results such as en, de, or fr."
30
+ }
31
+ },
32
+ additionalProperties: false
33
+ };
34
+ function createSearxngWebSearchProvider() {
35
+ return {
36
+ id: "searxng",
37
+ label: "SearXNG Search",
38
+ hint: "Self-hosted meta-search with no API key required",
39
+ onboardingScopes: ["text-inference"],
40
+ requiresCredential: true,
41
+ credentialLabel: "SearXNG Base URL",
42
+ envVars: ["SEARXNG_BASE_URL"],
43
+ placeholder: "http://localhost:8080",
44
+ signupUrl: "https://docs.searxng.org/",
45
+ autoDetectOrder: 200,
46
+ credentialPath: SEARXNG_CREDENTIAL_PATH,
47
+ ...createWebSearchProviderContractFields({
48
+ credentialPath: SEARXNG_CREDENTIAL_PATH,
49
+ searchCredential: {
50
+ type: "scoped",
51
+ scopeId: "searxng"
52
+ },
53
+ configuredCredential: {
54
+ pluginId: "searxng",
55
+ field: "baseUrl"
56
+ },
57
+ selectionPluginId: "searxng"
58
+ }),
59
+ credentialNote: ["For the SearXNG JSON API to work, make sure your SearXNG instance", "has the json format enabled in its settings.yml under search.formats."].join("\n"),
60
+ createTool: (ctx) => ({
61
+ description: "Search the web using a self-hosted SearXNG instance. Returns titles, URLs, and snippets.",
62
+ parameters: SearxngSearchSchema,
63
+ execute: async (args) => {
64
+ const { runSearxngSearch } = await loadSearxngClientModule();
65
+ return await runSearxngSearch({
66
+ config: ctx.config,
67
+ query: readStringParam(args, "query", { required: true }),
68
+ count: readPositiveIntegerParam(args, "count", {
69
+ max: 10,
70
+ message: "count must be an integer from 1 to 10."
71
+ }),
72
+ categories: readStringParam(args, "categories"),
73
+ language: readStringParam(args, "language")
74
+ });
75
+ }
76
+ })
77
+ };
78
+ }
79
+ //#endregion
80
+ export { createSearxngWebSearchProvider as t };
@@ -0,0 +1,2 @@
1
+ import { t as createSearxngWebSearchProvider } from "./searxng-search-provider-Da4Go6Vh.js";
2
+ export { createSearxngWebSearchProvider };
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "@openclaw/searxng-plugin",
3
+ "version": "2026.6.11",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "@openclaw/searxng-plugin",
9
+ "version": "2026.6.11"
10
+ }
11
+ }
12
+ }
@@ -0,0 +1,45 @@
1
+ {
2
+ "id": "searxng",
3
+ "icon": "https://cdn.simpleicons.org/searxng",
4
+ "activation": {
5
+ "onStartup": false
6
+ },
7
+ "uiHints": {
8
+ "webSearch.baseUrl": {
9
+ "label": "SearXNG Base URL",
10
+ "help": "Base URL of your SearXNG instance, such as http://localhost:8080 or https://search.example.com/searxng."
11
+ },
12
+ "webSearch.categories": {
13
+ "label": "SearXNG Categories",
14
+ "help": "Optional comma-separated categories such as general, news, or science."
15
+ },
16
+ "webSearch.language": {
17
+ "label": "SearXNG Language",
18
+ "help": "Optional language code for results such as en, de, or fr."
19
+ }
20
+ },
21
+ "contracts": {
22
+ "webSearchProviders": ["searxng"]
23
+ },
24
+ "configSchema": {
25
+ "type": "object",
26
+ "additionalProperties": false,
27
+ "properties": {
28
+ "webSearch": {
29
+ "type": "object",
30
+ "additionalProperties": false,
31
+ "properties": {
32
+ "baseUrl": {
33
+ "type": ["string", "object"]
34
+ },
35
+ "categories": {
36
+ "type": "string"
37
+ },
38
+ "language": {
39
+ "type": "string"
40
+ }
41
+ }
42
+ }
43
+ }
44
+ }
45
+ }
package/package.json CHANGED
@@ -1,13 +1,51 @@
1
1
  {
2
2
  "name": "@openclaw/searxng-plugin",
3
- "version": "0.0.0",
4
- "description": "Bootstrap reservation for the OpenClaw SearXNG plugin package.",
3
+ "version": "2026.6.11",
4
+ "description": "OpenClaw SearXNG plugin",
5
+ "type": "module",
6
+ "openclaw": {
7
+ "extensions": [
8
+ "./index.ts"
9
+ ],
10
+ "install": {
11
+ "clawhubSpec": "clawhub:@openclaw/searxng-plugin",
12
+ "npmSpec": "@openclaw/searxng-plugin",
13
+ "defaultChoice": "npm",
14
+ "minHostVersion": ">=2026.6.9",
15
+ "allowInvalidConfigRecovery": true
16
+ },
17
+ "compat": {
18
+ "pluginApi": ">=2026.6.11"
19
+ },
20
+ "build": {
21
+ "openclawVersion": "2026.6.11",
22
+ "bundledDist": false
23
+ },
24
+ "release": {
25
+ "publishToClawHub": true,
26
+ "publishToNpm": true
27
+ },
28
+ "runtimeExtensions": [
29
+ "./dist/index.js"
30
+ ]
31
+ },
5
32
  "repository": {
6
33
  "type": "git",
7
- "url": "git+https://github.com/openclaw/openclaw.git"
34
+ "url": "https://github.com/openclaw/openclaw"
35
+ },
36
+ "files": [
37
+ "dist/**",
38
+ "openclaw.plugin.json",
39
+ "npm-shrinkwrap.json",
40
+ "README.md"
41
+ ],
42
+ "peerDependencies": {
43
+ "openclaw": ">=2026.6.11"
44
+ },
45
+ "peerDependenciesMeta": {
46
+ "openclaw": {
47
+ "optional": true
48
+ }
8
49
  },
9
- "license": "Apache-2.0",
10
- "publishConfig": {
11
- "access": "public"
12
- }
50
+ "bundledDependencies": []
13
51
  }