regcode 0.1.0__tar.gz

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-0.1.0/.env ADDED
@@ -0,0 +1 @@
1
+ PYPI_TOKEN="pypi-AgEIcHlwaS5vcmcCJGRlMDQyOGU4LTI4YTEtNDVmNy04ZWUzLWU1NDdjNzhiNGJkNgACKlszLCI1NDJiMzFhOS1lNjBlLTQyYTYtOWY0Ny1mZDhhMWIyZTRmNTQiXQAABiDKhC48tmF9-4eFzQ3ZXXS3LEXaAsY2hSxXODNeVyMygw"
@@ -0,0 +1,13 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+ config.yaml
12
+ review.py
13
+ *.config.yaml
@@ -0,0 +1 @@
1
+ 3.12
@@ -0,0 +1,28 @@
1
+ # RegCode Instructions
2
+ Always make sure to comply to these rules. Under Project Stack, you will find technology you have to STRICTLY adhere to. Make sure to comply to instructions.
3
+
4
+ ## Project Stack:
5
+ - `uv`: Dependency management for Python runtime
6
+ - Alembic: Database migrations
7
+ - FastAPI: Backend Logic
8
+ - SQLAlchemy ORM: Python database ORM
9
+ - PostgreSQL/SQLITE: Target databases
10
+ - Pytest: Test suite under `tests`
11
+ - NextJS/React: Frontend Code under `frontend/`
12
+ - GitLab: Code Repository/ CI CD
13
+ - .gitlab-ci.yml: Gitlab Runner Config
14
+ - `ruff`: Linting and Code Quality Control
15
+ - `docker`: Deployment is done via `docker-compose.yml`
16
+ - `pyproject.toml`: Project Setup Config
17
+
18
+ ## Coding/Implentation Instructions:
19
+ - Always use `alembic` for database structure. Using `Base.create_all()` and similar syntax is PROHIBITED!
20
+ - Use `alembic revision -m <revision explanation>` to create new revisions. Making arbitrary strings as versions etc. is PROHIBITED!
21
+ - ALWAYS run `uv run ruff check --fix` after all changes at the end and fix linting issues
22
+ - ALWAYS run `uv run pytest` to run test suite after all changes and fix issues shown by tests.
23
+ - ALWAYS add test cases if relevant.
24
+ - NEVER use emojis! Use icon packs (for example `lucid-react`) if needed.
25
+ - NEVER commit changes. It is upto user to review your code and commit and push it. Unless user specifically asks for it, commiting code is PROHIBITED!
26
+
27
+ ## Project Vision
28
+ Under `GOALS.md` you will find current implementation scope and your tasks you need to read and implement. Update objectives in GOALS.md after you complete them and use it to track state of implementation. That is MANDATORY!
regcode-0.1.0/GOALS.md ADDED
@@ -0,0 +1,502 @@
1
+ # RegCode Goals
2
+
3
+ ## Vision
4
+ A minimalistic coding agent with:
5
+ - Python API bindings
6
+ - CLI interface
7
+ - YAML configuration (max_tokens, context_window, litellm provider settings, etc.)
8
+ - litellm as the LLM backbone
9
+
10
+ ---
11
+
12
+ ## Architecture
13
+
14
+ ```
15
+ regcode/
16
+ ├── config.yaml # All configuration
17
+ ├── regcode/
18
+ │ ├── __init__.py # Python API entry point
19
+ │ ├── main.py # Agent core (chat, code review, etc.)
20
+ │ ├── cli.py # CLI entry point (click)
21
+ │ └── config.py # Config loader (yaml + pydantic)
22
+ ├── tests/
23
+ │ ├── conftest.py
24
+ │ └── test_main.py
25
+ ├── pyproject.toml
26
+ ├── uv.lock
27
+ └── README.md
28
+ ```
29
+
30
+ No database. No Alembic. No Docker. No NextJS. No API endpoints.
31
+ Just a Python agent with CLI and config.
32
+
33
+ ---
34
+
35
+ ## Phase 1: Configuration
36
+
37
+ ### Step 1: Create config.yaml and config loader
38
+
39
+ **File: `config.yaml`**
40
+ ```yaml
41
+ agent:
42
+ model: "openai/gpt-4o"
43
+ max_tokens: 4096
44
+ context_window: 128000
45
+ temperature: 0.7
46
+
47
+ provider:
48
+ name: "openai"
49
+ api_key: "${OPENAI_API_KEY}"
50
+
51
+ system_prompt: |
52
+ You are a coding agent. Help the user with code.
53
+
54
+ tools:
55
+ code_review: true
56
+ security_scan: false
57
+ sandbox: false
58
+ ```
59
+
60
+ **File: `regcode/config.py`**
61
+ ```python
62
+ import os
63
+ from pathlib import Path
64
+
65
+ import yaml
66
+ from pydantic import BaseModel
67
+
68
+
69
+ class AgentConfig(BaseModel):
70
+ model: str = "openai/gpt-4o"
71
+ max_tokens: int = 4096
72
+ context_window: int = 128000
73
+ temperature: float = 0.7
74
+
75
+
76
+ class ProviderConfig(BaseModel):
77
+ name: str = "openai"
78
+ api_key: str = ""
79
+
80
+
81
+ class Config(BaseModel):
82
+ agent: AgentConfig = AgentConfig()
83
+ provider: ProviderConfig = ProviderConfig()
84
+ system_prompt: str = "You are a coding agent."
85
+ tools: dict = {}
86
+
87
+ @classmethod
88
+ def load(cls, path: str | Path = "config.yaml") -> "Config":
89
+ p = Path(path)
90
+ if not p.exists():
91
+ return cls()
92
+ with open(p) as f:
93
+ data = yaml.safe_load(f)
94
+ # Expand env vars in values
95
+ data = _expand_env_vars(data)
96
+ return cls(**data)
97
+
98
+
99
+ def _expand_env_vars(obj):
100
+ if isinstance(obj, str):
101
+ return os.path.expandvars(obj)
102
+ if isinstance(obj, dict):
103
+ return {k: _expand_env_vars(v) for k, v in obj.items()}
104
+ if isinstance(obj, list):
105
+ return [_expand_env_vars(v) for v in obj]
106
+ return obj
107
+ ```
108
+
109
+ **Test:**
110
+ ```python
111
+ # tests/test_config.py
112
+ import tempfile, os
113
+ from regcode.config import Config
114
+
115
+ def test_config_load_defaults():
116
+ c = Config()
117
+ assert c.agent.model == "openai/gpt-4o"
118
+ assert c.agent.max_tokens == 4096
119
+
120
+ def test_config_load_from_file(tmp_path):
121
+ yaml_file = tmp_path / "config.yaml"
122
+ yaml_file.write_text("agent:\n model: anthropic/claude-3-opus\n temperature: 0.5\n")
123
+ c = Config.load(yaml_file)
124
+ assert c.agent.model == "anthropic/claude-3-opus"
125
+ assert c.agent.temperature == 0.5
126
+
127
+ def test_config_env_var_expansion(tmp_path):
128
+ os.environ["TEST_KEY"] = "sk-test123"
129
+ yaml_file = tmp_path / "config.yaml"
130
+ yaml_file.write_text('provider:\n api_key: "${TEST_KEY}"\n')
131
+ c = Config.load(yaml_file)
132
+ assert c.provider.api_key == "sk-test123"
133
+ ```
134
+
135
+ ---
136
+
137
+ ## Phase 2: Agent Core
138
+
139
+ ### Step 2: Create the agent class using litellm
140
+
141
+ **File: `regcode/main.py`**
142
+
143
+ ```python
144
+ import litellm
145
+
146
+ from regcode.config import Config
147
+
148
+
149
+ class Agent:
150
+ """Core coding agent using litellm for LLM calls."""
151
+
152
+ def __init__(self, config: Config | None = None):
153
+ self.config = config or Config.load()
154
+ self.system_prompt = self.config.system_prompt
155
+ self._history: list[dict] = []
156
+
157
+ def chat(self, message: str, stream: bool = False) -> str:
158
+ """Send a message and get a response."""
159
+ messages = [
160
+ {"role": "system", "content": self.system_prompt},
161
+ *self._history,
162
+ {"role": "user", "content": message},
163
+ ]
164
+
165
+ if stream:
166
+ return self._stream(messages)
167
+
168
+ response = litellm.completion(
169
+ model=self.config.agent.model,
170
+ messages=messages,
171
+ max_tokens=self.config.agent.max_tokens,
172
+ temperature=self.config.agent.temperature,
173
+ api_key=self.config.provider.api_key or None,
174
+ )
175
+ reply = response.choices[0].message.content
176
+ self._history.append({"role": "user", "content": message})
177
+ self._history.append({"role": "assistant", "content": reply})
178
+ return reply
179
+
180
+ def _stream(self, messages: list[dict]) -> str:
181
+ """Stream response."""
182
+ response = litellm.completion(
183
+ model=self.config.agent.model,
184
+ messages=messages,
185
+ max_tokens=self.config.agent.max_tokens,
186
+ temperature=self.config.agent.temperature,
187
+ stream=True,
188
+ api_key=self.config.provider.api_key or None,
189
+ )
190
+ full = ""
191
+ for chunk in response:
192
+ delta = chunk.choices[0].delta.content
193
+ if delta:
194
+ full += delta
195
+ return full
196
+
197
+ def reset(self):
198
+ """Clear conversation history."""
199
+ self._history = []
200
+
201
+ @property
202
+ def history(self) -> list[dict]:
203
+ return list(self._history)
204
+ ```
205
+
206
+ **Test:**
207
+ ```python
208
+ # tests/test_main.py
209
+ from unittest.mock import patch, MagicMock
210
+ from regcode.main import Agent
211
+ from regcode.config import Config
212
+
213
+
214
+ def test_agent_chat():
215
+ config = Config(
216
+ agent={"model": "openai/gpt-4o", "max_tokens": 100},
217
+ system_prompt="test",
218
+ )
219
+ agent = Agent(config=config)
220
+
221
+ with patch("regcode.main.litellm.completion") as mock_completion:
222
+ mock_response = MagicMock()
223
+ mock_response.choices = [
224
+ MagicMock(message=MagicMock(content="Hello from agent"))
225
+ ]
226
+ mock_completion.return_value = mock_response
227
+
228
+ result = agent.chat("hi")
229
+ assert result == "Hello from agent"
230
+
231
+ # Check history was recorded
232
+ assert len(agent.history) == 2
233
+ assert agent.history[0]["role"] == "user"
234
+ assert agent.history[1]["role"] == "assistant"
235
+
236
+
237
+ def test_agent_reset():
238
+ config = Config()
239
+ agent = Agent(config=config)
240
+ with patch("regcode.main.litellm.completion") as mock_completion:
241
+ mock_completion.return_value = MagicMock(
242
+ choices=[MagicMock(message=MagicMock(content="x"))]
243
+ )
244
+ agent.chat("msg")
245
+ assert len(agent.history) == 2
246
+ agent.reset()
247
+ assert len(agent.history) == 0
248
+
249
+
250
+ def test_agent_stream():
251
+ config = Config()
252
+ agent = Agent(config=config)
253
+ with patch("regcode.main.litellm.completion") as mock_completion:
254
+ mock_chunk = MagicMock(choices=[MagicMock(delta=MagicMock(content="ok"))])
255
+ mock_completion.return_value = [mock_chunk]
256
+ result = agent.chat("hi", stream=True)
257
+ assert result == "ok"
258
+ ```
259
+
260
+ ---
261
+
262
+ ## Phase 3: Python API
263
+
264
+ ### Step 3: Create clean public API
265
+
266
+ **File: `regcode/__init__.py`**
267
+ ```python
268
+ from regcode.config import Config
269
+ from regcode.main import Agent
270
+
271
+ __all__ = ["Agent", "Config"]
272
+ __version__ = "0.1.0"
273
+ ```
274
+
275
+ **Test:**
276
+ ```python
277
+ # tests/test_api.py
278
+ import regcode
279
+
280
+
281
+ def test_import():
282
+ assert hasattr(regcode, "Agent")
283
+ assert hasattr(regcode, "Config")
284
+ assert regcode.__version__ == "0.1.0"
285
+
286
+
287
+ def test_agent_instantiation():
288
+ agent = regcode.Agent()
289
+ assert agent.config is not None
290
+ ```
291
+
292
+ ---
293
+
294
+ ## Phase 4: CLI
295
+
296
+ ### Step 4: Create CLI with click
297
+
298
+ **File: `regcode/cli.py`**
299
+
300
+ ```python
301
+ import click
302
+ import regcode
303
+
304
+
305
+ @click.group()
306
+ def cli():
307
+ """RegCode - Minimalistic Coding Agent"""
308
+ pass
309
+
310
+
311
+ @cli.command()
312
+ @click.option("--model", default=None, help="Model to use (e.g. openai/gpt-4o)")
313
+ @click.option("--config", "config_path", default="config.yaml", help="Path to config.yaml")
314
+ @click.option("--stream", is_flag=True, help="Stream response")
315
+ def chat(model, config_path, stream):
316
+ """Chat with the agent."""
317
+ config = regcode.Config.load(config_path)
318
+ if model:
319
+ config.agent.model = model
320
+ agent = regcode.Agent(config=config)
321
+
322
+ if stream:
323
+ msg = click.prompt("You", type=str)
324
+ full = ""
325
+ click.echo("Agent> ", nl=False)
326
+ for word in agent.chat(msg, stream=True).split():
327
+ click.echo(word, nl=False)
328
+ click.echo(" ", nl=False)
329
+ click.echo()
330
+ else:
331
+ msg = click.prompt("You", type=str)
332
+ reply = agent.chat(msg)
333
+ click.echo(f"Agent> {reply}")
334
+
335
+
336
+ @cli.command()
337
+ def configure():
338
+ """Print config or generate config.yaml."""
339
+ config = regcode.Config.load()
340
+ click.echo(config.model_dump_json(indent=2))
341
+
342
+
343
+ @cli.command()
344
+ def version():
345
+ """Print version."""
346
+ click.echo(f"RegCode v{regcode.__version__}")
347
+
348
+
349
+ if __name__ == "__main__":
350
+ cli()
351
+ ```
352
+
353
+ **pyproject.toml entry point:**
354
+ ```toml
355
+ [project.scripts]
356
+ regcode = "regcode.cli:cli"
357
+ ```
358
+
359
+ **Test:**
360
+ ```python
361
+ # tests/test_cli.py
362
+ from click.testing import CliRunner
363
+ from regcode.cli import cli
364
+
365
+
366
+ def test_cli_help():
367
+ r = CliRunner()
368
+ result = r.invoke(cli, ["--help"])
369
+ assert result.exit_code == 0
370
+ assert "RegCode" in result.output
371
+
372
+
373
+ def test_version_command():
374
+ r = CliRunner()
375
+ result = r.invoke(cli, ["version"])
376
+ assert result.exit_code == 0
377
+ assert "v0.1.0" in result.output
378
+
379
+
380
+ def test_configure_command():
381
+ r = CliRunner()
382
+ result = r.invoke(cli, ["configure"])
383
+ assert result.exit_code == 0
384
+ assert "model" in result.output
385
+ ```
386
+
387
+ ---
388
+
389
+ ## Phase 5: Add Dependencies
390
+
391
+ **pyproject.toml:**
392
+ ```toml
393
+ [project]
394
+ name = "regcode"
395
+ version = "0.1.0"
396
+ description = "Minimalistic coding agent with Python API and CLI"
397
+ readme = "README.md"
398
+ requires-python = ">=3.12"
399
+ dependencies = [
400
+ "litellm>=1.83.17",
401
+ "pydantic-settings>=2.14.2",
402
+ "pyyaml>=6.0",
403
+ "click>=8.0",
404
+ ]
405
+
406
+ [project.scripts]
407
+ regcode = "regcode.cli:cli"
408
+
409
+ [project.optional-dependencies]
410
+ dev = [
411
+ "pytest>=8.0",
412
+ "ruff>=0.8",
413
+ ]
414
+
415
+ [tool.ruff.lint]
416
+ select = ["E", "F", "W", "I"]
417
+
418
+ [tool.pytest.ini_options]
419
+ testpaths = ["tests"]
420
+ ```
421
+
422
+ ---
423
+
424
+ ## Verification
425
+
426
+ ```bash
427
+ uv sync
428
+ uv run pytest tests/ -v
429
+ uv run ruff check --fix regcode/ tests/
430
+ uv run regcode --help
431
+ uv run regcode configure
432
+ ```
433
+
434
+ ---
435
+
436
+ ## Phase 6: Monty Sandbox Integration
437
+
438
+ ### Step 6: Implement secure code execution with pydantic-monty
439
+
440
+ **File: `regcode/monty_sandbox.py`**
441
+
442
+ ```python
443
+ # Monty-based sandbox with:
444
+ # - Rust-based secure Python interpreter
445
+ # - Resource limits (memory, CPU, recursion, allocations)
446
+ # - External function registration (whiteboxed)
447
+ # - File I/O within sandbox directory
448
+ # - Proper event loop handling for pytest compatibility
449
+ ```
450
+
451
+ **File: `regcode/tools/builtins.py`** - Updated to use `MontySandbox` instead of `subprocess`
452
+
453
+ **File: `regcode/config.py`** - Added `SandboxConfig` with `use_monty` toggle
454
+
455
+ **File: `regcode/main.py`** - Agent now uses `MontySandbox` by default
456
+
457
+ **Test:** `tests/test_monty_sandbox.py` - 29 tests covering execution, limits, file I/O, external functions, integration
458
+
459
+ ## Implementation Status
460
+
461
+ All phases complete. 50/50 tests passing. Lint clean.
462
+
463
+ | Phase | Status | Details |
464
+ |-------|--------|---------|
465
+ | Phase 1: Configuration | DONE | `config.yaml`, `Config`/`AgentConfig`/`ProviderConfig` with env var expansion |
466
+ | Phase 2: Agent Core | DONE | `Agent` class with `chat()`, `_stream()`, `reset()`, `history` |
467
+ | Phase 3: Python API | DONE | Clean `regcode` package with `Agent` and `Config` exports |
468
+ | Phase 4: CLI | DONE | `regcode chat`, `regcode configure`, `regcode version` commands |
469
+ | Phase 5: Dependencies | DONE | `pyproject.toml` with litellm, pydantic, pyyaml, click, dev deps |
470
+ | Phase 6: Monty Sandbox | DONE | `MontySandbox` class with resource limits, external functions, file I/O, 29 tests |
471
+
472
+
473
+ ### Agent Permissions
474
+ - DONE: Implemented permissions for Agent: AgentPermission.Read, AgentPermission.Write, AgentPermission.Delete, AgentPermission.Execute
475
+ - DONE: Filter tools based on AgentPermissions
476
+ - DONE: Standard Permissions: [AgentPermission.Read, AgentPermission.Execute] (default instead of ALL)
477
+ - DONE: Fixed permission filtering to run after tools are registered (was running on empty registry)
478
+
479
+ ### Agent Response
480
+ - DONE: Added `tool_budget` (default 20) and when tool budget is exhausted, send message to agent telling it to stop using tools and respond with its answer
481
+ - DONE: When budget exhausted, LLM is called without tools (tools=None) so agent cannot make more tool calls
482
+ - DONE: Track `_tool_budget_exhausted` flag and `_tool_budget_remaining` counter per agent run
483
+ - DONE: Add explicit user message "You have exhausted your tool budget (20 tool calls) and must now answer..." when budget hits 0
484
+ - DONE: Added explicit break when budget was exhausted to return the agent's text response
485
+
486
+ ### Code Review DRY Refactoring
487
+ - DONE: Refactored `code_review()` to build enhanced prompt and delegate to `chat(messages=...)`, eliminating 149 lines of duplicated tool-call loop logic
488
+
489
+ ### ShellCommandTool Security Fix
490
+ - DONE: Replaced unrestricted `subprocess.run()` with a command allowlist approach. `ShellCommandTool` now validates every command against a whitelist of safe, read-only commands (`ls`, `cat`, `echo`, `head`, `tail`, `wc`, `grep`, `sort`, `uniq`, `date`, `pwd`, `whoami`, `id`, `uname`, `df`, `free`, `which`, `type`, `stat`, `file`, `sha256sum`, `md5sum`, `base64`, `xxd`, `od`, `hexdump`). Dangerous characters (`;`, `&&`, `|`, backticks, `$()`, etc.) are blocked at the character level. Path traversal (`..`) is rejected. This eliminates the previous security vulnerability where the LLM could execute arbitrary destructive commands with full host access.
491
+
492
+ ### LLM Hallucinated Tool Calls Fix
493
+ - DONE: Model was hallucinating tool calls (e.g. `<tool_call>`) when budget was exhausted and `tools=None`. Root cause: tool definitions were baked into the system prompt string, so even with `tools=None` the model still saw tool names and hallucinated calling them. Fix: when budget is exhausted, replace the system prompt with a version stripped of tool definitions (`_build_system_prompt_no_tools()`). Additionally set `tool_choice="none"` and stop sequences `["<tool_call>", "</tool_use>"]`. Message history (including tool_call_ids) is preserved intact to maintain proper conversation continuity.
494
+
495
+ ## Top Priority To-Dos:
496
+
497
+ - Currently agent could keep on loading files running into context window limitations. We need to add content compaction threshold and add automatic compaction support.
498
+ - I think for this agent to be able to work on big repos, we need to add `add_review_note` tool which basically can write review notes into file which can survive compaction. Review notes are supposed to be preleminary findings. Add `read_review_notes` as well. This must work intune with compaction feature
499
+ - Save tokens reported by litellm and use following logic `context_window=max_tokens + message_chain_token_size` and compact based on compaction threshold.
500
+ - _build_system_prompt in regcode/main.py and _build_code_review_system_prompt in the same file share an identical loop that constructs a "tool descriptions" string from registered tools. The same logic appears twice. This should be extracted into a single helper method, e.g. _format_tool_descriptions(self) -> str, called by both.
501
+
502
+ Additionally, _build_code_review_system_prompt in regcode/main.py calls self.registry.list_tools() and builds tool descriptions, but it could simply accept a system prompt string and tool descriptions as parameters, instead of duplicating the logic again.