@wrongstack/plugins 0.275.1 → 0.276.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.
@@ -1,283 +1,13 @@
1
- import { lookup } from 'dns/promises';
2
- import { isIPv4, isIPv6 } from 'net';
3
- import { isPrivateIPv4, isPrivateIPv6 } from '@wrongstack/core';
4
-
5
1
  // src/web-search/index.ts
6
2
  var API_VERSION = "^0.1.10";
7
- async function duckduckgoSearch(query, numResults) {
8
- const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}&kl=us-en`;
9
- const resp = await fetch(url, {
10
- headers: {
11
- "User-Agent": "Mozilla/5.0 (compatible; WrongStack/1.0; +https://wrongstack.com)"
12
- }
13
- });
14
- if (!resp.ok) throw new Error(`DuckDuckGo search failed: ${resp.status}`);
15
- const html = await resp.text();
16
- const results = [];
17
- const resultRe = /<a\b[^>]*class="[^"]*result__a[^"]*"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>([\s\S]*?)(?=<a\b[^>]*class="[^"]*result__a[^"]*"|<\/body>|$)/gi;
18
- let m;
19
- while ((m = resultRe.exec(html)) !== null && results.length < numResults) {
20
- const url2 = normalizeDuckDuckGoUrl(m[1]);
21
- if (!url2) continue;
22
- const title = htmlToText(m[2] ?? "");
23
- const snippet = extractDuckDuckGoSnippet(m[3] ?? "");
24
- results.push({
25
- url: url2,
26
- title,
27
- snippet,
28
- score: 1,
29
- source: "duckduckgo",
30
- cached: false
31
- });
32
- }
33
- if (results.length === 0 && /result__a|result__snippet|result__body|anomaly-modal|captcha/i.test(html)) {
34
- throw new Error("DuckDuckGo response format was not recognized or was blocked");
35
- }
36
- return results;
37
- }
38
- function normalizeDuckDuckGoUrl(rawUrl) {
39
- if (!rawUrl) return null;
40
- let url = htmlToText(rawUrl);
41
- if (url.startsWith("//duckduckgo.com/l/")) url = `https:${url}`;
42
- if (url.startsWith("/l/")) url = new URL(url, "https://duckduckgo.com").toString();
43
- try {
44
- const parsed = new URL(url);
45
- const redirected = parsed.searchParams.get("uddg");
46
- if (redirected) return redirected;
47
- return parsed.toString();
48
- } catch {
49
- return null;
50
- }
51
- }
52
- function htmlToText(html) {
53
- return html.replace(/<[^>]+>/g, "").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/\s+/g, " ").trim();
54
- }
55
- function extractDuckDuckGoSnippet(html) {
56
- const snippetRe = /<(?:a|div)\b[^>]*class="[^"]*(?:result__snippet|result__body)[^"]*"[^>]*>([\s\S]*?)<\/(?:a|div)>/i;
57
- const snippetMatch = snippetRe.exec(html);
58
- return htmlToText(snippetMatch?.[1] ?? "");
59
- }
60
- function assertSafeIp(ip) {
61
- if (isIPv4(ip) && isPrivateIPv4(ip)) {
62
- throw new Error(`Blocked private/loopback address: ${ip}`);
63
- }
64
- if (isIPv6(ip) && isPrivateIPv6(ip)) {
65
- throw new Error(`Blocked private/loopback address: ${ip}`);
66
- }
67
- }
68
- async function assertSafeUrl(rawUrl) {
69
- const u = new URL(rawUrl);
70
- if (u.protocol !== "http:" && u.protocol !== "https:") {
71
- throw new Error(`Unsupported protocol: ${u.protocol}`);
72
- }
73
- const host = u.hostname.startsWith("[") && u.hostname.endsWith("]") ? u.hostname.slice(1, -1) : u.hostname;
74
- if (host === "localhost" || host.endsWith(".localhost") || host === "" || host === "0.0.0.0") {
75
- throw new Error("Blocked localhost target");
76
- }
77
- if (isIPv4(host) || isIPv6(host)) {
78
- assertSafeIp(host);
79
- return;
80
- }
81
- let addrs;
82
- try {
83
- addrs = await lookup(host, { all: true });
84
- } catch {
85
- throw new Error(`Could not resolve host: ${host}`);
86
- }
87
- for (const { address } of addrs) assertSafeIp(address);
88
- }
89
- async function fetchUrl(url, format) {
90
- const MAX_REDIRECTS = 5;
91
- let currentUrl = url;
92
- let resp;
93
- for (let i = 0; i <= MAX_REDIRECTS; i++) {
94
- await assertSafeUrl(currentUrl);
95
- resp = await fetch(currentUrl, {
96
- redirect: "manual",
97
- headers: {
98
- "User-Agent": "Mozilla/5.0 (compatible; WrongStack/1.0; +https://wrongstack.com)",
99
- Accept: format === "text" ? "text/plain" : "text/html"
100
- }
101
- });
102
- if (resp.status >= 300 && resp.status < 400) {
103
- const loc = resp.headers.get("location");
104
- if (!loc) break;
105
- currentUrl = new URL(loc, currentUrl).toString();
106
- if (i === MAX_REDIRECTS) throw new Error("Too many redirects");
107
- continue;
108
- }
109
- break;
110
- }
111
- if (!resp) throw new Error(`Failed to fetch ${url}`);
112
- if (!resp.ok) throw new Error(`Failed to fetch ${url}: ${resp.status} ${resp.statusText}`);
113
- if (format === "text") {
114
- return resp.text();
115
- }
116
- let html = await resp.text();
117
- html = html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "");
118
- html = html.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "");
119
- html = html.replace(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi, (_, t) => `
120
- ## ${t.trim()}
121
- `);
122
- html = html.replace(/<p[^>]*>([\s\S]*?)<\/p>/gi, (_, t) => `${t.trim()}
123
-
124
- `);
125
- html = html.replace(/<br\s*\/?>/gi, "\n");
126
- html = html.replace(/<[^>]+>/g, "");
127
- html = html.replace(/&amp;/g, "&");
128
- html = html.replace(/&lt;/g, "<");
129
- html = html.replace(/&gt;/g, ">");
130
- html = html.replace(/&quot;/g, '"');
131
- html = html.replace(/&#39;/g, "'");
132
- html = html.replace(/\n{3,}/g, "\n\n");
133
- return html.trim().slice(0, 5e4);
134
- }
135
- function scoreResults(results, query) {
136
- const terms = query.toLowerCase().split(/\s+/);
137
- return results.map((r) => {
138
- const titleLower = r.title.toLowerCase();
139
- const snippetLower = r.snippet.toLowerCase();
140
- let score = r.score;
141
- for (const term of terms) {
142
- if (titleLower.includes(term)) score += 2;
143
- if (snippetLower.includes(term)) score += 1;
144
- }
145
- return { ...r, score };
146
- }).sort((a, b) => b.score - a.score);
147
- }
148
3
  var plugin = {
149
4
  name: "web-search",
150
- version: "0.1.0",
151
- description: "Cached web search with deduplication and relevance ranking",
5
+ version: "0.3.0",
6
+ description: "Retired \u2014 capabilities merged into built-in search and fetch tools",
152
7
  apiVersion: API_VERSION,
153
- capabilities: { tools: true, pipelines: ["request"] },
154
- defaultConfig: {
155
- cacheTtlMs: 3e5,
156
- maxResults: 10,
157
- userAgent: "WrongStack/1.0"
158
- },
159
- configSchema: {
160
- type: "object",
161
- properties: {
162
- cacheTtlMs: { type: "number", default: 3e5 },
163
- maxResults: { type: "number", default: 10 },
164
- userAgent: { type: "string", default: "WrongStack/1.0" }
165
- }
166
- },
8
+ capabilities: { tools: true },
167
9
  setup(api) {
168
- const cache = /* @__PURE__ */ new Map();
169
- const cacheTtlMs = api.config.extensions?.["web-search"]?.["cacheTtlMs"] ?? 3e5;
170
- const maxResults = api.config.extensions?.["web-search"]?.["maxResults"] ?? 10;
171
- api.tools.register({
172
- name: "web_search",
173
- description: "Search the web using DuckDuckGo with automatic caching and deduplication. Results are cached for faster subsequent queries.",
174
- inputSchema: {
175
- type: "object",
176
- properties: {
177
- query: { type: "string", description: "Search query" },
178
- numResults: { type: "number", default: 10, description: "Maximum number of results" },
179
- source: { type: "string", enum: ["duckduckgo"], default: "duckduckgo", description: "Search engine" },
180
- skipCache: { type: "boolean", default: false, description: "Skip cache and force fresh search" }
181
- },
182
- required: ["query"]
183
- },
184
- permission: "auto",
185
- mutating: true,
186
- async execute(input) {
187
- const query = input["query"];
188
- if (!query || typeof query !== "string" || query.trim() === "") {
189
- return { ok: false, error: "query is required and must be a non-empty string", results: [] };
190
- }
191
- const numResults = input["numResults"] ?? maxResults;
192
- const skipCache = input["skipCache"] ?? false;
193
- if (!skipCache) {
194
- const cached = cache.get(query);
195
- if (cached && Date.now() - cached.timestamp < cacheTtlMs) {
196
- const results = cached.results.map((r) => ({ ...r, cached: true }));
197
- api.metrics.counter("cache_hit", 1, { query: query.slice(0, 20) });
198
- return {
199
- ok: true,
200
- query,
201
- cached: true,
202
- results: results.slice(0, numResults),
203
- count: results.length
204
- };
205
- }
206
- }
207
- api.metrics.counter("cache_miss", 1, { query: query.slice(0, 20) });
208
- const seenUrls = /* @__PURE__ */ new Set();
209
- let rawResults;
210
- try {
211
- rawResults = await duckduckgoSearch(query, numResults * 2);
212
- } catch (err) {
213
- const msg = err instanceof Error ? err.message : String(err);
214
- return { ok: false, error: `Search failed: ${msg}`, results: [] };
215
- }
216
- const deduplicated = [];
217
- for (const r of rawResults) {
218
- const noQuery = r.url.split("?")[0] ?? r.url;
219
- const normalized = noQuery.split("#")[0] ?? r.url;
220
- if (!seenUrls.has(normalized) && r.url.startsWith("http")) {
221
- seenUrls.add(normalized);
222
- deduplicated.push(r);
223
- }
224
- }
225
- const ranked = scoreResults(deduplicated, query);
226
- cache.set(query, { results: ranked, timestamp: Date.now() });
227
- const now = Date.now();
228
- for (const [key, entry] of cache.entries()) {
229
- if (now - entry.timestamp > cacheTtlMs * 2) cache.delete(key);
230
- }
231
- return {
232
- ok: true,
233
- query,
234
- cached: false,
235
- results: ranked.slice(0, numResults),
236
- count: ranked.length
237
- };
238
- }
239
- });
240
- api.tools.register({
241
- name: "web_fetch",
242
- description: "Fetch a URL and return its content as markdown or plain text.",
243
- inputSchema: {
244
- type: "object",
245
- properties: {
246
- url: { type: "string", format: "uri", description: "URL to fetch" },
247
- format: { type: "string", enum: ["markdown", "text"], default: "markdown" }
248
- },
249
- required: ["url"]
250
- },
251
- permission: "confirm",
252
- mutating: false,
253
- async execute(input) {
254
- const rawUrl = input["url"];
255
- if (!rawUrl || typeof rawUrl !== "string") {
256
- return { ok: false, error: "url is required and must be a string" };
257
- }
258
- const url = rawUrl;
259
- const format = input["format"] ?? "markdown";
260
- if (!url.startsWith("http://") && !url.startsWith("https://")) {
261
- return { ok: false, error: "URL must start with http:// or https://" };
262
- }
263
- let content;
264
- try {
265
- content = await fetchUrl(url, format);
266
- } catch (err) {
267
- const msg = err instanceof Error ? err.message : String(err);
268
- return { ok: false, error: msg };
269
- }
270
- return {
271
- ok: true,
272
- url,
273
- format,
274
- contentLength: content.length,
275
- content: content.slice(0, 2e4),
276
- truncated: content.length > 2e4
277
- };
278
- }
279
- });
280
- api.log.info("web-search plugin loaded", { version: "0.1.0", cacheTtlMs });
10
+ api.log.info("web-search plugin retired \u2014 use the built-in search and fetch tools");
281
11
  }
282
12
  };
283
13
  var web_search_default = plugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrongstack/plugins",
3
- "version": "0.275.1",
3
+ "version": "0.276.3",
4
4
  "description": "Official WrongStack plugin collection — auto-doc, git-autocommit, shell-check, cost-tracker, file-watcher, web-search, json-path, cron, template-engine, semver-bump",
5
5
  "license": "MIT",
6
6
  "author": "ECOSTACK TECHNOLOGY OÜ",
@@ -63,7 +63,7 @@
63
63
  "vitest": "^4.1.8"
64
64
  },
65
65
  "dependencies": {
66
- "@wrongstack/core": "0.275.1"
66
+ "@wrongstack/core": "0.276.3"
67
67
  },
68
68
  "scripts": {
69
69
  "build": "tsup",