GroktoCrawl is a self-hosted, MIT-licensed alternative to Firecrawl. It implements the entire Firecrawl v2 API surface as a set of Python FastAPI microservices running in Docker. It provides web scraping, crawling, search, browser automation, document parsing, structured data extraction, and an autonomous AI research agent - all behind a single REST API on port 8080.
plugins/web/firecrawl/) already exists and talks to
api.firecrawl.dev. Our plugin will be structurally identical but point at
http://localhost:8080 instead - giving you a free, self-hosted, unlimited
replacement for the paid Firecrawl cloud.
The repo at C:\Users\Gaem4090\groktocrawl contains these services, all
orchestrated by a single docker-compose.yml:
| Service | Port | Purpose | Hermes Tool(s) |
|---|---|---|---|
agent-svc | :8080 | Main API gateway. All 27 endpoints. Job orchestration, agent research loop, crawl engine. | ALL tools |
scraper-svc | :8001 | URL to markdown. Three-tier: llms.txt, Accept header, Playwright+readability. 22 site adapters. | gtc_scrape, gtc_crawl |
slopsearx | :8081 | SearXNG meta-search. Proxies to Brave Search API. | gtc_search, gtc_agent, gtc_answer |
semantic-svc | :8003 | Vector indexing & semantic search. Qdrant + BAAI/bge-m3 (1024-dim). | gtc_search (rich), gtc_agent |
qdrant | internal | Vector database. 4GB memory limit. | via semantic-svc |
browser-svc | internal | Playwright headless Chrome. JS-rendered pages + /v2/browser sessions. | gtc_browser |
parse-svc | internal | Document parsing: PDF, EPUB, DOCX, PPTX, XLSX to markdown. | gtc_parse |
valkey | internal | Redis-compatible. Job store, scrape cache, rate limiting. | ALL (infrastructure) |
portal-svc | :8082 | Web dashboard. Monitor jobs, view results. | Optional (human) |
ofelia | internal | Docker job scheduler. Runs monitor cron jobs. | gtc_monitor |
ghcr.io/groktopus/groktocrawl-agentghcr.io/groktopus/groktocrawl-scraperghcr.io/groktopus/groktocrawl-browserghcr.io/groktopus/groktocrawl-semanticghcr.io/groktopus/groktocrawl-portalExtracted from openapi.json in the repo. All endpoints are on
http://localhost:8080 (the agent-svc container).
| Method | Path | Purpose | Hermes Tool |
|---|---|---|---|
| GET | /health | Health check | internal |
| POST | /v2/scrape | Scrape a single URL to markdown | gtc_scrape |
| POST | /v2/crawl | Crawl a website (BFS, configurable depth/limit) | gtc_crawl |
| GET | /v2/crawl/active | List active crawl jobs | gtc_crawl |
| GET | /v2/crawl/{job_id} | Get crawl job status | gtc_crawl |
| DELETE | /v2/crawl/{job_id} | Cancel a crawl | gtc_crawl |
| GET | /v2/crawl/{job_id}/errors | Get crawl errors | gtc_crawl |
| GET | /v2/crawl/{job_id}/stream | SSE stream of crawl events | internal |
| POST | /v2/search | Web search (fast or rich mode) | gtc_search |
| POST | /v2/agent | Autonomous research agent (SSE streaming) | gtc_agent |
| GET | /v2/agent/{job_id} | Get agent job status | gtc_agent |
| DELETE | /v2/agent/{job_id} | Cancel agent job | gtc_agent |
| POST | /v2/answer | Grounded Q&A with citations (search+scrape+LLM) | gtc_answer |
| POST | /v2/extract | Structured data extraction from URLs | gtc_extract |
| GET | /v2/extract/{job_id} | Get extract job status | gtc_extract |
| POST | /v2/map | Discover all URLs on a site | gtc_map |
| POST | /v2/browser | Create a browser session | gtc_browser |
| GET | /v2/browser | List browser sessions | gtc_browser |
| DELETE | /v2/browser/{session_id} | Destroy browser session | gtc_browser |
| POST | /v2/browser/{session_id}/execute | Execute browser actions (click, type, scroll) | gtc_browser |
| POST | /v2/monitor | Create a change monitor | gtc_monitor |
| GET | /v2/monitor | List monitors | gtc_monitor |
| GET | /v2/monitor/{id} | Get monitor status | gtc_monitor |
| PATCH | /v2/monitor/{id} | Update monitor | gtc_monitor |
| DELETE | /v2/monitor/{id} | Delete monitor | gtc_monitor |
| POST | /v2/parse | Parse a document file to markdown | gtc_parse |
| POST | /v2/batch/scrape | Batch scrape multiple URLs | gtc_batch_scrape |
| POST | /v2/generate-llmstxt | Generate llms.txt for a website | gtc_llmstxt |
| POST | /v2/crawl/params-preview | Preview crawl parameters (NL to params) | internal |
C:\Users\Gaem4090\groktocrawl.
Docker v29.5.3 + Compose v5.1.4 are installed and running. WSL2 (Ubuntu) is available.
Copy the sample env and configure the LLM provider. GroktoCrawl needs an LLM for the
/v2/agent and /v2/answer endpoints. You can use your Featherless API key here.
# From the groktocrawl directory
cd C:\Users\Gaem4090\groktocrawl
cp .env.sample .env
# Edit .env - set these required values:
# LLM provider (use Featherless - you already have this)
LLM_API_KEY=your-featherless-key
LLM_BASE_URL=https://api.featherless.ai/v1
LLM_MODEL=meta-llama/Llama-3.3-70B-Instruct
# Search backend (Brave API key - get free at https://brave.com/search/api/)
BRAVE_API_KEY=your-brave-search-api-key
# Optional: protect your API with a key
# API_KEY=generate-a-random-secret-here
slopsearx service needs a Brave Search API key
for web search. Get a free key at brave.com/search/api
(2,000 queries/month free). Without it, /v2/search, /v2/agent, and
/v2/answer won't work, but /v2/scrape, /v2/crawl, and
/v2/map will still function.
cd C:\Users\Gaem4090\groktocrawl
# Pull pre-built images and start all services
docker compose up -d
# Watch the logs to confirm everything starts
docker compose logs -f agent-svc
# Verify the API is healthy (in another terminal)
curl http://localhost:8080/health
Expected response: {"status":"healthy"}
This starts 10 containers: agent-svc, scraper-svc, slopsearx,
semantic-svc, qdrant, browser-svc, parse-svc,
valkey, portal-svc, ofelia.
# Scrape a page
curl -X POST http://localhost:8080/v2/scrape \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com"}'
# Search the web
curl -X POST http://localhost:8080/v2/search \
-H "Content-Type: application/json" \
-d '{"query":"rust programming","limit":3}'
# Map a site
curl -X POST http://localhost:8080/v2/map \
-H "Content-Type: application/json" \
-d '{"url":"https://docs.python.org","limit":10}'
# Portal dashboard
open http://localhost:8082
| Variable | Required | Default | Purpose |
|---|---|---|---|
LLM_API_KEY | For agent/answer | - | LLM provider API key |
LLM_BASE_URL | For agent/answer | http://llm-svc:8011/v1 | OpenAI-compatible LLM endpoint |
LLM_MODEL | For agent/answer | fixture-model | Model name |
BRAVE_API_KEY | For search | - | Brave Search API key |
API_KEY | Optional | - | Protect your API with a bearer token |
AGENT_PORT | Optional | 8080 | agent-svc port |
GITHUB_TOKEN | Optional | - | GitHub adapter (higher rate limit) |
SCRAPER_PROXY_URL | Optional | - | Outbound proxy for scraping |
The Hermes plugin reads one env var to know where GroktoCrawl is:
# Set in Hermes config or ~/.hermes/.env
GROKTOCRAWL_API_URL=http://localhost:8080
# If you set API_KEY in GroktoCrawl's .env, also set:
GROKTOCRAWL_API_KEY=your-api-key-here
groktocrawl in the repo root)
already reads GROKTOCRAWL_API_URL from env or ~/.hermes/.env. The CLI is a
1000+ line Python script with subcommands for every endpoint. Our Hermes plugin will reuse the same
env var convention.
Hermes has a first-class plugin system. Plugins live in
~/.hermes/plugins/ (user plugins) or plugins/ (bundled).
Each plugin is a Python package with an __init__.py that exports a
register(ctx) function. The PluginContext object provides
ctx.register_tool() which registers tools in the global tool registry.
model_tools.py calls discover_plugins() at startupPluginManager.discover_and_load() scans ~/.hermes/plugins/ for directories with __init__.pyregister(ctx) is called with a PluginContextctx.register_tool(name, toolset, schema, handler, ...) adds tools to the global registrydef register_tool(
self,
name: str, # Tool name (e.g. "gtc_scrape")
toolset: str, # Toolset group (e.g. "groktocrawl")
schema: dict, # JSON schema for the tool
handler: Callable, # Async or sync function called when tool is invoked
check_fn: Callable | None = None, # Availability check (return False to hide)
requires_env: list | None = None, # Env vars that must be set
is_async: bool = False, # Whether handler is async
description: str = "",
emoji: str = "",
override: bool = False, # True to replace existing tool
) -> None
~/.hermes/plugins/
web/
groktocrawl/ # <-- Our new plugin
__init__.py # register(ctx) entry point
provider.py # HTTP client + tool handlers
manifest.yaml # Plugin metadata (optional)
plugins/web/firecrawl/ in the Hermes repo is the structural template.
It has the same __init__.py + provider.py pattern and registers
a WebSearchProvider. Our plugin will register individual tools instead, giving
the agent direct access to all 27 GroktoCrawl endpoints.
These 12 tools will be registered as native Hermes tools. The agent (me) will be able to
call them directly, just like web_search or terminal. Each tool
maps to one or more GroktoCrawl API endpoints.
Scrape a single URL to markdown. Uses the three-tier strategy (llms.txt, Accept header, Playwright). 22 site-specific adapters for GitHub, YouTube, Project Gutenberg, security databases, etc.
{
"name": "gtc_scrape",
"description": "Scrape a single URL and return clean markdown. Handles JS-rendered pages, PDFs, and site-specific adapters (GitHub, YouTube, etc.).",
"parameters": {
"type": "object",
"properties": {
"url": {"type": "string", "description": "The URL to scrape"},
"formats": {"type": "array", "items": {"type": "string"}, "default": ["markdown"], "description": "Output formats: markdown, html, links, rawHtml"},
"only_main_content": {"type": "boolean", "default": true, "description": "Strip nav/header/footer boilerplate"},
"timeout": {"type": "integer", "default": 30000, "description": "Per-page timeout in ms"}
},
"required": ["url"]
}
}
Crawl a website with BFS. Configurable depth, page limit, path filtering. Returns a job ID; poll for results.
{
"name": "gtc_crawl",
"description": "Crawl a website starting from a URL. Returns a job_id - poll with action='status'. Configurable depth, page limit, and path patterns.",
"parameters": {
"type": "object",
"properties": {
"url": {"type": "string", "description": "Starting URL"},
"action": {"type": "string", "enum": ["start", "status", "cancel", "errors", "active"], "default": "start"},
"job_id": {"type": "string", "description": "Job ID for status/cancel/errors actions"},
"max_depth": {"type": "integer", "default": 2},
"limit": {"type": "integer", "default": 20, "description": "Max pages to crawl"},
"includes": {"type": "array", "items": {"type": "string"}, "description": "Glob patterns to include"},
"excludes": {"type": "array", "items": {"type": "string"}, "description": "Glob patterns to exclude"}
},
"required": ["url", "action"]
}
}
Web search via SearXNG/Brave. Two modes: fast (raw results, <1s)
and rich (scrapes top results + LLM synthesis, 1-3s).
{
"name": "gtc_search",
"description": "Search the web. Fast mode returns raw results (<1s). Rich mode scrapes top results and synthesizes with LLM (1-3s).",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string"},
"limit": {"type": "integer", "default": 5},
"search_type": {"type": "string", "enum": ["fast", "rich"], "default": "fast"},
"sources": {"type": "array", "items": {"type": "string"}, "description": "Search verticals"},
"categories": {"type": "array", "items": {"type": "string"}, "description": "SearXNG categories"}
},
"required": ["query"]
}
}
Autonomous research agent. Searches the web, scrapes relevant pages, and synthesizes a cited answer. Supports SSE streaming. This is GroktoCrawl's most powerful endpoint - it's a mini research agent inside your scraping API.
{
"name": "gtc_agent",
"description": "Start an autonomous research agent. It searches the web, scrapes sources, and synthesizes a cited answer. Use for complex research questions.",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Research question"},
"action": {"type": "string", "enum": ["start", "status", "cancel"], "default": "start"},
"job_id": {"type": "string", "description": "For status/cancel"},
"max_searches": {"type": "integer", "default": 5, "description": "Max search rounds"},
"model": {"type": "string", "description": "Override LLM model for this job"}
},
"required": ["query", "action"]
}
}
Grounded Q&A with inline citations. Synchronous (1-3s). Search, scrape top results, LLM synthesis with [N] citation markers.
{
"name": "gtc_answer",
"description": "Answer a question with grounded citations. Searches, scrapes top results, and synthesizes with [N] citation markers. 1-3s latency.",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string"},
"num_sources": {"type": "integer", "default": 5, "minimum": 1, "maximum": 20},
"model": {"type": "string", "description": "Override LLM model"}
},
"required": ["query"]
}
}
Structured data extraction. Provide a JSON schema, get structured data from URLs. Uses LLM to extract fields from scraped content.
{
"name": "gtc_extract",
"description": "Extract structured data from URLs using a JSON schema. LLM-powered extraction.",
"parameters": {
"type": "object",
"properties": {
"urls": {"type": "array", "items": {"type": "string"}},
"action": {"type": "string", "enum": ["start", "status"], "default": "start"},
"job_id": {"type": "string"},
"output_schema": {"type": "object", "description": "JSON schema for extracted data"},
"system_prompt": {"type": "string", "description": "Guide extraction behavior"}
},
"required": ["urls", "action"]
}
}
Discover all URLs on a site. Uses sitemap.xml, robots.txt, and link extraction.
{
"name": "gtc_map",
"description": "Discover all URLs on a website. Uses sitemap.xml, robots.txt, and HTML link extraction.",
"parameters": {
"type": "object",
"properties": {
"url": {"type": "string"},
"limit": {"type": "integer", "default": 50}
},
"required": ["url"]
}
}
Remote browser sessions. Create a Playwright session, execute actions (click, type, scroll, screenshot, executeScript), destroy session.
{
"name": "gtc_browser",
"description": "Manage remote Playwright browser sessions. Create, execute actions (click/type/scroll/screenshot), list, and destroy.",
"parameters": {
"type": "object",
"properties": {
"action": {"type": "string", "enum": ["create", "list", "destroy", "execute"]},
"session_id": {"type": "string", "description": "For destroy/execute"},
"actions": {"type": "array", "items": {"type": "object"}, "description": "Browser actions for execute"},
"ttl": {"type": "integer", "default": 300, "description": "Session TTL in seconds"}
},
"required": ["action"]
}
}
Change monitoring. Set up monitors that periodically scrape a URL and alert on changes. Managed by the Ofelia scheduler.
{
"name": "gtc_monitor",
"description": "Manage change monitors. Create monitors that periodically check a URL and alert on content changes.",
"parameters": {
"type": "object",
"properties": {
"action": {"type": "string", "enum": ["create", "list", "get", "update", "delete"]},
"monitor_id": {"type": "string"},
"url": {"type": "string", "description": "URL to monitor (for create)"},
"interval": {"type": "string", "default": "1h", "description": "Check interval"},
"selector": {"type": "string", "description": "CSS selector to watch"}
},
"required": ["action"]
}
}
Document parsing. Upload a file (PDF, EPUB, DOCX, PPTX, XLSX) and get markdown back.
{
"name": "gtc_parse",
"description": "Parse a document file (PDF, EPUB, DOCX, PPTX, XLSX) to markdown.",
"parameters": {
"type": "object",
"properties": {
"file_path": {"type": "string", "description": "Local file path to parse"}
},
"required": ["file_path"]
}
}
Generate an llms.txt file for a website. Crawls the site and produces a structured summary for LLM consumption.
{
"name": "gtc_llmstxt",
"description": "Generate an llms.txt file for a website. Crawls and produces a structured summary for LLMs.",
"parameters": {
"type": "object",
"properties": {
"url": {"type": "string"},
"action": {"type": "string", "enum": ["start", "status"], "default": "start"},
"job_id": {"type": "string"},
"max_urls": {"type": "integer", "default": 10}
},
"required": ["url", "action"]
}
}
Batch scrape multiple URLs. Submit a list of URLs, get all results back.
{
"name": "gtc_batch_scrape",
"description": "Scrape multiple URLs in batch. Returns a job_id for polling.",
"parameters": {
"type": "object",
"properties": {
"urls": {"type": "array", "items": {"type": "string"}},
"action": {"type": "string", "enum": ["start", "status", "cancel", "errors"], "default": "start"},
"job_id": {"type": "string"}
},
"required": ["urls", "action"]
}
}
The plugin is a thin HTTP client. Each tool handler makes an HTTP request to the GroktoCrawl API and returns the response. Here is the complete plugin code:
"""GroktoCrawl web scraping plugin - self-hosted Firecrawl alternative."""
from __future__ import annotations
from plugins.web.groktocrawl.provider import GroktoCrawlProvider
def register(ctx) -> None:
"""Register all GroktoCrawl tools with the plugin context."""
provider = GroktoCrawlProvider()
provider.register_tools(ctx)
"""GroktoCrawl provider - HTTP client + tool handlers."""
from __future__ import annotations
import os
import httpx
from typing import Any
def _get_api_url() -> str:
return os.getenv("GROKTOCRAWL_API_URL", "http://localhost:8080").rstrip("/")
def _get_api_key() -> str | None:
return os.getenv("GROKTOCRAWL_API_KEY")
def _headers() -> dict:
h = {"Content-Type": "application/json"}
key = _get_api_key()
if key:
h["Authorization"] = f"Bearer {key}"
return h
def _is_available() -> bool:
"""Check if GroktoCrawl API is reachable."""
try:
r = httpx.get(f"{_get_api_url()}/health", timeout=3)
return r.status_code == 200
except Exception:
return False
class GroktoCrawlProvider:
"""Registers 12 tools that proxy to the GroktoCrawl API."""
def register_tools(self, ctx) -> None:
ctx.register_tool(
name="gtc_scrape",
toolset="groktocrawl",
schema={
"type": "function",
"function": {
"name": "gtc_scrape",
"description": "Scrape a URL to clean markdown.",
"parameters": {
"type": "object",
"properties": {
"url": {"type": "string"},
"formats": {"type": "array", "items": {"type": "string"}, "default": ["markdown"]},
"only_main_content": {"type": "boolean", "default": True},
"timeout": {"type": "integer", "default": 30000},
},
"required": ["url"],
},
},
},
handler=self._scrape,
check_fn=lambda: _is_available(),
requires_env=["GROKTOCRAWL_API_URL"],
is_async=True,
description="Scrape a URL to markdown",
emoji="spider",
)
# ... 11 more tools registered the same way
# (gtc_crawl, gtc_search, gtc_agent, gtc_answer, gtc_extract,
# gtc_map, gtc_browser, gtc_monitor, gtc_parse, gtc_llmstxt,
# gtc_batch_scrape)
async def _scrape(self, url, formats=None, only_main_content=True, timeout=30000):
body = {"url": url, "formats": formats or ["markdown"],
"onlyMainContent": only_main_content, "timeout": timeout}
async with httpx.AsyncClient(timeout=60) as client:
r = await client.post(f"{_get_api_url()}/v2/scrape",
headers=_headers(), json=body)
r.raise_for_status()
return r.json()
async def _crawl(self, url, action="start", job_id=None,
max_depth=2, limit=20, includes=None, excludes=None):
base = _get_api_url()
if action == "start":
body = {"url": url, "maxDepth": max_depth, "limit": limit}
if includes: body["includes"] = includes
if excludes: body["excludes"] = excludes
async with httpx.AsyncClient(timeout=30) as c:
r = await c.post(f"{base}/v2/crawl", headers=_headers(), json=body)
r.raise_for_status()
return r.json()
elif action == "status" and job_id:
async with httpx.AsyncClient(timeout=30) as c:
r = await c.get(f"{base}/v2/crawl/{job_id}", headers=_headers())
r.raise_for_status()
return r.json()
elif action == "cancel" and job_id:
async with httpx.AsyncClient(timeout=30) as c:
r = await c.delete(f"{base}/v2/crawl/{job_id}", headers=_headers())
r.raise_for_status()
return r.json()
elif action == "active":
async with httpx.AsyncClient(timeout=30) as c:
r = await c.get(f"{base}/v2/crawl/active", headers=_headers())
r.raise_for_status()
return r.json()
async def _search(self, query, limit=5, search_type="fast",
sources=None, categories=None):
body = {"query": query, "limit": limit, "searchType": search_type}
if sources: body["sources"] = sources
if categories: body["categories"] = categories
async with httpx.AsyncClient(timeout=30) as c:
r = await c.post(f"{_get_api_url()}/v2/search", headers=_headers(), json=body)
r.raise_for_status()
return r.json()
async def _agent(self, query, action="start", job_id=None,
max_searches=5, model=None):
base = _get_api_url()
if action == "start":
body = {"query": query, "maxSearches": max_searches}
if model: body["model"] = model
async with httpx.AsyncClient(timeout=30) as c:
r = await c.post(f"{base}/v2/agent", headers=_headers(), json=body)
r.raise_for_status()
return r.json()
elif action == "status" and job_id:
async with httpx.AsyncClient(timeout=30) as c:
r = await c.get(f"{base}/v2/agent/{job_id}", headers=_headers())
r.raise_for_status()
return r.json()
async def _answer(self, query, num_sources=5, model=None):
body = {"query": query, "numSources": num_sources}
if model: body["model"] = model
async with httpx.AsyncClient(timeout=60) as c:
r = await c.post(f"{_get_api_url()}/v2/answer", headers=_headers(), json=body)
r.raise_for_status()
return r.json()
async def _map(self, url, limit=50):
async with httpx.AsyncClient(timeout=30) as c:
r = await c.post(f"{_get_api_url()}/v2/map", headers=_headers(),
json={"url": url, "limit": limit})
r.raise_for_status()
return r.json()
async def _browser(self, action, session_id=None, actions=None, ttl=300):
base = _get_api_url()
if action == "create":
async with httpx.AsyncClient(timeout=30) as c:
r = await c.post(f"{base}/v2/browser", headers=_headers(), json={"ttl": ttl})
r.raise_for_status()
return r.json()
elif action == "list":
async with httpx.AsyncClient(timeout=30) as c:
r = await c.get(f"{base}/v2/browser", headers=_headers())
r.raise_for_status()
return r.json()
elif action == "destroy" and session_id:
async with httpx.AsyncClient(timeout=30) as c:
r = await c.delete(f"{base}/v2/browser/{session_id}", headers=_headers())
r.raise_for_status()
return r.json()
elif action == "execute" and session_id:
async with httpx.AsyncClient(timeout=60) as c:
r = await c.post(f"{base}/v2/browser/{session_id}/execute",
headers=_headers(), json={"actions": actions or []})
r.raise_for_status()
return r.json()
async def _parse(self, file_path):
import pathlib
p = pathlib.Path(file_path)
if not p.exists():
return {"error": f"File not found: {file_path}"}
async with httpx.AsyncClient(timeout=120) as c:
with open(p, "rb") as f:
r = await c.post(f"{_get_api_url()}/v2/parse",
headers={k: v for k, v in _headers().items()
if k != "Content-Type"},
files={"file": (p.name, f)})
r.raise_for_status()
return r.json()
__init__.py, one provider.py.
The handler functions are async, use httpx (already in Hermes deps), and return
JSON dicts. Hermes handles the rest - schema generation, tool dispatch, result formatting.
# Health check
curl http://localhost:8080/health
# Expected: {"status":"healthy"}
# Test scrape
curl -X POST http://localhost:8080/v2/scrape \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com"}'
# Check Hermes sees the plugin
hermes plugins list
# Should show: groktocrawl (web) - 12 tool(s)
# Check tools are registered
hermes tools list | grep gtc_
# Should show all 12 tools:
# gtc_scrape, gtc_crawl, gtc_search, gtc_agent, gtc_answer,
# gtc_extract, gtc_map, gtc_browser, gtc_monitor, gtc_parse,
# gtc_llmstxt, gtc_batch_scrape
Ask the agent (me) to use the tools:
# In Hermes chat:
"Use gtc_scrape to scrape https://example.com and tell me what it says"
"Use gtc_search to search for 'rust async programming' with 3 results"
"Use gtc_map to find all URLs on https://docs.python.org"
Open http://localhost:8082 in your browser to see job history, active crawls, and monitor status.
# Start the stack
cd C:\Users\Gaem4090\groktocrawl
docker compose up -d
# Stop the stack
docker compose down
# Check logs
docker compose logs -f agent-svc # API logs
docker compose logs -f scraper-svc # Scraper logs
docker compose logs -f slopsearx # Search logs
# Check container status
docker compose ps
# Restart a single service
docker compose restart agent-svc
cd C:\Users\Gaem4090\groktocrawl
git pull
docker compose pull # Pull new images
docker compose up -d # Recreate changed containers
| Container | RAM (typical) | CPU |
|---|---|---|
| agent-svc | ~100MB | Low (idle), spikes on jobs |
| scraper-svc | ~150MB | Medium during scraping |
| browser-svc | ~300MB | High when rendering JS |
| semantic-svc | ~500MB | High during indexing |
| qdrant | ~500MB-4GB | Low (capped at 4GB) |
| valkey | ~50MB | Low |
| slopsearx | ~100MB | Low |
| Total | ~1.5-5.5GB |
| Problem | Solution |
|---|---|
| Tools do not appear in Hermes | Check hermes plugins list for errors. Ensure ~/.hermes/plugins/web/groktocrawl/__init__.py exists and has register(ctx) |
| Tool calls return connection error | Verify curl http://localhost:8080/health works. Check GROKTOCRAWL_API_URL env var |
| Search returns no results | Check BRAVE_API_KEY in .env. Check docker compose logs slopsearx |
| Agent/answer returns errors | Check LLM_API_KEY and LLM_BASE_URL in .env. Check docker compose logs agent-svc |
| Scraping JS-heavy sites fails | Check docker compose logs browser-svc. Browser-svc needs Playwright/Chromium |
| Qdrant OOM | Increase mem_limit in docker-compose.yml or reduce VECTOR_INDEX_MAX_DOCS |