@cmblir/pompos
v6.12.0
Published
Lazy, elegant terminal CLI for chatting with Claude / OpenAI / Gemini / Ollama, orchestrating multi-step LLM workflows, and running multi-agent Slack teams with cross-task memory. Banner-on-launch, slash-command ghost autocomplete, persistent sessions, lo
Maintainers
Readme
pompos
A terminal agent that learns on your Claude subscription — for $0 — and reaches you on every channel.
Chat in the terminal. Let the background learning loop distil your conversations into reusable skills on claude-cli (your Pro/Max subscription — no API bill). Wire it to Slack, Telegram, Discord, Matrix, Email, Signal, WhatsApp, or Voice. Fan a hard task out to a planner + workers. One small, auditable Node core — no daemon you can't read.
npx @cmblir/pompos # first run walks you through setup, then drops into chat한국어: README.ko.md
What it is
pompos is a single-binary-feel Node CLI in the "claw" family (Hermes → OpenClaw → nanoclaw). It is TUI-first: pompos with no arguments opens a chat REPL with a braille splash, slash commands, and ghost-text autocomplete. Underneath, every turn feeds a learning loop, and the same agent answers from any messaging channel you connect.
You can read the whole thing. No hosted service, no telemetry, config in plain JSON at ~/.pompos/, secrets in ~/.pompos/.env (0600).
Quick start
npm install -g @cmblir/pompos # or: npx @cmblir/pompos
pompos # fresh install → guided setup, then chatThe first run is a phased wizard (Hermes-style — get one clean chat working first, then layer the rest):
- Provider + model — arrow-key picker (
claude-cliis keyless; gemini/openai/anthropic take an API key), then two un-numbered follow-ups that belong to the same step: the context window (how much history to keep per turn) and the tool-permission mode eachclaudespawn runs under. - Verify — a one-token ping confirms the provider answers.
- Channel — Slack / Telegram / Matrix / HTTP built in; Discord / Email / WhatsApp ship in-tree (need a runtime dep via
pompos channels install <name>), Voice ships in-tree and needs only an OpenAI key, Signal needssignal-cli(optional). - Workspace — a folder of AGENTS.md / SOUL.md / TOOLS.md prompt files (optional).
- Skills — install a bundle from GitHub,
<user>/<repo>[@<ref>](optional). - Webhook — an outbound webhook (optional).
- Orchestration — turn on the planner + workers pipeline (optional).
Re-run it any time with pompos setup (--only <steps> / --skip <steps> re-run part of it), or /setup from inside chat.
For automation, pompos onboard takes the same values as flags and writes the
config without prompting:
$ pompos onboard --non-interactive --provider claude-cli
{"ok":true,"written":"/tmp/pompos-demo/config.json","provider":"claude-cli","model":null,"hasApiKey":false}$0 self-learning
pompos splits two provider slots: provider (chat — the hot path) and trainer (the learning loop — skill synthesis, reflection, the user model). They are independent, so a Claude Pro/Max subscription can power the learning while chat runs anywhere — or both run on one backend.
After every turn, a fire-and-forget loop records the trajectory and distils reusable skills, tagged trained_by. With trainer: { provider: "auto" } it auto-detects your claude-cli session and runs the loop for free; otherwise it mirrors the chat provider.
// ~/.pompos/config.json
{
"provider": "openai", // chat: pay-per-token (or claude-cli, ollama, …)
"model": "gpt-4.1",
"trainer": { "provider": "auto" } // learning: $0 on your Claude subscription
}| Setup | provider | trainer | Cost |
|---|---|---|---|
| Subscription only | claude-cli | auto | $0 |
| Hybrid (recommended) | openai / any | auto | chat only |
| Pure API | openai / any | openai | both metered |
Talk to it anywhere
Connect a channel and the same agent answers there — every listener forwards into the always-on daemon's shared session store, so chat, the dashboard, and all channels are one agent with one memory (and context follows across channels). Slack, Telegram, Matrix, and HTTP are built in; Discord, Email, and WhatsApp ship in-tree and run once their runtime dependency is installed into the config dir (pompos channels install <name>); Voice ships in-tree with no runtime package to install (it just needs an OpenAI key for Whisper), and Signal needs the external signal-cli binary on your PATH.
pompos service install # 1) the always-on daemon (the shared brain)
# 2) point a listener at it (Slack Socket Mode: SLACK_BOT_TOKEN + SLACK_APP_TOKEN in ~/.pompos/.env)
pompos slack listen # forwards inbound to the daemon, replies in-thread
pompos slack listen --provider orchestrator # …and orchestrate the reply
pompos slack listen --daemon-url http://127.0.0.1:19600 # non-default daemon
pompos channels # view configured channels
pompos channels enable|disable slack
pompos channels install discord # installs discord.js into ~/.pompos and enables the channelA listener is a thin forwarder: it owns the channel socket (Slack Socket Mode needs no public URL, just an app-level xapp- token) and POSTs each message to the daemon's /inbound, which binds the conversation to a persistent session and runs the provider. It needs a reachable daemon (pompos service install, or pompos daemon); override the target with --daemon-url / POMPOS_DAEMON_URL. Set it all up from the wizard's channel step, or /channels in chat.
Run it always-on
One process, every channel: pompos gateway runs the daemon core and your configured channel transports (Slack Socket Mode / Telegram long-poll / Matrix sync) in a single process — and because the channels live in-process, /handoff can notify the target channel with a resume marker (a failed notify rolls the handoff back).
pompos gateway # daemon core + every enabled channel, one process
pompos gateway --channels slack # explicit channel set
pompos service install gateway # …and keep it alive across rebootsThe gateway is authenticated by default: it mints a bearer token on first run and persists it to ~/.pompos/gateway.token (0600, never logged). Its own channels use it automatically; external callers read it from the file (--auth-token/--no-auth to override).
Or run pieces separately: the daemon is the agent core — one provider path and one session/memory store on 127.0.0.1 — and * listen commands are standalone single-channel forwarders.
pompos service install # daemon only: launchd (macOS) · systemd user unit (Linux) · pidfile fallback
pompos service status
pompos service uninstallThe backend is auto-detected (override with --backend launchd|systemd|fallback); flags pass through, e.g. pompos service install gateway --port 19600 --auth-token "$TOK" --channels slack.
Inbound channel messages are idempotent: each message's native id (Slack channel:ts, Telegram chat:message_id, Matrix event_id) is deduplicated by the daemon — a redelivery or listener-restart replay returns the recorded reply instead of running the provider twice. And every session-bound channel turn feeds the same post-task learning loop as the chat REPL (trainer auto → your Claude subscription, $0).
[!NOTE] A channel listener or the daemon refuses to start while
security.allowUnattendedSensitive=true— that flag bypasses the fail-closed tool-approval gate for every inbound message, so an always-on surface plus that flag is a remote-code-execution path. Keep sensitive-tool approval interactive.Unattended team runs are fail-closed. When the daemon/gateway answers an inbound channel message with a
claude-cliteam, the spawnedclauderuns in read-onlyplanmode (it can inspect but cannot run bash or write files), regardless of your interactivechat.permissionMode. This closes the message-to-RCE path where an untrusted sender drives host commands with no human watching. Interactivepompos chatis unchanged. To let unattended surfaces execute host commands, opt in explicitly withsecurity.unattendedExec=true.
Multi-agent orchestration
Set the provider to orchestrator and a hard request becomes Plan → Delegate → Synthesise: a planner decomposes the task, workers run the subtasks in parallel, then the planner merges the results. Workers are real agents with the tool registry.
pompos orchestrator set-planner claude-cli:claude-sonnet-4-6
pompos orchestrator workers add claude-cli:claude-sonnet-4-6
pompos orchestrator on # route chats through the pipelineFrom chat: /orchestrator opens an on/off picker, or /orchestrator on|off|planner <spec>|worker add <spec>. Details: docs/multi-agent.md.
Drive it from chat
The REPL has slash commands for everything you'd otherwise edit config for — point-and-pick, no JSON:
| Slash | Does |
|---|---|
| /config · /config set <key> <value> · /config unset <key> | change one setting via a picker, or set/unset a key directly by name; /setup re-runs the whole wizard |
| /provider · /model | pick provider / model from a searchable list |
| /trainer [set\|fallback] · /agent edit <name> | pick the trainer / an agent's provider+model from the same list (with an auto and a custom-id row) |
| /agent add <name> [--provider <p>] [--model <m>] [role…] | register a new agent, optionally pinning its provider and model up front |
| /team member add\|remove <team> <agent> | add or remove a member from a team |
| /workflow run\|resume\|clear <name> | run a saved workflow, resume it from where it left off, or clear its saved progress |
| /channels [<name> on\|off\|setup] | view / toggle channels; setup sets the bot token & credentials in-chat |
| /orchestrator [on\|off\|…] | view / toggle multi-agent (picker on bare call) |
| /context [turns N\|tokens N] | resize the chat history window |
| /agentic [on\|off] · /plan [on\|off] | let chat run tools (approval-gated); plan mode is read-only "propose first" |
| /skill · /personality · /memory · /loop · /goal | skills, personas, memory, loops, goals |
/help lists them all. Autocomplete works as you type: command names (/...) and, after a command, its arguments — /login → codex-cli/gemini-cli, /hud → on/off, /channels → channel names, subcommands for /task /team /agent /personality /trainer /orchestrator, and more — appear in a popup (↑/↓ select, Enter fill). The 2-step provider→model picks (/model, /trainer set, /orchestrator planner) show a ↹ pick hint; press Tab to open the drill-in modal. CJK/Hangul input composes inside the box.
Manage the always-on gateway without leaving the chat:
/gateway # or /gateway status — pid, port, /health, auth, channels
/gateway start [--port N] # spawn a detached gateway and wait for it to come up (--port is a one-off, not persisted)
/gateway port [N] # print the effective port + its source (flag/config/default), or set + persist a new one
/gateway stop # SIGTERM the recorded pidThe gateway records its pid to gateway.pid in the config dir, alongside the daemon's own daemon.pid. The port itself resolves in order: an explicit --port flag, then gateway.port in config.json (dashboard.port / daemon.port for the other two surfaces), then the historical default 19600 — so moving one surface off a collision doesn't require touching the others.
The REPL also animates when your terminal supports it: a launch reveal + wordmark shimmer, a braille streaming spinner with elapsed turn time, a thinking… indicator before the first token, a tweened context gauge, a live throughput meter in the HUD row, and a red input-border pulse after a failed turn.
| Env var | Effect |
|---|---|
| POMPOS_NO_MOTION | 1 disables every terminal animation (launch intro, streaming spinner, gauge tween, error flash). Animation is also off automatically without a TTY, under NO_COLOR, and on TERM=dumb. |
The dashboard
pompos dashboard # local web UI on http://127.0.0.1:19600 (override with --port N or dashboard.port in config.json)From chat, /dashboard [--port N] (or /dashboard stop) does the same.
[!IMPORTANT] Open it with
pompos dashboard, not a barepompos daemon. OnlycmdDashboardsets the loopback-origin allowance the browser needs; Chromium sends anOriginheader even on the module-script fetch fordashboard.jsitself, so under a bare daemon that very request 403s and the page never boots — it just sits on "connecting…".
A framework-free, zero-build SPA (web/ ships as source — native ES modules, no bundler, no dist/) over the daemon's JSON API: a grouped sidebar over 21 panels — Work, Agents, Automate, Knowledge, Gateway, System — a ⌘K / Ctrl+K command palette, and a "Live" activity strip under the topbar, all fed by one Server-Sent Events connection (GET /events). Motion — the palette, the live rail, reordering rows — respects prefers-reduced-motion.
The dashboard stopped being read-only. Agents, teams, tasks, config keys and workflows can now be created, edited and run from the browser, and the chat input takes every slash command the terminal REPL does, with the same autocomplete. Destructive actions ask first, naming what they're about to affect, instead of acting immediately. All of it runs through one endpoint, POST /slash, which calls the exact same dispatcher the CLI does — a command added to the terminal shows up in the dashboard with no extra wiring, and the two surfaces cannot disagree about what a command means. Authorization is unchanged: the same bearer token that has always gated the daemon gates these actions too.
Two gaps worth knowing about: creating an agent from the dashboard can't set a custom tool list yet (--tools has no slash-command form) — the agent is created correctly, just with the default tool set. And a long-running command like /loop or /task streams its progress to the browser, but closing the tab doesn't cancel the run underneath it; there's no abort wiring yet.
- Team Live now draws the real reporting hierarchy: each manager's reports sit under them, joined by edges, instead of a lead plus one flat row. Avatar tiles carry status rings + harness badges (
provider · model); click one to drill into its harness, current task, and recent activity. - Tasks shows where a task came from — a Slack channel + thread, or
pompos task startfrom the CLI — and the permission mode it actually ran under (attended, unattended-but-read-only, or the unattended-with-execution case worth flagging), plus a transcript viewer; atask:hit in Recall links straight to the same transcript. - Approvals and Devices are new panels. Resolving an approval needs a paired device's Ed25519 token — the browser can be that device now; see "Approving from the dashboard" below. The Devices panel's own table stays read-only for any other device: approving, revoking or rotating one still only happens via
pompos nodes. Approval events (exec.approval.requested/.resolved) ride the dashboard's SSE stream, so both the sidebar badge and the Approvals table refresh whichever panel you happen to be on. - The live rail picks up four new event types:
workflow.step,cost.tick,channel.inbound,provider.error.cost.tickonly fires for team-routed traffic — the plain/chatand/agentpaths never feed the configured cap to the cost accountant. There's nocron.fire: a scheduled job runs in whatever subprocess launchd/cron spawns, with no link back to the daemon's event bus, so a fire isn't observable here at all. - ⌘K jumps to any panel plus four fixed actions (start a task, new team, review approvals, rebuild the search index) — it doesn't resolve a team or agent by name.
- Chat's model picker lists each provider's live model catalogue when one is reachable (grouped per provider, since a live fetch can return hundreds), instead of a fixed curated set, and tags the picker
live/builtinso a fetched list is never mistaken for a frozen one. Without a live fetch it falls back tonpm run models:sync's last run, then the hand-written defaults — so the list is never empty just because a credential isn't configured on this machine.
Approving from the dashboard
Resolving an approval requires a paired device, not just the dashboard's auth token. The browser can be that device: the first time you press Approve (or the Pair this browser button on the Devices panel) it generates an Ed25519 keypair, keeps the private half in the browser as a non-extractable key, and pairs itself with the daemon.
- The first device is approved automatically when it pairs over loopback — otherwise there would be no one to approve it. That happens once per install: the slot closes as soon as any device has been approved and does not reopen, not even after you revoke every device.
- Every device after that is
pendinguntilpompos nodes approve <requestId>approves it from a terminal. There is deliberately no way to approve a new device from the dashboard — that would let one browser enrol another. - The private key cannot be exported, backed up, or moved to another browser.
Pairing again from a new browser is the recovery path; drop the old record
with
pompos nodes revoke <deviceId>. Because the bootstrap slot is spent, that new browser comes uppending— approve it withpompos nodes approve <requestId>(the Devices panel and the pairing message both print the id). - WebCrypto only exists on a secure origin, so pairing works on
http://localhost/http://127.0.0.1and over HTTPS. On a plain-HTTP LAN address the browser exposes nocrypto.subtleat all, and the panel says so and tells you to reopen the dashboard on its loopback address. There is no terminal fallback: answering an approval requires a paired device, andpompos nodesmanages devices, not approvals.
Providers
| Chat / trainer | Auth |
|---|---|
| claude-cli | subscription (Pro/Max) — keyless |
| anthropic · openai · gemini | API key |
| ollama | local, no key |
| nim · openrouter · groq · together · xai · deepseek · mistral · fireworks | OpenAI-compatible API key |
| custom | any OpenAI-compatible v1 endpoint (your base URL + key) |
| orchestrator | meta-provider — planner + workers over any of the above |
pompos providers info <name> prints what a slot needs before you commit to it:
$ pompos providers info anthropic
{
"name": "anthropic",
"requiresApiKey": true,
"keyPrefix": "sk-ant-",
"docs": "Anthropic Messages API (pay-per-token, requires sk-ant- key). Supports streaming + extended thinking. For subscription billing, use the `claude-cli` provider instead.",
"endpoint": "https://api.anthropic.com/v1/messages",
"defaultModel": "claude-opus-4-8",
"suggestedModels": [
"claude-opus-4-8",
"claude-opus-4-7",
"claude-opus-4-6",
"claude-sonnet-4-6",
"claude-sonnet-4-5",
"claude-haiku-4-5"
]
}What else it ships
- Tool registry — 12 categories (
agents,browser,coding,exec,fs,git,iot,learning,media,net,os,scheduling) plus stdio MCP. Sensitive tools (shell, write, network) are fail-closed behind an approval hook by default. - Skills — markdown instruction bundles composed into the system prompt.
pompos skills starterinstalls the bundled pack (concise·korean·commit-message·code-review·channel-style·summarize·explain·debug-coach);pompos skills install <user>/<repo>pulls more from GitHub;/skillspicks one in chat. - Durable recall — one SQLite + FTS5 index over sessions, skills, trajectories, and memory; rebuildable from the corpus (
pompos index rebuild). Optionally blend in embedding similarity (cfg.recall.embeddings, off by default; OpenAI/Gemini key or a local Ollama model — the $0 path stays pure FTS5);pompos index embedbackfills the vectors. - Loops & goals — durable foreground/
--detachloops and cron-scheduled goals that survive restart. - Personas — layered SOUL / workspace / personality / role / user-model / skills compose into the system prompt.
- Agent teams, live — build a hierarchy (a planner agent with sub-agents on different harnesses, e.g. a data-engineer on
gemini-cliand a backend onclaude-cli) via each agent'smanager; a Slack message to a team's channel auto-routes into the multi-agent loop, and the Team Live dashboard tab shows who is doing what, on which harness, and the real-time A→B delegation as it happens. - Default-on confinement — sensitive tools (
bash,python_exec/node_exec,git_*, theostools) run confined by default: writes are limited to the workspace + temp, secret dirs (~/.ssh,~/.aws, the config dir, …) are unreadable, and the secret-scrubbed env still applies. macOS usesseatbelt, Linux usesbubblewrap/firejail(auto-detected). Network is allowed; opt out withcfg.sandbox.confine=false. Runpompos sandbox statusto see the effective posture. - Sandboxes —
local/docker/ssh/singularity/modal/daytonabehind one API; pluggable backends for heavier isolation (containers, remote hosts) layered on top of the default local confinement. - MCP — stdio MCP servers in
cfg.mcp.serversboot with the daemon; their tools register asmcp:<server>:<tool>(always approval-gated). Manage them from the CLI:pompos mcp list/mcp add <name> --command <cmd> [--args "…"] [--allow-glob <glob>]/mcp remove <name>/mcp call <server> <tool> [--args-json '{…}'](spawns the server, runs one tool, tears it down). - Daemon lifecycle —
pompos daemon status | stop | logsover a pidfile;maxTokensin config raises the output cap;POMPOS_REQUEST_TIMEOUT_MSsets the per-provider idle timeout (default 120s).
Configuration & security
Config is plain JSON at ~/.pompos/config.json — parsed with JSON.parse, no shell or code execution; channel + provider secrets live in ~/.pompos/.env (written 0600, never logged). Move the dir with POMPOS_CONFIG_DIR=/path.
Sensitive tools deny by default unless an approval hook grants them; config.json and workflow state are written owner-only; secrets are scrubbed from the bash tool's child env and redacted from trajectories and synthesised skills.
Install / hack
npm install -g @cmblir/pompos # install
git clone https://github.com/cmblir/pompos && cd pompos && npm install && npm link # hack
node --test tests/*.test.mjs # run the suiteRequires Node 18+ (Node 22+ for Slack Socket Mode). macOS / Linux / WSL are first-class.
Coming from lazyclaw
This tool was lazyclaw through 6.10.0. Nothing you have set up needs changing:
- Your state stays where it is. An existing
~/.lazyclawkeeps being used in place — nothing is moved or copied. To adopt the new path,mv ~/.lazyclaw ~/.pompos; it is picked up automatically. LAZYCLAW_*variables still work. Each is mirrored to itsPOMPOS_*name at startup, so shell profiles, CI secrets and unit files keep taking effect. Whichever name you set explicitly wins.- The
lazyclawcommand still works, so installed launchd plists, systemd units and crontab lines that invoke it by name keep running, as do schedules created before the rename. - The dashboard keeps you signed in — a token saved under the old key is read and carried forward.
npm install -g @cmblir/pompos installs both commands. Releases publish to
@cmblir/pompos; the bare pompos name is unavailable on npm, which refuses it as
too close to the existing prompts package, and a scoped name is what npm itself
suggests in that case. The unscoped lazyclaw package stays at the 6.9.3 already
on npm and receives no further releases.
Docs
- docs/multi-agent.md — orchestrator pipeline + Slack teams
- docs/trainer-recipes.md — $0, hybrid, and
autotrainer configs - docs/persona-cookbook.md — persona compose stack
- docs/loop-goal-preflight.md — loops + scheduled goals
- CHANGELOG.md — release notes · README.ko.md — Korean
License
MIT · Source & issues: cmblir/pompos
