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

aphtml

v0.2.1

Published

AI-first TypeScript SDK for dynamic WebView development on Android & iOS. Designed for LLM tool-calling: create, update, search, and reuse HTML pages and CSS/JS modules in mobile WebViews.

Readme

aphtml

AI-first TypeScript SDK for dynamic WebView development on Android & iOS.

aphtml lets an LLM agent (or a human developer) rapidly create, update, search, and reuse HTML pages and CSS/JS modules inside mobile WebViews — all through a simple tool-calling interface.

Born from the same philosophy as sfhtml (single-file HTML AI-skill CLI), but designed specifically for mobile WebView and LLM tool-calling instead of desktop CLI usage.


Why aphtml?

| Problem | aphtml solution | |---|---| | sfhtml is desktop-only (Rust CLI) | Pure TypeScript SDK, works anywhere | | AI can't reuse existing CSS/JS on mobile | Module registry with AI-readable headers | | LLMs need structured interfaces | 23 MCP/OpenAI-compatible tool schemas | | Native bridge varies (RN, Capacitor, iOS, Android) | Pluggable BridgeAdapter interface | | Storage varies per platform | Pluggable StorageAdapter interface | | No undo / version history for pages | Git-style version history with branch support | | Page WebView can't call back to instance | Bridge RPC Proxy — zero-token bidirectional channel | | Full HTML output wastes tokens on small edits | Unified diff updates — AI sends only changed lines |


Install

npm install aphtml

Quick Start

import { ApHtml, MemoryStorage } from "aphtml";
import { MyBridge } from "./my-bridge"; // your BridgeAdapter implementation

const ap = new ApHtml({
  storage: new MemoryStorage(),
  bridge: new MyBridge(),
});

// 1. Register a reusable CSS module
await ap.exec("create_module", {
  name: "theme",
  type: "css",
  code: `:root { --color-primary: #6200ee; --color-surface: #fff; }`,
  purpose: "Global color tokens",
  exports: ["--color-primary", "--color-surface"],
  tags: ["theme", "colors"],
});

// 2. Create a page that uses it
await ap.exec("create_page", {
  name: "home",
  html: `<!DOCTYPE html>
<html>
<head>
  <link rel="stylesheet" href="theme.css">
</head>
<body>
  <h1 style="color: var(--color-primary)">Hello!</h1>
</body>
</html>`,
  summary: "Home screen",
  modules: ["theme.css"],
  show: true, // immediately display in WebView
});

LLM Tool Interface

Get MCP/OpenAI-compatible tool schemas:

const tools = ap.getTools(); // 23 tool definitions

// Execute a tool call from an LLM:
const result = await ap.exec(toolName, toolArgs);
// result: { ok: boolean, data?: unknown, error?: string }

MCP Integration

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";

const server = new Server({ name: "aphtml", version: "0.2.1" }, { capabilities: { tools: {} } });

server.setRequestHandler(CallToolRequestSchema, async (req) => {
  const result = await ap.exec(req.params.name, req.params.arguments ?? {});
  return {
    content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
  };
});

OpenAI Function Calling

const completion = await openai.chat.completions.create({
  model: "gpt-4o",
  tools: ap.getTools().map((t) => ({ type: "function", function: t })),
  messages: [{ role: "user", content: "Create a login page with our company theme" }],
});

for (const call of completion.choices[0].message.tool_calls ?? []) {
  const result = await ap.exec(call.function.name, JSON.parse(call.function.arguments));
  console.log(result);
}

Available Tools

Page Tools

| Tool | Description | |---|---| | create_page | Create a new HTML page; auto-injects AP-PAGE-HEADER. Returns version "0". | | update_page | Full replace or unified diff partial update (saves tokens). Auto-increments version. Optional from_version for branching. | | show_page | Display page in WebView via bridge | | get_page | Read full HTML + version history. Optional version param to retrieve a specific version. | | list_pages | List all pages with metadata, versions | | search_pages | Search by keyword | | delete_page | Remove a page (all versions) |

Module Tools

| Tool | Description | |---|---| | create_module | Register a reusable CSS/JS module; auto-injects AP-MODULE-HEADER | | update_module | Replace module code, preserving header | | get_module | Read full source code | | list_modules | List all modules with metadata | | delete_module | Remove a module |

Diagnostic Tools

| Tool | Description | |---|---| | deps_scan | Reverse dependency lookup — given a module name, find all pages and modules that reference it |


Module Headers (AI-readable metadata)

Every module gets an AP-MODULE-HEADER comment block so the AI understands what already exists before writing new code. This mirrors sfhtml's AI-SKILL-HEADER design.

CSS example:

/* AP-MODULE-HEADER START
# Module: theme
## Type: css
## Purpose
Global color tokens and typography
## Exports
- --color-primary
- --color-surface
## Tags
theme, colors, typography
AP-MODULE-HEADER END */

:root {
  --color-primary: #6200ee;
}

JS example:

/* AP-MODULE-HEADER START
# Module: chart-utils
## Type: js
## Purpose
Chart rendering helpers using Canvas API
## Exports
- function renderBarChart(data, container)
- function renderLineChart(data, container)
## Tags
chart, visualization, canvas
AP-MODULE-HEADER END */

export function renderBarChart(data, container) { ... }

Page header (HTML):

<!-- AP-PAGE-HEADER START
# Page: dashboard
## App: MyApp
## Purpose
User dashboard showing key metrics
## Modules
- theme.css
- chart-utils.js
## Changelog
- 2026-03-22: initial
AP-PAGE-HEADER END -->

Page Version History (v0.2.0)

Every page maintains a git-style linear version history with branch support. No new tools needed — version info flows through existing responses.

// create_page always starts at version "0"
await ap.exec("create_page", { name: "my-app", html: "..." });
// → version "0"

// update_page auto-increments
await ap.exec("update_page", { name: "my-app", full_html: "..." });
// → version "1"
await ap.exec("update_page", { name: "my-app", full_html: "..." });
// → version "2"

// Branch from a specific version
await ap.exec("update_page", { name: "my-app", full_html: "...", from_version: "1" });
// → version "1.0" (branch from v1)

// Get a specific version
const v0 = await ap.exec("get_page", { name: "my-app", version: "0" });

// Revert workflow: get old version, update with its content
const old = await ap.exec("get_page", { name: "my-app", version: "1" });
await ap.exec("update_page", { name: "my-app", full_html: old.data.html });
// → version "3" (content of v1)

Version info is included in every response:

{
  "ok": true,
  "data": {
    "name": "my-app",
    "html": "...",
    "meta": {
      "version": "2",
      "parentVersion": "1",
      "versions": [
        { "version": "0", "parentVersion": null, "updatedAt": "..." },
        { "version": "1", "parentVersion": "0", "updatedAt": "..." },
        { "version": "2", "parentVersion": "1", "updatedAt": "..." },
        { "version": "1.0", "parentVersion": "1", "updatedAt": "..." }
      ]
    }
  }
}

Bridge RPC Proxy (v0.2.0)

Pages can call back to the cloud instance via __aphtml.call() — a zero-token bidirectional channel. The SDK proxies RPC calls through the existing WebSocket, so page HTML never contains credentials.

Page WebView                     aphtml SDK                        Instance Daemon
────────────                     ──────────                        ───────────────
__aphtml.call('fs.read',         bridge.onMessage(msg)
  {path:'/tmp/x'})         ──►    filter() → transport.call()  ──►  daemon handles
  ◄── __aphtml._rpcResolve()   ◄── bridge.evalScript()          ◄──  result

Inside page HTML:

<script>
  const content = await __aphtml.call('fs.read', { path: '/tmp/data.json' });
  const result = await __aphtml.call('agent.execute', { prompt: 'Analyze...' });
</script>

Host app setup (whitelist required, secure by default):

const { bridge, rpc } = ap.connectInstance(transport, {
  rpcFilter: (pageId, method) => ['fs.read', 'agent.execute'].includes(method),
});

Adapters

Storage Adapter

Provide your own storage for persistence on device:

import type { StorageAdapter, ApPage, ApModule, PageMeta, ModuleMeta } from "aphtml";

// Example: Capacitor Filesystem
export class CapacitorStorage implements StorageAdapter {
  async getPage(name: string): Promise<ApPage | null> { /* ... */ }
  async savePage(page: ApPage): Promise<void> { /* ... */ }
  async deletePage(name: string): Promise<boolean> { /* ... */ }
  async listPages(): Promise<PageMeta[]> { /* ... */ }

  async getModule(name: string): Promise<ApModule | null> { /* ... */ }
  async saveModule(mod: ApModule): Promise<void> { /* ... */ }
  async deleteModule(name: string): Promise<boolean> { /* ... */ }
  async listModules(): Promise<ModuleMeta[]> { /* ... */ }
}

LiveBridge — Direct interaction with a running WebView

The built-in LiveBridge uses a local WebSocket server to talk to live WebViews in real time, with zero native code required.

[aphtml SDK / LLM / MCP server]  ◄─ WebSocket ─►  [WebView displaying HTML page]
         LiveBridge :4321                              aphtml-live.js (auto-injected)

Usage:

import { ApHtml, MemoryStorage, LiveBridge } from "aphtml";

// Start WebSocket server on port 4321
const bridge = new LiveBridge(4321);
// Physical device on same LAN: new LiveBridge(4321, { host: "192.168.1.42" })

const ap = new ApHtml({
  storage: new MemoryStorage(),
  bridge,
});

// Create a page – aphtml auto-injects the live-client script when showing
await ap.exec("create_page", {
  name: "dashboard",
  html: `<html><body><div id="chart">Loading...</div></body></html>`,
});

// Show it (pushes full HTML to any connected WebView)
await ap.exec("show_page", { name: "dashboard" });

// Later – LLM patches just the chart element via diff, live, no page reload
await ap.exec("update_page", {
  name: "dashboard",
  diff: [
    "@@ -1,1 +1,1 @@",
    '-<div id="chart">Loading...</div>',
    '+<div id="chart"><canvas id="c"></canvas></div>',
  ].join("\n"),
});

// Or run JS directly in the live WebView
await ap.exec("update_page", {
  name: "dashboard",
  eval_script: `renderBarChart(window.__data, document.getElementById('c'))`,
});

// Listen for events sent back from inside the WebView
bridge.onMessage((msg) => {
  console.log("WebView says:", msg.type, msg.data);
  // msg = { type: "user:click", pageId: "dashboard", data: { id: "buy-btn" } }
});

// When done
await bridge.close();

Inside the WebView page, send events back to the host:

// (the live-client script sets up window.__aphtml automatically)
window.__aphtml.post("user:click", { id: "buy-btn" });
window.__aphtml.post("form:submit",  { name: "Alice", age: 30 });

WebSocket protocol (aphtml-server → WebView):

| Message | Effect | |---|---| | { type: "full", html } | Replace entire page (document.open/write) | | { type: "eval", script } | Execute arbitrary JS in the page | | { type: "close" } | Navigate back / close tab |

Platform support:

  • Simulator/emulator → ws://localhost:PORT (default)
  • Physical device (same WiFi) → ws://192.168.x.x:PORT via { host: "..." } option
  • Works with any WebView that can make WebSocket connections (React Native WebView, WKWebView, Android WebView, Capacitor Browser, desktop Electron)

Manual BridgeAdapter

If you need to hook into the native WebView API (e.g. injectJavaScript on a React Native ref), implement BridgeAdapter directly:

import type { BridgeAdapter, BridgeMessage } from "aphtml";

// Example: React Native WebView
export class RNBridge implements BridgeAdapter {
  async showPage(pageId: string, html: string): Promise<void> {
    // navigate to WebView screen and set HTML source
  }
  async patchPage(pageId: string, selector: string, html: string): Promise<boolean> {
    // Low-level DOM patch (still available for direct bridge usage)
    // ref.current.injectJavaScript(`document.querySelector(...).outerHTML = ...`)
    return true;
  }
  async evalScript(pageId: string, script: string): Promise<unknown> {
    // ref.current.injectJavaScript(script)
    return undefined;
  }
  async closePage(pageId: string): Promise<void> { /* ... */ }
  onMessage(handler: (msg: BridgeMessage) => void): () => void {
    // WebView onMessage => handler({ type, data })
    return () => {}; // unsubscribe
  }
}

Cloud Instance — Remote Page Sync

Connect a cloud AI instance (e.g. OpenClaw) so it can push page mutations to your frontend in real time via JSON-RPC 2.0 over WebSocket.

┌─────────────┐                        ┌─────────────────┐
│ Frontend App│                        │  Cloud Instance  │
│  ┌─────────┐│   DaemonTransport      │  (OpenClaw)      │
│  │ aphtml   ││◄══════════════════════►│  daemon process  │
│  │ PageReg  ││   JSON-RPC 2.0 WS     │  AI can call:    │
│  │ Bridge   ││                        │  page.create     │
│  │ Tools    ││                        │  page.update     │
│  └─────────┘│                        │  page.show       │
│  ┌─────────┐│                        │  page.delete     │
│  │ WebView  ││                        └─────────────────┘
│  └─────────┘│
└─────────────┘

DaemonTransport

JSON-RPC 2.0 client over WebSocket with auto-reconnect, call queuing, heartbeat, and auth token injection.

import { DaemonTransport } from "aphtml";

const transport = new DaemonTransport({
  url: "ws://10.0.2.2:9000/rpc",
  autoReconnect: true,
  callTimeout: 30000,
  getToken: async () => auth.getToken(),
});
await transport.connect();

Works in Node.js (ws package) and React Native / browser (native WebSocket) automatically.

connectInstance

Connect a single cloud instance and receive page mutations:

const { bridge, rpc } = ap.connectInstance(transport, {
  filter: (method, params) => allowedPages.has(params.name as string),
  rpcFilter: (pageId, method, params) => allowedRpcMethods.has(method),
});

// Now the cloud instance can push pages:
//   { method: "page.create", params: { name: "dashboard", html: "..." } }
//   { method: "page.update", params: { name: "dashboard", diff: "@@ -1,1 +1,1 @@\n-...\n+..." } }
//   { method: "page.show",   params: { name: "dashboard" } }

// And pages can call back to the instance (via rpcFilter whitelist):
//   __aphtml.call('fs.read', { path: '/tmp/data.json' })  → proxied to daemon

// Later:
bridge.stop();
rpc.stop();

Group Chat — Multi-Instance Coordination

Initialize a group of AI instances with a coordinator. The frontend sends one group.init RPC to the coordinator, then only receives a unified page stream — all instance-to-instance coordination happens server-side.

  Frontend                  Coordinator              Other Instances
  ┌──────┐  group.init      ┌──────────┐  RPC       ┌──────────┐
  │aphtml ├────────────────►│ Instance A├───────────►│ Instance B│
  │      │  page.update     │(coordinator)│◄──────────│ (finance) │
  │      │◄─────────────────│          ├───────────►│ Instance C│
  └──────┘  unified stream  └──────────┘            │ (editor)  │
                                                     └──────────┘

initGroup

import { ApHtml, MemoryStorage, DaemonTransport } from "aphtml";
import type { GroupConfig } from "aphtml";

const ap = new ApHtml({ storage: new MemoryStorage(), bridge: myBridge });
const transport = new DaemonTransport({ url: "ws://coordinator:8080/rpc" });
await transport.connect();

const { bridge, rpc, result } = await ap.initGroup(transport, {
  coordinator: "main",
  members: [
    { role: "main",    host: "10.0.1.1", wsPort: 8080 },
    { role: "finance", host: "10.0.1.2", wsPort: 8081 },
    { role: "editor",  host: "10.0.1.3", wsPort: 8082 },
  ],
  prompt: "Build a real-time stock + news dashboard",
  output: {
    pages: ["dashboard"],
    format: "html",
    streaming: true,
  },
});

console.log(result.ok);               // true
console.log(result.groupId);           // "grp-001"
console.log(result.connectedMembers);  // 3

// Coordinator now assembles data from finance + editor instances
// and pushes unified page.update notifications to this frontend.

// Later:
bridge.stop();

Group Types

import type {
  GroupMember,        // { role, host, wsPort, meta? }
  GroupConfig,        // { coordinator, members, prompt, output? }
  GroupOutputConfig,  // { pages?, format?, streaming? }
  GroupInitResult,    // { ok, groupId?, connectedMembers?, error? }
} from "aphtml";

Registries

NavRegistry — Navigation Slots & Page Templates

Manage navigation slots (tabs, sidebar items) and page templates:

const nav = ap.navRegistry;
nav.setSlot({ id: "home", label: "Home", page: "home", icon: "house" });
nav.setSlot({ id: "settings", label: "Settings", page: "settings" });
const slots = nav.getSlots(); // all navigation items

LogRegistry — Structured Observability

Capture structured log entries from tools, bridges, and remote mutations:

const logs = ap.logRegistry;
logs.push({ level: "info", message: "Page created", pageId: "dashboard", trigger: "tool", ts: new Date().toISOString() });
const recent = logs.query({ level: "error", limit: 50 });

ToolRegistry — Dynamic Tools & System Prompt

Register dynamic tools at runtime and build LLM system prompts:

const tools = ap.toolRegistry;

// Register a custom tool
tools.register({
  schema: { name: "get_weather", description: "Get current weather", parameters: { ... } },
  execute: async (args) => ({ ok: true, data: { temp: 22 } }),
});

// Build system prompt with all available tools
const prompt = tools.buildSystemPrompt(ap.getSkillDoc());

// Export as LLM tool definitions
const llmTools = tools.toLLMToolDefs();

Testing

npm test

Uses MockBridge and MemoryStorage for fully offline, fast tests:

import { ApHtml, MemoryStorage, MockBridge } from "aphtml";

const bridge = new MockBridge();
const ap = new ApHtml({ storage: new MemoryStorage(), bridge });

await ap.exec("create_page", { name: "test", html: "<html>...</html>" });
await ap.exec("show_page", { name: "test" });

expect(bridge.calls[0].method).toBe("showPage");
bridge.simulateMessage({ type: "user:click", data: { id: "btn" } });

Recommended Agent Workflow

1. list_modules        → understand available assets
2. search_pages        → check if similar page exists
3. get_page            → read existing page before editing (includes version history)
4. create_module       → add new CSS/JS if needed
5. create_page         → build new page using existing modules (version "0")
6. deps_scan           → reverse-check which pages/modules reference a module
7. show_page           → render in WebView
8. update_page         → apply AI-driven corrections live (auto-increments version)
9. get_page(version)   → retrieve a previous version to revert or branch

Architecture

aphtml/
  src/
    types.ts              Core interfaces + Group/Transport/Cloud types
    header.ts             AP-PAGE-HEADER / AP-MODULE-HEADER parser & generator
    deps-scanner.ts       Scan HTML deps (mirrors sfhtml module_deps.rs)
    tools.ts              23 LLM-callable tool schemas + executor
    index.ts              Public API: ApHtml class (exec, connectInstance, initGroup)
    storage/
      adapter.ts          StorageAdapter interface
      memory.ts           MemoryStorage (built-in, volatile, version-aware)
    bridge/
      adapter.ts          BridgeAdapter interface + BRIDGE_CLIENT_JS snippet (with RPC)
      mock.ts             MockBridge for testing
      live-bridge.ts      LiveBridge — WebSocket real-time bridge
      remote-page-bridge.ts  RemotePageBridge — cloud → frontend page sync
      rpc-proxy.ts        BridgeRpcProxy — Page ↔ Instance bidirectional RPC
    transport/
      daemon-transport.ts JSON-RPC 2.0 over WebSocket (auto-reconnect, auth)
      mock.ts             MockTransport for testing
    registry/
      page-registry.ts    PageRegistry (CRUD + show + search)
      module-registry.ts  ModuleRegistry (CRUD + search + summarize)
      nav-registry.ts     NavRegistry (navigation slots + page templates)
      log-registry.ts     LogRegistry (structured logging + query)
      tool-registry.ts    ToolRegistry (dynamic tools + system prompt builder)
    cloud/
      cloud-manager.ts    CloudManager — machine lifecycle via RPC
    auth/
      mock.ts             MockAuth for testing
    file/
      file-processor.ts   FileProcessor — multi-provider text extraction
    vnc/
      mock.ts             MockVnc for testing

License

MIT