abpilot
v1.0.11
Published
Turn workspace AI skills into a local, themeable web console.
Downloads
1,669
Readme
abpilot (Axiom)
Turn workspace AI skills into a local, themeable web console.
abpilot scans your workspace for skills (any directory with a SKILL.md
plus an app.js) and mounts each one as an app in a local web console. Skills
can be headless HTTP APIs for agents, or full React UIs. The console also ships
with an optional AI chat panel that connects to a local
opencode server.
- Discover apps from
.opencode/skill,.claude/skills,.agents/skills,skills/, and more (see Skill sources). - Dark/light theme, synced into every app through
postMessage. - Per-app SurrealDB storage via
app.db(). - AI chat with sessions, model switching, attachments, image paste, and voice-to-text — powered by the opencode SDK.
Requirements
- Node.js >= 18
- Optional:
surrealdb/@surrealdb/nodeforapp.db()(installed as optional dependencies). - Optional: the opencode CLI, to use the AI chat panel.
Quick start
Run the console directly with no install:
npx abpilot ui --openOr install it globally:
npm install -g abpilot
abpilot ui --openBy default the console listens on http://127.0.0.1:4097 and scans the current directory for skills.
Enable the AI chat panel
The AI panel talks to a local opencode server over its HTTP API. Start one on the default port, then open the console:
opencode serve --port 4096 # keep running in another terminal
abpilot ui --open # the AI button appears when 4096 is reachableCLI
abpilot <command> [options]Commands:
| Command | Description |
| --- | --- |
| ui | Start the local web console (default port 4097). |
| list | List discovered apps and skills without starting the server. |
| login | Sign in with an email one-time code and store the token locally. |
| get-token | Print the stored auth token. |
| logout | Remove the stored auth token. |
| app init | Create app.info for an app directory. |
| app publish | Register a version from app.info, upload the artifact, and tag it. |
| help | Show help. |
Options:
| Option | Description |
| --- | --- |
| -p, --port <n> | Port to listen on (default: 4097). |
| --host <host> | Host to bind (default: 127.0.0.1); a host id for app init/app publish. |
| -w, --workspace <dir> | Workspace root to scan (default: cwd). |
| -o, --open | Open the browser on start. |
| --db-dir <dir> | Base directory for SurrealDB files (default: <tmp>/abpilot). |
| --reset-db | Delete the stored database before starting. |
| --opencode-host <host> | opencode server host for the AI panel (default: 127.0.0.1). |
| --opencode-port <port> | opencode server port for the AI panel (default: 4096). |
| --no-global | Skip global skill directories. |
| -e, --email <email> | Email for login (skips the prompt). |
| --code <code> | One-time code for login (skips the prompt). |
| --api-url <url> | Auth service base URL (default: production). |
| --apikey <key> | app init/app publish: use an API key instead of the stored token (or ABPILOT_APIKEY). |
| --dir <dir> | app init/app publish: app directory (default: cwd). |
| --name <title> | app init: create the app with this title (needs --host). |
| --id <app_id> | app init: use an existing app id. |
| --version <v> | app init: version to write (default: 1.0.0). |
| --mobile <dir> / --web <dir> | app init: mobile/web build dir. |
| --zip <file> | app publish: upload a prebuilt zip (else zip the declared dirs). |
| --notes <text> | app publish: version notes. |
| --tag <tag> | app publish: point this tag at the version. |
| --force | app init: overwrite an existing app.info. |
| --json | get-token: print the full stored credential as JSON. |
| --quiet | get-token: suppress expiry warnings. |
| -v, --version | Print version. |
| -h, --help | Print help. |
Login
abpilot login authenticates against the abpilot auth service
(passwordless email OTP) and stores the resulting JWT locally:
abpilot login # prompts for email, then the code
abpilot login --email [email protected]It calls POST /auth/request-code, prompts for the emailed code, then calls
POST /auth/verify-code and writes the token to
~/.abpilot/credentials.json (mode 0600). Override the location with
ABPILOT_HOME, and the service base URL with --api-url or
ABPILOT_API_URL. The default is the production URL
(https://5btchlheby6u46iaq7ttx62qdy0tzmut.lambda-url.us-east-1.on.aws/).
Use abpilot get-token to print the stored token (raw by default, for piping
into curl/scripts; --json prints the whole credential). It warns on stderr
when the token has expired — refresh with abpilot login.
TOKEN=$(abpilot get-token)
curl "$URL/auth/me" -H "authorization: Bearer $TOKEN"abpilot logout deletes the stored credential. Sessions are stateless JWTs, so
there is no server-side revocation — logging out only removes the local token.
The console (abpilot ui) mirrors this in the bottom-left of the sidebar:
Sign in opens an email-OTP dialog (request code → enter code), and Sign
out clears the credential. The console shares the same credential store, so
CLI and UI stay in sync. These talk to the server routes under /api/auth
(status, request-code, verify-code, logout); the token is held server-side
and never sent to the browser.
Apps (app init / app publish)
Platform apps are published from a directory with an app.info file
({ id, version, mobile?:{dir}, web?:{dir} }); see
skills/abpilot-app-skill/SKILL.md for the schema and rules. The app
subcommands drive it, authenticated by the stored token or an --apikey:
# create the app on the platform and write ./app.info (id + version)
abpilot app init --host hst_... --name "My App" --mobile mobile/dist --web web/dist
# or point at an existing app id
abpilot app init --id app_... --version 1.0.0
# publish: register the version, zip the declared dirs (or --zip), upload, tag
abpilot app publish --tag stable
abpilot app publish --host hst_... --zip build/app.zip --apikey "$ABPILOT_APIKEY"app publish resolves the host from the app id if --host is omitted. When no
--zip is given it zips the declared mobile/web dirs; if there is nothing to
zip it prints the presigned URL to upload yourself. An API key works for app
operations; the /apikeys management routes still require a JWT.
Workspace package app
If the workspace package.json declares host: { id, tag }, abpilot ui
fetches the public host-tag rollup (GET /hosts/{id}/tags/{tag}), finds the
app whose name matches package.json.name, downloads the app's tagged
artifact into <workspace>/.apps/<app_id>/, and serves it as a console app at
/apps/<name>:
{ "name": "my-app", "host": { "id": "hst_...", "tag": "stable" } }- The artifact (the version
.zip) is unpacked into.apps/<app_id>/(gitignored).web.dirresolves inside it first, then falls back to the workspace (useful while developing locally). A version marker avoids re-downloading; a new tag version re-downloads. web.diris the frontend-only compiled build, served statically with anindex.htmlSPA fallback. Apps have no server side.
The version endpoint (GET …/apps/{app_id}/versions/{version}) is public, so
downloading needs no login. If the fetch/download fails, the console just starts
without it.
Writing a skill
A skill is a directory with a SKILL.md and an entry file. The entry file
default-exports a function that receives the app context:
export default function (app) {
app.reg({
title: 'My App',
icon: '🧩', // emoji, or an image URL/path
entrypoint: '/apps/my-app', // optional; defaults to /apps/<skill-id>
description: 'Short subtitle',
order: 100, // lower sorts first
});
app.get('/', (req, res) => res.type('html').send('<h1>Hello</h1>'));
}CommonJS is also supported: module.exports = function (app) { ... }. The
function may be async and may await app.db(). Accepted entry file names:
app.js, app.mjs, app.cjs, app.ts, app.mts, app.cts, or
app/index.js.
The app object
| Member | Description |
| --- | --- |
| app.reg(def) | Set/merge registration metadata (title, icon, entrypoint, description, order). |
| app.use(...) | Express middleware / router. |
| app.get/post/put/patch/delete/all(path, ...handlers) | Express route helpers on the app router. |
| app.static(relDir, opts) | Serve a directory relative to the skill. |
| app.json() / app.urlencoded() | Optional local body parsers (global parsing is already enabled, so req.body works). |
| app.resolve(relPath) | Absolute path inside the skill directory. |
| app.dir / app.id | Absolute skill directory / skill id. |
| app.log(...) | Prefixed logger ([<id>] ...). |
| app.express | The express module, for advanced use. |
| app.db(options?) | Promise<Surreal> bound to this app (see Storage). |
Notes:
- Every app gets its own router; routes are relative to its
entrypoint. - Requests to the bare entrypoint redirect to a trailing slash, so relative
URLs (
fetch('api/tools')) resolve correctly from the UI. - Errors: call
next(err)(wrap async handlers in try/catch). The server returns500 { "error": "internal_error", "message": "..." }.
Two modes
Headless mode (HTTP API only). No UI; the app only exposes an HTTP API for
agents and scripts. Set headless: true in app.reg({...}) and the console
keeps it out of the sidebar and search (its routes stay mounted, so the API is
still reachable). See skills/abpilot-web-skill/templates/headless.js.
UI mode (React). The app serves an HTML shell and a React UI, and exposes
the same AI API. React and the UI stack are loaded in the browser from an import
map (esm.sh), and the UI's JSX/TSX is bundled on demand with the esbuild that
ships with abpilot, so no build step or local node_modules is required. Styling
uses Tailwind CSS + Headless UI (@headlessui/react) + clsx — no
custom CSS. See skills/abpilot-web-skill/templates/.
The skills/abpilot-web-skill/SKILL.md file is the full authoring guide — the
app.js contract, both modes, theme sync, and the AI API convention.
AI-facing HTTP API
Design each app's API so an agent can discover and call tools without prior
knowledge. Expose a manifest at GET <entrypoint>/api:
{
"name": "my-app",
"description": "What this app does",
"version": "1.0.0",
"protocol": "axiom-tools/1",
"tools": [
{
"name": "echo",
"method": "POST",
"path": "/api/echo",
"description": "Echo text back",
"input": { "type": "object", "properties": { "text": { "type": "string" } }, "required": ["text"] },
"output": { "type": "object", "properties": { "text": { "type": "string" } } }
}
]
}Use a consistent envelope: { "ok": true, "data": <result> } on success and
{ "ok": false, "error": { "code": "...", "message": "..." } } with a 4xx/5xx
status on failure. Full spec: skills/abpilot-web-skill/reference/ai-api.md.
Storage: app.db()
await app.db() returns a per-app SurrealDB client backed by a SurrealKV
file whose directory is a hash of the workspace directory + port. Data
persists across restarts; pass --reset-db to clear it. It is optional — if the
surrealdb packages are not installed, only calls to app.db() fail.
export default function (app) {
let schema = null;
const ensure = (db) => (schema ??= db.query('DEFINE TABLE IF NOT EXISTS note SCHEMALESS'));
app.get('/api/notes', async (req, res, next) => {
try {
const db = await app.db();
await ensure(db);
const [rows] = await db.query('SELECT * FROM note ORDER BY created DESC');
res.json({ ok: true, data: rows || [] });
} catch (err) {
next(err);
}
});
}Gotchas:
- Querying a table that has not been created throws "table does not exist".
Create it once with
DEFINE TABLE IF NOT EXISTS <name> SCHEMALESS. db.query(sql, vars)returns an array of statement results; for a singleSELECTthe rows are the first statement result (const [rows] = ...).- Store ids you expose to clients as strings (
uid = <string>rand::uuid()) rather than Surreal record ids.
Theme sync (UI mode)
The console posts { type: 'axiom:theme', theme: 'dark' | 'light' } into the
app's iframe on load and whenever the theme changes:
window.addEventListener('message', (event) => {
if (event.data && event.data.type === 'axiom:theme') {
document.documentElement.setAttribute('data-theme', event.data.theme);
}
});
// Handshake: ask the console to (re)send the theme once we are listening.
window.parent.postMessage({ type: 'axiom:theme:request' }, '*');Define your CSS with :root[data-theme='dark'] / :root[data-theme='light']
variables, plus a prefers-color-scheme fallback for the first paint.
AI chat panel
When a local opencode server is reachable, an AI button appears in the
toolbar. Clicking it opens a chat drawer on the right that connects to that
server through the abpilot backend (which uses the
@opencode-ai/sdk — the browser never talks to opencode directly).
Features:
- The entry appears automatically only while the configured opencode host/port
(default
127.0.0.1:4096) is reachable; it hides when the server is down. - Header shows the current conversation; click it to open the conversation list — select, create, or delete conversations.
- Live streaming of assistant text, reasoning, and tool calls over SSE.
- Model switching from the models exposed by the opencode server; the choice is persisted.
- Multi-line composer:
Enterto send,Shift+Enterfor a newline; it grows up to 20 lines, then scrolls. - Attachments: pick images or any file, or paste an image directly.
- Voice-to-text via the browser Web Speech API (button shown when supported).
- Stop button to abort a running response.
The backend routes live under /api/ai (status, models, sessions,
sessions/:id/messages, events, sessions/:id/abort); see lib/opencode.js.
Skill sources
Skill directories are discovered (recursively, up to a few levels) from these workspace roots:
.opencode/skill .opencode/skills .claude/skills .agents/skills
.github/skills .codex/skills .cursor/skills .gemini/skills
.windsurf/skills .ai/skills skills/and, unless --no-global is passed, from the global counterparts:
~/.config/opencode/skill ~/.config/opencode/skills ~/.claude/skills
~/.agents/skills ~/.codex/skills ~/.gemini/skillsThe apps bundled under skills/ in this package (Welcome, Notes, Hosts, API
Keys, Apps) are always available as built-ins. Hosts (hosts + members),
API Keys, and Apps (apps/versions/tags) are management UIs for the
platform and use your abpilot login token. The skills/abpilot-doc-skill
skill is a documentation-only reference for calling the platform API
directly — it has no app.js and mounts nothing.
Development
npm run dev # Vite dev server for the console UI (proxies /api to :4097)
npm run build # Build app/dist (the console UI)
npm run typecheck # tsc --noEmit
npm start # Start the console from source (node bin/abpilot.js ui)Project layout
bin/abpilot.js # CLI entry
lib/cli.js # Commands, flags, help
lib/server.js # Express server: static UI, /api/apps, mounts app routers
lib/skills.js # Skill discovery
lib/loader.js # Loads and initializes app.js modules
lib/app-context.js # The `app` object passed to skills
lib/db.js # Per-app SurrealDB manager
lib/auth.js # Auth service client + local credential storage
lib/opencode.js # opencode SDK service for the AI chat panel
app/src/ # Console React UI (built to app/dist)
skills/ # Built-in example apps + the abpilot-web-skill guideLicense
MIT © hailongz
