Closing the gap with top coding agents while staying "simple but powerful"
nano-opencode already has:
- ✅ Multi-provider support (Anthropic, OpenAI, OpenRouter, Gemini)
- ✅ Streaming output
- ✅ 13 core tools (read, write, edit, bash, glob, grep, etc.)
- ✅ MCP integration
- ✅ Session persistence
- ✅ Hooks system (safety, logging, timing, truncation)
- ✅ Agent architecture (Sisyphus default)
- ✅ Parallel tool execution (read-only batching)
- ✅ Todo enforcement loop
- ✅ Context compaction (TUI mode)
- ✅ Markdown rendering with syntax highlighting
| Feature | Claude Code | Codex CLI | nano-opencode |
|---|---|---|---|
| Plan Mode | ✅ | ❌ | ✅ Done |
| Web Search | ✅ | ❌ | ✅ Done |
| Image Support | ✅ | ✅ | ✅ Done |
| Sandbox | ✅ | ✅ | ✅ Done |
| Local Models | ❌ | ✅ | ✅ Ollama |
| Git Auto-commit | ✅ | ✅ | ✅ Done |
| Sub-agents | ✅ | ❌ | ✅ Done |
Goal: Let users review and approve implementation plans before execution.
Files to create/modify:
src/agents/plan.ts- Plan agent definitionsrc/tools/plan.ts- Plan-related toolssrc/cli.ts- Handle/plancommand
Implementation:
// src/agents/plan.ts
export const planAgent: AgentDefinition = {
name: 'plan',
description: 'Planning mode - proposes changes without executing',
systemPrompt: `You are in PLAN MODE.
Your job is to analyze the request and create a detailed implementation plan.
DO NOT execute any file modifications. Only propose changes.
Format your plan as:
1. Files to modify/create
2. Key changes in each file
3. Potential risks or considerations
4. Estimated complexity (low/medium/high)`,
tools: ['glob', 'grep', 'read_file', 'list_dir', 'todo_write'],
maxTurns: 10,
};User flow:
> /plan Add authentication to the API
◆ Plan Mode activated
Analyzing codebase...
📋 Implementation Plan:
1. Create src/auth/middleware.ts
2. Modify src/routes/index.ts
3. Add JWT dependency to package.json
Approve? [y/n/edit]
> y
◆ Executing plan...
Goal: Enable real-time information retrieval.
Files to create:
src/tools/websearch.ts
Implementation:
// src/tools/websearch.ts
import type { Tool } from '../types.js';
export const webSearchTool: Tool = {
name: 'web_search',
description: 'Search the web for current information',
parameters: {
type: 'object',
properties: {
query: { type: 'string', description: 'Search query' },
maxResults: { type: 'number', default: 5 },
},
required: ['query'],
},
execute: async (args) => {
const { query, maxResults = 5 } = args as { query: string; maxResults?: number };
// Use DuckDuckGo HTML API (no API key needed)
const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
const response = await fetch(url, {
headers: { 'User-Agent': 'nano-opencode/1.0' }
});
const html = await response.text();
// Parse results from HTML (simple regex extraction)
const results = parseSearchResults(html, maxResults);
return JSON.stringify(results, null, 2);
},
};
function parseSearchResults(html: string, max: number): SearchResult[] {
// Extract title, URL, snippet from DuckDuckGo HTML response
const results: SearchResult[] = [];
const regex = /<a[^>]*class="result__a"[^>]*href="([^"]*)"[^>]*>([^<]*)<\/a>/g;
let match;
while ((match = regex.exec(html)) && results.length < max) {
results.push({ url: match[1], title: match[2] });
}
return results;
}Goal: Allow image inputs for visual context.
Files to modify:
src/types.ts- Add image content typesrc/providers/*.ts- Handle image messagessrc/tools/screenshot.ts- New tool
Implementation:
// src/types.ts additions
export interface ImageContent {
type: 'image';
source: {
type: 'base64' | 'url';
media_type: 'image/png' | 'image/jpeg' | 'image/gif' | 'image/webp';
data?: string; // base64
url?: string;
};
}
export interface Message {
role: 'user' | 'assistant' | 'system';
content: string | (TextContent | ImageContent)[];
// ... existing fields
}// src/tools/screenshot.ts
import { spawn } from 'child_process';
import { readFileSync, unlinkSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
export const screenshotTool: Tool = {
name: 'screenshot',
description: 'Take a screenshot of the screen or a window',
parameters: {
type: 'object',
properties: {
region: { type: 'string', enum: ['full', 'window', 'selection'] },
},
},
execute: async (args) => {
const { region = 'full' } = args as { region?: string };
const tmpFile = join(tmpdir(), `screenshot-${Date.now()}.png`);
// Use platform-specific screenshot tool
const cmd = process.platform === 'darwin'
? ['screencapture', '-x', tmpFile]
: ['scrot', tmpFile];
await new Promise<void>((resolve, reject) => {
const proc = spawn(cmd[0], cmd.slice(1));
proc.on('close', (code) => code === 0 ? resolve() : reject());
});
const base64 = readFileSync(tmpFile, 'base64');
unlinkSync(tmpFile);
return JSON.stringify({
type: 'image',
media_type: 'image/png',
data: base64,
});
},
};Goal: Safe command execution with resource limits.
Files to create:
src/sandbox/index.tssrc/sandbox/docker.tssrc/sandbox/firejail.ts
Implementation:
// src/sandbox/index.ts
export interface SandboxOptions {
timeout?: number; // ms
maxMemory?: string; // e.g., '512m'
network?: boolean; // allow network access
readOnly?: string[]; // read-only paths
writeable?: string[]; // writeable paths
}
export async function runInSandbox(
command: string,
options: SandboxOptions = {}
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
const { timeout = 30000, maxMemory = '512m', network = false } = options;
// Try Docker first, fallback to firejail, then direct
if (await hasDocker()) {
return runInDocker(command, options);
} else if (await hasFirejail()) {
return runInFirejail(command, options);
} else {
console.warn('[sandbox] No sandbox available, running directly');
return runDirect(command, { timeout });
}
}Goal: Native local model support.
Files to create:
src/providers/ollama.ts
Implementation:
// src/providers/ollama.ts
import type { LLMProvider, Message, Tool, StreamChunk } from '../types.js';
export class OllamaProvider implements LLMProvider {
name = 'ollama';
private baseUrl: string;
private model: string;
constructor(model: string = 'llama3.2', baseUrl: string = 'http://localhost:11434') {
this.model = model;
this.baseUrl = baseUrl;
}
async chat(
messages: Message[],
tools: Tool[],
onChunk?: (chunk: StreamChunk) => void,
systemPrompt?: string
): Promise<Message> {
const response = await fetch(`${this.baseUrl}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: this.model,
messages: this.formatMessages(messages, systemPrompt),
tools: tools.map(this.formatTool),
stream: !!onChunk,
}),
});
if (onChunk) {
return this.handleStream(response, onChunk);
}
const data = await response.json();
return this.parseResponse(data);
}
private formatTool(tool: Tool) {
return {
type: 'function',
function: {
name: tool.name,
description: tool.description,
parameters: tool.parameters,
},
};
}
}Goal: Streamlined version control.
Files to create:
src/tools/git.ts
Implementation:
// src/tools/git.ts
export const gitCommitTool: Tool = {
name: 'git_commit',
description: 'Stage and commit changes with a generated message',
parameters: {
type: 'object',
properties: {
files: { type: 'array', items: { type: 'string' }, description: 'Files to stage (default: all modified)' },
message: { type: 'string', description: 'Commit message (auto-generated if not provided)' },
},
},
execute: async (args) => {
const { files, message } = args as { files?: string[]; message?: string };
// Stage files
const stageCmd = files?.length
? ['git', 'add', ...files]
: ['git', 'add', '-A'];
await runCommand(stageCmd);
// Get diff for message generation
const diff = await runCommand(['git', 'diff', '--cached', '--stat']);
// Use provided message or generate one
const commitMsg = message || await generateCommitMessage(diff);
// Commit
const result = await runCommand(['git', 'commit', '-m', commitMsg]);
return `Committed: ${commitMsg}\n\n${result}`;
},
};
async function generateCommitMessage(diff: string): Promise<string> {
// Simple heuristic-based message generation
const lines = diff.split('\n');
const stats = lines[lines.length - 2] || '';
if (stats.includes('insertions') && !stats.includes('deletions')) {
return 'Add new functionality';
} else if (stats.includes('deletions') && !stats.includes('insertions')) {
return 'Remove unused code';
} else {
return 'Update implementation';
}
}Goal: Spawn specialized agents for complex tasks.
Files to create:
src/agents/subagent.tssrc/tools/spawn.ts
Implementation:
// src/tools/spawn.ts
export const spawnAgentTool: Tool = {
name: 'spawn_agent',
description: 'Spawn a specialized sub-agent for a specific task',
parameters: {
type: 'object',
properties: {
type: {
type: 'string',
enum: ['research', 'code-review', 'test-writer', 'refactor'],
description: 'Type of sub-agent to spawn'
},
task: { type: 'string', description: 'Task for the sub-agent' },
context: { type: 'array', items: { type: 'string' }, description: 'Relevant file paths' },
},
required: ['type', 'task'],
},
execute: async (args) => {
const { type, task, context = [] } = args as SubAgentArgs;
// Create sub-agent with limited scope
const subAgent = createSubAgent(type, {
maxTurns: 10,
tools: getSubAgentTools(type),
context,
});
// Run sub-agent task
const result = await subAgent.run(task);
return `[${type} agent]\n${result}`;
},
};
const SUB_AGENT_TOOLS: Record<string, string[]> = {
'research': ['glob', 'grep', 'read_file', 'web_search'],
'code-review': ['glob', 'grep', 'read_file', 'diff'],
'test-writer': ['glob', 'grep', 'read_file', 'write_file'],
'refactor': ['glob', 'grep', 'read_file', 'edit_file'],
};Goal: IDE integration for broader adoption.
Structure:
nano-opencode-vscode/
├── package.json
├── src/
│ ├── extension.ts
│ ├── sidebarProvider.ts
│ └── commands.ts
└── webview/
└── chat.html
Key features:
- Sidebar chat panel
- File context injection
- Selection-based prompts
- Inline diff preview
| Priority | Feature | Effort | Impact | Status |
|---|---|---|---|---|
| P0 | Plan Mode | Medium | High | ✅ Done |
| P0 | Web Search | Low | High | ✅ Done |
| P1 | Image Support | Medium | Medium | ✅ Done |
| P1 | Ollama | Low | High | ✅ Done |
| P2 | Enhanced Sandbox | High | Medium | ✅ Done |
| P2 | Git Auto-commit | Low | Medium | ✅ Done |
| P3 | Sub-agents | High | High | ✅ Done |
| P3 | VS Code Extension | High | High | 📋 Planned |
- Simple > Complex - Every feature should add value without bloat
- ~1000 LOC target - If we exceed this significantly, reconsider
- No framework dependencies - Keep the dependency tree minimal
- Composable tools - Tools should work together naturally
- User control - Always allow manual override of automated features
- Plan mode reduces incorrect implementations by 50%
- Web search enables real-time documentation lookups
- Image support unlocks UI/screenshot debugging
- Ollama enables offline/private usage
- Sub-agents handle complex tasks autonomously
- Total codebase stays under 2000 LOC