regcode 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.
@@ -0,0 +1,78 @@
1
+ """Tool registry for managing available agent tools."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from regcode.tools.base import BaseTool, ToolResult
8
+
9
+
10
+ class ToolRegistry:
11
+ """Registry for agent tools. Manages tool discovery and execution."""
12
+
13
+ def __init__(self) -> None:
14
+ self._tools: dict[str, BaseTool] = {}
15
+
16
+ def register(self, tool: BaseTool) -> None:
17
+ """Register a tool with the registry."""
18
+ self._tools[tool.name] = tool
19
+
20
+ def unregister(self, tool_name: str) -> None:
21
+ """Unregister a tool by name."""
22
+ self._tools.pop(tool_name, None)
23
+
24
+ def get(self, name: str) -> BaseTool | None:
25
+ """Get a tool by name."""
26
+ return self._tools.get(name)
27
+
28
+ def list_tools(self) -> list[dict[str, Any]]:
29
+ """List all registered tools as OpenAI-compatible tool definitions."""
30
+ result = []
31
+ for tool in self._tools.values():
32
+ result.append({
33
+ "type": "function",
34
+ "function": {
35
+ "name": tool.name,
36
+ "description": tool.description,
37
+ "parameters": {
38
+ p.name: {
39
+ "type": p.type,
40
+ "description": p.description,
41
+ "required": p.required,
42
+ "default": p.default,
43
+ }
44
+ for p in tool.params
45
+ }
46
+ }
47
+ })
48
+ return result
49
+
50
+ def execute(self, tool_name: str, **kwargs: Any) -> ToolResult:
51
+ """Execute a registered tool by name."""
52
+ tool = self.get(tool_name)
53
+ if tool is None:
54
+ return ToolResult(
55
+ output=(
56
+ f"Unknown tool: {tool_name}. "
57
+ f"Available: {', '.join(self._tools.keys())}"
58
+ ),
59
+ error=True,
60
+ )
61
+ validation = tool.validate_params(**kwargs)
62
+ if validation is not None:
63
+ return validation
64
+ return tool.execute(**kwargs)
65
+
66
+ def enable_tool(self, tool_name: str) -> bool:
67
+ """Enable a tool (mark for use). Returns False if not registered."""
68
+ # For now, all registered tools are enabled
69
+ return tool_name in self._tools
70
+
71
+ def disable_tool(self, tool_name: str) -> bool:
72
+ """Disable a tool by removing it from execution."""
73
+ return self._tools.pop(tool_name, None) is not None
74
+
75
+ @property
76
+ def enabled_tools(self) -> list[str]:
77
+ """Get list of enabled tool names."""
78
+ return list(self._tools.keys())
@@ -0,0 +1,122 @@
1
+ """Review note tools for persistent storage across context compaction.
2
+
3
+ These tools allow the agent to save preliminary findings and observations
4
+ about the codebase that persist even when the context is compacted.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import TYPE_CHECKING, Any
10
+
11
+ from regcode.tools.base import BaseTool, ToolParam, ToolResult
12
+
13
+ if TYPE_CHECKING:
14
+ from regcode.conversation_manager import ContextManager
15
+
16
+
17
+ class AddReviewNoteTool(BaseTool):
18
+ """Add a review note to persistent storage.
19
+
20
+ Review notes are preliminary findings about the codebase that survive
21
+ context window compaction. Useful for tracking observations, potential
22
+ issues, or architectural patterns discovered during analysis.
23
+ """
24
+
25
+ name = "add_review_note"
26
+ description = (
27
+ "Add a review note to persistent storage. "
28
+ "Use this to save preliminary findings about the codebase "
29
+ "that will survive context window compaction. "
30
+ "Notes are useful for tracking observations, "
31
+ "potential issues, or architectural patterns."
32
+ )
33
+
34
+ def __init__(self, context_manager: ContextManager) -> None:
35
+ self._context_manager = context_manager
36
+
37
+ @property
38
+ def params(self) -> list[ToolParam]:
39
+ return [
40
+ ToolParam(
41
+ "title",
42
+ "string",
43
+ "Title for the review note",
44
+ required=True,
45
+ ),
46
+ ToolParam(
47
+ "content",
48
+ "string",
49
+ "The review note content",
50
+ required=True,
51
+ ),
52
+ ToolParam(
53
+ "importance",
54
+ "string",
55
+ "Importance level: low, medium, high",
56
+ required=False,
57
+ default="medium",
58
+ ),
59
+ ]
60
+
61
+ def execute(self, **kwargs: Any) -> ToolResult:
62
+ title = kwargs.get("title")
63
+ content = kwargs.get("content")
64
+ importance = kwargs.get("importance", "medium")
65
+
66
+ if not title:
67
+ return ToolResult(
68
+ output="Missing required parameter: title",
69
+ error=True,
70
+ )
71
+ if not content:
72
+ return ToolResult(
73
+ output="Missing required parameter: content",
74
+ error=True,
75
+ )
76
+
77
+ self._context_manager.add_review_note(
78
+ title=title,
79
+ content=content,
80
+ importance=importance,
81
+ )
82
+ return ToolResult(
83
+ output=f"Review note '{title}' added successfully",
84
+ exit_code=0,
85
+ )
86
+
87
+
88
+ class ReadReviewNotesTool(BaseTool):
89
+ """Read all review notes from persistent storage.
90
+
91
+ Returns all saved review notes, which persist across context window
92
+ compaction. Use this to recall previous findings about the codebase.
93
+ """
94
+
95
+ name = "read_review_notes"
96
+ description = (
97
+ "Read all review notes from persistent storage. "
98
+ "Returns all previously saved review notes about the codebase."
99
+ )
100
+
101
+ def __init__(self, context_manager: ContextManager) -> None:
102
+ self._context_manager = context_manager
103
+
104
+ @property
105
+ def params(self) -> list[ToolParam]:
106
+ return []
107
+
108
+ def execute(self, **kwargs: Any) -> ToolResult:
109
+ notes = self._context_manager.get_review_notes()
110
+ if not notes:
111
+ return ToolResult(output="No review notes found", exit_code=0)
112
+
113
+ lines = []
114
+ for note in notes:
115
+ lines.append(
116
+ f"- [{note.get('importance', 'medium')}] {note.get('title')}: "
117
+ f"{note.get('content', '')}"
118
+ )
119
+ return ToolResult(
120
+ output="\n".join(lines),
121
+ exit_code=0,
122
+ )
regcode/tui.py ADDED
@@ -0,0 +1,331 @@
1
+ """Terminal UI for the RegCode agent CLI.
2
+
3
+ Provides colored, structured output for user/assistant messages,
4
+ tool calls, tool results, and agent status updates.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import sys
10
+ import textwrap
11
+ from enum import Enum
12
+ from typing import IO
13
+
14
+ import click
15
+
16
+ from regcode.main import ToolCall
17
+
18
+ # ANSI color codes
19
+ _RESET = "\033[0m"
20
+ _BOLD = "\033[1m"
21
+ _DIM = "\033[2m"
22
+ _UNDERLINE = "\033[4m"
23
+
24
+ # Foreground colors
25
+ _CYAN = "\033[36m"
26
+ _GREEN = "\033[32m"
27
+ _YELLOW = "\033[33m"
28
+ _MAGENTA = "\033[35m"
29
+ _BLUE = "\033[34m"
30
+ _RED = "\033[31m"
31
+ _GRAY = "\033[90m"
32
+ _WHITE = "\033[97m"
33
+ _ORANGE = "\033[38;5;208m"
34
+
35
+ # Status icons (simple text alternatives for terminal compatibility)
36
+ _ICONS = {
37
+ "tool_call": "[TOOL]",
38
+ "tool_result": "[RESULT]",
39
+ "idle": "[IDLE]",
40
+ "budget_exhausted": "[BUDGET]",
41
+ }
42
+
43
+ # Separator characters
44
+ _SEP_CHAR = "─"
45
+
46
+
47
+ def _wrap_text(text: str, width: int = 72, indent: str = " ") -> str:
48
+ """Wrap text with an indent for readable terminal output."""
49
+ wrapped = textwrap.fill(text, width=width)
50
+ return "\n".join(f"{indent}{line}" for line in wrapped.splitlines())
51
+
52
+
53
+ class _ColorFormatter:
54
+ """Format terminal output with ANSI colors."""
55
+
56
+ def __init__(self, stream: IO[str] = sys.stdout, force_color: bool = False) -> None:
57
+ # Check if stdout is a TTY or force color
58
+ self._stream = stream
59
+ self._force_color = force_color or sys.stdout.isatty()
60
+
61
+ def colored(self, color: str, text: str) -> str:
62
+ """Return colored text, or plain text if colors disabled."""
63
+ if self._force_color:
64
+ return f"{color}{text}{_RESET}"
65
+ return text
66
+
67
+ def user_message(self, message: str) -> None:
68
+ """Print a user message with green highlight."""
69
+ header = self.colored(_GREEN + _BOLD, "YOU")
70
+ self._stream.write(f"{header} ")
71
+ self._stream.write(_wrap_text(message) + "\n")
72
+ self._stream.flush()
73
+
74
+ def assistant_message(self, message: str) -> None:
75
+ """Print an assistant message with blue/cyan highlight."""
76
+ header = self.colored(_CYAN + _BOLD, "AGENT")
77
+ self._stream.write(f"{header} ")
78
+ if message.strip():
79
+ self._stream.write(_wrap_text(message) + "\n")
80
+ else:
81
+ self._stream.write(" (empty response)\n")
82
+ self._stream.flush()
83
+
84
+ def streaming_indicator(self) -> None:
85
+ """Print a streaming indicator with blinking cursor."""
86
+ if self._force_color:
87
+ self._stream.write(
88
+ f"{self.colored(_CYAN + _BOLD, 'AGENT')}"
89
+ f" {_DIM}[streaming]{_RESET} "
90
+ )
91
+ else:
92
+ self._stream.write("AGENT [streaming] ")
93
+ self._stream.flush()
94
+
95
+ def tool_call(self, tool_name: str, tool_args: dict | None = None) -> None:
96
+ """Print a tool call notification in yellow - compact, no extra blank lines."""
97
+ header = self.colored(_YELLOW + _BOLD, _ICONS["tool_call"])
98
+ parts = [self.colored(_YELLOW + _BOLD, tool_name)]
99
+ if tool_args:
100
+ for key, value in tool_args.items():
101
+ val_str = str(value)
102
+ if len(val_str) > 40:
103
+ val_str = val_str[:37] + "..."
104
+ parts.append(f"{key}={val_str}")
105
+ line = f" {header} " + " ".join(parts)
106
+ self._stream.write(line + "\n")
107
+ self._stream.flush()
108
+
109
+ def tool_result(self, tool_name: str, result: str, error: bool = False) -> None:
110
+ """Print a tool result in magenta (or red if error)."""
111
+ color = _RED if error else _MAGENTA
112
+ header = self.colored(color + _BOLD, _ICONS["tool_result"])
113
+ self._stream.write(f"\n{header} {self.colored(color + _BOLD, tool_name)}\n")
114
+ truncated = result[:500] + ("..." if len(result) > 500 else "")
115
+ self._stream.write(_wrap_text(truncated) + "\n")
116
+ self._stream.flush()
117
+
118
+ def status_update(self, status: Enum, tool_call_info=None) -> None:
119
+ """Print a status update in gray dim text at the bottom area."""
120
+ color = _GRAY
121
+ if status.value == "budget_exhausted":
122
+ color = _RED
123
+ elif status.value == "idle":
124
+ color = _DIM
125
+
126
+ icon = _ICONS.get(status.value, "[STATUS]")
127
+ header = self.colored(color + _BOLD, icon)
128
+
129
+ if status.value == "budget_exhausted":
130
+ status_text = self.colored(color + _BOLD, "Tool budget exhausted.")
131
+ self._stream.write(f"\n{header} {status_text}\n")
132
+ elif status.value == "idle":
133
+ status_text = self.colored(color, "Agent is idle. Waiting for your input.")
134
+ self._stream.write(f"\n{header} {status_text}\n")
135
+ else:
136
+ status_text = self.colored(color, "Agent is working...")
137
+ self._stream.write(f"\n{header} {status_text}\n")
138
+ self._stream.flush()
139
+
140
+ def separator(self) -> None:
141
+ """Print a visual separator line."""
142
+ if self._force_color:
143
+ self._stream.write(
144
+ f"{self.colored(_DIM, _SEP_CHAR * 72)}\n"
145
+ )
146
+ else:
147
+ self._stream.write(f"{_SEP_CHAR * 72}\n")
148
+ self._stream.flush()
149
+
150
+ def heading(self, text: str) -> None:
151
+ """Print a bold heading."""
152
+ self._stream.write(f"{_BOLD}{text}{_RESET}\n")
153
+ self._stream.flush()
154
+
155
+ def info(self, text: str) -> None:
156
+ """Print an informational message in gray."""
157
+ self._stream.write(f"{self.colored(_GRAY, text)}\n")
158
+ self._stream.flush()
159
+
160
+ def flush(self) -> None:
161
+ """Flush the underlying output stream."""
162
+ self._stream.flush()
163
+
164
+ def error(self, text: str) -> None:
165
+ """Print an error message in red."""
166
+ prefix = "ERROR: " + text
167
+ self._stream.write(f"{self.colored(_RED + _BOLD, prefix)}\n")
168
+ self._stream.flush()
169
+
170
+ def success(self, text: str) -> None:
171
+ """Print a success message in green."""
172
+ self._stream.write(f"{self.colored(_GREEN, text)}\n")
173
+ self._stream.flush()
174
+
175
+
176
+ class ChatUI:
177
+ """Terminal UI for the RegCode chat session.
178
+
179
+ Provides colored, structured output for user/assistant messages,
180
+ tool calls, tool results, and agent status updates.
181
+
182
+ Usage:
183
+ ui = ChatUI()
184
+ ui.print_heading("RegCode v0.1.0")
185
+ ui.print_separator()
186
+
187
+ # Set up status callback on the agent
188
+ agent = Agent(
189
+ config=config,
190
+ status_callback=ui.handle_status,
191
+ )
192
+
193
+ # Chat loop
194
+ ui.print_user_message("Hello")
195
+ reply = agent.chat("Hello")
196
+ ui.print_assistant_message(reply)
197
+ """
198
+
199
+ def __init__(self, stream: IO[str] = sys.stdout, force_color: bool = False) -> None:
200
+ self._formatter = _ColorFormatter(stream, force_color)
201
+ self._stream = stream
202
+ self._streaming = False
203
+
204
+ @property
205
+ def formatter(self) -> _ColorFormatter:
206
+ """Return the underlying formatter."""
207
+ return self._formatter
208
+
209
+ # -- Public API --
210
+
211
+ def print_welcome(self, version: str = "0.1.0") -> None:
212
+ """Print the welcome/banner message."""
213
+ self._formatter.heading(f"RegCode - Minimalistic Coding Agent v{version}")
214
+ self._formatter.info(
215
+ "Type your message and press Enter. Agent will respond with full "
216
+ "tool access."
217
+ )
218
+ self._formatter.info("Press Ctrl+C to exit.")
219
+ self._stream.write("\n")
220
+ self._formatter.flush()
221
+
222
+ def print_separator(self) -> None:
223
+ """Print a visual separator between message rounds."""
224
+ self._formatter.separator()
225
+
226
+ def print_user_message(self, message: str) -> None:
227
+ """Print a formatted user message."""
228
+ self._formatter.user_message(message)
229
+
230
+ def print_assistant_message(self, message: str) -> None:
231
+ """Print a formatted assistant message."""
232
+ self._formatter.assistant_message(message)
233
+
234
+ def print_tool_call(self, tool_name: str, tool_args: dict | None = None) -> None:
235
+ """Print a tool call notification."""
236
+ self._formatter.tool_call(tool_name, tool_args)
237
+
238
+ def print_tool_result(
239
+ self, tool_name: str, result: str, error: bool = False
240
+ ) -> None:
241
+ """Print a tool result."""
242
+ self._formatter.tool_result(tool_name, result, error)
243
+
244
+ def handle_status(self, status, tool_call_info=None) -> None:
245
+ """Status callback handler for the Agent.
246
+
247
+ This method is called by the agent's status_callback mechanism
248
+ to report what the agent is currently doing.
249
+ """
250
+ if status.value == "tool_call" and tool_call_info is not None:
251
+ self.print_tool_call(
252
+ tool_call_info.tool_name,
253
+ tool_call_info.tool_args,
254
+ )
255
+ elif status.value == "tool_result" and tool_call_info is not None:
256
+ # Skip - tool results are printed via result_callback
257
+ pass
258
+ elif status.value == "budget_exhausted":
259
+ self._formatter.status_update(status)
260
+ elif status.value == "idle":
261
+ self._formatter.status_update(status)
262
+
263
+ def handle_result(
264
+ self,
265
+ status,
266
+ result_str: str,
267
+ error: bool,
268
+ tool_call: ToolCall | None = None,
269
+ ) -> None:
270
+ """Handle tool result callback.
271
+ Tool results are NOT displayed - only tool calls are shown via status_callback.
272
+ """
273
+ pass # Silence tool results - user only needs to see tool being called
274
+
275
+ def print_text_chunk(self, chunk: str) -> None:
276
+ """Print a text chunk from streaming output."""
277
+ sys.stdout.write(chunk)
278
+ sys.stdout.flush()
279
+
280
+ def get_user_input(self) -> str | None:
281
+ """Get user input with visible prompt. click.prompt echoes the input."""
282
+ try:
283
+ msg = click.prompt("YOU", prompt_suffix="> ", show_default=False)
284
+ except EOFError:
285
+ return None
286
+ return msg
287
+
288
+ def print_exit(self) -> None:
289
+ """Print exit message."""
290
+ self._formatter.info("Goodbye!")
291
+
292
+ def print_round_complete(self) -> None:
293
+ """Print a subtle separator after a complete round."""
294
+ if self._formatter._force_color:
295
+ self._stream.write(f"{self._formatter.colored(_DIM, '─' * 72)}\n")
296
+ else:
297
+ self._stream.write(f"{'─' * 72}\n")
298
+ self._stream.flush()
299
+
300
+ def print_interrupted(self) -> None:
301
+ """Print interrupted message."""
302
+ self._formatter.info("Interrupted by user. Goodbye!")
303
+
304
+ def start_streaming(self) -> None:
305
+ """Print a streaming indicator before streaming output."""
306
+ self._streaming = True
307
+ self._formatter.streaming_indicator()
308
+ self._stream.flush()
309
+
310
+ def end_streaming(self) -> None:
311
+ """Finalize streaming output with a newline."""
312
+ self._streaming = False
313
+ self._stream.write("\n")
314
+ self._stream.flush()
315
+
316
+ def print_prompt(self) -> None:
317
+ """Print the user input prompt."""
318
+ if self._formatter._force_color:
319
+ you_label = self._formatter.colored(_GREEN + _BOLD, "YOU")
320
+ self._stream.write(f" {you_label} ")
321
+ else:
322
+ self._stream.write(" YOU ")
323
+ self._stream.flush()
324
+
325
+ def clear(self) -> None:
326
+ """Clear the terminal screen (optional, best-effort)."""
327
+ try:
328
+ self._stream.write("\033[2J\033[H")
329
+ self._stream.flush()
330
+ except (IOError, ValueError):
331
+ pass
@@ -0,0 +1,163 @@
1
+ Metadata-Version: 2.4
2
+ Name: regcode
3
+ Version: 0.1.0
4
+ Summary: Minimalistic coding agent with Python API and CLI
5
+ Requires-Python: >=3.12
6
+ Requires-Dist: click>=8.0
7
+ Requires-Dist: litellm>=1.83.17
8
+ Requires-Dist: pydantic-monty>=0.0.18
9
+ Requires-Dist: pydantic-settings>=2.14.2
10
+ Requires-Dist: pytest>=8.0
11
+ Requires-Dist: pyyaml>=6.0
12
+ Requires-Dist: ruff>=0.8
13
+ Provides-Extra: dev
14
+ Description-Content-Type: text/markdown
15
+
16
+ # RegCode
17
+
18
+ A minimalistic coding agent with Python API bindings and CLI interface.
19
+
20
+ ## Features
21
+
22
+ - **litellm** as the LLM backbone (supports OpenAI, Anthropic, and 100+ providers)
23
+ - **YAML configuration** for model, tokens, temperature, provider settings
24
+ - **Pydantic models** for type-safe config loading with environment variable expansion
25
+ - **CLI** via click: `chat`, `configure`, `version`
26
+ - **Python API**: `import regcode; agent = regcode.Agent()`
27
+ - **Host filesystem I/O**: `write_file` and `patch_file` operate directly on the host filesystem with path traversal protection
28
+ - **Patch tool**: `patch_file` replaces a range of lines in an existing file
29
+ - **Robust tool call handling**: malformed tool calls and JSON parse errors are gracefully handled
30
+ - **Progressive streaming**: TUI always streams assistant responses progressively rather than all at once
31
+
32
+ ## Quick Start
33
+
34
+ ```bash
35
+ # Install dependencies
36
+ uv sync
37
+
38
+ # Configure your API key (or set OPENAI_API_KEY in your environment)
39
+ # Edit config.yaml or use environment variables
40
+
41
+ # Chat with the agent
42
+ uv run regcode chat
43
+
44
+ # View current config
45
+ uv run regcode configure
46
+
47
+ # View version
48
+ uv run regcode version
49
+ ```
50
+
51
+ ## Usage as a Library
52
+
53
+ ```python
54
+ import regcode
55
+
56
+ # Load config (reads config.yaml, falls back to defaults)
57
+ config = regcode.Config.load("config.yaml")
58
+
59
+ # Create agent
60
+ agent = regcode.Agent(config=config)
61
+
62
+ # Chat (TUI always streams responses progressively)
63
+ reply = agent.chat("Refactor this function to use async/await.")
64
+ print(reply)
65
+
66
+ # Reset conversation
67
+ agent.reset()
68
+ ```
69
+
70
+ ## Tools
71
+
72
+ RegCode includes several built-in tools the agent can use:
73
+
74
+ | Tool | Permission | Description |
75
+ |------|-----------|-------------|
76
+ | `run_script` | `EXECUTE` | Execute a Python script in the Monty sandbox |
77
+ | `shell_command` | `EXECUTE` | Execute shell commands (subprocess, not sandbox) |
78
+ | `read_file` | `READ` | Read the contents of a file from the filesystem |
79
+ | `write_file` | `WRITE` | Write content to a file on the host filesystem |
80
+ | `patch_file` | `WRITE` | Replace a range of lines in an existing file |
81
+ | `list_dir` | `READ` | List contents of a directory |
82
+ | `search_files` | `READ` | Search for files matching a pattern |
83
+ | `browse_dir` | `READ` | Recursively list all files in a directory tree |
84
+ | `search_dir` | `READ` | Search for a pattern inside all files within a directory |
85
+ | `fetch_git_diff` | `READ` | Fetch the git diff of the current repository |
86
+ | `add_review_note` | `WRITE` | Save review notes to persistent storage |
87
+ | `read_review_notes` | `READ` | Read previously saved review notes |
88
+ | `system_info` | `READ` | Get system information (OS, Python version, etc.) |
89
+
90
+ All file-writing tools (`write_file`, `patch_file`) include path traversal protection (`..` is blocked). The `patch_file` tool validates line number bounds and type correctness before performing the replacement.
91
+
92
+ ## Project Structure
93
+
94
+ ```
95
+ regcode/
96
+ ├── config.yaml # All configuration
97
+ ├── regcode/
98
+ │ ├── __init__.py # Python API entry point
99
+ │ ├── main.py # Agent core (chat, code review, etc.)
100
+ │ ├── cli.py # CLI entry point (click)
101
+ │ ├── config.py # Config loader (yaml + pydantic)
102
+ │ ├── permissions.py # Tool permission definitions
103
+ │ ├── tui.py # Terminal UI rendering
104
+ │ ├── conversation_manager.py
105
+ │ ├── sandbox.py
106
+ │ ├── monty_sandbox.py
107
+ │ └── tools/
108
+ │ ├── __init__.py
109
+ │ ├── base.py
110
+ │ ├── builtins.py # Built-in tool implementations
111
+ │ ├── registry.py
112
+ │ └── review_notes.py
113
+ ├── tests/
114
+ │ ├── conftest.py
115
+ │ ├── test_main.py
116
+ │ ├── test_config.py
117
+ │ ├── test_api.py
118
+ │ ├── test_cli.py
119
+ │ ├── test_context_manager.py
120
+ │ ├── test_monty_sandbox.py
121
+ │ ├── test_provider_extra.py
122
+ │ └── test_review_notes.py
123
+ ├── pyproject.toml
124
+ ├── uv.lock
125
+ └── README.md
126
+ ```
127
+
128
+ ## Testing & Linting
129
+
130
+ ```bash
131
+ uv run pytest tests/ -v
132
+ uv run ruff check --fix regcode/ tests/
133
+ ```
134
+
135
+ ## Configuration
136
+
137
+ Edit `config.yaml` to change the model, provider, or tools:
138
+
139
+ ```yaml
140
+ agent:
141
+ model: "openai/gpt-4o"
142
+ max_tokens: 4096
143
+ context_window: 128000
144
+ temperature: 0.7
145
+
146
+ provider:
147
+ name: "openai"
148
+ api_key: "${OPENAI_API_KEY}"
149
+
150
+ system_prompt: |
151
+ You are a coding agent. Help the user with code.
152
+
153
+ tools:
154
+ code_review: true
155
+ security_scan: false
156
+ sandbox: false
157
+ ```
158
+
159
+ Environment variables are expanded automatically (e.g., `${OPENAI_API_KEY}`).
160
+
161
+ ## License
162
+
163
+ MIT