Skip to main content

14days-build-claude-code-cli

Build a Claude Code-style AI agent CLI from scratch in Python with tool calling, file editing, bash execution, and MCP integration

الانتقال إلى التثبيت

معلومات المصدر

المستودع
reason-machines/claude-code-skills
آخر نشاط في المصدر
٦ يونيو ٢٠٢٦ في ٠٠:٤٢
لغة SKILL.md المكتشفة
الإنجليزية
النجوم
٤
التفرعات
١

خيارات التثبيت

يُحدَّد Prompt الذي يراجع المصدر أولًا بشكل افتراضي. يمكنك التبديل إلى أمر مباشر أو تنزيل نسخة محلية.

مراجعة ملفات المصدر

اقرأ SKILL.md وأي ملفات مرافقة يعرضها SkillsMP قبل أن تقرر التثبيت.

عرض SKILL.md

SKILL.md
تعليمات المصدر · معاينة للقراءة فقط
name
14days-build-claude-code-cli
description
Build a Claude Code-style AI agent CLI from scratch in Python with tool calling, file editing, bash execution, and MCP integration
triggers
["how do I build an AI code agent CLI","teach me to create a tool-calling agent","implement file editing tools for my agent","add bash execution to my AI assistant","create an agent harness with permissions","build a code agent with session memory","implement MCP protocol in my agent","add subagents and skills to my CLI"]
# 14days-build-claude-code-cli > Skill by [ara.so](https://ara.so) — Claude Code Skills collection. A 14-day tutorial project teaching you to build a production-style AI code agent CLI (`agent-code`) in Python. Learn the harness architecture: tool calling, file operations, bash execution, permissions, session memory, hooks, skills, subagents, worktree isolation, and MCP integration. ## What This Project Does This is an educational implementation of a Claude Code-style agent harness. You'll build `agent-code`, a CLI that: - Accepts one-shot prompts or enters an interactive REPL - Calls real LLMs (default: DeepSeek via Anthropic-compatible endpoint) - Uses tool calling (`tool_use` / `tool_result`) with file operations, web search, and bash - Implements safety: read-before-edit, diff preview, permission system - Manages sessions, project memory, and context compression - Supports slash commands, hooks, skills, subagents, and MCP tools **Core philosophy**: The model handles reasoning; the harness handles context, tools, permissions, execution, state, and feedback loops. ## Installation Each day is a standalone package. To follow the tutorial from Day 1: ```bash # Start your own project mkdir my-agent-code && cd my-agent-code uv init uv add anthropic typer rich aiofiles # Follow docs/day-01-hello-agent.md ``` To run a reference snapshot: ```bash git clone https://github.com/bozhouDev/14days-build-claude-code-cli cd 14days-build-claude-code-cli/packages/day-08-interactive-shell-plan-mode uv sync uv run agent-code "list files in this project" ``` **Web tutorial**: Visit https://buildcc.dev or run locally: ```bash cd agent-code-learn npm install && npm run dev # Open http://localhost:3000 ``` ## Configuration Set environment variables for the LLM provider: ```bash # Default: DeepSeek with Anthropic-compatible API export ANTHROPIC_AUTH_TOKEN="sk-your-deepseek-key" export ANTHROPIC_BASE_URL="https://api.deepseek.com/anthropic" # Or use official Claude export ANTHROPIC_AUTH_TOKEN="sk-ant-..." export ANTHROPIC_BASE_URL="https://api.anthropic.com" ``` Override model in CLI: ```bash uv run agent-code --model deepseek-v4-flash "your task" uv run agent-code --model claude-3-7-sonnet-20250219 "your task" ``` ## 14-Day Learning Path | Day | Topic | Key Harness Concepts | |-----|-------|---------------------| | 1 | Hello Agent | CLI, REPL, MockProvider, minimal Agent Loop | | 2 | Real Model + Tool Calling | AnthropicProvider, `tool_use` / `tool_result` | | 3 | File + Web Tools | cwd boundary, file read/search, web tools | | 4 | Safe Edit | read-before-edit, string replacement, diff preview | | 5 | Bash + Permission | command execution, permission system, background tasks | | 6 | Session + Memory | session JSONL, project memory, memdir | | 7 | Slash + Hooks | slash commands, hooks, cron `/loop` | | 8 | Interactive Shell + Plan Mode | interactive shell, TodoWrite, plan approval | | 9 | Skills | load domain knowledge and workflows on demand | | 10 | Subagents | delegate tasks to specialized subagents | | 11 | Context Compact | long context compression, cost tracking | | 12 | Agent Coordinator | lightweight multi-agent orchestration | | 13 | Worktree + Final Demo | worktree isolation, end-to-end code tasks | | 14 | MCP + ToolSearch | MCP client, tool discovery, ecosystem extension | ## Key Commands ### Basic Usage ```bash # One-shot prompt uv run agent-code "create a hello.py file" # Interactive REPL uv run agent-code # Specify model uv run agent-code --model deepseek-v4-flash "analyze this codebase" # Resume session uv run agent-code --session my-session-id ``` ### Interactive REPL Commands (Day 7+) ``` > /help # List all slash commands > /quit # Exit REPL > /new # Start new session > /save my-session # Save current session > /load my-session # Load previous session > /clear # Clear context > /tools # List available tools > /loop 5m "task" # Run task every 5 minutes ``` ### Plan Mode (Day 8+) ```bash # Enable plan mode for multi-step tasks uv run agent-code --plan-mode "refactor auth module" # Agent creates todo list, you approve each step ``` ## Core Architecture Examples ### Day 1-2: Minimal Agent Loop with Tool Calling ```python # src/agent_code/providers/anthropic_provider.py from anthropic import Anthropic from typing import List, Dict class AnthropicProvider: def __init__(self, model: str = "deepseek-v4-flash"): self.client = Anthropic() self.model = model def chat(self, messages: List[Dict], tools: List[Dict] = None) -> Dict: response = self.client.messages.create( model=self.model, messages=messages, tools=tools or [], max_tokens=8192 ) return response.model_dump() # src/agent_code/core/agent.py class Agent: def __init__(self, provider, tool_registry): self.provider = provider self.tools = tool_registry self.messages = [] def run(self, user_input: str): self.messages.append({"role": "user", "content": user_input}) while True: response = self.provider.chat( self.messages, self.tools.get_tool_schemas() ) if response["stop_reason"] == "end_turn": break # Handle tool calls for block in response["content"]: if block["type"] == "tool_use": result = self.tools.execute( block["name"], block["input"] ) self.messages.append({ "role": "user", "content": [{ "type": "tool_result", "tool_use_id": block["id"], "content": result }] }) ``` ### Day 3: File Tools with Safety ```python # src/agent_code/tools/file_tools.py import os from pathlib import Path class FileTools: def __init__(self, workspace_root: Path): self.workspace = workspace_root def _check_path(self, path: str) -> Path: """Ensure path is within workspace.""" full_path = (self.workspace / path).resolve() if not str(full_path).startswith(str(self.workspace)): raise ValueError(f"Path {path} outside workspace") return full_path def read_file(self, path: str) -> str: """Read file contents safely.""" file_path = self._check_path(path) return file_path.read_text() def list_files(self, pattern: str = "*") -> List[str]: """List files matching pattern.""" return [ str(p.relative_to(self.workspace)) for p in self.workspace.rglob(pattern) if p.is_file() ] ``` ### Day 4: Safe Edit with Diff Preview ```python # src/agent_code/tools/edit_tools.py from difflib import unified_diff class EditTools: def __init__(self, file_tools): self.files = file_tools def edit_file(self, path: str, old_text: str, new_text: str) -> str: """Replace text with read-before-edit validation.""" # 1. Read current content current = self.files.read_file(path) # 2. Validate old_text exists if old_text not in current: raise ValueError(f"old_text not found in {path}") # 3. Generate diff preview updated = current.replace(old_text, new_text, 1) diff = "\n".join(unified_diff( current.splitlines(), updated.splitlines(), fromfile=path, tofile=path, lineterm="" )) # 4. Apply change self.files._check_path(path).write_text(updated) return f"Applied:\n{diff}" ``` ### Day 5: Bash Tool with Permissions ```python # src/agent_code/tools/bash_tools.py import subprocess from typing import Set DANGEROUS_COMMANDS = {"rm", "sudo", "chmod", "mv"} class BashTools: def __init__(self, permission_callback): self.ask_permission = permission_callback self.allowed_commands: Set[str] = set() def execute_bash(self, command: str) -> str: """Execute bash command with permission check.""" first_word = command.split()[0] if first_word in DANGEROUS_COMMANDS: if first_word not in self.allowed_commands: if not self.ask_permission(f"Allow: {command}?"): return "Permission denied" self.allowed_commands.add(first_word) result = subprocess.run( command, shell=True, capture_output=True, text=True, timeout=30 ) return result.stdout + result.stderr ``` ### Day 6: Session Persistence ```python # src/agent_code/core/session.py import json from pathlib import Path from datetime import datetime class Session: def __init__(self, session_dir: Path): self.dir = session_dir self.dir.mkdir(parents=True, exist_ok=True) def save(self, session_id: str, messages: List[Dict]): """Save session to JSONL.""" file = self.dir / f"{session_id}.jsonl" with file.open("w") as f: for msg in messages: f.write(json.dumps(msg) + "\n") def load(self, session_id: str) -> List[Dict]: """Load session from JSONL.""" file = self.dir / f"{session_id}.jsonl" if not file.exists(): return [] messages = [] with file.open() as f: for line in f: messages.append(json.loads(line)) return messages def list_sessions(self) -> List[str]: """List all saved sessions.""" return [ p.stem for p in self.dir.glob("*.jsonl") ] ``` ### Day 7: Slash Commands ```python # src/agent_code/cli/slash_commands.py from typing import Callable, Dict class SlashRouter: def __init__(self): self.commands: Dict[str, Callable] = {} def register(self, name: str, handler: Callable): self.commands[name] = handler def handle(self, user_input: str) -> bool: """Return True if handled as slash command.""" if not user_input.startswith("/"): return False parts = user_input[1:].split(maxsplit=1) cmd = parts[0] args = parts[1] if len(parts) > 1 else "" if cmd in self.commands: self.commands[cmd](args) return True return False # Usage in REPL slash = SlashRouter() slash.register("help", lambda _: print("Available: /help, /quit, /save")) slash.register("save", lambda args: session.save(args, agent.messages)) while True: user_input = input("> ") if slash.handle(user_input): continue agent.run(user_input) ``` ### Day 8: Plan Mode with TodoWrite ```python # src/agent_code/tools/todo_tools.py class TodoTools: def __init__(self): self.todos = [] self.current_index = 0 def write_todos(self, tasks: List[str]) -> str: """Agent writes plan as todo list.""" self.todos = tasks self.current_index = 0 return f"Created plan with {len(tasks)} steps" def get_next_todo(self) -> str: """Get next unapproved task.""" if self.current_index >= len(self.todos): return "No more tasks" return self.todos[self.current_index] def approve_todo(self): """User approves current task.""" self.current_index += 1 # In agent loop (plan mode enabled) def run_with_plan_mode(agent, user_input): # 1. Agent creates plan agent.run(f"Create a todo list for: {user_input}") # 2. Execute each todo with approval while True: todo = todo_tools.get_next_todo() if todo == "No more tasks": break print(f"\nNext task: {todo}") if input("Approve? [y/n]: ").lower() != "y": break todo_tools.approve_todo() agent.run(f"Execute approved task: {todo}") ``` ## Common Patterns ### Tool Registry Pattern ```python class ToolRegistry: def __init__(self):
عرض على GitHub
ملف SKILL.md هذا كبير جدا، لذلك يعرض SkillsMP القسم الاول فقط هنا. عرض على GitHub