zero-vision 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +5 -0
  3. package/README.md +163 -0
  4. package/dist/cdp/attach.js +121 -0
  5. package/dist/cdp/attach.js.map +1 -0
  6. package/dist/cdp/client.js +53 -0
  7. package/dist/cdp/client.js.map +1 -0
  8. package/dist/cdp/extract.js +197 -0
  9. package/dist/cdp/extract.js.map +1 -0
  10. package/dist/cli.js +223 -0
  11. package/dist/cli.js.map +1 -0
  12. package/dist/engines/apple-fm.js +103 -0
  13. package/dist/engines/apple-fm.js.map +1 -0
  14. package/dist/engines/apple-vision.js +89 -0
  15. package/dist/engines/apple-vision.js.map +1 -0
  16. package/dist/engines/cloud/index.js +41 -0
  17. package/dist/engines/cloud/index.js.map +1 -0
  18. package/dist/engines/index.js +71 -0
  19. package/dist/engines/index.js.map +1 -0
  20. package/dist/engines/local-vlm.js +46 -0
  21. package/dist/engines/local-vlm.js.map +1 -0
  22. package/dist/engines/tesseract.js +221 -0
  23. package/dist/engines/tesseract.js.map +1 -0
  24. package/dist/mcp.js +225 -0
  25. package/dist/mcp.js.map +1 -0
  26. package/dist/native.js +66 -0
  27. package/dist/native.js.map +1 -0
  28. package/dist/snap.js +30 -0
  29. package/dist/snap.js.map +1 -0
  30. package/dist/test/argv.test.js +22 -0
  31. package/dist/test/argv.test.js.map +1 -0
  32. package/dist/test/ax.test.js +28 -0
  33. package/dist/test/ax.test.js.map +1 -0
  34. package/dist/test/engines.test.js +81 -0
  35. package/dist/test/engines.test.js.map +1 -0
  36. package/dist/test/fetch.test.js +46 -0
  37. package/dist/test/fetch.test.js.map +1 -0
  38. package/dist/test/ocr.test.js +34 -0
  39. package/dist/test/ocr.test.js.map +1 -0
  40. package/dist/test/parity.test.js +33 -0
  41. package/dist/test/parity.test.js.map +1 -0
  42. package/dist/test/port.test.js +55 -0
  43. package/dist/test/port.test.js.map +1 -0
  44. package/dist/test/snap.test.js +13 -0
  45. package/dist/test/snap.test.js.map +1 -0
  46. package/dist/test/tesseract.test.js +17 -0
  47. package/dist/test/tesseract.test.js.map +1 -0
  48. package/docs/spec.md +549 -0
  49. package/package.json +40 -0
package/docs/spec.md ADDED
@@ -0,0 +1,549 @@
1
+ # zero-vision
2
+
3
+ Status: accepted for implementation
4
+ Date: 2026-09-07
5
+ Audience: the engineer who builds this in TypeScript and Swift
6
+
7
+ **A local CLI and MCP server that reads a page, a screenshot, or a video as text. Default path never sends pixels off the machine.**
8
+
9
+ GitHub: `unfoundbox-crew/zero-vision`. npm package: `zero-vision` (unscoped `zrv` is taken — a 2020 React leftover). CLI / bin: `zrv`. Native helper: `zrv-native`. MCP stdio: `zrv mcp`.
10
+
11
+ v1 is macOS. Pixel engines need Apple silicon or a configured cloud key. CDP read works anywhere Node 22 can reach a Chrome debug port.
12
+
13
+ ---
14
+
15
+ ## 1. What this is, and what it is not
16
+
17
+ Agents today screenshot a tab, ship a 1080p or 1440p PNG to a frontier model, and pay visual tokens plus seconds of latency to read text that the browser or the OS already knows.
18
+
19
+ Claude bills images as 28×28 patches: `ceil(w/28) * ceil(h/28)` visual tokens, capped at 1,568 on standard models and 4,784 on Claude 4.7+ ([Vision docs](https://platform.claude.com/docs/en/build-with-claude/vision)). A 1920×1080 screenshot is 1,560 tokens on the standard tier and 2,691 on the high-res tier, before any reasoning.
20
+
21
+ A 30-day forensic audit of 168 visual sessions in this fleet (`~/.agentworth/agentworth.db`, 2026-09-07) measured what those screenshots were *for*:
22
+
23
+ | Share | Sessions | Job |
24
+ | --- | --- | --- |
25
+ | 65% | 110 | Pure OCR / verbatim text (error traces, tweets, "is this headline clipped") |
26
+ | 27% | 46 | MotionVector contact sheets and cut verification (3×3 sheets, slide text, title cards) |
27
+ | 8% | 12 | Genuine aesthetic / spatial reasoning |
28
+ | 0% | 0 | Porn |
29
+
30
+ Ninety-two percent of visual sessions were transcription. Eight percent needed a model that can see. The default engine is therefore a transcriber, not a VLM. The 8% gets an explicit `--engine`.
31
+
32
+ It is not a browser driver. It does not click, type, hover, or fill. Chrome DevTools MCP and Playwright MCP already do that with `uid` / `ref`.
33
+
34
+ It does not silently upload pixels. Cloud vision is an opt-in engine (`cloud-vlm`), never the default, never an automatic fallback.
35
+
36
+ ---
37
+
38
+ ## 2. Rejected approaches
39
+
40
+ | Rejected | Do this instead | Why |
41
+ | --- | --- | --- |
42
+ | One hardcoded OCR binary | Engine adapter. Four named engines, one result schema | The 8% will change engines. The 65% must not |
43
+ | Silent cloud fallback | `--engine cloud-vlm` or config. Default stays `apple-vision` | A router that uploads is the bug this tool exists to stop |
44
+ | `click(uid)` in v1 | Read only | chrome-devtools-mcp and Playwright MCP already click |
45
+ | Probe 9222 by default | Probe env, then 1948, then 9223. **9222 only with `--port` or `ZEROVISION_ALLOW_USER_CHROME=1`** | 9222 is often the human's daily Chrome |
46
+ | Puppeteer / Playwright | Raw `fetch` + `WebSocket` (Node 22) | Those libraries *are* the drivers we refuse to clone |
47
+ | ffmpeg required for video | AVFoundation in the Swift binary. ffmpeg optional | ffmpeg is a 50MB+ dep |
48
+ | Quote 15ms / 20ms OCR as a measured SLA | Warm-path **target**. Measure on `hello.png` before the README quotes a number | Apple does not publish 20ms. ANE concurrency is ~2 |
49
+ | Auto-route tree → OCR → cloud | Commands pick the producer. Engines pick the pixel backend | Silent fallback hides "the tree was empty" |
50
+
51
+ ---
52
+
53
+ ## 3. Prior art
54
+
55
+ Compared 2026-09-07 against current docs. Chrome DevTools MCP tools checked against v1.8.0 `snapshot.ts`.
56
+
57
+ | Tool | What it is | Perception | Drive the browser? | Local OCR | Video text |
58
+ | --- | --- | --- | --- | --- | --- |
59
+ | [Playwright MCP](https://playwright.dev/mcp/introduction) | Microsoft MCP. Accessibility snapshot + `ref=eN` | AX tree, optional screenshot | Yes | No | No |
60
+ | [Chrome DevTools MCP](https://github.com/ChromeDevTools/chrome-devtools-mcp) v1.8.0 | Google MCP. `take_snapshot`, `take_screenshot` | AX tree, screenshot, traces | Yes | No | No |
61
+ | [Stagehand](https://docs.stagehand.dev) v3/v4 | Browserbase SDK. `act` / `extract` / `observe` | AX + LLM | Yes | No | No |
62
+ | [Crawl4AI](https://docs.crawl4ai.com/core/simple-crawling/) | Self-hosted crawler | Rendered HTML | Crawl, not UI | No | No |
63
+ | [Browser-Use](https://github.com/browser-use/browser-use) | Python agent loop | Hybrid, still pays vision often | Yes | No | No |
64
+ | [ocrtool-mcp](https://github.com/ihugang/ocrtool-mcp) | Swift + Vision, MCP only | Image OCR | No | Yes | No |
65
+
66
+ **The hole:** none of these is attach-only Chrome text plus on-device OCR plus video/contact-sheet text plus a swappable VLM, behind one CLI. The 27% contact-sheet sessions are a still PNG (one image, many frames of text). That is `apple-vision` on a file, not a video pipeline and not a cloud VLM.
67
+
68
+ ---
69
+
70
+ ## 4. Architecture
71
+
72
+ ```text
73
+ zrv / zero-vision MCP
74
+ |
75
+ +------------------+------------------+
76
+ | |
77
+ CDP reader Pixel perceiver
78
+ (TypeScript) (engine adapter)
79
+ | |
80
+ localhost debug port +------------+------------+
81
+ attach, never launch | | |
82
+ v v v
83
+ apple-vision apple-fm local-vlm
84
+ (default) (fm+OCRTool) (MLX Qwen3-VL)
85
+ |
86
+ cloud-vlm (opt-in)
87
+ ```
88
+
89
+ Two producers:
90
+
91
+ 1. **CDP reader** — web pages already open (or a static URL fetched without a browser). Not an engine. Always local.
92
+ 2. **Pixel perceiver** — screenshots, clipboard, local files, video keyframes, contact sheets. Speaks one interface. Four engines implement it.
93
+
94
+ A third outcome, not a producer: **`opaque`**. If a canvas or video covers ≥30% of the viewport, the reader sets `opaque: true`. It does not upload. `zrv --ocr-opaque` captures those rectangles and runs the **current pixel engine**.
95
+
96
+ Default path for a tab: CDP text. Default path for a file: `apple-vision`. No implicit cloud.
97
+
98
+ ---
99
+
100
+ ## 5. Engine adapter
101
+
102
+ ### 5.1 Interface
103
+
104
+ TypeScript owns routing. Each engine is a process or a function that accepts the same input and returns the same JSON. Adding a fifth engine is a new file that implements this, plus a name in the registry. No other package changes.
105
+
106
+ ```ts
107
+ type EngineId = "apple-vision" | "apple-fm" | "local-vlm" | "cloud-vlm";
108
+ type Task = "transcribe" | "describe";
109
+
110
+ interface PerceiveInput {
111
+ kind: "image" | "video" | "clipboard";
112
+ path?: string; // absolute file
113
+ bytes?: Buffer; // stdin / base64 decoded, cap 30 MB
114
+ task: Task; // default transcribe
115
+ lang?: string[];
116
+ level?: "accurate" | "fast"; // apple-vision only
117
+ video?: { mode: "scene" | "interval" | "all-idr"; interval?: number; maxFrames?: number };
118
+ }
119
+
120
+ interface TextBlock {
121
+ text: string;
122
+ confidence?: number; // 0..1 when the engine has it
123
+ bbox?: { x: number; y: number; w: number; h: number }; // Vision-normalized, origin bottom-left
124
+ }
125
+
126
+ interface PerceptionResult {
127
+ ok: boolean;
128
+ engine: EngineId;
129
+ task: Task;
130
+ text: string; // always present on ok. Verbatim for transcribe, prose for describe
131
+ blocks?: TextBlock[]; // apple-vision. Others may omit
132
+ transcript?: { t: number; text: string }[]; // video only
133
+ ms: number;
134
+ tokens?: { input: number; output: number };
135
+ costUsd?: number;
136
+ model?: string; // fm / vlm / cloud model id
137
+ error?: string;
138
+ }
139
+ ```
140
+
141
+ `transcribe` is the 65%+27% path. `describe` is the 8% path. Engines that cannot describe (`apple-vision`) reject `task: describe` with exit 1: `engine apple-vision cannot describe; use apple-fm, local-vlm, or cloud-vlm`.
142
+
143
+ Selection, first match wins:
144
+
145
+ 1. `--engine <id>`
146
+ 2. `ZEROVISION_ENGINE`
147
+ 3. `~/.config/zero-vision/config.json` → `engine`
148
+ 4. `apple-vision`
149
+
150
+ Config shape:
151
+
152
+ ```json
153
+ {
154
+ "engine": "apple-vision",
155
+ "engines": {
156
+ "local-vlm": {
157
+ "modelPath": "~/.cache/huggingface/hub/models--mlx-community--Qwen3-VL-8B-Instruct-4bit"
158
+ },
159
+ "cloud-vlm": {
160
+ "provider": "gemini",
161
+ "model": "gemini-3.1-flash-lite",
162
+ "apiKeyEnv": "GEMINI_API_KEY"
163
+ }
164
+ }
165
+ }
166
+ ```
167
+
168
+ Do not read API keys from the config file. Env only.
169
+
170
+ ### 5.2 `apple-vision` (default)
171
+
172
+ Compiled Swift CLI, spawned, JSON on stdout. Same packaging as before: optionalDependency `@zero-vision/darwin-arm64`.
173
+
174
+ ```bash
175
+ zrv-native ocr-image --input <path> --level accurate|fast --json
176
+ zrv-native ocr-clipboard --level accurate --json
177
+ zrv-native ocr-video --input <path> --mode scene --interval 2 --max-frames 60 --json
178
+ ```
179
+
180
+ - macOS 15+: `RecognizeTextRequest`. macOS 14: `VNRecognizeTextRequest` revision 3. Pin the revision.
181
+ - `.accurate` default, `.fast` for `--level fast`. Language correction on. `--lang` repeatable.
182
+ - Serialize requests. ANE stalls above ~2 concurrent (Apple DTS 2025). Process-wide lock of 1.
183
+ - `bbox` is Vision-normalized (origin bottom-left). `--bbox css` flips origin if a caller asks.
184
+ - Exit codes: 0 ok, 1 usage, 2 input missing, 3 Vision unavailable, 4 recognize failed, 5 unsupported video container.
185
+
186
+ **Latency:** warm-path **target** after process start is under 200ms `.accurate` on a 1280×800 printed-English screenshot. A 20ms figure is a target for `.fast` on a cropped region, not a measurement. Do not print either number in the README until `hello.png` is timed on a named machine.
187
+
188
+ Video: AVFoundation `AVAssetImageGenerator`, in-memory `CGImage`. Scene mode = 2 fps sample, keep if 64-bit dHash Hamming > 10, always first and last. Caps: 60 frames default, 200 hard, 180 seconds. Dedup by casefolded whitespace-collapsed text.
189
+
190
+ Contact sheets (27% of sessions): they are PNGs. `ocr-image` on the sheet is the path. Do not split a 3×3 grid in v1. The whole sheet is one transcription. `--grid 3x3` is a later engine option, not a new engine.
191
+
192
+ ffmpeg: only if AVFoundation cannot decode. Temp `.mp4`, OCR, delete. Guaranteed containers: `mp4`, `mov`, `m4v`.
193
+
194
+ ### 5.3 `apple-fm`
195
+
196
+ On-device Foundation Model plus Vision `OCRTool`. Verified 2026-09-07: `/usr/bin/fm` is licensed, `fm available` reports `System model available`, and the CLI takes images:
197
+
198
+ ```bash
199
+ fm respond --image shot.png --text 'Transcribe all text verbatim.' --tool ocr --no-stream
200
+ ```
201
+
202
+ The adapter shells out to `fm`. It does not embed FoundationModels in the Swift OCR binary in v1 (two crash domains). Map stdout to `PerceptionResult.text`. `engine: "apple-fm"`, `model: "system"`. `blocks` omitted unless a later `fm` schema lands.
203
+
204
+ Requires macOS 27. If `fm` is missing or the license is unsigned, exit 3: `apple-fm unavailable`.
205
+
206
+ Use this when the user wants a sentence of understanding on top of OCR (clipped headline: "yes, the last two letters are cut") without leaving the machine. Still not Claude.
207
+
208
+ ### 5.4 `local-vlm`
209
+
210
+ Opt-in. Default weights: the already-cached MLX 4-bit Qwen3-VL-8B at
211
+
212
+ ```text
213
+ ~/.cache/huggingface/hub/models--mlx-community--Qwen3-VL-8B-Instruct-4bit
214
+ ```
215
+
216
+ (5.4 GB on disk, verified 2026-09-07). Do not download weights in v1. If the path is missing, exit 3 with the path we looked at.
217
+
218
+ Spawn via the SpacePilot / MLX runtime already on the machine (`llama-cpp` serves vision; `mlx-lm` is text-only — use a vision-capable runner). Timeout 60s. `task: describe` is the point of this engine. `task: transcribe` is allowed but the README must say `apple-vision` is the transcriber.
219
+
220
+ **Unflown on M1 Max.** SpacePilot lists `qwen2-5-vl-7b-instruct` as `runs_well` on paper and **unflown**. Do not quote tok/s. A local VLM is a privacy/latency play against a cloud round trip, not a quality play against Fable.
221
+
222
+ Fan rule: never load this model from a test suite on a laptop. One optional `ZEROVISION_LIVE_VLM=1` test, off by default.
223
+
224
+ ### 5.5 `cloud-vlm`
225
+
226
+ Opt-in. Never probed, never default, never chosen because another engine failed.
227
+
228
+ ```json
229
+ {
230
+ "provider": "gemini" | "anthropic" | "openai" | "custom",
231
+ "model": "gemini-3.1-flash-lite",
232
+ "baseUrl": "https://…", // custom only
233
+ "apiKeyEnv": "GEMINI_API_KEY"
234
+ }
235
+ ```
236
+
237
+ OpenAI-compatible POST for `custom`. Provider SDKs otherwise, lazy-loaded so the default install still has one runtime dependency (`@modelcontextprotocol/sdk`). Cloud adapters live in `src/engines/cloud/` and pull their SDK only when that engine is selected.
238
+
239
+ Prices move. Do not hardcode "$0.05/Mtok". Print `costUsd` from the provider's usage field when present; otherwise omit.
240
+
241
+ Refuse to run if `apiKeyEnv` is unset. Refuse to run if the input is larger than 10 MB. Log `engine=cloud-vlm model=… bytes=…` to stderr, never the image bytes.
242
+
243
+ This is the 8% aesthetic path when the user wants frontier quality. Creative direction, layout judgment, humor. Not transcription.
244
+
245
+ ---
246
+
247
+ ### 5.6 `tesseract` (Linux OCR)
248
+
249
+ Pure-npm OCR via tesseract.js — no apt, no system daemon. Default pixel
250
+ engine on Linux, same `PerceiveInput` (`task`, `lang`, `level`) as
251
+ `apple-vision`. `level: fast` downsamples long side to 1600px first.
252
+
253
+ When a system `tesseract` binary is on PATH, prefer it (same engine id,
254
+ faster, same output contract) and note `backend: "system"` vs `"wasm"`
255
+ in stderr. Either way the rank holds: local first, never cloud unless named.
256
+
257
+ Video on Linux is ffmpeg (optional dep): scene/interval keyframe extract,
258
+ then this engine per frame. Missing ffmpeg fails closed with the install
259
+ hint, mirroring the missing-native exit 3.
260
+
261
+ ---
262
+
263
+ ## 6. Native packaging (`apple-vision` binary)
264
+
265
+ Ship a **compiled executable**, not a dylib, not a `.node` addon, not `swift-sh`.
266
+
267
+ Node calls it with `spawn`, argv, and a timeout. JSON on stdout. Logs on stderr. One-shot per invocation.
268
+
269
+ ```text
270
+ npm package zero-vision
271
+ optionalDependency @zero-vision/darwin-arm64
272
+ ```
273
+
274
+ Resolve, in order: `ZEROVISION_NATIVE`, that package's `bin/zrv-native`, `./native/.build/release/zrv-native` (dev).
275
+
276
+ If the binary is missing, CDP commands still run. `apple-vision` commands exit 3.
277
+
278
+ Rejected: dylib / N-API, swift-sh, in-process Vision (a crash must not kill MCP).
279
+
280
+ v1 ships `darwin-arm64` only.
281
+
282
+ ---
283
+
284
+ ## 7. Snap (human CLI only)
285
+
286
+ `snap` is not an MCP tool. Interactive capture needs a TTY and a GUI session.
287
+
288
+ ```bash
289
+ zrv snap # screencapture -i -c -x image → clipboard
290
+ zrv snap --ocr # capture, run current engine, print text, pbcopy the text
291
+ zrv snap --save out.png
292
+ zrv snap --ocr --engine apple-fm
293
+ ```
294
+
295
+ `/usr/sbin/screencapture`: `-i` interactive, `-c` clipboard, `-x` silent, `-t png`. Temp files deleted unless `--save`. Escape → exit 130, print nothing.
296
+
297
+ ---
298
+
299
+ ## 8. Browser CDP
300
+
301
+ Unchanged in spirit. Node 22 `fetch` + `WebSocket`. No puppeteer, no playwright.
302
+
303
+ 1. `GET http://127.0.0.1:<port>/json/version` → `webSocketDebuggerUrl`
304
+ 2. WebSocket to the browser target
305
+ 3. `Target.setDiscoverTargets { discover: true }`
306
+ 4. `Target.getTargets`
307
+ 5. `Target.attachToTarget { targetId, flatten: true }`
308
+ 6. Detach on exit
309
+
310
+ Do not launch Chrome. Nothing listening → exit 2.
311
+
312
+ Port probe: `--port` / `ZEROVISION_PORT`, then `AGENT_CHROME_PORT`, then `1948`, then `9223`. First `/json/version` 200 in 150ms wins. **Never 9222** unless `--port 9222` or `ZEROVISION_ALLOW_USER_CHROME=1`. Loopback only unless `--host`.
313
+
314
+ Tab pick: `type=page` (or tab whose child is a page). Drop `chrome://`, `devtools://`, `chrome-extension://`, `edge://`. Default = first non-internal page, title+URL on stderr. `--tab` is id or unique substring. `--url` matches prefix. `--url --navigate` creates a **new** tab, never navigates an existing one. Two matches → exit 1 with the list.
315
+
316
+ Wait: `readyState` / `Page.loadEventFired` (8s), `Accessibility.enable` + `loadComplete` (2s, proceed anyway), quiet 250ms / 5s wall. `--wait-ms`, `--wait-text`. Then `Accessibility.disable` if we enabled it.
317
+
318
+ Shadow DOM: AX flattens it. Default `zrv` text is AX-derived, not `innerText`. Same-origin iframes: `getFullAXTree` per `frameId`. OOPIF: `setAutoAttach`, skip on attach failure.
319
+
320
+ **Text / markdown / a11y YAML:** interesting-only walk. Skip `ignored`, roles `none`/`presentation`. Fold `InlineTextBox`. Cap 24,000 characters. YAML is Playwright-shaped with `uid=eN`. `--a11y --verbose` dumps raw CDP nodes.
321
+
322
+ Opaque probe: `canvas, video` coverage. Do **not** call `canvas.getContext('webgl')`. `--ocr-opaque` screenshots qualifying clips (CSS viewport `getBoundingClientRect`, scroll into view first) and runs the current pixel engine.
323
+
324
+ `--url --fetch`: HTTP GET, no JS. SPA shell (<200 chars of text, `div#root`) → exit 1 telling them to open it in debug Chrome.
325
+
326
+ WebMCP: if the page has tools, the user already has chrome-devtools-mcp `list_webmcp_tools` behind `--categoryExperimentalWebmcp`. zero-vision does not clone that. A skill, not this server, should try page tools before a screenshot.
327
+
328
+ ---
329
+
330
+ ## 9. CLI
331
+
332
+ One bin: `zrv`. `snap` is an argv[0] alias.
333
+
334
+ ```text
335
+ zrv text of the chosen tab
336
+ zrv --md markdown
337
+ zrv --a11y interesting AX tree
338
+ zrv --url <url>
339
+ zrv --url <url> --navigate
340
+ zrv --url <url> --fetch
341
+ zrv --tab <id|substr>
342
+ zrv --tabs
343
+ zrv --ocr-opaque
344
+ zrv --engine apple-vision|apple-fm|local-vlm|cloud-vlm
345
+ zrv --wait-ms N
346
+ zrv --wait-text GLOB
347
+ zrv --scroll
348
+ zrv --json
349
+ zrv --port N
350
+
351
+ zrv ocr <file.png|jpg|heic|pdf|mp4|mov>
352
+ zrv ocr --clipboard
353
+ zrv ocr --task transcribe|describe
354
+ zrv ocr --engine local-vlm
355
+ zrv ocr --level fast
356
+ zrv ocr --lang en-US --lang ja-JP
357
+
358
+ zrv snap
359
+ zrv snap --ocr
360
+ zrv snap --save <path>
361
+
362
+ zrv mcp
363
+ ```
364
+
365
+ PDF: v1 OCRs page 1.
366
+
367
+ `--json` for tab reads includes `engine` only when a pixel engine ran (`--ocr-opaque`). CDP reads use `"source": "cdp"`.
368
+
369
+ stderr is for humans. stdout is the payload. MCP must not print banners on stdout.
370
+
371
+ ---
372
+
373
+ ## 10. MCP server
374
+
375
+ Bin `zrv-mcp` = `zrv mcp`. Transport: stdio. SDK: `@modelcontextprotocol/sdk`.
376
+
377
+ Five tools. No click. No screenshot-as-image.
378
+
379
+ `peek_tabs` — list attachable pages.
380
+
381
+ `peek_page` — `targetId` XOR `url` XOR default tab. `format: text|markdown`. `navigate`, `fetch`, `ocrOpaque`, `waitMs`, `engine`.
382
+
383
+ `peek_a11y` — same targeting, plus `verbose`.
384
+
385
+ `ocr_image` — exactly one of `path`, `base64`, `clipboard`. `engine`, `task` (default `transcribe`). Path absolute. Base64 cap 30 MB, temp file, unlink.
386
+
387
+ `ocr_video` — `path` absolute. `mode`, `interval`, `maxFrames`, `engine` (pixel engines that support video: `apple-vision` required in v1; others may refuse video).
388
+
389
+ Tool descriptions must say **prefer this over a screenshot when the goal is to read text**, and **use `apple-vision` unless the user asked to describe**.
390
+
391
+ ---
392
+
393
+ ## 11. Repository and build
394
+
395
+ Standalone repo, Apache-2.0. This spec is the design of record.
396
+
397
+ ```text
398
+ zero-vision/
399
+ package.json
400
+ tsconfig.json
401
+ src/
402
+ cli.ts
403
+ mcp.ts
404
+ cdp/client.ts
405
+ cdp/attach.ts
406
+ cdp/extract.ts
407
+ engines/
408
+ index.ts registry, PerceiveInput / PerceptionResult
409
+ apple-vision.ts spawn zrv-native
410
+ apple-fm.ts spawn fm respond --image --tool ocr
411
+ local-vlm.ts spawn MLX / llama-cpp vision
412
+ cloud/
413
+ index.ts
414
+ gemini.ts
415
+ anthropic.ts
416
+ openai.ts
417
+ custom.ts
418
+ snap.ts
419
+ native/
420
+ Package.swift
421
+ Sources/zrv-native/
422
+ main.swift
423
+ ImageOCR.swift
424
+ VideoOCR.swift
425
+ Clipboard.swift
426
+ Tests/zrv-nativeTests/
427
+ test/
428
+ cdp/
429
+ engines/ mock each adapter against the unified schema
430
+ extract/
431
+ cli/
432
+ fixtures/
433
+ pages/tiny.html
434
+ pages/spa-shell.html
435
+ ax/tiny.json
436
+ ocr/hello.png
437
+ ocr/hello.expected.txt
438
+ ocr/contact-sheet.png # 3×3 title cards, the 27% case
439
+ video/slides.mp4
440
+ .github/workflows/ci.yml
441
+ ```
442
+
443
+ `package.json` dependencies: `@modelcontextprotocol/sdk` only at install. Cloud SDKs are optional peerDependencies, imported only when `cloud-vlm` is selected.
444
+
445
+ Engines: `"node": ">=22"`.
446
+
447
+ CI: Ubuntu = TS + mock CDP + mock engines. macOS = plus `swift test` + `hello.png` contains `HELLO ZEROVISION` + contact-sheet fixture yields more than one title card. Skip `local-vlm` and `cloud-vlm` in CI.
448
+
449
+ Release: GitHub Actions builds the arm64 native binary, publishes `@zero-vision/darwin-arm64` then `zero-vision`. Restore the executable bit after `upload-artifact`.
450
+
451
+ ---
452
+
453
+ ## 12. Testing
454
+
455
+ | Layer | How |
456
+ | --- | --- |
457
+ | AX walk | Golden files from recorded `getFullAXTree` JSON |
458
+ | Port probe | Mock HTTP. Assert 9222 skipped |
459
+ | Engine schema | Each adapter, including mocks, round-trips `PerceptionResult` |
460
+ | apple-vision | `hello.png`. Skip if not darwin |
461
+ | Contact sheet | `contact-sheet.png` produces ≥3 distinct lines |
462
+ | Video | `slides.mp4`. Two timestamps, deduped |
463
+ | apple-fm | Mock `fm` stdout. Live `ZEROVISION_LIVE_FM=1` off by default |
464
+ | local-vlm / cloud-vlm | Mocks only in CI |
465
+ | Snap | Unit-test argv to screencapture, not interactive `-i` |
466
+
467
+ ---
468
+
469
+ ## 13. Security
470
+
471
+ - CDP is unauthenticated. Loopback only.
472
+ - Never default-attach to 9222.
473
+ - Never navigate unless `--navigate`.
474
+ - Never `Runtime.evaluate` user-supplied JS.
475
+ - OCR `path`: realpath, must be a file.
476
+ - MCP `base64`: size cap, `mkdtemp`, unlink in `finally`.
477
+ - `cloud-vlm` never runs without an env key. Never log image bytes.
478
+ - `snap` is CLI-only.
479
+ - Do not log page text to files.
480
+
481
+ ---
482
+
483
+ ## 14. Non-goals (v1)
484
+
485
+ - Click, type, hover, fill
486
+ - Launching or killing Chrome
487
+ - Silent cloud fallback
488
+ - Downloading VLM weights
489
+ - Splitting contact-sheet grids
490
+ - Windows / Linux OCR
491
+ - Browser extension, stealth, crawl
492
+ - Shipping UIDs as a click API
493
+
494
+ ---
495
+
496
+ ## 15. Key decisions
497
+
498
+ 1. **Read tool, not a driver.**
499
+ 2. **Engine adapter, one schema.** The 65% and the 8% do not share a backend. They share an interface.
500
+ 3. **Default is `apple-vision`.** Matches 92% of audited visual sessions (transcription + contact sheets).
501
+ 4. **Cloud is an engine, not a fallback.** Opt-in, keyed, logged.
502
+ 5. **`fm` is the on-device understander.** Licensed and available on macOS 27. OCRTool is the transcriber inside that session.
503
+ 6. **Local VLM is cached-weights-or-fail.** No surprise 15 GB download.
504
+ 7. **Compiled Swift for Vision only.** `fm` and MLX stay out of that process.
505
+ 8. **9222 is opt-in. AX-derived text, not `innerText`.**
506
+ 9. **Contact sheets are images.** The 27% case is `ocr-image`, not `ocr-video`.
507
+
508
+ ---
509
+
510
+ ## 16. Implementation order
511
+
512
+ 1. Engine interface + `apple-vision` + `hello.png`
513
+ 2. CDP attach + `zrv --tabs` + AX text
514
+ 3. Extract: markdown, a11y YAML, opaque probe
515
+ 4. Contact-sheet fixture
516
+ 5. CLI snap
517
+ 6. MCP, `engine` on pixel tools
518
+ 7. Video scene mode
519
+ 8. `apple-fm` adapter (`fm respond --image --tool ocr`)
520
+ 9. `local-vlm` adapter against the cached 4-bit Qwen3-VL
521
+ 10. `cloud-vlm` adapter, last, behind config
522
+ 11. Packaging
523
+
524
+ Do not start at MCP. Do not add click. Do not wire cloud before the default transcriber is boring.
525
+
526
+ ---
527
+
528
+ ## 17. Not confirmed
529
+
530
+ - End-to-end `apple-vision` latency on this M1 Max. Target ≠ measurement.
531
+ - Qwen3-VL-8B-4bit tok/s or TTFT on this machine. Cached, unflown.
532
+ - Whether `fm respond --tool ocr` returns structured blocks or only prose. Adapter treats stdout as `text` until a schema is verified.
533
+ - Whether `/json/list` order tracks focus in Chrome 144+.
534
+ - Current $ / MTok of any cloud Flash-class model. Read the provider on the day you wire `cloud-vlm`.
535
+
536
+ ---
537
+
538
+ ## 18. Sources
539
+
540
+ - Fleet audit, 168 visual sessions, `~/.agentworth/agentworth.db`, 2026-09-07 (65 / 27 / 8 / 0)
541
+ - `fm respond --help`, `fm available` → System model available, 2026-09-07
542
+ - Claude Vision patch tokens: https://platform.claude.com/docs/en/build-with-claude/vision
543
+ - Chrome CDP Accessibility / Target domains
544
+ - Playwright MCP snapshots: https://playwright.dev/mcp/snapshots
545
+ - Apple `VNRecognizeTextRequest`; WWDC26 Foundation Models `OCRTool` (session 241)
546
+ - Apple DTS ANE concurrency: https://developer.apple.com/forums/thread/784672
547
+ - SpacePilot `check` / `models` on this M1 Max, 2026-09-07: Qwen3-VL-8B-4bit 5.4 GB cached, Qwen2.5-VL-7B recipe unflown
548
+ - Node 22: global `fetch`, global `WebSocket`
549
+ - `screencapture(1)`
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "zero-vision",
3
+ "version": "0.1.0",
4
+ "description": "Read a page, a screenshot, or a video as text. Default path never sends pixels off the machine.",
5
+ "type": "module",
6
+ "license": "Apache-2.0",
7
+ "bin": {
8
+ "zrv": "dist/cli.js",
9
+ "zrv-mcp": "dist/mcp.js",
10
+ "snap": "dist/cli.js"
11
+ },
12
+ "files": [
13
+ "dist",
14
+ "docs/spec.md",
15
+ "README.md",
16
+ "LICENSE",
17
+ "NOTICE"
18
+ ],
19
+ "engines": {
20
+ "node": ">=22"
21
+ },
22
+ "scripts": {
23
+ "build": "tsc",
24
+ "native": "swift build -c release --package-path native --product zrv-native",
25
+ "test": "tsc && node --test dist/test/*.js",
26
+ "zrv": "node dist/cli.js"
27
+ },
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+ssh://git@github.com/unfoundbox-crew/zero-vision.git"
31
+ },
32
+ "dependencies": {
33
+ "@modelcontextprotocol/sdk": "^1.12.1",
34
+ "tesseract.js": "^6.0.1"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "^22.13.10",
38
+ "typescript": "^5.8.2"
39
+ }
40
+ }