regcode 0.4.0__tar.gz → 0.4.1__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 (41) hide show
  1. {regcode-0.4.0 → regcode-0.4.1}/PKG-INFO +1 -1
  2. {regcode-0.4.0 → regcode-0.4.1}/pyproject.toml +1 -1
  3. {regcode-0.4.0 → regcode-0.4.1}/regcode/__init__.py +1 -1
  4. {regcode-0.4.0 → regcode-0.4.1}/regcode/cli.py +14 -0
  5. regcode-0.4.1/regcode/dependencies.py +71 -0
  6. {regcode-0.4.0 → regcode-0.4.1}/regcode/tools/__init__.py +0 -2
  7. {regcode-0.4.0 → regcode-0.4.1}/regcode/tools/builtins.py +2 -1
  8. {regcode-0.4.0 → regcode-0.4.1}/regcode/tools/web_crawl.py +13 -23
  9. regcode-0.4.1/tests/test_dependencies.py +149 -0
  10. {regcode-0.4.0 → regcode-0.4.1}/uv.lock +1 -1
  11. {regcode-0.4.0 → regcode-0.4.1}/.gitignore +0 -0
  12. {regcode-0.4.0 → regcode-0.4.1}/.python-version +0 -0
  13. {regcode-0.4.0 → regcode-0.4.1}/.qwen/settings.json +0 -0
  14. {regcode-0.4.0 → regcode-0.4.1}/AGENTS.md +0 -0
  15. {regcode-0.4.0 → regcode-0.4.1}/GOALS.md +0 -0
  16. {regcode-0.4.0 → regcode-0.4.1}/README.md +0 -0
  17. {regcode-0.4.0 → regcode-0.4.1}/config.yaml.template +0 -0
  18. {regcode-0.4.0 → regcode-0.4.1}/main.py +0 -0
  19. {regcode-0.4.0 → regcode-0.4.1}/regcode/config.py +0 -0
  20. {regcode-0.4.0 → regcode-0.4.1}/regcode/conversation_manager.py +0 -0
  21. {regcode-0.4.0 → regcode-0.4.1}/regcode/main.py +0 -0
  22. {regcode-0.4.0 → regcode-0.4.1}/regcode/monty_sandbox.py +0 -0
  23. {regcode-0.4.0 → regcode-0.4.1}/regcode/permissions.py +0 -0
  24. {regcode-0.4.0 → regcode-0.4.1}/regcode/sandbox.py +0 -0
  25. {regcode-0.4.0 → regcode-0.4.1}/regcode/tools/base.py +0 -0
  26. {regcode-0.4.0 → regcode-0.4.1}/regcode/tools/registry.py +0 -0
  27. {regcode-0.4.0 → regcode-0.4.1}/regcode/tools/review_notes.py +0 -0
  28. {regcode-0.4.0 → regcode-0.4.1}/regcode/tracing.py +0 -0
  29. {regcode-0.4.0 → regcode-0.4.1}/regcode/tui.py +0 -0
  30. {regcode-0.4.0 → regcode-0.4.1}/repair_tui.md +0 -0
  31. {regcode-0.4.0 → regcode-0.4.1}/tests/__init__.py +0 -0
  32. {regcode-0.4.0 → regcode-0.4.1}/tests/conftest.py +0 -0
  33. {regcode-0.4.0 → regcode-0.4.1}/tests/test_api.py +0 -0
  34. {regcode-0.4.0 → regcode-0.4.1}/tests/test_cli.py +0 -0
  35. {regcode-0.4.0 → regcode-0.4.1}/tests/test_config.py +0 -0
  36. {regcode-0.4.0 → regcode-0.4.1}/tests/test_context_manager.py +0 -0
  37. {regcode-0.4.0 → regcode-0.4.1}/tests/test_main.py +0 -0
  38. {regcode-0.4.0 → regcode-0.4.1}/tests/test_monty_sandbox.py +0 -0
  39. {regcode-0.4.0 → regcode-0.4.1}/tests/test_osv_cve.py +0 -0
  40. {regcode-0.4.0 → regcode-0.4.1}/tests/test_provider_extra.py +0 -0
  41. {regcode-0.4.0 → regcode-0.4.1}/tests/test_review_notes.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: regcode
3
- Version: 0.4.0
3
+ Version: 0.4.1
4
4
  Summary: Minimalistic coding agent with Python API and CLI
5
5
  Requires-Python: >=3.12
6
6
  Requires-Dist: click>=8.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "regcode"
3
- version = "0.4.0"
3
+ version = "0.4.1"
4
4
  description = "Minimalistic coding agent with Python API and CLI"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -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.4.0"
5
+ __version__ = "0.4.1"
@@ -1,3 +1,4 @@
1
+ import logging
1
2
  import sys
2
3
  from pathlib import Path
3
4
 
@@ -5,9 +6,14 @@ import click
5
6
  import yaml
6
7
 
7
8
  import regcode
9
+ from regcode.dependencies import ensure_playwright_browsers
8
10
  from regcode.permissions import AgentPermission
9
11
  from regcode.tui import ChatUI
10
12
 
13
+ # Configure logging
14
+ logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
15
+ _logger = logging.getLogger(__name__)
16
+
11
17
 
12
18
  def _confirm(prompt_text):
13
19
  """Ask yes/no question, accepting y/n (case insensitive)."""
@@ -43,6 +49,14 @@ def cli(ctx):
43
49
  )
44
50
  def chat(model, config_path, stream, full, no_color):
45
51
  """Chat with the agent using a Textual TUI with sticky bottom bar."""
52
+ # Ensure Playwright browsers are available for web_crawl tool
53
+ _logger.info("Checking startup dependencies...")
54
+ if not ensure_playwright_browsers():
55
+ _logger.warning(
56
+ "Playwright browsers are not available. "
57
+ "The web_crawl tool will not function."
58
+ )
59
+
46
60
  # Pass None if the file doesn't exist so Config.load falls back
47
61
  # to ~/.regcode/config.yaml (matching the TUI's expected behaviour).
48
62
  _effective_path = config_path if Path(config_path).exists() else None
@@ -0,0 +1,71 @@
1
+ """Startup dependency checks and auto-installation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import os
7
+ import subprocess
8
+ import sys
9
+
10
+ _logger = logging.getLogger(__name__)
11
+
12
+
13
+ def ensure_playwright_browsers() -> bool:
14
+ """Check if Playwright browsers are installed; install if missing.
15
+
16
+ Returns True if browsers are available (either already installed
17
+ or newly installed), False if installation was skipped due to
18
+ user setting PLAYWRIGHT_NO_INSTALL=1.
19
+ """
20
+ if os.environ.get("PLAYWRIGHT_NO_INSTALL"):
21
+ _logger.warning(
22
+ "PLAYWRIGHT_NO_INSTALL is set; skipping browser check."
23
+ )
24
+ return False
25
+
26
+ # Dynamic import so tests can mock before calling
27
+ try:
28
+ from playwright.sync_api import sync_playwright # noqa: F811
29
+ except ImportError:
30
+ _logger.warning(
31
+ "playwright is not installed. Install with: "
32
+ "uv add playwright playwright-stealth && "
33
+ "uv run playwright install"
34
+ )
35
+ return False
36
+
37
+ try:
38
+ with sync_playwright() as p:
39
+ # Trigger a simple launch test — if browsers are missing,
40
+ # Chromium.launch() raises BrowserType.launch: Executable
41
+ # doesn't exist. We catch that specific error.
42
+ p.chromium.executable_path # noqa: B018
43
+ # Try to actually launch; this is the definitive check
44
+ browser = p.chromium.launch()
45
+ browser.close()
46
+ return True
47
+ except Exception:
48
+ pass
49
+
50
+ # Browsers are missing — attempt automatic installation
51
+ _logger.info("Playwright browsers not found. Installing...")
52
+ try:
53
+ subprocess.run(
54
+ [sys.executable, "-m", "playwright", "install", "--with-deps"],
55
+ check=True,
56
+ capture_output=True,
57
+ )
58
+ _logger.info("Playwright browsers installed successfully.")
59
+ return True
60
+ except subprocess.CalledProcessError as e:
61
+ stderr_text = e.stderr.decode() if e.stderr else ""
62
+ _logger.error(
63
+ "Failed to install Playwright browsers: %s", stderr_text
64
+ )
65
+ return False
66
+ except FileNotFoundError:
67
+ _logger.error(
68
+ "Could not find 'playwright' module. "
69
+ "Install it with: uv add playwright && uv run playwright install"
70
+ )
71
+ return False
@@ -4,8 +4,6 @@ from regcode.tools.base import BaseTool, ToolParam, ToolResult
4
4
  from regcode.tools.builtins import create_default_tools
5
5
  from regcode.tools.registry import ToolRegistry
6
6
 
7
- __version__ = "0.3.0"
8
-
9
7
  __all__ = [
10
8
  "BaseTool",
11
9
  "ToolParam",
@@ -17,7 +17,7 @@ from typing import Any
17
17
 
18
18
  from regcode.monty_sandbox import MontySandbox
19
19
  from regcode.tools.base import BaseTool, ToolParam, ToolResult
20
-
20
+ from regcode.tools.web_crawl import WebCrawlerTool
21
21
 
22
22
  class PythonCodeTool(BaseTool):
23
23
  """Execute Python code in a Monty sandbox."""
@@ -1367,4 +1367,5 @@ def create_default_tools() -> list[BaseTool]:
1367
1367
  SystemInfoTool(),
1368
1368
  FetchGitDiffTool(),
1369
1369
  OSVCVETool(),
1370
+ WebCrawlerTool(),
1370
1371
  ]
@@ -7,12 +7,13 @@ from typing import Any
7
7
  from regcode.tools.base import BaseTool, ToolParam, ToolResult
8
8
 
9
9
  try:
10
- from crawl4ai import AsyncWebCrawler
10
+ from crawl4ai import AsyncWebCrawler, CrawlerRunConfig
11
11
  from crawl4ai.markdown_generation_strategy import DefaultMarkdownGenerator
12
12
 
13
13
  CRAWL4AI_AVAILABLE = True
14
14
  except ImportError:
15
15
  AsyncWebCrawler = None # type: ignore
16
+ CrawlerRunConfig = None # type: ignore
16
17
  DefaultMarkdownGenerator = None # type: ignore
17
18
  CRAWL4AI_AVAILABLE = False
18
19
 
@@ -95,24 +96,13 @@ class WebCrawlerTool(BaseTool):
95
96
  exit_code=1,
96
97
  )
97
98
 
98
- try:
99
- import asyncio
99
+ import asyncio
100
100
 
101
- result = asyncio.get_event_loop().run_until_complete(
101
+ try:
102
+ result = asyncio.run(
102
103
  self._crawl_async(url, markdown, word_count_threshold, only_text)
103
104
  )
104
105
  return result
105
- except ImportError:
106
- # Fallback: try to run in a new event loop
107
- import asyncio
108
-
109
- async def _crawl():
110
- return await self._crawl_async(
111
- url, markdown, word_count_threshold, only_text
112
- )
113
-
114
- result = asyncio.run(_crawl())
115
- return result
116
106
  except Exception as e:
117
107
  return ToolResult(
118
108
  output=f"Error crawling URL: {str(e)}",
@@ -131,18 +121,17 @@ class WebCrawlerTool(BaseTool):
131
121
  try:
132
122
  async with AsyncWebCrawler(verbose=False) as crawler:
133
123
  if markdown:
134
- result = await crawler.arun(
135
- url=url,
136
- markdown_generator=DefaultMarkdownGenerator(
137
- word_count_threshold=word_count_threshold
138
- ),
124
+ config = CrawlerRunConfig( # type: ignore[union-attr]
125
+ markdown_generator=DefaultMarkdownGenerator(), # type: ignore[union-attr]
126
+ word_count_threshold=word_count_threshold,
139
127
  only_text=only_text,
140
128
  )
129
+ result = await crawler.arun(url=url, config=config)
141
130
  else:
142
- result = await crawler.arun(
143
- url=url,
131
+ config = CrawlerRunConfig( # type: ignore[union-attr]
144
132
  only_text=only_text,
145
133
  )
134
+ result = await crawler.arun(url=url, config=config)
146
135
 
147
136
  if not result.success:
148
137
  return ToolResult(
@@ -155,7 +144,8 @@ class WebCrawlerTool(BaseTool):
155
144
  if markdown and result.markdown:
156
145
  output = f"URL: {url}\n\n{result.markdown}"
157
146
  else:
158
- output = f"URL: {url}\n\n{result.html or result.markdown or 'No content extracted'}"
147
+ html = result.html or result.markdown or "No content extracted"
148
+ output = f"URL: {url}\n\n{html}"
159
149
 
160
150
  return ToolResult(
161
151
  output=output,
@@ -0,0 +1,149 @@
1
+ """Tests for regcode.dependencies startup checks."""
2
+
3
+ import subprocess
4
+ import sys
5
+ from unittest.mock import MagicMock, patch
6
+
7
+
8
+ def _clear_regcode_deps():
9
+ """Remove cached regcode modules so re-import picks up mocks."""
10
+ for mod in list(sys.modules):
11
+ if mod == "regcode" or mod.startswith("regcode."):
12
+ del sys.modules[mod]
13
+
14
+
15
+ class TestEnsurePlaywrightBrowsers:
16
+ """Tests for Playwright browser availability checking."""
17
+
18
+ def test_playwright_not_installed(self):
19
+ """When playwright is not installed, return False with warning."""
20
+ _clear_regcode_deps()
21
+
22
+ with patch.dict("sys.modules", {
23
+ "playwright": None,
24
+ "playwright.sync_api": None,
25
+ }):
26
+ from regcode import dependencies
27
+ result = dependencies.ensure_playwright_browsers()
28
+ assert result is False
29
+
30
+ def test_playwright_available_no_launch(self):
31
+ """When launch fails, auto-install is triggered."""
32
+ _clear_regcode_deps()
33
+
34
+ mock_browser = MagicMock()
35
+ mock_browser.close = MagicMock()
36
+ mock_p = MagicMock()
37
+ mock_p.chromium = MagicMock()
38
+ mock_p.chromium.executable_path = "/fake/path"
39
+ mock_p.chromium.launch = MagicMock(side_effect=Exception(
40
+ "BrowserType.launch: Executable doesn't exist"
41
+ ))
42
+ mock_instance = MagicMock()
43
+ mock_instance.__enter__ = MagicMock(return_value=mock_p)
44
+ mock_instance.__exit__ = MagicMock(return_value=None)
45
+
46
+ # Mock at the playwright module level so the import in
47
+ # dependencies.py picks up our mock
48
+ with patch.dict("sys.modules", {
49
+ "playwright": MagicMock(),
50
+ "playwright.sync_api": MagicMock(
51
+ sync_playwright=MagicMock(return_value=mock_instance)
52
+ ),
53
+ }):
54
+ from regcode.dependencies import ensure_playwright_browsers
55
+ # Auto-install succeeds
56
+ with patch.object(
57
+ subprocess, "run", return_value=MagicMock(returncode=0)
58
+ ):
59
+ result = ensure_playwright_browsers()
60
+ assert result is True
61
+
62
+ def test_playwright_available_with_launch(self):
63
+ """When browser launches successfully, return True."""
64
+ _clear_regcode_deps()
65
+
66
+ mock_browser = MagicMock()
67
+ mock_browser.close = MagicMock()
68
+ mock_p = MagicMock()
69
+ mock_p.chromium = MagicMock()
70
+ mock_p.chromium.executable_path = "/fake/path"
71
+ mock_p.chromium.launch = MagicMock(return_value=mock_browser)
72
+
73
+ mock_instance = MagicMock()
74
+ mock_instance.__enter__ = MagicMock(return_value=mock_p)
75
+ mock_instance.__exit__ = MagicMock(return_value=None)
76
+
77
+ with patch.dict("sys.modules", {
78
+ "playwright": MagicMock(),
79
+ "playwright.sync_api": MagicMock(
80
+ sync_playwright=MagicMock(return_value=mock_instance)
81
+ ),
82
+ }):
83
+ from regcode.dependencies import ensure_playwright_browsers
84
+ result = ensure_playwright_browsers()
85
+ assert result is True
86
+ mock_browser.close.assert_called_once()
87
+
88
+ def test_playwright_no_install_env_var(self):
89
+ """When PLAYWRIGHT_NO_INSTALL=1, skip check and return False."""
90
+ _clear_regcode_deps()
91
+
92
+ with patch.dict("os.environ", {"PLAYWRIGHT_NO_INSTALL": "1"}):
93
+ from regcode.dependencies import ensure_playwright_browsers
94
+ result = ensure_playwright_browsers()
95
+ assert result is False
96
+
97
+ def test_install_failure_returns_false(self):
98
+ """When auto-install fails, return False with error."""
99
+ _clear_regcode_deps()
100
+
101
+ mock_p = MagicMock()
102
+ mock_p.chromium = MagicMock()
103
+ mock_p.chromium.executable_path = "/fake/path"
104
+ mock_p.chromium.launch = MagicMock(side_effect=Exception("missing"))
105
+
106
+ mock_instance = MagicMock()
107
+ mock_instance.__enter__ = MagicMock(return_value=mock_p)
108
+ mock_instance.__exit__ = MagicMock(return_value=None)
109
+
110
+ with patch.dict("sys.modules", {
111
+ "playwright": MagicMock(),
112
+ "playwright.sync_api": MagicMock(
113
+ sync_playwright=MagicMock(return_value=mock_instance)
114
+ ),
115
+ }):
116
+ with patch.object(
117
+ subprocess,
118
+ "run",
119
+ side_effect=subprocess.CalledProcessError(1, "cmd", stderr=b"fail"),
120
+ ):
121
+ from regcode.dependencies import ensure_playwright_browsers
122
+ result = ensure_playwright_browsers()
123
+ assert result is False
124
+
125
+ def test_install_file_not_found(self):
126
+ """When playwright module is not found, return False."""
127
+ _clear_regcode_deps()
128
+
129
+ mock_p = MagicMock()
130
+ mock_p.chromium = MagicMock()
131
+ mock_p.chromium.executable_path = "/fake/path"
132
+ mock_p.chromium.launch = MagicMock(side_effect=Exception("missing"))
133
+
134
+ mock_instance = MagicMock()
135
+ mock_instance.__enter__ = MagicMock(return_value=mock_p)
136
+ mock_instance.__exit__ = MagicMock(return_value=None)
137
+
138
+ with patch.dict("sys.modules", {
139
+ "playwright": MagicMock(),
140
+ "playwright.sync_api": MagicMock(
141
+ sync_playwright=MagicMock(return_value=mock_instance)
142
+ ),
143
+ }):
144
+ with patch.object(
145
+ subprocess, "run", side_effect=FileNotFoundError("no playwright")
146
+ ):
147
+ from regcode.dependencies import ensure_playwright_browsers
148
+ result = ensure_playwright_browsers()
149
+ assert result is False
@@ -2835,7 +2835,7 @@ wheels = [
2835
2835
 
2836
2836
  [[package]]
2837
2837
  name = "regcode"
2838
- version = "0.4.0"
2838
+ version = "0.4.1"
2839
2839
  source = { editable = "." }
2840
2840
  dependencies = [
2841
2841
  { name = "click" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes