This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
AppClaw is an agentic AI layer for mobile automation (Android & iOS). Users describe goals in plain English and AppClaw orchestrates device interactions through appium-mcp (Model Context Protocol). It supports multiple LLM providers (Anthropic, OpenAI, Google Gemini, Groq, Ollama) via the Vercel AI SDK.
npm run build # TypeScript compilation (tsc) → dist/
npm run typecheck # Type-check without emitting
npm start # Run via tsx (dev mode)
npm start "goal" # Run with a goal argument
npm run dev # Run with file watching
npx tsx tests/verify-parsing.ts # Run parsing verification testsNo formal test framework (Jest/Vitest) is configured. Tests are ad-hoc scripts in tests/.
The CLI routes to 6 modes based on flags:
- Interactive (default) — prompts for platform/device/goal, runs agent loop
- YAML Flow (
--flow file.yaml) — declarative automation, zero LLM cost - Terminal Studio (
--tui, or its alias--playground) — multi-screen Ink app: platform/device picker, slash-command palette, step recording,/goalagent runs, settings, run history, and device mirroring —/streamrenders the screen inside the terminal (see Terminal Studio below). This is the interactive mode; the old--playgroundREPL was removed and the flag now routes here. - Explorer (
--explore) — PRD → YAML test flow generation - Record/Replay (
--record,--replay) — capture and replay sessions - Report (
--report) — Express server serving HTML reports from.appclaw/runs/
The interactive and goal-direct paths also accept --export [path] (optionally --export-dir <dir>) to write a replayable @appclaw/runner spec when the goal completes. Path resolution: empty → EXPORT_DIR/<goal-slug>.test.ts (EXPORT_DIR defaults to tests, the runner's own testDir, so an export is runnable where it lands); bare filename → EXPORT_DIR/<name>; anything with a directory hint → used verbatim. Implementation in src/sdk/goal-export.ts (translator + renderer) called from src/index.ts after the agent loop.
Public TypeScript API consumed by external tests (vitest/jest/mocha). Single entry: import { AppClaw } from '@appclaw/core'. Surface:
app.run(instruction, options?)— one natural-language step, non-throwing, returns{ success, action, message }.options(RunOptions) applies per-command overrides for this call only:waitTimeout/waitInterval(implicit-wait poll budget) andscrollMode/scrollTimes(scroll/swipe distance + count). Instance-wide defaults for all four live onAppClawOptions; per-call values win. Implicit wait: every element-bearing action polls its target until present (DOM re-reads page source, vision re-captures the screenshot) or the budget is exhausted —WAIT_TIMEOUT/WAIT_INTERVALenv, default 10s/300msapp.verify(claim)— assertion. ThrowsAppClawAssertionErroron failure (includesclaim,result, andscreenContentsfrom DOM page-source in DOM mode — in vision mode the LLM's reason is already inresult.message)app.runFlow(path)— wraps the YAML flow engineapp.runGoal(goal, { exportPath?, exportConfig? })— wraps the agent loop. WhenexportPathis set, the trajectory is filtered withkeepOnlyFinalAttempt()(drops the branch before any rejecteddone) then rendered as an@appclaw/runnerspec viagenerateSdkTest()app.teardown()— finalize report, close MCP
Helpers in src/sdk/goal-export.ts: keepOnlyFinalAttempt, instructionsFromHistory, decisionToInstruction, generateSdkTest, generateSdkTestFromInstructions (the last is used by the step recorders' /export *.test.ts — see packages/cli/src/step-recorder/).
The main Perception→Reasoning→Action loop:
- Perceive — get screen state (DOM XML or screenshot) via
src/perception/ - Reason — send trimmed DOM + goal + history to LLM via
src/llm/ - Act — execute action (tap, type, swipe) via appium-mcp through
src/mcp/ - Feedback — check for stuck state (
src/agent/stuck.ts), adapt if needed - Loop until goal complete or max steps reached
Supporting agent modules: planner (goal decomposition), recovery (checkpointing), human-in-the-loop (OTP/CAPTCHA pauses), episodic memory (trajectory reuse).
src/sdk/— Public TypeScript API for external tests.index.tsis theAppClawclass;goal-export.tstranslates agent histories back to natural-languageapp.run(...)calls and renders@appclaw/runnerspecs;step-runner.tsadapts the YAML flow engine to single-instruction calls;screen-snapshot.tscaptures visible DOM text for assertion-error context.src/llm/— Multi-provider LLM integration.provider.tsis the factory;prompts.tsbuilds system/user messages;schemas.tsdefines action schemas. Tools from appium-mcp are dynamically converted to Vercel AI SDK format.src/mcp/— Appium MCP client wrapper. Connects via stdio (subprocess) or SSE. Handles tool calling, element finding, screenshots, keyboard input.src/perception/— Screen parsing. Android (android-parser.ts) and iOS (ios-parser.ts) XML parsers.dom-trimmer.tscompacts DOM for LLM token efficiency.src/vision/— AI vision element location using df-vision + Gemini (Stark) or appium-mcp server-side vision. Returns normalized coordinates.src/flow/— YAML flow execution.parse-yaml-flow.tsparses declarative steps;run-yaml-flow.tsexecutes them. Supports natural language steps, phased execution, variable interpolation from.appclaw/env/.src/device/— Device setup pipeline: platform selection → device picking → iOS-specific setup → Appium session creation.src/memory/— Episodic memory. Records successful trajectories to~/.appclaw/trajectories.json, retrieves relevant past experiences via fingerprinting.src/report/— Execution reporting.writer.tscollects artifacts;renderer.tsgenerates HTML reports;server.tsserves them.src/ui/terminal.ts— Rich terminal output (spinners, boxes, gradient headers, markdown rendering). JSON output mode for IDE integration (json-emitter.ts).packages/cli/src/step-recorder/— Shared by every step-recording surface.flow-builder.tsrenders a recordedFlowStep[]as YAML or an@appclaw/runnerspec and resolves/exportpaths;screen-info.tsanswers "what's on screen?" via one vision call;memory-inspect.tsbacks/memory.json-bridge.tsis the headless NDJSON-over-stdio recorder behindappclaw --json --playground, which the VS Code / Cursor extension spawns — its wire protocol is a shipped contract (seevscode-extension/src/bridge.ts).
appclaw --tui (and its alias appclaw --playground) is a separate, multi-screen Ink app (distinct from the single-screen agent-run UI in packages/cli/src/ui/ink/) with its own observable store (store.ts, a small subscribe/snapshot pub-sub) and a screen router (TuiApp.tsx) switching between screens/{Welcome,DevicePicker,Main,Settings,History}Screen.tsx. commands.ts defines the /-prefixed command palette shown on the Main screen. Anything NOT starting with / is one deterministic instruction — runOneInstruction() — appended to store.steps; that recording is what /list, /yaml, /edit and /export operate on, and a step that fails is reported but not recorded. /goal <text> is the opt-in to the autonomous loop (a single flat runAgent() call, no multi-sub-goal planner — that stays on the plain appclaw "goal" path) and records nothing. Device listing goes straight through adb/xcrun simctl (@appclaw/core/device/emulator-list.ts), not through an MCP session, so the picker works before any Appium session exists.
Device mirroring is Android-only, and adb -s <serial> is device-agnostic — the same path covers a running emulator, a physical phone and a headless emulator.
/stream renders the device inside the terminal (packages/cli/src/tui/stream/). Frames go to the side panel on the main screen (components/StreamPanel.tsx), which draws only chrome and leaves a blank region so the command palette and prompt stay live alongside it; frame-loop.ts polls adb exec-out screencap every 200ms and paints that region with direct process.stdout.writes — never from React, since Ink rewrites its whole frame on every state change. terminal-caps.ts picks the backend from env (no capability query — Ink owns stdin in raw mode): Kitty graphics for Ghostty/kitty/WezTerm (kitty.ts sends a PNG by file path, a=T,f=100,t=f,c=…,r=…, so the terminal does the scaling), otherwise 24-bit ANSI half-blocks (halfblock.ts downsamples the raw RGBA framebuffer from screencap with no -p, so no PNG decoder and no new dependency). layout.ts holds the geometry both sides agree on. Force a backend with APPCLAW_STREAM_BACKEND=kitty|halfblock.
/stream-close stops it; device switches and quit() tear the frame loop down the same way, via resetStream().
All config is via .env file, validated by Zod schema. Key vars: LLM_PROVIDER, LLM_API_KEY, PLATFORM, AGENT_MODE (dom vs vision), MAX_STEPS, EXPORT_DIR (default location for --export writes), MCP_DEBUG (verbose appium-mcp logs — function-evaluated so the SDK can flip it at runtime via the mcpDebug option). See README.md for the full table.
- ES2022 modules throughout (import/export)
- TypeScript strict mode
- Zod for schema validation
- No DI framework — modules import each other directly
- Constants and model pricing in
src/constants.ts