A pure-Go TUI agent. Single binary around 30 MB. No CGO. Cross-compiled for darwin + linux × amd64 + arm64.
Marketing copy and feature tour live at georgebuilds.github.io/carlos. This README is for getting carlos running and for Go developers who want to contribute.
brew install georgebuilds/tap/carlos
carlos
Or grab a tarball from Releases and drop carlos into your $PATH.
Onboarding takes ~30 seconds:
- Your name
- A provider (Anthropic, OpenAI, OpenRouter, Ollama, or Gemini) and an API key
- A model from a curated dropdown (OpenRouter shows live pricing)
- Whether to enable the background daemon (scheduled runs, gateway delivery)
- Optional Obsidian vault path
Everything lands in ~/.carlos/config.yaml (mode 0600). Re-enter any single screen later with carlos onboard --only providers (or models, daemon, gateway, etc.).
After onboarding you're in the chat TUI. Type a question. carlos answers, calls tools when needed (prompting for the risky ones), and keeps the transcript in a SQLite event log at ~/.carlos/state.db.
| Key | What |
|---|---|
Ctrl+F |
open the frame switcher |
/help |
full slash-command list |
!<cmd> |
run a shell command in your context |
/agents |
open the sub-agent manage view |
/whoami |
current frame, mode, provider, model |
/permissions |
layered approval state + audit log |
/mcp |
list configured MCP servers and their tools |
A !-prefixed line runs in your context without leaving the chat. Enter runs it in the foreground; Ctrl+Z pushes a running job to the background; Ctrl+J opens the jobs overlay (j/k to navigate, d to cancel, Esc to close). The slash variants are /shell <cmd>, /bg, and /jobs. A background job can also outlive the session entirely: carlos run "<cmd>" hands it to the daemon, and carlos jobs / carlos attach <id> / carlos logs <id> / carlos stop <id> drive it from anywhere.
The recording is scripted in demos/usershell-jobs.tape; regenerate it with vhs demos/usershell-jobs.tape (requires charmbracelet/vhs).
carlos please "<prompt>" # one-shot, no TUI
carlos research "<question>" # multi-phase deep research
carlos memory search <query> # FTS5 over conversation summaries
carlos schedule list|add|rm # cron + natural language
carlos gateway add # wizard to configure ntfy / Telegram / Signal
carlos gateway test <channel> # verify ntfy / Telegram wiring
carlos daemon enable|disable # background service
All accept -f <frame> (or --frame) to scope to a specific frame. carlos please takes a single positional prompt; multi-word prompts must be quoted (a single hyphenated token like say-hello works without quotes).
carlos please shows a live 3-row bordered status panel while it runs (current tool, streaming-text preview, tool counter + provider/model); the assistant's final reply prints below the panel on completion.
carlos research shows a live phase tracker (decompose, search, fetch, read, synthesize, verify) and writes a cited report under ~/.carlos/frames/<frame>/research/:
- Go toolchain version pinned by
go.mod(currently1.26.3). giton$PATH(the sub-agent sandbox usesgit worktree).- No CGO, no system libraries.
git clone https://github.com/georgebuilds/carlos
cd carlos
go test ./...
go build ./cmd/carlos
./carlos
Cross-compile checks are cheap; run them when touching anything that imports OS-specific paths:
GOOS=linux GOARCH=arm64 go build ./cmd/carlos
GOOS=darwin GOARCH=amd64 go build ./cmd/carlos
go test ./...is the floor. Current count is ~3490 tests across 41 packages.go vet ./...must be clean.- New code aims for 80%+ coverage on touched packages.
- The sub-agent + daemon + event log paths have integration tests; if you touch any of them, run
go test -race ./internal/agent/... ./internal/daemon/...at least once before pushing.
cmd/carlos/ main TUI binary + daemon entry points
internal/
agent/ tool-use loop, event log, supervision, layered approval
config/ ~/.carlos/config.yaml schema + onboarding state
daemon/ background scheduler (UDS + launchd / systemd)
farewell/ bordered exit-panel + brew-update probe
frame/ per-session frames (personal + N user-defined)
gateway/ chat-surface adapters (ntfy, Telegram, Signal stub)
mcp/ Model Context Protocol client + tool adapter
memory/ SQLite FTS5 + summarizer
miniyaml/ hand-rolled YAML for frontmatter
notes/ Obsidian vault index + cache (Goldmark + miniyaml)
projectctx/ per-project context loader (walks AGENTS.md / CLAUDE.md)
providers/ anthropic, openai, openrouter, ollama, gemini
research/ decompose → search → fetch → read → synthesize → verify
sandbox/ local + git-worktree execution
schedule/ cron + natural-language grammar
skills/ skill format, loader, inducer, judge, replay-eval
theme/ light / dark / NO_COLOR / configurable accent
tools/ every registered tool (notes_*, web_*, code_search, etc.)
tui/ bubbletea chat / manage / onboarding / slash registry
usershell/ `!` prefix shell driver, jobs overlay, history
workspace/ trusted-workspaces store + read-only bash classifier
skills/ bundled starter skills (calendar/, ...)
docs/ GitHub Pages site + llms.txt
| You want to | Look here |
|---|---|
| Add a tool | internal/tools/, register in tools.go |
| Add a slash command | internal/tui/slash/slash.go Builtins + handler in internal/tui/chat/ |
| Wire an MCP server | mcp_servers: block in ~/.carlos/config.yaml; client + adapter live in internal/mcp/ |
| Add a provider | internal/providers/<name>/, satisfy the Provider interface |
| Add a frame field | internal/frame/frame.go, then sysprompt + render helpers |
| Change permission rules | internal/agent/policy.go (LayeredApprover) |
| Add an event type | internal/agent/eventlog_sqlite.go + state machine |
| Add an onboarding screen | internal/tui/onboarding/screen_*.go + flow wiring |
- Single immutable event log is the source of truth. The projection replays from it.
- Atomic writes for any file containing user state:
temp + fsync + rename. Seeinternal/config/config.go:Savefor the canonical recipe. - File modes:
0700directories,0600secret-bearing files (config.yaml,state.db,trusted-workspaces.json, artifact blobs),0644elsewhere. - Prefer editing existing files to creating new ones.
- Default to no comments; add one only when the WHY is non-obvious.
A v* tag push fires .github/workflows/release.yml which runs goreleaser to build the four-arch tarballs, drafts a GitHub release, and bumps georgebuilds/homebrew-tap/Formula/carlos.rb. Publish the draft when ready (gh release edit v0.X.Y --draft=false).
GPL-3.0-or-later. See LICENSE.



