Verbs

agentproto app

agentproto app install <appDir|url|file.agentapp|appId> [--ref <ref>] [--subdir <path>] [--data-dir <path>]
agentproto app resync <appId>
agentproto app list
agentproto app catalog [--refresh] [--json]
agentproto app uninstall <appId> [--json]
agentproto app update [<appId>|--all] [--dry-run] [--json]
agentproto app store [--print]
agentproto app pack   <appDir> [--out <path.agentapp>] [--release] [--json]
agentproto app unpack <file.agentapp> [--dir <outDir>] [--json]
agentproto app serve  [appDir] [--port <n>] [--app <appId>] [--json]
                      [--remote-mcp-url <url>] [--remote-mcp-auth <token>]
                      [--remote-app-id <appId>]
agentproto app build  <appDir> [--json]
agentproto app dev    <appDir> [--port <n>] [--json] [-- <viteArgs...>]
agentproto app init   <template> [dir]
agentproto app validate [dir] [--json]

This page covers the agentproto app CLI verb (install/serve/pack/build/ dev). For what a bundled agent can actually reach at runtime — app_run, and which tool ids an AGENT.md can declare — see Which tools can an app agent call?.

Bundle an agentproto app folder (one carrying a valid .agentproto/APP.md) into a single self-contained .agentapp tar.gz — the "APK for agentproto apps" — and unpack that bundle back into a folder, verifying an aggregate SHA-256 before restoring. Entirely local and dependency-free (system tar): no daemon, no network. build and dev compile/run an app's optional ui/ source project (Vite + TypeScript) into/against the static .agentproto/ui/ that serve, pack, and the MCP-Apps panel actually consume — see Optional ui/ source project below.

An app folder is any directory with .agentproto/APP.md plus the agents (.agentproto/agents/<id>/AGENT.md), workflows (.agentproto/workflows/<id>/WORKFLOW.md), and optional UI (.agentproto/ui/) it references — the shape defineApp().emit(dir) (@agentproto/app-kit) produces. The bundle walks the WHOLE folder (including .agentproto/ and any loose workspace files, but skipping any node_modules/ or .git/ directory at any depth — a ui/ source tree ships both and neither belongs in the shipped app), so unpacking restores the exact tree and relative paths that readAppRefs / app_install depend on survive the round-trip. The APP.md package block narrows what ships, and pack --release drops dev-only files and the build step (see pack below).

Subverbs

install <appDir|url|file.agentapp|appId> [--ref <ref>] [--subdir <path>] [--data-dir <path>]

Register the app (its id from .agentproto/APP.md) → <appDir> mapping in ~/.agentproto/apps.json, the same file the daemon's app_install writes, so agentproto app serve --app <id> and the daemon can resolve it. Idempotent: re-running for the same id updates the entry.

If APP.md's ui block declares a build ({ command, cwd?, sources? }), app_install builds the UI bundle first when it's missing or older than the newest matching sources file — see ui.build below. No ui.build and a missing ui.path fails install with a clear error naming the path, same as before this existed.

FlagDefaultDescription
--data-dir <path>see belowWhere the app's durable data — everything app_data_read / app_data_write / app_data_list touch — lives. Absolute, ~-relative, or relative to <appDir>. This is what keeps multi-GB generated output out of the app's source tree.

The registered data dir is resolved, in order: --data-dir → the entry's previously registered data dir (a bare re-install never moves an app's data) → the APP.md data.dir frontmatter hint (relative to <appDir>) → <appDir>/data. It is stored absolute; the daemon's app_install {dir, dataDir} follows the same precedence. A git URL or .agentapp install, whose app dir is replaced on every reinstall, falls back to ~/.agentproto/app-data/<url-encoded appId> instead of <appDir>/data (existing installs keep their recorded data dir).

How paths resolve against it (the daemon's rule, packages/runtime/src/app-data.ts):

  1. An app-relative path resolves under the data dir.
  2. Under the default layout (<appDir>/data) a leading data/ is the legacy spelling from when the plane was anchored at <appDir> and is dropped — data/trips/x.json and trips/x.json name the same file.
  3. If a path (or its top-level folder) does not exist under the data dir but does under <appDir>, it resolves there — files written by a pre-data-dir install keep working, and app_data_list merges both views. Move the folder into the data dir and the fallback stops applying.

Installing from a git URL or a .agentapp

A git URL (https://…, git@…, file://…) or a .agentapp (an https:// / file:// URL, or a local path) is installed by the running daemon (app_install {url, ref?, subdir?} / {file}), so start the daemon first:

  • git: shallow clone into <daemon state dir>/apps/<repo>[-<subdir>] (~/.agentproto/apps/…, never your cwd). --ref picks a branch or tag, --subdir the app's path inside the repo. The installed commit is pinned as source: { kind: "git", url, ref?, sha, subdir? }.
  • .agentapp: downloaded (30 s timeout, 200 MB cap) or read, digest verified exactly like unpack, then installed under <state dir>/apps/<id>. Pinned as source: { kind: "agentapp", url, sha256, version }. A digest mismatch refuses the install and leaves any previous install untouched.

Re-installing replaces the app dir atomically and keeps the app's data dir. app_list / app_status show source.

Installing by app id from the catalog

An @scope/name argument that is not an existing path is looked up in the daemon's app_catalog (the default catalog is https://agentproto.sh/catalog/v1/apps.json). The entry's pinned source is installed: {url, sha256} for a bundle, {url, ref, subdir, sha} for git, plus the catalog URL so update follows that catalog. A git entry still never runs ui.build without --allow-build. An existing path always wins; an unknown id fails and points at agentproto app catalog.

agentproto app install @agentik/session-chat

resync <appId>

Ask the daemon to re-check a git / .agentapp install against its source (git ls-remote vs the pinned sha; re-download vs the pinned sha256). Prints { "changed": false }, or reinstalls and prints { "changed": true, "from": …, "to": … }. Apps installed from a local directory have no source and error.

list

Print every registered app as id -> dir, each followed by its data dir (entries written before the field existed show <dir>/data). With nothing installed it prints No apps installed. Browse: agentproto app store (or: agentproto app catalog).

catalog [--refresh] [--json]

List the daemon's app_catalog: app id, version, tier, installed or not, update available, and which catalog source it came from, followed by any source warnings (unreachable source, stale cache). --refresh bypasses the daemon's 5-minute cache of remote sources. Needs the daemon.

uninstall <appId> [--json]

Remove an installed app's record from the daemon (app_uninstall). Its data dir is kept.

update [<appId>|--all] [--dry-run] [--json]

Without an app id, list the available updates (app_updates). With an app id (or --all), apply them (app_resync, which installs the current catalog entry with its digest verified). --dry-run only lists.

store [--print]

Open the daemon's App Store panel (<daemon url>/store) in the default browser; --print only prints the URL. See the App Store guide.

serve [appDir] [--port <n>] [--app <appId>] [--json]

Serve an agentproto app's UI as a standalone webapp with a working window.McpApp bridge wired to the daemon's /mcp endpoint. The same HTML dashboard that renders inside an MCP-Apps panel now runs in a plain browser tab with full MCP connectivity.

The UI root is resolved from APP.md frontmatter: when ui.path is declared (e.g. ui.path: ui/index.html), the directory containing that file is used as the UI root; when ui is absent, the legacy .agentproto/ui/ directory is used. When ui.build is declared, the bundle is built first if missing or stale (same as install, above); a missing resolved UI root with no ui.build (or a failing build) is a hard exit-2 error naming the path/command, not a silent 404.

ui.build — don't commit the generated bundle

ui:
  path: .agentproto/ui/index.html
  build:
    command: pnpm run build
    cwd: ui
    sources:
      - ui/src/**
      - ui/index.html
      - ui/vite.config.ts

command is a shell command line run with cwd cwd (default: the app dir); sources (default ["src/**"], relative to cwd) is compared by mtime against ui.path to decide staleness. app_install, this command, the daemon's GET /apps/:appId/ui, and the MCP panel all resolve it through the same ensureAppUiBuilt (@agentproto/runtime/app-ui-build), single-flight per bundle path so concurrent first requests build once. Output is captured to ~/.agentproto/logs/app-ui-build/<app>-<hash>.log (outside the app dir); a failing build surfaces that log's tail in the error instead of a bare 404. This is distinct from agentproto app build, which only knows the <appDir>/ui/ Vite-project convention and must be run by hand — ui.build is declarative (any command, any layout) and runs automatically.

FlagDefaultDescription
appDircurrent directoryDirectory holding .agentproto/APP.md. Ignored in remote mode (see below).
--app <appId>unsetServe an installed app by its registered id (from ~/.agentproto/apps.json, written by install) instead of a directory path. Mutually exclusive with appDir.
--port <n>PORT env, then ui.port in APP.md, else OS-assignedPort to bind. Resolution order: explicit --port > PORT env var > APP.md ui.port > OS-assigned. A declared ui.port that is already taken falls back to auto-assign; an explicit --port that is taken is a hard error. Not read in remote mode (no APP.md) — there --port or PORT env or auto-assign applies.
--remote-mcp-url <url>unsetStreamable-HTTP MCP endpoint of a remote server (e.g. https://api.example.com/mcp). Setting this enables remote mode (see below). Env: AGENTPROTO_REMOTE_MCP_URL.
--remote-mcp-auth <token>unsetBearer token sent as the Authorization header on every MCP request to the remote server. Env: AGENTPROTO_REMOTE_MCP_AUTH.
--remote-app-id <appId>unsetThe MCP-Apps app id to render in remote mode, or a full ui://… resource URI. A bare id is fetched as ui://<appId>. Env: AGENTPROTO_REMOTE_APP_ID.
--jsonfalsePrint { url, appDir, daemonMcpUrl } (or, in remote mode, { url, mode: "remote", appId, resourceUri, daemonMcpUrl }) on stdout instead of a human summary.

Start the daemon first (agentproto serve); the bridge proxies tool calls to http://127.0.0.1:<daemon.port>/mcp.

app serve also exposes a same-origin file-upload endpoint for app UIs: POST /__agentproto/upload?filename=<name> with the raw file bytes as the body. Files land in <appDir>/inbox/ with a sanitized, collision-avoided name, and the endpoint returns { path, bytes }. Uploads are capped at 200 MB.

Remote mode (--remote-mcp-url set): instead of serving a local app dir and proxying tool calls to the local daemon, app serve connects its MCP client to a REMOTE MCP server and renders one of ITS MCP-Apps ui:// resources as a browser tab. There is no local app directory in this mode — the HTML is fetched over MCP (readResource) and no ui.tools allowlist applies: every tool the remote server exposes is forwarded, with the loopback-only bind as the safety gate.

pack <appDir> [--out <path.agentapp>] [--release] [--json]

Reads <appDir>/.agentproto/APP.md, walks the entire app dir, computes an aggregate SHA-256 over every file, writes a manifest.json at the bundle root, and emits a gzipped tar archive containing manifest.json plus the app folder's contents (not a wrapping folder). Extraction therefore yields manifest.json, .agentproto/, and the loose files at top level.

FlagDefaultDescription
--out <path><safeId>-<version>.agentapp in cwdThe output .agentapp path. When omitted, derives a filesystem-safe filename from the app id and version (e.g. @agentproto/job-application-kit v0.1.0 → agentproto-job-application-kit-0.1.0.agentapp). With --entry, a directory means "into this dir", and a path ending in .agentapp is used as the bundle path itself.
--releasefalseBuild the publishable bundle (see below).
--entryfalseRequires --release. Also write a validated catalog entry next to the bundle (see below).
--asset-url <url>GitHub Releases URLWith --entry: the published bundle URL recorded in the entry's source.url.
--publisher <name>noneWith --entry: the entry's publisher field (APP.md store.publisher wins).
--media-base-url <url>https://raw.githubusercontent.com/agentproto/apps/main/media/<appId>/<version>With --entry: base URL of the store listing's local media (APP.md store: icon/screenshots), copied to media/<appId>/<version>/ next to the entry. See Store listing.
--jsonfalsePrint the generated manifest.json on stdout instead of a human summary (with --entry: the bundle path, entry file path, and the entry itself).

Fails with exit code 2 if <appDir> has no .agentproto/APP.md.

What ships. Every file except node_modules/ and .git/, filtered by the optional APP.md package block:

package:
  include: [".agentproto/**"]   # when set, only matching files ship
  exclude: ["notes/**"]         # matching files are dropped
  stripBuild: true              # drop ui.build from the packed APP.md

Globs are /-separated paths relative to the app dir: ** spans directories, * stays inside one segment (no braces, no negation). .agentproto/APP.md, the ui.path entry and the files in its sibling assets/ directory always ship.

--release is the mode for a bundle you publish:

  1. Runs ui.build first when the UI bundle is missing or stale (same check as app build / the daemon).
  2. Adds the default excludes to package.exclude (a declared exclude adds to these, it never removes them):
    • dev trees: ui/**, docs/**, data/**, scripts/**, and at the app root only dev/**, test/**, tests/** (so an agent named dev or test under .agentproto/agents/ still ships);
    • tests anywhere: **/__tests__/**, **/*.test.*, **/*.spec.*;
    • repo docs at the root: README.md, CHANGELOG.md, CONTRIBUTING.md (LICENSE and NOTICE ship);
    • tooling: .github/**, .vscode/**, .idea/**, .gitignore, .editorconfig, tsconfig*.json, vitest.config.*, jest.config.*, eslint.config.*, .eslintrc*, .prettierrc*;
    • artifacts: **/*.log, **/*.map, **/.DS_Store, **/.env, **/.env.*.
  3. Fails (missing-ui, exit 1) if the declared ui.path is not in the bundle after the build.
  4. Removes ui.build from the packed APP.md (unless package.stripBuild: false), so an install never runs a build command. The SHA-256 covers the rewritten APP.md, so always publish the digest of the release pack.

--entry writes the publishing pipeline's other half, a catalog entry for the bundle just packed:

  • Written next to the bundle as <slug>-<version>.entry.json, where <slug> is the last segment of the appId without its scope (@agentik/session-chat -> session-chat).
  • A validated AppCatalogEntry with tier: "bundle" and license: {kind: "free"}: appId/name/description come from APP.md, as do the optional category, icon, and placement; APP.md must declare a version or packing fails. source carries the bundle's manifest sha256 (exactly what unpack / app_install {sha256} verify), the bundle's byte size, version, and the url: --asset-url if given, else the GitHub Releases asset URL of the public agentproto/apps repo, tag <slug>@<version> (@ encoded %40), asset <slug>-<version>.agentapp.
  • Feed the entries to agentproto catalog build to produce the published apps.json; see distribute-an-app.

unpack <file.agentapp> [--dir <outDir>] [--json]

Extracts the bundle to a temp dir, reads and validates manifest.json (required, format: agentapp/v1), recomputes the aggregate SHA-256 over the listed files and compares it to the manifest — a mismatch means the bundle is corrupted and the command fails (exit 1) without restoring. On success it copies the app contents (excluding manifest.json, which is a bundle artifact, not part of the app) into the destination.

FlagDefaultDescription
--dir <dir><safeId>-<version> in cwdDest folder to restore into. When omitted, derives <safeId>-<version> from the manifest.
--jsonfalsePrint a machine-readable verification summary on stdout.

serve [appDir] [--port <n>] [--json]

Serves an agentproto app's UI as a standalone webapp with a working window.McpApp bridge, so the same UI that renders inside an MCP-Apps panel runs in a plain browser tab with full MCP connectivity. The UI root is resolved from APP.md frontmatter ui.path (directory containing the entry file), falling back to the legacy .agentproto/ui/. Port resolution: --port > PORT env var > the app's declared ui.port (APP.md frontmatter) > an OS-assigned free port. Requires the daemon (agentproto serve) to be running — the bridge forwards tool calls to its /mcp endpoint.

build <appDir> [--json]

Builds <appDir>/ui/ (the optional Vite UI source project — see Optional ui/ source project) into <appDir>/.agentproto/ui/, the static output serve/pack/the MCP-Apps panel consume.

  • No ui/package.json, or one with no scripts.build → no-op success (exit 0): the app is a hand-written static UI with nothing to compile. Human output: no ui build step — static UI passthrough. --json: {"built":false,"reason":"no-ui-project"} (missing ui/package.json) or {"built":false,"reason":"no-build-script"} (present but no scripts.build).
  • Otherwise runs <pm> run build with cwd <appDir>/ui, stdio inherited. The package manager is detected from a lockfile — pnpm-lock.yaml → pnpm, package-lock.json → npm, yarn.lock → yarn — checked in <appDir> first, then <appDir>/ui, defaulting to pnpm.
  • A non-zero build exit is a hard failure (exit 1). After a successful build, agentproto app build verifies <appDir>/.agentproto/ui/index.html exists; if the ui project's vite.config.ts doesn't emit there (outDir: "../.agentproto/ui"), that's also exit 1, with a hint.
  • --json success: {"built":true,"uiDir":"<abs .agentproto/ui path>"}.

Fails with exit code 2 if <appDir> has no .agentproto/APP.md.

dev <appDir> [--port <n>] [--json] [-- <viteArgs...>]

Runs <appDir>/ui/'s own dev server (<pm> run dev, same package-manager detection as build) with a live window.McpApp bridge — the app serve experience but with Vite's HMR instead of a static build. Requires <appDir>/ui/package.json to declare a scripts.dev; a hand-written static UI has nothing to hot-reload — use agentproto app serve instead (exit 2 otherwise).

Two servers run: the ui project's own dev server (whatever port it picks) and a bridge-only HTTP server this command owns — POST /__agentproto/tool-call plus OPTIONS/CORS, since the browser talks to the Vite dev origin, a different port than the bridge. --port sets the bridge server's port (default: OS-assigned). The dev server child is spawned with AGENTPROTO_BRIDGE_URL=http://127.0.0.1:<bridgePort> in its environment, so a scaffolded vite.config.ts can proxy /__agentproto to it. Extra args after -- are forwarded to <pm> run dev.

When APP.md declares ui.port (the same frontmatter serve reads) and no -- <viteArgs> were passed at all, dev appends --port <declared> to the <pm> run dev invocation itself, so the ui dev server's own URL is stable and matches the app's declared surface — this is unrelated to the --port flag above, which only controls the bridge server. Passing any explicit viteArgs disables the hint entirely: you're steering the dev server directly, so nothing gets merged on top of your flags.

Ctrl-C or the dev server child exiting tears both servers down; app dev exits with the child's exit code. --json prints {"bridgeUrl":"...","appDir":"..."} on one line before handing the terminal to the dev server.

init <template> [dir]

Scaffold [dir] (default: the current directory) from <template> — the same scaffoldApp operation pnpm create agentproto-app <dir> drives, so no second package install is needed. Refuses a non-empty target directory (exit 2, reason target-not-empty); an unknown template is also exit 2.

Templates:

TemplateShape
react-tsVite + TanStack Router/Query ui/ source project + .agentproto/ shell (the create-agentproto-app default)
vanillaminimal .agentproto/ shell + static UI
bookthe book-app trame (category book, library stub, install skill)
tramethe minimal AIP app trame — see below

The trame template emits everything validate (below) knows how to check, mirroring the book-factory app layout:

<dir>/.agentproto/APP.md                       (id from the dir slug, one agent, one workflow,
                                               ui.tools incl. app_state_get/app_state_list,
                                               verify.command: "node scripts/verify.mjs")
<dir>/.agentproto/agents/<slug>-agent/AGENT.md
<dir>/.agentproto/workflows/<slug>-flow/WORKFLOW.md
<dir>/.agentproto/workflows/<slug>-flow/prompts/run.md
<dir>/.agentproto/ui/index.html                (single file, window.McpApp-aware stage board)
<dir>/gates/example.mjs                        (exit 0 + one-line JSON report)
<dir>/scripts/verify.mjs                       (runs the gates, prints {ok, findings})
<dir>/data/DATA.md                             (the data-plane key dictionary)
<dir>/tests/gate.test.mjs                      (node --test suite)

The workflow ships ONE kind: agent step (harness-pinned: model, effort, role, promptFile — the file wins over the inline prompt) followed by ONE kind: gate step running node gates/example.mjs from the app root. A gate's cwd and args[] accept a LEADING $… run-time ref ($input|$item|$steps.<id>|$index) with trailing text — e.g. cwd: $input.bookDir or args: ["$input.bookDir/knowledge"]; a relative resolved cwd (incl. .) resolves against the run's own cwd.

validate [dir] [--json]

Check [dir] (default: the current directory) against the app loader. Exit 0 iff ALL of:

  1. loadAppHandle (@agentproto/app-kit) succeeds — APP.md parses, every AGENT.md loads, and the defineApp attachment invariant holds.
  2. Every declared workflow loads via @agentproto/workflow-loader — harness blocks, promptFile resolution, and kind: gate steps are validated there.
  3. Every ui.tools entry is a tool the daemon registers or any app_* tool. The daemon list is DAEMON_TOOL_NAMES (@agentproto/runtime/daemon-tool-names), generated from a real gateway's tools/list with every optional surface wired (workflows, pairing/devices, llm-endpoint, ...) and kept exact by a runtime test, so it follows the daemon of the same release. A tool added in a newer daemon needs a CLI from that release.
  4. When APP.md declares data.dir, <dir>/<data.dir>/DATA.md exists (the data plane must ship its key dictionary — see data/DATA.md).
  5. When APP.md declares verify.command, it is run — argv-split on whitespace, NO shell, cwd = the app dir, 10-minute cap. Its stdout is printed verbatim and its exit code propagated: with no other findings, validate exits with the verify command's own exit code.

Findings print one per line as [error] <scope>: <message> on stderr. --json prints { ok, findings: [{scope, level, message}] } on stdout; exit 0 iff ok.

Optional ui/ source project

An app's .agentproto/ui/ is always the thing actually served — a plain index.html + assets, hand-written or built. An app MAY additionally carry a ui/ source project (a Vite + TypeScript project) that builds into .agentproto/ui/:

  • vite.config.ts sets outDir: "../.agentproto/ui", emptyOutDir: true, and base: "./" (relative asset paths, since the daemon / app serve / the MCP-Apps panel serve static files with no rewrite rules and may serve from a subpath).
  • The router (if any) uses hash history (createHashHistory) for the same reason: one index.html + hash routes works from any subpath, and even file://, with no per-route HTML emission needed from the static host.
  • No ui/ directory, or a ui/ without a package.json build script, means the app is a hand-written static UI — app build no-ops successfully and app dev isn't available (use app serve).

create-agentproto-app scaffolds this shape (Vite + TanStack Router + TanStack Query + @agentproto/app-client) end to end.

The manifest.json

{
  "format": "agentapp/v1",
  "id": "@agentproto/job-application-kit",
  "name": "Job Application Kit",
  "version": "0.1.0",
  "description": "…",
  "agents": ["job-scout", "job-tailor"],
  "workflows": ["job-hunt"],
  "ui": ["index.html"],
  "files": [".agentproto/APP.md", "base-cv.json", "dossiers/…"],
  "fileCount": 29,
  "totalSize": 123456,
  "sha256": "abc123…",
  "createdAt": "2026-08-11T…",
  "agentprotoVersion": ">=0.1.0"
}

files are relative paths (sorted); ui is an array of bundled UI filenames when the app declares one. sha256 is an aggregate over the concatenated bytes of every bundled file in files order (excluding manifest.json itself), which is exactly what unpack recomputes to verify integrity.

Stage board

app serve and app dev both respond to GET /agentproto/stageboard.js with a dependency-free vanilla ES module (content-type: text/javascript, no-store caching — never stale in dev) that renders the app's state ledger as a stage board. There is nothing to build or bundle: import it from any served UI page and mount it on an element.

<div id="stages"></div>
<script type="module">
  import { mountStageBoard } from "/agentproto/stageboard.js";
  window.McpApp.connect().then((bridge) => {
    mountStageBoard(document.getElementById("stages"), {
      appId: "@you/your-app",          // the installed app id
      callTool: bridge.callTool,       // the window.McpApp bridge
      refreshMs: 15000,                // auto-refresh (optional)
      // onValidate: () => …,          // optional hook for a Validate button
      // onApprove: (approval) => …,   // optional custom approval handler
    });
  });
</script>

What it renders:

  • One column per stage in first-seen order (the folded snapshot's key order), one row per item — or a single stage-level row for ledgers that never used items.
  • Per status chips using the daemon's vocabulary (pending, running, gated-failed, blocked, done, approved).
  • A collapsible per-row gate detail: last gate-report findings (from payload.report.findings when present, else the raw payload), attempt count, and the appRunId of the most recent run that touched the row.
  • A visible "last updated" timestamp and a Refresh button; auto-refresh every refreshMs (default 15s).
  • A Validate button when you pass onValidate; otherwise the app's verify.command is shown if app_status exposes one, and the button is hidden when neither exists.
  • An Approvals block listing app_status.awaitingApprovals[] (when that field is present) with an Approve button per entry. Approving asks who via a prompt input and calls workflow_escalation_resolve; if that tool is not in the app's ui.tools allowlist, the board degrades gracefully (feature-detected from the 403). Pass onApprove to handle approvals yourself instead.

All styles are scoped under .ap-stageboard and driven by CSS variables — theme it by overriding e.g. --ap-stageboard-accent, --ap-stageboard-line, --ap-stageboard-good, --ap-stageboard-bad, --ap-stageboard-warn on an ancestor element.

The pure fold behind the render (toRows(snapshot, events)) is exported from the module too, so tests and other renderers can reuse it without a DOM.

Note: the create-agentproto-app trame template does not yet ship this snippet — until it does, copy the <script type="module"> block above into your app's UI by hand.

Examples

# Register an app, keeping its generated output on a big external volume
agentproto app install ./tripsmith --data-dir /Volumes/big/tripsmith-data

# Pack a real app into the current directory
agentproto app pack ./job-application-kit

# Pack with an explicit output path and JSON manifest
agentproto app pack ./job-application-kit --out kit.agentapp --json

# Unpack into a chosen folder (verifies SHA first)
agentproto app unpack kit.agentapp --dir ./my-app

# An unpacked folder is itself packable again (round-trip stable)
agentproto app pack ./my-app

# Scaffold the minimal app trame, then prove it is sound
agentproto app init trame ./my-app
agentproto app validate ./my-app --json

# Build a ui/ source project into .agentproto/ui/ (no-ops for a static UI)
agentproto app build ./my-app

# Run the ui/ dev server with a live MCP bridge (daemon must be running)
agentproto app dev ./my-app

# Forward extra args to the underlying dev server
agentproto app dev ./my-app -- --host 0.0.0.0