Skip to content

Latest commit

Β 

History

492 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

poisson β€” a coding agent that lives in your terminal

Embrace the entropy, probabilities favor the bold.

Go 1.25 single binary dependencies storage


poisson is an opinionated coding agent you run in your terminal. It streams a real conversation, calls tools (bash, file read/write/edit, subagents), tracks every token and dollar, and keeps your whole history in a local SQLite database you own β€” just one static binary and your terminal.

It talks to Anthropic (Claude), OpenAI (ChatGPT subscription), xAI (Grok, SuperGrok), OpenRouter (400+ models, API key), Ollama (local + cloud), and llama.cpp (local llama-server) β€” paste images, search past sessions full-text, compact context, approve risky shell commands from a popup.

go install github.com/mq37/poisson/cmd/px@latest   # needs Go 1.25+; installs to $(go env GOPATH)/bin
px login anthropic                                  # or: px login openai | xai β€” skip for Ollama, no auth needed
px                                                   # launch the TUI

⚠️ Experimental. poisson is under active, fast-moving development β€” commands, flags, and behavior can change without notice between versions.


✨ Features

  • Streaming REPL TUI β€” hand-rolled ANSI, no framework. Thinking blocks, tool cards with diffs, command palette (Ctrl+P), status bar with context % and cost.
  • Real tools, not just bash β€” read/write/edit/grep/glob, batch, web_search/web_ask/fetch (details), recall, set_title, subagents, and 13 built-in skills (below, user-extendable). bash is stateless β€” pass workdir explicitly.
  • Recovers leaked <invoke> tool calls β€” a weak/local model sometimes echoes other harnesses' <invoke>/<parameter> XML as plain text instead of a real call (never poisson's own format). Parsed back into a real, dispatchable call when it resolves cleanly; left as-is otherwise.
  • Bash safety guard, three speeds β€” Fast mode (default): deterministic auto-approve for read-only commands, LLM risk-classifies the rest (low auto-approves, medium/high/unknown asks you). The classifier also auto-denies β€” no human asked β€” a command whose own output would leak a secret to stdout (echo $AWS_SECRET_KEY, an unredirected doppler secrets download, ...); switch to Paranoid mode to review and approve one yourself if that's a false positive. Paranoid mode asks for everything; installs/destructive/npx-style always ask. Shift+Tab cycles Fast β†’ Paranoid β†’ Yolo β€” every command runs immediately, no approval of any kind, any risk. Opt-in only, never the default; use on a box/ sandbox you're fine handing full unattended shell access to.
  • Secret redaction β€” tool output is scanned for secret-shaped text (vendor tokens, PEM keys, JWTs, credential KEY=VALUE pairs) and masked with [REDACTED BY HARNESS] before reaching the model, TUI, or session store. Best-effort, not a guarantee.
  • Subagents run async β€” one subagent tool, dispatched by action: spawn (default) starts a child and returns a job ID immediately; the main agent keeps working (or you keep chatting) while it runs in the background. status polls progress (one job or every job), result retrieves the final output once, kill stops one early (or every job at once) β€” see docs/async-subagent-plan.md.
  • Subagents on any provider β€” a spawned subagent inherits the main session's model/effort by default; override either, or hand it a provider/model qualified ID to run it on a completely different provider. Same-provider overrides auto-run like today; a different provider asks you first by default, same popup as a risky bash command. List a set of providers under [subagent] trusted_providers in config.toml to let them spawn on each other with no prompt β€” both sides of the pair must be listed, and trust skips only the popup, never the configured-provider and known-model checks.
  • Podman sandboxes β€” one sandbox tool, dispatched by action: create gives an isolated container (bash calls passing its sandboxId then skip the approval gate entirely β€” the container is the boundary); cp/destroy/resurrect/list manage it from any session, or via /sandbox ls/kill. Requires podman. (details)
  • Sessions in SQLite β€” every message/tool/API call persisted, FTS5 full-text search, resume any session, auto-compaction when context fills up.
  • Exact cost & tokens, live in the status bar and /cost, plus live usage-limit tracking for Anthropic/OpenAI subscription accounts.
  • Image input β€” paste (Ctrl+V) or @screenshot.png. (details)
  • <render> file citations β€” cites a snippet (<render file="path" from="10" to="50"/>) instead of retyping it, zero output-token cost; ref="<commit-or-branch>" cites a git ref instead of the working tree. A citation that fails to resolve (bad path/ref) gets a couple of automatic, visibly-marked retries in the same turn before the answer is considered done.
  • Clickable file:///http(s):// links β€” any such link in a reply (markdown [label](url) or a bare URL) renders as a real OSC 8 terminal hyperlink; supporting terminals (kitty, iTerm2, wezterm, ...) open it on click instead of you copying a path out and opening it by hand. Pairs with the canvas skill below for HTML reports.
  • Message queueing β€” type while the agent works; sent at the next turn boundary instead of waiting for the whole turn to finish.
  • px orchestrate β€” run persistent, isolated, headless poisson instances managed remotely over Telegram (one systemd-nspawn container per instance, one forum topic per instance), plus an optional unconfined host-direct instance kind for real approvals with no isolation. (details, host mode)

🧰 Built-in skills

Fifteen skills ship baked into the px binary β€” no setup, no config directory needed. The skill tool loads one by bare name (each is a SKILL.md) and works the same for subagents as it does in the main session. Fourteen live under the code/ topic group (internal/skills/builtin/code/): code-quality, code-review, tdd, feature-impact, review-pr, stacked-diff-review, check-work, council, grilling, create-issue, create-pr, sandbox, create-skill, ponytail β€” covering code review, TDD discipline, blast-radius impact analysis, independent self-verification, multi-persona critique, issue/PR drafting, and shipping the smallest correct diff. canvas (internal/skills/builtin/canvas/) is ungrouped, since building a self-contained HTML report/dashboard instead of a markdown wall applies to any task, not just code.

Skills nest one directory deep for topic grouping: <name>/SKILL.md (ungrouped) or <group>/<name>/SKILL.md. Group is purely organizational β€” it changes the directory and the system-prompt listing (grouped skills show as a bare-name line under one group description, instead of each carrying its own description), never lookup or invocation.

Add your own under ~/.poisson/skills/<name>/SKILL.md or ~/.poisson/skills/<group>/<name>/SKILL.md β€” a user skill with the same name as a built-in one overrides it regardless of group, so you can customize any of the thirteen without touching the binary. /reload rediscovers user skills without restarting.


πŸš€ Install & run

Requires Go 1.25+. Everything else is vendored by the module graph. The quickstart's go install above puts px at $(go env GOPATH)/bin/px β€” make sure that's on your PATH. Building from a local clone instead (e.g. to hack on it) still works: ./build.sh (compiles CGO_ENABLED=0 -> ./px).

Authenticate a provider (stored in ~/.poisson/auth.json, mode 0600):

px login anthropic   # Claude Pro/Max β€” browser OAuth (subscription billing)
px login openai      # ChatGPT Plus/Pro β€” browser OAuth (Codex subscription)
px login xai         # SuperGrok β€” browser OAuth
px login openrouter  # plain API key from https://openrouter.ai/keys
px login ollama      # local Ollama at http://localhost:11434 (no auth needed)
# llamacpp: local llama-server at http://localhost:11212, no auth needed either

# anthropic/openai on a headless/SSH host: add --manual to paste the auth
# code instead of waiting on a local browser callback that can't be reached.
px login anthropic --manual

Then just:

px                  # interactive TUI
px sessions         # list past sessions
px resume <id>      # open the TUI resumed straight into a past session
px cost             # total spend
px version

# Headless one-shot (no TUI). Risky bash is denied unless --yolo (for *you*
# in a real shell only β€” the agent cannot nest it via the bash tool).
px -p "summarize this repo"
px -p --yolo "run the test suite and fix failures"

πŸ€– Providers & models

Anthropic (Claude, OAuth or api_key), OpenAI (ChatGPT/Codex subscription, OAuth), xAI (Grok, SuperGrok OAuth), OpenRouter (400+ models, API key), Ollama (local daemon or cloud, no auth), and llama.cpp (local llama-server, no auth). Each ships a curated default and tracks that provider's latest model lineup β€” any <provider>/<model-name> works even before poisson has a built-in entry for it (see Adding a custom / unlisted model). llamacpp/Ollama share the same wire format; alpaca discovers cached GGUF models and launches llama-server with sane defaults.

Custom provider instances: [custom_providers.<name>] defines a second (third, ...) Ollama-compatible endpoint under a name you pick β€” e.g. a daemon on a remote host, alongside your local one:

[custom_providers.bastion]              # a second Ollama instance, any name
type = "ollama"                         # only "ollama" supported today
base_url = "http://bastion-host:11434"
model = "laguna-s-2.1:q4_K_M"           # optional β€” omit to discover live via /api/tags

Works everywhere a built-in provider does: /providers, /model, px -p bastion/<model>, subagent provider pinning. No login needed (same as ollama/llamacpp). Curate a model with the same [models.<name>."<model>"] table a built-in provider uses (context window, effort levels, vision).

Switch anytime: /model, /effort, /providers (or the Ctrl+P palette). Reasoning effort levels: low Β· medium Β· high Β· xhigh Β· max (default medium; each model advertises which it supports).


βš™οΈ Configuration

Everything lives in ~/.poisson/ (created on first run, mode 0700):

File What
~/.poisson/config.toml all settings (created with commented defaults on first run)
~/.poisson/auth.json OAuth tokens / API keys (mode 0600)
~/.poisson/poisson.db SQLite: sessions, messages, tool/API calls, FTS index

config.toml is written for you with every option commented out β€” uncomment to override. The knobs (each [<provider>].model sets that provider's default model β€” the one used when it's the active provider, or when you switch to it via /providers without naming a model):

# Default model as "<provider>/<model>" β€” sets the provider and its model in one
# line (a bare "<model>" applies to the default provider). Overrides the
# per-provider settings below.
# model = "anthropic/claude-sonnet-5"

# Reasoning effort for every request (low | medium | high | xhigh | max)
# effort = "medium"

[provider]
# default = "ollama"                 # anthropic | ollama | xai | openai | openrouter | llamacpp

[anthropic]
# model = "claude-opus-5"          # or claude-sonnet-5 / claude-fable-5-1 (all adaptive-reasoning)
# classifier = "claude-sonnet-5"    # bash-risk classifier for this provider
# api_key = "sk-ant-..."             # optional; OAuth (auth.json) preferred

[openai]
# model = "gpt-5.6-terra"           # via ChatGPT Codex subscription (px login openai)
                                    # or gpt-5.6-sol / gpt-5.6-luna / gpt-5.5

[xai]
# model = "grok-build"          # via SuperGrok subscription (px login xai)

[openrouter]
# model = "deepseek/deepseek-v4-flash-0731"  # via API key (px login openrouter)
# api_key = "sk-or-..."                       # optional; prompted by px login openrouter
# base_url = "https://openrouter.ai/api/v1"

[ollama]
# base_url = "http://localhost:11434"
# model = "glm-5.2:cloud"

[llamacpp]
# base_url = "http://localhost:11212"     # local llama-server instance
# model = "unsloth/Laguna-S-2.1-GGUF"

# [custom_providers.bastion]              # a second Ollama instance, any name
# type = "ollama"                         # only "ollama" supported
# base_url = "http://bastion-host:11434"
# model = "laguna-s-2.1:q4_K_M"           # optional β€” omit to discover live via /api/tags
#
# [models.bastion."laguna-s-2.1:q4_K_M"]  # same [models.*] schema as any built-in provider
# context_window = 262144

# [subagent]
# trusted_providers = ["anthropic", "openai"]  # skip the approval popup spawning subagents between these

[classifier]
# model = ""                         # fallback bash-risk classifier (bare = all providers, "provider/model" = one)

[compaction]
# threshold = 0.85                   # compact when context passes this fraction
# reserve_tokens = 16384             # ...or this much headroom is left, whichever is hit first
# model = ""                         # summarizer model or provider/model (default: session model)

[tui]
# theme = "dark"                     # dark | light
# show_tokens = true                 # context % in the status bar
# show_cost = true                   # $ in the status bar

# Pricing (per 1M tokens) and model metadata (context window, effort levels,
# vision) overrides β€” see "Adding a custom / unlisted model" below for the
# full [pricing.*] / [models.*] example.

Adding a custom / unlisted model

Any model = "<provider>/<model-name>" works right away, even if poisson has no built-in entry for it β€” you just get a generic fallback context window and no effort levels/vision/adaptive-thinking. Two optional config.toml blocks fill that in:

# 1. Teach poisson the model's real capabilities (shows up in /model, and
#    gates which reasoning-effort levels the picker offers).
[models.ollama."glm-5.2:cloud"]
context_window = 200000
effort_levels = ["low", "medium", "high"]
vision = true

# 2. Teach it what the model costs, so /cost and the status bar aren't
#    silently $0. Ollama already defaults every model to $0 (built-in
#    wildcard, since most are local); Anthropic/OpenAI/xAI have no such
#    wildcard, so an unlisted model on those three shows $0 cost until you
#    add its rates here.
[pricing.ollama."glm-5.2:cloud"]
input = 0.5
output = 2.0

Both blocks key on the exact model name (quoted, since it usually contains ./:), under [models.<provider>."<model>"] / [pricing.<provider>."<model>"]. Pricing (only) also matches by prefix if the key ends in * β€” e.g. [pricing.ollama."*"] is the built-in fallback that prices every unlisted Ollama model at $0; model metadata overrides always need the exact name.


⌨️ Keys & commands

Bottom-bar keys (input focus):

Enter send Β· Tab switch input/conversation focus Β· Ctrl+V image
Ctrl+F find Β· Ctrl+P palette Β· Ctrl+L effort Β· Ctrl+T fold thinking
Ctrl+E expand tool Β· Ctrl+M model picker Β· Ctrl+S session picker
Ctrl+B /btw prompt Β· Ctrl+R/Ctrl+N step input history
Ctrl+G finish subagents now Β· Shift+Tab cycle fast/paranoid/yolo approval mode
Esc cancel running turn Β· Ctrl+C clear input (twice to exit)

Click-drag selects text (auto-scrolls past the edge); Ctrl+Y copies to the system clipboard via OSC 52 (works over SSH) β€” plain Ctrl+ because most terminals already claim Ctrl+Shift+C for their own copy action.

Slash commands: /help /status /subscriptions /model /effort /classifier-model /providers /sessions /resume /search /new /clear /name /compact /cost /reload /sandbox /btw /openai-reset-usage /quit. Type @ to fuzzy-attach a file (or @image.png for an image).

/classifier-model picks which model rates bash-command risk for the approval gate β€” usually worth pointing at something small and fast, since the answer is one word and an expensive session model otherwise pays its own rate per gated command. Mid-turn only unless made permanent via that provider's own classifier = "..." in config.toml (see the [anthropic] block above). Resolution order: /classifier-model pin β†’ [<provider>] classifier β†’ [classifier] model β†’ the session's own model.


πŸ“¦ Dependencies β€” deliberately tiny

poisson has 3 direct dependencies (modernc.org/sqlite, golang.org/x/term, golang.org/x/image); everything else is stdlib.


🧭 Design

  • Single static binary β€” CGO_ENABLED=0, one file, copy it anywhere.
  • Local-first & private β€” your data lives in ~/.poisson/poisson.db. No telemetry, no analytics, no phone-home.
  • Suckless-ish β€” simplicity over features, delete-before-add, readable code.
  • Tested without the network β€” the suite mocks every provider; it never makes a real API call.

πŸ›°οΈ px orchestrate β€” Telegram-driven agent orchestrator

Runs persistent, isolated, headless poisson instances on a dedicated host, managed remotely from a Telegram group β€” one forum topic per instance. Full design/rationale: docs/orchestrator-plan.md, docs/orchestrator-host-mode-plan.md.

Two instance kinds:

Kind Command Isolation Approvals
Box (default) /new, /new-box systemd-nspawn container, real root minus CAP_SYS_MODULE, host networking yolo (auto-approved β€” blast radius is one disposable container)
Host-direct /new-host none β€” runs directly on the orchestrator host real Telegram approval round-trip, always

Host-direct is functionally the same as SSHing into the orchestrator host yourself and running px there. It's opt-in at the config level and requires an explicit confirmation flag on every single invocation β€” see Β§4/Β§5 below.

Every instance also gets a send_file tool: the agent can attach a local file to its output and it's delivered as real Telegram media in the instance's topic β€” images render inline (sendPhoto), everything else comes through as a downloadable document (sendDocument), up to 20MB.

1. Prerequisites

  • A Linux host you're comfortable dedicating to this (systemd, arm64 or amd64), reachable over SSH as root. Referred to below as <host> β€” substitute your own hostname or IP everywhere you see it.
  • systemd-nspawn/machinectl/systemd-run (box instances only β€” apt install systemd-container on Debian/Ubuntu).
  • A Telegram bot: message @BotFather, /newbot, save the token. In Bot Settings β†’ Group Privacy, turn privacy off (so the bot sees every message, not just ones addressed to it).
  • A Telegram group with Topics enabled (this auto-promotes it to a supergroup) β€” the group where you'll run everything. Add the bot as an admin with "Manage Topics" and "Post/Delete Messages" permissions.

2. Find your chat id and user id

Post any message in the group, then:

curl -s "https://api.telegram.org/bot<TOKEN>/getUpdates" | jq .

Read message.chat.id (a large negative number for a supergroup) and message.from.id (your own numeric Telegram user id) from the response. Neither is secret on its own, but treat them the same as any other credential-adjacent config value β€” don't paste them into a public issue/PR.

3. Host prep (box instances only)

ssh root@<host> 'apt install -y systemd-container
mkdir -p /var/lib/machines /var/lib/px-orchestrate/instances'

Build a golden rootfs once β€” every box instance is cloned from it:

ssh root@<host> 'machinectl pull-tar https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-arm64-root.tar.xz px-golden'

(use the amd64 tarball on an x86_64 host). Inside the image, install what instances need and disable the guest's own network management (nspawn's --resolv-conf=bind-host handles DNS instead):

ssh root@<host> 'systemd-nspawn --directory=/var/lib/machines/px-golden --bind-ro=/etc/resolv.conf -- \
  bash -c "apt update && apt install -y git ripgrep ca-certificates curl jq && \
  systemctl disable systemd-networkd systemd-resolved systemd-networkd.socket && \
  passwd -l root && mkdir -p /root/.poisson /work"'

4. Deploy px

CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o px ./cmd/px   # arm64; amd64 for an x86_64 host
scp px root@<host>:/usr/local/bin/px.new
ssh root@<host> 'mv /usr/local/bin/px.new /usr/local/bin/px && chmod 755 /usr/local/bin/px'

Provider credentials for host-direct instances (and for px orchestrate itself, which needs its own session) come from /root/.poisson/auth.json on <host> β€” px login <provider> there, or copy over one provider's entry from your own ~/.poisson/auth.json (never the whole file to a shared host). Box instances get their own scoped copy automatically, generated per instance β€” see AuthorizedProviders in docs/orchestrator-plan.md.

5. Configure [orchestrator]

Append to /root/.poisson/config.toml on <host>:

[orchestrator]
chat_id = -1001234567890                 # from step 2, negative
allowed_user_ids = ["123456789"]         # from step 2 β€” everyone else is silently ignored
allowed_models = ["anthropic/claude-sonnet-5"]
default_model = "anthropic/claude-sonnet-5"
max_instances = 5                        # box instances only
state_dir = "/var/lib/px-orchestrate"
image = "px-golden"

# Host-direct instances β€” leave both at their defaults (off/unlimited-once-
# allowed) unless you specifically want /new-host available:
# allow_host_instances = true
# max_host_instances = 0                 # 0 = unlimited once allowed

The bot token itself does not go in config.toml β€” see the next step.

6. Secrets + systemd unit

ssh root@<host> 'install -m 600 /dev/stdin /etc/px-orchestrate.env <<EOF
POISSON_TELEGRAM_TOKEN=<your bot token>
EOF'
scp deploy/px-orchestrate.service root@<host>:/etc/systemd/system/
ssh root@<host> 'systemctl daemon-reload && systemctl enable --now px-orchestrate'

Verify: ssh root@<host> 'HOME=/root px orchestrate --config-check' prints the fully resolved config (token shown as present/missing only, never the value itself). --dry-run (no root/systemd-nspawn required) smoke-tests the command dispatch against fake instances β€” nothing real gets created.

7. Commands (in the Telegram group)

/new [name] [repo-url]        create a box instance (alias for /new-box)
/new-box [name] [repo-url]    same, explicit
/new-host [name]              refused with a warning + the exact re-invocation
/new-host [name] --confirm-unconfined-host
                               actually creates a host-direct instance
/list                         every instance's name/status/model
/status                       this instance's status, cost, sessions
/model <provider/model>       switch this instance's model (next turn)
/suspend / /resume            power off (keep state) / power back on
/kill                         permanently destroy this instance
/approve / /deny [reason]     resolve a pending bash-approval (host instances only)
/cancel                       stop the current turn

Send a plain message (no /) in an instance's own topic to give it a task.


poisson Β· run px Β· /help for the tour

About

opinionated coding agent you run in your terminal

Resources

Stars

6 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages