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.
- {regcode-0.4.0 → regcode-0.4.1}/PKG-INFO +1 -1
- {regcode-0.4.0 → regcode-0.4.1}/pyproject.toml +1 -1
- {regcode-0.4.0 → regcode-0.4.1}/regcode/__init__.py +1 -1
- {regcode-0.4.0 → regcode-0.4.1}/regcode/cli.py +14 -0
- regcode-0.4.1/regcode/dependencies.py +71 -0
- {regcode-0.4.0 → regcode-0.4.1}/regcode/tools/__init__.py +0 -2
- {regcode-0.4.0 → regcode-0.4.1}/regcode/tools/builtins.py +2 -1
- {regcode-0.4.0 → regcode-0.4.1}/regcode/tools/web_crawl.py +13 -23
- regcode-0.4.1/tests/test_dependencies.py +149 -0
- {regcode-0.4.0 → regcode-0.4.1}/uv.lock +1 -1
- {regcode-0.4.0 → regcode-0.4.1}/.gitignore +0 -0
- {regcode-0.4.0 → regcode-0.4.1}/.python-version +0 -0
- {regcode-0.4.0 → regcode-0.4.1}/.qwen/settings.json +0 -0
- {regcode-0.4.0 → regcode-0.4.1}/AGENTS.md +0 -0
- {regcode-0.4.0 → regcode-0.4.1}/GOALS.md +0 -0
- {regcode-0.4.0 → regcode-0.4.1}/README.md +0 -0
- {regcode-0.4.0 → regcode-0.4.1}/config.yaml.template +0 -0
- {regcode-0.4.0 → regcode-0.4.1}/main.py +0 -0
- {regcode-0.4.0 → regcode-0.4.1}/regcode/config.py +0 -0
- {regcode-0.4.0 → regcode-0.4.1}/regcode/conversation_manager.py +0 -0
- {regcode-0.4.0 → regcode-0.4.1}/regcode/main.py +0 -0
- {regcode-0.4.0 → regcode-0.4.1}/regcode/monty_sandbox.py +0 -0
- {regcode-0.4.0 → regcode-0.4.1}/regcode/permissions.py +0 -0
- {regcode-0.4.0 → regcode-0.4.1}/regcode/sandbox.py +0 -0
- {regcode-0.4.0 → regcode-0.4.1}/regcode/tools/base.py +0 -0
- {regcode-0.4.0 → regcode-0.4.1}/regcode/tools/registry.py +0 -0
- {regcode-0.4.0 → regcode-0.4.1}/regcode/tools/review_notes.py +0 -0
- {regcode-0.4.0 → regcode-0.4.1}/regcode/tracing.py +0 -0
- {regcode-0.4.0 → regcode-0.4.1}/regcode/tui.py +0 -0
- {regcode-0.4.0 → regcode-0.4.1}/repair_tui.md +0 -0
- {regcode-0.4.0 → regcode-0.4.1}/tests/__init__.py +0 -0
- {regcode-0.4.0 → regcode-0.4.1}/tests/conftest.py +0 -0
- {regcode-0.4.0 → regcode-0.4.1}/tests/test_api.py +0 -0
- {regcode-0.4.0 → regcode-0.4.1}/tests/test_cli.py +0 -0
- {regcode-0.4.0 → regcode-0.4.1}/tests/test_config.py +0 -0
- {regcode-0.4.0 → regcode-0.4.1}/tests/test_context_manager.py +0 -0
- {regcode-0.4.0 → regcode-0.4.1}/tests/test_main.py +0 -0
- {regcode-0.4.0 → regcode-0.4.1}/tests/test_monty_sandbox.py +0 -0
- {regcode-0.4.0 → regcode-0.4.1}/tests/test_osv_cve.py +0 -0
- {regcode-0.4.0 → regcode-0.4.1}/tests/test_provider_extra.py +0 -0
- {regcode-0.4.0 → regcode-0.4.1}/tests/test_review_notes.py +0 -0
|
@@ -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
|
|
@@ -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
|
-
|
|
99
|
-
import asyncio
|
|
99
|
+
import asyncio
|
|
100
100
|
|
|
101
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|