# master-fetch (hound-mcp) — Status

Workspace status doc for the hound-mcp OSS tool (web fetch+search MCP). Holds
(1) the current-shipped-version detail and (2) the dev-notes / API-quirks
section that must survive context compaction. Earlier shipped versions are
one-liners below; full build logs live in CHANGELOG.md and git history.

Project: github.com/dondai1234/master-fetch, PyPI package `hound-mcp`, MIT.
Module `master_fetch`, CLI `hound`. 6 tools (smart_fetch, smart_crawl,
smart_search, screenshot, cache_clear, version). Single warm Patchright browser
("Google Chrome for Testing", real_chrome=False) for smart_fetch/screenshot
only, eager at startup. 10 keyless search backends, neural-reranked with a local
ONNX cross-encoder.

---

## Current shipped version: 10.5.0 (2026-07-21)

Termux/Android support + HTTP-only graceful degradation. The lean install
(`pip install hound-mcp`) no longer pulls playwright (no aarch64/Termux wheels).
Browser deps (playwright, patchright, curl_cffi, browserforge, etc.) moved
from core deps to the `[all]` extra with loose pins (>=, not ==). When
scrapling's browser deps can't import, hound falls back to HTTP-only mode:
`_get_scrapling()` catches ImportError, `_fallback_http_get()` fetches via
httpx, `_translate_response` uses trafilatura (no scrapling Convertor needed),
`_auto_escalate` skips the stealthy tier, browser methods raise clear
RuntimeError. Doctor adds a browser deps check (non-blocking).

What works in HTTP-only mode: web_fetch (HTTP tier), web_search, web_crawl,
cache_clear, hound_version. Disabled: stealthy browser escalation, screenshot,
persistent browser sessions.

Verification:
- pytest -> 742 passed, 0 failed (18 new graceful degradation tests).
- PyPI hound-mcp 10.5.0 live; global install verified.
- npm @houndmcp/hound-mcp-pi 10.5.0 live.
- GitHub v10.5.0 release created.
- pip --dry-run confirms lean install has NO playwright.

### Previous: 10.4.1 (2026-07-20)

Removed Internet Archive fallback (archive.py deleted, 692 lines removed,
hard-blocks return clean errors immediately); added hound --reinstall command
(--force-reinstall --no-deps hound-mcp[all]); fixed doctor [all] extras check
(rapidocr v3, not rapidocr_onnxruntime v1). 724 tests.

### Previous: 10.4.0 (2026-07-20)

Universal error detection: 4xx/5xx no longer treated as real content. The
_detect_content_issue function only caught JS shells, bot challenges (403/503),
and geo redirects. Did NOT catch 4xx/5xx with content. result.error stayed empty
while content_ok was False. Fix (3 layers): (1) any 4xx/5xx now sets
result.error to http_error_{status}; (2) _auto_escalate broadened to include
429/500/502 (stealthy browser may avoid rate limits); (3) Pi extension
hound.ts cleanError() maps server error strings to human-readable messages.
Live-verified: GitHub 404, Google 404, dead domain, httpbin 429/500, real 200.

### Previous: 10.3.1 (2026-07-20, npm patch only)

Pi extension hardening: 8 bugs fixed in hound.ts (lazy HOUND_EXE, AbortSignal,
kill-before-respawn, readPkgVersion, screenshot fix, hound_version formatting,
session_start notification, version sync check). Git install fixed (root
package.json). Sync script created.

### Previous: 10.3.0 (2026-07-20)

Token-optimized tool definitions (3,900 -> 2,746 tokens, -29.6%). Fixed two v10
bugs (envelope signals + archive_fallback NOT in client-visible _TOOL_DEFS / NOT
wired through dispatch). Official Pi agent extension (pi-extension/ dir, npm
@houndmcp/hound-mcp-pi, 6 native Pi tools + minimal TUI). 705 tests.

### Previous: 10.2.1 (2026-07-18)

Patch: surfaced the v10 envelope + Internet Archive fallback in the smart_fetch
tool docstring + clarified instructions "unbypassable" line. No behavior change.
705 tests. (NOTE: this fix went to the method docstring, which the low-level
MCP server does NOT send. The REAL fix was in 10.3.0, which put the signals in
_TOOL_DEFS.)

### Previous: 10.2.0 (2026-07-18)

Brick-proof self-update (updater.py: --no-deps, detached helper + rename trick,
self-heal, surviving ~/.hound/repair.py) + hound doctor + hound --rollback.
705 tests.
---

## Version history (one-liners; full detail in CHANGELOG.md + git history)

- **10.5.0 (2026-07-21)** -- Termux/Android support: scrapling[ai] -> scrapling (core only) in deps, browser deps (playwright, patchright, curl_cffi, browserforge) moved to [all] extra with loose pins (>=1.50). Graceful degradation: _get_scrapling catches ImportError, _FallbackResponse + _fallback_http_get for httpx fetch, _translate_response uses trafilatura when scrapling unavailable, _auto_escalate skips browser tier, browser methods raise clear RuntimeError, _prewarm_stealthy no-op. Doctor adds browser deps check. 18 new tests. 742 tests.
- **10.4.1 (2026-07-20)** -- removed Internet Archive fallback (archive.py deleted, 692 lines removed, hard-blocks return clean errors immediately); added hound --reinstall command (--force-reinstall --no-deps hound-mcp[all]); fixed doctor [all] extras check (rapidocr v3, not rapidocr_onnxruntime v1). 724 tests.
- **10.4.0 (2026-07-20)** -- universal error detection: 4xx/5xx no longer treated as real content (_detect_content_issue sets result.error on any 4xx/5xx; _auto_escalate broadened to 429/500/502; Pi extension cleanError() maps server errors to human-readable messages). 705 tests.
- **10.3.1 (2026-07-20, npm only)** -- Pi extension hardening: 8 bugs fixed in hound.ts (lazy HOUND_EXE, AbortSignal, kill-before-respawn, readPkgVersion, screenshot fix, hound_version formatting, session_start notification, version sync check). Git install fix (root package.json). Sync script.
- **10.3.0 (2026-07-20)** -- token-optimized tool defs (3900->2746 tok, -29.6%); fixed two v10 bugs (envelope signals + archive_fallback NOT in client-visible _TOOL_DEFS / NOT wired through dispatch); official Pi agent extension (pi-extension/ dir, npm @houndmcp/hound-mcp-pi, 6 native Pi tools + minimal TUI). 705 tests.
- **10.2.1 (2026-07-18)** -- surfaced the v10 envelope + Internet Archive fallback in the smart_fetch tool docstring (the description every MCP client shows agents); clarified instructions "unbypassable" line. No behavior change. 705 tests.
- **10.2.0 (2026-07-18)** -- brick-proof self-update (updater.py: --no-deps, detached helper + rename trick, self-heal, surviving ~/.hound/repair.py) + hound doctor + hound --rollback. 705 tests.
- **10.1.0 (2026-07-18)** -- polished cross-platform CLI (cli_ui.py, stdlib-only: bordered -v panel, quiet -u self-update, styled --help; color/NO_COLOR/Windows VT, Unicode+ASCII border fallback). 693 tests.
- **10.0.0 (2026-07-18)** — Internet Archive recovery (auto-fallback to Wayback on hard-fail, id_ marker, honest source/archived_at) + research-grade envelope (page_type, content_age_days/is_stale, source_type/is_official, smart next_action) + cache round-trips envelope + construction refactor (model_copy) + pytest pythonpath=[src]. 687 tests.
- **9.2.0 (2026-07-08)** — idle browser close frees RAM (HOUND_BROWSER_IDLE_TIMEOUT, default 300s). 635 tests.
- **9.1.2 (2026-07-07)** — packaging hardening: mcp>=1.27.0 + pydantic>=2.0 declared as direct runtime deps (were transitive). Lazy search/reranker imports kept. 626 tests.
- **9.1.1 (2026-07-07)** — startup reliability: no-op search prewarm/shutdown imports removed from serve paths; `_safe_imported_prewarm` resolves optional prewarm modules off the event loop; search_engines lazy-loads search_metasearch only inside multi_search; search lazy-loads reranker. Removed broad unawaited-coroutine warning suppression.
- **9.1.0 (2026-07-06)** — native streamable HTTP transport (MCP 2025-03-26 spec): `hound --http --host --port` serves at `/mcp` for Open WebUI v0.6.31+, no mcpo proxy. Legacy SSE replaced, not kept. Post-release fix `cba618a`: prewarm_stealthy runs Scrapling import via asyncio.to_thread so browser warmup can't freeze the handshake.
- **9.0.0 (2026-07-05)** — clean Windows shutdown (sys.unraisablehook + subprocess-transport flush, fixes 'failed to load' stderr noise); CI lifecycle test; leaner tool defs (drop rare knobs, still work via additionalProperties); repo professionalism (.gitattributes, community files, STATUS.md gitignored). 624 tests.
- **8.2.1 (2026-07-02)** — faster first search (reranker ONNX load overlaps engine fetch via race-safe asyncio.Lock ensure_reranker). 622 tests.
- **8.2.0 (2026-06-28)** — fixes 'failed to load': heavy imports deferred (cold start 5.45s->0.52s), browser prewarm isolated (BaseException + 30s timeout), bulletproof shutdown. 619 tests.
- **8.1.0 (2026-06-28)** — real Qwant backend (10 keyless engines), circuit breaker for blocked backends, tracking-aware dedup. 613 tests.
- **8.0.0 (2026-06-28)** — sitemap-mode crawl, outgoing-links field, related-queries mining, PDF section-map with page ranges, tool-def overhaul, metasearch status robustness. 603 tests.
- **7.5.0 (2026-06-25)** — vendored ddgs metasearch, 9 backends in parallel, diversity quorum, 100% HTTP search (no browser for search). 585 tests.
- **7.4.0 (2026-06-25, SUPERSEDED by 7.5)** — shared-browser search backbone + parallel race. Replaced: sometimes only Bing (race returned whatever finished first = garbage).
- **7.3.1 (SUPERSEDED)** — made smart_fetch's stealthy browser LAZY at startup without authorization. Scope-discipline lesson recorded. Reverted in 7.4.
- **7.3.0 (2026-06-24)** — Qwant replaces Brave (keyless JSON API). 616 tests.
- **7.2.0 (2026-06-24)** — diverse independent pool: Brave added, Google removed (CAPTCHAs), Mojeek dropped, Yahoo opt-in. Consensus ranking. BM25/keyword removed (neural-only). 614 tests.
- **7.1.0 (2026-06-24)** — cold-start timeout fix, agent QoL (summary + next_action), CUT research mode + expand as bloat. SERL resilience layer (pacer, circuit breaker, HOUND_SEARCH_PROXY). 616 tests.
- **7.0.0 (2026-06-24)** — local keyless search flagship. TinyFish HARD-REMOVED. Hand-rolled engine scrapers + neural rerank. 599 tests.
- **6.0.0 (2026-06-23)** — crawl flagship (best-first priority queue) + PDF flagship (CID-garbage auto-OCR fallback, quality_score, ToC). 549 tests.
- **5.0.0 / 5.0.1 (2026-06-22)** — OCR, query-focused focus markdown, smart_search filters, smart_crawl BFS, metadata, actions, media URLs. 528 tests. Search was TinyFish-keyed at this point (removed in v7).
- **4.0.0 / 4.0.1 / 4.0.2 / 4.0.3 (2026-06-20)** — PDF extraction flagship, caching hardening, single warm browser instance (8->5 tools), agent QoL batch, reddit routing. 442 tests.

### Key decisions / cut scope (still valid, do not re-propose)
- Residential proxy rotation, LLM extraction via user endpoint, CSS-schema
  extraction, captcha-solving hook, diff/monitor tool, deep content-aware rerank
  (mode=deep): all CUT. See CHANGELOG for reasoning. Do not re-propose.
- No free keyless general web search API without rate-limits exists in 2026
  (Bing Search API killed summer 2025, Brave Search API free tier killed Feb
  2026). General web search = scraping or paid API. That's why hound vendors a
  multi-backend metasearch.
- Parallel-race early-return sacrifices quality (returns whatever finishes
  first, often the weakest engine). The v7.5 diversity quorum (wait for >=3
  backends) is the deliberate correction.

---

## Dev notes / API quirks (must survive context compaction)

Project-specific quirks. NOT cross-project (those go in MEMORY.md). Keep only
CURRENT quirks; superseded engine layers (v7.0-7.4 hand-rolled, SERL coordinator,
browser-rendered DDG) are deleted since the code no longer exists.

### Search (v7.5+ vendored ddgs metasearch, search_metasearch.py)

10 text backends: duckduckgo (httpx transport, POST, html.duckduckgo.com/html),
brave (primp, search.brave.com), google (primp, Android UA + CONSENT cookie, often
CAPTCHAs under load but carried by others), startpage (primp, POST, needs `sc`
token from homepage first; Google-index privacy frontend), qwant (primp, keyless
JSON api.qwant.com/v3/search/web, count=10 EXACTLY, impersonate='safari' PINNED,
param order shuffled for fingerprint resistance), grokipedia (primp, JSON
api/typeahead, priority 1.9), wikipedia (primp, opensearch API + extracts,
priority 2.0, topic-match only), yahoo (primp, Bing-index from diff server, /RU=
redirect decode), mojeek (primp, own index), yandex (primp, yandex.com/search/site).
**Bing disabled** (DDG+Yahoo serve it).

- Transport: primp.Client(impersonate='random', impersonate_os='random') for most;
  httpx.Client(http2=True, _random_ssl_context cipher shuffle + _H2Patch
  randomizes HTTP/2 SETTINGS) for DDG. Both sync -> metasearch wraps in
  asyncio.to_thread.
- metasearch() async aggregator: asyncio.wait(FIRST_COMPLETED) loop; min_engines=
  min(3,len(instances)) + soft_deadline=2.0 + quorum_results=max_results+4;
  early-return cancels laggards (await with `except BaseException` since
  CancelledError is BaseException on py3.11+). Dedup by _normalize_url, tracks
  ALL backends per URL (seen[key]['backends'] set) for consensus. status per
  backend: ok/empty/error:X/timeout/preempted.
- Circuit breaker (v8.1): module-level `_BACKEND_HEALTH: {name: block-until ts}`.
  `_record_block` (cooldown 60s), `_is_circuit_open`, `_record_success` (clears).
  Circuit-open backends skipped at instance-building; MetaBlockedException ->
  status 'blocked' + `_record_block`; a backend that contributes ->
  `_record_success`. Empty/timeout do NOT trip the breaker. The quorum
  `min_engines` is based on HEALTHY instances. `_reset_circuit_breaker()` is the
  test hook.
- Dedup `_normalize_url` (v8.1): strips ONLY tracking params (utm_*, fbclid,
  gclid, ref, ref_src, source, _ga, mc_cid, mc_eid, igshid, si) and KEEPS real
  query. Previously dropped the whole query string (collapsed ?page=2 vs ?page=3).
- _HOUND_TO_BACKEND: bing->yahoo, qwant->duckduckgo (no ddgs qwant).
  _INDEX_FAMILY by ddgs provider: duckduckgo+yahoo='bing', google+startpage=
  'google', others each own. consensus = distinct families.
- `HOUND_SEARCH_PROXY` env -> primp/httpx proxy param (per-IP throttle escape for
  heavy bulk use; normal single-user use does NOT need it).
- search_engines.py adapter: EngineReport.preempted = cancelled-because-enough
  (NOT blocked); engine_blocked in search.py = r.blocked only.

### Reranker (reranker.py, search.py)

- Neural reranker ONNX model ~1.4s cold load. `ensure_reranker(*, download=True)`
  (async) serializes via an asyncio.Lock (lazily created, module-level
  `_reranker_lock=None`) so prewarm + first search share ONE load. Top-level fast
  path: `if _reranker is not None: return` (NO _reranker_tried short-circuit at
  top, that bypassed the lock and was the race). Under the lock: `if
  _reranker_tried: return None`.
- `get_reranker()` is PEEK-ONLY (returns _reranker or None, never loads).
  `_load_reranker()` is the sync load body. `prewarm_reranker()` ->
  `ensure_reranker(download=False)`.
- search.py: `asyncio.create_task(ensure_reranker())` started BEFORE
  `await multi_search` so the model load OVERLAPS the engine fetch (diversity
  quorum ~2s), then `await _rerank_task` before _rank. First search =
  max(engine_fetch, model_load), not sum.

### Startup / lazy imports (the 'failed to load' fix, v8.2 + v9.1.1)

- `import master_fetch.server` was 5.45s cold because trafilatura,
  search_metasearch (primp/httpx/lxml/h2/fake_useragent), mcp.server.fastmcp,
  and mcp.types were imported at MODULE LEVEL. Now lazy-imported at call sites.
  Result: import 0.52s. mcp.server.Server + mcp.types still imported IN serve()
  before the first response (irreducible SDK cost, ~2s, cached after first run).
  TEST: test_v8_2_startup asserts these heavy modules are NOT in sys.modules
  after `import master_fetch.server` (checked in a fresh subprocess). RULE: any
  new heavy dep added to server.py MUST be lazy-imported at its call site, not
  at module top, or the handshake stalls again.
- v9.1.1: stdio/HTTP serve paths no longer import `master_fetch.search_engines`
  at startup just to call no-op prewarm/shutdown hooks. `search_engines`
  lazy-loads `search_metasearch` only inside `multi_search()`. `search`
  lazy-loads `reranker`. `_safe_imported_prewarm` resolves optional prewarm
  modules inside `asyncio.to_thread()` before their async prewarm runs.

### Browser / sessions (v9.2 idle close)

- Single warm Patchright Chrome, shared by smart_fetch + screenshot. Eager
  prewarm at startup via `_prewarm_stealthy` (wrapped in
  `asyncio.wait_for(timeout=30.0)`, catches BaseException so a hung launch can't
  crash the server or hold _auto_session_lock). `_safe_prewarm(coro_fn, timeout)`
  is the module-level helper for search/reranker prewarm.
- v9.2 idle close: `AUTO_SESSION_IDLE_TIMEOUT` (read from
  `HOUND_BROWSER_IDLE_TIMEOUT` env, default 300, 0=off) closes the warm browser
  after that many seconds idle. The idle monitor (`_start_idle_monitor`,
  `_ensure_idle_monitor`, race-safe under `_sessions_lock`) closes the session
  (process exits, OS reclaims RAM); next fetch's `_ensure_auto_session`
  cold-relaunches. Timer resets only on a fetch that uses the browser
  (stealthy/screenshot via _ensure_auto_session); pure HTTP fetches do NOT reset.
- AUTO_SESSION_IDLE_TIMEOUT is read at IMPORT time via `_env_int()` in server.py.
  Tests that need a different value: `monkeypatch.setattr("master_fetch.server.AUTO_SESSION_IDLE_TIMEOUT", X)`.

### Clean Windows shutdown (v9.0.0, the stderr-noise fix)

- On Windows the ProactorEventLoop's pipe transports emit `Exception ignored in
  __del__` tracebacks to stderr during GC AFTER the loop closes (RuntimeError:
  Event loop is closed; ValueError: I/O operation on closed pipe). Process exits
  0 but an MCP client reading stderr sees a crash-like traceback -> 'failed to
  load'. v8.2's finally block CAN'T catch these (they fire in __del__ post-loop).
- FIX: `serve()` installs `sys.unraisablehook = _quiet_asyncio_del_hook` that
  inspects `traceback.extract_tb(args.exc_traceback)` -> FrameSummary.filename
  (NOT f_code, FrameSummary has no f_code) and swallows ONLY when 'asyncio' is in
  a frame filename AND exc_type in (RuntimeError, ValueError, ResourceWarning).
  Everything else passes to the original hook (real __del__ bugs stay visible).
  NOTE: `warnings.filterwarnings` does NOT prevent the ValueError (the f-string
  evaluates __repr__ which raises BEFORE _warn checks the filter). The hook is
  the only thing that silences it.
- `_shutdown_close_sessions`: close session -> `await asyncio.sleep(0.15)` (drain
  pending callbacks while loop alive) -> `_close_all_subprocess_transports()`
  iterates `loop._subprocess_transports` and closes each -> short flush.

### Transport

- Stdio (default): `hound` with no flag. Used by Claude Code, Cursor, OpenCode, Pi.
- Streamable HTTP (v9.1.0): `hound --http --host 127.0.0.1 --port 8765` serves at
  `/mcp`. Uses `StreamableHTTPSessionManager` + a `_StreamableHTTPASGIApp` class
  instance (NOT a plain async function, Starlette wraps those as HTTP handlers
  not raw ASGI). Open WebUI v0.6.31+ connects directly, no mcpo proxy. Tests:
  `tests/test_v91_http.py`.

### Response / chunking / contextvars (v8)

- `_apply_chunking` REBUILDS the ResponseModel on truncation (two construction
  sites). It must copy EVERY contextvar-driven field (metadata, media, links,
  quality_score, table_of_contents, content_ok) or they silently vanish on
  truncated responses. When adding a new contextvar-driven response field, add it
  to BOTH constructors or it'll work on short pages and disappear on long ones.
- contextvar propagation through the fetch path is fine: `_INCLUDE_LINKS`/
  `_INCLUDE_MEDIA` set in `smart_fetch` (the method) are visible inside
  `_translate_response` even though the http tier runs via `bulk_get`'s
  `gather(*timed_tasks)` because `_translate_response` is called AFTER
  `await gather(...)` in the main task, not inside the gathered tasks.
- metasearch status: 'ok' vs 'empty'. A backend that returned valid results which
  all got deduped by an earlier-finishing backend must be 'ok' (it contributed =
  confirmed consensus), NOT 'empty'. The `touched` flag tracks dupe-matches;
  `status = 'ok' if (added or touched) else 'empty'`.

### Crawl (v8)

- sitemap transport is injected: `sitemap.discover_sitemap` takes an `http_get`
  callable (url -> (status, body) | None) so it's unit-testable with a fake.
  crawl._sitemap_map builds one with primp + a stdlib urllib fallback (some hosts
  reject primp fingerprints).
- PDF section-map: bookmark ToC gets `end_page` via `_add_end_pages` (next entry
  at same/shallower level - 1, else total). PDFs without bookmarks get a
  heading-based fallback (`_heading_outline`, deduped, capped at 60). `_extract_toc`
  reads `len(pdf)` for total pages (guarded for fakes without __len__).
- links classification: anchors classified by container (nav/header/footer/aside
  -> navigation for same-domain / dropped for external) vs main-content
  (article/main/section/p/li -> citation for same-domain, content_external for
  off-domain). primary_source = canonical/JSON-LD on a DIFFERENT host, else first
  off-domain in-content link on a known primary host (arxiv/doi/github/wikimedia).
  Same-domain links are never a primary source.
- related_queries is extractive, not SERP-scraped: mined from result
  titles+snippets via bigram doc-frequency (no LLM, no per-engine 'related
  searches' markup dependency). Filter: drop bigrams where every token is in the
  query; drop df<2; fall back to unigrams.

### PDF (v6)

- CID font corruption: PDFs with embedded font subsets lacking a ToUnicode CMap
  make pdfplumber emit `(cid:71)...` garbage. But glyphs RENDER correctly, so
  rendering the page (pypdfium2) + OCR (rapidocr) recovers the real text. Detect
  CID-garbage ratio >= 0.30 per page -> batch-OCR -> replace + honest marker.
- pypdfium2 `pdf.get_toc()` returns a GENERATOR (materialize with list()). Each
  PdfBookmark has `.level` (0-based) + `.get_title()` + `.get_dest()` ->
  PdfDest with `.get_index()` (0-based; page = idx+1). get_dest() may return None
  for URI-action bookmarks. Most arxiv papers lack a ToC; books/reports have one.
- PDF quality_score must EXCLUDE honest markers: compute from RAW rendered page
  text + OCR status BEFORE adding markers, or the scaffolding text inflates it.
- OCR test fixture must be detector-friendly: a tiny line on a blank A4 page
  doesn't fire RapidOCR's text detector on all OS/versions. Use realistic
  multi-line text filling the page. Never assert exact OCR output in CI; assert
  the path ran + a meaningful guard instead.

### Environment / install quirks

- **Dev venv ships a BUILT WHEEL, not editable**: `.venv/` has a built wheel in
  site-packages, NOT an editable install. `pytest` runs the STALE installed
  copy. Before testing any `src/` edit: `.venv/Scripts/pip install -e . --no-deps`.
  Verify: `python -c "import master_fetch; print(master_fetch.__file__)"` must
  show `src/master_fetch/...`, not `.venv/Lib/site-packages/...`.
- **Optional-dep masking (recurring class)**: a dev venv with a dep installed
  manually MASKS a missing pyproject declaration. Happened on 4.0.0 (pdfplumber),
  5.0.0 (onnxruntime). After adding an optional dep, verify in a CLEAN venv that
  `pip install pkg[all]` delivers it AND that the dep can be INSTANTIATED (not
  just imported, import misses backend deps loaded at __init__). Grep the BUILT
  wheel METADATA for `Requires-Dist: ...; extra == 'all'`.
- **Global hound reinstall lock**: force-reinstalling the GLOBAL hound-mcp fails
  with WinError 32 on `hound.exe` if a hound process (often the pi
  `web-research.ts` singleton) still holds the launcher. Find it via
  `Get-CimInstance Win32_Process | ?{$_.CommandLine -like '*hound*'}` or
  `tasklist | grep -i hound.exe`; kill it, then reinstall.
  v10.2.0+ RECOVERY: `hound -u` is brick-proof (--no-deps, detached helper +
  rename trick, self-heal) and `~/.hound/repair.py` (written on every update /
  `hound --doctor`) is the universal recovery: `python ~/.hound/repair.py` stops
  hound + force-reinstalls. LEGACY (pre-10.2.0): a failed `--force-reinstall`
  left a `~ound_mcp-X.dist-info` (tilde = interrupted uninstall) + no proper
  dist-info -> 'install is corrupted'; clean those + `_editable_impl_hound_mcp.pth`
  before retrying. There is also an UNRELATED `hound` (1.0.1) PyPI package in
  site-packages sometimes; leave it, not ours.
- **edit tool + apostrophes**: replacing long blocks containing straight
  apostrophes fails the edit tool's exact match. Workaround: splice
  programmatically with a Python script (read lines, replace range, write back).
- **old.reddit.com thing-block class has a LEADING SPACE**: real HTML is
  `class=" thing id-t3_..."`, never `class="thing...`. Regex must allow
  `class="[^"]*\bthing\b`.
- **Single warm browser confirmed not the source of phantom Chrome**: hound only
  launches ms-playwright chromium ("Google Chrome for Testing", real_chrome=False
  everywhere; scrapling uses channel="chromium" when real_chrome is falsy). Any
  plain "Google Chrome" (Program Files) is another tool/MCP server, NOT hound.

### npm publishing (pi extension)

- npm package: @houndmcp/hound-mcp-pi (scoped to the houndmcp org on npm).
  The pi-package keyword in package.json makes it show on pi.dev/packages.
- npm killed TOTP (authenticator app codes) in Sep 2025. Security keys only
  (WebAuthn/passkeys). Windows Hello (PIN) can act as a software passkey
  for the browser, but Dondai's machine could not satisfy it.
- Workaround: granular access token with "Bypass 2FA" + "Read and write"
  permissions + scoped to "All packages" (NOT org-scoped; org-scoped tokens
  can only manage org settings, NOT publish). Write token to pi-extension/.npmrc,
  run `npm publish`, delete .npmrc. The .npmrc is gitignored.
- npm propagation lag: registry returns 404 for ~2-5 min after publish before
  the CDN catches up. The publish itself succeeds (re-publish gives
  "cannot publish over previously published version" which confirms it's live).
- npm `--auth-token` CLI flag does NOT work for publish (npm ignores it).
  Must use .npmrc: `//registry.npmjs.org/:_authToken=TOKEN`.

### Pi extension hardening (v10.3.1, npm patch release)

Extension-only patch. Python hound-mcp stays at 10.3.0. npm
@houndmcp/hound-mcp-pi bumped to 10.3.1.

Bugs fixed in pi-extension/extensions/hound.ts:
- HOUND_EXE was resolved at module load time and cached forever. If hound
  was not installed when the extension loaded, it stayed null even after
  the user installed hound. Now re-resolved on each ensureReady() if null.
- call() ignored the AbortSignal parameter. If the user cancelled a tool
  call (Esc), the hound subprocess kept working. Now abort-aware: rejects
  the pending call on signal.abort.
- _initOnce() didn't kill the existing process before spawning a new one.
  On retry (dead spawn) or re-init after death, the old process was
  orphaned. Now kills + nulls before spawn.
- clientInfo version was hardcoded "10.3.0". Now read from package.json
  via readPkgVersion() (best-effort, falls back to "unknown").
- screenshot promptSnippet said "full_page, image_type" as top-level
  params. They're in the options bag. Fixed to "options.full_page".
- hound_version returned raw JSON text. Now formats nicely ("Hound 10.3.0
  (up to date)") + includes extension version. Falls back to
  `hound --version` via execSync if MCP server is down.
- resolveHoundExe fallback only checked Python311. Now checks 311/312/
  313/310.
- No session_start notification when hound not found. Now notifies the
  user: "Hound not found" or "Hound failed to start".
- No version sync check. Now calls version MCP method at session_start
  and warns if extension vs hound major versions diverge.

Git install fix:
- Repo root had no package.json. pi install git:... cloned but found no
  pi manifest + no extensions/ dir = discovered nothing. Added root
  package.json with pi.extensions pointing to ./pi-extension/extensions/
  hound.ts.

Dev tooling:
- scripts/check_pi_extension_sync.py: compares _TOOL_DEFS in server.py
  with hound.ts descriptions. Checks tool name completeness, key term
  presence, and version lockstep (Python lockstep + npm lockstep +
  major cross-check). Run before publishing to catch description drift.

### Testing footguns (cross-platform)

- Faking `sys.platform = "win32"` on a Linux test run makes `shutil.which`
  (used by `updater._hound_launcher_path`) take its Windows branch and hit
  `_winapi.NeedCurrentDirectoryForExePath`, which is `None` on Linux. Py 3.12+
  added that call to `shutil.which`'s Windows path -> breaks on ubuntu 3.12/3.13
  CI but PASSES on 3.11 + on a Windows dev box (real `_winapi` there). Lesson:
  any test that fakes `sys.platform="win32"` on Linux MUST also mock the
  `shutil.which`/`_hound_launcher_path` call it triggers, or it passes locally
  + on 3.11 CI and only fails on 3.12/3.13 CI. CI caught this on the 10.2.0
  push (the 2 TestHelperSource `_spawn_helper` tests); fixed in 60859b7
  (test-only; the 10.2.0 wheel was unaffected).

---

## Marketing / community state (2026-07-20)

### BREAKTHROUGH: 131 stars (2026-07-20)

Went from ~30 stars (accumulated over 1.5 months) to 131 stars in a single
 day after two posts in agent-harness subreddits. This is the right
 audience: agent users who need web research, not generic MCP/LLM browsers.

 Successful posts (reference these for future agent-harness subreddits):

 1. r/PiCodingAgent post (2026-07-20):
    Path: `C:/Users/Dondai/More Files/Desktop/TXT/hound-pi-reddit-post.md`
    Result: 6k views in 4h, ~10+ star gain. Very successful.

 2. r/opencodeCLI post (2026-07-20):
    Path: `C:/Users/Dondai/More Files/Desktop/TXT/hound-opencode-reddit-post.md`
    Includes manual setup section + "Tell your agent" section with optimized
    OpenCode prompt (paste-ready, universal/cross-platform, no personal paths).
    Demo video URL removed (Dondai uploads himself).

 3. r/hermesagent post (2026-07-20):
    Path: `C:/Users/Dondai/More Files/Desktop/TXT/hound-hermes-reddit-post.md`
    Framed for Hermes: YAML config (~/.hermes/config.yaml), no built-in tools to
    disable, supports_parallel_tool_calls: true, tool names prefixed
    mcp_hound_*, /reload-mcp for in-session reload.

 Key takeaways for future posts to other agent-harness subreddits:
 - SAVE all posts to `C:/Users/Dondai/More Files/Desktop/TXT/`, not directly on Desktop.
 - Frame for the specific audience (not copy-paste). Each harness is different.
 - Include a "tell your agent" paste-ready prompt that auto-configures hound.
 - Mention demo video(s) backing claims (not just yapping).
 - Brief on limitations: top-tier sites blocked but hound manages 95%. Don't
   over-explain.
 - Side-by-side demo gallery in README (OpenCode + Pi videos) backs claims.

 Not yet posted: r/LocalLLaMA (best targeted fit), r/selfhosted +
 r/SelfHostedAI, r/ClaudeAI, r/cursor, Hacker News (Show HN Tue/Wed/Thu 8am
 Pacific), dev.to (cross-post).

### General marketing state

- README restructured: crawl4ai-style scannable sections, emoji-headed capability
  sections, comparison table (Hound vs Crawl4AI / Parallel Search / Firecrawl),
  honest-limits section, demo videos embedded inline (two stacked: OpenCode + Pi).
  OpenCode video: `https://github.com/user-attachments/assets/4e69e476-9c46-40cd-9bdf-f1b45882ac3e`
  Pi video: `https://github.com/user-attachments/assets/18b502c0-1e7b-4cb3-a730-074a9fbd0993`
- Demo files at `C:/Users/Dondai/More Files/Desktop/hound-demo/`:
  `prompt_1_no_urls.txt`, `prompt_2_with_urls.txt` (anti-cheat guardraided demo
  prompts, symmetric across hound/crawl4ai/parallel-search), `hn_show_post.txt`
  (Hacker News Show HN, ready to post Tue/Wed/Thu 8-9am Pacific),
  `localllama_post.txt` (r/LocalLLaMA text post, ready), `x_post.txt`,
  `reddit_post.txt`, `comparison_notes.txt`.
- Posted: r/mcp (v10.3.0 post with comparison table, did decent, got a Docker
  comment from an Open WebUI user), X (3 views, dead), r/PiCodingAgent
  (breakthrough, 6k views 4h), r/opencodeCLI (2026-07-20).
- MCP directories NOT YET listed on: Smithery (smithery.ai, `smithery mcp
  publish`), Glama (glama.ai, auto-indexes GitHub, claim it), PulseMCP
  (pulsemcp.com, Submit button), MCP.so (mcp.so, GitHub issues), official MCP
  registry (registry.modelcontextprotocol.io, PR to
  github.com/modelcontextprotocol/servers), awesome-mcp-servers (punkpeye +
  wong2 + appcypher GitHub PRs). These are free passive long-tail; do this week.
- opencode config (`C:/Users/Dondai/.config/opencode/opencode.json`): hound
  (local), crawl4ai (local shim at `C:/Users/Dondai/.pi/agent/workspace/crawl4ai-venv/crawl4ai_mcp_shim.py`),
  parallel-search (remote at `https://search.parallel.ai/mcp`), agent-browser,
  playwright, mcptokens. Built-in websearch/webfetch/codesearch disabled.
- Open WebUI: hound works natively via streamable HTTP (v9.1.0), no mcpo proxy.
- Pi extension: npm @houndmcp/hound-mcp-pi@10.5.0 (pi-package keyword, shows on
  pi.dev/packages). Install: pi install npm:@houndmcp/hound-mcp-pi.
  Docker reply draft: C:/Users/Dondai/More Files/Desktop/hound-reply-docker.md
