simframe 0.1.0 → 0.4.1

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
@@ -4,59 +4,41 @@
4
4
  [![npm](https://img.shields.io/npm/v/simframe.svg)](https://www.npmjs.com/package/simframe)
5
5
  [![license](https://img.shields.io/npm/l/simframe.svg)](./LICENSE)
6
6
 
7
- **Always-warm iOS Simulator frames for coding agents.**
7
+ **Eyes, hands and memory for an agent driving the iOS Simulator.**
8
8
 
9
9
  [Website](https://lvlrsajjad.github.io/simframe/) · [npm](https://www.npmjs.com/package/simframe)
10
10
 
11
- An agent that drives the iOS Simulator spends most of its time waiting on
12
- screenshots. Every "let me check the screen" is a fresh `simctl io screenshot`:
13
- a process spawn, a framebuffer grab, a file write, an image encode. On this
14
- machine that is ~130 ms of pure blocking latency, paid again on every look — and
15
- the agent pays it *twice* whenever it screenshots too early, sees a
16
- mid-animation frame, and has to look again.
11
+ An agent driving the iOS Simulator is slow for three reasons, and only the first
12
+ one is obvious:
17
13
 
18
- simframe removes the wait from the request path. A tiny background loop keeps
19
- the newest frame of your simulator permanently warm on disk, so when the agent
20
- asks what's on screen it gets an answer in **~20 ms** instead of ~130 ms — and
21
- can ask "did anything change?" for **~2 ms and no image at all**.
14
+ 1. **Every look is a wait.** `simctl io screenshot` costs ~130 ms of blocking
15
+ latency, paid again on every glance — and paid twice whenever the agent
16
+ captures mid-animation and has to look again.
17
+ 2. **Every step is a round trip.** Tap, screenshot, reason, tap, screenshot. A
18
+ twelve-step flow costs twelve model turns, and the model turns cost far more
19
+ than the milliseconds.
20
+ 3. **Nothing is remembered.** The same screen gets re-read and re-reasoned about
21
+ every single time it appears.
22
22
 
23
- ```
24
- without simframe with simframe
25
- agent asks ──► spawn simctl ──► grab ──► encode ──► image ~130-400 ms
26
- agent asks ──► read the frame that is already there ──► image ~20 ms
27
- agent polls ──► read 300 bytes of JSON ──► text ~2 ms
28
- ```
23
+ simframe attacks all three: a background loop keeps the newest frame warm, whole
24
+ flows run in one call, and screens the agent has seen before are answered from
25
+ memory.
29
26
 
30
- ## Why this makes an agent faster
27
+ ## What changed, measured
31
28
 
32
- Speed is not only latency. It is also *how many* round trips a question takes
33
- and how many tokens each one costs.
29
+ Same four-tab navigation flow, on a real production app:
34
30
 
35
- | Question the agent has | Before | With simframe |
31
+ | | Before | With simframe |
36
32
  | --- | --- | --- |
37
- | "What's on screen?" | screenshot, ~130-400 ms, full image every time | `sim_look`, ~20 ms, warm frame |
38
- | "Has it finished loading yet?" | screenshot in a loop, an image per attempt | `sim_state`, ~2 ms, **text only** |
39
- | "Did my tap do anything?" | screenshot, compare by eye | `sim_state` — a stable screen hash plus an ASCII map of which regions moved |
40
- | "Wait for the animation to end" | sleep, screenshot, hope, repeat | `sim_wait` — blocks until the screen actually settles, then returns the frame |
41
- | "What did that transition look like?" | 5 screenshots, 5 round trips, 5 images | `sim_strip` — the last N buffered frames tiled into **one** image, looking backwards in time |
42
-
43
- The change map is the part that pays for itself. A screen hash and a
44
- 4×8 movement grid cost a couple of hundred bytes, so an agent can poll freely
45
- and only spend image tokens when there is genuinely something new to look at:
33
+ | Look at the screen | ~130–400 ms, blocking | **~20 ms**, already captured |
34
+ | "Did anything change?" | a full image | **~2 ms**, text only |
35
+ | A 5-step flow | 5+ model round trips | **1 call**, ~7 s |
36
+ | Finding a control | read tree (~570 ms) + reason | **~1 ms** from memory |
37
+ | Same flow, 3rd run | no improvement — every run is the first | **3304 ms, 4/4 from memory** |
46
38
 
47
- ```
48
- $ simframe state
49
- iPhone 17 Pro frame #3 age 124ms 322x700
50
- hash 007cfefefefefefefefefefefefefe00 diff 0.15453 stable 0ms
51
- @@@@
52
- ###*
53
- +*#*
54
- :.#*
55
- ..#*
56
- ..#*
57
- ..#*
58
- @@@@ ← a screen sliding in from the right, mid-transition
59
- ```
39
+ The same four-tab tour, run three times back to back: **7370 ms → 5160 ms →
40
+ 3304 ms**, with 1, then 3, then 4 of the four controls resolved from memory and
41
+ no mis-taps. What is left is mostly the app's own animation and data load.
60
42
 
61
43
  ## Install
62
44
 
@@ -65,171 +47,271 @@ npm install -g simframe
65
47
  simframe doctor
66
48
  ```
67
49
 
68
- `doctor` verifies Xcode's command line tools, `sips`, a booted simulator, and
69
- an actual round-trip capture.
50
+ `doctor` checks each capability separately and tells you what you have:
51
+
52
+ ```
53
+ ok xcrun xcrun version 72.
54
+ ok sips available
55
+ ok input driver (idb) companion built Sep 1 2026
56
+ ok on-device OCR available
57
+ ok booted simulator iPhone 17 Pro (iOS 26.5)
58
+ ok capture frame #888 322x700 in 2ms (age 538ms)
59
+ ```
70
60
 
71
61
  ### Claude Code
72
62
 
73
63
  ```bash
74
- claude mcp add simframe -- npx -y simframe mcp
64
+ claude mcp add --scope user simframe -- npx -y simframe mcp
75
65
  ```
76
66
 
67
+ `--scope user` makes it available in every session; without it the server is
68
+ registered only for the directory you ran the command in.
69
+
77
70
  ### Any other MCP client
78
71
 
79
72
  ```json
80
73
  {
81
74
  "mcpServers": {
82
- "simframe": {
83
- "command": "npx",
84
- "args": ["-y", "simframe", "mcp"]
85
- }
75
+ "simframe": { "command": "npx", "args": ["-y", "simframe", "mcp"] }
86
76
  }
87
77
  }
88
78
  ```
89
79
 
90
- Nothing else to set up. Capture starts on the first tool call, targets the
91
- booted simulator, and stops itself 15 minutes after the last request.
80
+ ## Capabilities are independent
92
81
 
93
- ## MCP tools
82
+ Each layer works without the ones above it, and `doctor` tells you which you
83
+ have. **Observation needs nothing but Xcode.**
94
84
 
95
- | Tool | What it does |
96
- | --- | --- |
97
- | `sim_look` | The newest buffered frame as an image, no capture wait. `detail`: `low` (~420 px) / `normal` (~700 px, default) / `high` (~1100 px) / `full` (native). |
98
- | `sim_state` | Text only: screen hash, how long the screen has been still, change since the previous frame, and the region movement map. |
99
- | `sim_wait` | Blocks until the screen settles (`mode: "stable"`) or moves away from what it shows now (`mode: "change"`), then returns the frame. |
100
- | `sim_strip` | The last N buffered frames tiled into one image, oldest first, with millisecond offsets. |
101
- | `sim_capture` | `status` / `start` / `stop` for the background loops. Rarely needed. |
102
- | `sim_devices` | Booted simulators simframe can capture. |
85
+ | Capability | Needs | Without it |
86
+ | --- | --- | --- |
87
+ | Watch the screen, wait, recall | nothing extra | — |
88
+ | Read labels + coordinates from pixels | `swiftc` (Xcode CLT) | falls back to the accessibility tree alone |
89
+ | Tap, type, swipe | [`idb`](https://fbidb.io) | simframe observes but cannot touch |
103
90
 
104
- Every tool takes an optional `device` (UDID or a substring of the name) and
105
- defaults to the booted simulator.
91
+ ```bash
92
+ # input, optional
93
+ brew tap facebook/fb && brew install idb-companion && pipx install fb-idb
94
+ ```
106
95
 
107
- ## CLI
96
+ Homebrew may ask you to trust the tap first; that is a deliberate prompt for a
97
+ human, and the narrow form is `brew trust --formula facebook/fb/idb-companion`.
108
98
 
109
- The same capabilities without an agent, useful for debugging and scripts:
99
+ ## The tools
100
+
101
+ | Tool | What it does |
102
+ | --- | --- |
103
+ | `sim_look` | Newest frame as an image, no capture wait. |
104
+ | `sim_state` | Text only: screen hash, what changed **since your last look**, region movement map. |
105
+ | `sim_wait` | Waits for the screen to change *and then* settle. |
106
+ | `sim_do` | A whole flow in one call — tap, type, scroll, assert — each step settling before the next. |
107
+ | `sim_ui` | The screen as labels + tap coordinates, from accessibility **and** OCR. |
108
+ | `sim_recall` | Look backwards: a timeline of what happened, or the frame from N seconds ago. |
109
+ | `sim_strip` | Recent frames tiled into one image. |
110
+ | `sim_capture` / `sim_devices` | Manage capture loops; list simulators. |
111
+
112
+ ## Baselines: the thing to understand
113
+
114
+ Every change question is really "changed **since when**?" — and the answer is
115
+ almost never "since the previous frame". A UI transition is over in about 700 ms,
116
+ so comparing consecutive frames tells a caller that polls every few seconds
117
+ "nothing changed", even though the screen is completely different from when it
118
+ last looked.
119
+
120
+ So simframe compares against **the last frame you observed**. Over MCP that is
121
+ automatic. From the CLI, capture a baseline before you act:
110
122
 
111
123
  ```bash
112
- simframe start # start the capture loop
113
- simframe state # metadata + change map
114
- simframe frame --out=now.png # newest frame, --detail=low|normal|high|full
115
- simframe wait --stable-ms=700 # block until the screen settles
116
- simframe wait --change # block until the screen changes
117
- simframe strip --count=6 # contact sheet of recent frames
118
- simframe status # what is running, and how fresh
119
- simframe stop --all
120
- simframe devices --all
121
- simframe doctor
124
+ H=$(simframe mark)
125
+ # ...tap, launch, navigate...
126
+ simframe wait --since=$H # change, then settle
127
+ simframe state --since=$H # what moved, as text
122
128
  ```
123
129
 
124
- ## How it works
130
+ The same applies to waiting. `--mode=settle` (the default) waits for a change and
131
+ *then* for stillness, because a bare "wait until stable" called in the moment
132
+ before an animation starts will correctly, and uselessly, return immediately.
133
+
134
+ ## Screen memory
135
+
136
+ An accessibility tree is a promise apps do not always keep. In testing against a
137
+ real production app, its custom tab bar published **no children at all**, its
138
+ icon buttons carried unreadable private-use glyphs, and its React Native text
139
+ inputs were **absent from the tree entirely** — the controls used most were
140
+ exactly the ones that could not be tapped by name.
141
+
142
+ So simframe reads the screen two ways and remembers the result:
143
+
144
+ - **Accessibility** gives real hit targets, types and enabled state.
145
+ - **On-device OCR** (Apple's Vision, ~290 ms, no model round trip) gives every
146
+ label a person can actually see, with coordinates.
147
+ - The merge is keyed by a **layout hash**, so the next visit is a file read.
125
148
 
126
149
  ```
127
- ┌──────────────────────────── background, one per simulator ───┐
128
- │ xcrun simctl io screenshot ──► sips -Z ──► decode PNG │
129
- │ ~130 ms ~30 ms ~6 ms │
130
- │ │ │
131
- │ rename into ~/.simframe/<udid>/ │
132
- │ latest.png · ring/<seq>.png · state.json │
133
- └───────────────────────────────────────────────────────────────┘
134
- │ a rename is atomic
135
- ┌──────────────────────────▼────────────────────────────────────┐
136
- │ MCP server / CLI: stat + read. No simctl in the request path.│
137
- └───────────────────────────────────────────────────────────────┘
150
+ first visit to a screen ~1000 ms read tree + OCR, store the map
151
+ every visit after that ~1 ms look it up
138
152
  ```
139
153
 
140
- A few decisions worth knowing about:
141
-
142
- - **Files are the IPC.** The loop renames completed frames into place and
143
- readers just read them. A rename is atomic, so a reader can never see a
144
- half-written frame, and there is no socket, port or protocol to get wrong.
145
- - **Zero image dependencies.** Resizing uses `sips`, which ships with macOS.
146
- PNG encode/decode and all frame comparison are a few hundred lines of plain
147
- JavaScript over `node:zlib`. The only runtime dependency is the MCP SDK.
148
- - **It backs off when nothing is happening.** 4 fps while the screen is moving,
149
- 1.5 fps once it has been still for 2.5 s, snapping back instantly on change.
150
- Measured on an M-series Mac: **1.1 % CPU idle, 3.1 % active.**
151
- - **Comparison is done on a small grayscale grid,** which is why "did anything
152
- change?" costs microseconds. The screen hash is a 128-bit mean-threshold
153
- hash: the same screen always produces the same hash, even though the JPEG and
154
- PNG bytes coming out of `simctl` are not stable frame to frame.
155
- - **One writer per device.** Ownership is recorded in `meta.json`; a second loop
156
- refuses to start, and a loop that has been superseded retires itself. Two
157
- loops would otherwise overwrite and prune each other's frames.
158
- - **Capture is independent of the Simulator window.** `simctl` reads the
159
- framebuffer, so frames keep flowing while the window is hidden, behind other
160
- windows, or on another Space.
154
+ OCR is also more accurate than measuring by eye. On one tab bar the first tab
155
+ centre sat at x=62, not the x=40 an even five-way split predicts — a silent
156
+ mis-tap on every attempt.
157
+
158
+ Two details that matter:
159
+
160
+ - **Containers do not absorb their contents.** A tab bar encloses all five tab
161
+ labels but is not any of them, so the merge only combines an element with text
162
+ of comparable size.
163
+ - **Ambiguity is reported, not guessed.** A word that is both a screen title and
164
+ a tab returns an error listing both with coordinates, because silently tapping
165
+ the title looks exactly like nothing happening.
166
+
167
+ ### Why a layout hash, not a frame hash
168
+
169
+ The frame hash changes whenever any pixel group changes — a clock digit, one new
170
+ row of data — which makes it useless as a key for "have I seen this screen
171
+ before?". The layout hash crops the status bar and takes a difference hash over a
172
+ 12×24 grid.
173
+
174
+ A mean-threshold hash was tried first and was actively dangerous: low-contrast
175
+ app screens collapsed onto identical values, so unrelated screens matched at
176
+ distance 0 and taps landed on the wrong control. Measured on a real app:
177
+
178
+ | | Hamming distance |
179
+ | --- | --- |
180
+ | Same screen, revisited while settled | **0–4** |
181
+ | Same screen, but loading vs loaded | 48–98 |
182
+ | **Different screens** | **77–113** |
183
+
184
+ Only one of those errors is dangerous. Matching the *wrong* screen would tap the
185
+ wrong control, and that needs two different screens to land within 12 bits of
186
+ each other — the closest pair ever measured was 77. Failing to recognise a screen
187
+ you have seen is harmless: it rebuilds the map, costs ~600 ms, and taps correctly.
188
+ So the tolerance is deliberately far below the collision floor rather than tuned
189
+ to maximise hits.
190
+
191
+ That middle row is worth knowing about: a screen mid-load genuinely does not look
192
+ like the same screen loaded, so the first visit after a cold launch usually
193
+ rebuilds. Hit rates climb as an app warms up, which is exactly what the three-pass
194
+ numbers above show.
195
+
196
+ ## Does this work on *your* app?
197
+
198
+ Nothing in simframe is written for a particular app. What varies between apps is
199
+ how much of the accessibility tree exists, and simframe is built to degrade
200
+ rather than fail:
201
+
202
+ - **Good tree** → tap by label, batch aggressively, everything just works.
203
+ - **Partial tree** (custom tab bars, icon buttons) → OCR fills the gaps; you tap
204
+ by the visible text instead.
205
+ - **No tree at all** → OCR alone still yields labels and coordinates.
206
+
207
+ Run `simframe ui` on any screen to see exactly what simframe can see, with each
208
+ target marked `ax` or `ocr`. If something you can read is not listed, that is a
209
+ bug worth reporting.
210
+
211
+ Two honest caveats. OCR reads **text**, so a purely graphical icon with no label
212
+ is invisible to both paths — use `sim_ui` to get its coordinates from the tree,
213
+ or tap by position. And the confirm-button vocabulary (`APPLY`, `OK`, `SAVE`,
214
+ `DONE`…) is English; a localised UI needs those words extended.
161
215
 
162
216
  ## Measured
163
217
 
164
- iPhone 17 Pro, iOS 26.5, Apple Silicon, default settings:
218
+ iPhone 17 Pro, iOS 26.5, Apple Silicon, default settings.
165
219
 
166
220
  | | |
167
221
  | --- | --- |
168
222
  | Warm frame read (`sim_look`) | ~20 ms |
169
223
  | State check (`sim_state`) | ~2 ms |
170
224
  | Contact sheet (`sim_strip`, 5 frames) | ~30 ms |
171
- | Cold start (first frame after boot) | ~400 ms, once |
172
- | Frame age when read | ≤ ~250 ms active, ≤ ~670 ms idle |
173
- | Raw `simctl io screenshot` for comparison | ~130 ms, on every single look |
174
- | CPU | 1.1 % idle, 3.1 % active |
175
- | Disk | ~2 MB per device (24-frame ring) |
225
+ | Accessibility tree read | ~570 ms |
226
+ | On-device OCR of a full frame | ~290 ms |
227
+ | Screen map: first visit / remembered | ~600 ms / **~1 ms** |
228
+ | Raw `simctl io screenshot`, for comparison | ~130 ms, every look |
229
+ | Cold start, first frame | ~400 ms, once |
230
+ | CPU | 1.1 % idle · 3.1 % active |
231
+ | Frame memory | ~60 s of screen, ~2.7 MB |
176
232
 
177
- Image sizes are chosen for token cost as much as legibility: at `detail: normal`
178
- a frame is ~322×700, roughly a third of the pixels — and so roughly a third of
179
- the image tokens — of a native-resolution screenshot, while the status bar stays
180
- readable. `sim_state` sends no image at all.
233
+ ## How it works
181
234
 
182
- ## Requirements
235
+ ```
236
+ ┌──────────────────── background, one loop per simulator ─────────────────────┐
237
+ │ simctl screenshot ──► sips ──► decode ──► hash + diff ──► rename into │
238
+ │ ~130 ms ~30 ms ~6 ms ~/.simframe/<udid>/ │
239
+ └─────────────────────────────────────────────────────────────────────────────┘
240
+ │ a rename is atomic
241
+ ┌────────────────────────────────────▼────────────────────────────────────────┐
242
+ │ MCP server / CLI: stat + read. No simctl anywhere in the request path. │
243
+ │ Screen memory: layout hash ──► label → point, built once per screen. │
244
+ └─────────────────────────────────────────────────────────────────────────────┘
245
+ ```
183
246
 
184
- - macOS with Xcode command line tools (`xcrun simctl`)
185
- - Node.js ≥ 18.17
186
- - A booted iOS Simulator
247
+ - **Files are the IPC.** The loop renames completed frames into place; readers
248
+ just read them. A rename is atomic, so a reader can never see a half-written
249
+ frame, and there is no socket or protocol to get wrong.
250
+ - **Almost no dependencies.** Resizing uses `sips`; PNG codec, hashing and frame
251
+ comparison are plain JavaScript over `node:zlib`. The only runtime dependency
252
+ is the MCP SDK. OCR is a ~60-line Swift file compiled on first use.
253
+ - **It backs off when nothing happens.** 4 fps while the screen moves, 1.5 fps
254
+ once still, snapping back instantly on change.
255
+ - **One writer per device.** Ownership lives in `meta.json`; `stop` refuses to
256
+ kill a loop another client is using unless forced.
257
+ - **A wedged capture loop never looks like a calm screen.** Every answer carries
258
+ a liveness check, and `wait` fails loudly rather than quietly timing out.
259
+ - **An action with no visible effect is reported, not waited out.** Selecting a
260
+ radio button moves ~0.1 % of the screen — below the change threshold — which
261
+ used to burn the full timeout. Now the step returns in ~3 s marked
262
+ `[no visible change]`, so you know to check rather than wait.
263
+
264
+ ## CLI
265
+
266
+ ```bash
267
+ simframe start # start the capture loop
268
+ simframe mark # hash of the current frame, for --since
269
+ simframe state --since=$H # what changed, as text
270
+ simframe frame --out=now.png # newest frame
271
+ simframe wait --since=$H # change, then settle
272
+ simframe ui # labels + tap points (ax and ocr)
273
+ simframe recall # what happened in the last minute
274
+ simframe recall --ago=15000 # the frame from 15s ago
275
+ simframe strip --count=6 # contact sheet
276
+ simframe status / stop [--force] / devices / doctor
277
+ ```
187
278
 
188
279
  ## Limitations
189
280
 
190
- - Simulators only. `simctl` cannot capture a physical device.
191
- - simframe **reads** the screen; it does not tap, swipe or type. It is meant to
192
- sit alongside whatever already drives input, replacing only the screenshot.
193
- - Capture tops out near 6 fps, because `simctl io screenshot` costs ~130 ms.
194
- Fast animations are sampled, not recorded.
281
+ - Simulators only — `simctl` cannot capture a physical device.
282
+ - Capture tops out near 6 fps, because `simctl io screenshot` costs ~130 ms. Fast
283
+ animations are sampled, not recorded.
284
+ - Region maps need a baseline inside the ~90 s history window. Older baselines
285
+ still get a reliable changed / did-not-change, without a map of what moved.
286
+ - Screen memory assumes a screen's layout is stable. A screen that reflows
287
+ dramatically between visits will simply be rebuilt.
288
+ - It speeds up *confirming* a fix, not *locating* one. A bug living in a memo
289
+ comparator or a stale closure is not visible in any frame.
195
290
 
196
291
  ## Roadmap
197
292
 
198
- - A higher-frame-rate backend via `simctl io recordVideo` piped through ffmpeg,
199
- used automatically when ffmpeg is present.
200
- - Optional accessibility-tree text alongside the frame, so an agent can read
201
- labels without spending image tokens.
202
- - Fusing input with settle-and-look, so tap → wait → see is one round trip
203
- rather than three.
293
+ - **Verify-after-tap.** A tap can move the screen without doing what you meant —
294
+ a swipe that animates but does not navigate still reports `changed`. Comparing
295
+ against the expected destination would catch it.
296
+ - **Reduce the input dependency.** idb is the one heavyweight requirement. Its
297
+ simulator input is a reimplementation of the Indigo HID transport rather than a
298
+ public API, so replacing it is real work, not a wrapper — but it is the last
299
+ thing standing between simframe and a zero-install tool.
300
+ - **Extend the confirm vocabulary beyond English.**
204
301
 
205
302
  ## Releasing
206
303
 
207
- `npm version <patch|minor|major>` does not update `server.json`, so bump both,
208
- then push the tag:
304
+ `npm version` does not touch `server.json`, so bump both, then push the tag:
209
305
 
210
306
  ```bash
211
- npm version minor --no-git-tag-version # bumps package.json
212
- $EDITOR server.json # match "version" and packages[0].version
213
- git commit -am "Release v0.2.0" && git tag v0.2.0
214
- git push && git push --tags
307
+ npm version minor --no-git-tag-version
308
+ $EDITOR server.json # match "version" and packages[0].version
309
+ git commit -am "Release vX.Y.Z" && git tag vX.Y.Z && git push && git push --tags
215
310
  ```
216
311
 
217
- The `release` workflow then verifies that the tag, `package.json` and
218
- `server.json` all agree, validates `server.json` against the live registry, and
219
- publishes to npm and to the MCP Registry. It needs an npm automation token in
220
- the `NPM_TOKEN` repository secret; the MCP Registry needs no secret, because it
221
- trusts the workflow's GitHub OIDC identity.
222
-
223
- To publish by hand instead:
224
-
225
- ```bash
226
- npm publish --access public
227
-
228
- curl -fsSL https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_darwin_arm64.tar.gz | tar -xz mcp-publisher
229
- ./mcp-publisher validate
230
- ./mcp-publisher login github
231
- ./mcp-publisher publish
232
- ```
312
+ The `release` workflow verifies tag/`package.json`/`server.json` agree, validates
313
+ `server.json` against the live registry, and publishes to npm and the MCP
314
+ Registry. It needs `NPM_TOKEN`; the registry uses GitHub OIDC and needs no secret.
233
315
 
234
316
  ## License
235
317
 
@@ -0,0 +1,29 @@
1
+ import Foundation
2
+ import Vision
3
+ import AppKit
4
+
5
+ let args = CommandLine.arguments
6
+ guard args.count > 1, let img = NSImage(contentsOfFile: args[1]),
7
+ let cg = img.cgImage(forProposedRect: nil, context: nil, hints: nil) else {
8
+ FileHandle.standardError.write("cannot read image\n".data(using: .utf8)!); exit(1)
9
+ }
10
+ let req = VNRecognizeTextRequest()
11
+ req.recognitionLevel = .accurate
12
+ req.usesLanguageCorrection = false
13
+ try! VNImageRequestHandler(cgImage: cg, options: [:]).perform([req])
14
+ let w = Double(cg.width), h = Double(cg.height)
15
+ var out: [[String: Any]] = []
16
+ for obs in (req.results ?? []) {
17
+ guard let top = obs.topCandidates(1).first else { continue }
18
+ let b = obs.boundingBox // normalized, origin bottom-left
19
+ out.append([
20
+ "text": top.string,
21
+ "confidence": top.confidence,
22
+ "x": b.origin.x * w,
23
+ "y": (1 - b.origin.y - b.size.height) * h,
24
+ "width": b.size.width * w,
25
+ "height": b.size.height * h,
26
+ ])
27
+ }
28
+ let data = try! JSONSerialization.data(withJSONObject: out)
29
+ FileHandle.standardOutput.write(data)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "simframe",
3
- "version": "0.1.0",
3
+ "version": "0.4.1",
4
4
  "mcpName": "io.github.lvlrSajjad/simframe",
5
5
  "description": "Always-warm iOS Simulator frames: agents read the screen in ~20ms instead of waiting on screenshots. MCP server + CLI.",
6
6
  "keywords": [
@@ -40,6 +40,7 @@
40
40
  },
41
41
  "files": [
42
42
  "src",
43
+ "native",
43
44
  "README.md",
44
45
  "LICENSE"
45
46
  ],