Terminal AI coding agent
Terminal AI coding agent. Seven LLM providers (OpenAI, Anthropic, Claude Code, SGLang, Bedrock, Vertex, custom), model-driven tool calling gated by hooks, prompt-injecting skills, sub-agents, an MCP client/server, and SQLite session history — over a candy-core render loop with Markdown replies. Runs offline out of the box.
composer require sugarcraft/sugar-crush
composer install
./bin/sugarcrush
# With no configuration it runs the offline EchoProvider through the full
# engine — zero network, zero keys. Point it at a real model instead:
export SUGARCRUSH_PROVIDER=openai
export OPENAI_API_KEY=sk-...
export SUGARCRUSH_MODEL=gpt-4o # optional; provider default otherwise
./bin/sugarcrush
# One-shot (never attaches to the TTY, never opens the alt-screen):
sugarcrush -p "explain the Width helper"
sugarcrush -p "audit this" --output-format json | jq -r .result
SUGARCRUSH_PROVIDER accepts openai, anthropic, claude-code, sglang, bedrock, vertex or custom. To skip PHP SDKs entirely, set SUGARCRUSH_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, Glob, Grep, WebFetch, WebSearch, Doctor and Skill, all jailed to the project root. 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. 12 built-ins, plus ~/.sugar-crush/skills and <project>/.sugar-crush/skills.pcntl_fork worker pool; teammates claim tasks from a flock-backed SQLite task list inside isolated git worktrees.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.Model/Program tree with buffer-diffed output: Markdown-rendered replies via CandyShine, live tool-call lifecycle, unified diffs for Edit results, inline images via candy-mosaic, mouse zones and a command palette.| 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.