Terminal AI coding agent
Terminal AI coding agent. Seven LLM providers (OpenAI, Anthropic, Claude Code, SGLang, Bedrock, Vertex, custom), twenty-eight built-in tools gated by hooks and a six-mode permission gate, workspace checkpoints with undo and rewind, a context engine that prunes and compacts on its own, sub-agents you can watch and message, memory, skills, MCP, workflows, SQLite sessions, a WebSocket server with a browser UI, and an Agent Client Protocol agent for editors — over a sugar-core render loop with Markdown replies. Runs offline out of the box.
composer require sugarcraft/candy-code
composer install
./bin/candy-code
# With no configuration it runs the offline EchoProvider through the full
# engine — zero network, zero keys. Point it at a real model instead:
export CANDY_CODE_PROVIDER=openai
export OPENAI_API_KEY=sk-...
export CANDY_CODE_MODEL=gpt-4o # optional; provider default otherwise
./bin/candy-code
# One-shot (never attaches to the TTY, never opens the alt-screen):
candy-code -p "explain the Width helper"
candy-code -p "audit this" --output-format json | jq -r .result
CANDY_CODE_PROVIDER accepts openai, anthropic, claude-code, sglang, bedrock, vertex or custom. To skip PHP SDKs entirely, set CANDY_CODE_BACKEND_CMD to a script that reads JSON history on stdin and writes the reply to stdout. See docs/ENVIRONMENT.md for the full environment-variable reference.
ProviderInterface — OpenAI, Anthropic, Claude Code CLI, SGLang, Bedrock, Vertex, custom — with capability introspection, streaming, function calling, embeddings and per-model cost.EngineBackend calls the provider, runs the returned tool calls through the hook gate, feeds results back, and repeats until the model answers without calling tools or maxSteps is hit.Bash, Read, Edit, Write, ApplyPatch (multi-file patches, all or nothing), Glob, Grep, WebFetch, WebSearch, doctor, Skill, Lsp, Memory, RepoMap, Todo, Prune, Compress, Recall, Workflow, Team, AskUser, PlanExit, Task, the messaging tools SendMessage, Subagents and InterruptAgent, and BoardRead/BoardPost for parallel batches — path tools jailed to the project root, edits refused on a file that changed since it was read, plus every trusted MCP server's tools. Implement Tools\Tool for your own.ProtectFilesHook, ConfirmRemoveHook, AuditHook, plus BashEscapeDenyHook when a worktree root is set and PermissionGateHook last, which adapts the six-mode PermissionGate onto the same chain. YAML config and external ScriptHook supported, the latter selecting its outcome by exit code.SKILL.md files inject prompt context. The system prompt carries only name + description; the model pulls a full body through the Skill tool. 8 built-ins, plus ~/.candy-code/skills, <project>/.candy-code/skills and skills imported from Claude Code and opencode.Task tool delegates to presets with their own model, tools, skills and permission mode — in parallel, nested, in the background or in a git worktree of their own, every run resumable; a preset model the provider does not serve falls back to the session's model. Each run gets a live line, an agents strip and an Agent View with its own transcript, where you can message, pause, cancel or send it to the background (Ctrl+X b). The agent messages, waits on and interrupts its sub-agents with SendMessage, Subagents and InterruptAgent, sub-agents reply to their parent, a parallel batch shares a board, and teammates claim tasks from a shared SQLite board through the Team tool.WorkflowEngine runs multi-stage agent pipelines — sequential, fan-out parallel, chained pipeline, and task-then-verifier — from a PHP DSL or YAML, with SIGINT-safe pause and stage-granularity resume..mcp.json with ${VAR} interpolation, plus stdio/HTTP servers to host your own tools. Per-preset mcpServers allowlists are enforced, not decorative.Prune/Compress/Recall tools, re-injection after a compaction, and recovery from overflows, cut-off replies and dropped streams.Alt+M toggles plan mode and the model leaves it with PlanExit, asking you to approve its plan; auto puts security findings to you and can have the title model review the rest), argument-scoped rules; shell lines proven read-only — sed scripts that only read, simple awk programs, for/while read loops over read-only bodies — run unasked (permissions.autoAllowReadOnly); a remembers each part of a chain and answers at once, e edits the scope first, a grant ignores a leading in-project cd, and /permissions mode / /permissions revoke switch the mode and drop grants; an optional bubblewrap sandbox for Bash, and a per-turn workspace checkpoint behind /undo, /redo, /rewind and /diff, with opt-in auto-commit, post-edit lint, LSP diagnostics and auto-test.Memory tool with BM25 recall, auto-consolidation at the end of a turn, a periodic dream pass, and a git history you can restore from.candy-code serve runs sessions headless behind an authenticated WebSocket speaking candy-code.v1; the CandyCodeWeb browser UI and candy-code attach drive them. Remote access is gated by --allowed-host and a --allowed-ips client allow-list, and --allow-dir-browse (confined to --browse-root) lets a client pick a new session's project directory — as /new's folder picker does in the TUI. candy-code acp serves editors — Zed, JetBrains, Neovim — over the Agent Client Protocol on stdio.@file, @diff and @https://… attachments, $skill mentions for one turn, !command shell lines, opt-in AI! comments saved in your editor, /handoff into a fresh session seeded with a state summary, and /newrule to draft a project rule from the conversation.Model/Program tree with buffer-diffed output: Markdown-rendered replies via SugarMarkdown, live tool-call lifecycle, unified diffs for Edit results, inline images via sugar-image, mouse zones and text selection via sugar-mouse, and a command palette whose fuzzy filter requires every typed character to match. A slash command is offered when any such alignment starts on its first character, so /ab still finds a__ab.| Class | Method | Description |
|---|---|---|
| ProviderInterface | complete(CompleteRequest): CompleteResponse | The single LLM abstraction — one batch completion |
| ProviderInterface | completeStream(CompleteRequest): \Generator | Same call, yielded as streaming chunks |
| ProviderInterface | supportsStreaming(), supportsVision(), contextWindow(), costPer1kTokens(model, direction) | Capability + cost introspection, so callers do not guess |
| EngineBackend | new(provider, model) | Factory — bridges the chat Backend seam to the agent engine |
| EngineBackend | withTools(tools), withHooks(manager), withSkillRegistry(registry), withMaxSteps(n) | Fluent wiring of the agent loop (immutable — each returns a new instance) |
| EngineBackend | complete(history, ?onToken, ?onEvent): Message | Run one bounded agentic turn, emitting tool-lifecycle events |
| HookManager | registerBuiltIns() | Registers ProtectFilesHook, ConfirmRemoveHook, AuditHook |
| HookManager | preToolUse(HookContext): HookResult | Gate a tool call — allow / deny / modify / ask |
| HookManager | resolveAsk(ask, approved, feedback) | Settle an answered ASK into a final allow or deny |
| SkillRegistry | findForPrompt(prompt): array | Match discovered SKILL.md files to the current turn |
| SkillRegistry | getForPaths(paths): array | Path-scoped skills for files a tool just touched |
| SkillRegistry | isAutoInvocable(name), isUserInvocable(name), isContextFork(name) | Frontmatter flag enforcement, not decoration |
| AgentPreset | __construct(name, description, tools, model, permissionMode, skills, mcpServers, …) | Readonly sub-agent descriptor — model, tool allow/deny, skills, MCP allowlist, effort, isolation |
| McpClient | startServers(), stopServers() | Launch / tear down the configured stdio + HTTP MCP servers |
| McpClient | listTools(), callTool(server, tool, args), callToolByName(tool, args) | Discover and invoke MCP tools, filtered by the active preset's allowlist |
| PermissionGate | evaluate(ToolCall): PermissionDecision | Evaluate a call against the active mode, with a mode-independent rm-rf circuit breaker |
| PermissionGate | mode(): PermissionMode | One of default, accept-edits, plan, auto, dont-ask, bypass-permissions |
| WorkflowEngine | run(workflowPath, context): WorkflowResult | Execute a YAML multi-stage agent pipeline |
| WorkflowEngine | runFromPhp(workflowClass, context): WorkflowResult | Same, from the PHP builder DSL |
| WorkflowEngine | pause(id), resume(id), getStatus(id) | Stage-granularity pause / resume across a signal |
| SessionStore | createSession(id, provider, model, ?systemPrompt, ?name) | Open a session row in the SQLite (WAL) store |
| SessionStore | addMessage(sessionId, message), addToolCall(sessionId, messageId, toolCall) | Append a turn and its tool calls, FK-enforced |
| SessionStore | listSessions(limit), forkSession(id), renameSession(id, name) | Back the /sessions, /fork and /rename commands |
| SessionStore | pruneSessions(daysOld, ?exemptSessionId): int | Opt-in retention sweep; never drops a named or resuming session |
VHS-recorded GIFs of every example shipped with the app. Regenerated automatically on every push that touches the source.