@sylphx/iris 0.2.0 → 0.3.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.
package/README.md CHANGED
@@ -1,261 +1,90 @@
1
- <div align="center">
2
-
3
1
  # Iris
4
2
 
5
- ### Image evidence for agents
6
-
7
- Canonical package: **`@sylphx/iris`** · bin **`iris`**
8
-
9
- **Iris** — **Rust-first** local image facts (`@sylphx/iris`) local image facts, not default VLM guesses. `sharp`/`exifr` are optional, not required for MCP.
10
-
11
- <p align="center">
12
- <img src="https://mark.sylphx.com/api/v1/banner?type=holo&theme=tokyonight&text=image+reader+mcp&desc=Evidence-first+image+reading+for+AI+agents+%E2%80%94+metadata%2C+OCR+text%2C+regions%2C+and+ci&height=200&animation=rise&credit=0" alt="image-reader-mcp — Sylphx Mark banner" width="100%" />
13
- </p>
14
-
15
- ### Your agent looked at the image. **Did it see the truth?**
16
-
17
- Evidence-first image reading for AI agents. One call turns any local image into an
18
- **Agent Media Twin** — dimensions, metadata, optional OCR with bounding boxes, and
19
- trust warnings you can cite without asking a vision LLM to guess.
20
-
21
- [![npm version](https://img.shields.io/npm/v/@sylphx/iris?style=flat-square)](https://www.npmjs.com/package/@sylphx/iris)
22
- [![License](https://img.shields.io/badge/License-MIT-blue?style=flat-square)](https://opensource.org/licenses/MIT)
23
- [![CI/CD](https://img.shields.io/github/actions/workflow/status/SylphxAI/image-reader-mcp/ci.yml?style=flat-square&label=CI/CD)](https://github.com/SylphxAI/image-reader-mcp/actions/workflows/ci.yml)
24
- [![TypeScript](https://img.shields.io/badge/TypeScript-7.0-blue.svg?style=flat-square)](https://www.typescriptlang.org/)
25
-
26
- **Local-first** · **One smart `read_image` call** · **Evidence with bbox + provenance** · **23 tests**
27
-
28
- SOTA family roadmap: [docs/roadmap/sota-family-roadmap.md](docs/roadmap/sota-family-roadmap.md).
29
-
30
- [⭐ Star this repo](https://github.com/SylphxAI/image-reader-mcp) if agents should read images with facts, not vision-model guesses.
31
- · [Quick start](#quick-start) · [See it work](#see-it-work) · [Why not vision LLM guess?](#why-not-vision-llm-guess)
32
-
33
- This repository is product SSOT. Sibling agent tools live in separate repos.
34
- Each instrument is an independent repository (marketplace + stars).
35
-
36
- </div>
37
-
38
- ---
39
-
40
-
41
-
42
- ## See objects (L2, optional)
43
-
44
- With a local Florence-class sidecar or Ollama, the same `read_image` can return **open-vocab objects** with pixel bboxes and scores:
45
-
46
- ```json
47
- { "path": "/abs/photo.jpg", "include_semantics": true, "semantics_prompt": "people and animals" }
48
- ```
3
+ ### Image facts with pixel-level proof
49
4
 
50
- Objects are `scored_non_locator` evidence — deterministic L0/L1 facts (geometry/OCR/layout) stay authoritative and always on.
51
-
52
- ## Read images (not vague vision)
53
-
54
- Iris is **local-first**: geometry + OCR + **layout blocks** + **agent_map** so a text-only agent can understand picture architecture without a vision model.
55
-
56
- Spec: [docs/specs/agent-image-read-contract.md](docs/specs/agent-image-read-contract.md)
57
-
58
- **Local-first frontier:** Rust decode, Tesseract native layout (no npm ML), optional Ollama VLM; cloud URL optional. Zero API key. Optional **L2 local semantics** (include_semantics) detects open-vocab objects (people/animals/things) with pixel bboxes via an official Florence-class sidecar (examples/florence-sidecar/) or Ollama -- never authority over OCR/layout locators.
59
-
60
- ## Product docs
61
-
62
- | Doc | Purpose |
63
- | --- | --- |
64
- | [docs/POSITIONING.md](docs/POSITIONING.md) | Strategic positioning |
65
- | [docs/COMPETITIVE.md](docs/COMPETITIVE.md) | Peer anchors and wedge |
66
- | [docs/EVIDENCE_CONTRACT.md](docs/EVIDENCE_CONTRACT.md) | Evidence = result contract |
67
- | [docs/TOOL_SURFACE.md](docs/TOOL_SURFACE.md) | Few clear tools policy |
68
- | [docs/PRODUCT_INDEPENDENCE.md](docs/PRODUCT_INDEPENDENCE.md) | This repo is SSOT |
69
- | [docs/IPPB.md](docs/IPPB.md) | Independent public product bar |
70
- | [docs/PUBLISH.md](docs/PUBLISH.md) | npm/git publish status |
71
-
72
- ## The problem
73
-
74
- Images are not filenames. They are pixels, EXIF, orientation, embedded GPS,
75
- hidden metadata, and text that only exists if you OCR it with geometry.
76
-
77
- Most agent stacks route images through a **vision LLM** and get a plausible
78
- description. Dimensions get rounded. OCR text gets paraphrased. Metadata
79
- disappears. Citations become "the model said so." Then the agent hallucinates —
80
- confidently.
81
-
82
- **Image Reader MCP is built for the moment your agent needs measurable facts about
83
- an image, not a creative caption.**
84
-
85
- ## Why not vision LLM guess?
86
-
87
- | Typical vision path | Image Reader MCP |
88
- | --- | --- |
89
- | "Describe this image" | Return filename, mime, dimensions, and structured metadata |
90
- | Paraphrased OCR | Optional Tesseract lines with bounding boxes and confidence |
91
- | GPS and EXIF leak into context | GPS redacted; trust warnings for suspicious metadata |
92
- | No provenance | Agent Media Twin JSON with measurable, citeable fields |
93
- | Cloud API by default | **Local-first** — Rust decode + optional Tesseract OCR; sharp/exifr only optional fallbacks |
94
- | Ship and pray | **23** unit tests on schema, metadata, OCR hooks, safety limits, doctor, and release gate |
95
-
96
- ## See it work
97
-
98
- ### Install (30 seconds)
5
+ Iris gives agents deterministic facts from screenshots, UI captures, forms,
6
+ diagrams, charts, and other images. It returns dimensions, metadata, OCR
7
+ regions, layout blocks, crops, and trust warnings without requiring a
8
+ generative vision model.
99
9
 
100
10
  ```bash
101
- npm install -g @sylphx/iris
102
- iris doctor
103
- claude mcp add iris -- npx @sylphx/iris
11
+ npx -y @sylphx/iris
104
12
  ```
105
13
 
106
- **Install once. Call once.**
14
+ For Claude Code:
107
15
 
108
16
  ```bash
109
- claude mcp add image-reader -- npx @sylphx/iris
110
- ```
111
-
112
- ```json
113
- {
114
- "path": "/absolute/path/to/photo.jpg",
115
- "include_metadata": true,
116
- "include_ocr": true
117
- }
17
+ claude mcp add iris -- npx -y @sylphx/iris
118
18
  ```
119
19
 
120
- `read_image` inspects the file locally and returns an Agent Media Twin — no
121
- generative LLM required:
20
+ ## The fastest useful workflow
122
21
 
123
22
  ```json
124
23
  {
125
- "filename": "photo.jpg",
126
- "mime": "image/jpeg",
127
- "dimensions": { "width": 4032, "height": 3024 },
128
- "orientation": 1,
129
- "metadata": {
130
- "Make": "ExampleCamera",
131
- "Model": "Pro X",
132
- "DateTimeOriginal": "2026-03-15T14:22:10"
133
- },
134
- "ocr": {
135
- "available": true,
136
- "lines": [
137
- {
138
- "text": "INVOICE #1042",
139
- "bbox": { "x": 120, "y": 48, "width": 310, "height": 36 },
140
- "confidence": 92
141
- }
142
- ]
143
- },
144
- "trust_warnings": []
24
+ "path": "/absolute/path/to/screenshot.png",
25
+ "include_ocr": true
145
26
  }
146
27
  ```
147
28
 
148
- Abbreviated shape optional OCR skips gracefully when Tesseract is not installed.
29
+ The result includes image dimensions, hash, OCR lines with bounding boxes,
30
+ layout blocks, a text map for agents, and explicit warnings or gaps.
149
31
 
150
- ## MCP Tool Surface
32
+ ## Jobs Iris is built for
151
33
 
152
- | Tool | Use it when the agent needs to... |
34
+ | Ask your agent | Iris returns |
153
35
  | --- | --- |
154
- | `read_image` | Read a local image and return dimensions, mime, metadata, optional OCR, and trust warnings. |
155
-
156
- Supported formats: PNG, JPEG, GIF, WebP, TIFF, and other formats the **Rust decode engine** supports (optional sharp covers additional formats when installed).
157
-
158
- ## Quick Start
159
-
160
- ### Claude Code
161
-
162
- ```bash
163
- claude mcp add image-reader -- npx @sylphx/iris
164
- ```
36
+ | Read this screenshot.” | text regions and layout facts |
37
+ | “What changed between these UI captures?” | image diff evidence |
38
+ | “Extract this form.” | OCR lines and boxes |
39
+ | “Crop the important region.” | citeable pixel evidence |
40
+ | “Check the image metadata.” | format, dimensions, EXIF trust warnings |
165
41
 
166
- ### Claude Desktop
42
+ ## Tool surface
167
43
 
168
- Add this to `claude_desktop_config.json`:
169
-
170
- ```json
171
- {
172
- "mcpServers": {
173
- "image-reader": {
174
- "command": "npx",
175
- "args": ["@sylphx/iris"]
176
- }
177
- }
178
- }
179
- ```
180
-
181
- ### Any MCP Client
182
-
183
- ```bash
184
- npx @sylphx/iris
185
- ```
44
+ | Tool | Purpose |
45
+ | --- | --- |
46
+ | `read_image` | Primary image facts and optional OCR |
47
+ | `image_probe` | Cheap metadata and geometry probe |
48
+ | `crop_region` | Extract a citeable pixel region |
49
+ | `compare_images` | Report pixel and layout differences between two images |
186
50
 
187
- Node.js `>=22.13` is required. Optional OCR uses a local Tesseract adapter when
188
- installed — no cloud credentials required by default.
51
+ ## Predictable defaults
189
52
 
190
- ## Security model
53
+ - `fast` returns deterministic metadata, geometry, and layout.
54
+ - `quality` explicitly enables OCR and local semantic helpers.
55
+ - Generative captions and object detection are opt-in and never authoritative.
56
+ - GPS metadata is redacted unless explicitly requested.
57
+ - Oversized or unsupported files fail with a structured error.
191
58
 
192
- - **Local-first** `read_image` resolves paths on the local machine; no cloud vision API by default.
193
- - **GPS redaction** — location metadata is stripped from agent-facing output unless explicitly opted in.
194
- - **Size and format limits** — oversized or unsupported inputs return structured errors, not partial guesses.
195
- - **Optional OCR** — Tesseract runs locally when installed; missing OCR is reported as `available: false`, not silent failure.
196
- - **Trust warnings** — suspicious EXIF, orientation, or metadata anomalies surface in `trust_warnings` for agent verification.
59
+ ## Why agents trust it
197
60
 
198
- ## Release proof
61
+ Every result keeps source path and hash, pixel locators, extraction route,
62
+ confidence or warnings, and known gaps. OCR and optional model output never
63
+ replace deterministic geometry.
199
64
 
200
- Claims are backed by CI `benchmark:release-gate` and the shipped-path matrix (Rust-default route, no legacy Node engine on primary tools).
65
+ ## Companion MCP tools
201
66
 
202
- ```bash
203
- bun run benchmark:release-gate
204
- ```
67
+ | Product | Job |
68
+ | --- | --- |
69
+ | [Citra](https://github.com/SylphxAI/citra) | PDF answers with page-level proof |
70
+ | [Cue](https://github.com/SylphxAI/cue) | Video timelines and timestamp evidence |
71
+ | [Spine](https://github.com/SylphxAI/spine) | Repository architecture and impact |
72
+ | [Locus](https://github.com/SylphxAI/locus) | Exact code-chunk retrieval |
73
+ | [Lookout](https://github.com/SylphxAI/lookout) | Web research with source excerpts |
205
74
 
206
- Artifact: `benchmark-artifacts/image_reader_release_gate.json` must report `status: passed` before release.
75
+ Each product is independent. Install only the tools your agent needs.
207
76
 
208
77
  ## Development
209
78
 
210
79
  ```bash
211
- git clone https://github.com/SylphxAI/image-reader-mcp.git
212
- cd image-reader-mcp
213
80
  bun install
214
81
  bun run build
215
82
  bun test
216
- bun run doctor
217
- bun run benchmark:release-gate
218
- ```
219
-
220
- Useful checks:
221
-
222
- ```bash
223
- bun run check
224
- bun run typecheck
225
- bun run validate
83
+ cargo test
84
+ bun run benchmark:public-proof
226
85
  bun run benchmark:release-gate
227
86
  ```
228
87
 
229
- Example `read_image` requests live in [`examples/`](examples/).
230
-
231
- ## Support
232
-
233
- - [Issues](https://github.com/SylphxAI/image-reader-mcp/issues)
234
- - [npm package](https://www.npmjs.com/package/@sylphx/iris)
235
- - Portfolio orchestration: [smart-reader-mcp](https://github.com/SylphxAI/smart-reader-mcp)
236
-
237
- ## Help this reach more builders
238
-
239
- If vision-model guesses have wasted your context, your citations, or your trust
240
- in agent output, you are exactly who this project is for.
241
-
242
- **[⭐ Star the repo](https://github.com/SylphxAI/image-reader-mcp)** — it is the
243
- fastest way to help more agent builders find evidence-first image reading. Share
244
- it in your MCP client setup, team wiki, or agent stack README.
245
-
246
- ### Discovery (in progress)
247
-
248
- | Channel | Status |
249
- | --- | --- |
250
- | [Glama MCP directory](https://glama.ai/mcp/servers/SylphxAI/image-reader-mcp) | Listed — [claim server](https://glama.ai/mcp/servers/SylphxAI/image-reader-mcp/admin) for full discoverability |
251
- | [Official MCP Registry](https://registry.modelcontextprotocol.io/v0.1/servers?search=io.github.SylphxAI/image-reader-mcp) | Listed — `io.github.SylphxAI/image-reader-mcp` @ v0.1.0 |
252
- | [TensorBlock MCP Index PR #1113](https://github.com/TensorBlock/awesome-mcp-servers/pull/1113) | Open — multimedia/document processing listing |
253
- | [MCP servers community issue #4500](https://github.com/modelcontextprotocol/servers/issues/4500) | Open — community server highlight |
254
- | [mcp.so listing issue #3068](https://github.com/chatmcp/mcpso/issues/3068) | Open — directory submission request |
255
- | [mcpservers.org submit](https://mcpservers.org/submit) | Not listed yet — free web-form submission |
256
-
257
- Know another MCP directory? [Open an issue](https://github.com/SylphxAI/image-reader-mcp/issues/new) with the link.
258
-
259
88
  ## License
260
89
 
261
- MIT © [SylphxAI](https://github.com/SylphxAI)
90
+ MIT
@@ -20,12 +20,14 @@ var __toESM = (mod, isNodeMode, target) => {
20
20
  }
21
21
  target = mod != null ? __create(__getProtoOf(mod)) : {};
22
22
  const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
23
- for (let key of __getOwnPropNames(mod))
24
- if (!__hasOwnProp.call(to, key))
25
- __defProp(to, key, {
26
- get: __accessProp.bind(mod, key),
27
- enumerable: true
28
- });
23
+ if (mod && typeof mod === "object" || typeof mod === "function") {
24
+ for (let key of __getOwnPropNames(mod))
25
+ if (!__hasOwnProp.call(to, key))
26
+ __defProp(to, key, {
27
+ get: __accessProp.bind(mod, key),
28
+ enumerable: true
29
+ });
30
+ }
29
31
  if (canCache)
30
32
  cache.set(mod, to);
31
33
  return to;
@@ -93,7 +95,7 @@ var init_is = __esm(() => {
93
95
  });
94
96
 
95
97
  // node_modules/detect-libc/lib/process.js
96
- var require_process = __commonJS((exports, module) => {
98
+ var require_process = __commonJS(function(exports, module) {
97
99
  var isLinux = () => process.platform === "linux";
98
100
  var report = null;
99
101
  var getReport = () => {
@@ -113,7 +115,7 @@ var require_process = __commonJS((exports, module) => {
113
115
  });
114
116
 
115
117
  // node_modules/detect-libc/lib/filesystem.js
116
- var require_filesystem = __commonJS((exports, module) => {
118
+ var require_filesystem = __commonJS(function(exports, module) {
117
119
  var fs = __require("fs");
118
120
  var LDD_PATH = "/usr/bin/ldd";
119
121
  var SELF_PATH = "/proc/self/exe";
@@ -147,7 +149,7 @@ var require_filesystem = __commonJS((exports, module) => {
147
149
  });
148
150
 
149
151
  // node_modules/detect-libc/lib/elf.js
150
- var require_elf = __commonJS((exports, module) => {
152
+ var require_elf = __commonJS(function(exports, module) {
151
153
  var interpreterPath = (elf) => {
152
154
  if (elf.length < 64) {
153
155
  return null;
@@ -181,7 +183,7 @@ var require_elf = __commonJS((exports, module) => {
181
183
  });
182
184
 
183
185
  // node_modules/detect-libc/lib/detect-libc.js
184
- var require_detect_libc = __commonJS((exports, module) => {
186
+ var require_detect_libc = __commonJS(function(exports, module) {
185
187
  var childProcess = __require("child_process");
186
188
  var { isLinux, getReport } = require_process();
187
189
  var { LDD_PATH, SELF_PATH, readFile, readFileSync } = require_filesystem();
@@ -427,7 +429,7 @@ var require_detect_libc = __commonJS((exports, module) => {
427
429
  });
428
430
 
429
431
  // node_modules/semver/internal/constants.js
430
- var require_constants = __commonJS((exports, module) => {
432
+ var require_constants = __commonJS(function(exports, module) {
431
433
  var SEMVER_SPEC_VERSION = "2.0.0";
432
434
  var MAX_LENGTH = 256;
433
435
  var MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER || 9007199254740991;
@@ -455,13 +457,13 @@ var require_constants = __commonJS((exports, module) => {
455
457
  });
456
458
 
457
459
  // node_modules/semver/internal/debug.js
458
- var require_debug = __commonJS((exports, module) => {
460
+ var require_debug = __commonJS(function(exports, module) {
459
461
  var debug = typeof process === "object" && process.env && process.env.NODE_DEBUG && /\bsemver\b/i.test(process.env.NODE_DEBUG) ? (...args) => console.error("SEMVER", ...args) : () => {};
460
462
  module.exports = debug;
461
463
  });
462
464
 
463
465
  // node_modules/semver/internal/re.js
464
- var require_re = __commonJS((exports, module) => {
466
+ var require_re = __commonJS(function(exports, module) {
465
467
  var {
466
468
  MAX_SAFE_COMPONENT_LENGTH,
467
469
  MAX_SAFE_BUILD_LENGTH,
@@ -546,7 +548,7 @@ var require_re = __commonJS((exports, module) => {
546
548
  });
547
549
 
548
550
  // node_modules/semver/internal/parse-options.js
549
- var require_parse_options = __commonJS((exports, module) => {
551
+ var require_parse_options = __commonJS(function(exports, module) {
550
552
  var looseOption = Object.freeze({ loose: true });
551
553
  var emptyOpts = Object.freeze({});
552
554
  var parseOptions = (options) => {
@@ -562,7 +564,7 @@ var require_parse_options = __commonJS((exports, module) => {
562
564
  });
563
565
 
564
566
  // node_modules/semver/internal/identifiers.js
565
- var require_identifiers = __commonJS((exports, module) => {
567
+ var require_identifiers = __commonJS(function(exports, module) {
566
568
  var numeric = /^[0-9]+$/;
567
569
  var compareIdentifiers = (a, b) => {
568
570
  if (typeof a === "number" && typeof b === "number") {
@@ -584,7 +586,7 @@ var require_identifiers = __commonJS((exports, module) => {
584
586
  });
585
587
 
586
588
  // node_modules/semver/classes/semver.js
587
- var require_semver = __commonJS((exports, module) => {
589
+ var require_semver = __commonJS(function(exports, module) {
588
590
  var debug = require_debug();
589
591
  var { MAX_LENGTH, MAX_SAFE_INTEGER } = require_constants();
590
592
  var { safeRe: re, t } = require_re();
@@ -866,7 +868,7 @@ var require_semver = __commonJS((exports, module) => {
866
868
  });
867
869
 
868
870
  // node_modules/semver/functions/parse.js
869
- var require_parse = __commonJS((exports, module) => {
871
+ var require_parse = __commonJS(function(exports, module) {
870
872
  var SemVer = require_semver();
871
873
  var parse = (version, options, throwErrors = false) => {
872
874
  if (version instanceof SemVer) {
@@ -885,7 +887,7 @@ var require_parse = __commonJS((exports, module) => {
885
887
  });
886
888
 
887
889
  // node_modules/semver/functions/valid.js
888
- var require_valid = __commonJS((exports, module) => {
890
+ var require_valid = __commonJS(function(exports, module) {
889
891
  var parse = require_parse();
890
892
  var valid = (version, options) => {
891
893
  const v = parse(version, options);
@@ -895,7 +897,7 @@ var require_valid = __commonJS((exports, module) => {
895
897
  });
896
898
 
897
899
  // node_modules/semver/functions/clean.js
898
- var require_clean = __commonJS((exports, module) => {
900
+ var require_clean = __commonJS(function(exports, module) {
899
901
  var parse = require_parse();
900
902
  var clean = (version, options) => {
901
903
  const s = parse(version.trim().replace(/^[=v]+/, ""), options);
@@ -905,7 +907,7 @@ var require_clean = __commonJS((exports, module) => {
905
907
  });
906
908
 
907
909
  // node_modules/semver/functions/inc.js
908
- var require_inc = __commonJS((exports, module) => {
910
+ var require_inc = __commonJS(function(exports, module) {
909
911
  var SemVer = require_semver();
910
912
  var inc = (version, release, options, identifier, identifierBase) => {
911
913
  if (typeof options === "string") {
@@ -923,7 +925,7 @@ var require_inc = __commonJS((exports, module) => {
923
925
  });
924
926
 
925
927
  // node_modules/semver/functions/diff.js
926
- var require_diff = __commonJS((exports, module) => {
928
+ var require_diff = __commonJS(function(exports, module) {
927
929
  var parse = require_parse();
928
930
  var diff = (version1, version2) => {
929
931
  const v1 = parse(version1, null, true);
@@ -964,28 +966,28 @@ var require_diff = __commonJS((exports, module) => {
964
966
  });
965
967
 
966
968
  // node_modules/semver/functions/major.js
967
- var require_major = __commonJS((exports, module) => {
969
+ var require_major = __commonJS(function(exports, module) {
968
970
  var SemVer = require_semver();
969
971
  var major = (a, loose) => new SemVer(a, loose).major;
970
972
  module.exports = major;
971
973
  });
972
974
 
973
975
  // node_modules/semver/functions/minor.js
974
- var require_minor = __commonJS((exports, module) => {
976
+ var require_minor = __commonJS(function(exports, module) {
975
977
  var SemVer = require_semver();
976
978
  var minor = (a, loose) => new SemVer(a, loose).minor;
977
979
  module.exports = minor;
978
980
  });
979
981
 
980
982
  // node_modules/semver/functions/patch.js
981
- var require_patch = __commonJS((exports, module) => {
983
+ var require_patch = __commonJS(function(exports, module) {
982
984
  var SemVer = require_semver();
983
985
  var patch = (a, loose) => new SemVer(a, loose).patch;
984
986
  module.exports = patch;
985
987
  });
986
988
 
987
989
  // node_modules/semver/functions/prerelease.js
988
- var require_prerelease = __commonJS((exports, module) => {
990
+ var require_prerelease = __commonJS(function(exports, module) {
989
991
  var parse = require_parse();
990
992
  var prerelease = (version, options) => {
991
993
  const parsed = parse(version, options);
@@ -995,28 +997,28 @@ var require_prerelease = __commonJS((exports, module) => {
995
997
  });
996
998
 
997
999
  // node_modules/semver/functions/compare.js
998
- var require_compare = __commonJS((exports, module) => {
1000
+ var require_compare = __commonJS(function(exports, module) {
999
1001
  var SemVer = require_semver();
1000
1002
  var compare = (a, b, loose) => new SemVer(a, loose).compare(new SemVer(b, loose));
1001
1003
  module.exports = compare;
1002
1004
  });
1003
1005
 
1004
1006
  // node_modules/semver/functions/rcompare.js
1005
- var require_rcompare = __commonJS((exports, module) => {
1007
+ var require_rcompare = __commonJS(function(exports, module) {
1006
1008
  var compare = require_compare();
1007
1009
  var rcompare = (a, b, loose) => compare(b, a, loose);
1008
1010
  module.exports = rcompare;
1009
1011
  });
1010
1012
 
1011
1013
  // node_modules/semver/functions/compare-loose.js
1012
- var require_compare_loose = __commonJS((exports, module) => {
1014
+ var require_compare_loose = __commonJS(function(exports, module) {
1013
1015
  var compare = require_compare();
1014
1016
  var compareLoose = (a, b) => compare(a, b, true);
1015
1017
  module.exports = compareLoose;
1016
1018
  });
1017
1019
 
1018
1020
  // node_modules/semver/functions/compare-build.js
1019
- var require_compare_build = __commonJS((exports, module) => {
1021
+ var require_compare_build = __commonJS(function(exports, module) {
1020
1022
  var SemVer = require_semver();
1021
1023
  var compareBuild = (a, b, loose) => {
1022
1024
  const versionA = new SemVer(a, loose);
@@ -1027,63 +1029,63 @@ var require_compare_build = __commonJS((exports, module) => {
1027
1029
  });
1028
1030
 
1029
1031
  // node_modules/semver/functions/sort.js
1030
- var require_sort = __commonJS((exports, module) => {
1032
+ var require_sort = __commonJS(function(exports, module) {
1031
1033
  var compareBuild = require_compare_build();
1032
1034
  var sort = (list, loose) => list.sort((a, b) => compareBuild(a, b, loose));
1033
1035
  module.exports = sort;
1034
1036
  });
1035
1037
 
1036
1038
  // node_modules/semver/functions/rsort.js
1037
- var require_rsort = __commonJS((exports, module) => {
1039
+ var require_rsort = __commonJS(function(exports, module) {
1038
1040
  var compareBuild = require_compare_build();
1039
1041
  var rsort = (list, loose) => list.sort((a, b) => compareBuild(b, a, loose));
1040
1042
  module.exports = rsort;
1041
1043
  });
1042
1044
 
1043
1045
  // node_modules/semver/functions/gt.js
1044
- var require_gt = __commonJS((exports, module) => {
1046
+ var require_gt = __commonJS(function(exports, module) {
1045
1047
  var compare = require_compare();
1046
1048
  var gt = (a, b, loose) => compare(a, b, loose) > 0;
1047
1049
  module.exports = gt;
1048
1050
  });
1049
1051
 
1050
1052
  // node_modules/semver/functions/lt.js
1051
- var require_lt = __commonJS((exports, module) => {
1053
+ var require_lt = __commonJS(function(exports, module) {
1052
1054
  var compare = require_compare();
1053
1055
  var lt = (a, b, loose) => compare(a, b, loose) < 0;
1054
1056
  module.exports = lt;
1055
1057
  });
1056
1058
 
1057
1059
  // node_modules/semver/functions/eq.js
1058
- var require_eq = __commonJS((exports, module) => {
1060
+ var require_eq = __commonJS(function(exports, module) {
1059
1061
  var compare = require_compare();
1060
1062
  var eq = (a, b, loose) => compare(a, b, loose) === 0;
1061
1063
  module.exports = eq;
1062
1064
  });
1063
1065
 
1064
1066
  // node_modules/semver/functions/neq.js
1065
- var require_neq = __commonJS((exports, module) => {
1067
+ var require_neq = __commonJS(function(exports, module) {
1066
1068
  var compare = require_compare();
1067
1069
  var neq = (a, b, loose) => compare(a, b, loose) !== 0;
1068
1070
  module.exports = neq;
1069
1071
  });
1070
1072
 
1071
1073
  // node_modules/semver/functions/gte.js
1072
- var require_gte = __commonJS((exports, module) => {
1074
+ var require_gte = __commonJS(function(exports, module) {
1073
1075
  var compare = require_compare();
1074
1076
  var gte = (a, b, loose) => compare(a, b, loose) >= 0;
1075
1077
  module.exports = gte;
1076
1078
  });
1077
1079
 
1078
1080
  // node_modules/semver/functions/lte.js
1079
- var require_lte = __commonJS((exports, module) => {
1081
+ var require_lte = __commonJS(function(exports, module) {
1080
1082
  var compare = require_compare();
1081
1083
  var lte = (a, b, loose) => compare(a, b, loose) <= 0;
1082
1084
  module.exports = lte;
1083
1085
  });
1084
1086
 
1085
1087
  // node_modules/semver/functions/cmp.js
1086
- var require_cmp = __commonJS((exports, module) => {
1088
+ var require_cmp = __commonJS(function(exports, module) {
1087
1089
  var eq = require_eq();
1088
1090
  var neq = require_neq();
1089
1091
  var gt = require_gt();
@@ -1130,7 +1132,7 @@ var require_cmp = __commonJS((exports, module) => {
1130
1132
  });
1131
1133
 
1132
1134
  // node_modules/semver/functions/coerce.js
1133
- var require_coerce = __commonJS((exports, module) => {
1135
+ var require_coerce = __commonJS(function(exports, module) {
1134
1136
  var SemVer = require_semver();
1135
1137
  var parse = require_parse();
1136
1138
  var { safeRe: re, t } = require_re();
@@ -1173,7 +1175,7 @@ var require_coerce = __commonJS((exports, module) => {
1173
1175
  });
1174
1176
 
1175
1177
  // node_modules/semver/functions/truncate.js
1176
- var require_truncate = __commonJS((exports, module) => {
1178
+ var require_truncate = __commonJS(function(exports, module) {
1177
1179
  var parse = require_parse();
1178
1180
  var constants = require_constants();
1179
1181
  var SemVer = require_semver();
@@ -1211,7 +1213,7 @@ var require_truncate = __commonJS((exports, module) => {
1211
1213
  });
1212
1214
 
1213
1215
  // node_modules/semver/internal/lrucache.js
1214
- var require_lrucache = __commonJS((exports, module) => {
1216
+ var require_lrucache = __commonJS(function(exports, module) {
1215
1217
  class LRUCache {
1216
1218
  constructor() {
1217
1219
  this.max = 1000;
@@ -1246,7 +1248,7 @@ var require_lrucache = __commonJS((exports, module) => {
1246
1248
  });
1247
1249
 
1248
1250
  // node_modules/semver/classes/range.js
1249
- var require_range = __commonJS((exports, module) => {
1251
+ var require_range = __commonJS(function(exports, module) {
1250
1252
  var SPACE_CHARACTERS = /\s+/g;
1251
1253
 
1252
1254
  class Range {
@@ -1628,7 +1630,7 @@ var require_range = __commonJS((exports, module) => {
1628
1630
  });
1629
1631
 
1630
1632
  // node_modules/semver/classes/comparator.js
1631
- var require_comparator = __commonJS((exports, module) => {
1633
+ var require_comparator = __commonJS(function(exports, module) {
1632
1634
  var ANY = Symbol("SemVer ANY");
1633
1635
 
1634
1636
  class Comparator {
@@ -1739,7 +1741,7 @@ var require_comparator = __commonJS((exports, module) => {
1739
1741
  });
1740
1742
 
1741
1743
  // node_modules/semver/functions/satisfies.js
1742
- var require_satisfies = __commonJS((exports, module) => {
1744
+ var require_satisfies = __commonJS(function(exports, module) {
1743
1745
  var Range = require_range();
1744
1746
  var satisfies = (version, range, options) => {
1745
1747
  try {
@@ -1753,14 +1755,14 @@ var require_satisfies = __commonJS((exports, module) => {
1753
1755
  });
1754
1756
 
1755
1757
  // node_modules/semver/ranges/to-comparators.js
1756
- var require_to_comparators = __commonJS((exports, module) => {
1758
+ var require_to_comparators = __commonJS(function(exports, module) {
1757
1759
  var Range = require_range();
1758
1760
  var toComparators = (range, options) => new Range(range, options).set.map((comp) => comp.map((c) => c.value).join(" ").trim().split(" "));
1759
1761
  module.exports = toComparators;
1760
1762
  });
1761
1763
 
1762
1764
  // node_modules/semver/ranges/max-satisfying.js
1763
- var require_max_satisfying = __commonJS((exports, module) => {
1765
+ var require_max_satisfying = __commonJS(function(exports, module) {
1764
1766
  var SemVer = require_semver();
1765
1767
  var Range = require_range();
1766
1768
  var maxSatisfying = (versions, range, options) => {
@@ -1786,7 +1788,7 @@ var require_max_satisfying = __commonJS((exports, module) => {
1786
1788
  });
1787
1789
 
1788
1790
  // node_modules/semver/ranges/min-satisfying.js
1789
- var require_min_satisfying = __commonJS((exports, module) => {
1791
+ var require_min_satisfying = __commonJS(function(exports, module) {
1790
1792
  var SemVer = require_semver();
1791
1793
  var Range = require_range();
1792
1794
  var minSatisfying = (versions, range, options) => {
@@ -1812,7 +1814,7 @@ var require_min_satisfying = __commonJS((exports, module) => {
1812
1814
  });
1813
1815
 
1814
1816
  // node_modules/semver/ranges/min-version.js
1815
- var require_min_version = __commonJS((exports, module) => {
1817
+ var require_min_version = __commonJS(function(exports, module) {
1816
1818
  var SemVer = require_semver();
1817
1819
  var Range = require_range();
1818
1820
  var gt = require_gt();
@@ -1866,7 +1868,7 @@ var require_min_version = __commonJS((exports, module) => {
1866
1868
  });
1867
1869
 
1868
1870
  // node_modules/semver/ranges/valid.js
1869
- var require_valid2 = __commonJS((exports, module) => {
1871
+ var require_valid2 = __commonJS(function(exports, module) {
1870
1872
  var Range = require_range();
1871
1873
  var validRange = (range, options) => {
1872
1874
  try {
@@ -1879,7 +1881,7 @@ var require_valid2 = __commonJS((exports, module) => {
1879
1881
  });
1880
1882
 
1881
1883
  // node_modules/semver/ranges/outside.js
1882
- var require_outside = __commonJS((exports, module) => {
1884
+ var require_outside = __commonJS(function(exports, module) {
1883
1885
  var SemVer = require_semver();
1884
1886
  var Comparator = require_comparator();
1885
1887
  var { ANY } = Comparator;
@@ -1945,21 +1947,21 @@ var require_outside = __commonJS((exports, module) => {
1945
1947
  });
1946
1948
 
1947
1949
  // node_modules/semver/ranges/gtr.js
1948
- var require_gtr = __commonJS((exports, module) => {
1950
+ var require_gtr = __commonJS(function(exports, module) {
1949
1951
  var outside = require_outside();
1950
1952
  var gtr = (version, range, options) => outside(version, range, ">", options);
1951
1953
  module.exports = gtr;
1952
1954
  });
1953
1955
 
1954
1956
  // node_modules/semver/ranges/ltr.js
1955
- var require_ltr = __commonJS((exports, module) => {
1957
+ var require_ltr = __commonJS(function(exports, module) {
1956
1958
  var outside = require_outside();
1957
1959
  var ltr = (version, range, options) => outside(version, range, "<", options);
1958
1960
  module.exports = ltr;
1959
1961
  });
1960
1962
 
1961
1963
  // node_modules/semver/ranges/intersects.js
1962
- var require_intersects = __commonJS((exports, module) => {
1964
+ var require_intersects = __commonJS(function(exports, module) {
1963
1965
  var Range = require_range();
1964
1966
  var intersects = (r1, r2, options) => {
1965
1967
  r1 = new Range(r1, options);
@@ -1970,7 +1972,7 @@ var require_intersects = __commonJS((exports, module) => {
1970
1972
  });
1971
1973
 
1972
1974
  // node_modules/semver/ranges/simplify.js
1973
- var require_simplify = __commonJS((exports, module) => {
1975
+ var require_simplify = __commonJS(function(exports, module) {
1974
1976
  var satisfies = require_satisfies();
1975
1977
  var compare = require_compare();
1976
1978
  module.exports = (versions, range, options) => {
@@ -2017,7 +2019,7 @@ var require_simplify = __commonJS((exports, module) => {
2017
2019
  });
2018
2020
 
2019
2021
  // node_modules/semver/ranges/subset.js
2020
- var require_subset = __commonJS((exports, module) => {
2022
+ var require_subset = __commonJS(function(exports, module) {
2021
2023
  var Range = require_range();
2022
2024
  var Comparator = require_comparator();
2023
2025
  var { ANY } = Comparator;
@@ -2177,7 +2179,7 @@ var require_subset = __commonJS((exports, module) => {
2177
2179
  });
2178
2180
 
2179
2181
  // node_modules/semver/index.js
2180
- var require_semver2 = __commonJS((exports, module) => {
2182
+ var require_semver2 = __commonJS(function(exports, module) {
2181
2183
  var internalRe = require_re();
2182
2184
  var constants = require_constants();
2183
2185
  var SemVer = require_semver();
@@ -4508,7 +4510,7 @@ var init_operation = __esm(() => {
4508
4510
  });
4509
4511
 
4510
4512
  // node_modules/@img/colour/color.cjs
4511
- var require_color = __commonJS((exports, module) => {
4513
+ var require_color = __commonJS(function(exports, module) {
4512
4514
  var __defProp2 = Object.defineProperty;
4513
4515
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4514
4516
  var __getOwnPropNames2 = Object.getOwnPropertyNames;
@@ -6084,7 +6086,7 @@ var require_color = __commonJS((exports, module) => {
6084
6086
  });
6085
6087
 
6086
6088
  // node_modules/@img/colour/index.cjs
6087
- var require_colour = __commonJS((exports, module) => {
6089
+ var require_colour = __commonJS(function(exports, module) {
6088
6090
  module.exports = require_color().default;
6089
6091
  });
6090
6092
 
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@sylphx/iris",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "mcpName": "io.github.SylphxAI/iris",
5
- "description": "Iris \u2014 evidence-first image reading for AI agents. Metadata, OCR text, regions, citeable evidence. Local-first. No generative LLM authority.",
5
+ "description": "Iris image facts with pixel-level proof. Local-first metadata, OCR boxes, layout, crops, and image diff evidence.",
6
6
  "type": "module",
7
7
  "bin": {
8
8
  "iris": "./bin/iris"
@@ -24,9 +24,9 @@
24
24
  "license": "MIT",
25
25
  "repository": {
26
26
  "type": "git",
27
- "url": "git+https://github.com/SylphxAI/image-reader-mcp.git"
27
+ "url": "git+https://github.com/SylphxAI/iris.git"
28
28
  },
29
- "homepage": "https://github.com/SylphxAI/image-reader-mcp#readme",
29
+ "homepage": "https://sylphxai.github.io/iris/",
30
30
  "keywords": [
31
31
  "mcp",
32
32
  "model-context-protocol",
@@ -39,7 +39,7 @@
39
39
  "agent-media-twin"
40
40
  ],
41
41
  "engines": {
42
- "node": ">=22.13.0"
42
+ "bun": ">=1.4.0"
43
43
  },
44
44
  "scripts": {
45
45
  "build:rust": "cargo build --release -p image-reader-core -p image-reader-cli -p image-reader-mcp-server && bun scripts/stage-rust-mcp.ts",
@@ -63,7 +63,10 @@
63
63
  "prepublishOnly": "bun run clean && bun run build && bun run build:rust",
64
64
  "release": "bun run typecheck && bun run check && bun run build && bun test && changeset publish",
65
65
  "benchmark:public-proof": "bun scripts/public-proof.ts",
66
- "brand:pack-plan": "bun scripts/brand-pack-plan.ts"
66
+ "brand:pack-plan": "bun scripts/brand-pack-plan.ts",
67
+ "docs:dev": "vitepress dev docs",
68
+ "docs:build": "vitepress build docs",
69
+ "docs:preview": "vitepress preview docs"
67
70
  },
68
71
  "dependencies": {
69
72
  "@modelcontextprotocol/sdk": "^1.29.0",
@@ -75,16 +78,17 @@
75
78
  "@changesets/cli": "^2.31.0",
76
79
  "@types/node": "^25.9.1",
77
80
  "bunup": "0.16.31",
78
- "typescript": "^7.0.2"
81
+ "typescript": "^7.0.2",
82
+ "vitepress": "1.6.4"
79
83
  },
80
- "packageManager": "bun@1.3.12",
84
+ "packageManager": "bun@1.4.0",
81
85
  "private": false,
82
86
  "optionalDependencies": {
83
87
  "sharp": "^0.35.0",
84
88
  "exifr": "^7.1.3",
85
- "@sylphx/iris-darwin-arm64": "0.2.0",
86
- "@sylphx/iris-darwin-x64": "0.2.0",
87
- "@sylphx/iris-linux-x64-gnu": "0.2.0",
88
- "@sylphx/iris-linux-arm64-gnu": "0.2.0"
89
+ "@sylphx/iris-darwin-arm64": "0.3.0",
90
+ "@sylphx/iris-darwin-x64": "0.3.0",
91
+ "@sylphx/iris-linux-x64-gnu": "0.3.0",
92
+ "@sylphx/iris-linux-arm64-gnu": "0.3.0"
89
93
  }
90
94
  }
@@ -116,8 +116,9 @@ export const readImage = tool()
116
116
  const fileStat = await stat(resolvedPath);
117
117
  validateImageSafety({ fileSizeBytes: fileStat.size });
118
118
 
119
+ const profile = input.profile ?? 'fast';
119
120
  const includeMetadata = input.include_metadata ?? true;
120
- const includeOcr = input.include_ocr ?? false;
121
+ const includeOcr = input.include_ocr ?? profile === 'quality';
121
122
  const ocrLanguages = input.ocr_languages ?? ['eng'];
122
123
  const useRustDecode = shouldUseRustDecodeEngine();
123
124
 
@@ -243,7 +244,11 @@ export const readImage = tool()
243
244
  };
244
245
  }
245
246
 
246
- twin = await applyImageIntelligence(twin, resolvedPath, input, includeOcr);
247
+ const intelligenceInput =
248
+ profile === 'quality' && input.include_semantics === undefined
249
+ ? { ...input, include_semantics: true as const }
250
+ : input;
251
+ twin = await applyImageIntelligence(twin, resolvedPath, intelligenceInput, includeOcr);
247
252
 
248
253
  if (input.region !== undefined) {
249
254
  if (!useRustDecode) {
@@ -20,6 +20,12 @@ export const imageDimensionsSchema = z.object({
20
20
 
21
21
  export const readImageArgsSchema = z.object({
22
22
  path: z.string().min(1).describe('Path to the local image file (absolute or relative to cwd).'),
23
+ profile: z
24
+ .enum(['fast', 'quality'])
25
+ .optional()
26
+ .describe(
27
+ 'Predictable work profile. Fast is deterministic geometry; quality explicitly enables OCR and optional local semantics. '
28
+ ),
23
29
  include_metadata: z
24
30
  .boolean()
25
31
  .optional()