regcode 0.6.3__tar.gz → 0.8.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.
Files changed (46) hide show
  1. {regcode-0.6.3 → regcode-0.8.0}/AGENTS.md +1 -9
  2. {regcode-0.6.3 → regcode-0.8.0}/PKG-INFO +3 -1
  3. {regcode-0.6.3 → regcode-0.8.0}/pyproject.toml +3 -1
  4. {regcode-0.6.3 → regcode-0.8.0}/regcode/__init__.py +1 -1
  5. regcode-0.8.0/regcode/clipboard.py +378 -0
  6. {regcode-0.6.3 → regcode-0.8.0}/regcode/config.py +1 -0
  7. {regcode-0.6.3 → regcode-0.8.0}/regcode/main.py +376 -27
  8. regcode-0.8.0/regcode/paste.py +188 -0
  9. {regcode-0.6.3 → regcode-0.8.0}/regcode/tools/builtins.py +259 -3
  10. {regcode-0.6.3 → regcode-0.8.0}/regcode/tui.py +353 -27
  11. regcode-0.8.0/tests/test_clipboard.py +502 -0
  12. {regcode-0.6.3 → regcode-0.8.0}/tests/test_main.py +12 -9
  13. {regcode-0.6.3 → regcode-0.8.0}/uv.lock +23 -1
  14. {regcode-0.6.3 → regcode-0.8.0}/.gitignore +0 -0
  15. {regcode-0.6.3 → regcode-0.8.0}/.gitlab-ci.yml +0 -0
  16. {regcode-0.6.3 → regcode-0.8.0}/.python-version +0 -0
  17. {regcode-0.6.3 → regcode-0.8.0}/.qwen/settings.json +0 -0
  18. {regcode-0.6.3 → regcode-0.8.0}/GOALS.md +0 -0
  19. {regcode-0.6.3 → regcode-0.8.0}/README.md +0 -0
  20. {regcode-0.6.3 → regcode-0.8.0}/config.yaml.template +0 -0
  21. {regcode-0.6.3 → regcode-0.8.0}/main.py +0 -0
  22. {regcode-0.6.3 → regcode-0.8.0}/regcode/cli.py +0 -0
  23. {regcode-0.6.3 → regcode-0.8.0}/regcode/conversation_manager.py +0 -0
  24. {regcode-0.6.3 → regcode-0.8.0}/regcode/dependencies.py +0 -0
  25. {regcode-0.6.3 → regcode-0.8.0}/regcode/monty_sandbox.py +0 -0
  26. {regcode-0.6.3 → regcode-0.8.0}/regcode/permissions.py +0 -0
  27. {regcode-0.6.3 → regcode-0.8.0}/regcode/sandbox.py +0 -0
  28. {regcode-0.6.3 → regcode-0.8.0}/regcode/tools/__init__.py +0 -0
  29. {regcode-0.6.3 → regcode-0.8.0}/regcode/tools/base.py +0 -0
  30. {regcode-0.6.3 → regcode-0.8.0}/regcode/tools/registry.py +0 -0
  31. {regcode-0.6.3 → regcode-0.8.0}/regcode/tools/review_notes.py +0 -0
  32. {regcode-0.6.3 → regcode-0.8.0}/regcode/tools/web_crawl.py +0 -0
  33. {regcode-0.6.3 → regcode-0.8.0}/regcode/tools/web_search.py +0 -0
  34. {regcode-0.6.3 → regcode-0.8.0}/regcode/tracing.py +0 -0
  35. {regcode-0.6.3 → regcode-0.8.0}/tests/__init__.py +0 -0
  36. {regcode-0.6.3 → regcode-0.8.0}/tests/conftest.py +0 -0
  37. {regcode-0.6.3 → regcode-0.8.0}/tests/test_api.py +0 -0
  38. {regcode-0.6.3 → regcode-0.8.0}/tests/test_cli.py +0 -0
  39. {regcode-0.6.3 → regcode-0.8.0}/tests/test_config.py +0 -0
  40. {regcode-0.6.3 → regcode-0.8.0}/tests/test_context_manager.py +0 -0
  41. {regcode-0.6.3 → regcode-0.8.0}/tests/test_dependencies.py +0 -0
  42. {regcode-0.6.3 → regcode-0.8.0}/tests/test_monty_sandbox.py +0 -0
  43. {regcode-0.6.3 → regcode-0.8.0}/tests/test_osv_cve.py +0 -0
  44. {regcode-0.6.3 → regcode-0.8.0}/tests/test_provider_extra.py +0 -0
  45. {regcode-0.6.3 → regcode-0.8.0}/tests/test_review_notes.py +0 -0
  46. {regcode-0.6.3 → regcode-0.8.0}/tests/test_web_search.py +0 -0
@@ -3,25 +3,17 @@ Always make sure to comply to these rules. Under Project Stack, you will find te
3
3
 
4
4
  ## Project Stack:
5
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
6
  - Pytest: Test suite under `tests`
11
- - NextJS/React: Frontend Code under `frontend/`
12
7
  - GitLab: Code Repository/ CI CD
13
8
  - .gitlab-ci.yml: Gitlab Runner Config
14
9
  - `ruff`: Linting and Code Quality Control
15
- - `docker`: Deployment is done via `docker-compose.yml`
16
10
  - `pyproject.toml`: Project Setup Config
17
11
 
18
12
  ## 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!
13
+
21
14
  - ALWAYS run `uv run ruff check --fix` after all changes at the end and fix linting issues
22
15
  - ALWAYS run `uv run pytest` to run test suite after all changes and fix issues shown by tests.
23
16
  - ALWAYS add test cases if relevant.
24
- - NEVER use emojis! Use icon packs (for example `lucid-react`) if needed.
25
17
  - 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
18
 
27
19
  ## Project Vision
@@ -1,14 +1,16 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: regcode
3
- Version: 0.6.3
3
+ Version: 0.8.0
4
4
  Summary: Minimalistic coding agent with Python API and CLI
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: click>=8.0
7
7
  Requires-Dist: crawl4ai>=0.4.0
8
8
  Requires-Dist: litellm>=1.83.17
9
9
  Requires-Dist: mlflow>=3.14.0
10
+ Requires-Dist: pathspec>=1.1.1
10
11
  Requires-Dist: pydantic-monty>=0.0.18
11
12
  Requires-Dist: pydantic-settings>=2.14.2
13
+ Requires-Dist: pyperclip>=1.9.0
12
14
  Requires-Dist: pytest>=8.0
13
15
  Requires-Dist: pyyaml>=6.0
14
16
  Requires-Dist: ruff>=0.8
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "regcode"
3
- version = "0.6.3"
3
+ version = "0.8.0"
4
4
  description = "Minimalistic coding agent with Python API and CLI"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -15,6 +15,8 @@ dependencies = [
15
15
  "pydantic-monty>=0.0.18",
16
16
  "mlflow>=3.14.0",
17
17
  "textual>=2.0.0",
18
+ "pathspec>=1.1.1",
19
+ "pyperclip>=1.9.0",
18
20
  ]
19
21
 
20
22
  [project.scripts]
@@ -2,4 +2,4 @@ from regcode.config import Config
2
2
  from regcode.main import Agent, AgentStatus, ToolCall
3
3
 
4
4
  __all__ = ["Agent", "Config", "AgentStatus", "ToolCall"]
5
- __version__ = "0.6.3"
5
+ __version__ = "0.8.0"
@@ -0,0 +1,378 @@
1
+ """Clipboard utilities for image and text detection, with base64 encoding.
2
+
3
+ Supports Wayland (wl-paste) and X11 (xclip/xsel) and WSL clipboard backends.
4
+ Detects image vs text content and provides base64 encoding for images.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import base64
10
+ import logging
11
+ import mimetypes
12
+ import subprocess
13
+ from dataclasses import dataclass
14
+ from typing import Optional
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ @dataclass
20
+ class ClipboardContent:
21
+ """Represents clipboard content with type information."""
22
+
23
+ content: str # Base64 for images, raw text for text
24
+ content_type: str # "image" or "text"
25
+ mime_type: str # e.g., "image/png", "text/plain"
26
+ is_image: bool
27
+
28
+
29
+ # Image extensions to MIME types mapping
30
+ IMAGE_EXTENSIONS: dict[str, str] = {
31
+ ".png": "image/png",
32
+ ".jpg": "image/jpeg",
33
+ ".jpeg": "image/jpeg",
34
+ ".gif": "image/gif",
35
+ ".webp": "image/webp",
36
+ ".bmp": "image/bmp",
37
+ ".tiff": "image/tiff",
38
+ ".svg": "image/svg+xml",
39
+ ".ico": "image/x-icon",
40
+ ".avif": "image/avif",
41
+ }
42
+
43
+
44
+ def detect_image_mime_type(ext: str) -> str:
45
+ """Detect MIME type from file extension."""
46
+ ext = ext.lower()
47
+ return IMAGE_EXTENSIONS.get(ext, "application/octet-stream")
48
+
49
+
50
+ def get_clipboard_backend() -> str:
51
+ """Detect which clipboard backend is available.
52
+
53
+ Returns one of: 'wl-clipboard', 'xclip', 'xsel', 'wsl', or empty string.
54
+ """
55
+ # Check Wayland first
56
+ try:
57
+ result = subprocess.run(
58
+ ["which", "wl-paste"],
59
+ capture_output=True,
60
+ text=True,
61
+ )
62
+ if result.returncode == 0:
63
+ return "wl-clipboard"
64
+ except (FileNotFoundError, OSError):
65
+ pass
66
+
67
+ # Check X11
68
+ try:
69
+ result = subprocess.run(
70
+ ["which", "xclip"],
71
+ capture_output=True,
72
+ text=True,
73
+ )
74
+ if result.returncode == 0:
75
+ return "xclip"
76
+ except (FileNotFoundError, OSError):
77
+ pass
78
+
79
+ try:
80
+ result = subprocess.run(
81
+ ["which", "xsel"],
82
+ capture_output=True,
83
+ text=True,
84
+ )
85
+ if result.returncode == 0:
86
+ return "xsel"
87
+ except (FileNotFoundError, OSError):
88
+ pass
89
+
90
+ # Check WSL clipboard integration (clip.exe + powershell.exe)
91
+ try:
92
+ result = subprocess.run(
93
+ ["which", "clip.exe"],
94
+ capture_output=True,
95
+ text=True,
96
+ )
97
+ if result.returncode == 0:
98
+ result2 = subprocess.run(
99
+ ["which", "powershell.exe"],
100
+ capture_output=True,
101
+ text=True,
102
+ )
103
+ if result2.returncode == 0:
104
+ return "wsl"
105
+ except (FileNotFoundError, OSError):
106
+ pass
107
+
108
+ return ""
109
+
110
+
111
+ def read_clipboard_text(backend: str) -> str:
112
+ """Read plain text from clipboard.
113
+
114
+ Args:
115
+ backend: Clipboard backend to use ('wl-clipboard', 'xclip', 'xsel', 'wsl', '')
116
+
117
+ Returns:
118
+ Text content from clipboard, or empty string on failure.
119
+ """
120
+ try:
121
+ if backend == "wl-clipboard":
122
+ result = subprocess.run(
123
+ ["wl-paste", "-t", "text/plain"],
124
+ capture_output=True,
125
+ text=True,
126
+ timeout=5,
127
+ )
128
+ elif backend == "xclip":
129
+ result = subprocess.run(
130
+ ["xclip", "-selection", "clipboard", "-o"],
131
+ capture_output=True,
132
+ text=True,
133
+ timeout=5,
134
+ )
135
+ elif backend == "xsel":
136
+ result = subprocess.run(
137
+ ["xsel", "--clipboard", "--output"],
138
+ capture_output=True,
139
+ text=True,
140
+ timeout=5,
141
+ )
142
+ elif backend == "wsl":
143
+ # WSL clipboard: use PowerShell to read clipboard
144
+ ps_script = (
145
+ "[Convert]::ToBase64String("
146
+ "[Text.Encoding]::UTF8.GetBytes((Get-Clipboard -Raw)))"
147
+ )
148
+ result = subprocess.run(
149
+ [
150
+ "powershell.exe",
151
+ "-noprofile",
152
+ "-command",
153
+ ps_script,
154
+ ],
155
+ capture_output=True,
156
+ text=True,
157
+ timeout=5,
158
+ )
159
+ if result.returncode == 0 and result.stdout.strip():
160
+ try:
161
+ base64_data = result.stdout.strip()
162
+ decoded_bytes = base64.b64decode(base64_data)
163
+ text = decoded_bytes.decode("utf-8")
164
+ # Normalize Windows line endings to Unix for terminal display
165
+ text = text.replace("\r\n", "\n").replace("\r", "\n")
166
+ return text.strip()
167
+ except Exception:
168
+ return ""
169
+ return ""
170
+ else:
171
+ return ""
172
+
173
+ if result.returncode == 0:
174
+ text = result.stdout
175
+ # Normalize Windows line endings to Unix for terminal display
176
+ text = text.replace("\r\n", "\n").replace("\r", "\n")
177
+ return text.strip()
178
+ else:
179
+ logger.debug("Failed to read text from clipboard: %s", result.stderr)
180
+ return ""
181
+ except (subprocess.TimeoutExpired, OSError, subprocess.SubprocessError) as e:
182
+ logger.debug("Error reading clipboard text: %s", e)
183
+ return ""
184
+
185
+
186
+ def read_clipboard_image(backend: str) -> Optional[bytes]:
187
+ """Read raw image bytes from clipboard.
188
+
189
+ Args:
190
+ backend: Clipboard backend to use
191
+
192
+ Returns:
193
+ Raw image bytes, or None if clipboard doesn't contain an image.
194
+ """
195
+ try:
196
+ if backend == "wl-clipboard":
197
+ result = subprocess.run(
198
+ ["wl-paste", "-t", "image/png"],
199
+ capture_output=True,
200
+ timeout=5,
201
+ )
202
+ if result.returncode == 0 and len(result.stdout) > 0:
203
+ return result.stdout
204
+ elif backend == "xclip":
205
+ # xclip doesn't support direct image output, need to check with xwd
206
+ # This is a fallback - primary Wayland support is through wl-clipboard
207
+ result = subprocess.run(
208
+ ["wl-paste", "-t", "image/png"],
209
+ capture_output=True,
210
+ timeout=5,
211
+ )
212
+ if result.returncode == 0 and len(result.stdout) > 0:
213
+ return result.stdout
214
+ elif backend == "wsl":
215
+ # WSL: load Windows Forms assembly and read clipboard image
216
+ ps_script = (
217
+ "Add-Type -AssemblyName System.Windows.Forms; "
218
+ "$ms = New-Object IO.MemoryStream; "
219
+ "[System.Windows.Forms.Clipboard]::GetImage().Save($ms, "
220
+ "[System.Drawing.Imaging.ImageFormat]::Png); "
221
+ "[Convert]::ToBase64String($ms.ToArray())"
222
+ )
223
+ result = subprocess.run(
224
+ [
225
+ "powershell.exe",
226
+ "-NoProfile",
227
+ "-NonInteractive",
228
+ "-Command",
229
+ ps_script,
230
+ ],
231
+ capture_output=True,
232
+ text=True,
233
+ timeout=10,
234
+ )
235
+ if result.returncode == 0 and result.stdout.strip():
236
+ try:
237
+ # PowerShell wraps base64 with newlines, remove all whitespace
238
+ clean = result.stdout.replace("\n", "").replace("\r", "").strip()
239
+ return base64.b64decode(clean)
240
+ except Exception:
241
+ logger.debug("Failed to decode WSL clipboard image base64")
242
+ return None
243
+ except (subprocess.TimeoutExpired, OSError, subprocess.SubprocessError) as e:
244
+ logger.debug("Error reading clipboard image: %s", e)
245
+
246
+ return None
247
+
248
+
249
+ def clipboard_is_image(backend: str) -> bool:
250
+ """Check if clipboard contains an image.
251
+
252
+ Args:
253
+ backend: Clipboard backend to use
254
+
255
+ Returns:
256
+ True if clipboard contains an image, False otherwise.
257
+ """
258
+ try:
259
+ if backend == "wl-clipboard":
260
+ result = subprocess.run(
261
+ ["wl-paste", "-t", "image/png"],
262
+ capture_output=True,
263
+ timeout=5,
264
+ )
265
+ if result.returncode == 0 and len(result.stdout) > 0:
266
+ # Verify it's actually a PNG
267
+ if result.stdout[:8] == b"\x89PNG\r\n\x1a\n":
268
+ return True
269
+ elif backend in ("xclip", "xsel"):
270
+ # For X11, check if we can extract image
271
+ return read_clipboard_image(backend) is not None
272
+ elif backend == "wsl":
273
+ # For WSL, try to get image via PowerShell
274
+ return read_clipboard_image(backend) is not None
275
+ except (subprocess.TimeoutExpired, OSError):
276
+ pass
277
+
278
+ return False
279
+
280
+
281
+ def get_image_mime_type(data: bytes) -> str:
282
+ """Detect MIME type from image data bytes.
283
+
284
+ Args:
285
+ data: Raw image bytes
286
+
287
+ Returns:
288
+ MIME type string (e.g., "image/png")
289
+ """
290
+ # Check PNG signature
291
+ if data[:8] == b"\x89PNG\r\n\x1a\n":
292
+ return "image/png"
293
+
294
+ # Check JPEG signature
295
+ if data[:2] == b"\xff\xd8":
296
+ return "image/jpeg"
297
+
298
+ # Check GIF signature
299
+ if data[:4] in (b"GIF87a", b"GIF89a"):
300
+ return "image/gif"
301
+
302
+ # Check WebP signature
303
+ if data[:4] == b"RIFF" and data[8:12] == b"WEBP":
304
+ return "image/webp"
305
+
306
+ # Fallback: try to detect from magic bytes
307
+ mime_type = mimetypes.guess_type("image.tmp")
308
+ if mime_type and mime_type[0]:
309
+ return mime_type[0]
310
+
311
+ return "application/octet-stream"
312
+
313
+
314
+ def encode_image_to_base64(data: bytes, mime_type: str = "") -> str:
315
+ """Encode image bytes to base64 data URL.
316
+
317
+ Args:
318
+ data: Raw image bytes
319
+ mime_type: MIME type of the image
320
+
321
+ Returns:
322
+ Base64 data URL string (e.g., "data:image/png;base64,...")
323
+ """
324
+ if not mime_type:
325
+ mime_type = get_image_mime_type(data)
326
+
327
+ base64_data = base64.b64encode(data).decode("utf-8")
328
+ return f"data:{mime_type};base64,{base64_data}"
329
+
330
+
331
+ def get_clipboard_content() -> ClipboardContent:
332
+ """Get clipboard content, auto-detecting image vs text.
333
+
334
+ Returns:
335
+ ClipboardContent with detected type and base64-encoded data for images.
336
+ """
337
+ backend = get_clipboard_backend()
338
+ if not backend:
339
+ return ClipboardContent(
340
+ content="",
341
+ content_type="text",
342
+ mime_type="text/plain",
343
+ is_image=False,
344
+ )
345
+
346
+ # Check if clipboard contains an image
347
+ if clipboard_is_image(backend):
348
+ image_data = read_clipboard_image(backend)
349
+ if image_data is not None:
350
+ mime_type = get_image_mime_type(image_data)
351
+ base64_data = encode_image_to_base64(image_data, mime_type)
352
+ return ClipboardContent(
353
+ content=base64_data,
354
+ content_type="image",
355
+ mime_type=mime_type,
356
+ is_image=True,
357
+ )
358
+
359
+ # Fall back to text
360
+ text_content = read_clipboard_text(backend)
361
+ return ClipboardContent(
362
+ content=text_content,
363
+ content_type="text",
364
+ mime_type="text/plain",
365
+ is_image=False,
366
+ )
367
+
368
+
369
+ def read_multiline_text(backend: str) -> str:
370
+ """Read potentially multiline text from clipboard.
371
+
372
+ Args:
373
+ backend: Clipboard backend to use
374
+
375
+ Returns:
376
+ Multiline text content from clipboard.
377
+ """
378
+ return read_clipboard_text(backend)
@@ -125,6 +125,7 @@ class Config(BaseModel):
125
125
  tool_budget: int = 20
126
126
  permissions: list[AgentPermission] = [AgentPermission.READ, AgentPermission.EXECUTE]
127
127
  toolset_config: ToolSetConfig = ToolSetConfig()
128
+ debug_mode: bool = False
128
129
 
129
130
  @classmethod
130
131
  def load(cls, path: str | None = None) -> "Config":