Skip to content

shrwnsan/search-plus

v3.5.2Apache-2.0

Enhanced web search with multi-service fallback (TinyFish, Tavily, Brave, Exa, Jina, plus keyless Firecrawl/Defuddle) — works without API keys

Changelog

All notable changes to the search-plus plugin will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

[3.5.2] - 2026-11-03

Changed

  • Skills sync to the shrwnsan/agents hub is now gated on a fail-closed portability allowlist (.github/sync-skills.json; no entry = no sync). search-plus opts in — meta-search keeps syncing as before.

[3.5.1] - 2026-10-29

Fixed

  • URL validator no longer rejects legitimate .com.<ccTLD> second-level domains such as amazon.com.au or carousell.com.hk (#87). The malformed-TLD check now tests the hostname with a 3+ letter qualifier instead of the full URL string, so a .com. appearing in a query string no longer triggers it either.
  • Removed a redundant unanchored example.com URL pattern that rejected any host merely ending in the substring example.com (e.g. notexample.com); reserved example.com/net/org hosts are still rejected by the hostname check.
  • Offline regression tests for the suspicious-domain gate: ccTLD hosts pass, malformed .com.<3+ letter> TLDs and reserved test domains stay rejected.

[3.5.0] - 2026-10-23

Added

  • TinyFish search and fetch provider: set SEARCH_PLUS_TINYFISH_API_KEY (free key, no card) to add TinyFish Search as the first keyed search provider (after SearXNG, before Tavily), and TinyFish Fetch, which renders pages in a real browser, after Jina Reader and before Firecrawl in the extraction chain. Both are free within TinyFish's daily allowances; a 402 (allowance used), 429, or per-URL error such as bot_blocked falls through to the next provider.
  • Search order chosen from a head-to-head eval (eval-028): TinyFish put the official source first on 23 of 29 queries, against 3–5 for Tavily. Tavily still runs as the fallback and is the only provider that adds a synthesized answer.

Changed

  • A Tavily search failure now logs its error message, like the other providers.
  • TINYFISH_API_KEY is accepted as a fallback when SEARCH_PLUS_TINYFISH_API_KEY is not set, matching the other providers (a deprecation notice points to the prefixed name).

[3.4.0] - 2026-10-15

Added

  • SearXNG search provider (PRD-009): set SEARCH_PLUS_SEARXNG_URL to your own instance to try it first for search; optional SEARCH_PLUS_SEARXNG_API_KEY is sent as a Bearer token. A 403 response explains that JSON output must be enabled in the instance's settings.yml.
  • Offline test coverage for every search provider (PRD-009 F3): recorded, sanitized responses with stubbed fetch check each provider's endpoint, auth, normalization, and fallback order.
  • Docs-sync test (PRD-009 F4): fails when a provider host is missing from SKILL.md allowedDomains or a SEARCH_PLUS_* variable is undocumented.

Fixed

  • Placeholder content no longer counts as success (PRD-009 F2): parked-domain pages, blank bodies, and provider error bodies returned with HTTP 200 now fail that attempt, so the extraction chain moves to the next provider and the attempt shows as rejected under Tried:. Generic words such as "forbidden" or "rate limit" are not treated as failures, because real articles contain them.
  • Tavily search with zero results now falls through to the next provider, like the other providers.

[3.3.1] - 2026-10-15

Security

  • Direct markdown fetch could reach internal addresses (PRD-009 F1): redirects are now followed manually (max 5 hops) and every hop is re-validated, so a public URL can no longer bounce the request to a loopback or private address. The private-address blocklist now also covers 0.0.0.0/8, 100.64.0.0/10, and IPv6 loopback/unspecified, unique-local, link-local, and IPv4-mapped addresses; bracketed IPv6 literals in URLs are classified correctly.
  • DNS rebinding on the direct fetch is closed: the direct fetch now uses its own http/https transport whose DNS resolution validates every address at connect time — inside the resolver call the connection itself uses, honoring both the single-address and Node ≥ 20 autoSelectFamily (all: true) contracts. A short-TTL answer can no longer hand the socket a private address the pre-flight check never saw.
  • The blocklist now covers the remaining IANA special-purpose ranges: IPv4 protocol assignments (192.0.0.0/24), TEST-NET-1/2/3, benchmarking (198.18.0.0/15), multicast (224.0.0.0/4), and reserved/broadcast (240.0.0.0/4); IPv6 IPv4-compatible (::/96), 6to4 (2002::/16), NAT64 (64:ff9b::/96), and Teredo (2001:0::/32) are checked through their embedded IPv4, plus documentation (2001:db8::/32) and multicast (ff00::/8). Subdomains and trailing-dot forms of localhost are rejected before DNS resolution.
  • Direct-fetch bodies are size-capped at 2 MiB of decoded bytes (gzip/deflate/br responses are decompressed first so a compression bomb cannot slip past), and a body declaring a larger Content-Length is refused before any byte is downloaded.

Changed

  • The direct fetch checks status and Content-Type before downloading the body, so non-markdown pages are no longer fully downloaded and discarded.
  • The direct fetch sends a User-Agent (previously fetch's implicit one) so UA-filtering origins do not silently waste the chain head, and transport errors keep their code (ETIMEDOUT etc.) when the underlying error message is empty.
  • The SSRF regression suite runs fully offline: redirect hops, DNS (including a rebinding scenario), and the transport are stubbed, so the DNS-dependent skip is gone.

3.3.0 - 2026-10-08

Added

  • Direct markdown fetch at the head of the URL extraction chain (#99): before any third-party service, the origin is asked for markdown (Accept: text/markdown). Trusted only on 200 + text/markdown + non-empty body; anything else falls through silently to the existing chain with the attempt visible in the Tried: output. Always-on, no configuration, one round trip for sites that honor content negotiation.

Fixed

  • github-service.mjs Promise.race timeout timers are now cleared on completion (#98)
  • Wayback availability-API outages are reported as such instead of "No Wayback snapshot available" (#98)
  • Reserved test domains (example.com/net/org) are rejected with an actionable "reserved test domain" message instead of the generic suspicious-domain wording (#98)
  • agents/search-plus.md documents the real CLI output contract instead of a schema with fields nothing produces (#96)

3.2.0 - 2026-10-08

Removed

  • Legacy recovery machinery (~1,000 lines): handle-search-error.mjs and handle-rate-limit.mjs were unreachable dead code — the only error that could reach their dispatcher was the fixed all-failed message, against which every branch was inert (the 879-line file behaved as a stderr string prefixer). The rate-limit path slept 60s + 120s against the hook's 25s deadline, and its retry-after parsing read a field no provider error carries. SEARCH_PLUS_RECOVERY_TIMEOUT_MS and SEARCH_PLUS_451_SIMPLE_MODE are gone with them (SEARCH_PLUS_404_MODE remains, still live in content extraction).
  • scripts/test-451-recovery.mjs (repo root): already broken — imported a path that no longer exists; not referenced by CI.

Fixed

  • Content-validation false signals: Jina Reader frontmatter keys (URL Source:, Markdown Content:) no longer blanket-reject all Jina output; Wayback banner chrome no longer fails validation for wayback-sourced content (every snapshot embeds it); parked-domain "buy this domain" spam now fails validation instead of passing as page content.
  • Jina removed from the Wayback snapshot extractors: r.jina.ai is blocked for web.archive.org until 2035, making the second extractor provably dead weight.

Changed

  • Web-search all-failed stderr message no longer carries the Search failed: prefix previously added by the deleted recovery dispatcher.

3.1.0 - 2026-10-07

Fixed

  • Keyless URL extraction was broken: spoofed Chrome 91 headers made r.jina.ai return a Cloudflare 403 challenge; extraction no longer sends browser headers to API services
  • Hook output was never applied: progress logs went to stdout, so Claude Code could not parse the hook JSON. Stdout now carries only the JSON (logs go to stderr with SEARCH_PLUS_DEBUG=1)
  • Hook false positives: recovery fired on successful pages that mentioned "403"/"429"/"empty". Detection now uses the tool's status code and result structure
  • Failures reported as success: failed URL extraction exited 0 and printed ~50 KB of failure JSON. It now exits 1 and lists each service tried
  • Wayback Machine fallback never ran (cacheURL.substring is not a function)
  • Tavily requests left a 15s timer running after completion (clearTimeout on a promise)

Added

  • PostToolUseFailure hook for WebSearch/WebFetch: tool errors (e.g. a WebFetch 403) are now recovered, not only successful-but-empty results
  • Firecrawl search and scrape: keyless by default, optional SEARCH_PLUS_FIRECRAWL_API_KEY. Restores web search with no API keys
  • Defuddle (defuddle.md) keyless URL→markdown extraction fallback
  • 25s internal deadline in the hook so it exits cleanly before the 30s hook timeout
  • SEARCH_PLUS_DEBUG=1 for progress logs

Changed

  • Script output is compact markdown (title, source, content / numbered results) instead of raw JSON. Hook context is capped below Claude Code's 10,000-character limit
  • The per-call service health check is now opt-in; it spent a Tavily credit and Jina tokens on every extraction
  • allowed-tools in SKILL.md uses Claude Code tool names (WebSearch, WebFetch)

Removed

  • Dead fallbacks: Google Cache and Bing Cache (both shut down), Yandex Turbo, "double/triple" Jina redirects, a malformed "textise" URL, and Tavily retries that only changed the User-Agent sent to the Tavily API

3.0.1 - 2026-04-16

Fixed

  • Error recovery now uses all 4 providers (performHybridSearch) instead of Tavily-only contentExtractor.tavily.search() — 12 call sites fixed across handle-search-error.mjs and handle-rate-limit.mjs

Removed

  • ~250 lines of dead code: trySearXNGSearch(), tryDuckDuckGoHTML(), tryStartpageHTML() functions, dead transformers, and dead service bonus entries
  • Unused security-utils.mjs import

Changed

  • Updated STANDARD_RESPONSE_FORMAT.md to document current providers only
  • Updated README sandbox guidance with root cause and excludedCommands workaround

3.0.0 - 2026-04-14

Added

  • Brave Search API provider — independent 30B+ page index, fastest latency (~670ms avg), SEARCH_PLUS_BRAVE_API_KEY
  • Exa AI Search provider — neural/semantic search for technical docs (~1.2s avg), SEARCH_PLUS_EXA_API_KEY
  • Jina Search API provider (s.jina.ai) — replaces dead free services, SEARCH_PLUS_JINA_API_KEY
  • 4-service sequential fallback chain: Tavily → Brave → Exa → Jina Search
  • Response transformers for all new providers (brave, exa, jina-search)
  • Service reliability bonuses for relevance scoring: Brave +0.09, Exa +0.08, Jina Search +0.07
  • Environment variable deprecation warnings for old naming conventions
  • Sandbox compatibility documentation

Changed

  • performHybridSearch() replaced dead Promise.any([SearXNG, DuckDuckGo, Startpage]) with sequential 4-service chain
  • Env var naming standardized to SEARCH_PLUS_ prefix (old names still work with deprecation warnings)
  • Jina env var canonical name changed from SEARCH_PLUS_JINAAI_API_KEY to SEARCH_PLUS_JINA_API_KEY
  • Plugin description updated to reflect multi-service architecture

Removed

  • BREAKING: Free keyless web search path — was dead anyway (100% failure rate since early 2026)
    • SearXNG public instances: all return 403/429/timeout or are offline
    • DuckDuckGo HTML: returns CAPTCHA on all requests
    • Startpage HTML: returns JS-only shell with 0 parseable results

Not Affected

  • URL extraction via Jina Reader (r.jina.ai) — still works without API key at 20 RPM
  • GitHub content extraction via gh CLI — unchanged

2.11.0 - 2026-04-12

Added

  • Sandbox compatibility notes in SKILL.md
  • PostToolUseFailure limitation documentation

2.10.1 - 2026-04-11

Previous versions were not tracked in a CHANGELOG.