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.
- regcode/__init__.py +5 -0
- regcode/cli.py +180 -0
- regcode/config.py +153 -0
- regcode/conversation_manager.py +154 -0
- regcode/main.py +893 -0
- regcode/monty_sandbox.py +415 -0
- regcode/permissions.py +27 -0
- regcode/sandbox.py +382 -0
- regcode/tools/__init__.py +13 -0
- regcode/tools/base.py +125 -0
- regcode/tools/builtins.py +947 -0
- regcode/tools/registry.py +78 -0
- regcode/tools/review_notes.py +122 -0
- regcode/tui.py +331 -0
- regcode-0.1.0.dist-info/METADATA +163 -0
- regcode-0.1.0.dist-info/RECORD +18 -0
- regcode-0.1.0.dist-info/WHEEL +4 -0
- regcode-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -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
|