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 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
@@ -0,0 +1,3 @@
1
+ """Deep Coding Agent — A Claude Code-like AI coding assistant built with Deep Agents."""
2
+
3
+ __version__ = "0.1.0"
code_craft/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Allow running with: python -m code_craft"""
2
+
3
+ from .cli import main
4
+
5
+ main()
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>&gt;</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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ code-craft = code_craft.cli:main
@@ -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.