从源码构建需 Zig ≥ 0.16.0;CDP 模式需本机有 Chrome/Chromium,否则用 kuri-fetch 独立模式;默认监听 127.0.0.1:8080,可用 KURI_SECRET 开启鉴权。

kuri
justrach活跃维护用 Zig 写的浏览器自动化与移动端控制工具,给 AI Agent 提供省 token 的 CDP 快照、HAR 录制和原生 adb 客户端,统一搞定网页抓取和 iOS/Android 设备操作。
Browser automation, web crawling, and iOS + Android device control for AI agents. Zig-native, token-efficient CDP snapshots, HAR recording, native adb wire-protocol client, and a standalone fetcher.
curl -fsSL https://kuri.trilok.ai/download | shREADME 未说明显存要求;CDP 模式需本机安装 Chrome/Chromium,独立抓取模式无需浏览器;预编译包支持 macOS arm64/x86_64 与 Linux x86_64/arm64。
用 Zig 写的浏览器自动化与设备控制工具,为 AI agent 提供低 token 的 CDP 快照和 HTTP 接口
用 Zig 写的浏览器自动化与移动端控制工具,给 AI Agent 提供省 token 的 CDP 快照、HAR 录制和原生 adb 客户端,统一搞定网页抓取和 iOS/Android 设备操作。
核心能力
- 用 @eN 紧凑引用格式输出无障碍树快照,降低 token 消耗
- 提供 135 个 HTTP 端点,覆盖导航、点击、截图、HAR 录制等
- POST /batch 单次调用批量执行多条命令,减少往返
- 内置原生 adb 协议客户端,可控制 iOS 与 Android 设备
- kuri-fetch 独立抓取网页,无需 Chrome 运行时
容易踩的坑
- v0.6.1最新正式版2026-10-09 · 2026-10-09 09:25 UTC
v0.6.1
Fixes
- Telemetry skips
/health— container liveness probes (Docker/k8shttpGet,curl /health) call/healthevery few seconds, so they made up almost every recorded request. They are no longer recorded; every other route is recorded as before.
Note — macOS assets in this release are not signed or notarized. Gatekeeper will quarantine them; clear it with
xattr -d com.apple.quarantine <binary>. - Telemetry skips
- v0.6.02026-08-04 · 2026-08-04 02:20 UTC
查看更新说明
v0.6.0
Features
- Xcode build commands in kuri-mobile —
ios list-schemes,build,build-run,test,product, andcleandelegate to xcodebuild through the existing toolchain resolution, so they work even whenxcode-selectpoints at CommandLineTools.build-rungoes from source to a running simulator app in one command: build, resolve the product, install on the booted simulator, launch. - Session defaults —
ios defaults set|show|clearpersistsproject,scheme,configuration, andudidso the build-family commands can be invoked with no arguments. Explicit flags and positionals always win, and no other command consults the stored values. - MCP server —
kuri-mobile mcpserves every iOS and Android command plusdoctor(79 tools) over MCP stdio, built on the mcp-zig library. The tool registry is generated at comptime from the same tables that render the CLI help, so new commands appear as MCP tools with no extra work. Measured against XcodeBuildMCP 2.7.0 driving the same project: ready to serve in ~9 ms vs ~414 ms,tools/listin ~0.4 ms vs ~10–25 ms, warm incremental build-run ~2.2 s vs ~2.9–3.1 s.
Removed
connectfeature and nanostore dependency — the/connect/*routes,kuri-agent connect,kuri-connect-broker, and relay-fetch mode shipped in v0.5.1 are removed from main. They depended on the privatejustrach/nanostoresibling library, which made every build of kuri require a private checkout plus aNANOSTORE_PATsecret in CI, and its pinned commit did not compile on the project's Zig 0.17.0-dev toolchain. Building kuri now needs only this repo.KURI_VAULT_PASSPHRASE,KURI_RELAY, andKURI_BROKER*no longer have any effect.
macOS assets in this release are signed and notarized.
- Xcode build commands in kuri-mobile —
- v0.5.12026-07-31 · 2026-07-31 09:28 UTC
查看更新说明
v0.5.1
Features
- Diff-first agent observations —
/diff/snapshotand MCPtake_snapshot_diffreturn only page changes after the first observation, with a full-snapshot fallback when navigation replaces the page. - Richer snapshot targeting — stable refs, scoped re-capture, optional hierarchy output, and list truncation are available through the HTTP and MCP snapshot surfaces;
get_page_stateprovides a compact orientation payload. - HAR capture and interaction replay — API-shaped response capture, CDP action dispatch, and accessibility-signature replay are available through
/har/*,/dispatch, and/replay. - Anonymous usage telemetry (opt-out) — kuri records aggregate, non-identifying usage signal (route name, method, status, latency, response byte count, platform, version, and a random per-install id) and ships it as an OpenTelemetry-aligned
kuri.telemetry.v1payload. On by default; disable withKURI_NO_TELEMETRY=1or--no-telemetry. Override the endpoint withKURI_TELEMETRY_URL. Never collects URLs, page content, cookies, selectors, or anything that identifies a user — the query string is stripped before telemetry sees a request. See docs/telemetry.md.
Fixes
- Single-sourced versions —
kuri,kuri-fetch,kuri-browse, andkuri-mcpnow read their version frombuild.zig.zonthroughbuild_options. - Background-safe iOS input — tap, swipe, and type operations now require explicit
--host-input/--allow-host-inputconsent (orKURI_ALLOW_HOST_INPUT=1) and simulator window geometry failures report actionable diagnostics. - Resilient Chrome/CDP launch — failed DevTools startup retries with a fresh port and stale-lock cleanup, and connection failures surface actionable errors instead of terminating the server abruptly.
Release verification
- Built with Zig 0.16.0 on Darwin arm64 from commit
a48d24f3f49e75619d5f69d0dde574ad8f7ed46d. - macOS arm64 notarization was accepted by Apple as submission
b758aa90-b7b2-4e87-af61-4687763d7acf. - macOS x86_64 notarization was accepted by Apple as submission
7894cfd0-cfdd-4c45-8cd4-dd617288dee3. - The macOS deliverables contain signed bare executables.
xcrun staplerreturned Error 73 for those standalone binaries, so the archives are notarized but not described as stapled.
- Diff-first agent observations —
- v0.4.142026-07-26 · 2026-07-26 04:33 UTC
查看更新说明
v0.4.14
Android element extraction, rebuilt.
uitreereported what was in the XML; what an agent needs is what can be acted on, and named the way it reads on screen. Prompted by a comparison against CursorTouch/Android-MCP, whose element handling was better than ours even though kuri's command surface is roughly twice the size.Fixes
android tap --labelnever worked. The tool table advertised the flag andandroid toolsprinted it, but the dispatcher only ever read positional coordinates — every documented invocation failed withmissing argument: x y. It now resolves selectors, and the same selectors work onfind- No
cli.zigtest had ever run.main.zigaggregates tests through an explicit_ = @import(...)block and both the Android and iOS dispatchers were missing from it, so theirtestblocks compiled but never executed and a deliberately failing assertion still reported success. That is how a flag could be advertised and unimplemented at the same time. Both are now in the list - uiautomator attribute values were never XML-decoded. A settings row labelled "Network & internet" was reported as
Network & internet, so the text an agent read differed from the text on screen and--label "Network & internet"could not match it. Named and numeric references are both decoded now, and an ampersand that is not part of an entity is preserved rather than swallowed - Elements with no label were dropped entirely. The filter kept anything clickable or carrying text/id/content-desc, which discards a scrollable
RecyclerViewor an unlabelledSwitch— there was no way to discover the list could be scrolled
Features — Android
android state(aliassnapshot). Foreground activity, screen size and every actionable element with tap-ready centroids, in one call instead of three (current-activity,screen-info,uitree) plus manual filtering- Interactive-element detection.
focusable,clickable,long-clickable,checkable,scrollable,selectedandpassword, plus a narrow widget-class allowlist for apps that ship aButtonwithout settingclickable.uitree --interactiverestricts output to these: on a Settings screen that is 72 elements down to 14 - Names are synthesized from descendants. A clickable row whose label lives in child
TextViews — the most common Android list layout there is — used to list as nameless and could not be addressed by label at all. The hierarchy is now parsed with its nesting intact, and a row takes its name from its children, stopping at children that are themselves actionable so their text stays attributed to them - Selectors on
findandtap:--id,--class,--descand--indexalongside--label, AND-ed so they narrow.--idaccepts eitherbtn_loginorcom.example.app:id/btn_login, and listings print the short form, so what you read is what you can paste back - State is reported, not just capability:
*checked/*unchecked,*password,*focused,*selected,*scrollable. Whether a switch is on is not something a label can tell you android type --clearreplaces a field's contents rather than appending to them.input textonly appends, and Android has no select-all keyevent that holds across versions, so the character count comes from the focused element and the deletes go in one round tripandroid notificationsreads posted notifications as package/title/text;--openpulls the shade down instead. Needs no device driverandroid wait <ms>for the settlewait-for-uicannot express. Runs without a device attached, so it cannot fail a script with "no device attached"
Tests
59 in
kuri-mobile, up from 44 — and 8 of the previous 44 were never being executed. Verified live against a headless Android 16 emulator:state,uitree --interactive, the selectors,tap --label(launcher → Messages),type --clear(replaces - v0.4.132026-07-26 · 2026-07-26 03:27 UTC
查看更新说明
v0.4.13
Seven HTTP routes answered with results they never obtained from the browser. Each returned
200with a well-formed body, so no caller could distinguish "Chrome reported nothing" from "kuri never asked Chrome" — the failure mode that costs the most, because it looks exactly like success.Fixes — routes that fabricated their answers
/react/treereported{"react":false}on every React page. React double-buffers fibers, and the__reactContainer$…property is stamped on the host node at mount and keeps pointing at whichever fiber existed then. After the first commit that is the off-screen side of the pair, whose.childisnull— so detection concluded there was no React at all. It now follows whichever side actually rendered, via.alternate. Confirmed against React 19.2.5, where the mount-time fiber hashasChild:falsewhile its alternate has children; the route now returns a real tree withFunctionComponent,Context.ConsumerandHostComponentnodes at their true depths/trace/stopreturned an empty acknowledgement.Tracing.startnever asked fortransferMode: ReturnAsStream, so Chrome kept the trace to itself and there was no stream handle to drain — the route reported success and wrote nothing, every time.IO.read/IO.closenow drain the stream; a routine page load yields ~400 KB of real trace on disk/networkreturned a canned{"status":"ok"}. No request was ever recorded. Now a bounded ring of real requests carrying url, method and MIME type/mainframereturned the same hard-coded string to every caller, regardless of tab or page. Now the actualframeId,loaderIdandurlfromPage.getFrameTree/exposebindings fired into the void. The binding was registered with Chrome but its invocations were never captured, so/expose/callsstayed empty no matter how many times the page called it. Payloads are now recorded with the call/screencast/stopreturned a canned ok without ever reporting what it had captured. Now reports the realframe_count/consoleand/errorshad regressed to returning empty on any page, having lost their collector
Fixes — script injection
/evaluateand/evalhandleJSON-escaped the expression twice. The expression arrives already escaped; escaping it again turned a newline into\nand then\\n, which decodes back to a literal backslash-n in the JavaScript source. Chrome answered every multi-line or quote-containing expression withSyntaxError: Invalid or unexpected token— the two routes whose entire purpose is running arbitrary JS could not run any non-trivial JS. Escaped exactly once nowstealth.jspolluted every page's error stream. The file was a top-level script containingconst originalQuery, but it is injected twice by design — once viaPage.addScriptToEvaluateOnNewDocument, once evaluated into the already-loaded page. Re-running it in the same global, which happens whenever tab discovery re-runs against a live tab, re-declared the const. The resultingSyntaxErroraborted the whole re-injection at parse time, so none of the stealth measures after it applied, and it surfaced in the page's own error stream, meaning/errorsreported a kuri-internal failure as if it were the page's. Now an IIFE behind an idempotence guard, which also stops the permissions wrapper being wrapped around itself./errorson a clean page is[]
Fixes — memory safety
- The event rings handed out pointers into storage they were about to overwrite. A snapshot borrowed the ring's own buffers, so the next event that wrapped the ring rewrote memory the caller was still reading — a use-after-free reachable from ordinary traffic. Every ring (
RequestRing,ScreencastRing,BindingCallRing,NetworkRing) now dupes into a caller-supplied per-request arena, and the contract is stated once and referenced from each - **
extractObject/extractFieldwere blind to JSON escap
用户点评
登录用户的真实使用记录,可按版本和硬件筛选还没有人点评。第一个说出真实体验的人最有价值。
Install
curl -fsSL https://kuri.trilok.ai/download | sh
macOS arm64/x86_64 and Linux x86_64/arm64. Single binary, no runtime deps.
Direct downloads: macOS arm64 · macOS x86_64 · Linux x86_64 · Linux arm64
Browser automation & web crawling for AI agents. Written in Zig. Zero Node.js.
CDP automation · A11y snapshots · HAR recording · Standalone fetcher · Interactive terminal browser · Agentic CLI · Security testing · iOS + Android device control
Quick Start · Benchmarks · kuri-agent · Security Testing · API · Skills · Changelog
Why teams switch to Kuri: current Apple Silicon
ReleaseFastbuilds stay sub-2 MB per binary, and a fresh Google Flights rerun on 2026-04-23 measured 3,392 tokens for a fullkuri-agentloop (go→snap→click→snap→eval). Cross-tool deltas should be rerun in the same environment before quoting a percentage.
Why Kuri Wins for Agents
Most browser tooling was built for QA engineers. Kuri is built for agent loops: read the page, keep token cost low, act on stable refs, and move on.
- 135 HTTP endpoints — full parity with agent-browser and browser-use, from React inspection to Core Web Vitals.
- 7-12% fewer tokens than agent-browser on real pages thanks to
@eNref format and zero-prefix rendering. - 44x lighter observations with
/page/state(48 tokens) vs full snapshot (2,124 tokens) for the same Google Flights page. - Batch execution —
POST /batchsends N commands in one HTTP call, eliminating N-1 round-trips and N-1 LLM turns. - React-compatible — trusted CDP mouse events and per-character key events fire React 18/19
onClickandonChange.
Snapshot tokens: Google Flights SIN → TPE
Fresh rerun on 2026-05-24 in this workspace, measured with wc -c and chars/4 approximation.
| Tool / Mode | Chars | ~Tokens | Note |
|---|---|---|---|
kuri snap (full) | 8,499 | 2,124 | All nodes + interactive refs |
kuri snap (interactive only) | ~3,000 | ~750 | Best for agent loops |
kuri /page/state | 190 | 48 | Lightweight observation (url, title, scroll%, counts) |
| agent-browser snap (estimated) | ~9,183 | ~2,295 | [ref=e0] format overhead |
Token efficiency: kuri vs agent-browser
| Page | kuri tokens | agent-browser tokens | Savings |
|---|---|---|---|
| example.com | 40 | 35 | -13% (trivial page, agent-browser skips root) |
| Hacker News | 386 | ~440 | 12% fewer |
| Google Flights SIN→TPE | 2,124 | ~2,295 | 7% fewer |
The savings come from kuri's compact format:
@e0refs (3 chars) vs[ref=e0](9 chars)- No
-prefix per line (saves 2 chars × line count) - Same indentation, same node filtering
Full workflow cost: go → snap → click → snap → eval
| Tool | Tokens per cycle |
|---|---|
| kuri-agent | ~3,400 |
With /page/state instead of second snap | ~1,700 |
With POST /batch (all in one call) | ~1,700 (same tokens, 1 HTTP call instead of 5) |
kuri vs libretto
libretto (Playwright + Node) is the closest competitor on per-step token cost. Measured head-to-head on 2026-07-04 — same Chrome, same tab, real tiktoken o200k_base counts (full methodology and reproduction: benchmarks/libretto_comparison.md). The honest split:
| Axis | Winner | Detail |
|---|---|---|
| Latency per call | kuri | 4–117 ms vs 1,344–1,500 ms (13–376× faster — persistent server vs Node-per-command) |
| Snapshot tokens, typical page | kuri | simple 61 vs 151 (2.5×), article 265 vs 363 (1.37×) — tighter grammar |
| Snapshot tokens, large list | split | kuri default 4,424 vs 813 — kuri emits all 259 refs, libretto truncates by default. With limit=5 kuri renders 555 tokens (1.46× under libretto), 34 refs + … +45 more markers |
| Trajectory (feed, 9 clicks) | kuri, barely | 898 vs 939 tokens (limit=5 base + diff loop vs exec loop) — parity-to-slight-edge; the morning's 5.1× loss was the untruncated base |
| Repeat runs | libretto | compiles trajectories to a Playwright script → 0-token replays; kuri re-pays the loop every run |
What kuri gained from studying libretto (all shipped this release): a diff-first loop (take_snapshot_diff, ~38 tokens/step); an adaptive diff that falls back to a full snapshot with a ! page replaced header on navigation; identity-only removal lines; screenshots written to disk (path returned, bytes never enter context); get_page_state over MCP; and — after rewriting parseA11yNodes as a real DFS tree walk — opt-in list truncation (/snapshot?limit=N, one … +K more line per capped run), scoped re-capture (scope=@ref), and hierarchy indentation, also exposed as uid/limit on MCP take_snapshot. The 9-click feed trajectory that cost 44,285 tokens with naive full re-snapshots costs 898 with a truncated base + diffs — 49× cheaper, and past libretto's 939.
The older tables above use a
chars/4token approximation; the libretto comparison uses realtiktokencounts. Rerun cross-tool numbers in your own environment before quoting a percentage.
Binary size and memory
Measured on Apple M4 Pro, macOS 26.4.1. Current binaries were built with -Doptimize=ReleaseFast.
| Binary | Current size |
|---|---|
kuri | 1,093,840 B (1.04 MiB) |
kuri-agent | 629,904 B (615 KiB) |
kuri-browse | 1,089,120 B (1.04 MiB) |
kuri-fetch | 2,063,488 B (1.97 MiB) |
RSS stayed flat across the Zig 0.16 migration
Measured against the current v0.4.3 ReleaseFast build with /usr/bin/time -l.
| Command | v0.4.3 mean max RSS |
|---|---|
kuri-fetch --version | ~2.45 MiB |
kuri-browse --version | ~2.45 MiB |
kuri-fetch --quiet --dump markdown http://example.com/ | ~9.17 MiB |
The Problem
Every browser automation tool drags in Playwright (~300 MB), a Node.js runtime, and a cascade of npm dependencies. Your AI agent just wants to read a page, click a button, and move on. Kuri is a single Zig binary. Four modes, zero runtime:
kuri → CDP server (Chrome automation, a11y snapshots, HAR)
kuri-fetch → standalone fetcher (no Chrome, QuickJS for JS, ~2 MB)
kuri-browse → interactive terminal browser (navigate, follow links, search)
kuri-agent → agentic CLI (scriptable Chrome automation + security testing)
📦 Installation
One-line install (macOS / Linux)
curl -fsSL https://raw.githubusercontent.com/justrach/kuri/release-channel/stable/install.sh | sh
Detects your platform, downloads the right binary, installs to ~/.local/bin.
Downloads come from Kuri's self-managed release-channel branch. macOS binaries are locally signed with a Developer ID certificate. GitHub Release assets mirror these same tarballs.
bun / npm
bun install -g kuri-agent
# or: npm install -g kuri-agent
Downloads the correct native binary for your platform at install time.
Release channel
Kuri's stable binaries live on the release-channel branch and are served directly from GitHub raw URLs.
- Stable installer:
https://raw.githubusercontent.com/justrach/kuri/release-channel/stable/install.sh - Stable manifest:
https://raw.githubusercontent.com/justrach/kuri/release-channel/stable/latest.json - Branch view:
https://github.com/justrach/kuri/tree/release-channel/stable - Direct download pattern:
https://raw.githubusercontent.com/justrach/kuri/release-channel/stable/<version>/kuri-<version>-<target>.tar.gz
Manual
Download the tarball for your platform from the stable release manifest or from the GitHub Releases page and unpack it to your $PATH.
Stable install URL:
curl -fsSL https://raw.githubusercontent.com/justrach/kuri/release-channel/stable/install.sh | sh
The manifest includes exact asset URLs plus SHA-256 checksums for aarch64-linux, x86_64-linux, aarch64-macos, and x86_64-macos.
Platform support
| Platform | Status |
|---|---|
macOS (aarch64, x86_64) | Prebuilt binaries, signed + notarized |
Linux (aarch64, x86_64) | Prebuilt binaries |
Windows (x86_64) | Experimental — cross-compile only. zig build -Dtarget=x86_64-windows-gnu is CI-verified, but Chrome automation, daemonization, signal-based shutdown, HAR recording, and the file-backed auth store are all stubbed with error.UnsupportedOnWindows at runtime. Use WSL2 if you need the real feature set. Tracked at #153. |
Kuri leans on POSIX primitives (fork, clock_gettime, raw sockets) in several spots, so a full native Windows port is real work. The compile-level baseline above lets the --version/--help paths and pure in-memory operations run; the gnarly bits (Chrome, sockets, daemonize) need real Win32 implementations before they come off the stub list. If you want to take any of those on, +1 #153 or open a PR.
Build from source
Requires Zig ≥ 0.16.0.
git clone https://github.com/justrach/kuri.git
cd kuri
zig build -Doptimize=ReleaseFast
# Binaries in zig-out/bin/: kuri kuri-agent kuri-fetch kuri-browse
⚡ Quick Start
Requirements: Zig ≥ 0.16.0 · Chrome/Chromium (for CDP mode)
git clone https://github.com/justrach/kuri.git
cd kuri
zig build # build everything
zig build test # run 252+ tests
# CDP mode — launches Chrome automatically
./zig-out/bin/kuri
# Standalone mode — no Chrome needed
./zig-out/bin/kuri-fetch https://example.com
# Interactive browser — browse from your terminal
./zig-out/bin/kuri-browse https://example.com
# Experimental standalone browser runtime — separate build, not production
(cd kuri-browser && zig build run -- render https://example.com)
(cd kuri-browser && zig build run -- bench --offline)
First run, shortest path
# start the server; if CDP_URL is unset, kuri launches managed Chrome for you
./zig-out/bin/kuri
# discover tabs from that managed browser
curl -s http://127.0.0.1:8080/discover
# inspect the discovered tab list
curl -s http://127.0.0.1:8080/tabs
Session-first agent loop
For agent-style HTTP usage, prefer a session header plus /tab/new, /page/info, and /snapshot instead of repeating tab_id on every call.
SESSION=hn-demo
BASE=http://127.0.0.1:8080
curl -s -H "X-Kuri-Session: $SESSION" \
"$BASE/tab/new?url=https%3A%2F%2Fnews.ycombinator.com"
curl -s -H "X-Kuri-Session: $SESSION" "$BASE/page/info"
SNAP=$(curl -s -H "X-Kuri-Session: $SESSION" "$BASE/snapshot?filter=interactive&format=compact")
MORE_REF=$(printf '%s' "$SNAP" | python3 -c 'import re,sys; print(re.search(r"\"More\" @(e\\d+)", sys.stdin.read()).group(1))')
curl -s -H "X-Kuri-Session: $SESSION" "$BASE/action?action=click&ref=$MORE_REF"
curl -s -H "X-Kuri-Session: $SESSION" "$BASE/page/info"
There is also a thin experimental wrapper at tools/kuri_harness.py if you want Python helpers on top of the same HTTP surface.
If you already have Chrome running with remote debugging, set CDP_URL to either the WebSocket or HTTP endpoint:
CDP_URL=ws://127.0.0.1:9222/devtools/browser/... ./zig-out/bin/kuri
# or
CDP_URL=http://127.0.0.1:9222 ./zig-out/bin/kuri
Browse vercel.com in 4 commands
# 1. Discover Chrome tabs
curl -s http://localhost:8080/discover
# → {"discovered":1,"total_tabs":1}
# 2. Get tab ID
curl -s http://localhost:8080/tabs
# → [{"id":"ABC123","url":"chrome://newtab/","title":"New Tab"}]
# 3. Navigate
curl -s "http://localhost:8080/navigate?tab_id=ABC123&url=https://vercel.com"
# 4. Get accessibility snapshot (token-optimized for LLMs)
curl -s "http://localhost:8080/snapshot?tab_id=ABC123&filter=interactive"
# → [{"ref":"e0","role":"link","name":"VercelLogotype"},
# {"ref":"e1","role":"button","name":"Ask AI"}, ...]
🌐 HTTP API
All endpoints return JSON. Optional auth via KURI_SECRET env var. 135 endpoints — full parity with agent-browser and browser-use.
Core
| Path | Description |
|---|---|
GET /health | Server status, tab count, version |
GET /tabs | List all registered tabs |
GET /discover | Auto-discover Chrome tabs via CDP |
GET /tab/current | Get or set the current tab for an X-Kuri-Session |
GET /page/info | Live URL/title/ready-state/viewport/scroll for the active tab |
GET /page/state | Compact page observation: url, title, scroll%, viewport, counts of forms/links/images/inputs |
POST /batch | Execute multiple commands in one HTTP call — returns array of results |
GET /browdie | 🌰 (easter egg) |
Browser Control
| Path | Params | Description |
|---|---|---|
GET /navigate | tab_id, url | Navigate tab to URL |
GET /tab/new | url, activate, wait | Create a new tab and optionally hydrate/set current tab |
GET /tab/close | tab_id | Close a tab |
GET /window/new | url, activate, wait | Create a new window/tab target |
GET /snapshot | tab_id, filter, format | A11y tree snapshot with eN refs. Use filter=interactive&format=compact for low-token agent loops. |
GET /text | tab_id | Extract page text |
GET /screenshot | tab_id, format, quality, save | Capture screenshot (base64); save=true writes the PNG to STATE_DIR/screenshots and returns {path,bytes} instead |
GET /screenshot/annotated | tab_id | Screenshot with numbered element labels |
GET /screenshot/diff | tab_id, baseline | Visual diff between current and baseline screenshots |
GET /action | tab_id, ref, action, value | Click/type/fill/select/scroll/hover/dblclick/check/uncheck/blur by ref |
GET /evaluate | tab_id, expression | Execute JavaScript |
GET /evalhandle | tab_id, expression | Execute JS, return objectId handle (not value) |
GET /close | tab_id | Close tab + cleanup |
GET /bringtofront | tab_id | Bring tab to front |
Actions
| Path | Params | Description |
|---|---|---|
GET /clear | ref | Clear input field value |
GET /selectall | ref | Select all text in input/contenteditable |
GET /setvalue | ref, value | Set input value directly (bypasses key events) |
GET /dispatch | ref, type | Dispatch custom DOM event on element |
GET /boundingbox | ref | Get element bounding rect (x, y, width, height, centerX, centerY) |
GET /getattribute | ref, name | Get element attribute by name |
GET /inputvalue | ref | Get current input element value |
GET /element/state | ref, check | Quick boolean: exists, visible, enabled, checked |
GET /find-element | text/role/label/placeholder/testid | Semantic locator — find element without snap |
GET /highlight | ref or selector | Highlight element with overlay |
Mouse & Touch
| Path | Params | Description |
|---|---|---|
GET /mouse/move | x, y | Move mouse to coordinates |
GET /mouse/down | x, y, button | Mouse button down |
GET /mouse/up | x, y, button | Mouse button up |
GET /mouse/wheel | x, y, deltaX, deltaY | Mouse wheel scroll |
GET /tap | x, y | Touch tap (touchStart + touchEnd) |
GET /swipe | startX, startY, endX, endY | Touch swipe gesture |
GET /drag | src_ref, tgt_ref | Drag element to target |
Keyboard
| Path | Params | Description |
|---|---|---|
GET /keyboard/type | tab_id, text | Type text via key events |
GET /keyboard/inserttext | tab_id, text | Insert text directly |
GET /keydown | tab_id, key | Key down event |
GET /keyup | tab_id, key | Key up event |
Content Extraction
| Path | Description |
|---|---|
GET /markdown | Convert page to Markdown |
GET /links | Extract all links |
GET /dom/query | CSS selector query |
GET /dom/html | Get element HTML |
GET /dom/attributes | Get element attributes |
GET /pdf | Print page to PDF |
GET /find | In-page text search |
Waiting
| Path | Params | Description |
|---|---|---|
GET /wait | selector, text, url, state, visible, timeout | Wait for selector/text/URL pattern/networkidle/load state |
GET /wait/function | expression, timeout | Wait for arbitrary JS expression to be truthy |
GET /wait/download | timeout | Wait for file download to complete |
Dialog Handling
| Path | Description |
|---|---|
GET /dialog/auto | Auto-handle all JS dialogs (accept or dismiss) |
GET /dialog/accept | Accept current dialog (with optional prompt text) |
GET /dialog/dismiss | Dismiss current dialog |
Network & HAR
| Path | Description |
|---|---|
GET /har/start | Start recording network traffic |
GET /har/stop | Stop + return HAR 1.2 JSON |
GET /har/status | Recording state + entry count |
GET /har/replay | API map with curl/fetch/python code snippets |
GET /cookies | Get cookies |
GET /cookies/set | Set cookies |
GET /cookies/delete | Delete cookies |
GET /cookies/clear | Clear all cookies |
GET /headers | Set custom request headers |
GET /intercept/start | Start request interception |
GET /intercept/stop | Stop request interception |
GET /intercept/requests | List intercepted requests |
GET /request/detail | Get response body for a request ID |
GET /response/body | Fetch URL and return response body |
GET /network | Network traffic stats |
GET /download | Trigger file download |
Navigation & State
| Path | Description |
|---|---|
GET /back | Browser back |
GET /forward | Browser forward |
GET /reload | Reload page |
GET /stop | Stop page loading |
GET /pushstate | SPA navigation via history.pushState |
GET /storage/local | Get/set localStorage |
GET /storage/session | Get/set sessionStorage |
GET /storage/local/clear | Clear localStorage |
GET /storage/session/clear | Clear sessionStorage |
GET /session/save | Save browser session |
GET /session/load | Restore browser session |
GET /session/list | List saved sessions |
GET /setcontent | Set page HTML directly (POST) |
Auth Profiles
| Path | Description |
|---|---|
GET /auth/profile/save | Save cookies + storage as a named auth profile |
GET /auth/profile/load | Restore a named auth profile into a tab |
GET /auth/profile/list | List saved auth profiles |
GET /auth/profile/delete | Delete a saved auth profile |
GET /auth/extract | Extract auth tokens (JWT, cookies, headers) |
GET /set/credentials | Set HTTP basic auth credentials |
On macOS, auth profile secrets are stored in the user Keychain.
Emulation
| Path | Params | Description |
|---|---|---|
GET /emulate | device type, screen size | Device emulation |
GET /set/viewport | width, height | Set viewport size |
GET /set/useragent | ua | Set user agent |
GET /set/media | media | Emulate media type |
GET /set/offline | offline | Toggle offline mode |
GET /geolocation | lat, lng | Override geolocation |
GET /timezone | timezone | Override timezone (e.g. America/New_York) |
GET /locale | locale | Override locale (e.g. en-US) |
GET /permissions | name, state | Grant/deny permissions (geolocation, notifications, clipboard) |
Scripts & Injection
| Path | Description |
|---|---|
GET /script/inject | Inject JavaScript into page (persists across nav) |
GET /initscript/remove | Remove a previously injected init script |
GET /addstyle | Inject CSS stylesheet |
GET /expose | Expose a named function to page JS context |
React Inspection
| Path | Description |
|---|---|
GET /react/tree | React component tree via DevTools hook |
GET /react/inspect | React component props and state |
GET /react/renders | React render tracking (start/stop) |
GET /react/suspense | React Suspense boundary status |
Recording & Performance
| Path | Description |
|---|---|
GET /recording/start | Record user actions (click, input, navigate) |
GET /recording/stop | Stop recording + return action log |
GET /vitals | Core Web Vitals (LCP, CLS, FID, TTFB, FCP, domInteractive) |
GET /perf/lcp | Largest Contentful Paint timing |
GET /trace/start | Start performance trace |
GET /trace/stop | Stop trace |
GET /profiler/start | Start JS profiler |
GET /profiler/stop | Stop profiler |
Debugging
| Path | Description |
|---|---|
GET /debug/enable | Enable in-page debug HUD and optional freeze mode |
GET /debug/disable | Disable in-page debug HUD |
GET /inspect | Element inspection |
GET /errors | Collect JS errors |
GET /console | Read console logs |
GET /frames | List page frames |
GET /frame | Switch to iframe context by name or URL |
GET /mainframe | Switch back to main frame |
GET /diff/snapshot | Compact +/~/- diff vs the previous call for this tab — the token-efficient action loop (aliased by /snapshot/changes). Falls back to a full snapshot with a ! page replaced header on mass change. |
GET /diff/url | Compare two URLs side by side (navigate, snapshot, diff) |
Streaming
| Path | Description |
|---|---|
GET /screencast/start | Start screen recording |
GET /screencast/stop | Stop screen recording |
GET /video/start | Start video capture |
GET /video/stop | Stop video capture |
GET /ws/start | WebSocket tunnel start |
GET /ws/stop | WebSocket tunnel stop |
Agent-friendly loop
The lowest-friction server loop is:
GET /tab/new?url=...GET /page/state(lightweight) orGET /snapshot?filter=interactive&format=compact(full)GET /action?action=click&ref=eN- Repeat — or use
POST /batchfor multi-step operations in one call
url and expression query params are percent-decoded. Send X-Kuri-Session: my-agent to persist tab context server-side.
🧠 Skills
The repo includes a user-extensible skill area:
skills/kuri-skill.mdis the base Kuri HTTP-agent skillskills/custom/is reserved for your own project-specific skillsskills/custom/hackernews-page-2.mdis a concrete example custom skill.claude/skills/kuri-server/SKILL.mdstays in sync for Claude-style repo skills
The base skill now also explains which browser path to use:
kuriHTTP API: production Chrome/CDP automation with sessions, snapshots, actions, HAR, cookies, and screenshotskuri-fetch: standalone no-Chrome fetch/text extractionkuri-browse: interactive terminal browsingkuri-agent: scriptable CLI automation against the Kuri serverkuri-browser/: experimental separate Zig-native browser runtime for parity work
For the experimental browser CLI:
cd kuri-browser
zig build run -- render https://news.ycombinator.com --selector ".titleline a" --dump text
zig build run -- render https://todomvc.com/examples/react/dist/ --js --wait-eval "document.querySelectorAll('.todo-list li').length >= 1"
zig build run -- parity --offline
zig build run -- bench --offline
zig build run -- serve-cdp --port 9333
kuri-browser serve-cdp exposes Chrome-style HTTP discovery plus a minimal WebSocket JSON-RPC router for protocol smoke tests. Runtime eval returns V8-shaped CDP remote objects backed by QuickJS; this does not add a V8 dependency and is not full Playwright/Puppeteer compatibility yet.
Screenshots in kuri-browser currently delegate to the main Kuri/CDP renderer. Start ./zig-out/bin/kuri first, then:
cd kuri-browser
zig build run -- screenshot https://example.com --out example.jpg --compress --kuri-base http://127.0.0.1:8080
--compress captures a PNG baseline and JPEG candidate, writes the smaller file, and reports byte savings. Current local measurement on https://example.com: 20,523 bytes PNG to 18,183 bytes JPEG quality 50, saving 2,340 bytes or 11%.
Advanced
| Path | Description |
|---|---|
GET /diff/snapshot | Compact +/~/- delta vs the previous snapshot (agent action loop) |
GET /emulate | Device emulation |
GET /geolocation | Set geolocation |
POST /upload | File upload |
GET /script/inject | Inject JavaScript |
GET /intercept/start | Start request interception |
GET /intercept/stop | Stop interception |
GET /screenshot/annotated | Screenshot with element annotations |
GET /screenshot/diff | Visual diff between screenshots |
GET /screencast/start | Start screencast |
GET /screencast/stop | Stop screencast |
GET /video/start | Start video recording |
GET /video/stop | Stop video recording |
GET /console | Get console messages |
GET /stop | Stop page loading |
GET /get | Direct HTTP fetch (server-side) |
GET /scrollintoview | Scroll a referenced element into view |
GET /drag | Drag from one ref to another |
GET /keyboard/type | Type text with key events |
GET /keyboard/inserttext | Insert text directly |
GET /keydown | Dispatch a keydown event |
GET /keyup | Dispatch a keyup event |
GET /wait | Wait for ready state or element conditions |
GET /tab/close | Close a tab |
GET /highlight | Highlight an element by ref or selector |
GET /errors | Get page/runtime errors |
GET /set/offline | Toggle offline network emulation |
GET /set/media | Set emulated media features |
GET /set/credentials | Set HTTP basic auth credentials |
GET /find | Find text matches in the current page |
GET /trace/start | Start Chrome tracing |
GET /trace/stop | Stop tracing and return trace data |
GET /profiler/start | Start JS profiler |
GET /profiler/stop | Stop JS profiler |
GET /inspect | Inspect an element or page state |
GET /set/viewport | Set viewport size |
GET /set/useragent | Override user agent |
GET /dom/attributes | Get element attributes |
GET /frames | List frame tree |
GET /network | Inspect network state/requests |
🛡️ Stealth & Bot Evasion
Kuri applies anti-detection patches automatically on startup — no manual config needed.
What's applied
Page.addScriptToEvaluateOnNewDocument— stealth patches run before any page JS- navigator.webdriver = false — hides automation flag at Chromium level (
--disable-blink-features=AutomationControlled) - WebGL/Canvas/AudioContext spoofing — defeats fingerprint-based detection
- UA rotation — 5 realistic Chrome/Safari/Firefox user agents
- chrome.csi/chrome.loadTimes — stubs for Akamai-specific checks
Bot block detection
Navigate auto-detects blocks and returns structured fallback:
curl -s "http://localhost:8080/navigate?tab_id=ABC&url=https://protected-site.com"
# If blocked:
# {"blocked":true,"blocker":"akamai","ref_code":"0.7d...",
# "fallback":{"suggestions":["Open URL directly in browser","Use KURI_PROXY"]}}
# If ok: normal CDP response
Detects: Akamai, Cloudflare, PerimeterX, DataDome, generic captcha.
Proxy support
KURI_PROXY=socks5://user:pass@residential-proxy:1080 ./zig-out/bin/kuri
KURI_PROXY=http://proxy:8080 ./zig-out/bin/kuri
Tested sites
| Site | Protection | Result |
|---|---|---|
| Singapore Airlines | Akamai WAF | ✅ Bypassed (was blocked before v0.4.0) |
| Shopee SG | Custom anti-fraud | ✅ Page loads, redirects to login |
| Google Flights | None | ✅ Full interaction |
| Booking.com | PerimeterX | ⚠️ Needs proxy |
🔧 kuri-fetch
Standalone HTTP fetcher — no Chrome, no Playwright, no npm. Ships as a ~2 MB binary with built-in QuickJS for JS execution.
zig build fetch # build + run
# Default: convert to Markdown
kuri-fetch https://example.com
# Extract links
kuri-fetch -d links https://news.ycombinator.com
# Structured JSON output
kuri-fetch --json https://example.com
# Execute inline scripts via QuickJS
kuri-fetch --js https://example.com
# Write to file, quiet mode
kuri-fetch -o page.md -q https://example.com
# Pipe-friendly: content → stdout, status → stderr
kuri-fetch -d text https://example.com | wc -w
Features
- 5 output modes —
markdown,html,links,text,json - QuickJS JS engine —
--jsexecutes inline<script>tags - DOM stubs —
document.querySelector,getElementById,window.location,document.title,console.log,setTimeout(SSR-style) - SSRF defense — blocks private IPs, metadata endpoints, non-HTTP schemes
- Colored output — respects
NO_COLOR,TERM=dumb,--no-color, TTY detection - File output —
-o/--outputwith byte count + timing summary - Custom UA —
--user-agentflag - Quiet mode —
-qsuppresses stderr status
🌐 kuri-browse
Interactive terminal browser — browse the web from your terminal. No Chrome needed.
zig build browse # build + run
kuri-browse https://example.com
🌰 kuri-browse — terminal browser
→ loading https://example.com
# Example Domain
This domain is for use in documentation examples...
Learn more [1]
───── Links ─────
[1] https://iana.org/domains/example
✓ 528 bytes, 1 links (133ms)
[nav] https://example.com> 1 ← type 1 to follow the link
Commands
| Command | Action |
|---|---|
<number> | Follow link [N] |
<url> | Navigate (if contains .) |
:go <url> | Navigate to URL |
:back, :b | Go back in history |
:forward, :f | Go forward |
:reload, :r | Re-fetch current page |
:links, :l | Show link index |
/<term> | Search in page (highlights matches) |
:search <t> | Search in page |
:n, :next | Re-highlight search |
:history | Show navigation history |
:help, :h | Show all commands |
:quit, :q | Exit |
Features
- Colored markdown rendering — headings, links, code blocks, bold, blockquotes
- Numbered links — every link gets
[N], type the number to follow it - Navigation history — back/forward like a real browser
- In-page search —
/termhighlights all matches - Relative URL resolution — follows links naturally across pages
- Smart filtering — skips
javascript:andmailto:hrefs
🤖 kuri-agent
Scriptable CLI for Chrome automation — drives the browser command-by-command from your terminal or shell scripts. Shares session state across invocations via ~/.kuri/session.json.
zig build agent # build kuri-agent
# 1. Find a Chrome tab
kuri-agent tabs
# → ws://127.0.0.1:9222/devtools/page/ABC123 https://example.com
# 2. Attach to it
kuri-agent use ws://127.0.0.1:9222/devtools/page/ABC123
# 3. Navigate + interact
kuri-agent go https://example.com
kuri-agent snap --interactive # → [{"ref":"e0","role":"link","name":"More info"}]
kuri-agent click e0
kuri-agent shot # saves ~/.kuri/screenshots/<ts>.png
Commands
| Command | Description |
|---|---|
tabs [--port N] | List Chrome tabs |
use <ws_url> | Attach to a tab (saves session) |
open [url] [--port N] | Open a new tab (optionally navigating to url) |
status | Show current session |
go <url> | Navigate to URL |
snap [--interactive] [--json] [--text] [--depth N] | A11y snapshot, saves eN refs |
click <ref> | Click element by ref (CDP mouse events, React-compatible) |
type <ref> <text> | Type into element (per-character key events, React-compatible) |
fill <ref> <text> | Fill input value |
select <ref> <value> | Select dropdown option |
hover <ref> | Hover over element |
focus <ref> | Focus element |
scroll | Scroll the page |
viewport [width height] | Get or set viewport dimensions |
eval <js> | Evaluate JavaScript |
text [selector] | Get page text |
shot [--out file.png] | Screenshot |
back | Navigate back |
forward | Navigate forward |
reload | Reload current page |
cookies | List cookies with security flags |
headers | Check security response headers |
audit | Full security audit |
storage [local|session|all] | Dump localStorage / sessionStorage |
jwt | Extract and decode JWTs from cookies and storage |
fetch <method> <url> [--data <json>] | Authenticated fetch using page cookies |
probe <url-template> <start> <end> | IDOR probe: iterate numeric IDs in URL |
grab <ref> | Click ref, intercept window.open, follow redirect in-tab |
wait-for-tab [--port N] | Poll for a new tab, auto-switch session |
stealth | Apply anti-detection patches |
set-header <name> <value> | Add a custom header to all requests |
show-headers | Show stored extra headers |
clear-headers | Remove all extra headers |
📱 kuri-mobile (iOS + Android)
Native Zig CLI for driving iOS Simulators, real iPhones (listing + launch/terminate), and Android devices/emulators — inspired by mobile-device-mcp, reimplemented in Zig with no Bun/Node/Gradle/Xcode in the build path.
cd kuri-mobile && zig build && cp zig-out/bin/kuri-mobile ../zig-out/bin/
# The main `kuri` binary forwards android/ios subcommands to kuri-mobile:
kuri ios list-devices # sims + real devices (usbmuxd, native)
kuri ios openurl https://example.com # navigate Safari
kuri ios screenshot out.png # auto-picks booted sim
kuri ios launch com.apple.Preferences
kuri android list-devices # native Zig adb wire-protocol client
kuri android tap 540 1200
kuri android swipe 100 1500 100 500
kuri android screenshot phone.png
kuri android uitree # flat element list via uiautomator dump
What's native Zig: adb host protocol (libc sockets, 4-hex framing over host:transport:/shell:/exec:), Android XML UI tree parser, usbmuxd ListDevices plist client.
What shells out: xcrun simctl (iOS Simulator), xcrun devicectl (iOS real-device launch/terminate).
Driverless by design: no on-device app is installed, so run_code sandboxes and XCUITest-backed tap/uitree on real iOS devices are intentionally not available. See kuri-mobile/README.md for the full parity matrix vs upstream.
🔒 Security Testing
kuri-agent supports browser-native security trajectories — log in once, then run reconnaissance and header/cookie audits without leaving the terminal.
Trajectories
Enumerate → Inspect — after authenticating, dump auth cookies and check security flags:
kuri-agent go https://target.example.com/login
kuri-agent snap --interactive
kuri-agent fill e2 myuser
kuri-agent fill e3 mypassword
kuri-agent click e4 # submit login
kuri-agent cookies
# cookies (3):
# session_id domain=.example.com path=/ [Secure] [HttpOnly] [SameSite=Strict]
# csrf_token domain=.example.com path=/ [Secure] [!HttpOnly]
# tracking domain=.example.com path=/ [!Secure] [!HttpOnly]
Header audit — check what security headers the target sends:
kuri-agent go https://target.example.com
kuri-agent headers
# → {"url":"https://...","status":200,"headers":{
# "content-security-policy":"default-src 'self'",
# "strict-transport-security":"max-age=31536000",
# "x-frame-options":"(missing)",
# "x-content-type-options":"nosniff", ...}}
Full audit — HTTPS, missing headers, JS-visible cookies in one shot:
kuri-agent audit
# → {"protocol":"https:","url":"https://...","score":6,
# "issues":["MISSING:x-frame-options","COOKIES_EXPOSED_TO_JS:2"],
# "headers":{"content-security-policy":"default-src 'self'", ...}}
Cross-account trajectory — use eval to replay API calls with different tokens:
# After login, grab the auth token from localStorage
kuri-agent eval "localStorage.getItem('token')"
# Probe a resource ID with the current session
kuri-agent eval "fetch('/api/assessments/42').then(r=>r.status)"
# Check for IDOR: does a different user's resource return 200 or 403?
kuri-agent eval "fetch('/api/assessments/99').then(r=>r.status)"
Trajectory Report Format
kuri-agent outputs JSON suitable for pipeline integration. Each security command emits a single JSON line — pipe through jq for triage:
kuri-agent audit | jq '.issues[]'
kuri-agent cookies | head -20
kuri-agent headers | jq '.headers | to_entries[] | select(.value == "(missing)") | .key'
🏗 Architecture
┌──────────────────────────────────────────────────────────┐
│ HTTP API Layer │
│ (std.http.Server, thread-per-connection) │
├──────────────┬──────────────────┬────────────────────────┤
│ Browser │ Crawler Engine │ kuri-fetch / browse │
│ Bridge │ │ (standalone CLIs) │
├──────────────┼──────────────────┼────────────────────────┤
│ CDP Client │ URL Validator │ std.http.Client │
│ Tab Registry │ HTML→Markdown │ QuickJS JS Engine │
│ A11y Snapshot│ Link Extractor │ DOM Stubs (Layer 3) │
│ Ref Cache │ Text Extractor │ SSRF Validator │
│ HAR Recorder │ │ Colored Renderer │
│ Stealth JS │ │ History + REPL │
├──────────────┴──────────────────┴────────────────────────┤
│ Chrome Lifecycle Manager │
│ (launch, health-check, auto-restart, port detection) │
└──────────────────────────────────────────────────────────┘
Memory Model
- Arena-per-request — all per-request memory freed in one
deinit()call - No GC —
GeneralPurposeAllocatorin debug mode catches every leak - Proper cleanup chains —
Launcher → Bridge → CdpClients → HarRecorders → Snapshots → Tabs errdeferguards — partial failures roll back cleanly
Chrome Lifecycle
| Mode | Behavior |
|---|---|
Managed (no CDP_URL) | Launches Chrome headless, finds free CDP port, supervises, auto-restarts on crash (max 3 retries), kills on shutdown |
External (CDP_URL set) | Connects to existing Chrome, health-checks via /json/version, does NOT kill on shutdown |
📁 Structure
kuri/
├── build.zig # Build system (Zig 0.16.0)
├── build.zig.zon # Package manifest + QuickJS dep
├── src/
│ ├── main.zig # CDP server entry point
│ ├── fetch_main.zig # kuri-fetch CLI entry point
│ ├── browse_main.zig # kuri-browse CLI entry point
│ ├── js_engine.zig # QuickJS wrapper + DOM stubs
│ ├── bench.zig # Benchmark harness
│ ├── chrome/
│ │ └── launcher.zig # Chrome lifecycle manager
│ ├── server/
│ │ ├── router.zig # HTTP route dispatch (40+ endpoints)
│ │ ├── middleware.zig # Auth (constant-time comparison)
│ │ └── response.zig # JSON response helpers
│ ├── bridge/
│ │ ├── bridge.zig # Central state (tabs, CDP, HAR, snapshots)
│ │ └── config.zig # Env var configuration
│ ├── cdp/
│ │ ├── client.zig # CDP WebSocket client
│ │ ├── websocket.zig # WebSocket frame codec
│ │ ├── protocol.zig # CDP method constants
│ │ ├── actions.zig # High-level CDP actions
│ │ ├── stealth.zig # Bot detection bypass
│ │ └── har.zig # HAR 1.2 recorder
│ ├── snapshot/
│ │ ├── a11y.zig # A11y tree with interactive filter
│ │ ├── diff.zig # Snapshot delta diffing
│ │ └── ref_cache.zig # eN ref → node ID cache
│ ├── crawler/
│ │ ├── validator.zig # SSRF defense, URL validation
│ │ ├── markdown.zig # HTML → Markdown (SIMD tag counting)
│ │ ├── fetcher.zig # Page fetching
│ │ ├── extractor.zig # Readability extraction
│ │ └── pipeline.zig # Parallel crawl pipeline
│ ├── storage/
│ │ ├── local.zig # Local file writer
│ │ └── r2.zig # R2/S3 uploader
│ ├── util/
│ │ └── json.zig # JSON helpers
│ └── test/
│ ├── harness.zig # Test HTTP client
│ ├── integration.zig # Integration tests
│ └── merjs_e2e.zig # E2E tests
├── js/
│ ├── stealth.js # Bot detection bypass
│ └── readability.js # Content extraction
├── kuri-browser/ # Native Zig rendering experiments
└── kuri-mobile/ # iOS + Android device control (Zig-native adb + usbmuxd)
├── src/
│ ├── common/ # io helpers, unified UI tree parser
│ ├── android/ # adb wire protocol client, driver, CLI
│ └── ios/ # simctl, usbmuxd, devicectl, CLI
└── README.md # Full parity matrix vs mobile-device-mcp
⚙️ Configuration
| Env Var | Default | Description |
|---|---|---|
HOST | 127.0.0.1 | Server bind address |
PORT | 8080 | Server port |
CDP_URL | (none) | Connect to existing Chrome (ws://... or http://127.0.0.1:9222) |
KURI_SECRET | (none) | Auth secret for API requests |
STATE_DIR | .kuri | Session state directory |
REQUEST_TIMEOUT_MS | 30000 | HTTP request timeout |
NAVIGATE_TIMEOUT_MS | 30000 | Navigation timeout |
STALE_TAB_INTERVAL_S | 30 | Stale tab cleanup interval |
NO_COLOR | (none) | Disable colored CLI output |
💰 Token Cost
For a 50-page monitoring task (from Pinchtab benchmarks):
| Method | Tokens | Cost ($) | Best For |
|---|---|---|---|
/text | ~40,000 | $0.20 | Read-heavy (13× cheaper than screenshots) |
/snapshot?filter=interactive&format=compact | ~40,000 | $0.20 | Low-token element interaction |
/snapshot (full) | ~525,000 | $2.63 | Full page understanding |
/screenshot | ~100,000 | $1.00 | Visual verification |
🤝 Contributing
Open an issue before submitting a large PR so we can align on the approach.
git clone https://github.com/justrach/kuri.git
cd kuri
zig build test # 252+ tests must pass
zig build test-fetch # kuri-fetch tests (69 tests)
zig build test-browse # kuri-browse tests (22 tests)
See CONTRIBUTORS.md for guidelines.
Credits
| Project | What we borrowed |
|---|---|
| agent-browser | @eN ref system, snapshot diffing, HAR recording patterns |
| Pinchtab | Browser control architecture for AI agents |
| Pathik | High-performance crawling patterns |
| QuickJS-ng via mitchellh/zig-quickjs-ng | JS engine for kuri-fetch |
| Lightpanda | Zig-native headless browser pioneer, CDP compatibility patterns |
| Zig 0.16.0 | The whole stack |
License
Apache-2.0