mirror of
https://github.com/shareAI-lab/analysis_claude_code.git
synced 2026-02-04 21:16:48 +08:00
- Remove all reverse-engineered Claude Code source code - Replace with 100% original educational content from mini-claude-code - Add clear disclaimer: independent project, not affiliated with Anthropic - 5 progressive agent implementations (v0-v4, ~1100 lines total) - Include agent-builder skill for teaching agent construction - Bilingual documentation (EN + ZH) This repository now focuses purely on teaching how modern AI agents work through original, from-scratch implementations. Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
280 lines
9.7 KiB
Python
280 lines
9.7 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Agent Scaffold Script - Create a new agent project with best practices.
|
|
|
|
Usage:
|
|
python init_agent.py <agent-name> [--level 0-4] [--path <output-dir>]
|
|
|
|
Examples:
|
|
python init_agent.py my-agent # Level 1 (4 tools)
|
|
python init_agent.py my-agent --level 0 # Minimal (bash only)
|
|
python init_agent.py my-agent --level 2 # With TodoWrite
|
|
python init_agent.py my-agent --path ./bots # Custom output directory
|
|
"""
|
|
|
|
import argparse
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
# Agent templates for each level
|
|
TEMPLATES = {
|
|
0: '''#!/usr/bin/env python3
|
|
"""
|
|
Level 0 Agent - Bash is All You Need (~50 lines)
|
|
|
|
Core insight: One tool (bash) can do everything.
|
|
Subagents via self-recursion: python {name}.py "subtask"
|
|
"""
|
|
|
|
from anthropic import Anthropic
|
|
from dotenv import load_dotenv
|
|
import subprocess
|
|
import os
|
|
|
|
load_dotenv()
|
|
|
|
client = Anthropic(
|
|
api_key=os.getenv("ANTHROPIC_API_KEY"),
|
|
base_url=os.getenv("ANTHROPIC_BASE_URL")
|
|
)
|
|
MODEL = os.getenv("MODEL_NAME", "claude-sonnet-4-20250514")
|
|
|
|
SYSTEM = """You are a coding agent. Use bash for everything:
|
|
- Read: cat, grep, find, ls
|
|
- Write: echo 'content' > file
|
|
- Subagent: python {name}.py "subtask"
|
|
"""
|
|
|
|
TOOL = [{{
|
|
"name": "bash",
|
|
"description": "Execute shell command",
|
|
"input_schema": {{"type": "object", "properties": {{"command": {{"type": "string"}}}}, "required": ["command"]}}
|
|
}}]
|
|
|
|
def run(prompt, history=[]):
|
|
history.append({{"role": "user", "content": prompt}})
|
|
while True:
|
|
r = client.messages.create(model=MODEL, system=SYSTEM, messages=history, tools=TOOL, max_tokens=8000)
|
|
history.append({{"role": "assistant", "content": r.content}})
|
|
if r.stop_reason != "tool_use":
|
|
return "".join(b.text for b in r.content if hasattr(b, "text"))
|
|
results = []
|
|
for b in r.content:
|
|
if b.type == "tool_use":
|
|
print(f"> {{b.input['command']}}")
|
|
try:
|
|
out = subprocess.run(b.input["command"], shell=True, capture_output=True, text=True, timeout=60)
|
|
output = (out.stdout + out.stderr).strip() or "(empty)"
|
|
except Exception as e:
|
|
output = f"Error: {{e}}"
|
|
results.append({{"type": "tool_result", "tool_use_id": b.id, "content": output[:50000]}})
|
|
history.append({{"role": "user", "content": results}})
|
|
|
|
if __name__ == "__main__":
|
|
h = []
|
|
print("{name} - Level 0 Agent\\nType 'q' to quit.\\n")
|
|
while (q := input(">> ").strip()) not in ("q", "quit", ""):
|
|
print(run(q, h), "\\n")
|
|
''',
|
|
|
|
1: '''#!/usr/bin/env python3
|
|
"""
|
|
Level 1 Agent - Model as Agent (~200 lines)
|
|
|
|
Core insight: 4 tools cover 90% of coding tasks.
|
|
The model IS the agent. Code just runs the loop.
|
|
"""
|
|
|
|
from anthropic import Anthropic
|
|
from dotenv import load_dotenv
|
|
from pathlib import Path
|
|
import subprocess
|
|
import os
|
|
|
|
load_dotenv()
|
|
|
|
client = Anthropic(
|
|
api_key=os.getenv("ANTHROPIC_API_KEY"),
|
|
base_url=os.getenv("ANTHROPIC_BASE_URL")
|
|
)
|
|
MODEL = os.getenv("MODEL_NAME", "claude-sonnet-4-20250514")
|
|
WORKDIR = Path.cwd()
|
|
|
|
SYSTEM = f"""You are a coding agent at {{WORKDIR}}.
|
|
|
|
Rules:
|
|
- Prefer tools over prose. Act, don't just explain.
|
|
- Never invent file paths. Use ls/find first if unsure.
|
|
- Make minimal changes. Don't over-engineer.
|
|
- After finishing, summarize what changed."""
|
|
|
|
TOOLS = [
|
|
{{"name": "bash", "description": "Run shell command",
|
|
"input_schema": {{"type": "object", "properties": {{"command": {{"type": "string"}}}}, "required": ["command"]}}}},
|
|
{{"name": "read_file", "description": "Read file contents",
|
|
"input_schema": {{"type": "object", "properties": {{"path": {{"type": "string"}}}}, "required": ["path"]}}}},
|
|
{{"name": "write_file", "description": "Write content to file",
|
|
"input_schema": {{"type": "object", "properties": {{"path": {{"type": "string"}}, "content": {{"type": "string"}}}}, "required": ["path", "content"]}}}},
|
|
{{"name": "edit_file", "description": "Replace exact text in file",
|
|
"input_schema": {{"type": "object", "properties": {{"path": {{"type": "string"}}, "old_text": {{"type": "string"}}, "new_text": {{"type": "string"}}}}, "required": ["path", "old_text", "new_text"]}}}},
|
|
]
|
|
|
|
def safe_path(p: str) -> Path:
|
|
"""Prevent path escape attacks."""
|
|
path = (WORKDIR / p).resolve()
|
|
if not path.is_relative_to(WORKDIR):
|
|
raise ValueError(f"Path escapes workspace: {{p}}")
|
|
return path
|
|
|
|
def execute(name: str, args: dict) -> str:
|
|
"""Execute a tool and return result."""
|
|
if name == "bash":
|
|
dangerous = ["rm -rf /", "sudo", "shutdown", "> /dev/"]
|
|
if any(d in args["command"] for d in dangerous):
|
|
return "Error: Dangerous command blocked"
|
|
try:
|
|
r = subprocess.run(args["command"], shell=True, cwd=WORKDIR, capture_output=True, text=True, timeout=60)
|
|
return (r.stdout + r.stderr).strip()[:50000] or "(empty)"
|
|
except subprocess.TimeoutExpired:
|
|
return "Error: Timeout (60s)"
|
|
except Exception as e:
|
|
return f"Error: {{e}}"
|
|
|
|
if name == "read_file":
|
|
try:
|
|
return safe_path(args["path"]).read_text()[:50000]
|
|
except Exception as e:
|
|
return f"Error: {{e}}"
|
|
|
|
if name == "write_file":
|
|
try:
|
|
p = safe_path(args["path"])
|
|
p.parent.mkdir(parents=True, exist_ok=True)
|
|
p.write_text(args["content"])
|
|
return f"Wrote {{len(args['content'])}} bytes to {{args['path']}}"
|
|
except Exception as e:
|
|
return f"Error: {{e}}"
|
|
|
|
if name == "edit_file":
|
|
try:
|
|
p = safe_path(args["path"])
|
|
content = p.read_text()
|
|
if args["old_text"] not in content:
|
|
return f"Error: Text not found in {{args['path']}}"
|
|
p.write_text(content.replace(args["old_text"], args["new_text"], 1))
|
|
return f"Edited {{args['path']}}"
|
|
except Exception as e:
|
|
return f"Error: {{e}}"
|
|
|
|
return f"Unknown tool: {{name}}"
|
|
|
|
def agent(prompt: str, history: list = None) -> str:
|
|
"""Run the agent loop."""
|
|
if history is None:
|
|
history = []
|
|
history.append({{"role": "user", "content": prompt}})
|
|
|
|
while True:
|
|
response = client.messages.create(
|
|
model=MODEL, system=SYSTEM, messages=history, tools=TOOLS, max_tokens=8000
|
|
)
|
|
history.append({{"role": "assistant", "content": response.content}})
|
|
|
|
if response.stop_reason != "tool_use":
|
|
return "".join(b.text for b in response.content if hasattr(b, "text"))
|
|
|
|
results = []
|
|
for block in response.content:
|
|
if block.type == "tool_use":
|
|
print(f"> {{block.name}}: {{str(block.input)[:100]}}")
|
|
output = execute(block.name, block.input)
|
|
print(f" {{output[:100]}}...")
|
|
results.append({{"type": "tool_result", "tool_use_id": block.id, "content": output}})
|
|
history.append({{"role": "user", "content": results}})
|
|
|
|
if __name__ == "__main__":
|
|
print(f"{name} - Level 1 Agent at {{WORKDIR}}")
|
|
print("Type 'q' to quit.\\n")
|
|
h = []
|
|
while True:
|
|
try:
|
|
query = input(">> ").strip()
|
|
except (EOFError, KeyboardInterrupt):
|
|
break
|
|
if query in ("q", "quit", "exit", ""):
|
|
break
|
|
print(agent(query, h), "\\n")
|
|
''',
|
|
}
|
|
|
|
ENV_TEMPLATE = '''# API Configuration
|
|
ANTHROPIC_API_KEY=sk-xxx
|
|
ANTHROPIC_BASE_URL=https://api.anthropic.com
|
|
MODEL_NAME=claude-sonnet-4-20250514
|
|
'''
|
|
|
|
|
|
def create_agent(name: str, level: int, output_dir: Path):
|
|
"""Create a new agent project."""
|
|
# Validate level
|
|
if level not in TEMPLATES and level not in (2, 3, 4):
|
|
print(f"Error: Level {level} not yet implemented in scaffold.")
|
|
print("Available levels: 0 (minimal), 1 (4 tools)")
|
|
print("For levels 2-4, copy from mini-claude-code repository.")
|
|
sys.exit(1)
|
|
|
|
# Create output directory
|
|
agent_dir = output_dir / name
|
|
agent_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Write agent file
|
|
agent_file = agent_dir / f"{name}.py"
|
|
template = TEMPLATES.get(level, TEMPLATES[1])
|
|
agent_file.write_text(template.format(name=name))
|
|
print(f"Created: {agent_file}")
|
|
|
|
# Write .env.example
|
|
env_file = agent_dir / ".env.example"
|
|
env_file.write_text(ENV_TEMPLATE)
|
|
print(f"Created: {env_file}")
|
|
|
|
# Write .gitignore
|
|
gitignore = agent_dir / ".gitignore"
|
|
gitignore.write_text(".env\n__pycache__/\n*.pyc\n")
|
|
print(f"Created: {gitignore}")
|
|
|
|
print(f"\nAgent '{name}' created at {agent_dir}")
|
|
print(f"\nNext steps:")
|
|
print(f" 1. cd {agent_dir}")
|
|
print(f" 2. cp .env.example .env")
|
|
print(f" 3. Edit .env with your API key")
|
|
print(f" 4. pip install anthropic python-dotenv")
|
|
print(f" 5. python {name}.py")
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Scaffold a new AI coding agent project",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog="""
|
|
Levels:
|
|
0 Minimal (~50 lines) - Single bash tool, self-recursion for subagents
|
|
1 Basic (~200 lines) - 4 core tools: bash, read, write, edit
|
|
2 Todo (~300 lines) - + TodoWrite for structured planning
|
|
3 Subagent (~450) - + Task tool for context isolation
|
|
4 Skills (~550) - + Skill tool for domain expertise
|
|
"""
|
|
)
|
|
parser.add_argument("name", help="Name of the agent to create")
|
|
parser.add_argument("--level", type=int, default=1, choices=[0, 1, 2, 3, 4],
|
|
help="Complexity level (default: 1)")
|
|
parser.add_argument("--path", type=Path, default=Path.cwd(),
|
|
help="Output directory (default: current directory)")
|
|
|
|
args = parser.parse_args()
|
|
create_agent(args.name, args.level, args.path)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|