A Claude-Code-look terminal agent whose harness is a headless server, so the same core can drive a CLI/TUI today and an app later. The server supports OpenAI-compatible models plus subscription-backed Codex, Grok, and Anthropic Agent SDK runtimes.
TUI (Ink) ──SSE──▶ server (Bun) ──▶ LangChain agent ──▶ Zen / Codex / Grok
│
└────────▶ Anthropic Agent SDK ──▶ Claude subscription OAuth
packages/protocol— shared wire contract:AgentEvent, RESTROUTES,sse()/readSSE(). The frozen source of truth.packages/server—Bun.serveHTTP + SSE. Runs the lean LangChain loop for OpenAI-compatible providers or the native Anthropic Agent SDK loop for Claude, translating both into the sameAgentEventstream and using the same Chunky tools.packages/tui— Ink client styled exactly like Claude Code (theme/components lifted fromkimi-2-6-code-main): terracotta rounded input box,✻banner, streaming text,⏺tool lines, spinner/status line,/slash menu. Reduces theAgentEventstream into a live transcript.
Requires Bun. Zen config is in .env (gitignored): ZEN_BASE_URL, ZEN_MODEL, ZEN_API_KEY, CHUNKY_PORT.
For Anthropic subscription OAuth, authenticate the real Claude CLI once with claude auth login --claudeai; Chunky reuses that OAuth session through @anthropic-ai/claude-agent-sdk.
bun install
# Terminal 1 — start the harness
bun run server # listens on http://localhost:4599
# Terminal 2 — the TUI (must be a real interactive terminal)
bun run tui -- --live # connects to the server, streams real GLM-5.2
# or, with no server:
bun run tui -- --mock # demo the UI against a fake streamVerified: typing a question in --live streams the real GLM-5.2 answer back into the Claude-Code UI.
- ✅ End-to-end pipe (TUI → SSE → DeepAgents → Zen → GLM-5.2).
- ✅ Persistence + resume: sqlite session/event store; reconnecting to a sessionId replays the transcript, survives a server restart.
GET /api/sessionsis the resume picker. - ✅ Provider registry + OAuth:
zen(API key) plusgrok/codexOAuth providers (ported from opencode) withauth.jsontoken storage and LangChain custom-fetch token injection./loginopens an arrow-navigable provider picker and auto-opens the browser (loopback PKCE flow, polls to confirm);GET /api/providers. - ✅ Live model catalog + picker (
/model): a fuzzy-searchable list of every provider's models — Zen's live/v1/modelsplus curated Codex/Grok sets, enriched with models.dev metadata./model add|hide|restore <provider> <model-id>changes the global catalog immediately;/model list <provider>shows its overrides. Hidden models leave existing sessions untouched, custom ids persist across restarts, and the agent can make the same explicit changes throughmanage_models. Reasoning models get low/medium/high/xhigh/max effort; Codex adds standard/fast speed. - ✅ Real nested threads:
spawn_threadlaunches a full independent child agent run on its own LangGraphthread_id, streaming events tagged with the child'sthreadIdover the session SSE; children can spawn children. The TUI renders the tree (run--threads, ctrl+t to expand/collapse). - ✅ Native Anthropic Agent SDK + OAuth: the
anthropicprovider uses the real SDK agent loop and Claude subscription OAuth, disables every Claude built-in tool and filesystem setting, injects Chunky'sread/bash/write/edit/spawn_threadtools through an in-process MCP server, uses Chunky's lean system prompt verbatim, streams into the existing protocol, and resumes SDK sessions by Chunky thread id. - ✅ Durable agent memory across restart: a
bun:sqliteLangGraph checkpointer (bun-sqlite-saver.ts, ported from the official better-sqlite3 saver which Bun can't load). Verified: teach the agent a fact, restart the server, ask in the same session — it remembers. Each thread_id keeps its own checkpoint. - ✅ Real coding tools: the lean
read/bash/write/edittools operate directly inWORKSPACE(CHUNKY_WORKSPACE || cwd), with file paths jailed to that root. GPT/Codex models instead receiveapply_patch(OpenAI V4A format, path-escape-guarded, all-or-nothing); Anthropic receives the same Chunky implementations through in-process MCP wrappers. - ✅ Goal mode (
/goal): set an objective and the agent works toward it autonomously — after each turn that ends without the goal being declared done, the server injects a hidden continuation nudge and runs another turn, until the model callsgoal_complete(with an evidence summary) orgoal_blocked(a genuine impasse), or a turn budget (default 20,/goal --turns N …orCHUNKY_GOAL_MAX_TURNS) is hit. Goal state is persisted per-session in sqlite (survives restart/resume) and streamsgoal.updatelifecycle markers into the transcript + agoal:segment on the status line./goalshows status,/goal pause|resume|clearmanage it; Esc pauses an active goal so it never silently resumes. Works on both the LangChain and native Anthropic-SDK runtimes (the three goal tools resolve the root session from any thread depth).GET/POST /api/sessions/:id/goal. Ported in spirit frompi-codex-goal/pi-goal. - ✅ Workflows-mode goals (
/goal --workflows <objective>, alias--dynamite): the goal agent becomes an orchestrator — the kickoff/continuation prompts tell it to delegate ALL substantive work to dynamic-workflowruns (each sub-agent a real child thread), judge results between runs, and finish with a verification workflow beforegoal_complete. Workflow tiers anchorsmall/mediumto the global active selection and routebigto the advisor, so an orchestrator on a premium model fans out on the everyday model. Sessions can carry a pinned model selection (sqlitesessions.selection) that overrides the global/modelchoice for their runs. Theworkflow+create_goaltools are now also exposed on the native Anthropic-SDK runtime, so a Claude orchestrator can orchestrate. - ✅ Shipit (
/shipit [notes]): hand a long, context-heavy planning conversation off to a fresh executor.POST /api/sessions/:id/shipinjects a hidden prompt telling the CURRENT session's model to distill everything agreed into a handoff brief (end state, decisions + why, constraints, pointers, verification checks) and call the newship_goaltool — which creates a new session in the same repo, pins it to the orchestrator model (the advisor when configured, else the active selection), sets a workflows-mode goal with the brief as objective, and starts it through the session bus. The brief lands as the new session's first visible message (provenance-labelledshipped from <session>), so opening it reads like a briefed fresh thread; the dirty session stays behind as the archive. - ✅ Remote access via relay (
bun run pair): pair a phone with a QR code, and the server dials out to a hosted relay (relay.chunky.to, private sibling repo../chunky-relay) over an outbound WebSocket — no port forwarding, works from any network. All traffic is end-to-end encrypted (X25519 pairing via the QR, XSalsa20-Poly1305 per frame; the relay only ever routes ciphertext and account presence). The phone side speaks the existingROUTES+SSE contract through a tunnel (@chunky/protocol/relay-clientis the reference client); relay accounts support Sign in with Apple, GitHub OAuth, and a dev-mode login. Non-loopback HTTP now requires a bearer token (serverTokenin settings.json) — direct LAN/Tailscale clients use that, the TUI/app on loopback need nothing. Verified by a cross-repo E2E suite (pairing → encrypted fetch → streamed SSE → store-and-forward → relay-blindness byte check). ⚠️ Known bug — Claude-family models via Zen error on tool calls.claude-*on the Zen gateway throwsInvalid response from "wrapModelCall"on any tool call (Zen's SSE sends emptyid/modelon chunks after the first, breaking LangChain's chunk aggregation; reproduces via raw curl, so it's upstream/Zen-side). Since the agent always has tools, useglm-5.2(or Codex) for tool work until this is worked around. Non-tool chat with Claude-on-Zen is unaffected.- ⬜ Concurrent (not just sequential) thread spawns; interrupt/steer mid-run; tool-approval (HITL); packaging to a binary; work around the Zen/Claude tool-call bug (patch the SSE stream, or use the native Anthropic provider).
Install the latest release (no prerequisites — if Bun isn't installed, the installer bootstraps a private copy under ~/.chunky/bun):
curl -fsSL https://chunky.to/install | bashRun chunky update to install the latest release, or chunky update --version vX.Y.Z to pin one. chunky update --check reports availability without changing files. Chunky checks for updates in the background at most once every 24 hours; network failures are ignored. The previous app is retained for one rollback: chunky update --rollback. Runtime state remains in ~/.chunky/state and is never replaced by updates.
Chunky is free software, licensed under the GNU Affero General Public License v3.0 (AGPL-3.0-only). You may use, modify, and redistribute it under those terms; if you run a modified version as a network service, you must make its source available to users. Copyright (C) 2026 Max Headley.