craft-code 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- code_craft/AGENTS.md +35 -0
- code_craft/__init__.py +3 -0
- code_craft/__main__.py +5 -0
- code_craft/agent.py +140 -0
- code_craft/auth.py +146 -0
- code_craft/cli.py +254 -0
- code_craft/config.py +62 -0
- code_craft/interrupt_handler.py +131 -0
- code_craft/prompts.py +60 -0
- code_craft/skills/code-review/SKILL.md +21 -0
- code_craft/skills/testing/SKILL.md +22 -0
- code_craft/tools.py +59 -0
- craft_code-0.1.0.dist-info/METADATA +121 -0
- craft_code-0.1.0.dist-info/RECORD +17 -0
- craft_code-0.1.0.dist-info/WHEEL +4 -0
- craft_code-0.1.0.dist-info/entry_points.txt +2 -0
- craft_code-0.1.0.dist-info/licenses/LICENSE +21 -0
code_craft/AGENTS.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Project: Code Craft
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
A Claude Code-like AI coding assistant built with LangChain Deep Agents.
|
|
5
|
+
|
|
6
|
+
## Tech Stack
|
|
7
|
+
- Python 3.11+
|
|
8
|
+
- LangChain Deep Agents framework
|
|
9
|
+
- LangGraph for orchestration
|
|
10
|
+
- Rich for terminal UI
|
|
11
|
+
|
|
12
|
+
## Project Structure
|
|
13
|
+
```
|
|
14
|
+
code_craft/
|
|
15
|
+
__init__.py — package metadata
|
|
16
|
+
agent.py — core agent factory (build_agent)
|
|
17
|
+
cli.py — interactive CLI entry point
|
|
18
|
+
config.py — AgentConfig dataclass
|
|
19
|
+
interrupt_handler.py — human-in-the-loop approval UI
|
|
20
|
+
prompts.py — system prompts for agent and subagents
|
|
21
|
+
tools.py — custom tools (git, delete, test runner)
|
|
22
|
+
skills/
|
|
23
|
+
testing/SKILL.md
|
|
24
|
+
code_review/SKILL.md
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Commands
|
|
28
|
+
- Run: `code-craft` or `python -m code_craft.cli`
|
|
29
|
+
- Test: `pytest`
|
|
30
|
+
- Lint: `ruff check .`
|
|
31
|
+
|
|
32
|
+
## Conventions
|
|
33
|
+
- Use type hints on public functions
|
|
34
|
+
- Keep modules focused — one responsibility per file
|
|
35
|
+
- Use Rich for all terminal output
|
code_craft/__init__.py
ADDED
code_craft/__main__.py
ADDED
code_craft/agent.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Core agent factory — builds the deep coding agent with all capabilities."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from deepagents import create_deep_agent
|
|
9
|
+
from deepagents.backends import (
|
|
10
|
+
CompositeBackend,
|
|
11
|
+
LocalShellBackend,
|
|
12
|
+
StateBackend,
|
|
13
|
+
StoreBackend,
|
|
14
|
+
)
|
|
15
|
+
from deepagents.backends.utils import create_file_data
|
|
16
|
+
from langchain.chat_models import init_chat_model
|
|
17
|
+
from langgraph.checkpoint.memory import MemorySaver
|
|
18
|
+
from langgraph.store.memory import InMemoryStore
|
|
19
|
+
|
|
20
|
+
from .config import AgentConfig
|
|
21
|
+
from .prompts import CODE_REVIEW_PROMPT, CODING_SYSTEM_PROMPT, RESEARCH_PROMPT
|
|
22
|
+
from .tools import CUSTOM_TOOLS
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _load_agents_md(config: AgentConfig, store: InMemoryStore) -> None:
|
|
26
|
+
"""Load AGENTS.md into the store if it exists on disk."""
|
|
27
|
+
agents_md_file = config.agents_md_file
|
|
28
|
+
if agents_md_file.exists():
|
|
29
|
+
content = agents_md_file.read_text()
|
|
30
|
+
store.put(
|
|
31
|
+
namespace=("filesystem",),
|
|
32
|
+
key="/AGENTS.md",
|
|
33
|
+
value=create_file_data(content),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _build_memory_prompt(config: AgentConfig) -> str:
|
|
38
|
+
"""Append project root and persistent memory instructions to the system prompt."""
|
|
39
|
+
return CODING_SYSTEM_PROMPT + f"""
|
|
40
|
+
|
|
41
|
+
## Working directory
|
|
42
|
+
Your project root is: `{config.resolved_root}`
|
|
43
|
+
In the virtual filesystem exposed by your tools, this maps to `/`.
|
|
44
|
+
**Always start file exploration from `/`** — never explore system paths like /usr, /bin, /etc.
|
|
45
|
+
When using `ls`, `read_file`, `write_file`, or `edit_file` without an explicit path, default to `/`.
|
|
46
|
+
|
|
47
|
+
## Persistent memory
|
|
48
|
+
You have access to persistent memory at {config.memories_dir}:
|
|
49
|
+
- {config.memories_dir}project_context.md — tech stack, architecture, conventions
|
|
50
|
+
- {config.memories_dir}user_preferences.md — coding style preferences
|
|
51
|
+
- {config.memories_dir}known_issues.md — recurring bugs or gotchas
|
|
52
|
+
|
|
53
|
+
Read these at session start. Update them when you learn something new about the project.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _build_subagents(subagent_model: str) -> list[dict]:
|
|
58
|
+
"""Define specialized subagents the main agent can delegate to."""
|
|
59
|
+
return [
|
|
60
|
+
{
|
|
61
|
+
"name": "code-reviewer",
|
|
62
|
+
"description": (
|
|
63
|
+
"Performs in-depth code review: checks for bugs, security issues, "
|
|
64
|
+
"performance problems, and style violations."
|
|
65
|
+
),
|
|
66
|
+
"system_prompt": CODE_REVIEW_PROMPT,
|
|
67
|
+
"model": subagent_model,
|
|
68
|
+
"tools": [],
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"name": "researcher",
|
|
72
|
+
"description": (
|
|
73
|
+
"Researches APIs, libraries, and documentation to answer "
|
|
74
|
+
"technical questions."
|
|
75
|
+
),
|
|
76
|
+
"system_prompt": RESEARCH_PROMPT,
|
|
77
|
+
"model": subagent_model,
|
|
78
|
+
"tools": [],
|
|
79
|
+
},
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _make_backend(config: AgentConfig):
|
|
84
|
+
"""Build a composite backend: local filesystem + persistent memory store."""
|
|
85
|
+
|
|
86
|
+
def factory(runtime):
|
|
87
|
+
return CompositeBackend(
|
|
88
|
+
default=LocalShellBackend(
|
|
89
|
+
root_dir=str(config.resolved_root),
|
|
90
|
+
env={**os.environ, "PWD": str(config.resolved_root)},
|
|
91
|
+
),
|
|
92
|
+
routes={
|
|
93
|
+
config.memories_dir: StoreBackend(runtime),
|
|
94
|
+
},
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
return factory
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def build_agent(config: AgentConfig | None = None):
|
|
101
|
+
"""Build and return the fully configured deep coding agent.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
(agent, checkpointer, store) tuple for use in the CLI or programmatically.
|
|
105
|
+
"""
|
|
106
|
+
if config is None:
|
|
107
|
+
config = AgentConfig()
|
|
108
|
+
|
|
109
|
+
checkpointer = MemorySaver()
|
|
110
|
+
store = InMemoryStore()
|
|
111
|
+
|
|
112
|
+
# Pre-load AGENTS.md into the store
|
|
113
|
+
_load_agents_md(config, store)
|
|
114
|
+
|
|
115
|
+
model = init_chat_model(
|
|
116
|
+
model=config.model,
|
|
117
|
+
max_retries=config.max_retries,
|
|
118
|
+
timeout=config.timeout,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
system_prompt = _build_memory_prompt(config)
|
|
122
|
+
|
|
123
|
+
# Resolve skills directory relative to the project root, not the shell's CWD
|
|
124
|
+
skills_dir = (config.resolved_root / config.skills_dir).resolve()
|
|
125
|
+
skills = [str(skills_dir)] if skills_dir.exists() else []
|
|
126
|
+
|
|
127
|
+
agent = create_deep_agent(
|
|
128
|
+
model=model,
|
|
129
|
+
system_prompt=system_prompt,
|
|
130
|
+
backend=_make_backend(config),
|
|
131
|
+
tools=CUSTOM_TOOLS,
|
|
132
|
+
subagents=_build_subagents(config.subagent_model),
|
|
133
|
+
skills=skills,
|
|
134
|
+
interrupt_on=config.interrupt_tools,
|
|
135
|
+
memory=["/AGENTS.md"],
|
|
136
|
+
store=store,
|
|
137
|
+
checkpointer=checkpointer,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
return agent, checkpointer, store
|
code_craft/auth.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""First-run API key setup — checks env, checks ~/.config, prompts once if missing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.rule import Rule
|
|
11
|
+
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
# Where we persist credentials between sessions (never stored in the project dir)
|
|
15
|
+
_CONFIG_DIR = Path.home() / ".config" / "deep-coding-agent"
|
|
16
|
+
_CREDENTIALS_FILE = _CONFIG_DIR / "credentials"
|
|
17
|
+
|
|
18
|
+
# Credential keys written to / read from the file
|
|
19
|
+
_CRED_KEY = "OPENROUTER_API_KEY"
|
|
20
|
+
_PERSISTED_KEYS = ("OPENROUTER_API_KEY", "MODEL", "SUBAGENT_MODEL")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _load_saved_credentials() -> dict[str, str]:
|
|
24
|
+
"""Read saved credentials from ~/.config/deep-coding-agent/credentials."""
|
|
25
|
+
result: dict[str, str] = {}
|
|
26
|
+
if _CREDENTIALS_FILE.exists():
|
|
27
|
+
for line in _CREDENTIALS_FILE.read_text().splitlines():
|
|
28
|
+
line = line.strip()
|
|
29
|
+
for key in _PERSISTED_KEYS:
|
|
30
|
+
if line.startswith(f"{key}="):
|
|
31
|
+
result[key] = line.split("=", 1)[1].strip()
|
|
32
|
+
return result
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _load_saved_key() -> str | None:
|
|
36
|
+
"""Read a previously saved API key from ~/.config/deep-coding-agent/credentials."""
|
|
37
|
+
return _load_saved_credentials().get(_CRED_KEY)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _save_key(api_key: str) -> None:
|
|
41
|
+
"""Persist the API key (and current model settings) to ~/.config/deep-coding-agent/credentials."""
|
|
42
|
+
_CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
43
|
+
lines = [f"{_CRED_KEY}={api_key}"]
|
|
44
|
+
for key in ("MODEL", "SUBAGENT_MODEL"):
|
|
45
|
+
value = os.environ.get(key)
|
|
46
|
+
if value:
|
|
47
|
+
lines.append(f"{key}={value}")
|
|
48
|
+
_CREDENTIALS_FILE.write_text("\n".join(lines) + "\n")
|
|
49
|
+
_CREDENTIALS_FILE.chmod(0o600) # owner read/write only
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _prompt_for_key() -> str:
|
|
53
|
+
"""Interactively ask the user for their OpenRouter API key."""
|
|
54
|
+
console.print()
|
|
55
|
+
console.print(Rule("[bold yellow]Welcome to Deep Coding Agent[/bold yellow]"))
|
|
56
|
+
console.print()
|
|
57
|
+
console.print(" To get started, you need an [bold]OpenRouter API key[/bold].")
|
|
58
|
+
console.print(
|
|
59
|
+
" Get one at: [link=https://openrouter.ai/settings/keys]"
|
|
60
|
+
"https://openrouter.ai/settings/keys[/link]"
|
|
61
|
+
)
|
|
62
|
+
console.print()
|
|
63
|
+
|
|
64
|
+
while True:
|
|
65
|
+
try:
|
|
66
|
+
key = console.input(" [bold]Enter your OpenRouter API key:[/bold] ").strip()
|
|
67
|
+
except (EOFError, KeyboardInterrupt):
|
|
68
|
+
console.print()
|
|
69
|
+
console.print("[dim]Setup cancelled.[/dim]")
|
|
70
|
+
raise SystemExit(0)
|
|
71
|
+
|
|
72
|
+
if not key:
|
|
73
|
+
console.print(" [yellow]API key cannot be empty. Try again.[/yellow]")
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
if not key.startswith("sk-or-"):
|
|
77
|
+
console.print(
|
|
78
|
+
" [yellow]That doesn't look like an OpenRouter key "
|
|
79
|
+
"(should start with 'sk-or-'). Try again.[/yellow]"
|
|
80
|
+
)
|
|
81
|
+
continue
|
|
82
|
+
|
|
83
|
+
break
|
|
84
|
+
|
|
85
|
+
# Ask whether to save for future sessions
|
|
86
|
+
console.print()
|
|
87
|
+
try:
|
|
88
|
+
save = console.input(
|
|
89
|
+
" Save key to [dim]~/.config/deep-coding-agent/credentials[/dim]"
|
|
90
|
+
" for future sessions? [bold][Y/n][/bold]: "
|
|
91
|
+
).strip().lower()
|
|
92
|
+
except (EOFError, KeyboardInterrupt):
|
|
93
|
+
save = "n"
|
|
94
|
+
|
|
95
|
+
if save in ("", "y", "yes"):
|
|
96
|
+
_save_key(key)
|
|
97
|
+
console.print(" [green]✓ Key saved.[/green]")
|
|
98
|
+
else:
|
|
99
|
+
console.print(" [dim]Key will be used for this session only.[/dim]")
|
|
100
|
+
|
|
101
|
+
console.print()
|
|
102
|
+
return key
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def ensure_api_key() -> None:
|
|
106
|
+
"""Guarantee OPENROUTER_API_KEY (and model settings) are in the environment.
|
|
107
|
+
|
|
108
|
+
Resolution order:
|
|
109
|
+
1. Already set in the shell environment → use it, done
|
|
110
|
+
2. Saved in ~/.config/deep-coding-agent/credentials → load it, done
|
|
111
|
+
3. Neither found → prompt once, optionally save, inject into env
|
|
112
|
+
"""
|
|
113
|
+
# 2. Load any previously saved credentials (API key + model settings)
|
|
114
|
+
saved = _load_saved_credentials()
|
|
115
|
+
for key, value in saved.items():
|
|
116
|
+
if not os.environ.get(key):
|
|
117
|
+
os.environ[key] = value
|
|
118
|
+
|
|
119
|
+
# 1. API key already set (shell export, CI secret, or just loaded above)
|
|
120
|
+
if os.environ.get(_CRED_KEY):
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
# 3. First run — prompt the user
|
|
124
|
+
key = _prompt_for_key()
|
|
125
|
+
os.environ[_CRED_KEY] = key
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def logout() -> None:
|
|
129
|
+
"""Remove saved credentials from ~/.config/deep-coding-agent/credentials."""
|
|
130
|
+
if not _CREDENTIALS_FILE.exists():
|
|
131
|
+
console.print("[dim]No saved credentials found — nothing to remove.[/dim]")
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
_CREDENTIALS_FILE.unlink()
|
|
135
|
+
console.print("[green]✓ Credentials removed.[/green]")
|
|
136
|
+
console.print(f"[dim]Deleted: {_CREDENTIALS_FILE}[/dim]")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def purge_config() -> None:
|
|
140
|
+
"""Remove the entire ~/.config/deep-coding-agent directory."""
|
|
141
|
+
if not _CONFIG_DIR.exists():
|
|
142
|
+
console.print("[dim]No config directory found — nothing to remove.[/dim]")
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
shutil.rmtree(_CONFIG_DIR)
|
|
146
|
+
console.print(f"[green]✓ Config directory removed: {_CONFIG_DIR}[/green]")
|
code_craft/cli.py
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""Interactive CLI for the deep coding agent — the main entry point."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
import uuid
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from langgraph.types import Command
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.markdown import Markdown
|
|
12
|
+
from rich.rule import Rule
|
|
13
|
+
|
|
14
|
+
from .agent import build_agent
|
|
15
|
+
from .auth import ensure_api_key, logout, purge_config
|
|
16
|
+
from .config import AgentConfig
|
|
17
|
+
from .interrupt_handler import collect_decisions
|
|
18
|
+
|
|
19
|
+
from prompt_toolkit import prompt as pt_prompt
|
|
20
|
+
from prompt_toolkit.formatted_text import HTML
|
|
21
|
+
|
|
22
|
+
console = Console()
|
|
23
|
+
|
|
24
|
+
BANNER = r"""
|
|
25
|
+
[bold cyan]╔══════════════════════════════════════════╗
|
|
26
|
+
║ Code-Craft Coding Agent v0.1.0 ║
|
|
27
|
+
╚══════════════════════════════════════════╝[/bold cyan]
|
|
28
|
+
|
|
29
|
+
Type your request, or use these commands:
|
|
30
|
+
[green]/help[/green] — show this help
|
|
31
|
+
[green]/new[/green] — start a new thread
|
|
32
|
+
[green]/logout[/green] — remove saved API key
|
|
33
|
+
[green]/quit[/green] — exit
|
|
34
|
+
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _trust_check(project_root: Path) -> bool:
|
|
39
|
+
"""Show a Claude Code-style trust prompt before granting filesystem access.
|
|
40
|
+
|
|
41
|
+
Returns True if the user trusts the folder, False to abort.
|
|
42
|
+
"""
|
|
43
|
+
console.print()
|
|
44
|
+
console.print(Rule("[bold yellow]Accessing workspace[/bold yellow]"))
|
|
45
|
+
console.print()
|
|
46
|
+
console.print(f" [bold]{project_root}[/bold]")
|
|
47
|
+
console.print()
|
|
48
|
+
console.print(
|
|
49
|
+
" Quick safety check: Is this a project you created or one you trust?\n"
|
|
50
|
+
" (Like your own code, a well-known open source project, or work from\n"
|
|
51
|
+
" your team.) If not, take a moment to review what's in this folder first."
|
|
52
|
+
)
|
|
53
|
+
console.print()
|
|
54
|
+
console.print(" [dim]The agent will be able to read, edit, and execute files here.[/dim]")
|
|
55
|
+
console.print()
|
|
56
|
+
|
|
57
|
+
options = [
|
|
58
|
+
("1", "Yes, I trust this folder", True),
|
|
59
|
+
("2", "No, exit", False),
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
for key, label, _ in options:
|
|
63
|
+
console.print(f" [bold cyan]>[/bold cyan] [bold]{key}.[/bold] {label}")
|
|
64
|
+
|
|
65
|
+
console.print()
|
|
66
|
+
|
|
67
|
+
while True:
|
|
68
|
+
try:
|
|
69
|
+
choice = console.input(" Enter number to confirm (or Ctrl+C to cancel): ").strip()
|
|
70
|
+
except (EOFError, KeyboardInterrupt):
|
|
71
|
+
console.print()
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
if choice == "1":
|
|
75
|
+
console.print()
|
|
76
|
+
return True
|
|
77
|
+
elif choice == "2":
|
|
78
|
+
console.print()
|
|
79
|
+
return False
|
|
80
|
+
else:
|
|
81
|
+
console.print(" [yellow]Please enter 1 or 2.[/yellow]")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _tool_call_detail(name: str, args: dict) -> str:
|
|
85
|
+
"""Brief inline detail for a tool call."""
|
|
86
|
+
if name in ("read_file", "edit_file", "write_file", "delete_file"):
|
|
87
|
+
return f" [dim]{args.get('file_path', '')}[/dim]"
|
|
88
|
+
if name == "execute":
|
|
89
|
+
cmd = args.get("command", "")
|
|
90
|
+
if len(cmd) > 80:
|
|
91
|
+
cmd = cmd[:80] + "..."
|
|
92
|
+
return f" [dim]{cmd}[/dim]"
|
|
93
|
+
if name == "run_tests":
|
|
94
|
+
return f" [dim]{args.get('command', 'pytest')}[/dim]"
|
|
95
|
+
return ""
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _display_stream_chunk(chunk: dict) -> None:
|
|
99
|
+
"""Display a single stream update chunk — shows tool calls as they happen."""
|
|
100
|
+
if not isinstance(chunk, dict):
|
|
101
|
+
return
|
|
102
|
+
for node_name, output in chunk.items():
|
|
103
|
+
if node_name == "__interrupt__":
|
|
104
|
+
continue
|
|
105
|
+
if not isinstance(output, dict):
|
|
106
|
+
continue
|
|
107
|
+
messages = output.get("messages", [])
|
|
108
|
+
if not isinstance(messages, list):
|
|
109
|
+
continue
|
|
110
|
+
for msg in messages:
|
|
111
|
+
if hasattr(msg, "tool_calls") and msg.tool_calls:
|
|
112
|
+
for tc in msg.tool_calls:
|
|
113
|
+
name = tc["name"]
|
|
114
|
+
detail = _tool_call_detail(name, tc.get("args", {}))
|
|
115
|
+
console.print(f" [bold cyan]→ {name}[/bold cyan]{detail}")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _has_interrupts(state) -> bool:
|
|
119
|
+
"""Check whether a LangGraph state snapshot has pending interrupts."""
|
|
120
|
+
return bool(state.tasks and any(t.interrupts for t in state.tasks))
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _get_interrupt_value(state) -> dict:
|
|
124
|
+
"""Extract the first interrupt value from a state snapshot."""
|
|
125
|
+
for task in state.tasks:
|
|
126
|
+
for interrupt in task.interrupts:
|
|
127
|
+
return interrupt.value
|
|
128
|
+
return {}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _print_response(content: str) -> None:
|
|
132
|
+
"""Render the agent's response as markdown."""
|
|
133
|
+
console.print()
|
|
134
|
+
console.print(Markdown(content))
|
|
135
|
+
console.print()
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def main():
|
|
139
|
+
"""Run the interactive coding agent CLI."""
|
|
140
|
+
import sys
|
|
141
|
+
|
|
142
|
+
# Handle top-level subcommands before anything else
|
|
143
|
+
if len(sys.argv) > 1:
|
|
144
|
+
subcmd = sys.argv[1].lower()
|
|
145
|
+
if subcmd == "logout":
|
|
146
|
+
logout()
|
|
147
|
+
sys.exit(0)
|
|
148
|
+
elif subcmd == "uninstall":
|
|
149
|
+
purge_config()
|
|
150
|
+
console.print()
|
|
151
|
+
console.print(" To fully uninstall the package, run:")
|
|
152
|
+
console.print(" [bold]pip uninstall code-craft[/bold]")
|
|
153
|
+
console.print(" [dim]or, if installed with uv:[/dim]")
|
|
154
|
+
console.print(" [bold]uv pip uninstall code-craft[/bold]")
|
|
155
|
+
sys.exit(0)
|
|
156
|
+
elif subcmd in ("--help", "-h", "help"):
|
|
157
|
+
console.print(BANNER)
|
|
158
|
+
sys.exit(0)
|
|
159
|
+
|
|
160
|
+
# Ensure API key is available before anything else — prompts once if missing
|
|
161
|
+
ensure_api_key()
|
|
162
|
+
|
|
163
|
+
config = AgentConfig()
|
|
164
|
+
|
|
165
|
+
console.print(BANNER)
|
|
166
|
+
|
|
167
|
+
# --- Trust gate: must pass before the agent is built or any files touched ---
|
|
168
|
+
if not _trust_check(config.resolved_root):
|
|
169
|
+
console.print("[dim]Exiting. No files were accessed.[/dim]")
|
|
170
|
+
sys.exit(0)
|
|
171
|
+
|
|
172
|
+
console.print(f"[dim]Model: {config.model}[/dim]")
|
|
173
|
+
console.print(f"[dim]Project root: {config.resolved_root}[/dim]")
|
|
174
|
+
console.print()
|
|
175
|
+
|
|
176
|
+
try:
|
|
177
|
+
agent, checkpointer, store = build_agent(config)
|
|
178
|
+
except Exception as e:
|
|
179
|
+
console.print(f"[red]Failed to build agent: {e}[/red]")
|
|
180
|
+
sys.exit(1)
|
|
181
|
+
|
|
182
|
+
thread_id = str(uuid.uuid4())
|
|
183
|
+
lang_config = {"configurable": {"thread_id": thread_id}}
|
|
184
|
+
|
|
185
|
+
console.print(f"[dim]Thread: {thread_id[:8]}...[/dim]")
|
|
186
|
+
|
|
187
|
+
while True:
|
|
188
|
+
try:
|
|
189
|
+
console.rule()
|
|
190
|
+
user_input = pt_prompt(HTML("<ansigreen><b>></b></ansigreen> ")).strip()
|
|
191
|
+
console.rule()
|
|
192
|
+
except (EOFError, KeyboardInterrupt):
|
|
193
|
+
console.print("\n[dim]Goodbye![/dim]")
|
|
194
|
+
break
|
|
195
|
+
|
|
196
|
+
if not user_input:
|
|
197
|
+
continue
|
|
198
|
+
|
|
199
|
+
# Handle commands
|
|
200
|
+
if user_input.startswith("/"):
|
|
201
|
+
cmd = user_input.lower()
|
|
202
|
+
if cmd in ("/quit", "/exit", "/q"):
|
|
203
|
+
console.print("[dim]Goodbye![/dim]")
|
|
204
|
+
break
|
|
205
|
+
elif cmd == "/new":
|
|
206
|
+
thread_id = str(uuid.uuid4())
|
|
207
|
+
lang_config = {"configurable": {"thread_id": thread_id}}
|
|
208
|
+
console.print(f"[dim]New thread: {thread_id[:8]}...[/dim]")
|
|
209
|
+
continue
|
|
210
|
+
elif cmd == "/help":
|
|
211
|
+
console.print(BANNER)
|
|
212
|
+
continue
|
|
213
|
+
elif cmd == "/logout":
|
|
214
|
+
logout()
|
|
215
|
+
continue
|
|
216
|
+
else:
|
|
217
|
+
console.print(f"[yellow]Unknown command: {user_input}[/yellow]")
|
|
218
|
+
continue
|
|
219
|
+
|
|
220
|
+
# Stream agent execution with real-time step display
|
|
221
|
+
try:
|
|
222
|
+
input_data = {"messages": [{"role": "user", "content": user_input}]}
|
|
223
|
+
|
|
224
|
+
for chunk in agent.stream(input_data, lang_config, stream_mode="updates"):
|
|
225
|
+
_display_stream_chunk(chunk)
|
|
226
|
+
|
|
227
|
+
# Handle interrupts (approval loop)
|
|
228
|
+
state = agent.get_state(lang_config)
|
|
229
|
+
while _has_interrupts(state):
|
|
230
|
+
decisions = collect_decisions(_get_interrupt_value(state))
|
|
231
|
+
for chunk in agent.stream(
|
|
232
|
+
Command(resume={"decisions": decisions}),
|
|
233
|
+
lang_config,
|
|
234
|
+
stream_mode="updates",
|
|
235
|
+
):
|
|
236
|
+
_display_stream_chunk(chunk)
|
|
237
|
+
state = agent.get_state(lang_config)
|
|
238
|
+
|
|
239
|
+
# Print the final response
|
|
240
|
+
messages = state.values.get("messages", [])
|
|
241
|
+
if messages:
|
|
242
|
+
last_msg = messages[-1]
|
|
243
|
+
content = getattr(last_msg, "content", str(last_msg))
|
|
244
|
+
if content:
|
|
245
|
+
_print_response(content)
|
|
246
|
+
|
|
247
|
+
except KeyboardInterrupt:
|
|
248
|
+
console.print("\n[yellow]Interrupted.[/yellow]")
|
|
249
|
+
except Exception as e:
|
|
250
|
+
console.print(f"\n[red]Error: {e}[/red]")
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
if __name__ == "__main__":
|
|
254
|
+
main()
|
code_craft/config.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Configuration for the coding agent."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from dotenv import load_dotenv
|
|
8
|
+
|
|
9
|
+
load_dotenv()
|
|
10
|
+
|
|
11
|
+
# Directory containing this file — used to locate bundled package data
|
|
12
|
+
_PACKAGE_DIR = Path(__file__).parent
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class AgentConfig:
|
|
17
|
+
"""Configuration for the deep coding agent."""
|
|
18
|
+
|
|
19
|
+
# Model settings
|
|
20
|
+
# Supports provider:model format — e.g. anthropic:claude-sonnet-4-6
|
|
21
|
+
# or openrouter:anthropic/claude-sonnet-4-6 (requires langchain-openrouter)
|
|
22
|
+
# Uses field(default_factory=...) so env vars are read at instantiation time,
|
|
23
|
+
# after ensure_api_key() has loaded credentials into the environment.
|
|
24
|
+
model: str = field(default_factory=lambda: os.getenv("MODEL", "openrouter:anthropic/claude-sonnet-4-6"))
|
|
25
|
+
# Subagent model defaults to main model if unset — set to a cheaper model to save credits
|
|
26
|
+
subagent_model: str = field(
|
|
27
|
+
default_factory=lambda: os.getenv("SUBAGENT_MODEL", "") or os.getenv("MODEL", "openrouter:anthropic/claude-sonnet-4-6")
|
|
28
|
+
)
|
|
29
|
+
max_retries: int = 3
|
|
30
|
+
timeout: int = 120_000 # milliseconds (120 seconds)
|
|
31
|
+
|
|
32
|
+
# Project settings
|
|
33
|
+
project_root: str = os.getenv("PROJECT_ROOT", ".")
|
|
34
|
+
|
|
35
|
+
# Agent limits
|
|
36
|
+
model_call_limit: int = 50
|
|
37
|
+
tool_call_limit: int = 100
|
|
38
|
+
|
|
39
|
+
# Human-in-the-loop: which tools require approval
|
|
40
|
+
interrupt_tools: dict = field(default_factory=lambda: {
|
|
41
|
+
"execute": True,
|
|
42
|
+
"write_file": {"allowed_decisions": ["approve", "reject"]},
|
|
43
|
+
"edit_file": True,
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
# Paths — skills_dir defaults to the bundled package skills, so they work
|
|
47
|
+
# whether the package is installed via pip or run from source.
|
|
48
|
+
agents_md_path: str = "AGENTS.md"
|
|
49
|
+
memories_dir: str = "/memories/"
|
|
50
|
+
skills_dir: str = str(_PACKAGE_DIR / "skills")
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def resolved_root(self) -> Path:
|
|
54
|
+
return Path(self.project_root).resolve()
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def agents_md_file(self) -> Path:
|
|
58
|
+
"""AGENTS.md: prefer the project's own file, fall back to bundled template."""
|
|
59
|
+
project_file = self.resolved_root / self.agents_md_path
|
|
60
|
+
if project_file.exists():
|
|
61
|
+
return project_file
|
|
62
|
+
return _PACKAGE_DIR / "AGENTS.md"
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Human-in-the-loop interrupt handling for tool approval."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import difflib
|
|
6
|
+
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
from rich.text import Text
|
|
11
|
+
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _format_action(action: dict) -> Panel:
|
|
16
|
+
"""Format a tool call action as a rich panel for display."""
|
|
17
|
+
name = action.get("name", "unknown")
|
|
18
|
+
args = action.get("args", {})
|
|
19
|
+
|
|
20
|
+
table = Table(show_header=False, box=None, padding=(0, 1))
|
|
21
|
+
for key, value in args.items():
|
|
22
|
+
display_value = str(value)
|
|
23
|
+
if len(display_value) > 200:
|
|
24
|
+
display_value = display_value[:200] + "..."
|
|
25
|
+
table.add_row(f"[bold]{key}[/bold]", display_value)
|
|
26
|
+
|
|
27
|
+
return Panel(table, title=f"[bold yellow]{name}[/bold yellow]", border_style="yellow")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _print_edit_diff(action: dict) -> None:
|
|
31
|
+
"""Print an edit_file action as a colored unified diff between horizontal rules."""
|
|
32
|
+
args = action.get("args", {})
|
|
33
|
+
old_string = args.get("old_string")
|
|
34
|
+
new_string = args.get("new_string")
|
|
35
|
+
file_path = args.get("file_path", "unknown file")
|
|
36
|
+
|
|
37
|
+
if old_string is None or new_string is None:
|
|
38
|
+
console.print(_format_action(action))
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
old_lines = old_string.splitlines(keepends=True)
|
|
42
|
+
new_lines = new_string.splitlines(keepends=True)
|
|
43
|
+
diff = list(difflib.unified_diff(old_lines, new_lines, fromfile=file_path, tofile=file_path))
|
|
44
|
+
|
|
45
|
+
console.rule(f"[bold yellow]edit_file[/bold yellow] → {file_path}")
|
|
46
|
+
|
|
47
|
+
text = Text()
|
|
48
|
+
for line in diff:
|
|
49
|
+
stripped = line.rstrip("\n")
|
|
50
|
+
if line.startswith("---") or line.startswith("+++"):
|
|
51
|
+
text.append(stripped + "\n", style="bold")
|
|
52
|
+
elif line.startswith("@@"):
|
|
53
|
+
text.append(stripped + "\n", style="dim cyan")
|
|
54
|
+
elif line.startswith("-"):
|
|
55
|
+
text.append(stripped + "\n", style="red strike")
|
|
56
|
+
elif line.startswith("+"):
|
|
57
|
+
text.append(stripped + "\n", style="green")
|
|
58
|
+
else:
|
|
59
|
+
text.append(stripped + "\n")
|
|
60
|
+
|
|
61
|
+
# Remove trailing newline for cleaner display
|
|
62
|
+
if text.plain.endswith("\n"):
|
|
63
|
+
text.right_crop(1)
|
|
64
|
+
|
|
65
|
+
console.print(text)
|
|
66
|
+
console.rule()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def collect_decisions(interrupt_value: dict) -> list[dict]:
|
|
70
|
+
"""Display pending actions and collect user approve/edit/reject decisions.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
interrupt_value: The interrupt value dict with action_requests and review_configs.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
List of decision dicts (one per action).
|
|
77
|
+
"""
|
|
78
|
+
action_requests = interrupt_value.get("action_requests", [])
|
|
79
|
+
review_configs = interrupt_value.get("review_configs", [])
|
|
80
|
+
|
|
81
|
+
decisions = []
|
|
82
|
+
for i, action in enumerate(action_requests):
|
|
83
|
+
console.print()
|
|
84
|
+
if action.get("name") == "edit_file":
|
|
85
|
+
_print_edit_diff(action)
|
|
86
|
+
else:
|
|
87
|
+
console.print(_format_action(action))
|
|
88
|
+
|
|
89
|
+
# Determine allowed decisions
|
|
90
|
+
allowed = ["approve", "edit", "reject"]
|
|
91
|
+
if i < len(review_configs):
|
|
92
|
+
rc = review_configs[i]
|
|
93
|
+
if isinstance(rc, dict) and "allowed_decisions" in rc:
|
|
94
|
+
allowed = rc["allowed_decisions"]
|
|
95
|
+
|
|
96
|
+
prompt_parts = []
|
|
97
|
+
if "approve" in allowed:
|
|
98
|
+
prompt_parts.append("[green]a[/green]pprove")
|
|
99
|
+
if "edit" in allowed:
|
|
100
|
+
prompt_parts.append("[yellow]e[/yellow]dit")
|
|
101
|
+
if "reject" in allowed:
|
|
102
|
+
prompt_parts.append("[red]r[/red]eject")
|
|
103
|
+
|
|
104
|
+
prompt = " / ".join(prompt_parts) + "? "
|
|
105
|
+
console.rule()
|
|
106
|
+
choice = console.input(prompt).strip().lower()
|
|
107
|
+
console.rule()
|
|
108
|
+
|
|
109
|
+
if choice in ("a", "approve"):
|
|
110
|
+
decisions.append({"type": "approve"})
|
|
111
|
+
elif choice in ("r", "reject"):
|
|
112
|
+
decisions.append({"type": "reject"})
|
|
113
|
+
elif choice in ("e", "edit") and "edit" in allowed:
|
|
114
|
+
console.print("[dim]Enter edited arguments as key=value pairs (blank line to finish):[/dim]")
|
|
115
|
+
new_args = dict(action.get("args", {}))
|
|
116
|
+
while True:
|
|
117
|
+
line = console.input(" > ").strip()
|
|
118
|
+
if not line:
|
|
119
|
+
break
|
|
120
|
+
if "=" in line:
|
|
121
|
+
key, _, value = line.partition("=")
|
|
122
|
+
new_args[key.strip()] = value.strip()
|
|
123
|
+
decisions.append({
|
|
124
|
+
"type": "edit",
|
|
125
|
+
"edited_action": {"name": action["name"], "args": new_args},
|
|
126
|
+
})
|
|
127
|
+
else:
|
|
128
|
+
console.print("[red]Invalid choice, defaulting to reject.[/red]")
|
|
129
|
+
decisions.append({"type": "reject"})
|
|
130
|
+
|
|
131
|
+
return decisions
|
code_craft/prompts.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""System prompts for the coding agent."""
|
|
2
|
+
|
|
3
|
+
CODING_SYSTEM_PROMPT = """\
|
|
4
|
+
You are an expert AI coding assistant. Your job is to help developers
|
|
5
|
+
write, debug, refactor, and understand code in their project directory.
|
|
6
|
+
|
|
7
|
+
## Your capabilities
|
|
8
|
+
- Read, write, and edit files in the project directory
|
|
9
|
+
- Execute shell commands (tests, linters, build tools, git, etc.)
|
|
10
|
+
- Plan complex multi-step changes using your todo list
|
|
11
|
+
- Delegate deep research or isolated subtasks to subagents
|
|
12
|
+
|
|
13
|
+
## Your workflow
|
|
14
|
+
1. Always read relevant files before making changes
|
|
15
|
+
2. Write a plan using write_todos before multi-step changes
|
|
16
|
+
3. Make targeted, minimal changes — don't over-refactor
|
|
17
|
+
4. Run tests after changes to verify correctness
|
|
18
|
+
5. Report what you did and why
|
|
19
|
+
|
|
20
|
+
## Safety rules
|
|
21
|
+
- Never delete files without explicit user permission
|
|
22
|
+
- Always show diffs or summaries of your changes
|
|
23
|
+
- Ask before running commands that modify external state (git push, deploy, etc.)
|
|
24
|
+
- Never commit secrets, credentials, or API keys
|
|
25
|
+
- Prefer editing existing files over creating new ones
|
|
26
|
+
|
|
27
|
+
## Shell commands
|
|
28
|
+
- Always use non-interactive flags to prevent commands from hanging on prompts:
|
|
29
|
+
- npx: use `npx --yes` (e.g. `npx --yes create-react-app my-app`)
|
|
30
|
+
- apt: use `apt-get -y`
|
|
31
|
+
- pip: use `pip install --no-input`
|
|
32
|
+
- npm init: use `npm init -y`
|
|
33
|
+
- Commands run without a terminal, so interactive prompts will hang forever
|
|
34
|
+
|
|
35
|
+
## Style
|
|
36
|
+
- Be concise. Lead with the answer or action.
|
|
37
|
+
- When showing code changes, highlight what changed and why.
|
|
38
|
+
- If a task is ambiguous, ask one clarifying question before proceeding.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
CODE_REVIEW_PROMPT = """\
|
|
42
|
+
You are an expert code reviewer. Analyze the provided code for:
|
|
43
|
+
- Bugs and logic errors
|
|
44
|
+
- Security vulnerabilities (injection, XSS, auth flaws)
|
|
45
|
+
- Performance issues (N+1 queries, unnecessary allocations)
|
|
46
|
+
- Style and convention violations
|
|
47
|
+
- Missing error handling or edge cases
|
|
48
|
+
- Missing tests
|
|
49
|
+
|
|
50
|
+
Return a structured review with severity levels: critical, warning, info.
|
|
51
|
+
Be specific — reference line numbers and suggest fixes.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
RESEARCH_PROMPT = """\
|
|
55
|
+
You are a technical researcher. When asked a question:
|
|
56
|
+
1. Search for authoritative documentation and examples
|
|
57
|
+
2. Verify information across multiple sources
|
|
58
|
+
3. Provide concise, accurate answers with source references
|
|
59
|
+
4. Include code examples when relevant
|
|
60
|
+
"""
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: code-review
|
|
3
|
+
description: Use when asked to review code, check for bugs, security issues, or audit a file.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Code Review Skill
|
|
7
|
+
|
|
8
|
+
Use this when asked to review code, check for issues, or audit a file.
|
|
9
|
+
|
|
10
|
+
## Workflow
|
|
11
|
+
1. Read the file(s) to review
|
|
12
|
+
2. Analyze for bugs, security issues, performance, and style
|
|
13
|
+
3. Categorize findings by severity: critical, warning, info
|
|
14
|
+
4. Suggest specific fixes with code snippets
|
|
15
|
+
5. Summarize the overall health of the code
|
|
16
|
+
|
|
17
|
+
## Focus Areas
|
|
18
|
+
- **Security**: injection, XSS, auth flaws, hardcoded secrets
|
|
19
|
+
- **Bugs**: off-by-one, null refs, race conditions, logic errors
|
|
20
|
+
- **Performance**: N+1 queries, unnecessary allocations, blocking I/O
|
|
21
|
+
- **Style**: naming, structure, dead code, missing types
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: testing
|
|
3
|
+
description: Use when asked to write, run, or debug tests for any module or function.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Testing Skill
|
|
7
|
+
|
|
8
|
+
Use this when asked to write, run, or debug tests.
|
|
9
|
+
|
|
10
|
+
## Workflow
|
|
11
|
+
1. Read the source file being tested
|
|
12
|
+
2. Identify all public functions/methods and their edge cases
|
|
13
|
+
3. Write unit tests using the project's test framework (default: pytest)
|
|
14
|
+
4. Run the tests to verify they pass
|
|
15
|
+
5. Fix any failures and re-run until green
|
|
16
|
+
|
|
17
|
+
## Conventions
|
|
18
|
+
- Test file path: `tests/test_<module>.py`
|
|
19
|
+
- Use fixtures for shared setup
|
|
20
|
+
- Mock external dependencies (network, DB) but not internal logic
|
|
21
|
+
- Each test should be independent and deterministic
|
|
22
|
+
- Name tests: `test_<function>_<scenario>_<expected>`
|
code_craft/tools.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Custom tools that extend the built-in Deep Agents toolset."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import subprocess
|
|
5
|
+
|
|
6
|
+
from langchain_core.tools import tool
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@tool
|
|
10
|
+
def delete_file(path: str) -> str:
|
|
11
|
+
"""Permanently delete a file. Use with caution — this cannot be undone."""
|
|
12
|
+
if not os.path.exists(path):
|
|
13
|
+
return f"Error: {path} does not exist"
|
|
14
|
+
os.remove(path)
|
|
15
|
+
return f"Deleted {path}"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@tool
|
|
19
|
+
def git_status() -> str:
|
|
20
|
+
"""Show the current git status of the project."""
|
|
21
|
+
result = subprocess.run(
|
|
22
|
+
["git", "status", "--short"],
|
|
23
|
+
capture_output=True,
|
|
24
|
+
text=True,
|
|
25
|
+
timeout=30,
|
|
26
|
+
)
|
|
27
|
+
if result.returncode != 0:
|
|
28
|
+
return f"Error: {result.stderr}"
|
|
29
|
+
return result.stdout or "Working tree clean"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@tool
|
|
33
|
+
def git_diff(path: str = "") -> str:
|
|
34
|
+
"""Show git diff for staged and unstaged changes. Optionally filter by path."""
|
|
35
|
+
cmd = ["git", "diff"]
|
|
36
|
+
if path:
|
|
37
|
+
cmd.append(path)
|
|
38
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
|
39
|
+
if result.returncode != 0:
|
|
40
|
+
return f"Error: {result.stderr}"
|
|
41
|
+
return result.stdout or "No changes"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@tool
|
|
45
|
+
def run_tests(command: str = "pytest") -> str:
|
|
46
|
+
"""Run the project's test suite. Defaults to pytest."""
|
|
47
|
+
result = subprocess.run(
|
|
48
|
+
command.split(),
|
|
49
|
+
capture_output=True,
|
|
50
|
+
text=True,
|
|
51
|
+
timeout=300,
|
|
52
|
+
)
|
|
53
|
+
output = result.stdout
|
|
54
|
+
if result.stderr:
|
|
55
|
+
output += f"\nSTDERR:\n{result.stderr}"
|
|
56
|
+
return output
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
CUSTOM_TOOLS = [delete_file, git_status, git_diff, run_tests]
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: craft-code
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Claude Code-like AI coding assistant built with LangChain Deep Agents
|
|
5
|
+
Project-URL: Homepage, https://github.com/shubhamagarwal/code-craft
|
|
6
|
+
Project-URL: Repository, https://github.com/shubhamagarwal/code-craft
|
|
7
|
+
Project-URL: Bug Tracker, https://github.com/shubhamagarwal/code-craft/issues
|
|
8
|
+
Author: Shubham Agarwal
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: agent,ai,claude,coding-assistant,langchain,llm
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
22
|
+
Requires-Python: >=3.11
|
|
23
|
+
Requires-Dist: deepagents>=0.4.0
|
|
24
|
+
Requires-Dist: langchain-anthropic>=0.3.0
|
|
25
|
+
Requires-Dist: langchain-openrouter>=0.1.0
|
|
26
|
+
Requires-Dist: langgraph-checkpoint>=2.0.0
|
|
27
|
+
Requires-Dist: langgraph>=0.3.0
|
|
28
|
+
Requires-Dist: prompt-toolkit>=3.0.0
|
|
29
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
30
|
+
Requires-Dist: rich>=13.0.0
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: build>=1.0; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
34
|
+
Requires-Dist: ruff>=0.8.0; extra == 'dev'
|
|
35
|
+
Requires-Dist: twine>=5.0; extra == 'dev'
|
|
36
|
+
Provides-Extra: openrouter
|
|
37
|
+
Requires-Dist: langchain-openrouter>=0.1.0; extra == 'openrouter'
|
|
38
|
+
Provides-Extra: sandbox
|
|
39
|
+
Requires-Dist: langchain-daytona>=0.1.0; extra == 'sandbox'
|
|
40
|
+
Description-Content-Type: text/markdown
|
|
41
|
+
|
|
42
|
+
# Code Craft
|
|
43
|
+
|
|
44
|
+
A Claude Code-like AI coding assistant built with [LangChain Deep Agents](https://docs.langchain.com/oss/python/deepagents/overview).
|
|
45
|
+
|
|
46
|
+
## Features
|
|
47
|
+
|
|
48
|
+
- **File system access** — reads, writes, and edits project files
|
|
49
|
+
- **Shell execution** — runs commands (tests, linters, build tools)
|
|
50
|
+
- **Agentic planning** — breaks tasks into steps with `write_todos`
|
|
51
|
+
- **Human-in-the-loop** — asks approval before destructive operations
|
|
52
|
+
- **Persistent memory** — remembers project context across sessions
|
|
53
|
+
- **Subagent delegation** — spawns code-reviewer and researcher subagents
|
|
54
|
+
- **Skill system** — lazy-loaded workflows for testing, code review, etc.
|
|
55
|
+
|
|
56
|
+
## Quick Start
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
# 1. Clone and install
|
|
60
|
+
cd code-craft
|
|
61
|
+
uv venv && source .venv/bin/activate
|
|
62
|
+
uv pip install -e .
|
|
63
|
+
|
|
64
|
+
# 2. Set your API key
|
|
65
|
+
cp .env.example .env
|
|
66
|
+
# Edit .env and add your ANTHROPIC_API_KEY
|
|
67
|
+
|
|
68
|
+
# 3. Run
|
|
69
|
+
coding-agent
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Usage
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# Interactive CLI
|
|
76
|
+
coding-agent
|
|
77
|
+
|
|
78
|
+
# Or run directly
|
|
79
|
+
python -m code_craft
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Inside the CLI:
|
|
83
|
+
- Type your coding request
|
|
84
|
+
- The agent will ask for approval before file writes and shell commands
|
|
85
|
+
- Use `/new` to start a fresh thread, `/quit` to exit
|
|
86
|
+
|
|
87
|
+
## Programmatic Usage
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from code_craft.agent import build_agent
|
|
91
|
+
from code_craft.config import AgentConfig
|
|
92
|
+
|
|
93
|
+
config = AgentConfig(project_root="/path/to/your/project")
|
|
94
|
+
agent, checkpointer, store = build_agent(config)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Configuration
|
|
98
|
+
|
|
99
|
+
Set these in `.env` or as environment variables:
|
|
100
|
+
|
|
101
|
+
| Variable | Default | Description |
|
|
102
|
+
|---|---|---|
|
|
103
|
+
| `ANTHROPIC_API_KEY` | (required) | Your Anthropic API key |
|
|
104
|
+
| `MODEL` | `anthropic:claude-sonnet-4-6` | Model to use |
|
|
105
|
+
| `PROJECT_ROOT` | `.` | Root directory for file operations |
|
|
106
|
+
|
|
107
|
+
## Architecture
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
code_craft/
|
|
111
|
+
agent.py — Agent factory with backends, subagents, skills
|
|
112
|
+
cli.py — Interactive Rich-powered CLI
|
|
113
|
+
config.py — AgentConfig dataclass
|
|
114
|
+
interrupt_handler.py — Human-in-the-loop approval UI
|
|
115
|
+
prompts.py — System prompts
|
|
116
|
+
tools.py — Custom tools (git, delete, tests)
|
|
117
|
+
skills/
|
|
118
|
+
testing/SKILL.md — Test writing workflow
|
|
119
|
+
code_review/SKILL.md — Code review workflow
|
|
120
|
+
AGENTS.md — Project context for the agent
|
|
121
|
+
```
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
code_craft/AGENTS.md,sha256=bgE40QDX_gKzRSaLf_q96oN-eQo6UUEe8w9DOA4arQ0,943
|
|
2
|
+
code_craft/__init__.py,sha256=1jM6UvGpiKPOPQCN6DQ6_vCtU-Mc4YCJ3PH9-UUzSHc,114
|
|
3
|
+
code_craft/__main__.py,sha256=hhH1lYKvDczHUHWNp0ohEIDBS0FxAwQcEsPXvlc-Z0o,78
|
|
4
|
+
code_craft/agent.py,sha256=Duf5Z99XmE_aG4nCjx1uo1d-72W-2IuDMmb0R81cJI0,4526
|
|
5
|
+
code_craft/auth.py,sha256=3VIlBQEP6n1RNWg4vkVV5mPftSfPOfCibK9syya_pUA,4989
|
|
6
|
+
code_craft/cli.py,sha256=bZMjhvnpXC9s-rn4qfhzTFVXGt0qW20pWF2PGnJrJKQ,8640
|
|
7
|
+
code_craft/config.py,sha256=AAHX-KxzOZvnWTpWW7PaVCbqcuK1F8eOWoI3YCvnDnA,2236
|
|
8
|
+
code_craft/interrupt_handler.py,sha256=PgoNZsrVkyeZgU2dTVF7SQqxhiaSRnjO_ta3Tg4D-bQ,4590
|
|
9
|
+
code_craft/prompts.py,sha256=P6vGLBOFisFH2x60gOAIYKz1xAWi0r24gkZ5lsFKksg,2318
|
|
10
|
+
code_craft/tools.py,sha256=BXQgWG43ybhwZjAFcLNvSvRM7vE3s0AM7C8g3WcNQeo,1541
|
|
11
|
+
code_craft/skills/code-review/SKILL.md,sha256=ECoyTN7mlzd--hHcTrL7dQFsPZSkYMPiYrVp1KfQCGQ,736
|
|
12
|
+
code_craft/skills/testing/SKILL.md,sha256=r-fvtOCAxqLlZtUmxePxYqI_u2XWgGxPc2WuM6QDJPE,705
|
|
13
|
+
craft_code-0.1.0.dist-info/METADATA,sha256=Z9BiOjAmNy4hmsW53gyW2rpZ8YzlaP2pAzuQJuOzwZ0,3962
|
|
14
|
+
craft_code-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
15
|
+
craft_code-0.1.0.dist-info/entry_points.txt,sha256=DGyWYFa6PvKo8Cg9PWtAX32cKQVO_V3lmeSlz0O35YM,51
|
|
16
|
+
craft_code-0.1.0.dist-info/licenses/LICENSE,sha256=0noz4EH8s_K8GHRQJmSKl7UY56UVxBy4pbawpT6Bo-0,1072
|
|
17
|
+
craft_code-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Shubham Agarwal
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|