janito 1.6.0__py3-none-any.whl → 1.8.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.
- janito/__init__.py +1 -1
- janito/agent/config.py +3 -3
- janito/agent/config_defaults.py +3 -2
- janito/agent/conversation.py +73 -27
- janito/agent/conversation_api.py +104 -4
- janito/agent/conversation_exceptions.py +6 -0
- janito/agent/conversation_tool_calls.py +17 -3
- janito/agent/event.py +24 -0
- janito/agent/event_dispatcher.py +24 -0
- janito/agent/event_handler_protocol.py +5 -0
- janito/agent/event_system.py +15 -0
- janito/agent/message_handler.py +4 -1
- janito/agent/message_handler_protocol.py +5 -0
- janito/agent/openai_client.py +5 -6
- janito/agent/openai_schema_generator.py +23 -4
- janito/agent/platform_discovery.py +90 -0
- janito/agent/profile_manager.py +34 -110
- janito/agent/queued_message_handler.py +22 -3
- janito/agent/rich_message_handler.py +3 -1
- janito/agent/templates/profiles/system_prompt_template_base.txt.j2 +14 -0
- janito/agent/templates/profiles/system_prompt_template_base_pt.txt.j2 +13 -0
- janito/agent/test_handler_protocols.py +47 -0
- janito/agent/tests/__init__.py +1 -0
- janito/agent/tool_base.py +1 -1
- janito/agent/tool_executor.py +109 -0
- janito/agent/tool_registry.py +3 -75
- janito/agent/tool_use_tracker.py +46 -0
- janito/agent/tools/__init__.py +11 -8
- janito/agent/tools/ask_user.py +26 -12
- janito/agent/tools/create_directory.py +50 -18
- janito/agent/tools/create_file.py +60 -29
- janito/agent/tools/dir_walk_utils.py +16 -0
- janito/agent/tools/fetch_url.py +10 -11
- janito/agent/tools/find_files.py +49 -40
- janito/agent/tools/get_lines.py +60 -25
- janito/agent/tools/memory.py +48 -0
- janito/agent/tools/move_file.py +72 -23
- janito/agent/tools/outline_file/__init__.py +85 -0
- janito/agent/tools/outline_file/formatting.py +20 -0
- janito/agent/tools/outline_file/markdown_outline.py +14 -0
- janito/agent/tools/outline_file/python_outline.py +71 -0
- janito/agent/tools/present_choices.py +62 -0
- janito/agent/tools/present_choices_test.py +18 -0
- janito/agent/tools/remove_directory.py +31 -26
- janito/agent/tools/remove_file.py +31 -13
- janito/agent/tools/replace_text_in_file.py +135 -36
- janito/agent/tools/run_bash_command.py +113 -97
- janito/agent/tools/run_powershell_command.py +169 -0
- janito/agent/tools/run_python_command.py +53 -29
- janito/agent/tools/search_outline.py +17 -0
- janito/agent/tools/search_text.py +208 -0
- janito/agent/tools/tools_utils.py +47 -4
- janito/agent/tools/utils.py +14 -15
- janito/agent/tools/validate_file_syntax.py +163 -0
- janito/cli/_print_config.py +1 -1
- janito/cli/arg_parser.py +36 -4
- janito/cli/config_commands.py +1 -1
- janito/cli/logging_setup.py +7 -2
- janito/cli/main.py +97 -3
- janito/cli/runner/__init__.py +0 -2
- janito/cli/runner/_termweb_log_utils.py +17 -0
- janito/cli/runner/cli_main.py +121 -89
- janito/cli/runner/config.py +6 -4
- janito/cli/termweb_starter.py +73 -0
- janito/cli_chat_shell/chat_loop.py +52 -13
- janito/cli_chat_shell/chat_state.py +1 -1
- janito/cli_chat_shell/chat_ui.py +2 -3
- janito/cli_chat_shell/commands/__init__.py +17 -6
- janito/cli_chat_shell/commands/{history_reset.py → history_start.py} +13 -5
- janito/cli_chat_shell/commands/lang.py +16 -0
- janito/cli_chat_shell/commands/prompt.py +42 -0
- janito/cli_chat_shell/commands/session_control.py +36 -1
- janito/cli_chat_shell/commands/sum.py +49 -0
- janito/cli_chat_shell/commands/termweb_log.py +86 -0
- janito/cli_chat_shell/commands/utility.py +5 -2
- janito/cli_chat_shell/commands/verbose.py +29 -0
- janito/cli_chat_shell/load_prompt.py +47 -8
- janito/cli_chat_shell/session_manager.py +9 -1
- janito/cli_chat_shell/shell_command_completer.py +20 -0
- janito/cli_chat_shell/ui.py +110 -93
- janito/i18n/__init__.py +35 -0
- janito/i18n/messages.py +23 -0
- janito/i18n/pt.py +46 -0
- janito/rich_utils.py +43 -43
- janito/termweb/app.py +95 -0
- janito/termweb/static/editor.html +238 -0
- janito/termweb/static/editor.html.bak +238 -0
- janito/termweb/static/explorer.html.bak +59 -0
- janito/termweb/static/favicon.ico +0 -0
- janito/termweb/static/favicon.ico.bak +0 -0
- janito/termweb/static/index.html +55 -0
- janito/termweb/static/index.html.bak +55 -0
- janito/termweb/static/index.html.bak.bak +175 -0
- janito/termweb/static/landing.html.bak +36 -0
- janito/termweb/static/termicon.svg +1 -0
- janito/termweb/static/termweb.css +235 -0
- janito/termweb/static/termweb.css.bak +286 -0
- janito/termweb/static/termweb.js +187 -0
- janito/termweb/static/termweb.js.bak +187 -0
- janito/termweb/static/termweb.js.bak.bak +157 -0
- janito/termweb/static/termweb_quickopen.js +135 -0
- janito/termweb/static/termweb_quickopen.js.bak +125 -0
- janito/web/app.py +10 -13
- {janito-1.6.0.dist-info → janito-1.8.0.dist-info}/METADATA +73 -32
- janito-1.8.0.dist-info/RECORD +127 -0
- {janito-1.6.0.dist-info → janito-1.8.0.dist-info}/WHEEL +1 -1
- janito/agent/tool_registry_core.py +0 -2
- janito/agent/tools/get_file_outline.py +0 -117
- janito/agent/tools/py_compile_file.py +0 -40
- janito/agent/tools/replace_file.py +0 -51
- janito/agent/tools/search_files.py +0 -71
- janito/cli/runner/scan.py +0 -44
- janito/cli_chat_shell/commands/system.py +0 -73
- janito-1.6.0.dist-info/RECORD +0 -81
- {janito-1.6.0.dist-info → janito-1.8.0.dist-info}/entry_points.txt +0 -0
- {janito-1.6.0.dist-info → janito-1.8.0.dist-info}/licenses/LICENSE +0 -0
- {janito-1.6.0.dist-info → janito-1.8.0.dist-info}/top_level.txt +0 -0
@@ -1,15 +1,16 @@
|
|
1
1
|
import subprocess
|
2
2
|
import tempfile
|
3
3
|
import sys
|
4
|
+
import os
|
4
5
|
from janito.agent.tool_base import ToolBase
|
5
6
|
from janito.agent.tool_registry import register_tool
|
7
|
+
from janito.i18n import tr
|
6
8
|
|
7
9
|
|
8
10
|
@register_tool(name="run_python_command")
|
9
11
|
class RunPythonCommandTool(ToolBase):
|
10
12
|
"""
|
11
13
|
Tool to execute Python code in a subprocess and capture output.
|
12
|
-
|
13
14
|
Args:
|
14
15
|
code (str): The Python code to execute.
|
15
16
|
timeout (int, optional): Timeout in seconds for the command. Defaults to 60.
|
@@ -19,7 +20,7 @@ class RunPythonCommandTool(ToolBase):
|
|
19
20
|
str: File paths and line counts for stdout and stderr, or direct output if small enough.
|
20
21
|
"""
|
21
22
|
|
22
|
-
def
|
23
|
+
def run(
|
23
24
|
self,
|
24
25
|
code: str,
|
25
26
|
timeout: int = 60,
|
@@ -27,19 +28,25 @@ class RunPythonCommandTool(ToolBase):
|
|
27
28
|
interactive: bool = False,
|
28
29
|
) -> str:
|
29
30
|
if not code.strip():
|
30
|
-
self.report_warning(
|
31
|
-
|
32
|
-
|
31
|
+
self.report_warning(
|
32
|
+
tr("⚠️ Warning: Empty code provided. Operation skipped.")
|
33
|
+
)
|
34
|
+
return tr("Warning: Empty code provided. Operation skipped.")
|
35
|
+
self.report_info(tr("🐍 Running Python code: ...\n{code}\n", code=code))
|
33
36
|
if interactive:
|
34
37
|
self.report_info(
|
35
|
-
|
38
|
+
tr(
|
39
|
+
"⚠️ Warning: This code might be interactive, require user input, and might hang."
|
40
|
+
)
|
36
41
|
)
|
37
42
|
sys.stdout.flush()
|
38
43
|
if require_confirmation:
|
39
|
-
confirmed = self.confirm_action(
|
44
|
+
confirmed = self.confirm_action(
|
45
|
+
tr("Do you want to execute this Python code?")
|
46
|
+
)
|
40
47
|
if not confirmed:
|
41
|
-
self.report_warning("Execution cancelled by user.")
|
42
|
-
return "Execution cancelled by user."
|
48
|
+
self.report_warning(tr("Execution cancelled by user."))
|
49
|
+
return tr("Execution cancelled by user.")
|
43
50
|
try:
|
44
51
|
with (
|
45
52
|
tempfile.NamedTemporaryFile(
|
@@ -64,19 +71,25 @@ class RunPythonCommandTool(ToolBase):
|
|
64
71
|
):
|
65
72
|
code_file.write(code)
|
66
73
|
code_file.flush()
|
74
|
+
env = os.environ.copy()
|
75
|
+
env["PYTHONIOENCODING"] = "utf-8"
|
67
76
|
process = subprocess.Popen(
|
68
77
|
[sys.executable, code_file.name],
|
69
78
|
stdout=stdout_file,
|
70
79
|
stderr=stderr_file,
|
71
80
|
text=True,
|
81
|
+
env=env,
|
72
82
|
)
|
73
83
|
try:
|
74
84
|
return_code = process.wait(timeout=timeout)
|
75
85
|
except subprocess.TimeoutExpired:
|
76
86
|
process.kill()
|
77
|
-
self.report_error(
|
78
|
-
|
79
|
-
|
87
|
+
self.report_error(
|
88
|
+
tr(" ❌ Timed out after {timeout} seconds.", timeout=timeout)
|
89
|
+
)
|
90
|
+
return tr(
|
91
|
+
"Code timed out after {timeout} seconds.", timeout=timeout
|
92
|
+
)
|
80
93
|
stdout_file.flush()
|
81
94
|
stderr_file.flush()
|
82
95
|
with open(
|
@@ -91,7 +104,6 @@ class RunPythonCommandTool(ToolBase):
|
|
91
104
|
err_f.seek(0)
|
92
105
|
for line in err_f:
|
93
106
|
self.report_stderr(line)
|
94
|
-
# Count lines
|
95
107
|
with open(
|
96
108
|
stdout_file.name, "r", encoding="utf-8", errors="replace"
|
97
109
|
) as out_f:
|
@@ -100,11 +112,14 @@ class RunPythonCommandTool(ToolBase):
|
|
100
112
|
stderr_file.name, "r", encoding="utf-8", errors="replace"
|
101
113
|
) as err_f:
|
102
114
|
stderr_lines = sum(1 for _ in err_f)
|
103
|
-
self.report_success(
|
115
|
+
self.report_success(
|
116
|
+
tr(" ✅ return code {return_code}", return_code=return_code)
|
117
|
+
)
|
104
118
|
warning_msg = ""
|
105
119
|
if interactive:
|
106
|
-
warning_msg =
|
107
|
-
|
120
|
+
warning_msg = tr(
|
121
|
+
"⚠️ Warning: This code might be interactive, require user input, and might hang.\n"
|
122
|
+
)
|
108
123
|
with open(
|
109
124
|
stdout_file.name, "r", encoding="utf-8", errors="replace"
|
110
125
|
) as out_f:
|
@@ -113,27 +128,36 @@ class RunPythonCommandTool(ToolBase):
|
|
113
128
|
stderr_file.name, "r", encoding="utf-8", errors="replace"
|
114
129
|
) as err_f:
|
115
130
|
stderr_content = err_f.read()
|
116
|
-
# Thresholds
|
117
131
|
max_lines = 100
|
118
132
|
if stdout_lines <= max_lines and stderr_lines <= max_lines:
|
119
|
-
result = (
|
120
|
-
|
121
|
-
|
133
|
+
result = warning_msg + tr(
|
134
|
+
"Return code: {return_code}\n--- STDOUT ---\n{stdout_content}",
|
135
|
+
return_code=return_code,
|
136
|
+
stdout_content=stdout_content,
|
122
137
|
)
|
123
138
|
if stderr_content.strip():
|
124
|
-
result +=
|
139
|
+
result += tr(
|
140
|
+
"\n--- STDERR ---\n{stderr_content}",
|
141
|
+
stderr_content=stderr_content,
|
142
|
+
)
|
125
143
|
return result
|
126
144
|
else:
|
127
|
-
result = (
|
128
|
-
|
129
|
-
|
145
|
+
result = warning_msg + tr(
|
146
|
+
"stdout_file: {stdout_file} (lines: {stdout_lines})\n",
|
147
|
+
stdout_file=stdout_file.name,
|
148
|
+
stdout_lines=stdout_lines,
|
130
149
|
)
|
131
150
|
if stderr_lines > 0 and stderr_content.strip():
|
132
|
-
result += (
|
133
|
-
|
151
|
+
result += tr(
|
152
|
+
"stderr_file: {stderr_file} (lines: {stderr_lines})\n",
|
153
|
+
stderr_file=stderr_file.name,
|
154
|
+
stderr_lines=stderr_lines,
|
134
155
|
)
|
135
|
-
result +=
|
156
|
+
result += tr(
|
157
|
+
"returncode: {return_code}\nUse the get_lines tool to inspect the contents of these files when needed.",
|
158
|
+
return_code=return_code,
|
159
|
+
)
|
136
160
|
return result
|
137
161
|
except Exception as e:
|
138
|
-
self.report_error(
|
139
|
-
return
|
162
|
+
self.report_error(tr(" ❌ Error: {error}", error=e))
|
163
|
+
return tr("Error running code: {error}", error=e)
|
@@ -0,0 +1,17 @@
|
|
1
|
+
from janito.agent.tool_base import ToolBase
|
2
|
+
from janito.agent.tool_registry import register_tool
|
3
|
+
|
4
|
+
|
5
|
+
@register_tool(name="search_outline")
|
6
|
+
class SearchOutlineTool(ToolBase):
|
7
|
+
"""
|
8
|
+
Tool for searching outlines in files.
|
9
|
+
"""
|
10
|
+
|
11
|
+
def run(self, file_path: str) -> str:
|
12
|
+
# ... rest of implementation ...
|
13
|
+
# Example warnings and successes:
|
14
|
+
# self.report_warning(tr("No files found with supported extensions."))
|
15
|
+
# self.report_warning(tr("Error reading {file_path}: {error}", file_path=file_path, error=e))
|
16
|
+
# self.report_success(tr("✅ {count} {match_word} found", count=len(output), match_word=pluralize('match', len(output))))
|
17
|
+
pass
|
@@ -0,0 +1,208 @@
|
|
1
|
+
from janito.agent.tool_base import ToolBase
|
2
|
+
from janito.agent.tool_registry import register_tool
|
3
|
+
from janito.agent.tools.tools_utils import pluralize
|
4
|
+
from janito.i18n import tr
|
5
|
+
import os
|
6
|
+
import re
|
7
|
+
from janito.agent.tools.gitignore_utils import filter_ignored
|
8
|
+
|
9
|
+
|
10
|
+
def is_binary_file(path, blocksize=1024):
|
11
|
+
try:
|
12
|
+
with open(path, "rb") as f:
|
13
|
+
chunk = f.read(blocksize)
|
14
|
+
if b"\0" in chunk:
|
15
|
+
return True
|
16
|
+
text_characters = bytearray(
|
17
|
+
{7, 8, 9, 10, 12, 13, 27} | set(range(0x20, 0x100))
|
18
|
+
)
|
19
|
+
nontext = chunk.translate(None, text_characters)
|
20
|
+
if len(nontext) / max(1, len(chunk)) > 0.3:
|
21
|
+
return True
|
22
|
+
except Exception:
|
23
|
+
return True
|
24
|
+
return False
|
25
|
+
|
26
|
+
|
27
|
+
@register_tool(name="search_text")
|
28
|
+
class SearchTextTool(ToolBase):
|
29
|
+
"""
|
30
|
+
Search for a text pattern (regex or plain string) in all files within one or more directories or file paths and return matching lines. Respects .gitignore.
|
31
|
+
|
32
|
+
Args:
|
33
|
+
paths (str): String of one or more paths (space-separated) to search in. Each path can be a directory or a file.
|
34
|
+
pattern (str): Regex pattern or plain text substring to search for in files. Tries regex first, falls back to substring if regex is invalid.
|
35
|
+
is_regex (bool): If True, treat pattern as regex. If False, treat as plain text. Defaults to False.
|
36
|
+
max_depth (int, optional): Maximum directory depth to search. If 0 (default), search is recursive with no depth limit. If >0, limits recursion to that depth. Setting max_depth=1 disables recursion (only top-level directory). Ignored for file paths.
|
37
|
+
max_results (int): Maximum number of results to return. 0 means no limit (default).
|
38
|
+
ignore_utf8_errors (bool): If True, ignore utf-8 decode errors. Defaults to True.
|
39
|
+
Returns:
|
40
|
+
str: Matching lines from files as a newline-separated string, each formatted as 'filepath:lineno: line'.
|
41
|
+
If max_results is reached, appends a note to the output.
|
42
|
+
"""
|
43
|
+
|
44
|
+
def run(
|
45
|
+
self,
|
46
|
+
paths: str,
|
47
|
+
pattern: str,
|
48
|
+
is_regex: bool = False,
|
49
|
+
max_depth: int = 0,
|
50
|
+
max_results: int = 0,
|
51
|
+
ignore_utf8_errors: bool = True,
|
52
|
+
) -> str:
|
53
|
+
if not pattern:
|
54
|
+
self.report_warning(
|
55
|
+
tr("⚠️ Warning: Empty search pattern provided. Operation skipped.")
|
56
|
+
)
|
57
|
+
return tr("Warning: Empty search pattern provided. Operation skipped.")
|
58
|
+
regex = None
|
59
|
+
use_regex = False
|
60
|
+
if is_regex:
|
61
|
+
try:
|
62
|
+
regex = re.compile(pattern)
|
63
|
+
use_regex = True
|
64
|
+
except re.error as e:
|
65
|
+
self.report_warning(
|
66
|
+
tr(
|
67
|
+
"Invalid regex pattern: {error}. Falling back to no results.",
|
68
|
+
error=e,
|
69
|
+
)
|
70
|
+
)
|
71
|
+
return tr(
|
72
|
+
"Warning: Invalid regex pattern: {error}. No results.", error=e
|
73
|
+
)
|
74
|
+
else:
|
75
|
+
try:
|
76
|
+
regex = re.compile(pattern)
|
77
|
+
use_regex = True
|
78
|
+
except re.error:
|
79
|
+
regex = None
|
80
|
+
use_regex = False
|
81
|
+
output = []
|
82
|
+
limit_reached = False
|
83
|
+
total_results = 0
|
84
|
+
paths_list = paths.split()
|
85
|
+
for search_path in paths_list:
|
86
|
+
from janito.agent.tools.tools_utils import display_path
|
87
|
+
|
88
|
+
info_str = tr(
|
89
|
+
"🔍 Searching for {search_type} '{pattern}' in '{disp_path}'",
|
90
|
+
search_type=("text-regex" if use_regex else "text"),
|
91
|
+
pattern=pattern,
|
92
|
+
disp_path=display_path(search_path),
|
93
|
+
)
|
94
|
+
if max_depth > 0:
|
95
|
+
info_str += tr(" [max_depth={max_depth}]", max_depth=max_depth)
|
96
|
+
self.report_info(info_str)
|
97
|
+
dir_output = []
|
98
|
+
dir_limit_reached = False
|
99
|
+
if os.path.isfile(search_path):
|
100
|
+
# Handle single file
|
101
|
+
path = search_path
|
102
|
+
if not is_binary_file(path):
|
103
|
+
try:
|
104
|
+
open_kwargs = {"mode": "r", "encoding": "utf-8"}
|
105
|
+
if ignore_utf8_errors:
|
106
|
+
open_kwargs["errors"] = "ignore"
|
107
|
+
with open(path, **open_kwargs) as f:
|
108
|
+
for lineno, line in enumerate(f, 1):
|
109
|
+
if use_regex:
|
110
|
+
if regex.search(line):
|
111
|
+
dir_output.append(
|
112
|
+
f"{path}:{lineno}: {line.strip()}"
|
113
|
+
)
|
114
|
+
else:
|
115
|
+
if pattern in line:
|
116
|
+
dir_output.append(
|
117
|
+
f"{path}:{lineno}: {line.strip()}"
|
118
|
+
)
|
119
|
+
if (
|
120
|
+
max_results > 0
|
121
|
+
and (total_results + len(dir_output)) >= max_results
|
122
|
+
):
|
123
|
+
dir_limit_reached = True
|
124
|
+
break
|
125
|
+
except Exception:
|
126
|
+
pass
|
127
|
+
output.extend(dir_output)
|
128
|
+
total_results += len(dir_output)
|
129
|
+
if dir_limit_reached:
|
130
|
+
limit_reached = True
|
131
|
+
break
|
132
|
+
continue
|
133
|
+
# Directory logic as before
|
134
|
+
if max_depth == 1:
|
135
|
+
walk_result = next(os.walk(search_path), None)
|
136
|
+
if walk_result is None:
|
137
|
+
walker = [(search_path, [], [])]
|
138
|
+
else:
|
139
|
+
_, dirs, files = walk_result
|
140
|
+
dirs, files = filter_ignored(search_path, dirs, files)
|
141
|
+
walker = [(search_path, dirs, files)]
|
142
|
+
else:
|
143
|
+
walker = os.walk(search_path)
|
144
|
+
stop_search = False
|
145
|
+
for root, dirs, files in walker:
|
146
|
+
if stop_search:
|
147
|
+
break
|
148
|
+
rel_path = os.path.relpath(root, search_path)
|
149
|
+
depth = 0 if rel_path == "." else rel_path.count(os.sep) + 1
|
150
|
+
if max_depth == 1 and depth > 0:
|
151
|
+
break
|
152
|
+
if max_depth > 0 and depth > max_depth:
|
153
|
+
continue
|
154
|
+
dirs, files = filter_ignored(root, dirs, files)
|
155
|
+
for filename in files:
|
156
|
+
if stop_search:
|
157
|
+
break
|
158
|
+
path = os.path.join(root, filename)
|
159
|
+
if is_binary_file(path):
|
160
|
+
continue
|
161
|
+
try:
|
162
|
+
open_kwargs = {"mode": "r", "encoding": "utf-8"}
|
163
|
+
if ignore_utf8_errors:
|
164
|
+
open_kwargs["errors"] = "ignore"
|
165
|
+
with open(path, **open_kwargs) as f:
|
166
|
+
for lineno, line in enumerate(f, 1):
|
167
|
+
if use_regex:
|
168
|
+
if regex.search(line):
|
169
|
+
dir_output.append(
|
170
|
+
f"{path}:{lineno}: {line.strip()}"
|
171
|
+
)
|
172
|
+
else:
|
173
|
+
if pattern in line:
|
174
|
+
dir_output.append(
|
175
|
+
f"{path}:{lineno}: {line.strip()}"
|
176
|
+
)
|
177
|
+
if (
|
178
|
+
max_results > 0
|
179
|
+
and (total_results + len(dir_output)) >= max_results
|
180
|
+
):
|
181
|
+
dir_limit_reached = True
|
182
|
+
stop_search = True
|
183
|
+
break
|
184
|
+
except Exception:
|
185
|
+
continue
|
186
|
+
output.extend(dir_output)
|
187
|
+
total_results += len(dir_output)
|
188
|
+
if dir_limit_reached:
|
189
|
+
limit_reached = True
|
190
|
+
break
|
191
|
+
header = tr(
|
192
|
+
"[search_text] Pattern: '{pattern}' | Regex: {use_regex} | Results: {count}",
|
193
|
+
pattern=pattern,
|
194
|
+
use_regex=use_regex,
|
195
|
+
count=len(output),
|
196
|
+
)
|
197
|
+
result = header + "\n" + "\n".join(output)
|
198
|
+
if limit_reached:
|
199
|
+
result += tr("\n[Note: max_results limit reached, output truncated.]")
|
200
|
+
self.report_success(
|
201
|
+
tr(
|
202
|
+
" ✅ {count} {line_word} found{limit}",
|
203
|
+
count=len(output),
|
204
|
+
line_word=pluralize("line", len(output)),
|
205
|
+
limit=(" (limit reached)" if limit_reached else ""),
|
206
|
+
)
|
207
|
+
)
|
208
|
+
return result
|
@@ -1,9 +1,27 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
import os
|
2
|
+
import urllib.parse
|
3
|
+
from janito.agent.tools.gitignore_utils import filter_ignored
|
4
|
+
from janito.agent.runtime_config import runtime_config
|
5
|
+
|
3
6
|
|
7
|
+
def display_path(path):
|
8
|
+
"""
|
9
|
+
Returns a display-friendly path. If runtime_config['termweb_port'] is set, injects an ANSI hyperlink to the local web file viewer.
|
10
|
+
Args:
|
11
|
+
path (str): Path to display.
|
12
|
+
Returns:
|
13
|
+
str: Display path, optionally as an ANSI hyperlink.
|
14
|
+
"""
|
4
15
|
if os.path.isabs(path):
|
5
|
-
|
6
|
-
|
16
|
+
disp = path
|
17
|
+
else:
|
18
|
+
disp = os.path.relpath(path)
|
19
|
+
port = runtime_config.get("termweb_port")
|
20
|
+
if port:
|
21
|
+
url = f"http://localhost:{port}/?path={urllib.parse.quote(path)}"
|
22
|
+
# Use Rich markup for hyperlinks
|
23
|
+
return f"[link={url}]{disp}[/link]"
|
24
|
+
return disp
|
7
25
|
|
8
26
|
|
9
27
|
def pluralize(word: str, count: int) -> str:
|
@@ -11,3 +29,28 @@ def pluralize(word: str, count: int) -> str:
|
|
11
29
|
if count == 1 or word.endswith("s"):
|
12
30
|
return word
|
13
31
|
return word + "s"
|
32
|
+
|
33
|
+
|
34
|
+
def find_files_with_extensions(directories, extensions, max_depth=0):
|
35
|
+
"""
|
36
|
+
Find files in given directories with specified extensions, respecting .gitignore.
|
37
|
+
|
38
|
+
Args:
|
39
|
+
directories (list[str]): Directories to search.
|
40
|
+
extensions (list[str]): File extensions to include (e.g., ['.py', '.md']).
|
41
|
+
max_depth (int, optional): Maximum directory depth to search. If 0, unlimited.
|
42
|
+
Returns:
|
43
|
+
list[str]: List of matching file paths.
|
44
|
+
"""
|
45
|
+
output = []
|
46
|
+
for directory in directories:
|
47
|
+
for root, dirs, files in os.walk(directory):
|
48
|
+
rel_path = os.path.relpath(root, directory)
|
49
|
+
depth = 0 if rel_path == "." else rel_path.count(os.sep) + 1
|
50
|
+
if max_depth > 0 and depth > max_depth:
|
51
|
+
continue
|
52
|
+
dirs, files = filter_ignored(root, dirs, files)
|
53
|
+
for filename in files:
|
54
|
+
if any(filename.lower().endswith(ext) for ext in extensions):
|
55
|
+
output.append(os.path.join(root, filename))
|
56
|
+
return output
|
janito/agent/tools/utils.py
CHANGED
@@ -11,24 +11,23 @@ def expand_path(path: str) -> str:
|
|
11
11
|
return path
|
12
12
|
|
13
13
|
|
14
|
-
def display_path(
|
14
|
+
def display_path(path: str) -> str:
|
15
15
|
"""
|
16
16
|
Returns a user-friendly path for display:
|
17
|
-
- If the
|
18
|
-
- If the
|
19
|
-
-
|
20
|
-
- Else, show the
|
17
|
+
- If the path is relative, return it as-is.
|
18
|
+
- If the path starts with ~, keep it as ~.
|
19
|
+
- If the path is under the home directory, replace the home dir with ~.
|
20
|
+
- Else, show the absolute path.
|
21
21
|
"""
|
22
|
-
# Detect relative path (POSIX or Windows)
|
23
22
|
if not (
|
24
|
-
|
25
|
-
or
|
26
|
-
or (os.name == "nt" and len(
|
23
|
+
path.startswith("/")
|
24
|
+
or path.startswith("~")
|
25
|
+
or (os.name == "nt" and len(path) > 1 and path[1] == ":")
|
27
26
|
):
|
28
|
-
return
|
27
|
+
return path
|
29
28
|
home = os.path.expanduser("~")
|
30
|
-
if
|
31
|
-
return
|
32
|
-
if
|
33
|
-
return "~" +
|
34
|
-
return
|
29
|
+
if path.startswith("~"):
|
30
|
+
return path
|
31
|
+
if path.startswith(home):
|
32
|
+
return "~" + path[len(home) :]
|
33
|
+
return path
|
@@ -0,0 +1,163 @@
|
|
1
|
+
from janito.agent.tool_base import ToolBase
|
2
|
+
from janito.agent.tool_registry import register_tool
|
3
|
+
from janito.i18n import tr
|
4
|
+
import os
|
5
|
+
import json
|
6
|
+
import yaml
|
7
|
+
from janito.agent.tools.utils import display_path
|
8
|
+
|
9
|
+
|
10
|
+
@register_tool(name="validate_file_syntax")
|
11
|
+
class ValidateFileSyntaxTool(ToolBase):
|
12
|
+
"""
|
13
|
+
Validate a file for syntax issues.
|
14
|
+
|
15
|
+
Supported types:
|
16
|
+
- Python (.py, .pyw)
|
17
|
+
- JSON (.json)
|
18
|
+
- YAML (.yml, .yaml)
|
19
|
+
- PowerShell (.ps1)
|
20
|
+
- XML (.xml)
|
21
|
+
- HTML (.html, .htm) [lxml]
|
22
|
+
|
23
|
+
Args:
|
24
|
+
file_path (str): Path to the file to validate.
|
25
|
+
Returns:
|
26
|
+
str: Validation status message. Example:
|
27
|
+
- "✅ Syntax OK"
|
28
|
+
- "⚠️ Warning: Syntax error: <error message>"
|
29
|
+
- "⚠️ Warning: Unsupported file extension: <ext>"
|
30
|
+
"""
|
31
|
+
|
32
|
+
def run(self, file_path: str) -> str:
|
33
|
+
disp_path = display_path(file_path)
|
34
|
+
self.report_info(
|
35
|
+
tr("🔎 Validating syntax for: {disp_path} ...", disp_path=disp_path)
|
36
|
+
)
|
37
|
+
ext = os.path.splitext(file_path)[1].lower()
|
38
|
+
try:
|
39
|
+
if ext in [".py", ".pyw"]:
|
40
|
+
import py_compile
|
41
|
+
|
42
|
+
py_compile.compile(file_path, doraise=True)
|
43
|
+
elif ext == ".json":
|
44
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
45
|
+
json.load(f)
|
46
|
+
elif ext in [".yml", ".yaml"]:
|
47
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
48
|
+
yaml.safe_load(f)
|
49
|
+
elif ext == ".ps1":
|
50
|
+
from janito.agent.tools.run_powershell_command import (
|
51
|
+
RunPowerShellCommandTool,
|
52
|
+
)
|
53
|
+
|
54
|
+
ps_tool = RunPowerShellCommandTool()
|
55
|
+
check_cmd = "if (Get-Command Invoke-ScriptAnalyzer -ErrorAction SilentlyContinue) { Write-Output 'PSScriptAnalyzerAvailable' } else { Write-Output 'PSScriptAnalyzerMissing' }"
|
56
|
+
check_result = ps_tool.run(command=check_cmd, timeout=15)
|
57
|
+
if "PSScriptAnalyzerMissing" in check_result:
|
58
|
+
msg = tr(
|
59
|
+
"⚠️ Warning: PSScriptAnalyzer is not installed. For best PowerShell syntax validation, install it with:\n Install-Module -Name PSScriptAnalyzer -Scope CurrentUser\n"
|
60
|
+
)
|
61
|
+
self.report_warning(msg)
|
62
|
+
return msg
|
63
|
+
analyze_cmd = f"Invoke-ScriptAnalyzer -Path '{file_path}' -Severity Error | ConvertTo-Json"
|
64
|
+
analyze_result = ps_tool.run(command=analyze_cmd, timeout=30)
|
65
|
+
if "[]" in analyze_result or analyze_result.strip() == "":
|
66
|
+
self.report_success(tr("✅ Syntax OK"))
|
67
|
+
return tr("✅ Syntax valid")
|
68
|
+
else:
|
69
|
+
msg = tr(
|
70
|
+
"⚠️ Warning: PowerShell syntax issues found:\n{analyze_result}",
|
71
|
+
analyze_result=analyze_result,
|
72
|
+
)
|
73
|
+
self.report_warning(msg)
|
74
|
+
return msg
|
75
|
+
elif ext == ".xml":
|
76
|
+
try:
|
77
|
+
from lxml import etree
|
78
|
+
except ImportError:
|
79
|
+
msg = tr("⚠️ lxml not installed. Cannot validate XML.")
|
80
|
+
self.report_warning(msg)
|
81
|
+
return msg
|
82
|
+
with open(file_path, "rb") as f:
|
83
|
+
etree.parse(f)
|
84
|
+
elif ext in (".html", ".htm"):
|
85
|
+
try:
|
86
|
+
from lxml import html
|
87
|
+
except ImportError:
|
88
|
+
msg = tr("⚠️ lxml not installed. Cannot validate HTML.")
|
89
|
+
self.report_warning(msg)
|
90
|
+
return msg
|
91
|
+
with open(file_path, "rb") as f:
|
92
|
+
html.parse(f)
|
93
|
+
from lxml import etree
|
94
|
+
|
95
|
+
parser = etree.HTMLParser(recover=False)
|
96
|
+
with open(file_path, "rb") as f:
|
97
|
+
etree.parse(f, parser=parser)
|
98
|
+
if parser.error_log:
|
99
|
+
errors = "\n".join(str(e) for e in parser.error_log)
|
100
|
+
raise ValueError(
|
101
|
+
tr("HTML syntax errors found:\n{errors}", errors=errors)
|
102
|
+
)
|
103
|
+
elif ext == ".md":
|
104
|
+
import re
|
105
|
+
|
106
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
107
|
+
content = f.read()
|
108
|
+
errors = []
|
109
|
+
# Rule: Headers must start with # followed by a space
|
110
|
+
for i, line in enumerate(content.splitlines(), 1):
|
111
|
+
if re.match(r"^#+[^ #]", line):
|
112
|
+
errors.append(
|
113
|
+
f"Line {i}: Header missing space after # | {line.strip()}"
|
114
|
+
)
|
115
|
+
# Rule: Unclosed code blocks
|
116
|
+
if content.count("```") % 2 != 0:
|
117
|
+
errors.append("Unclosed code block (```) detected")
|
118
|
+
# Rule: Unclosed links/images (flag only if line contains [text]( but not ))
|
119
|
+
for i, line in enumerate(content.splitlines(), 1):
|
120
|
+
if re.search(r"\[[^\]]*\]\([^)]+$", line):
|
121
|
+
errors.append(
|
122
|
+
f"Line {i}: Unclosed link or image (missing closing parenthesis) | {line.strip()}"
|
123
|
+
)
|
124
|
+
# Rule: List items must start with -, *, or + followed by space
|
125
|
+
for i, line in enumerate(content.splitlines(), 1):
|
126
|
+
# Skip horizontal rules like --- or ***
|
127
|
+
if re.match(r"^([-*+])\1{1,}", line):
|
128
|
+
continue
|
129
|
+
# Skip table rows (lines starting with |)
|
130
|
+
if line.lstrip().startswith("|"):
|
131
|
+
continue
|
132
|
+
# Only flag as list item if there is text after the bullet (not just emphasis)
|
133
|
+
if re.match(r"^[-*+][^ \n]", line):
|
134
|
+
stripped = line.strip()
|
135
|
+
# If the line is surrounded by * and ends with *, it's likely emphasis, not a list
|
136
|
+
if not (
|
137
|
+
stripped.startswith("*")
|
138
|
+
and stripped.endswith("*")
|
139
|
+
and len(stripped) > 2
|
140
|
+
):
|
141
|
+
errors.append(
|
142
|
+
f"Line {i}: List item missing space after bullet | {line.strip()}"
|
143
|
+
)
|
144
|
+
# Rule: Inline code must have even number of backticks
|
145
|
+
if content.count("`") % 2 != 0:
|
146
|
+
errors.append("Unclosed inline code (`) detected")
|
147
|
+
if errors:
|
148
|
+
msg = tr(
|
149
|
+
"⚠️ Warning: Markdown syntax issues found:\n{errors}",
|
150
|
+
errors="\n".join(errors),
|
151
|
+
)
|
152
|
+
self.report_warning(msg)
|
153
|
+
return msg
|
154
|
+
else:
|
155
|
+
msg = tr("⚠️ Warning: Unsupported file extension: {ext}", ext=ext)
|
156
|
+
self.report_warning(msg)
|
157
|
+
return msg
|
158
|
+
self.report_success(tr("✅ Syntax OK"))
|
159
|
+
return tr("✅ Syntax valid")
|
160
|
+
except Exception as e:
|
161
|
+
msg = tr("⚠️ Warning: Syntax error: {error}", error=e)
|
162
|
+
self.report_warning(msg)
|
163
|
+
return msg
|
janito/cli/_print_config.py
CHANGED