npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ceraph/react-native-mcp

v0.5.2

Published

MCP server for React Native and Expo development workflow

Downloads

4,082

Readme

@ceraph/react-native-mcp

MCP server for React Native and Expo development. Automatic build error capture, console monitoring, reliable screen interactions, and prebuild detection.

Works with any MCP client: Claude Code, Cursor, Codex, Windsurf, and others.

⚠️ Ignore the npm i line above. Run this once instead:

npx @ceraph/react-native-mcp@latest init

Platform support

This MCP is iOS-only. Both a real iOS device (the original supported configuration) and a booted iOS Simulator now work. The runtime path (devicectl / idb / WebDriverAgent / simctl) is Apple-specific.

| | Status | Notes | |---|---|---| | iOS device (USB) | Supported | WebDriverAgent installs through Xcode (one-time); WDA is reached at localhost:8100. Default — no env var needed. | | iOS Simulator | Supported | ceraph_start installs the optional appium-webdriveragent dep and builds + launches the WDA automatically the first time you target a simulator. See Simulator setup below. | | Android (device or emulator) | Not supported | | | Linux / Windows host | Not supported | | | Expo Go | Not supported | Needs dev client or prebuilt app. |

If you don't have a Mac with Xcode, stop here — nothing in this package will work for you yet.

Choosing device vs simulator

By default the MCP auto-detects: if a real device is connected, it uses the device; otherwise it uses a booted simulator. The one exception is when you're already driving a simulator — once rn_wda_start (or ceraph_start's simulator path) has an active WDA session, auto keeps routing to that simulator even if a phone is also plugged in. Override explicitly:

export CERAPH_TARGET=device     # always use real device
export CERAPH_TARGET=simulator  # always use simulator
export CERAPH_TARGET=headless   # always use a simulator with no Simulator.app window
export CERAPH_TARGET=auto       # default behavior (also: unset)

For a one-off override without touching the environment, pass target: "device" | "simulator" | "headless" to ceraph_start ("headless" runs on a simulator with no Simulator.app window — see Headless mode) — it takes precedence over CERAPH_TARGET for that call only, and an explicit device with no device connected fails fast (no silent simulator fallback). The target is resolved per call, so separate calls can drive the device and the simulator independently.

The rn_target_status tool reports which target is currently active and the resolved WDA base URL.

Requirements

  • macOS with Xcode 14+ (Command Line Tools installed)
  • iOS 16+ device connected to the Mac over USB and unlocked OR a booted iOS Simulator
  • Node.js 18+
  • WebDriverAgent running and reachable. On a real device, this is localhost:8100 (port-forwarded from the device by Xcode / iproxy). On a simulator, the port is captured at runtime from rn_wda_start — see Simulator setup.
  • Expo dev client or prebuilt app (Expo Go is not supported)

Multi-developer Macs: The runtime signal listener binds to localhost:8101 (WDA uses 8100, Metro uses 8081). Only one developer on a given Mac can run the MCP at a time without a port collision. If you share a Mac and need parallel sessions, override the port via CERAPH_SIGNAL_PORT when starting the MCP server.

Environment variables

| Variable | Default | Purpose | |---|---|---| | CERAPH_SIGNAL_HOST | auto-detected | Override the Mac host the on-device signal channel posts to, for unusual networks where auto-detection picks the wrong address. Leave it unset for normal use. | | CERAPH_SIGNAL_PORT | 8101 | Port the signal listener binds to on the Mac. Change when 8101 is taken. | | CERAPH_TARGET | auto | Which iOS runtime to drive. device forces the real-device path even if a simulator is booted; simulator forces simctl/sim-WDA even if a device is connected; headless forces a simulator with no Simulator.app window (see Headless mode); auto prefers device when both are available, unless a simulator WDA session is already active (then it stays on that simulator). | | CERAPH_FAST_RELOAD | true | Fast Reload (see Fast Reload). Set to false or 0 to turn it off. |

Simulator setup

On a simulator, the WDA needs the optional appium-webdriveragent package — ceraph_start (and rn_wda_start) install it automatically the first time you target a simulator, then build and launch the WDA for you. The first build is a one-time ~1 minute; later runs reuse the cache at <project>/.ceraph/wda-derived/. It's an optional dep, so device-only setups skip the ~200 MB download.

Boot any iPhone simulator (iOS ≥ 16) and run ceraph_start:

open -a Simulator
# or: xcrun simctl boot <UDID>

xcrun simctl list devices booted shows what's currently up.

Once WDA is up, every screen_* and app_* tool routes to the simulator automatically. Call rn_wda_stop to tear it down (or just exit the MCP — shutdown stops the xcodebuild child cleanly).

Concurrent device + simulator: Once a simulator WDA session is active (rn_wda_start, or ceraph_start targeting a simulator), auto-detection routes to that simulator even while a phone is plugged in — an actively-driven sim outranks a merely-connected device. To drive the phone instead while a sim session is up, set CERAPH_TARGET=device (or pass target: "device"). Before any sim session exists, auto prefers a connected device.

Headless mode

To run on a simulator with no Simulator.app window, pass target: "headless" to ceraph_start (or headless: true to rn_build_ios). It builds with xcodebuild instead of expo run:ios (which always opens the GUI), boots the simulator headlessly (xcrun simctl boot), installs and launches the app, and deep-links dev-client builds to Metro. Everything else — WDA, snapshots, screenshots — works exactly as for a windowed simulator.

To make headless the default for every run — the right choice on a cloud Mac or any machine with no display, where headless is the only viable mode — set it once in the environment:

export CERAPH_TARGET=headless

With that set, a bare ceraph_start (no target) and a bare rn_build_ios (no headless) both run headless. A per-call target / headless still overrides for that one call — target: "device" or headless: false beats the env preference.

Use it for CI, a headless or cloud Mac with no display, background runs, or just to keep the simulator window out of the way. It's also far lighter: for an actively-rendering app a windowed simulator costs roughly 4× the CPU and ~290 MB more RAM than headless (the macOS window server compositing the live frame dominates), so a headless fleet scales much further per Mac.

Notes: macOS + Xcode only. The app still loads its JS from Metro — ceraph_start manages that for you; standalone rn_build_ios with headless ensures Metro is running too. Audio still plays through your Mac (it routes independent of the window).

Quick Setup

Run this from your project root:

npx @ceraph/react-native-mcp@latest init

This automatically:

  • Configures MCP servers for all detected clients (Claude Code, Cursor, Codex, VS Code, Windsurf, Antigravity)
  • Installs a Claude Code hook that injects runtime errors into your conversation automatically
  • Adds .rn-errors.json to your .gitignore
  • Signs you in (browser OAuth) so Pro features (test-user provisioning) can reach the Ceraph server

Uninstalling Ceraph

npx @ceraph/react-native-mcp uninstall

Non-destructive by default: it reverses what init did and reports a per-step status, leaving your camera test images and OAuth token in place so you can re-install without losing state. Only @ceraph/react-native-mcp is removed from package.json.

Flags

| Flag | Effect | | --- | --- | | -y, --yes | Skip confirmation prompts. | | --purge-data | Also delete .ceraph/ (camera images + videos, design snapshots, run artifacts, hooks). Alias: --purge-images. | | --global | Also delete ~/.ceraph/auth.json, signing you out of every Ceraph install on the machine. | | --dry-run | Preview the step list without writing anything. | | --project-dir <path> | Operate against a specific subpackage. Required when a monorepo has more than one React Native app. |

Not auto-reverted

A few mutations are hand-edits the uninstaller can't safely make for you. Each surfaces as a manual step with the exact lines to remove:

  • Bare React Native Info.plist and AndroidManifest.xml scheme entries.
  • Expo dynamic config (app.config.js / app.config.ts) — the file path and scheme value to remove.
  • useEffect that contains installCeraph() alongside your own statements — the file and line to edit.

Automatic auto-lock (optional)

If your consumer app has expo-keep-awake (Expo SDK installs it by default) or react-native-keep-awake, Ceraph activates it in __DEV__ so your iPhone screen stays awake during Ceraph runs. It subscribes to AppState and releases the keep-awake when the app backgrounds, then re-acquires on foreground. Production builds tree-shake the entire path.

To add the optional dep to an Expo app:

npx expo install expo-keep-awake

Fast Reload

Fast Reload reconnects your app to Metro automatically after a disconnect. It runs in __DEV__ only and is ON by default.

When the bundler connection drops — laptop sleep, a Metro restart, a network blip — Fast Reload detects it and reconnects the app on its own the moment Metro is back, including after the phone returns from sleep.

You can confirm it's working from the Metro / device console — right before the auto-reload you'll see:

[ceraph-fast-reload] reconnected after Metro disconnect

Turning it off

Two equivalent ways:

# Environment variable
CERAPH_FAST_RELOAD=false   # or 0
// In your installCeraph call
installCeraph({ fastReload: false });

What it does and doesn't do

Fast Reload reconnects the JS bundle. A native or config change (a new native module, an app.json / Info.plist edit, a Babel/Metro config change) still needs a manual rebuild. A reload is also a fresh mount, so in-memory component state is lost across it, as with any reload.

USB bridge (real-device only)

Real iOS devices reach WebDriverAgent through iproxy 8100 8100 -u <udid> (libimobiledevice). The MCP's doctor auto-spawns this when:

  1. WDA at localhost:8100 is unreachable, AND
  2. A real iOS device is connected (detected via xcrun devicectl)

If iproxy isn't installed, doctor surfaces the remediation: brew install libimobiledevice. The auto-spawned child is killed cleanly on MCP server SIGINT / SIGTERM, so it doesn't hang around between sessions. Already-running iproxy (from Xcode's automatic forwarding, or a prior session) is detected and reused, not re-launched.

Manual Setup

If you prefer to configure manually, follow the instructions for your client below.

Claude Code

Add to .mcp.json in your project root:

{
  "mcpServers": {
    "react-native-mcp": {
      "command": "npx",
      "args": ["-y", "@ceraph/react-native-mcp@latest"]
    }
  }
}

Cursor

Add to .cursor/mcp.json in your project root:

{
  "mcpServers": {
    "react-native-mcp": {
      "command": "npx",
      "args": ["-y", "@ceraph/react-native-mcp@latest"]
    }
  }
}

Codex

Add to .codex/config.toml in your project root:

[mcp_servers.react-native-mcp]
command = "npx"
args = ["-y", "@ceraph/react-native-mcp@latest"]

VS Code / Copilot

Add to .vscode/mcp.json in your project root:

{
  "mcpServers": {
    "react-native-mcp": {
      "command": "npx",
      "args": ["-y", "@ceraph/react-native-mcp@latest"]
    }
  }
}

Windsurf

Add to ~/.codeium/windsurf/mcp_config.json (or Settings → Advanced Settings → Cascade):

{
  "mcpServers": {
    "react-native-mcp": {
      "command": "npx",
      "args": ["-y", "@ceraph/react-native-mcp@latest"]
    }
  }
}

Antigravity

Add to ~/.gemini/antigravity/mcp_config.json (or Manage MCP Servers → View raw config):

{
  "mcpServers": {
    "react-native-mcp": {
      "command": "npx",
      "args": ["-y", "@ceraph/react-native-mcp@latest"]
    }
  }
}

Cline / Roo Code (VS Code extensions)

Open VS Code Settings → Extensions → Cline (or Roo Code) → MCP Servers, then add:

{
  "react-native-mcp": {
    "command": "npx",
    "args": ["-y", "@ceraph/react-native-mcp@latest"]
  }
}

JetBrains IDEs

Settings → Tools → MCP Servers → Add, then enter:

  • Name: react-native-mcp
  • Command: npx
  • Args: -y @ceraph/react-native-mcp@latest

Tools

Build & Runtime

| Tool | Description | |---|---| | ceraph_start | Call this first. Brings a connected iPhone to ready-to-test in one call: Metro, then build/install + foreground the app (keep-awake shim) and start WebDriverAgent concurrently, then verify on-device automation. Runs only what's missing, stops at the first failed gate with its remediation. Drives a connected device by default; pass target: "simulator" or target: "headless" to target a windowed or headless simulator instead. | | ceraph_doctor | Diagnose readiness before driving the app: WDA reachable, device connected + awake, app installed, required env vars present, plus camera-shim setup (unwrapped <CameraView>, missing or orphaned imageKey). Returns structured failing checks with their remediation. ceraph_start runs it automatically as its final verify stage. | | rn_build_ios | Build the app with expo run:ios. Captures Xcode output and returns structured errors (file, line, message, type). Optionally runs prebuild --clean first. Pass headless: true to build with xcodebuild and boot/install/launch on a simulator with no Simulator.app window (Headless mode). | | rn_metro_start | Start Metro dev server. Monitors console output for runtime errors, JS exceptions, and red screens. | | rn_get_errors | Return all captured build and runtime errors without re-running anything. | | rn_get_console | Return recent Metro console output, filtered by log level. | | rn_check_prebuild | Detect if prebuild --clean is needed by diffing package.json, app.json, and Podfile.lock against the last successful build. | | rn_stop | Kill all managed React Native processes. |

Target & Simulator

| Tool | Description | |---|---| | rn_target_status | Report which iOS target (device vs simulator) is currently selected, the resolved WDA base URL, and whether a simulator WDA session is running. | | rn_wda_start | Build + launch WebDriverAgent against a booted iOS simulator (requires the optional appium-webdriveragent dep). Captures the WDA port and routes every subsequent screen_* / app_* tool to the simulator automatically. Idempotent — re-calling returns the existing session. Real-device users don't need this tool. | | rn_wda_stop | Stop the simulator WDA session started by rn_wda_start. Idempotent. Real-device WDA is unaffected. |

Screen Interaction

| Tool | Description | |---|---| | ceraph_snapshot | Structured accessibility-tree snapshot of the current screen — a tree of elements with roles, names, values, on-screen bounds, live state, and a stable ref per element. Deterministic, no LLM. | | screen_tap | Tap at coordinates with automatic pixel ratio correction. Screenshot coordinates are divided by the device pixel ratio (2x/3x) so taps land where you expect. | | screen_tap_and_verify | Find an element by text/label/type, tap its center, and optionally assert a follow-up selector is visible — resolved against one accessibility snapshot in a single W3C action. The reliable way to tap: no coordinate guessing. | | screen_tap_chain | Tap a sequence of elements as one W3C action, all resolved against a single snapshot (zero gestures sent if any query fails). | | screen_type_into_field | Focus an input (by text/label/type) and type into it in one W3C action. Set clearFirst: true to clear the field before typing. | | screen_swipe | Swipe up/down/left/right. Defaults to a 60%-of-axis swipe from screen center. | | screen_scroll_to | Repeatedly swipe until a matching element appears in the accessibility tree (no tap). | | screen_long_press | Long-press a matched element for a configurable duration. | | screen_press_key | Press a hardware/system button: home, volumeUp, volumeDown, lock. | | screen_open_url | Open a URL / deep link (e.g. myapp://product/42) to jump straight to a screen — useful for testing your app's deep-link routes. | | screen_screenshot | Capture a pixel screenshot of the current screen (a viewable image block) — for confirming actual rendered appearance. For structured screen data (elements, roles, bounds, refs to act on) use ceraph_snapshot. | | screen_wait_for | Poll the source tree until an element appears (or disappears). Pass timeoutMs: 0 for a one-shot visible / not-visible check. |

App Lifecycle & Device

| Tool | Description | |---|---| | app_launch | Launch an app by bundle ID. Uses xcrun devicectl (Xcode 15+) on a real device, falls back to idb, then simctl. Terminates any existing instance first. | | app_terminate | Terminate an app by bundle ID. | | app_activate | Bring an app to foreground via WDA without a cold restart. | | app_list_installed | List installed apps on the connected device (cached 30s). | | app_info | Report the foreground app's bundleId, pid, and name. | | rn_reset_state | Reset the running app's persisted state — AsyncStorage.clear plus any registered cleanup hooks — without a rebuild. Requires installCeraph active in the app; dev-only (no-ops in production). | | rn_reload | Reload the app's JS bundle, wait for it to come back, then capture a screenshot. Requires installCeraph active in the app; dev-only. | | device_ensure_awake | Ensure the device is awake and unlocked. Returns structured remediation when locked. | | device_set_orientation | Set portrait or landscape. |

Driving the app

Ceraph gives you the two things that make driving an iOS app by hand reliable:

  • ceraph_snapshot — a structured, deterministic view of what's on screen (elements, roles, names, values, on-screen bounds, and a stable ref per element). Call it to see what's tappable / typeable before you act, and again after each action to observe the new state.
  • The screen_* / app_* primitivesscreen_tap, screen_tap_and_verify, screen_type_into_field, screen_swipe, screen_scroll_to, screen_open_url, screen_screenshot, and the rest. You plan the steps; Ceraph drives WebDriverAgent and reports the outcome.

The loop is: ceraph_start (reach a ready state) → ceraph_snapshot (observe) → primitives (act) → snapshot again. Inspect runtime errors with rn_get_errors and console output with rn_get_console between steps — the in-app shim posts JS exceptions, unhandled rejections, network failures, and console.error to Ceraph automatically once installCeraph() is active.

Test users

For anything behind sign-in or a protected screen, provision a managed test user so the app can be driven past auth. Provisioning requires sign-in (npx @ceraph/react-native-mcp@latest init — the token is shared with your Ceraph account).

| Tool | Description | |---|---| | ceraph_test_data_provision | Mint a managed test user on demand against your auth provider (Clerk / Auth0 / Supabase / Firebase / Cognito). Returns email + password (+ a session token when the provider exposes one); 24-hour TTL; optional tag to label it. | | ceraph_test_data_list | List active on-demand test users for the project. | | ceraph_test_data_cleanup | Delete on-demand test users by id, or all: true to wipe them. |

Camera & media testing

iOS blocks external processes from injecting camera frames, so Ceraph ships a __DEV__-gated shim that serves pre-configured media instead: a still image for takePictureAsync and a video clip for recordAsync. It's a subpath import of this package, so testing camera-heavy RN flows takes two lines of code. Both kinds use the same workflow — drop files keyed by name, generate a static registry, and the same <CeraphCamera imageKey> value selects the image for photo capture and the clip for recording.

Installation

Nothing extra. The shim ships in the same package — it is exposed via the @ceraph/react-native-mcp/shim subpath. react, react-native, and expo-camera are optional peer dependencies; bring whichever versions your app already uses.

Setup

1. Drop test images into .ceraph/camera-images/ in your repo root, one file per scenario, with descriptive lowercase filenames. The filename without the extension IS the imageKey you reference in code:

.ceraph/camera-images/profile.jpg       → imageKey="profile"
.ceraph/camera-images/id-card.png       → imageKey="id-card"
.ceraph/camera-images/product.jpg       → imageKey="product"

Supported extensions: .jpg, .jpeg, .png, .webp, .heic. On collision the order above is the priority. These images are committed to your repo by default so test runs are reproducible across machines and CI.

Drop test clips into .ceraph/camera-videos/ the same way — one file per scenario, the stem is the imageKey. Supported extensions: .mp4, .mov, .m4v (priority in that order). The same imageKey that selects a still for takePictureAsync selects a clip for recordAsync, so a screen can have both, or just one:

.ceraph/camera-videos/treadmill.mp4     → imageKey="treadmill"
.ceraph/camera-videos/id-scan.mov       → imageKey="id-scan"

2. Generate the static registry. Call the MCP tool rn_sync_camera_registry — it scans .ceraph/camera-images/ and .ceraph/camera-videos/ and writes a _registry.ts next to each set, with one require() per file (Metro requires statically analysable paths, hence the codegen step). ceraph_doctor also runs this automatically, so you usually never have to call it by hand.

3. Apply the registry at app boot, gated behind __DEV__:

// App.tsx
import { applyCeraphMediaRegistries } from "./.ceraph/_media-registries";

if (__DEV__) {
  applyCeraphMediaRegistries();
}

ceraph init injects this single applyCeraphMediaRegistries seam into your installCeraph({ ... }) call automatically, so you usually don't wire it by hand. The generated .ceraph/_media-registries.ts applies every camera-media registry that exists (images, video, and future kinds) — so adding a new kind is picked up on the next registry sync + a Metro reload, with no change to your layout and no re-init. The per-kind registries still call configureTestImage / configureTestVideo for you; the 1-arg configureTestImage(require(...)) form still works for back-compat, but the codegen above is the recommended workflow.

4. Replace <CameraView> with <CeraphCamera> and set imageKey per screen:

import { CeraphCamera } from "@ceraph/react-native-mcp/shim";

export function ScanProfileScreen() {
  return (
    <CeraphCamera
      imageKey="profile"
      style={{ flex: 1 }}
      facing="front"
      ref={cameraRef}
    />
  );
}

The imageKey prop selects which image the shim returns when the shutter is triggered. The AI tool consuming the MCP picks the right key per screen at code-write time, based on what that camera screen captures conceptually (face → "profile", ID document → "id-card", etc.). <CeraphCamera> takes the same prop shape as expo-camera's CameraView and forwards every other prop verbatim. In production builds, __DEV__ is false, the registries are dead-stripped, the imageKey prop is a no-op, and <CeraphCamera> lazily requires expo-camera and renders the real <CameraView> — zero overhead.

For a screen that records video, the same <CeraphCamera> handles it — recordAsync returns the clip registered under that imageKey (or falls through to the real recorder in production). A screen that both photographs and records just needs a clip AND an image registered under the same key.

Adding test images via MCP

Typical loop: the dev pastes an image into chat and says "use this for the handwriting screen." The AI assistant calls ceraph_add_camera_image to write the bytes into .ceraph/camera-images/ and regenerate _registry.ts in one step — no manual cp, no follow-up call to rn_sync_camera_registry.

// Tool call
{
  "name": "ceraph_add_camera_image",
  "arguments": {
    "imageKey": "handwriting-sample",
    "imageBase64": "iVBORw0KGgoAAAANSUhEUgAA...",
    "contentType": "image/png"   // optional hint
  }
}

// Result shape
{
  "written": ".ceraph/camera-images/handwriting-sample.png",
  "registry": ".ceraph/camera-images/_registry.ts",
  "registered": ["handwriting-sample", "id-card", "profile"],
  "detectedContentType": "image/png",
  "detectedExt": "png",
  "replaced": false,
  "bytes": 48213,
  "registryState": "written",
  "warnings": []
}

Errors return { error, message, remediation } with isError: true — e.g. an existing imageKey without overwrite: true, a payload over the 5 MB cap, or a stem that doesn't match the [a-z0-9]+(?:-[a-z0-9]+)* convention (same rule documented in Setup — the stem IS the imageKey).

Notes:

  • The declared contentType is a hint only. The extension is chosen from magic-byte detection of JPEG/PNG/WebP/HEIC; on mismatch the tool writes using the detected type and surfaces a content-type-mismatch warning.
  • imageKey is validated against a strict lowercase-hyphen regex (no .., no slashes, no NUL), so it can't escape .ceraph/camera-images/ — path traversal is blocked by construction.
  • overwrite: true is required to replace an existing imageKey; if the new extension differs (e.g. profile.jpgprofile.png), the stale sibling is removed and reported as removedSiblingPath.
  • data:image/...;base64,... URI prefixes are accepted and stripped automatically.

Adding test clips via MCP

The video counterpart of ceraph_add_camera_image: the AI assistant calls ceraph_add_camera_video to write a clip into .ceraph/camera-videos/ and regenerate its _registry.ts in one step.

// Tool call
{
  "name": "ceraph_add_camera_video",
  "arguments": {
    "videoKey": "treadmill",
    "videoBase64": "AAAAIGZ0eXBpc29t...",
    "contentType": "video/mp4"   // optional hint
  }
}

// Result shape — identical to ceraph_add_camera_image
{
  "written": ".ceraph/camera-videos/treadmill.mp4",
  "registry": ".ceraph/camera-videos/_registry.ts",
  "registered": ["treadmill"],
  "detectedContentType": "video/mp4",
  "detectedExt": "mp4",
  "replaced": false,
  "bytes": 1048576,
  "registryState": "written",
  "warnings": []
}

Everything works exactly like the image tool: contentType is a hint only (the extension is chosen from magic-byte/container detection of MP4/MOV/M4V, warning on mismatch); videoKey is regex-validated and path-traversal is blocked by construction; overwrite: true is required to replace an existing key and a stale sibling at a different extension is removed + reported as removedSiblingPath; data:video/...;base64,... prefixes are stripped. The cap is 25 MB (vs 5 MB for images), since clips are legitimately larger.

Resolution order at runtime

When <CeraphCamera> renders in dev/test mode, the active test image is chosen in this order:

  1. If the component received an imageKey prop, that key is selected.
  2. Otherwise, the key previously selected via selectTestImageKey is used.
  3. Otherwise, the "default" key is used (only when something registered it).
  4. Otherwise, the shim falls through to the real <CameraView>.

Video (recordAsync) resolves the same way — imageKey → previously-selected key → "default" if registered → fall through to the real recorder — with one deliberate asymmetry: there is no bundled default clip. Images ship with a bundled 1024×1024 black PNG as the "default" fallback so an uninitialized camera still renders something; an empty placeholder clip would never be useful, so video simply falls through to the real recorder when no clip is registered for the key.

Production safety

  • __DEV__ is false in any production build, so configureTestImage / configureTestVideo are never called and the shim always falls through to the real camera.
  • The configured media is referenced inside the __DEV__ branch only — Metro tree-shakes both requires out of the production bundle, so test images and clips are never shipped to end users.
  • expo-camera is imported lazily via require() inside the production branch. Apps that do not yet use expo-camera can still install the shim without a missing-module error.

Hooks

A hook is your own code that Ceraph runs inside your app when it reaches a given route while driving the app — to clear an in-app blocker (an auth wall, a paywall, a feature gate, or a provisioned test user missing app-side data) so you can keep going. Because a hook runs in the app's own JS runtime, it can set your app's auth token, flip an entitlement flag, call your app's own login, and hit your backend — whatever it takes to get past the wall. Ceraph owns when it fires; you own what it does.

1. Write a hook in .ceraph/hooks/, one file per route, named *.hook.ts or *.hook.js. It exports the route it fires on and a run function, and may import your app's own modules:

// .ceraph/hooks/login.hook.ts  →  fires when Ceraph reaches "/login"
import { signIn } from "../../src/services/auth"; // your app's own code

export const route = "/login";   // the screen's route path
export const once = true;        // fire at most once per run (the default)

export async function run(ctx) {
  // ctx.user is the provisioned test identity in use.
  await signIn(ctx.user.email, ctx.user.password); // mutate live app auth state
  return { ok: true };
}

2. That's it. Ceraph watches .ceraph/hooks/, bundles each hook into your dev app, and wires your app entry automatically. Add, edit, or remove a hook file and it re-syncs on its own — a newly added hook's wiring loads on the next full bundle (restart Metro / rebuild; a new import can't come in over Fast Refresh).

When Ceraph reaches /login while driving the app, it fires login.hook.ts in the app to clear the blocker before continuing.

The run(ctx) contract

run receives one argument and returns { ok, error? } (synchronous or async — both are accepted):

| ctx field | | |---|---| | ctx.route | The route Ceraph reached (the one this hook is registered for). | | ctx.runId | Stable identifier of the current run. | | ctx.user | The test identity in use — { email, password, sessionToken? } — or null when no test user is in use. The same identity Ceraph binds to <<CERAPH_TEST_*>> placeholders. |

Return { ok: true } once the blocker is cleared, or { ok: false, error: "…" } to record why it couldn't be. Throwing is fine too — it's caught and recorded as ok: false.

  • export const route (required) — the screen's route path, a string literal. A hook whose route is never reached simply never fires.
  • export const once (optional, default true) — true fires at most once per run (a sign-in / provisioning hook); set false to fire on every landing (a dismiss-on-sight paywall hook).

License

FSL-1.1-MIT — free for use; competing commercial products restricted. Auto-converts to MIT 2 years after each release.