HTTP daemon and CLI architecture for agent-driven browser extension testing with Playwright.
This package provides the core infrastructure for enabling LLM agents to interact with browser extensions through Playwright. It ships a persistent HTTP daemon that manages browser lifecycle and a unified mm CLI that agents (and developers) use to drive sessions.
The design is consumer-agnostic: the core handles protocol, tooling, and knowledge — consumers provide extension-specific logic by implementing the ISessionManager interface and injecting capabilities.
┌─────────────────────────────────┐
│ LLM Agent / Dev │
└────────────┬────────────────────┘
│ mm CLI commands
▼
┌─────────────────────────────────┐
│ mm CLI (src/cli/mm.ts) │
│ discover / auto-start daemon │
└────────────┬────────────────────┘
│ HTTP (127.0.0.1)
▼
┌───────────────────────────────────────────────────────────────────┐
│ HTTP Daemon (createServer) │
│ │
│ ┌──────────┐ ┌──────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ Routes │ │ RequestQueue │ │ Tool │ │ Knowledge │ │
│ │ /health │ │ (async mutex)│ │ Registry │ │ Store │ │
│ │ /status │ │ │ │ 25+ tools │ │ │ │
│ │ /launch │ └──────────────┘ └─────┬──────┘ └────────────┘ │
│ │ /cleanup │ │ │
│ │ /tool/:n │ ▼ │
│ └──────────┘ ┌──────────────────┐ │
│ │ ToolContext │ │
│ │ sessionManager │ │
│ │ page / refMap │ │
│ │ workflowContext │ │
│ │ knowledgeStore │ │
│ └────────┬─────────┘ │
└──────────────────────────────────────┼───────────────────────────┘
│
┌─────────────────────┼─────────────────────┐
│ ISessionManager │
│ (consumer implementation) │
│ │
│ Session lifecycle Page management │
│ Extension state A11y reference map │
│ Navigation Screenshots │
│ Capabilities (opt) Environment config │
└─────────────────────┬─────────────────────┘
│
┌─────────────────────┼─────────────────────┐
│ WorkflowContext │
│ │
│ build? fixture? │
│ chain? contractSeeding? │
│ stateSnapshot? mockServer? │
│ config: EnvironmentConfig │
└─────────────────────┬─────────────────────┘
│
▼
┌───────────────────────────────────────────┐
│ Playwright → Chrome Browser │
│ Browser Extension │
└───────────────────────────────────────────┘
- Node.js
^20 || ^22 || >=24 - TypeScript
>=5.0(for consumer type definitions) - Playwright
^1.49.0(peer dependency)
As a project dependency (the CLI is available via npx mm or yarn mm):
yarn add @metamask/client-mcp-coreAs a global CLI (puts mm directly on your PATH — recommended for LLM agents):
npm install -g @metamask/client-mcp-coreThe global CLI can target any project via --project or MM_PROJECT (see Project Targeting).
Consuming this package requires two things: a daemon entry point and a configuration file.
// daemon.ts
import { createServer, allocatePort } from '@metamask/client-mcp-core';
import { MySessionManager } from './my-session-manager';
import { createMyContext } from './my-context';
const server = createServer({
sessionManager: new MySessionManager(),
contextFactory: async () => {
// Consumer owns port allocation — use the allocatePort() helper
// or any other strategy that fits your infrastructure.
const anvil = await allocatePort();
const fixture = await allocatePort();
await Promise.all([
new Promise<void>((r) => anvil.server.close(() => r())),
new Promise<void>((r) => fixture.server.close(() => r())),
]);
return createMyContext({
ports: { anvil: anvil.port, fixture: fixture.port },
});
},
});
server.start().then((state) => {
console.error(`Daemon started on port ${state.port}`);
});Create mm-client-cli.config.ts in your project root:
export default {
daemon: 'path/to/daemon.ts',
runtime: 'tsx',
};The daemon field tells the CLI where the daemon entry point lives. The runtime field specifies the TypeScript runner (defaults to tsx).
The CLI uses cosmiconfig for config discovery, so you can also use mm-client-cli.config.js, .mm-client-clirc.json, or other supported formats.
mm launch # auto-starts daemon, opens browser session
mm describe-screen # get element references
mm click e3 # interact using a11y refs
mm cleanup --shutdown # stop browser and daemonIf running from outside the project directory (e.g., a parent folder containing multiple repos):
mm --project ./my-extension launch
mm --project ./my-extension describe-screen
# Or set once via environment variable
export MM_PROJECT=/path/to/my-extension
mm launchThe architecture relies on a persistent background HTTP daemon that manages the browser lifecycle:
- Worktree Isolation: Each git worktree runs its own daemon instance, tracked via a
.mm-serverstate file in the project root. This allows parallel work across branches. - Port Allocation: The daemon allocates its own HTTP port automatically. Sub-service ports (Anvil, fixture server, etc.) are allocated by the consumer's
contextFactoryand reported back viaallocatedPorts. TheallocatePort()helper is exported for convenience. - Auto-Start: The daemon starts automatically on
mm launchif not already running, and shuts down after a period of inactivity (default: 30 minutes). - Request Serialization: A
RequestQueue(async mutex) ensures only one tool executes at a time, preventing race conditions on shared browser state. - Health Checks: Each daemon generates a unique nonce on startup. The CLI verifies daemon identity via
GET /healthto detect stale.mm-serverfiles from crashed processes. - Logs: Daemon activity is logged to
.mm-daemon.log.
ISessionManager is the core abstraction boundary between this package and consumer implementations. Consumers must implement this interface to provide extension-specific browser control.
type ISessionManager = {
// Session Lifecycle
hasActiveSession(): boolean;
getSessionId(): string | undefined;
launch(input: SessionLaunchInput): Promise<SessionLaunchResult>;
cleanup(): Promise<boolean>;
// Page Management
getPage(): Page;
setActivePage(page: Page): void;
getTrackedPages(): TrackedPage[];
classifyPageRole(page: Page, extensionId?: string): TabRole;
getContext(): BrowserContext;
// Extension State
getExtensionState(): Promise<ExtensionState>;
// A11y Reference Map
setRefMap(map: Map<string, string>): void;
getRefMap(): Map<string, string>;
resolveA11yRef(ref: string): string | undefined;
// Navigation
navigateToHome(): Promise<void>;
navigateToSettings(): Promise<void>;
navigateToUrl(url: string): Promise<Page>;
navigateToNotification(): Promise<Page>;
waitForNotificationPage(timeoutMs: number): Promise<Page>;
// Screenshots
screenshot(options: SessionScreenshotOptions): Promise<ScreenshotResult>;
// Capabilities (optional, extension-specific)
getBuildCapability(): BuildCapability | undefined;
getFixtureCapability(): FixtureCapability | undefined;
getChainCapability(): ChainCapability | undefined;
getContractSeedingCapability(): ContractSeedingCapability | undefined;
getStateSnapshotCapability(): StateSnapshotCapability | undefined;
// Environment
getEnvironmentMode(): EnvironmentMode;
setContext(context: 'e2e' | 'prod', options?: Record<string, unknown>): void;
getContextInfo(): { currentContext: 'e2e' | 'prod'; ... };
};The WorkflowContext aggregates optional capabilities that consumers inject through the contextFactory. The tool system checks for capabilities at runtime — tools that depend on missing capabilities return clear errors.
type WorkflowContext = {
build?: BuildCapability;
fixture?: FixtureCapability;
chain?: ChainCapability;
contractSeeding?: ContractSeedingCapability;
stateSnapshot?: StateSnapshotCapability;
mockServer?: MockServerCapability;
config: EnvironmentConfig;
allocatedPorts?: PortMap; // reported to /status and persisted in .mm-server
};Capabilities are created by the consumer's contextFactory function. The factory is responsible for allocating any sub-service ports it needs (the allocatePort() helper is exported for convenience):
async function createMyContext(options: {
ports: { anvil: number; fixture: number };
}): Promise<WorkflowContext> {
return {
build: new MyBuildCapability(),
fixture: new MyFixtureCapability(options.ports.fixture),
chain: new MyChainCapability(options.ports.anvil),
allocatedPorts: {
anvil: options.ports.anvil,
fixture: options.ports.fixture,
},
config: {
environment: 'e2e',
extensionName: 'MyExtension',
defaultPassword: 'test-password',
artifactsDir: './test-artifacts',
defaultChainId: 1337,
ports: {
anvil: options.ports.anvil,
fixtureServer: options.ports.fixture,
},
},
};
}| Capability | Purpose | Enables Tools |
|---|---|---|
BuildCapability |
Build extension from source | build |
FixtureCapability |
Manage wallet state via fixtures | launch (state modes) |
ChainCapability |
Local blockchain (Anvil) lifecycle | Chain interactions |
ContractSeedingCapability |
Deploy smart contracts to Anvil | seed_contract, seed_contracts, get_contract_address, list_contracts |
StateSnapshotCapability |
Read extension state and detect screens | get_state |
MockServerCapability |
HTTP mock server for API stubbing | Mock-dependent tests |
Each capability interface is defined in src/capabilities/types.ts:
type BuildCapability = {
build(options?: BuildOptions): Promise<BuildResult>;
getExtensionPath(): string;
isBuilt(): Promise<boolean>;
};
type FixtureCapability = {
start(state: WalletState): Promise<void>;
stop(): Promise<void>;
getDefaultState(): WalletState;
getOnboardingState(): WalletState;
resolvePreset(presetName: string): WalletState;
};
type ChainCapability = {
start(): Promise<void>;
stop(): Promise<void>;
isRunning(): boolean;
setPort(port: number): void;
};
type ContractSeedingCapability = {
deployContract(
name: string,
options?: DeployOptions,
): Promise<ContractDeployment>;
deployContracts(
names: string[],
options?: DeployOptions,
): Promise<{
deployed: ContractDeployment[];
failed: { name: string; error: string }[];
}>;
getContractAddress(name: string): string | null;
listDeployedContracts(): ContractInfo[];
getAvailableContracts(): string[];
clearRegistry(): void;
initialize(): void;
};
type StateSnapshotCapability = {
getState(page: Page, options: StateOptions): Promise<StateSnapshot>;
detectCurrentScreen(page: Page): Promise<string>;
};
type MockServerCapability = {
start(): Promise<void>;
stop(): Promise<void>;
isRunning(): boolean;
getServer(): unknown;
getPort(): number;
};Tools are standalone functions registered in a central toolRegistry. Each tool receives a ToolContext and returns a ToolResponse.
type ToolFunction<TParams, TResult> = (
params: TParams,
context: ToolContext,
) => Promise<ToolResponse<TResult>>;
type ToolContext = {
sessionManager: ISessionManager;
page: Page;
refMap: Map<string, string>;
workflowContext: WorkflowContext;
knowledgeStore: KnowledgeStore;
};The daemon routes POST /tool/:name requests through the registry, applies Zod validation on inputs, executes the tool through the request queue, and captures observations (extension state, test IDs, a11y snapshot) after each execution.
Registered tools:
| Tool | Description |
|---|---|
| Lifecycle | |
build |
Triggers an extension build using the configured BuildCapability. Accepts build type and force options. |
launch |
Launches a new browser session with the configured extension. Supports state modes (default, onboarding, custom), fixture presets, goal/tag metadata, and optional contract seeding on start. |
cleanup |
Tears down the active browser session and cleans up all resources (browser, services, fixtures). |
| Interaction | |
click |
Clicks an element identified by a11y ref, test ID, or CSS selector. Waits for the element to be visible before clicking. Supports within to scope the target inside a parent element. |
type |
Types text into an input element identified by a11y ref, test ID, or CSS selector. Clears the field first, then sets the new value (uses Playwright's fill()). Supports within scoping. |
wait_for |
Waits for an element to become visible on the page within a configurable timeout. Supports within to scope the target inside a parent element. |
get_text |
Reads the text content of an element identified by a11y ref, test ID, or CSS selector. Returns the text, target descriptor, and character length. Supports within scoping. Categorized as read-only (no observations in response). |
clipboard |
Reads from or writes to the system clipboard via Chrome DevTools Protocol. Useful for pasting seed phrases or copying addresses. |
| Navigation | |
navigate |
Navigates the browser to a named screen (home, settings, notification) or an arbitrary URL. |
switch_to_tab |
Switches the active page to a tab matching a given role (e.g., extension, dapp) or URL prefix. |
close_tab |
Closes a browser tab matching a given role or URL. Falls back to the extension tab if the active tab is closed. |
wait_for_notification |
Waits for the extension notification popup to appear within a timeout. Returns the notification page URL. |
| Discovery | |
describe_screen |
Captures a comprehensive screen snapshot: extension state, visible test IDs, trimmed a11y tree with refs, optional screenshot, and prior knowledge from historical sessions. |
accessibility_snapshot |
Captures a trimmed accessibility tree of the current page with deterministic refs (e1, e2, ...). Supports scoping to a root CSS selector. |
list_testids |
Collects all visible data-testid attributes from the current page with text previews and visibility status. |
| State | |
get_state |
Retrieves the current extension state (URL, screen, network, balance, account) and tracked tab information. |
get_context |
Returns the current environment context (e2e or prod), session status, available capabilities, and whether context switching is allowed. |
set_context |
Switches the session environment between e2e and prod modes. Blocked while a session is active. |
| Screenshots | |
screenshot |
Captures a screenshot of the current page. Supports naming, full-page capture, scoping to a CSS selector, and optional base64 output. |
| Knowledge | |
knowledge_last |
Retrieves the N most recent step records from the knowledge store, with optional scope and filter parameters. |
knowledge_search |
Searches step records by query string with token-based matching and synonym expansion. Scores results by relevance to screen, URL, test IDs, and a11y nodes. |
knowledge_summarize |
Generates a recipe-style summary of a session's tool invocations, showing the step sequence with targets and outcomes. |
knowledge_sessions |
Lists available knowledge sessions with metadata (goal, flow tags, timestamps), with optional filtering. |
| Contracts | |
seed_contract |
Deploys a single smart contract to the local Anvil chain by name. Requires ContractSeedingCapability. |
seed_contracts |
Deploys multiple smart contracts in sequence. Returns both successful deployments and individual failures. |
get_contract_address |
Looks up the deployed address of a contract by name from the session's deployment registry. |
list_contracts |
Lists all contracts deployed in the current session with addresses and deployment timestamps. |
| Batching | |
run_steps |
Executes a batch of tool invocations sequentially. Supports stopOnError to halt on first failure, includeObservations ('all', 'none', 'failures') to control observations, and batchTimeoutMs to set an overall deadline (remaining steps are skipped on timeout). Accepts tool aliases like navigate_home / navigate-home. Returns per-step results with timing. |
The core uses Playwright's ariaSnapshot() to build a deterministic reference map of interactive elements. Each element gets a short ref like e1, e2, etc., mapped to an ARIA selector.
Agents call describe_screen to get the current reference map, then use refs for interaction:
mm describe-screen → { ..., a11y: [{ ref: "e1", role: "button", name: "Submit" }, ...] }
mm click e1 → clicks the "Submit" button
mm type e3 "hello" → types into the element mapped to e3
This accessibility-first approach provides reliable element targeting that survives minor UI changes.
The KnowledgeStore provides cross-session learning by recording every tool execution as a structured step record:
- Step Recording: Each tool invocation captures the tool name, input, outcome, observation (extension state, visible test IDs, a11y nodes), and timing.
- Session Metadata: Sessions are tagged with goals, flow tags, and free-form tags for filtering.
- Prior Knowledge: Before tool execution, the store can generate context from historical sessions — similar steps, suggested actions, and patterns to avoid — based on the current screen state.
- Search: Token-based search with synonym expansion across sessions, scored by relevance to screen, URL, test IDs, and a11y nodes.
- Sensitive Data Handling: Input text for password fields and other sensitive inputs is automatically redacted.
Knowledge artifacts are stored on disk at test-artifacts/llm-knowledge/ organized by session ID.
The package supports two environment modes via discriminated union configuration:
E2E Testing — Full test infrastructure with local chain, fixtures, and contract seeding:
const e2eConfig: E2EEnvironmentConfig = {
environment: 'e2e',
extensionName: 'MetaMask',
defaultPassword: 'password123',
artifactsDir: './test-artifacts',
defaultChainId: 1337,
ports: { anvil: 8545, fixtureServer: 12345 },
};Production-like — Minimal configuration without test infrastructure:
const prodConfig: ProdEnvironmentConfig = {
environment: 'prod',
extensionName: 'MetaMask',
};Use set_context / get_context tools to switch between modes at runtime (requires no active session).
The createServer() function accepts a ServerConfig object:
type ServerConfig = {
/** Session manager instance (required) */
sessionManager: ISessionManager;
/** Factory function to create workflow context (may be sync or async) */
contextFactory: () => WorkflowContext | Promise<WorkflowContext>;
/** Idle timeout in milliseconds (optional, defaults to 1_800_000 = 30 min) */
idleShutdownMs?: number;
/** Per-request execution timeout in milliseconds (default: 30_000) */
requestTimeoutMs?: number;
/** Path to log file (optional) */
logFilePath?: string;
};The contextFactory is called once during start(). It is responsible for allocating any sub-service ports and returning a WorkflowContext. The core validates the returned shape at runtime — config.environment must be a string and every value in allocatedPorts (if provided) must be a finite number.
The allocatePort() utility is exported as a convenience for consumers who need ephemeral port allocation inside their factory.
The returned ServerInstance exposes:
start(): Promise<DaemonState>— CallscontextFactory, starts HTTP server, writes.mm-serverstate, sets up idle timeout and signal handlers.stop(): Promise<void>— Stops accepting connections, cleans up session, removes.mm-serverstate.
The daemon exposes the following endpoints on 127.0.0.1:
| Method | Path | Description |
|---|---|---|
GET |
/health |
Health check with nonce verification |
GET |
/status |
Daemon status (PID, port, uptime, sub-ports) |
POST |
/launch |
Start a browser session |
POST |
/cleanup |
Stop the current browser session |
POST |
/tool/:name |
Execute a registered tool with JSON body |
All responses follow a consistent shape:
// Success
{ ok: true, result: T, observations?: { state, testIds, a11y } }
// Error
{ ok: false, error: { code: string, message: string } }The observations field is included for mutating tools (click, type, navigate, launch, cleanup, build, etc.) and for run_steps when its includeObservations parameter is 'all' (default) or 'failures'. Read-only and discovery tools omit observations from the response.
The mm CLI provides a unified interface for agents and developers. All commands communicate with the daemon over HTTP — the daemon is auto-started on mm launch if not already running.
| Option | Description |
|---|---|
--project <path> |
Target a specific project directory (absolute or relative). Overrides MM_PROJECT and git-based discovery. |
| Environment Variable | Description |
|---|---|
MM_PROJECT |
Default project directory when --project is not provided. Falls back to the current git worktree root. |
By default, the CLI resolves the target project from the current git worktree. This works when running from inside the project directory. For other scenarios, the resolution order is:
--project <path>— Explicit flag, highest priority. Accepts absolute or relative paths.MM_PROJECT— Environment variable. Useful for setting once in agent config or shell profile.- Git worktree —
git rev-parse --show-toplevelfrom the current working directory (existing behavior).
# From inside the project (unchanged)
mm launch
# From a parent folder containing multiple repos
mm --project ./metamask-extension launch
# Via environment variable
export MM_PROJECT=/path/to/metamask-extension
mm describe-screen| Command | Description |
|---|---|
mm launch [--context e2e|prod] [--state default|onboarding|custom] [--extension-path <path>] [--goal <text>] [--force] [--flow-tags <tags>] |
Auto-starts the daemon if needed, then launches a headed Chrome session with the configured extension. Use --context to set the environment context before launching. Use --state to control wallet initialization. Use --extension-path to override the extension directory. Use --goal and --flow-tags for knowledge tagging. Use --force to replace an existing session. |
mm cleanup [--shutdown] |
Stops the browser, tears down test services (fixture server, Anvil, mock server), and releases session resources. Add --shutdown to also terminate the daemon process. |
mm stop [--force] |
Stops the daemon process (symmetric to mm serve). Sends a best-effort cleanup before shutdown. Use --force to remove stale .mm-server state from crashed daemons. |
mm status |
Displays the daemon's current status: PID, port, uptime, allocated sub-ports, and whether a browser session is active. |
mm serve [--background] |
Manually starts the HTTP daemon without launching a browser session. Use --background to detach the process. Fails if a daemon is already running for this worktree. |
| Command | Description |
|---|---|
mm click <ref> [--selector <css>] [--testid <id>] [--within <scope>] |
Clicks an element by its accessibility reference (e.g., e3). The ref comes from a prior describe-screen call. Waits for the element to be visible before clicking. Use --within to scope the target inside a parent element (testid:<id>, selector:<css>, or a bare a11y ref). |
mm type <ref> <text> [--selector <css>] [--testid <id>] [--within <scope>] |
Types text into an input element identified by its accessibility reference. Clears the field first, then sets the new value (uses Playwright's fill()). Use --within to scope the target inside a parent element. |
mm get-text <ref> [--selector <css>] [--testid <id>] [--within <scope>] |
Reads the text content of an element. Returns the inner text, target descriptor, and character length. Useful for asserting visible values without screenshots. |
mm describe-screen |
Captures the full screen state: extension info, visible test IDs, a trimmed accessibility tree with deterministic refs (e1, e2, ...), and prior knowledge from historical sessions. This is the primary command for understanding what's on screen before interacting. |
mm screenshot [--name <name>] |
Takes a full-page screenshot of the current page. Saves to the artifacts directory. Use --name to set a descriptive filename. |
mm wait-for <ref> [--timeout <ms>] [--selector <css>] [--testid <id>] [--within <scope>] |
Blocks until an element identified by its accessibility reference becomes visible, or the timeout expires. Default timeout is 15 seconds. Use --within to scope the target inside a parent element. |
mm wait-for-notification [--timeout <ms>] |
Waits for the extension notification popup to appear within a timeout. Returns the notification page URL. |
mm clipboard <read|write> [text] |
Reads from or writes to the system clipboard via Chrome DevTools Protocol. Useful for pasting seed phrases or copying addresses. |
| Command | Description |
|---|---|
mm navigate <url> |
Opens a new tab and navigates to the given URL. Useful for navigating to dApps or external pages. |
mm navigate-home |
Navigates the extension tab to the wallet home screen. |
mm navigate-settings |
Navigates the extension tab to the settings page. |
mm switch-to-tab <role> | --role <role> | --url <url> |
Switches the active page to a tab matching a given role (e.g., extension, dapp) or URL prefix. Supports a positional role as first argument. |
mm close-tab --role <role> | --url <url> |
Closes a browser tab matching a given role or URL. Falls back to the extension tab if the active tab is closed. |
| Command | Description |
|---|---|
mm get-state |
Returns the current extension state: loaded status, current URL, screen name, network, chain ID, account address, and balance. Also lists all tracked browser tabs. |
mm get-context |
Returns the current environment context (e2e or prod), session status, available capabilities, and whether context switching is allowed. |
mm set-context <e2e|prod> |
Switches the session environment between e2e and prod modes. Blocked while a session is active — run mm cleanup first. |
| Command | Description |
|---|---|
mm knowledge-search <query> |
Searches the knowledge store for past tool invocations matching the query. Results are scored by relevance to screen, URL, test IDs, and a11y nodes. |
mm knowledge-last |
Retrieves the most recent step records from the current session's knowledge store. |
mm knowledge-sessions |
Lists recent knowledge sessions with metadata (goal, flow tags, timestamps). |
mm knowledge-summarize [--session <id>] |
Generates a recipe-style summary of a session's tool invocations, showing the step sequence with targets and outcomes. |
| Command | Description |
|---|---|
mm run-steps <json> |
Executes a batch of tool invocations sequentially from a JSON definition. Each step specifies a tool name and arguments. |
For the full agent-facing reference and workflow guidelines, see SKILL.md.
Tool errors are classified into specific error codes for structured handling:
| Code | Meaning |
|---|---|
MM_TARGET_NOT_FOUND |
Element not found by ref, testId, or selector |
MM_WAIT_TIMEOUT |
Timeout waiting for element or condition |
MM_CLICK_FAILED |
Click operation failed |
MM_TYPE_FAILED |
Type operation failed |
MM_NAVIGATION_FAILED |
Navigation error or network failure |
MM_PAGE_CLOSED |
Browser page was closed unexpectedly |
MM_NOTIFICATION_TIMEOUT |
Notification popup did not appear |
MM_TAB_NOT_FOUND |
Tab not found by role or URL |
MM_DISCOVERY_FAILED |
Discovery tool failure |
MM_SCREENSHOT_FAILED |
Screenshot capture failure |
MM_BATCH_TIMEOUT |
batchTimeoutMs deadline exceeded in run_steps |
MM_CONTRACT_NOT_FOUND |
Unknown contract name |
MM_SEED_FAILED |
Contract deployment failure |
MM_CONTEXT_SWITCH_BLOCKED |
Context switch while session is active |
yarn build # Build the package
yarn test # Run tests and type checks
yarn lint # Lint everything
yarn lint:fix # Auto-fix lint issues(MIT OR Apache-2.0)