Skip to content

Commit aec278e

Browse files
committed
Sync public bindings to v8.1.0
Bring the public release up to date with simulang-js 8.1.0 based on the upstream simulang-rs API, adding accessibility-tree navigation, window-scoped capture/grounding, multi-monitor support, and model auth checks, plus supporting tooling. Added: - AccessibilityNode.parent()/ancestors()/lowestCommonAncestor() and ancestry checks for navigating accessibility-tree relationships - AccessibilityNode.fromPoint(x, y) and AccessibilityTree.findByDescription() - AccessibilityNode.url for a node's raw hyperlink target - Window.screenshot()/ground() for window-scoped capture and grounding - Window frame-relative input, focus(), screen(), and boundingBox() - Screen.all()/fromWindow()/boundingBox() for multi-monitor support - Instance.windows()/close()/kill()/isRunning() - Image.drawDot()/Screenshot.drawDot() for annotating results - AskModel LLM bindings and GroundingModel/AskModel/SttModel checkAuth() - CHANGELOG.md, changelog-stamping release script, and example scripts Changed: - Frame-relative window input now throws on off-display targets instead of silently clamping to a screen edge - Corrected the documented coordinate space to OS-native units (logical points on macOS, physical pixels on Windows/Linux) - Clearer per-provider diagnostics when no model credentials are available Breaking: - Renamed Image/Screenshot.addGrid() -> drawGrid() - Renamed Screenshot.toGlobalPhysicalPixels() -> toGlobalDesktopCoordinates() - Removed Screen.dimensions(); use Screen.boundingBox()
1 parent 44db666 commit aec278e

30 files changed

Lines changed: 2415 additions & 670 deletions

.cursor/rules/changelog.mdc

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
description: >-
3+
Update CHANGELOG.md when a commit changes public API or user-visible library
4+
behavior. Required before finishing work that touches the JS surface.
5+
alwaysApply: true
6+
---
7+
8+
# Changelog maintenance
9+
10+
`CHANGELOG.md` is the release notes source for `@simular-ai/simulang-js` and the input that drives the [semantic versioning](https://semver.org/) bump on release (breaking → major, new API/behavior → minor, fixes only → patch). Keep it current whenever you change what consumers see.
11+
12+
## Update the changelog when
13+
14+
- Public exports change in `index.d.ts`, `wrapped.d.ts`, or `wrapped.js` (new, removed, or renamed symbols)
15+
- Method or function signatures, return types, or thrown errors change
16+
- Runtime semantics change (defaults, coordinate system, sync vs async, resource lifetime, platform quirks documented in JSDoc)
17+
- Breaking changes — mark clearly under **Changed** or **Removed** and call out migration steps
18+
19+
Add bullets under **`## [Unreleased]`** using [Keep a Changelog](https://keepachangelog.com/) sections: **Added**, **Changed**, **Deprecated**, **Removed**, **Fixed**, **Security**.
20+
21+
## Skip the changelog when
22+
23+
- Changes are internal to Rust with no JS/API surface difference
24+
- CI, tooling, or dependency-only bumps with no behavior change
25+
- Tests, examples, or comments that only reflect existing behavior
26+
27+
## On release
28+
29+
The `version` lifecycle script (`scripts/stamp-changelog.mjs`) renames `[Unreleased]` to the bumped version and date automatically — don't edit those headings by hand. Just make sure the `[Unreleased]` body accurately reflects what's about to ship before running `npm version`. See `PUBLISH.md`.
30+
31+
## Style
32+
33+
- Write for library consumers, not implementers — describe outcomes ("`Instance.isRunning()` polls process state") not file names
34+
- One concise bullet per user-visible change; group related API under one bullet when it ships together

.github/workflows/CI.yml

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ jobs:
2929
timeout-minutes: 5
3030
runs-on: ubuntu-latest
3131
steps:
32-
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
32+
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
3333
env:
3434
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/.gitconfig-blank # Create a blank gitconfig file in the temp directory. Ensures local gitconfig is not used.
3535
- name: Fetch github.com SSH host keys
@@ -41,7 +41,7 @@ jobs:
4141
ssh-keyscan github.com
4242
echo 'EOF'
4343
} >> "$GITHUB_OUTPUT"
44-
- uses: EmbarkStudios/cargo-deny-action@91bf2b620e09e18d6eb78b92e7861937469acedb # v2.0.17
44+
- uses: EmbarkStudios/cargo-deny-action@bb137d7af7e4fb67e5f82a49c4fce4fad40782fe # v2.0.20
4545
with:
4646
ssh-key: ${{ secrets.SIMULANG_RS_SSH_KEY }}
4747
ssh-known-hosts: ${{ steps.keyscan.outputs.known-hosts }}
@@ -99,7 +99,7 @@ jobs:
9999

100100
steps:
101101
# Setup
102-
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
102+
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
103103
env:
104104
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/.gitconfig-blank # Create a blank gitconfig file in the temp directory. Ensures local gitconfig is not used.
105105

@@ -141,7 +141,7 @@ jobs:
141141
target/
142142
key: ${{ matrix.settings.target }}-cargo-${{ matrix.settings.host }}
143143
- name: Install
144-
uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 # master on 2026-03-27
144+
uses: dtolnay/rust-toolchain@67ef31d5b988238dd797d409d6f9574278e20537 # master on 2026-03-27
145145
with:
146146
toolchain: stable
147147
components: clippy, rustfmt
@@ -248,16 +248,23 @@ jobs:
248248
timeout-minutes: 20
249249
permissions:
250250
contents: write
251+
packages: write # required to mirror to GitHub Packages
252+
id-token: write # required for npm trusted publishing (OIDC) to npmjs.org
251253
needs:
252254
- build
253255
if: startsWith(github.ref, 'refs/tags/')
254256
steps:
255-
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
257+
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
256258
env:
257259
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/.gitconfig-blank # Create a blank gitconfig file in the temp directory. Ensures local gitconfig is not used.
258260
- name: Setup node
259261
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
260262
with:
263+
# Intentionally no `registry-url`: setup-node would otherwise inject
264+
# NODE_AUTH_TOKEN handling into ~/.npmrc, which conflicts with the
265+
# OIDC-based trusted publish to npmjs.org and causes 404s. The
266+
# GitHub Packages publish writes its own auth line and the npmjs
267+
# publish wipes ~/.npmrc beforehand (see "Publish" step below).
261268
node-version: 24
262269
cache: npm
263270
- name: Install dependencies
@@ -275,7 +282,8 @@ jobs:
275282
shell: bash
276283
- name: Publish
277284
env:
278-
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
285+
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # used for the GitHub Packages publish below
286+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # napi prepublish reads this when creating the GitHub release
279287
run: |
280288
set -euo pipefail
281289
@@ -290,12 +298,22 @@ jobs:
290298
exit 0
291299
fi
292300
293-
# Write the npmjs auth token to ~/.npmrc so `npm publish` (and napi's
294-
# child publishes for per-platform packages) pick it up.
295-
echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc
301+
# 1. Publish to GitHub Packages first. It's org-private, so a
302+
# broken release here is easy to delete and retry, whereas an
303+
# npmjs publish is immutable (you can only deprecate, not
304+
# delete, after 72h).
305+
echo "//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}" > ~/.npmrc
306+
npm publish --registry=https://npm.pkg.github.com/ --access restricted --tag "$DIST_TAG"
296307
308+
# 2. Publish to npmjs.org via OIDC trusted publishing.
309+
# The npm CLI picks up the GitHub Actions OIDC token automatically
310+
# when `id-token: write` is granted to the job (see permissions
311+
# block above) and the package is configured for trusted
312+
# publishing on npmjs.org. Wipe ~/.npmrc first so the GitHub
313+
# Packages auth line from step 1 doesn't shadow the OIDC token.
314+
rm -f ~/.npmrc
297315
NAPI_PREPUBLISH_FLAGS="--no-gh-release" \
298-
npm publish --access public --tag "$DIST_TAG"
316+
npm publish --registry=https://registry.npmjs.org/ --access public --tag "$DIST_TAG"
299317
300318
# Mintlify: generate MDX docs from the just-published index.d.ts and
301319
# open a PR on simular-ai/docs that archives them under
@@ -304,7 +322,7 @@ jobs:
304322
run: npm run docs:mintlify
305323

306324
- name: Check out simular-ai/docs
307-
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
325+
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
308326
with:
309327
repository: simular-ai/docs
310328
token: ${{ secrets.DOCS_REPO_TOKEN }}

CHANGELOG.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# Changelog
2+
3+
All notable changes to `@simular-ai/simulang-js` are documented in this file.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7+
8+
## [Unreleased]
9+
10+
## [8.1.0] - 2026-06-23
11+
12+
### Added
13+
14+
- `AccessibilityNode.parent()`, `ancestors()`, direct/strict ancestry checks, and `lowestCommonAncestor(other)` for navigating accessibility-tree relationships. `parent()` returns `null` when a node has no parent, `ancestors()` stops only when the parent chain is fully resolved, and both throw on lookup failure; `lowestCommonAncestor` returns a `[node, level]` tuple where `level` is from the reached parentless node, returns `null` only for resolved unrelated trees, and throws if either parent chain cannot be resolved.
15+
- `Window.screenshot(hideCursor)` — captures just this window's pixels from its own backing store (macOS `ScreenCaptureKit` window filter / Windows `Windows.Graphics.Capture`), so occluding windows don't bleed through and hardware-accelerated content (Chrome, Electron, D3D apps) is captured correctly; off-display or minimized windows throw.
16+
- `Window.ground(model, concept)` — locate a concept within this window and return its global desktop coordinates (sugar for `screenshot(true).ground(model, concept)`); restricting the search to the window's bounds is faster and more accurate than grounding a full-screen screenshot.
17+
- `AccessibilityNode.url` — the node's raw hyperlink target as a `string`, or `null` when the node isn't a link, has no target, or the platform doesn't expose it.
18+
- `Image.drawDot(x, y, radius, red, green, blue)` and `Screenshot.drawDot(...)` — paint a filled opaque RGB disc at an image-pixel coordinate (companion to `drawGrid`; `radius` 0 paints a single pixel, out-of-bounds points are no-ops). Handy for annotating grounding results.
19+
20+
### Changed
21+
22+
- Frame-relative input on a `Window` (`moveMouse`, `click`) now throws when the target point maps off every connected display, instead of letting the OS silently clamp the cursor to a screen edge and click the wrong location.
23+
24+
## [8.0.0] - 2026-06-05
25+
26+
### Added
27+
28+
- `AccessibilityNode.fromPoint(x, y)` — element under a screen coordinate via the platform hit-test (UIA `ElementFromPoint` on Windows, `AXUIElementCopyElementAtPosition` on macOS, recursive AT-SPI `GetAccessibleAtPoint` on Linux). Coordinates are global desktop coordinates in OS-native units (physical pixels on Windows/Linux, logical points on macOS), the same space as `boundingBox()` and `MouseController`; returns an uncached, one-shot handle for reading properties such as `.boundingBox()` / `.overallDescription`, or `null` when the point has no accessible element (empty desktop, gaps, or — on Linux — outside the focused app). Throws only on a genuine backend failure.
29+
- `AccessibilityTree.findByDescription(description)` — every node whose `overallDescription` exactly equals `description` (pre-order depth-first)
30+
- `GroundingModel.checkAuth()`, `AskModel.checkAuth()`, and `SttModel.checkAuth()` — probe the provider's auth-check endpoint to validate credentials before launching any UI automation. Throws (and logs a warning) on rejection or transport failure; no-op for providers without an auth-check endpoint. Idiom: `try { model.checkAuth() } catch { process.exit(1) }`.
31+
32+
### Changed
33+
34+
- `GroundingModel.default()` / `AskModel.default()` / `SttModel.default()` now produce a per-candidate diagnostic when no provider has working credentials, naming each provider and explaining exactly why its credentials were unavailable (env var unset, env var empty, credentials file missing, …). The previous error misleadingly said "no provider advertises a VLM service" even when the provider was correctly configured but its API-key env var was unset.
35+
- Corrected the documented coordinate space across the API (`MouseController`, `Window.boundingBox()`, `AccessibilityNode.boundingBox()` / `fromPoint()`, `Screen.boundingBox()`, screenshots, and grounding output). Coordinates are in **OS-native units** — physical pixels on Windows/Linux and **logical points on macOS** (where one point spans two hardware pixels on a 2× display) — not uniformly "physical pixels" as previously stated. Behavior is unchanged; coordinates still round-trip between these APIs without conversion on a given OS.
36+
37+
### Breaking
38+
39+
- `Image.addGrid()` and `Screenshot.addGrid()` are renamed to `drawGrid()` to match the underlying `simulang-rs` API. Same signature and behavior; update call sites from `.addGrid(w, h)` to `.drawGrid(w, h)`.
40+
- `Screenshot.toGlobalPhysicalPixels()` is renamed to `Screenshot.toGlobalDesktopCoordinates()`. The old name implied physical pixels, but the result is in the canonical global-desktop space (OS-native units — logical points on macOS, physical pixels on Windows/Linux). Same signature and behavior; update call sites from `.toGlobalPhysicalPixels(...)` to `.toGlobalDesktopCoordinates(...)`.
41+
42+
## [7.0.1] - 2026-05-22
43+
44+
## [7.0.0] - 2026-05-22
45+
46+
### Added
47+
48+
- `Instance.windows()` — enumerate visible top-level windows belonging to a specific opened application (companion to the global `Window.all()` / `Window.allForPid()` enumerators).
49+
- `Window.boundingBox()` — live bounding box of a window in global physical pixels (`right` / `bottom` exclusive, Playwright / DOM convention).
50+
- `Screen.boundingBox()` — live bounding box of a screen in global physical pixels (matches `Window.boundingBox()` shape; secondary monitors may report negative `left` / `top`).
51+
- `Instance.close()`, `Instance.kill()`, and `Instance.isRunning()` — request graceful exit, force-terminate, or poll whether the underlying process is still running.
52+
- `Screen.all()`, `Screen.fromWindow(window)`, and `Window.screen()` — enumerate connected displays and find the screen a specific window lives on (multi-monitor support). Matches the OS-native algorithm (`NSWindow.screen` on macOS, `MonitorFromWindow` on Windows); throws when the window has no measurable overlap with any display.
53+
- `Window.focus()` — bring a specific window to the foreground.
54+
- `Window.moveMouse()`, `Window.button()`, `Window.click()`, `Window.scroll()` — frame-relative mouse input on a window. Coordinates are relative to the window's `boundingBox()` top-left (i.e. include the title bar / chrome); the caller is responsible for focusing the window first if needed.
55+
56+
### Changed
57+
58+
- Clarified that mouse coordinates in `MouseController` are in the **global virtual desktop** (top-left of the primary monitor at `(0, 0)`, physical pixels, may be **negative** on secondary monitors arranged to the left of / above the primary). The previous "current screen" wording was wrong on multi-monitor setups.
59+
60+
### Breaking
61+
62+
- `Screen.dimensions()` is removed; use `Screen.boundingBox()` and read its `right - left` / `bottom - top` (or the `left` / `top` origin). The previous `[x, y, width, height]` tuple shape was redundant with `Window.boundingBox()` and the accessibility-tree `BoundingBox`, and a single canonical shape is friendlier for AI agents driving the API.
63+
64+
## [6.0.1] - 2026-05-16
65+
66+
### Added
67+
68+
- `AskModel` (`AskModel.default()`, `AskModel.byAlias()`, `AskModel.availableAliases()`, `ask()`) — JS bindings for the `ask` LLM primitive (OpenAI-compatible chat completions, optional vision via `Image` attachments).
69+
70+
## [6.0.0] - 2026-05-15
71+
72+
### Added
73+
74+
- `setPauseHook(fn | null)` on the JS wrapper — synchronous hook invoked before every exported call; used by `@simular-ai/simulang-log-viewer` for pause/grab behavior without wrapping each API by hand.
75+
- Default stderr logging on `import '@simular-ai/simulang-js'``initLogger` auto-installs unless overridden; filter follows `RUST_LOG` or `simulang_rs=info,warn`.

CLAUDE.md

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@
22

33
Node.js bindings for the Rust `simulang-rs` crate (via napi-rs). Cross-platform
44
desktop automation: apps, windows, accessibility trees, mouse/keyboard,
5-
screenshots, clipboard, audio, and VLM/STT model access.
5+
screenshots, clipboard, audio, and VLM/LLM/STT model access.
66

77
This file ships in the npm tarball alongside `index.d.ts` and is versioned
88
with it.
99

1010
## Where the API is documented
1111

1212
Read **`index.d.ts`** first — it is the source of truth. Every class,
13-
function, and enum is fully typed (~1500 lines) and carries JSDoc covering
13+
function, and enum is fully typed (~2100 lines) and carries JSDoc covering
1414
idioms, lifecycle rules, platform quirks, and inter-API trade-offs that types
1515
alone can't express. The JSDoc is generated from doc comments,
1616
so the per-symbol guidance there is authoritative — trust it over any
@@ -24,12 +24,22 @@ restatement elsewhere.
2424
- Many objects are **handles to platform resources** (windows, audio devices,
2525
accessibility trees, file/directory handles). Their lifetime matters;
2626
dropping them can free the underlying resource.
27-
- Coordinates are uniform across platforms: **top-left origin, global
28-
physical pixels** (also called device pixels — the raw hardware pixels of
29-
the display, not the logical / CSS / point units used in browsers and
30-
some desktop UI frameworks). On a 2× HiDPI display, a 1920×1080-logical
31-
screen is 3840×2160 in these coordinates. Image and screenshot dimensions
32-
are likewise in physical pixels.
27+
- Coordinates live on the **global desktop**: top-left origin at `(0, 0)` on
28+
the primary monitor, in **OS-native units** — the unit is **not** the same
29+
on every platform:
30+
- **Windows / Linux**: **physical pixels** (raw hardware pixels).
31+
- **macOS**: **logical points** — on a 2× Retina display one point spans two
32+
hardware pixels, so a 1920×1080-logical screen is `1920×1080` here, not
33+
`3840×2160`.
34+
35+
These are the native units the OS input/accessibility APIs expect, **not**
36+
the browser logical/CSS pixel. Within a single OS every API speaks that OS's
37+
unit, so coordinates round-trip between `MouseController`, the various
38+
`boundingBox()` methods, `Screenshot.toGlobalDesktopCoordinates()`, and
39+
grounding output **without conversion**; only code crossing into a different
40+
coordinate system (e.g. an Electron overlay measured in CSS pixels) must
41+
account for the per-OS unit. Monitors arranged to the left of / above the
42+
primary contribute **negative** coordinates, so don't assume `x, y >= 0`.
3343

3444
## Logging is on by default
3545

0 commit comments

Comments
 (0)