An independent, headless Mini Program simulator for macOS 26+ arm64. It exposes a
compiled Mini Program dist to AI clients over MCP stdio without linking to the
MiniSimulator app or its Swift runtime client. Both a dist/app.json layout and
the common dist/miniprogram/app.json layout are accepted.
V1 is Skyline-first while supporting both providers:
mini_headless_session_open defaults renderer to "skyline"; pass
"webview" explicitly for PageFrame WebView. V1 intentionally exposes only:
- persistent session open, close, state, and cross-MCP warm reattachment;
- one-call bounded page observations spanning screenshot, document, Console, and Network domains;
- source-informed event waits for visible text, exact runtime network callsites, document revisions, and provider frames;
- PNG screenshots of the active provider surface, with pixel dimensions, viewport mapping scales, presentation readiness metadata, and retained MCP artifact resources;
- coordinate and document-scoped node-ref tap, pointer, scroll, swipe, text, and key input;
navigateTo,redirectTo,reLaunch,switchTab, andnavigateBack(delta);- semantic DOM tree and child-node reads;
- coordinate hit testing and node box models;
- inline, matched, and computed styles;
- component data;
- bounded, filter-before-limit Console and Network streams with event waits and per-request Network drill-down.
Elements uses the official Mini Program WARemoteDebug protocol for both
providers. WebView is not modeled as raw Chromium CDP, and Console/Network are
the AppService business streams. Arbitrary evaluate, mock, state mutation,
performance, and security tools are outside V1.
The product is delivered as two artifacts:
- a small Rslib JavaScript wrapper containing the MCP adapter, persistent daemon, user commands, and documentation;
- a separate immutable Runtime Bundle containing Electron, native code, the complete Runtime Pack, and the complete built SessionWorker.
Maintainers assemble that Runtime Bundle from the MiniSimulator donor repository's
Vendor/Electron.app, Vendor/RuntimePack, current built SessionWorker and
RuntimeHost, and freshly built Mini Headless native binaries. End
users install only a Mini Headless plugin. They do not install, launch, or
depend on MiniSimulator.app.
Each plugin contains the same prebuilt Rslib wrapper and a release descriptor that pins one Runtime Bundle URL, byte length, opaque bundle ID, and explicit Runtime/ProjectOwner contract versions. The MCP stdio server becomes ready first, then downloads a missing Runtime Bundle in the background and atomically installs it under:
~/.mini-headless/runtimes/v8-<runtime-bundle-id>/runtime
Concurrent Codex, Claude Code, Cursor, and CodeBuddy adapters share the same
install lock, selected Runtime Bundle, persistent daemon, and retained
sessions. There is no required install, setup, credential import, or doctor
command. setup <runtime-directory> remains an explicit maintainer override
for a local bundle.
The 0.1.0-alpha.1 prerelease pins its immutable Runtime Bundle in
runtime/release.json. Installation downloads that bundle on first use; it
does not hash or revalidate Mini Headless-owned files.
Install the prerelease directly from the public repository marketplace:
codex plugin marketplace add Nightwalk2001/MiniHeadless
codex plugin add mini-headless@nightwalk
claude plugin marketplace add Nightwalk2001/MiniHeadless
claude plugin install mini-headless@nightwalk
codebuddy plugin marketplace add Nightwalk2001/MiniHeadless
codebuddy plugin install mini-headless@nightwalkCursor users install the same repository through the Cursor Marketplace, or
load a checkout with bun run dev:cursor into ~/.cursor/plugins/local. Every
host starts the bundled wrapper automatically; none requires a separate
mcp add, Runtime setup, credential import, or doctor step.
See docs/usage.md for client configuration, the first
session_open, observation revisions, warm-session behavior, and maintainer
release ordering.
- Read the Mini Program source, then call
mini_headless_session_openwith an absolute compileddistpath and optional absolutesourceRoot. Keep itssessionIdplusobservation.navigationRevision. - Use
mini_headless_page_observeas the normal read path. It can return a visually settled screenshot, DOM tree, filtered Console entries, and filtered Network entries together. Its observation window states whether navigation remained stable across those domains. - Use
mini_headless_page_waitfor the source-derived visible text or exact Networkinitiator.callsiteIdthat proves the intended state. Use the specialized screenshot, Elements, Console, Network, andnetwork_gettools only for drill-down. Console and Network waits are event-driven; do not poll with fixed sleeps. - Act with
mini_headless_inputormini_headless_navigate, passing the latestexpectedNavigationRevision. Both return a fresh observation; input also reports the observed navigation, document, frame, and visual-settlement effect. - Call
mini_headless_session_stateonly for diagnostics or recovery, not between normal operations. - Call
mini_headless_session_closeto dispose the worker when finished. To reuse it instead, leave it open and disconnect the MCP adapter; the daemon detaches it and retains the warm session for five minutes.
STALE_TARGET on a mutation means its expectedNavigationRevision no longer
names the active page. Observe again before deciding what to do. Read-only
screenshot and Elements calls safely rebase once onto the active document;
input is never automatically replayed. Navigation is never replayed after a
renderer transition starts; the daemon may repeat one failed preflight only
when the worker proves that it restored the original route before dispatch.
mini_headless_screenshot returns width and height in PNG pixels, the
logical viewport, inputScaleX, and inputScaleY, plus stability and
readiness (first provider frame, visual quietness, timeout, and animation state).
Screenshots never guess business readiness from URLs. The PNG is available as a
mini-headless://artifact/... resource for 30 minutes independently of session
lifetime and is inlined only when inlineImage is true. Input actions and Elements
nodeAt use viewport logical coordinates, not PNG pixel coordinates.
Convert a point selected from the image before using it:
logicalX = imageX * inputScaleX
logicalY = imageY * inputScaleY
Both coordinate spaces use a top-left origin.
AI client
└─ short-lived Rslib MCP stdio adapter
└─ versioned private current-user Unix socket, framed MSB1
└─ persistent TypeScript daemon
├─ one napi-rs ProjectOwner actor per canonical project
└─ one warm session
├─ hidden Electron SessionWorker
│ ├─ commands: stdin, direct MSB1
│ ├─ events: fd 3, direct MSB1
│ ├─ official AppService + Skyline/PageFrame
│ └─ input + navigation + inspector streams
└─ Skyline/WebView native frame capture
The stdio adapter may come and go without terminating the daemon. Identical
session specifications reattach to a warm session; a disconnected session is
retained for five minutes, subject to a four-session LRU bound. Explicit close
disposes the Electron worker. Mini Headless does not watch compiled dist for
live rebuilds; a new session is required after the consumer regenerates output.
The daemon owns ProjectOwner actors until daemon shutdown so catalog, compiler,
and CAS state remain warm across MCP connections.
The adapter-to-daemon boundary is a versioned private UDS protocol with request/response correlation. The daemon does not wrap the real SessionWorker in a second per-operation RPC. It sends the worker's native command envelopes directly over stdin and consumes its native lifecycle, frame, inspector, Console, and Network events directly from fd 3; both streams use framed MSB1 MessagePack. The current private daemon protocol version is V5.
TypeScript owns MCP schemas, the persistent daemon, session lifetimes, Electron
workers, semantic inspector routing, Console, and Network. Rust owns the project
catalog, dependency graph, compilation scheduling, artifacts, CAS, and
long-lived project state. Node calls Rust in-process through napi-rs; there is
no Rust child-process or MSB1 compatibility boundary. C++ owns only the narrow
runtime-native seams: the MiniSimulator NodeArena MNS1 decoder, stable Skyline
shared-memory snapshots, the WebView IOSurface sender, and the daemon-side
WebView IOSurface receiver/PNG capture.
The MiniSimulator app and this package intentionally evolve independently. Parent-repository sources are donor evidence for the Runtime Pack, never runtime imports.
Requires Node.js 20 or newer, Bun, the Rust toolchain, the sibling
../MiniSimulator runtime-core checkout, and the sibling ../minicrab
checkout for the general compiler and minicrab-common.
bun install
bun run check
bun run build:wrapper
bun run build:nativeTo exercise the current checkout as a real Codex plugin, use the complete local development flow:
bun run devIt builds ProjectOwner against the current Rust ProjectCore, builds the
current MiniSimulator SessionWorker, RuntimeHost, RuntimeResourceView and native
adapters, assembles Runtime Bundle V8 under .codex-dev/runtime, creates a
cache-busted local marketplace under .codex-dev, and installs
mini-headless@mini-headless-dev. Start a new Codex task after the command
finishes so the newly installed MCP tools are discovered.
To load the same checkout as a Cursor local plugin after that Runtime Bundle exists:
bun run dev:cursorIt rebuilds the wrapper, writes .cursor-dev/plugins/mini-headless with
dist and skills symlinked for live wrapper iteration, and installs that
folder at ~/.cursor/plugins/local/mini-headless. Reload the Cursor window so
the MCP tools are discovered. Re-run bun run dev:cursor-prepare after
wrapper changes if the plugin snapshot is stale; dist is a symlink, so a
wrapper rebuild plus MCP reconnect is usually enough. Re-run bun run dev:runtime after native or SessionWorker changes.
Rslib emits dist/main.js and dist/daemon.js. tsgo is the TypeScript checker
and Biome owns formatting and linting. TypeScript uses double quotes, omits
semicolons, avoids unnecessary blocks, and prefers type over interface.
build:native uses the NAPI-RS v3 CLI for
runtime/native/project-owner.darwin-arm64.node, then CMake and Node-API for
runtime/native/inspector-node-store.node,
runtime/native/skyline-snapshot.node,
runtime/native/webview-snapshot.node, and
runtime/native/webview-surface.node. Mini Headless owns the
project-owner-napi adapter; the shared Rust engine follows the sibling
minicrab Rust 2024, rustfmt, workspace-lint, and optimized-release policies.
Provenance is recorded in
docs/rust-provenance.md.
After build:native, assemble a complete Runtime Bundle from reviewed absolute
inputs:
bun run prepare:runtime \
--electron-app /absolute/path/to/MiniSimulator/Vendor/Electron.app \
--runtime-pack /absolute/path/to/MiniSimulator/Vendor/RuntimePack \
--session-worker /absolute/path/to/built/SessionWorker \
--native-directory /absolute/path/to/MiniHeadless/runtime/native \
--output /absolute/path/to/new/runtimeThe same command prepares development and release bundles from already built
inputs. It copies the current Vendor/runtime closure, writes the V8 manifest
with an opaque build ID and explicit contract versions, and validates that the
assembled closure is structurally complete.
bun run dev:runtime performs the current-checkout build and assembly in one
command. The full ordering is documented in
docs/usage.md.
The plugin does not embed the large runtime and never falls back to
MiniSimulator.app or an installed WeChat DevTools application. Its pinned
release descriptor lets the wrapper download this separately published Runtime
Bundle on first use:
runtime/
manifest.json
electron/MiniHeadlessRuntime.app/
Native/project-owner.darwin-arm64.node
Native/inspector-node-store.node
Native/runtime-resource-view.node
Native/skyline-snapshot.node
Native/webview-snapshot.node
Native/webview-surface.node
pack/ # complete Runtime Pack
SessionWorker/ # complete built SessionWorker
main.js
The Runtime Bundle manifest uses schemaVersion: 8. ProjectOwner produces
the current LaunchSnapshotRef, compile scopes, and ArtifactSlice DTOs
consumed by the synchronously shipped SessionWorker contract.
runtime/manifest.example.json documents that contract, but is not a runnable
release manifest. prepare:runtime copies the Electron app and Runtime Pack
including base libraries from the current Vendor tree, adds the built native
binaries and prepared worker closure, and produces the complete input required
by session_open.
The manifest carries an opaque runtimeBundleID plus explicit Runtime and
ProjectOwner contract versions. Mini Headless does not hash or revalidate its
own files. Downloads are bounded by the release descriptor's byte length;
archive paths and the extracted structural closure are validated before atomic
publication. An incomplete or manifest-invalid bundle fails before any child
process starts.
The checked-in MiniSimulator Vendor/RuntimePack is the build donor, not a
complete V8 Runtime Bundle by itself. Its files become publishable only as part of the
self-contained closure produced by prepare:runtime.
Source and static builds do not prove that either provider can boot. Real
Skyline and WebView evidence still requires a complete distributable Runtime
Bundle and a compiled consumer dist.
The adapter version, daemon protocol version, runtime root, opaque bundle ID, and explicit contract versions select and verify the private daemon namespace.
For a source checkout, configure an MCP client with only the absolute wrapper entry:
{
"mcpServers": {
"mini-headless": {
"command": "node",
"args": ["/absolute/path/to/MiniHeadless/dist/main.js"]
}
}
}Released plugins generate this configuration themselves. The installed
selection is the no-environment default.
MINI_HEADLESS_RUNTIME_ROOT is an explicit development override, not
normal consumer setup.
The adapter writes MCP protocol data only to stdout. Daemon and child-process diagnostics go to stderr or the Mini Headless diagnostics directory.
When no explicit credential path is configured, every ticket refresh first imports the current MiniSimulator login from:
~/Library/Application Support/nightwalk.tencent.MiniSimulator/DevToolsAuth/login.json
If that login is unavailable or expired, the runtime tries its last Mini Headless-owned copy and then scans:
~/Library/Application Support/微信开发者工具
selects the newest unexpired login, and copies it to:
~/.mini-headless/auth/login.json
The user normally only needs to be logged in to MiniSimulator. WeChat DevTools
discovery remains the final fallback. If no usable login is present,
session_open asks the user to sign in and retry.
MINI_HEADLESS_CREDENTIALS_PATH is an advanced explicit override. When it
is set, the runtime never scans or overwrites it; the file must be a regular,
non-symlink file with mode 0600 and this bounded shape:
{
"openid": "...",
"signature": "...",
"signatureExpiredTime": 1893456000000
}auth import <login-json> and auth import-wechat remain available as manual
account-selection and troubleshooting overrides. Both commands stop the
matching persistent daemon before replacing credentials. doctor is likewise
optional troubleshooting; it is not part of installation or first use.
See docs/architecture.md for ownership, protocols,
identity, inspector, and release gates.