Sakura Development Guide
Table of Contents
- Repository Structure
- Prerequisites
- Environment Setup
- Running Locally
- Testing
- Project Conventions
- Architecture Patterns
- Available Scripts
Repository Structure
Sakura is a monorepo with three independent packages:
sakura/
├── src/ # CLI (main product)
├── api/ # Express API → AWS Lambda
├── web/ # Next.js marketing/dashboard site
├── test/ # CLI test suite
└── scripts/ # Deployment scripts
Sources: package.json, CLAUDE.md
Prerequisites
| Requirement | Version | Notes |
|---|---|---|
| Node.js | ≥ 20 | Required by all three packages |
| npm | bundled with Node | Used for all packages |
| AWS CLI | any | Required for AWS passthrough features |
| TypeScript | ^5.8.2 | Dev dependency in all packages |
Sources: package.json, api/package.json
Environment Setup
1. Clone and install
git clone <repo-url>
cd sakura
# Install CLI dependencies
npm install
# Install API dependencies
cd api && npm install && cd ..
# Install web dependencies
cd web && npm install && cd ..
2. Configure the CLI
Run sakura init (or create manually) to generate .sakura/config.toml:
version = 1
defaultProfile = "default"
[profiles.default]
provider = "openai"
model = "gpt-5.2"
temperature = 0.2
[profiles.coding-claude]
provider = "anthropic"
model = "claude-sonnet-4-20250514"
temperature = 0.2
Sources: src/config.ts
3. API keys
The CLI resolves keys in this priority order:
- Environment variable —
OPENAI_API_KEYorANTHROPIC_API_KEY - Bundled keys —
dist/keys.json(for packaged distributions) - AWS SSM —
/sakura/api/openai-keyor/sakura/api/anthropic-key - API proxy —
sakura login(useshttps://api.sakura-ai.dev)
Sources: src/auth/keys.ts, src/repl/index.ts
For local development, set environment variables:
export OPENAI_API_KEY=sk-...
export ANTHROPIC_API_KEY=sk-ant-...
4. API environment (.env)
cd api
cp .env.example .env # if available, or create manually
The API reads secrets from AWS SSM Parameter Store in production. For local dev, set them directly in api/.env.
Sources: api/src/services/secrets.ts
5. MCP servers (optional)
Create .sakura/mcp.json in your project root or ~/.sakura/mcp.json globally:
{
"mcpServers": {
"my-server": {
"command": "npx",
"args": ["-y", "@my/mcp-server"],
"trustLevel": "trust_read_only"
}
}
}
Environment variables in MCP config support ${VAR_NAME} expansion.
Sources: src/mcp/config.ts
Running Locally
CLI (interactive REPL)
# Development mode (ts-node, no build required)
npm run dev
# Or after building
npm run build
node dist/sakura.js
Running sakura with no arguments starts the interactive REPL. Running with a subcommand (e.g., sakura ask "...") uses one-shot mode.
Sources: src/sakura.ts
API server
cd api
npm run dev # Loads .env, runs ts-node ESM loader
The API runs as a standard Express server locally. In production it wraps with serverless-http for Lambda.
Sources: api/src/server.ts, api/src/lambda.ts
Web frontend
cd web
npm run dev # Next.js dev server (hot reload)
Sources: web/next.config.mjs
Testing
All tests live in test/ and use Vitest.
Sources: test/
Run all tests
npm test
Run a single test file
npm test -- test/routing.test.ts
Filter by test name
npm test -- --grep "classifyAwsRisk"
Watch mode
npm test -- --watch
Test files and what they cover
| File | Coverage |
|---|---|
test/aws-guard.test.ts | classifyAwsRisk — readonly vs mutating classification |
test/routing.test.ts | routeIntent — keyword-based intent routing |
test/config.test.ts | Config loading, TOML parsing, profile resolution |
test/plan-store.test.ts | PlanStore save/load, Zod schema validation |
test/sessions.test.ts | SessionStore persistence |
test/tool-registry.test.ts | Tool registration and lookup |
test/tools.test.ts | Individual tool execution |
test/slash-commands.test.ts | REPL slash command parsing |
test/diff.test.ts | Unified diff computation |
test/format.test.ts | Terminal formatting helpers |
test/prompts.test.ts | Prompt building logic |
test/files.test.ts | File read/write utilities |
test/utils.test.ts | Miscellaneous utilities |
Sources: test/, package.json
Note: The API (
api/) has no test suite currently. The web (web/) has no test suite currently.
Project Conventions
TypeScript
- Strict mode everywhere —
"strict": truein alltsconfig.jsonfiles - ESM modules —
"type": "module"in allpackage.jsonfiles; use.jsextensions in imports even for.tssource files - Target: ES2022 with
moduleResolution: "Bundler" - No implicit
any
Sources: tsconfig.json, api/tsconfig.json
File naming
- kebab-case for filenames:
aws-handler.ts,tool-loop.ts,plan-store.ts - PascalCase for classes:
AwsCliRunner,PlanStore,SessionStore,McpClient - camelCase for functions and variables
Module structure
Each subsystem is a directory with focused, single-responsibility files:
src/
├── aws/ # guard.ts (risk classification), runner.ts (CLI execution)
├── auth/ # keys.ts (API key resolution), session.ts (JWT storage)
├── mcp/ # config.ts, client.ts, lifecycle.ts, bridge.ts, builder.ts
├── planning/ # planner.ts, store.ts, multi-step.ts, subagent.ts
├── providers/ # provider.ts (interface), openai.ts, anthropic.ts, api.ts
├── repl/ # index.ts, tool-loop.ts, aws-handler.ts, slash-commands.ts
├── scaffold/ # registry.ts, command.ts, build.ts, phase.ts
├── sessions/ # store.ts
├── tools/ # registry.ts, executor.ts, index.ts, implementations/
└── util/ # format.ts, context.ts, git.ts, search.ts, etc.
Sources: src/
Provider interface
All LLM providers implement the LlmProvider interface. Never call provider SDKs directly outside of src/providers/:
export interface LlmProvider {
readonly name: ProviderName;
complete(req: ProviderRequest): Promise<ProviderResponse>;
stream?(req: ProviderRequest): AsyncIterable<string>;
}
Sources: src/providers/provider.ts
Tool registration pattern
Tools self-register at import time using registerTool(). The src/tools/index.ts file imports all implementations to trigger registration:
// src/tools/index.ts
import "./implementations/read.js";
import "./implementations/write.js";
// ...
Each tool file calls registerTool({ name, description, trustLevel, parameters, execute }) at module load.
Sources: src/tools/index.ts, src/tools/registry.ts
Trust levels
Every tool declares a TrustLevel that controls whether user confirmation is required:
| Level | Behavior |
|---|---|
trusted | Always executes without prompt |
trust_read_only | Executes for reads; prompts for writes (e.g., AWS mutating commands) |
trust_working_dir | Executes within cwd; prompts for paths outside |
not_trusted | Always prompts |
Sources: src/tools/registry.ts, src/tools/executor.ts
Plan/apply safety gate
Mutating operations (AWS commands, code changes) always go through a plan → confirm → apply cycle:
Sources: src/aws/guard.ts, src/planning/store.ts, src/cli.ts
Prompt management
System prompts are fetched from the Sakura API (/api/v1/prompts/:id) and cached for 60 seconds. Never hardcode system prompts inline — use getPrompt(id, vars):
const prompt = await getPrompt("aws_translate", { user_prompt: userInput });
Sources: src/prompts-client.ts
Error handling
- Use
ExitError(exitCode)to terminate the process with a specific exit code — it is caught inmain()and callsprocess.exit() - Tool errors return
{ success: false, output: "", error: "..." }— never throw from toolexecute() - Provider errors propagate as thrown
Errorinstances
Sources: src/errors.ts, src/tools/executor.ts
Context management
The tool loop auto-compacts conversation history when it exceeds 32,000 characters, using the compact_conversation prompt. History is also trimmed per-iteration to keep API Gateway payloads under the 30-second timeout limit.
Sources: src/repl/tool-loop.ts, src/util/context.ts
Architecture Patterns
Intent routing
Sources: src/routing.ts, src/repl/index.ts
MCP integration
MCP tools are registered with the prefix mcp__<serverName>__<toolName> and become available to the LLM tool loop automatically.
Sources: src/mcp/config.ts, src/mcp/lifecycle.ts, src/mcp/bridge.ts
Plugin system
Plugins are invoked via @tag prefixes in user input (e.g., @optimize build the auth module):
// Parsed before routing
const { input, tags } = parsePluginTags(rawInput);
const transformed = await applyPlugins(input, tags, ctx);
Sources: src/plugins/registry.ts, src/plugins/optimize.ts
Available Scripts
CLI (root)
| Command | Description |
|---|---|
npm run dev | Run CLI with ts-node ESM loader (no build needed) |
npm run build | Compile TypeScript → dist/ |
npm test | Run all tests with Vitest |
npm test -- test/<file> | Run a single test file |
npm test -- --grep "<name>" | Filter tests by name |
npm test -- --watch | Watch mode |
Sources: package.json
API
| Command | Description |
|---|---|
npm run dev | Dev server with .env file loading |
npm run build | Compile TypeScript → dist/ |
npm start | Run compiled server |
Sources: api/package.json
Web
| Command | Description |
|---|---|
npm run dev | Next.js dev server with hot reload |
npm run build | Static export → web/out/ |
Sources: web/next.config.mjs
Deployment scripts
| Script | Description |
|---|---|
scripts/deploy-api.sh |