# Plan: Web Crawler **Goal:** Maven can crawl web pages on demand or on a schedule — fetch page content, extract structured data (via LLM or CSS selectors), and store results as facts, notes, or reminders. Used for: price monitoring, documentation updates, recipe extraction, content summarization. **Done when:** - `internal/crawl/` package — HTTP fetcher with polite defaults (rate limiting, robots.txt respect, user-agent) - Content extraction: HTML→plaintext (Go stdlib `golang.org/x/net/html`), or full-page LLM summarization - Crawl scheduler in config — `crawls: [{name, url, selector, schedule, store_as}]` - On-demand crawl via voice: "maven, посмотри страницу X и запиши цену" - Results are stored as facts/notes through `ipc.CoreAPI` - Crawl history visible on mavweb `/tools` page **Scope:** - New `internal/crawl/` package — fetcher, parser, scheduler, extractor - Config extension: `crawls` array in `config.Config` - Reuses `internal/llm.Client` for intelligent extraction (e.g., "extract the price, description, and availability from this page") - Reuses `internal/ipc.CoreAPI` for storing results - Reuses `internal/router.Embedder` for deduplication (don't re-store identical content) - Reuses `internal/routine.Routine` mechanics for scheduled crawls **Steps:** 1. Create `internal/crawl/fetcher.go` — `Fetch(url) ([]byte, error)`: HTTP GET with timeout (30s), rate limiting (1 req/sec), `robots.txt` check via `github.com/temoto/robotstxt` 2. Create `internal/crawl/extractor.go` — `Extract(html []byte, extraction_type string) (map[string]string, error)`: for simple extraction use CSS selector (`github.com/PuerkitoBio/goquery`); for complex extraction use `llm.Client` with a prompt 3. Create `internal/crawl/scheduler.go` — `Scheduler` that reads `crawls` config, runs each on its cron schedule, tracks last-run via facts 4. Create `internal/crawl/dedup.go` — compute content hash, skip if identical to last fetched (stored as fact `kind=config, key=crawl:hash:`) 5. Wire on-demand crawl into `IntentAct` — new tool verb `crawl` that accepts a URL argument 6. Wire scheduled crawls into `cmd/mavend/main.go` — separate goroutine manages the crawl scheduler 7. Add IPC methods `MethodTriggerCrawl(name)`, `MethodListCrawls`, `MethodGetCrawlResult(name)` 8. Add `crawls` block to `config.Config` and `deploy/mavend.json` 9. Test with a static HTML page — verify extraction matches expected values, verify scheduling fires correctly ## Shipped 2026-08-01 (#259) Built as `internal/crawl` (pure: robots, extraction, watcher) plus `cmd/mavend/crawls.go` (fetcher, ticker, dedup facts), on top of the guarded `internal/webfetch` door added with the feed reader (#258). Off unless configured, in two separately-switched halves: `crawl.on_demand` for a URL he names, `crawl.watches` for a scheduled re-read. **Limits are code, not documentation** (`internal/webfetch`, tested one test per limit): host allowlist/denylist, no private addresses (loopback, RFC1918 — hence the LAN and the `10.42.0.0/24` wg range —, link-local incl. cloud metadata, CGNAT, v6 ULA) enforced in the dialer's `Control` hook so DNS rebinding and every redirect hop are covered, response size cap, redirect cap, timeout, one request per host per second. `robots.txt` is fetched first, cached per host, and a `Disallow` is refused with no override. Deliberate deviations from the plan above: - **No CSS selectors and no LLM structured extraction** (steps 2). The output is plaintext handed to the phraser as context for the question he asked. A 1.7B extracting a JSON price table from 4000 runes is a worse bet than reading, and `goquery` is not vendored. - **No `crawl` act verb and no new IPC methods** (steps 5, 7). Reading a page is a query source (`queryWeb` in `actions_query.go`, last in the chain, behind Kiwix once that is wired), not an action he commands. Nothing needs a new wire method to work. - **Notes, not facts.** A page's text is not a fact about him. Only the dedup hash is a fact (`crawl:hash:`, kind `config`, source `poll:crawl`). - **Nothing is dispatched.** A changed page writes a note; it does not nudge. Not a nag. - **No `/tools` crawl history page.** The notes and the hash facts are already visible on `/dash`. **No new dependency.** The vendored tree has no `x/net/html`, no `goquery` and no `temoto/robotstxt`, so robots parsing and HTML-to-text are stdlib (`regexp`, `html`) — RE2 has no backreferences, hence the `pairsRE` builder in `extract.go`.