dsh-free-search 0.4.9 → 0.4.10

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
@@ -36,7 +36,7 @@ dsh 默认的搜索 provider 依赖 DeepSeek 官方 API key(`DEEPSEEK_API_KEY`
36
36
  - **统一引擎回退** —— 任何引擎失败(付费/免费,缺 key/401/限流/网络)自动轮流尝试下一个引擎:首选引擎 → 其他引擎(exa/tavily/keenable 无 key 也会尝试,因为它们自带 keyless 免费额度)→ 剩余免费引擎,搜索永不直接失败;结果顶部注明实际生效的引擎(如 `Note: perplexity unavailable or failed, using exa.`)
37
37
  - **时间过滤** —— `advanced_search` 工具支持 `timeRange`:固定档、自定义相对值、绝对日期三种形式(详见下方逻辑说明)
38
38
  - **系统提示词注入** —— agent 知道当前用哪个引擎、哪些需要 key
39
- - **版本号 + 检查更新** —— 设置卡片显示当前版本(v0.4.9),"检查更新"按钮直连 npm registry 对比最新版,有新版本时提示并可一键跳转
39
+ - **版本号 + 检查更新** —— 设置卡片显示当前版本(v0.4.10),"检查更新"按钮直连 npm registry 对比最新版,有新版本时提示并可一键跳转
40
40
  - **结果缓存** —— 相同查询(含引擎/时间过滤参数)5 分钟内命中缓存(LRU 50 条),防免费引擎限流、省付费额度;时长可在设置页 0-5 分钟自由配置(0 关闭)
41
41
  - **免费标注** —— 设置页中免费引擎带绿色 `FREE` 徽章,付费引擎带橙色 `API KEY` 徽章
42
42
  - **网页抓取(web_fetch)** —— 让 agent 抓取网页内容(官方 `dsh-web-fetch-http` provider,纯 JS,零额外依赖)
@@ -271,7 +271,7 @@ This plugin provides multiple free search engines with automatic fallback, compl
271
271
  - **Unified Engine Fallback** — Any engine failure (paid or free, missing key, 401, rate limit, network error) automatically tries the next engine: the configured engine first, then other engines (exa/tavily/keenable are tried even without a key because they have built-in keyless quota), then the remaining free engines (Bing/AnySearch etc.) — with a note attached to the results naming the engine that actually served them (e.g. `Note: perplexity unavailable or failed, using exa.`). Search never fails outright.
272
272
  - **Time Filtering** — The `advanced_search` tool supports `timeRange`: fixed tiers, custom relative values, or an absolute date (details below)
273
273
  - **System Prompt Injection** — The agent is aware of the currently active engine and which engines require API keys
274
- - **Version + Update Check** — The settings card shows the current version (v0.4.9), and a "Check update" button queries the npm registry to compare against the latest release, prompting a one-click jump when a newer version exists
274
+ - **Version + Update Check** — The settings card shows the current version (v0.4.10), and a "Check update" button queries the npm registry to compare against the latest release, prompting a one-click jump when a newer version exists
275
275
  - **Result Caching** — Identical queries (same engine / time-filter args) hit an LRU cache (50 entries) for up to 5 minutes, protecting free engines from rate-limiting and saving paid quota; the TTL is configurable from 0-5 minutes in the settings UI (0 disables caching)
276
276
  - **Visual Badges** — Free engines feature a green `FREE` badge, while paid engines show an orange `API KEY` badge in the settings UI
277
277
  - **Webpage Fetching (`web_fetch`)** — Allows the agent to read full webpage contents (official `dsh-web-fetch-http` provider, pure JS, zero extra dependencies)
package/lib/client.js CHANGED
@@ -142,7 +142,7 @@ window.__ModuleLoader__.load({
142
142
  };
143
143
  const tt = (lang) => I18N[lang === "en" ? "en" : "zh"];
144
144
  // 当前插件版本(与 lib/index.js 的 PLUGIN_VERSION 保持一致)
145
- const PLUGIN_VERSION = "0.4.9";
145
+ const PLUGIN_VERSION = "0.4.10";
146
146
  const ENGINES = [
147
147
  { id: "ddg", label: "DuckDuckGo · HTML", badge: "FREE", link: "https://duckduckgo.com" },
148
148
  { id: "ddg-lite", label: "DuckDuckGo · Lite", badge: "FREE", link: "https://duckduckgo.com" },
@@ -201,7 +201,7 @@ window.__ModuleLoader__.load({
201
201
  const [keenableKey, setKeenableKey] = react.useState("");
202
202
  const [perplexityKey, setPerplexityKey] = react.useState("");
203
203
  const [deepseekKey, setDeepseekKey] = react.useState("");
204
- const [platforms, setPlatforms] = react.useState(["github", "v2ex", "bilibili"]);
204
+ const [platforms, setPlatforms] = react.useState(["github", "v2ex", "bilibili", "reddit"]);
205
205
  const [cacheTtl, setCacheTtl] = react.useState(5);
206
206
  const [keysConfigured, setKeysConfigured] = react.useState({});
207
207
  const [lang, setLang] = react.useState("zh");
@@ -227,7 +227,7 @@ window.__ModuleLoader__.load({
227
227
  setKeenableKey(v.keenableApiKey ?? "");
228
228
  setPerplexityKey(v.perplexityApiKey ?? "");
229
229
  setDeepseekKey(v.deepseekApiKey ?? "");
230
- setPlatforms(Array.isArray(v.platforms) && v.platforms.length > 0 ? v.platforms : ["github", "v2ex", "bilibili"]);
230
+ setPlatforms(Array.isArray(v.platforms) && v.platforms.length > 0 ? v.platforms : ["github", "v2ex", "bilibili", "reddit"]);
231
231
  setCacheTtl(v.cacheTtl === undefined ? 5 : Math.min(Math.max(Number(v.cacheTtl) ?? 5, 0), 5));
232
232
  // secrets 字段标记哪些 key 已配置(值被脱敏,仅显示"已配置")
233
233
  const configured = {};
@@ -515,7 +515,7 @@ react_jsx_runtime.jsx("div", {
515
515
  react_jsx_runtime.jsx("div", {
516
516
  className: "dshfs-platforms",
517
517
  children: [
518
- ["github", "GitHub"], ["v2ex", "V2EX"], ["bilibili", "Bilibili"],
518
+ ["github", "GitHub"], ["v2ex", "V2EX"], ["bilibili", "Bilibili"], ["reddit", "Reddit"],
519
519
  ].map(([id, label]) =>
520
520
  react_jsx_runtime.jsx("label", {
521
521
  className: "dshfs-platform",
package/lib/index.js CHANGED
@@ -18,7 +18,7 @@ const FREE_ENGINES = ["ddg", "ddg-lite", "bing", "searxng", "anysearch"];
18
18
  const ALL_ENGINES = ["ddg", "ddg-lite", "bing", "searxng", "anysearch", "exa", "tavily", "keenable", "perplexity", "deepseek-official"];
19
19
 
20
20
  // 当前插件版本(发布时与 package.json 同步)
21
- const PLUGIN_VERSION = "0.4.9";
21
+ const PLUGIN_VERSION = "0.4.10";
22
22
  // 检查更新的 npm registry 元数据地址(dsh-free-search 是 npmjs 上的公开包)
23
23
  const NPM_REGISTRY_URL = "https://registry.npmjs.org/dsh-free-search/latest";
24
24
  const PLUGIN_NPM_URL = "https://www.npmjs.com/package/dsh-free-search";
@@ -115,6 +115,8 @@ function decodeEntities(text) {
115
115
 
116
116
  //#region 结果缓存(防限流/省额度,LRU 50 条,TTL 可配置 0-5 分钟)
117
117
  const CACHE_MAX_ENTRIES = 50;
118
+ // fallback 条目(实际引擎 ≠ 首选引擎时)TTL = 配置 TTL 的 1/5(默认 5 分钟 → 60s):
119
+ // 首选引擎恢复后最多 1 分钟即可拿到新结果,避免回退结果被完整 TTL 钉死;首选成功条目仍用完整 TTL。
118
120
 
119
121
  function buildCacheKey(query, maxResults, timeRangeLabel, preferred) {
120
122
  return [query ?? "", maxResults ?? 5, timeRangeLabel ?? "", preferred].join("\u0000");
@@ -302,7 +304,8 @@ async function searchSearxng(query, maxResults, options, signal) {
302
304
  const instances = options?.searxngInstances?.length
303
305
  ? options.searxngInstances
304
306
  : SEARXNG_INSTANCES;
305
- let lastError = null;
307
+ // 聚合所有实例的失败原因,避免只显示最后一个实例的错误
308
+ const errors = [];
306
309
  for (const base of instances) {
307
310
  try {
308
311
  const params = new URLSearchParams({ q: query, format: "json" });
@@ -322,12 +325,12 @@ async function searchSearxng(query, maxResults, options, signal) {
322
325
  clearTimeout(timer);
323
326
  signal?.removeEventListener("abort", onAbort);
324
327
  if (!response.ok) {
325
- lastError = new Error(`${base} HTTP ${response.status}`);
328
+ errors.push(`${base}: HTTP ${response.status}`);
326
329
  continue;
327
330
  }
328
331
  const data = await response.json().catch(() => null);
329
332
  if (!data || !Array.isArray(data.results)) {
330
- lastError = new Error(`${base} invalid JSON`);
333
+ errors.push(`${base}: invalid JSON`);
331
334
  continue;
332
335
  }
333
336
  const sources = data.results
@@ -340,12 +343,16 @@ async function searchSearxng(query, maxResults, options, signal) {
340
343
  if (sources.length > 0) {
341
344
  return { sources: uniqueSources(sources, maxResults ?? 10), truncated: false };
342
345
  }
343
- lastError = new Error(`${base} 0 results`);
346
+ errors.push(`${base}: 0 results`);
344
347
  } catch (error) {
345
- lastError = error;
348
+ const message = error instanceof Error ? error.message : String(error);
349
+ errors.push(`${base}: ${message}`);
346
350
  }
347
351
  }
348
- throw lastError ?? new Error("all SearXNG instances failed");
352
+ // 空实例列表兜底:避免 "all SearXNG instances failed: " 尾巴悬空
353
+ const detail = errors.length > 0 ? errors.join(", ") : "no instances configured";
354
+ // Note 会引用这个错误消息,截断避免 6 实例全挂时刷屏
355
+ throw new Error(`all SearXNG instances failed: ${detail.slice(0, 300)}`);
349
356
  }
350
357
  //#endregion
351
358
 
@@ -450,7 +457,8 @@ async function searchExaMCP(query, maxResults, signal) {
450
457
  url,
451
458
  ...(title ? { title } : {}),
452
459
  ...(highlights ? { snippet: highlights.slice(0, 300) } : {}),
453
- ...(published ? { publishedAt: published } : {}),
460
+ // 只保留日期形态(ISO 或 YYYY-MM-DD),过滤 "N/A" 等占位符
461
+ ...(published && /^\d{4}-\d{2}-\d{2}/.test(published) ? { publishedAt: published } : {}),
454
462
  });
455
463
  }
456
464
  return { sources, truncated: false };
@@ -462,6 +470,7 @@ const PLATFORMS = {
462
470
  github: { name: "GitHub" },
463
471
  v2ex: { name: "V2EX" },
464
472
  bilibili: { name: "Bilibili" },
473
+ reddit: { name: "Reddit" },
465
474
  };
466
475
 
467
476
  async function searchGithub(query, maxResults, signal) {
@@ -700,7 +709,8 @@ function extractKeenableSources(text, maxResults) {
700
709
  url,
701
710
  ...(title ? { title } : {}),
702
711
  ...(snippets ? { snippet: snippets.slice(0, 300) } : {}),
703
- ...(published ? { publishedAt: published } : {}),
712
+ // 与 Exa MCP 一致:只保留日期形态,过滤 "N/A" 等占位符
713
+ ...(published && /^\d{4}-\d{2}-\d{2}/.test(published) ? { publishedAt: published } : {}),
704
714
  });
705
715
  }
706
716
  return uniqueSources(sources, maxResults ?? 10);
@@ -799,6 +809,7 @@ async function searchKeenable(query, maxResults, apiKey, timeRange, signal) {
799
809
 
800
810
  async function searchPerplexity(query, maxResults, apiKey, signal) {
801
811
  if (!apiKey) throw new Error("Perplexity search requires PERPLEXITY_API_KEY");
812
+ // 内置 20s 超时(与外部 signal 组合):调用方不传 signal 时也不会永久卡住
802
813
  const response = await fetch("https://api.perplexity.ai/chat/completions", {
803
814
  method: "POST",
804
815
  redirect: "error",
@@ -812,7 +823,7 @@ async function searchPerplexity(query, maxResults, apiKey, signal) {
812
823
  max_tokens: 1024,
813
824
  messages: [{ role: "user", content: query }],
814
825
  }),
815
- ...(signal !== undefined ? { signal } : {}),
826
+ signal: AbortSignal.any([...(signal !== undefined ? [signal] : []), AbortSignal.timeout(20000)]),
816
827
  });
817
828
  if (!response.ok) {
818
829
  const detail = await response.text().catch(() => "");
@@ -834,6 +845,7 @@ async function searchPerplexity(query, maxResults, apiKey, signal) {
834
845
 
835
846
  async function searchDeepSeekOfficial(query, maxResults, apiKey, signal) {
836
847
  if (!apiKey) throw new Error("DeepSeek search requires DEEPSEEK_API_KEY");
848
+ // 内置 20s 超时(与外部 signal 组合):调用方不传 signal 时也不会永久卡住
837
849
  const response = await fetch("https://api.deepseek.com/anthropic/v1/messages", {
838
850
  method: "POST",
839
851
  redirect: "error",
@@ -856,7 +868,7 @@ async function searchDeepSeekOfficial(query, maxResults, apiKey, signal) {
856
868
  ],
857
869
  tools: [{ type: "web_search_20250305", name: "web_search", max_uses: 1 }],
858
870
  }),
859
- ...(signal !== undefined ? { signal } : {}),
871
+ signal: AbortSignal.any([...(signal !== undefined ? [signal] : []), AbortSignal.timeout(20000)]),
860
872
  });
861
873
  if (!response.ok) {
862
874
  const detail = await response.text().catch(() => "");
@@ -1140,7 +1152,7 @@ const Config = z.object({
1140
1152
  region: z.string(),
1141
1153
  bingMarket: z.string().default("zh-CN"),
1142
1154
  searxngInstances: z.array(z.string()),
1143
- platforms: z.array(z.string()).default(["github", "v2ex", "bilibili"]),
1155
+ platforms: z.array(z.string()).default(["github", "v2ex", "bilibili", "reddit"]),
1144
1156
  exaApiKey: z.string().role("secret"),
1145
1157
  tavilyApiKey: z.string().role("secret"),
1146
1158
  keenableApiKey: z.string().role("secret"),
@@ -1182,6 +1194,10 @@ function apply(ctx, config) {
1182
1194
  },
1183
1195
  // 单 query 结果缓存:key=query+maxResults+timeRangeLabel+preferred,Map 天然 LRU
1184
1196
  async search(request, signal) {
1197
+ // 公共咽喉校验:web_search / advanced_search / raw-search 三条路径都经过这里
1198
+ if (request === null || typeof request !== "object" || typeof request.query !== "string" || request.query.trim().length === 0) {
1199
+ throw new Error("query is required");
1200
+ }
1185
1201
  const cfg = current();
1186
1202
  // 首选引擎:free_search 工具显式指定(request.engine)优先于设置(cfg.provider)
1187
1203
  const preferred =
@@ -1205,9 +1221,8 @@ function apply(ctx, config) {
1205
1221
  if (signal?.aborted) throw new Error("search aborted");
1206
1222
  searchCache.delete(cacheKey);
1207
1223
  searchCache.set(cacheKey, hit);
1208
- // 浅拷贝 + 私有标记:调用方改 sources 数组不影响缓存(数组元素仍共享引用,
1209
- // 但下游 advanced_search/raw-search 都会重新 map 构造,无污染风险)
1210
- return { ...hit.value, _cache: "hit" };
1224
+ // 浅拷贝 + 私有标记:sources 数组也复制一层,彻底隔离缓存对象(调用方 push/改元素不影响缓存)
1225
+ return { ...hit.value, sources: hit.value.sources?.slice(), _cache: "hit" };
1211
1226
  }
1212
1227
  if (hit) searchCache.delete(cacheKey);
1213
1228
  }
@@ -1243,35 +1258,44 @@ function apply(ctx, config) {
1243
1258
  let usedEngine = null;
1244
1259
  // 首选引擎若被尝试后失败,记录失败详情(用于 Note)
1245
1260
  let preferredFailure = null;
1261
+ // 总超时预算:串行回退时限制整条引擎链的总时长,防止各引擎超时累加达分钟级
1262
+ const BUDGET_MS = 30000;
1263
+ const deadline = Date.now() + BUDGET_MS;
1246
1264
  for (const engine of chain) {
1265
+ const remaining = deadline - Date.now();
1266
+ if (remaining <= 0) {
1267
+ throw new Error(`search timed out after ${BUDGET_MS / 1000}s`);
1268
+ }
1269
+ // 组合外部取消 signal + 剩余预算超时:官方 web_search 取消、引擎超时、总预算都能触发
1270
+ const effSignal = AbortSignal.any([...(signal !== undefined ? [signal] : []), AbortSignal.timeout(remaining)]);
1247
1271
  try {
1248
1272
  let result;
1249
1273
  if (engine === "ddg") {
1250
- result = await searchDdgHtml(request.query, request.maxResults, { ...cfg, timeRange }, signal);
1274
+ result = await searchDdgHtml(request.query, request.maxResults, { ...cfg, timeRange }, effSignal);
1251
1275
  } else if (engine === "ddg-lite") {
1252
- result = await searchDdgLite(request.query, request.maxResults, { ...cfg, timeRange }, signal);
1276
+ result = await searchDdgLite(request.query, request.maxResults, { ...cfg, timeRange }, effSignal);
1253
1277
  } else if (engine === "bing") {
1254
- result = await searchBing(request.query, request.maxResults, cfg, signal);
1278
+ result = await searchBing(request.query, request.maxResults, cfg, effSignal);
1255
1279
  } else if (engine === "searxng") {
1256
- result = await searchSearxng(request.query, request.maxResults, { ...cfg, timeRange }, signal);
1280
+ result = await searchSearxng(request.query, request.maxResults, { ...cfg, timeRange }, effSignal);
1257
1281
  } else if (engine === "anysearch") {
1258
- result = await searchAnysearch(request.query, request.maxResults, signal);
1282
+ result = await searchAnysearch(request.query, request.maxResults, effSignal);
1259
1283
  } else if (engine === "exa") {
1260
1284
  // exa:有 key 走 REST,无 key 走 keyless MCP(免费)
1261
1285
  const key = await resolveApiKey("EXA_API_KEY", "exaApiKey");
1262
1286
  if (key) {
1263
- result = await searchExa(request.query, request.maxResults, key, timeRange, signal);
1287
+ result = await searchExa(request.query, request.maxResults, key, timeRange, effSignal);
1264
1288
  } else {
1265
- result = await searchExaMCP(request.query, request.maxResults, signal);
1289
+ result = await searchExaMCP(request.query, request.maxResults, effSignal);
1266
1290
  }
1267
1291
  } else if (engine === "tavily") {
1268
1292
  // tavily:有 key 走账号档,无 key 走 keyless(免费匿名额度)
1269
1293
  const key = await resolveApiKey("TAVILY_API_KEY", "tavilyApiKey");
1270
- result = await searchTavily(request.query, request.maxResults, key, timeRange, signal);
1294
+ result = await searchTavily(request.query, request.maxResults, key, timeRange, effSignal);
1271
1295
  } else if (engine === "keenable") {
1272
1296
  // keenable:有 key 走 REST,无 key 走 keyless MCP(免费)
1273
1297
  const key = await resolveApiKey("KEENABLE_API_KEY", "keenableApiKey");
1274
- result = await searchKeenable(request.query, request.maxResults, key, timeRange, signal);
1298
+ result = await searchKeenable(request.query, request.maxResults, key, timeRange, effSignal);
1275
1299
  } else if (engine === "perplexity") {
1276
1300
  const key = await resolveApiKey("PERPLEXITY_API_KEY", "perplexityApiKey");
1277
1301
  if (!key) {
@@ -1280,7 +1304,7 @@ function apply(ctx, config) {
1280
1304
  logger.warn(`free-search: engine "${engine}" skipped (no key), trying next engine`);
1281
1305
  continue; // 无 key 跳过
1282
1306
  }
1283
- result = await searchPerplexity(request.query, request.maxResults, key, signal);
1307
+ result = await searchPerplexity(request.query, request.maxResults, key, effSignal);
1284
1308
  } else if (engine === "deepseek-official") {
1285
1309
  const key = await resolveApiKey("DEEPSEEK_API_KEY", "deepseekApiKey");
1286
1310
  if (!key) {
@@ -1289,7 +1313,7 @@ function apply(ctx, config) {
1289
1313
  logger.warn(`free-search: engine "${engine}" skipped (no key), trying next engine`);
1290
1314
  continue; // 无 key 跳过
1291
1315
  }
1292
- result = await searchDeepSeekOfficial(request.query, request.maxResults, key, signal);
1316
+ result = await searchDeepSeekOfficial(request.query, request.maxResults, key, effSignal);
1293
1317
  } else {
1294
1318
  continue;
1295
1319
  }
@@ -1313,9 +1337,11 @@ function apply(ctx, config) {
1313
1337
  // 写入缓存(只缓存成功结果,失败走 throw 天然不缓存)
1314
1338
  const cached = { ...result, provider: engine, engine: engine };
1315
1339
  if (cacheKey !== null) {
1340
+ // fallback 条目(实际引擎≠首选)用配置 TTL 的 1/5,首选成功保持完整 TTL
1341
+ const entryTtlMs = engine !== preferred ? Math.max(cacheTtlMs / 5, 1000) : cacheTtlMs;
1316
1342
  searchCache.set(cacheKey, {
1317
1343
  value: cached,
1318
- expiresAt: Date.now() + cacheTtlMs,
1344
+ expiresAt: Date.now() + entryTtlMs,
1319
1345
  });
1320
1346
  if (searchCache.size > CACHE_MAX_ENTRIES) {
1321
1347
  const oldest = searchCache.keys().next().value;
@@ -1514,18 +1540,18 @@ function apply(ctx, config) {
1514
1540
  }, "free-search: test engines tool");
1515
1541
  });
1516
1542
 
1517
- // 平台搜索工具:GitHub / V2EX / Bilibili(公开 API,零依赖)
1543
+ // 平台搜索工具:GitHub / V2EX / Bilibili / Reddit(公开 API,零依赖)
1518
1544
  ctx.inject(["tools"], (sctx) => {
1519
1545
  sctx.effect(() => {
1520
1546
  const dispose = sctx.tools.register(
1521
1547
  defineTool({
1522
1548
  name: "platform_search",
1523
1549
  description:
1524
- "Search a specific platform (GitHub / V2EX / Bilibili) for a query. Returns source URLs with titles and snippets. Use this when the user asks about repos, code, forum threads, or videos.",
1550
+ "Search a specific platform (GitHub / V2EX / Bilibili / Reddit) for a query. Returns source URLs with titles and snippets. Use this when the user asks about repos, code, forum threads, videos, or discussions.",
1525
1551
  parameters: {
1526
1552
  platform: {
1527
1553
  type: "string",
1528
- description: "Platform to search: github, v2ex, bilibili",
1554
+ description: "Platform to search: github, v2ex, bilibili, reddit",
1529
1555
  },
1530
1556
  query: {
1531
1557
  type: "string",
@@ -1567,7 +1593,7 @@ function apply(ctx, config) {
1567
1593
  throw new Error(`unknown platform "${platform}" - use one of: ${Object.keys(PLATFORMS).join(", ")}`);
1568
1594
  }
1569
1595
  // 平台开关:settings 里禁用某平台时,工具明确告知
1570
- const enabled = current().platforms ?? ["github", "v2ex", "bilibili"];
1596
+ const enabled = current().platforms ?? ["github", "v2ex", "bilibili", "reddit"];
1571
1597
  if (!enabled.includes(platform)) {
1572
1598
  throw new Error(
1573
1599
  `platform "${platform}" is disabled in Free Search settings - enable it in Settings > Plugins > Free Search to use it`
@@ -1653,6 +1679,7 @@ function apply(ctx, config) {
1653
1679
  },
1654
1680
  },
1655
1681
  async execute(args) {
1682
+ if (!args.query || !String(args.query).trim()) throw new Error("query is required");
1656
1683
  const request = {
1657
1684
  query: args.query,
1658
1685
  maxResults: Math.min(args.maxResults ?? 5, 10),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-free-search",
3
- "version": "0.4.9",
3
+ "version": "0.4.10",
4
4
  "description": "Free web search for DeepSeek Harness: 10 engines (Bing/DuckDuckGo/AnySearch/SearXNG/Exa/Tavily/Keenable keyless) + time filtering + platform search + web_fetch, with web settings UI.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",