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.
Files changed (117) hide show
  1. janito/__init__.py +1 -1
  2. janito/agent/config.py +3 -3
  3. janito/agent/config_defaults.py +3 -2
  4. janito/agent/conversation.py +73 -27
  5. janito/agent/conversation_api.py +104 -4
  6. janito/agent/conversation_exceptions.py +6 -0
  7. janito/agent/conversation_tool_calls.py +17 -3
  8. janito/agent/event.py +24 -0
  9. janito/agent/event_dispatcher.py +24 -0
  10. janito/agent/event_handler_protocol.py +5 -0
  11. janito/agent/event_system.py +15 -0
  12. janito/agent/message_handler.py +4 -1
  13. janito/agent/message_handler_protocol.py +5 -0
  14. janito/agent/openai_client.py +5 -6
  15. janito/agent/openai_schema_generator.py +23 -4
  16. janito/agent/platform_discovery.py +90 -0
  17. janito/agent/profile_manager.py +34 -110
  18. janito/agent/queued_message_handler.py +22 -3
  19. janito/agent/rich_message_handler.py +3 -1
  20. janito/agent/templates/profiles/system_prompt_template_base.txt.j2 +14 -0
  21. janito/agent/templates/profiles/system_prompt_template_base_pt.txt.j2 +13 -0
  22. janito/agent/test_handler_protocols.py +47 -0
  23. janito/agent/tests/__init__.py +1 -0
  24. janito/agent/tool_base.py +1 -1
  25. janito/agent/tool_executor.py +109 -0
  26. janito/agent/tool_registry.py +3 -75
  27. janito/agent/tool_use_tracker.py +46 -0
  28. janito/agent/tools/__init__.py +11 -8
  29. janito/agent/tools/ask_user.py +26 -12
  30. janito/agent/tools/create_directory.py +50 -18
  31. janito/agent/tools/create_file.py +60 -29
  32. janito/agent/tools/dir_walk_utils.py +16 -0
  33. janito/agent/tools/fetch_url.py +10 -11
  34. janito/agent/tools/find_files.py +49 -40
  35. janito/agent/tools/get_lines.py +60 -25
  36. janito/agent/tools/memory.py +48 -0
  37. janito/agent/tools/move_file.py +72 -23
  38. janito/agent/tools/outline_file/__init__.py +85 -0
  39. janito/agent/tools/outline_file/formatting.py +20 -0
  40. janito/agent/tools/outline_file/markdown_outline.py +14 -0
  41. janito/agent/tools/outline_file/python_outline.py +71 -0
  42. janito/agent/tools/present_choices.py +62 -0
  43. janito/agent/tools/present_choices_test.py +18 -0
  44. janito/agent/tools/remove_directory.py +31 -26
  45. janito/agent/tools/remove_file.py +31 -13
  46. janito/agent/tools/replace_text_in_file.py +135 -36
  47. janito/agent/tools/run_bash_command.py +113 -97
  48. janito/agent/tools/run_powershell_command.py +169 -0
  49. janito/agent/tools/run_python_command.py +53 -29
  50. janito/agent/tools/search_outline.py +17 -0
  51. janito/agent/tools/search_text.py +208 -0
  52. janito/agent/tools/tools_utils.py +47 -4
  53. janito/agent/tools/utils.py +14 -15
  54. janito/agent/tools/validate_file_syntax.py +163 -0
  55. janito/cli/_print_config.py +1 -1
  56. janito/cli/arg_parser.py +36 -4
  57. janito/cli/config_commands.py +1 -1
  58. janito/cli/logging_setup.py +7 -2
  59. janito/cli/main.py +97 -3
  60. janito/cli/runner/__init__.py +0 -2
  61. janito/cli/runner/_termweb_log_utils.py +17 -0
  62. janito/cli/runner/cli_main.py +121 -89
  63. janito/cli/runner/config.py +6 -4
  64. janito/cli/termweb_starter.py +73 -0
  65. janito/cli_chat_shell/chat_loop.py +52 -13
  66. janito/cli_chat_shell/chat_state.py +1 -1
  67. janito/cli_chat_shell/chat_ui.py +2 -3
  68. janito/cli_chat_shell/commands/__init__.py +17 -6
  69. janito/cli_chat_shell/commands/{history_reset.py → history_start.py} +13 -5
  70. janito/cli_chat_shell/commands/lang.py +16 -0
  71. janito/cli_chat_shell/commands/prompt.py +42 -0
  72. janito/cli_chat_shell/commands/session_control.py +36 -1
  73. janito/cli_chat_shell/commands/sum.py +49 -0
  74. janito/cli_chat_shell/commands/termweb_log.py +86 -0
  75. janito/cli_chat_shell/commands/utility.py +5 -2
  76. janito/cli_chat_shell/commands/verbose.py +29 -0
  77. janito/cli_chat_shell/load_prompt.py +47 -8
  78. janito/cli_chat_shell/session_manager.py +9 -1
  79. janito/cli_chat_shell/shell_command_completer.py +20 -0
  80. janito/cli_chat_shell/ui.py +110 -93
  81. janito/i18n/__init__.py +35 -0
  82. janito/i18n/messages.py +23 -0
  83. janito/i18n/pt.py +46 -0
  84. janito/rich_utils.py +43 -43
  85. janito/termweb/app.py +95 -0
  86. janito/termweb/static/editor.html +238 -0
  87. janito/termweb/static/editor.html.bak +238 -0
  88. janito/termweb/static/explorer.html.bak +59 -0
  89. janito/termweb/static/favicon.ico +0 -0
  90. janito/termweb/static/favicon.ico.bak +0 -0
  91. janito/termweb/static/index.html +55 -0
  92. janito/termweb/static/index.html.bak +55 -0
  93. janito/termweb/static/index.html.bak.bak +175 -0
  94. janito/termweb/static/landing.html.bak +36 -0
  95. janito/termweb/static/termicon.svg +1 -0
  96. janito/termweb/static/termweb.css +235 -0
  97. janito/termweb/static/termweb.css.bak +286 -0
  98. janito/termweb/static/termweb.js +187 -0
  99. janito/termweb/static/termweb.js.bak +187 -0
  100. janito/termweb/static/termweb.js.bak.bak +157 -0
  101. janito/termweb/static/termweb_quickopen.js +135 -0
  102. janito/termweb/static/termweb_quickopen.js.bak +125 -0
  103. janito/web/app.py +10 -13
  104. {janito-1.6.0.dist-info → janito-1.8.0.dist-info}/METADATA +73 -32
  105. janito-1.8.0.dist-info/RECORD +127 -0
  106. {janito-1.6.0.dist-info → janito-1.8.0.dist-info}/WHEEL +1 -1
  107. janito/agent/tool_registry_core.py +0 -2
  108. janito/agent/tools/get_file_outline.py +0 -117
  109. janito/agent/tools/py_compile_file.py +0 -40
  110. janito/agent/tools/replace_file.py +0 -51
  111. janito/agent/tools/search_files.py +0 -71
  112. janito/cli/runner/scan.py +0 -44
  113. janito/cli_chat_shell/commands/system.py +0 -73
  114. janito-1.6.0.dist-info/RECORD +0 -81
  115. {janito-1.6.0.dist-info → janito-1.8.0.dist-info}/entry_points.txt +0 -0
  116. {janito-1.6.0.dist-info → janito-1.8.0.dist-info}/licenses/LICENSE +0 -0
  117. {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 call(
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("⚠️ Warning: Empty code provided. Operation skipped.")
31
- return "Warning: Empty code provided. Operation skipped."
32
- self.report_info(f"🐍 Running Python code:\n{code}\n")
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
- "⚠️ Warning: This code might be interactive, require user input, and might hang."
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("Do you want to execute this Python code?")
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(f" ❌ Timed out after {timeout} seconds.")
78
- return f"Code timed out after {timeout} seconds."
79
- # Print live output to user
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(f" ✅ return code {return_code}")
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 = "⚠️ Warning: This code might be interactive, require user input, and might hang.\n"
107
- # Read output contents
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
- warning_msg
121
- + f"Return code: {return_code}\n--- STDOUT ---\n{stdout_content}"
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 += f"\n--- STDERR ---\n{stderr_content}"
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
- warning_msg
129
- + f"stdout_file: {stdout_file.name} (lines: {stdout_lines})\n"
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
- f"stderr_file: {stderr_file.name} (lines: {stderr_lines})\n"
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 += f"returncode: {return_code}\nUse the get_lines tool to inspect the contents of these files when needed."
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(f" ❌ Error: {e}")
139
- return f"Error running code: {e}"
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
- def display_path(path):
2
- import os
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
- return path
6
- return os.path.relpath(path)
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
@@ -11,24 +11,23 @@ def expand_path(path: str) -> str:
11
11
  return path
12
12
 
13
13
 
14
- def display_path(original_path: str, expanded_path: str) -> str:
14
+ def display_path(path: str) -> str:
15
15
  """
16
16
  Returns a user-friendly path for display:
17
- - If the original path is relative, return it as-is.
18
- - If the original path starts with ~, keep it as ~.
19
- - Otherwise, if the expanded path is under the home directory, replace the home dir with ~.
20
- - Else, show the expanded path.
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
- original_path.startswith("/")
25
- or original_path.startswith("~")
26
- or (os.name == "nt" and len(original_path) > 1 and original_path[1] == ":")
23
+ path.startswith("/")
24
+ or path.startswith("~")
25
+ or (os.name == "nt" and len(path) > 1 and path[1] == ":")
27
26
  ):
28
- return original_path
27
+ return path
29
28
  home = os.path.expanduser("~")
30
- if original_path.startswith("~"):
31
- return original_path
32
- if expanded_path.startswith(home):
33
- return "~" + expanded_path[len(home) :]
34
- return expanded_path
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
@@ -82,7 +82,7 @@ def print_full_config(
82
82
  Path(__file__).parent
83
83
  / "agent"
84
84
  / "templates"
85
- / "system_prompt_template.j2"
85
+ / "system_prompt_template_default.j2"
86
86
  )
87
87
  for key, value in default_items.items():
88
88
  if key == "system_prompt_template" and value is None: