
lynx-devtool
Use when working with Lynx DevTool or debugging a Lynx app, page, or device, especially when the task mentions clients or sessions, CDP or App commands, DOM/CSS inspection, runtime or console logs, screenshots, heap snapshots, Page.reload or App.openPage, global switches, or inspecting a ReactLynx component tree (`reactlynx tree`), searching components (`reactlynx find`), inspecting props/state/hooks (`reactlynx component`), or mutating props/state/context (`reactlynx update-prop` / `update-state` / `update-context`) on Android, iOS, or Desktop.
Related Skills
Use when working with Lynx DevTool or debugging a Lynx app, page, or device, especially when the task mentions clients or sessions, CDP or App commands, DOM/CSS inspection, runtime or console logs, screenshots, heap snapshots, Page.reload or App.openPage, global switches, or inspecting a ReactLynx component tree (`reactlynx tree`), searching components (`reactlynx find`), inspecting props/state/hooks (`reactlynx component`), or mutating props/state/context (`reactlynx update-prop` / `update-state` / `update-context`) on Android, iOS, or Desktop.
DevTool Skill
This skill allows you to interact with Lynx applications running on connected devices (Android, iOS, Desktop) using the Lynx DevTool CLI.
Usage
The CLI is located at <path_to_the_skill>/scripts/index.mjs relative to this skill's directory. You can run it using node.
The programmatic API is located at <path_to_the_skill>/scripts/connector.mjs. This entry re-exports everything from @lynx-js/devtool-connector, @lynx-js/devtool-connector/transport, and @lynx-js/devtool-connector/streams, and also provides createDefaultTransports() and createDefaultConnector() helpers that match the CLI defaults.
In the skill directory, use:
node <path_to_the_skill>/scripts/index.mjs <command>
Note: All command outputs are multi-line JSON. You can use jq or Node.js to process the data.
Use as a Library
If you want to drive Lynx DevTool directly from JavaScript instead of shelling out to the CLI, import from scripts/connector.mjs.
import {
Connector,
createDefaultConnector,
} from "<path_to_the_skill>/scripts/connector.mjs";
const connector = createDefaultConnector();
const clients = await connector.listClients();
console.log(clients);
For fuller programmatic workflows, see Library Usage Reference and Programmatic Debugging Example.
You can also construct the connector manually if you need custom transports:
import {
AndroidTransport,
Connector,
DesktopTransport,
iOSTransport,
} from "<path_to_the_skill>/scripts/connector.mjs";
const connector = new Connector([
new AndroidTransport({ host: "127.0.0.1", port: 5037 }),
new DesktopTransport(),
new iOSTransport(),
]);
Global Options
-h, --help: Display help for command.
Note: Each subcommand supports the --help flag (e.g. node <path_to_the_skill>/scripts/index.mjs cdp --help). Use this to view the full list of available arguments and their descriptions.
Client Targeting
Commands that accept -c, --client <clientId> also accept --client-name <name>.
Use --client-name to resolve a client from list-clients by package/app identifier
(AppProcessName, bundleId, bundleName, or App), for example:
node <path_to_the_skill>/scripts/index.mjs cdp --client-name com.example.app -m DOM.getDocument
node <path_to_the_skill>/scripts/index.mjs list-sessions --client-name com.lynx.uiapp
If multiple clients match the name, use list-clients and pass the exact client ID with --client.
Commands
1. List Clients
List all available Lynx clients (apps with DevTool enabled).
node <path_to_the_skill>/scripts/index.mjs list-clients
2. List Sessions
List all active debugging sessions. A session corresponds to a specific Lynx view or context.
node <path_to_the_skill>/scripts/index.mjs list-sessions
# Optional: Filter by client ID
node <path_to_the_skill>/scripts/index.mjs list-sessions --client <clientId>
3. Send CDP Command
Send a Chrome DevTools Protocol (CDP) command to a specific session.
Note that Lynx only supports a part of the standard CDP command.
LynxView note: when the target session is a LynxView, you MUST read Supported CDP Methods before sending a CDP command.
WebView note: when the target session is a WebView (for exampletype: "web"or an HTTP/HTTPS URL), use the standard Chrome DevTools Protocol documentation for CDP method names, parameters, and enable prerequisites. The localreferences/cdppages focus on LynxView support and Lynx-specific extensions, which may returnmethod not foundon WebView targets.
node <path_to_the_skill>/scripts/index.mjs cdp -m <method> [options] [params]
-m, --method <method>: The CDP method name (e.g.,DOM.getDocument,Runtime.evaluate).-c, --client <clientId>: (Optional) The Client ID. If omitted, uses the first available client.--client-name <name>: (Optional) Package/app name resolved fromlist-clients.-s, --session <sessionId>: (Optional) The Session ID. If omitted, uses the latest session (with the largest session ID).--thread <thread>: (Optional) Target VM thread,backgroundormain. Defaults tobackground.[params]: (Optional) JSON string of parameters for the command.
When --thread main is used, only Debugger.*, Runtime.*, HeapProfiler.*, and Profiler.* methods are supported.
Example:
# Get the document root
node <path_to_the_skill>/scripts/index.mjs cdp -m DOM.getDocument
# Evaluate JavaScript
node <path_to_the_skill>/scripts/index.mjs cdp -m Runtime.evaluate '{"expression": "2 + 2"}'
# Evaluate JavaScript on the main-thread VM
node <path_to_the_skill>/scripts/index.mjs cdp --thread main -m Runtime.evaluate '{"expression": "2 + 2"}'
4. Send App Command
Send an App-level command.
node <path_to_the_skill>/scripts/index.mjs app -m <method> [options] [params]
-m, --method <method>: The App method name (e.g.,App.openPage).-c, --client <clientId>: (Optional) Client ID.--client-name <name>: (Optional) Package/app name resolved fromlist-clients.[params]: (Optional) JSON string of parameters.
You MUST read Supported App Methods before sending an App command.
5. Open URL
Open a specific URL in the Lynx app.
node <path_to_the_skill>/scripts/index.mjs open <url> [options]
<url>: The URL to open.-c, --client <clientId>: (Optional) Client ID.
Example:
node <path_to_the_skill>/scripts/index.mjs open "lynx://example/page"
6. Inspect
Output the inspector URL for a client/session.
node <path_to_the_skill>/scripts/index.mjs inspect [options]
-c, --client <clientId>: (Optional) Client ID.-s, --session <sessionId>: (Optional) Session ID.--port <port>: (Optional) Daemon port. Defaults to21783.
7. Get Console
Capture console logs from the device.
node <path_to_the_skill>/scripts/index.mjs get-console [options]
-c, --client <clientId>: (Optional) Client ID.-s, --session <sessionId>: (Optional) Session ID.--offset <number>: Skip N messages.--limit <number>: Limit number of messages.--include-stack-traces: Include stack traces for non-error messages.--level <levels>: Filter log levels (e.g.,error,warning).--thread <thread...>: Target VM thread(s):backgroundormain. If omitted, both threads are collected by default.
8. Get Sources
List all parsed scripts. This is useful for finding script IDs to use with other commands (e.g., Debugger.getScriptSource). The command automatically fetches all currently loaded scripts.
node <path_to_the_skill>/scripts/index.mjs get-sources [options]
-c, --client <clientId>: (Optional) Client ID.-s, --session <sessionId>: (Optional) Session ID.
9. Take Screenshot
Take a screenshot of the current page.
node <path_to_the_skill>/scripts/index.mjs take-screenshot [options]
-c, --client <clientId>: (Optional) Client ID.-s, --session <sessionId>: (Optional) Session ID.--fullscreen: (Optional) Capture the screenshot infullscreenmode. Defaults tolynxviewmode if not provided.-o, --output <path>: (Optional) Output file path.
10. Take Heap Snapshot
Capture a QuickJS heap snapshot from the current Lynx session and save it as a .heapsnapshot file.
node <path_to_the_skill>/scripts/index.mjs take-heap-snapshot [options]
-c, --client <clientId>: (Optional) Client ID.-s, --session <sessionId>: (Optional) Session ID.--thread <thread>: (Optional) Target VM thread,backgroundormain. Defaults tobackground.-o, --output <path>: (Optional) Output file path. Defaults to the OS temp directory.
11. Global Switch
Manage DevTool global switches.
# List all supported keys and their current values
node <path_to_the_skill>/scripts/index.mjs global-switch list [options]
# Get one key
node <path_to_the_skill>/scripts/index.mjs global-switch get --key <globalKey> [options]
# Set one key
node <path_to_the_skill>/scripts/index.mjs global-switch set --key <globalKey> --status <on|off> [options]
-c, --client <clientId>: (Optional) Client ID.
global-switch list options:
--fail-fast: Abort on first key-read failure.
global-switch get options:
--key <globalKey>: Global switch key. (Required)
global-switch set options:
--key <globalKey>: Global switch key. (Required)--status <on|off>: Target switch status. (Required)
For the full key list and examples, see Global Switch Reference.
12. Query Global Memory Usage
Query Lynx global memory usage through the global Memory.* CDP domain. Use the generic cdp command and send the request to the global DevTool handler with session ID -1.
# Get global Lynx memory usage across live instances
node <path_to_the_skill>/scripts/index.mjs cdp -s -1 -m Memory.getAllMemoryUsage
node <path_to_the_skill>/scripts/index.mjs cdp -s -1 -m Memory.getAllMemoryUsage '{"timeoutMs":50000}'
-c, --client <clientId>: (Optional) Client ID.-s, --session <sessionId>: CDP session ID. Use-1for the global DevTool handler unless you have a platform-specific reason to override it.params.timeoutMs(Optional): Non-negative timeout in milliseconds. Maximum value is300000.
When the DevTool MCP server is available, prefer the Memory_getAllMemoryUsage MCP tool for the same raw payload instead of shelling out to the CLI.
13. Recording
Record Lynx page interactions via TestBench (CDP-based). Captures all actions (template loads, touch events, JS module calls, data updates) and produces a JSON replay file.
# Start recording (BEFORE opening the target page)
node <path_to_the_skill>/scripts/index.mjs recorder start [options]
# Stop recording and save the replay file
node <path_to_the_skill>/scripts/index.mjs recorder end [options]
-c, --client <clientId>: (Optional) Client ID forstartandend.-o, --output <path>: (Optional) Output file or directory path forend. Defaults to~/.lynx-devtool/files/lynxrecorder/recording-<clientId>-<timestamp>.json.
Workflow:
- Run
recorder start. If it enablesenable_debug_mode, restart the app and runrecorder startagain. - User opens and interacts with the Lynx page.
- Run
recorder end --output <file.json>to stop and save. - Report the absolute file path to the user.
Important: For a replayable file, open or reload the target page after recorder start so the recording includes loadTemplate.
See Recording Reference for more details.
14. ReactLynx Component Tree
Print the component tree of a running ReactLynx page, decoded from @lynx-js/preact-devtools. The CLI opens a Lynx.onVMEvent stream, sends a Preact DevTools init+refresh handshake, and renders the resulting operation_v2 payloads as an ASCII tree.
node <path_to_the_skill>/scripts/index.mjs reactlynx tree [options]
-c, --client <clientId>: (Optional) Client ID.-s, --session <sessionId>: (Optional) Session ID.--depth <n>: (Optional) Maximum tree depth to print. Default: unbounded.--show-shells: Include the syntheticFragment/Root/Anonymouswrappers ReactLynx inserts. They are hidden by default.--json: Emit{ labels, roots, nodes }instead of ASCII; use this when a script will consume the tree.
Output uses @cN [type] Name references (the convention from agent-react-devtools). Labels are pre-order DFS over visible roots and reset on every invocation, so they are stable within a single command but not across runs:
@c1 [fn] App
├─ @c2 [fn] Header
│ └─ @c3 [fn] Logo
└─ @c4 [fn] Body
Requirements:
- The page must be a dev build running
@lynx-js/preact-devtools(production bundles stripsetupReactLynx()). Successful initialization logs[PREACT DEVTOOLS] Devtools initialized successfullyto the device console. @lynx-js/preact-devtoolsmust include thedocument.bodyandpreactDevtoolsCtx.Nodefixes (PR #2 + PR #5 againstlynx-family/preact-devtools). Without them, therefreshchannel will return zerooperation_v2frames and the CLI will print the "stale preact-devtools" diagnostic.
When the tree comes back empty, the CLI exits with code 1 and writes one of three targeted diagnostics on stderr:
saw 0 frames: nothing replied on thePreactDevtoolschannel. The App is most likely missing@lynx-js/preact-devtools, is a production build, has not finishedsetupReactLynx(), or you picked the wrong--session.saw N frames but no operation_v2: the hook is loaded but itsrefreshhandler is buggy. Upgrade@lynx-js/preact-devtoolsto a build that contains PRs #2 and #5.tree is empty: every node was unmounted between commits -- rare, rerun withDEBUG(below) to see the raw envelopes.
For deep debugging, set DEBUG=devtool-mcp-server:reactlynx to log every PreactDevtools frame (type + payload size) on stderr while leaving stdout (the tree / JSON) clean:
DEBUG='devtool-mcp-server:reactlynx' node <path_to_the_skill>/scripts/index.mjs reactlynx tree
15. ReactLynx Component Inspect
Inspect a single ReactLynx component (props / state / hooks / context / signals) by sending the Preact DevTools inspect envelope and reading back inspect-result.
node <path_to_the_skill>/scripts/index.mjs reactlynx component <ref> [options]
<ref>: either a label@cNproduced byreactlynx tree/reactlynx find, or a numeric vnode id.- With
@cN, the CLI does an extra init+refresh+tree round-trip first to resolve the label. Pass--show-shellsif (and only if) the label was generated with shells visible. - With a numeric id (e.g.
3856353762), the snapshot is skipped -- one round-trip total.
- With
-c, --client <clientId>,-s, --session <sessionId>: (Optional) standard targeting flags.--show-shells: When resolving@cN, count synthetic Fragment / Root / Anonymous wrappers the same wayreactlynx tree --show-shellsdoes.--json: Print the rawInspectDatapayload as JSON. Default output is a compact ASCII summary.
Example output:
@c5 (id=3856353783) [fn] TUXIntroViewListCell key=1. HMR
source: src/TUXIntroViewListCell.tsx:42:3
props:
{
"title": "1. HMR",
"icon": { "type": "vnode", "name": "TUXIcon" }
}
16. ReactLynx Component Find
Find every component whose name matches a substring or regex. Output is ordered identically to reactlynx tree (pre-order DFS) so the @cN labels round-trip with the other subcommands.
node <path_to_the_skill>/scripts/index.mjs reactlynx find <pattern> [options]
<pattern>: substring (default, case-insensitive) or JavaScript regex with--regex.-c, --client <clientId>,-s, --session <sessionId>: (Optional) standard targeting flags.--regex: Treat<pattern>as a JavaScript regular expression (e.g.--regex '^Toast(List)?$').--show-shells: Include synthetic Fragment / Root / Anonymous wrappers.--limit <n>: Maximum number of matches to print. Default50.--json: Emit[{ label, id, name, type, key, ancestors: [{label, name}] }, ...]for scripted post-processing.
Example output:
@c8 [fn] TUXCenterToastActivator
in @c1 TUXApp > @c2 Provider > @c3 App
@c10 [fn] TUXTopToastActivator
in @c1 TUXApp > @c2 Provider > @c3 App
reactlynx find is the recommended way to discover labels for follow-up reactlynx component @cN calls when the tree is too large to scan visually.





