Skip to content

anyrust/aphtml

Repository files navigation

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

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors