|
| 1 | +#!/usr/bin/env bun |
| 2 | +/** |
| 3 | + * nano-opencode minimal: Zero-dependency AI agent (~150 LOC) |
| 4 | + * Works with Node.js 18+, Bun, Deno - any device with fetch |
| 5 | + * |
| 6 | + * Usage: ANTHROPIC_API_KEY=sk-... bun nano-minimal.ts "read package.json" |
| 7 | + */ |
| 8 | + |
| 9 | +import { readFileSync, writeFileSync, existsSync, readdirSync, statSync } from 'fs'; |
| 10 | +import { execFileSync } from 'child_process'; |
| 11 | + |
| 12 | +// === CONFIG === |
| 13 | +const API_KEY = process.env.ANTHROPIC_API_KEY || ''; |
| 14 | +const MODEL = process.env.MODEL || 'claude-sonnet-4-20250514'; |
| 15 | +const API_URL = 'https://api.anthropic.com/v1/messages'; |
| 16 | + |
| 17 | +// === TOOLS === |
| 18 | +const TOOLS = [ |
| 19 | + { name: 'read_file', description: 'Read file', input_schema: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] } }, |
| 20 | + { name: 'write_file', description: 'Write file', input_schema: { type: 'object', properties: { path: { type: 'string' }, content: { type: 'string' } }, required: ['path', 'content'] } }, |
| 21 | + { name: 'edit_file', description: 'Edit file', input_schema: { type: 'object', properties: { path: { type: 'string' }, old_string: { type: 'string' }, new_string: { type: 'string' } }, required: ['path', 'old_string', 'new_string'] } }, |
| 22 | + { name: 'bash', description: 'Run command', input_schema: { type: 'object', properties: { command: { type: 'string' } }, required: ['command'] } }, |
| 23 | + { name: 'list_dir', description: 'List directory', input_schema: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] } }, |
| 24 | +]; |
| 25 | + |
| 26 | +// === TOOL EXECUTION === |
| 27 | +function run(name: string, i: Record<string, string>): string { |
| 28 | + try { |
| 29 | + if (name === 'read_file') return existsSync(i.path) ? readFileSync(i.path, 'utf-8') : 'File not found'; |
| 30 | + if (name === 'write_file') { writeFileSync(i.path, i.content); return 'OK'; } |
| 31 | + if (name === 'edit_file') { |
| 32 | + const c = readFileSync(i.path, 'utf-8'); |
| 33 | + if (!c.includes(i.old_string)) return 'old_string not found'; |
| 34 | + writeFileSync(i.path, c.replace(i.old_string, i.new_string)); |
| 35 | + return 'OK'; |
| 36 | + } |
| 37 | + if (name === 'bash') return execFileSync('sh', ['-c', i.command], { encoding: 'utf-8', timeout: 30000 }).slice(0, 50000); |
| 38 | + if (name === 'list_dir') return readdirSync(i.path || '.').map(f => `${statSync(`${i.path || '.'}/${f}`).isDirectory() ? 'd' : '-'} ${f}`).join('\n'); |
| 39 | + return 'Unknown tool'; |
| 40 | + } catch (e) { return `Error: ${e}`; } |
| 41 | +} |
| 42 | + |
| 43 | +// === API CALL === |
| 44 | +interface Message { role: 'user' | 'assistant'; content: unknown } |
| 45 | +interface Block { type: string; id?: string; name?: string; input?: Record<string, string>; text?: string } |
| 46 | + |
| 47 | +async function call(messages: Message[]): Promise<{ content: Block[]; stop_reason: string }> { |
| 48 | + const res = await fetch(API_URL, { |
| 49 | + method: 'POST', |
| 50 | + headers: { 'Content-Type': 'application/json', 'x-api-key': API_KEY, 'anthropic-version': '2023-06-01' }, |
| 51 | + body: JSON.stringify({ model: MODEL, max_tokens: 8192, tools: TOOLS, messages, system: 'You are a coding assistant. Use tools to help.' }), |
| 52 | + }); |
| 53 | + if (!res.ok) throw new Error(`API error: ${res.status} ${await res.text()}`); |
| 54 | + return res.json(); |
| 55 | +} |
| 56 | + |
| 57 | +// === AGENT LOOP === |
| 58 | +async function agent(prompt: string): Promise<string> { |
| 59 | + const messages: Message[] = [{ role: 'user', content: prompt }]; |
| 60 | + |
| 61 | + while (true) { |
| 62 | + const res = await call(messages); |
| 63 | + messages.push({ role: 'assistant', content: res.content }); |
| 64 | + |
| 65 | + if (res.stop_reason !== 'tool_use') { |
| 66 | + return res.content.filter(b => b.type === 'text').map(b => b.text).join(''); |
| 67 | + } |
| 68 | + |
| 69 | + const results = res.content.filter(b => b.type === 'tool_use').map(b => { |
| 70 | + console.log(`⚡ ${b.name}`); |
| 71 | + const r = run(b.name!, b.input!); |
| 72 | + console.log(r.slice(0, 100) + (r.length > 100 ? '...' : '')); |
| 73 | + return { type: 'tool_result', tool_use_id: b.id, content: r }; |
| 74 | + }); |
| 75 | + |
| 76 | + messages.push({ role: 'user', content: results }); |
| 77 | + } |
| 78 | +} |
| 79 | + |
| 80 | +// === MAIN === |
| 81 | +const prompt = process.argv.slice(2).join(' '); |
| 82 | +if (!prompt) { console.log('Usage: bun nano-minimal.ts "your prompt"'); process.exit(1); } |
| 83 | +if (!API_KEY) { console.log('Set ANTHROPIC_API_KEY'); process.exit(1); } |
| 84 | + |
| 85 | +agent(prompt).then(console.log).catch(e => console.error('Error:', e)); |
0 commit comments