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 +237 -155
- package/native/ocr.swift +29 -0
- package/package.json +2 -1
- package/src/actions.js +244 -0
- package/src/analyze.js +59 -0
- package/src/cli.js +198 -15
- package/src/daemon.js +139 -9
- package/src/index.js +382 -15
- package/src/input.js +221 -0
- package/src/intent.js +100 -0
- package/src/mcp.js +284 -22
- package/src/ocr.js +71 -0
- package/src/screenmap.js +229 -0
- package/src/simctl.js +26 -1
package/README.md
CHANGED
|
@@ -4,59 +4,41 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/simframe)
|
|
5
5
|
[](./LICENSE)
|
|
6
6
|
|
|
7
|
-
**
|
|
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
|
|
12
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
25
|
-
|
|
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
|
-
##
|
|
27
|
+
## What changed, measured
|
|
31
28
|
|
|
32
|
-
|
|
33
|
-
and how many tokens each one costs.
|
|
29
|
+
Same four-tab navigation flow, on a real production app:
|
|
34
30
|
|
|
35
|
-
|
|
|
31
|
+
| | Before | With simframe |
|
|
36
32
|
| --- | --- | --- |
|
|
37
|
-
|
|
|
38
|
-
| "
|
|
39
|
-
|
|
|
40
|
-
|
|
|
41
|
-
|
|
|
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
|
-
|
|
49
|
-
|
|
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`
|
|
69
|
-
|
|
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
|
-
|
|
91
|
-
booted simulator, and stops itself 15 minutes after the last request.
|
|
80
|
+
## Capabilities are independent
|
|
92
81
|
|
|
93
|
-
|
|
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
|
-
|
|
|
96
|
-
| --- | --- |
|
|
97
|
-
|
|
|
98
|
-
|
|
|
99
|
-
|
|
|
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
|
-
|
|
105
|
-
|
|
91
|
+
```bash
|
|
92
|
+
# input, optional
|
|
93
|
+
brew tap facebook/fb && brew install idb-companion && pipx install fb-idb
|
|
94
|
+
```
|
|
106
95
|
|
|
107
|
-
|
|
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
|
|
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
|
|
113
|
-
|
|
114
|
-
simframe
|
|
115
|
-
simframe
|
|
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
|
-
|
|
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
|
-
|
|
128
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
|
172
|
-
|
|
|
173
|
-
|
|
|
174
|
-
|
|
|
175
|
-
|
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
|
191
|
-
-
|
|
192
|
-
|
|
193
|
-
-
|
|
194
|
-
|
|
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
|
-
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
|
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
|
|
212
|
-
$EDITOR server.json
|
|
213
|
-
git commit -am "Release
|
|
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
|
|
218
|
-
`server.json`
|
|
219
|
-
|
|
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
|
|
package/native/ocr.swift
ADDED
|
@@ -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
|
|
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
|
],
|