janito 3.8.0__py3-none-any.whl → 3.10.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 (77) hide show
  1. janito/agent_events.py +75 -0
  2. janito/cli/chat_mode/session.py +1 -0
  3. janito/cli/chat_mode/shell/commands/__init__.py +2 -0
  4. janito/cli/chat_mode/shell/commands/interactive.py +33 -0
  5. janito/cli/chat_mode/toolbar.py +16 -1
  6. janito/cli/cli_commands/list_tools.py +1 -1
  7. janito/cli/core/runner.py +33 -0
  8. janito/cli/main_cli.py +9 -0
  9. janito/cli/prompt_core.py +301 -257
  10. janito/cli/rich_terminal_reporter.py +170 -171
  11. janito/cli/single_shot_mode/handler.py +19 -0
  12. janito/llm/agent.py +65 -0
  13. janito/{tools/adapters/local → plugins/core/filemanager/tools}/validate_file_syntax/core.py +7 -2
  14. janito/plugins/core/filemanager/tools/validate_file_syntax/txt_validator.py +28 -0
  15. janito/plugins/manager.py +1 -1
  16. janito/{tools/adapters → plugins/tools}/local/__init__.py +7 -0
  17. janito/{tools/adapters → plugins/tools}/local/adapter.py +1 -1
  18. janito/{tools/adapters → plugins/tools}/local/ask_user.py +1 -1
  19. janito/{tools/adapters → plugins/tools}/local/copy_file.py +1 -1
  20. janito/{tools/adapters → plugins/tools}/local/create_directory.py +45 -2
  21. janito/{tools/adapters → plugins/tools}/local/create_file.py +10 -6
  22. janito/{tools/adapters → plugins/tools}/local/delete_text_in_file.py +2 -2
  23. janito/{tools/adapters → plugins/tools}/local/fetch_url.py +3 -3
  24. janito/{tools/adapters → plugins/tools}/local/find_files.py +1 -1
  25. janito/{tools/adapters → plugins/tools}/local/get_file_outline/core.py +2 -2
  26. janito/{tools/adapters → plugins/tools}/local/move_file.py +1 -1
  27. janito/{tools/adapters → plugins/tools}/local/open_html_in_browser.py +1 -1
  28. janito/{tools/adapters → plugins/tools}/local/open_url.py +1 -1
  29. janito/{tools/adapters → plugins/tools}/local/python_code_run.py +1 -1
  30. janito/{tools/adapters → plugins/tools}/local/python_command_run.py +1 -1
  31. janito/{tools/adapters → plugins/tools}/local/python_file_run.py +1 -1
  32. janito/{tools/adapters → plugins/tools}/local/read_chart.py +1 -1
  33. janito/{tools/adapters → plugins/tools}/local/read_files.py +1 -1
  34. janito/{tools/adapters → plugins/tools}/local/remove_directory.py +1 -1
  35. janito/{tools/adapters → plugins/tools}/local/remove_file.py +1 -1
  36. janito/{tools/adapters → plugins/tools}/local/replace_text_in_file.py +2 -2
  37. janito/{tools/adapters → plugins/tools}/local/run_bash_command.py +1 -1
  38. janito/{tools/adapters → plugins/tools}/local/run_powershell_command.py +1 -1
  39. janito/{tools/adapters → plugins/tools}/local/search_text/core.py +2 -2
  40. janito/{tools/adapters → plugins/tools}/local/show_image.py +1 -1
  41. janito/{tools/adapters → plugins/tools}/local/show_image_grid.py +1 -1
  42. janito/plugins/tools/local/validate_file_syntax/__init__.py +1 -0
  43. janito/plugins/tools/local/validate_file_syntax/core.py +119 -0
  44. janito/plugins/tools/local/validate_file_syntax/txt_validator.py +28 -0
  45. janito/{tools/adapters → plugins/tools}/local/view_file.py +1 -1
  46. janito/tests/test_tool_adapter_case_insensitive.py +112 -0
  47. janito/tools/__init__.py +2 -2
  48. janito/tools/inspect_registry.py +1 -1
  49. janito/tools/tool_base.py +8 -1
  50. janito/tools/tools_adapter.py +514 -510
  51. {janito-3.8.0.dist-info → janito-3.10.0.dist-info}/METADATA +84 -84
  52. {janito-3.8.0.dist-info → janito-3.10.0.dist-info}/RECORD +77 -70
  53. /janito/{tools/adapters/local → plugins/core/filemanager/tools}/validate_file_syntax/__init__.py +0 -0
  54. /janito/{tools/adapters → plugins/tools}/__init__.py +0 -0
  55. /janito/{tools/adapters → plugins/tools}/local/get_file_outline/__init__.py +0 -0
  56. /janito/{tools/adapters → plugins/tools}/local/get_file_outline/java_outline.py +0 -0
  57. /janito/{tools/adapters → plugins/tools}/local/get_file_outline/markdown_outline.py +0 -0
  58. /janito/{tools/adapters → plugins/tools}/local/get_file_outline/python_outline.py +0 -0
  59. /janito/{tools/adapters → plugins/tools}/local/get_file_outline/search_outline.py +0 -0
  60. /janito/{tools/adapters → plugins/tools}/local/search_text/__init__.py +0 -0
  61. /janito/{tools/adapters → plugins/tools}/local/search_text/match_lines.py +0 -0
  62. /janito/{tools/adapters → plugins/tools}/local/search_text/pattern_utils.py +0 -0
  63. /janito/{tools/adapters → plugins/tools}/local/search_text/traverse_directory.py +0 -0
  64. /janito/{tools/adapters → plugins/tools}/local/validate_file_syntax/css_validator.py +0 -0
  65. /janito/{tools/adapters → plugins/tools}/local/validate_file_syntax/html_validator.py +0 -0
  66. /janito/{tools/adapters → plugins/tools}/local/validate_file_syntax/jinja2_validator.py +0 -0
  67. /janito/{tools/adapters → plugins/tools}/local/validate_file_syntax/js_validator.py +0 -0
  68. /janito/{tools/adapters → plugins/tools}/local/validate_file_syntax/json_validator.py +0 -0
  69. /janito/{tools/adapters → plugins/tools}/local/validate_file_syntax/markdown_validator.py +0 -0
  70. /janito/{tools/adapters → plugins/tools}/local/validate_file_syntax/ps1_validator.py +0 -0
  71. /janito/{tools/adapters → plugins/tools}/local/validate_file_syntax/python_validator.py +0 -0
  72. /janito/{tools/adapters → plugins/tools}/local/validate_file_syntax/xml_validator.py +0 -0
  73. /janito/{tools/adapters → plugins/tools}/local/validate_file_syntax/yaml_validator.py +0 -0
  74. {janito-3.8.0.dist-info → janito-3.10.0.dist-info}/WHEEL +0 -0
  75. {janito-3.8.0.dist-info → janito-3.10.0.dist-info}/entry_points.txt +0 -0
  76. {janito-3.8.0.dist-info → janito-3.10.0.dist-info}/licenses/LICENSE +0 -0
  77. {janito-3.8.0.dist-info → janito-3.10.0.dist-info}/top_level.txt +0 -0
@@ -1,171 +1,170 @@
1
- from rich.console import Console
2
- from rich.markdown import Markdown
3
- from rich.pretty import Pretty
4
- from rich.panel import Panel
5
- from rich.text import Text
6
- from janito.event_bus.handler import EventHandlerBase
7
- import janito.driver_events as driver_events
8
- from janito.report_events import ReportSubtype, ReportAction
9
- from janito.event_bus.bus import event_bus
10
- from janito.llm import message_parts
11
-
12
-
13
- import sys
14
-
15
-
16
- class RichTerminalReporter(EventHandlerBase):
17
- """
18
- Handles UI rendering for janito events using Rich.
19
-
20
- - For ResponseReceived events, iterates over the 'parts' field and displays each part appropriately:
21
- - TextMessagePart: rendered as Markdown (uses 'content' field)
22
- - Other MessageParts: displayed using Pretty or a suitable Rich representation
23
- - For RequestFinished events, output is printed only if raw mode is enabled (using Pretty formatting).
24
- - Report events (info, success, error, etc.) are always printed with appropriate styling.
25
- """
26
-
27
- def __init__(self, raw_mode=False):
28
- from janito.cli.console import shared_console
29
-
30
- self.console = shared_console
31
- self.raw_mode = raw_mode
32
- import janito.report_events as report_events
33
-
34
- import janito.tools.tool_events as tool_events
35
-
36
- super().__init__(driver_events, report_events, tool_events)
37
- self._waiting_printed = False
38
-
39
- def on_RequestStarted(self, event):
40
- # Print waiting message with provider and model name
41
- provider = None
42
- model = None
43
- if hasattr(event, "payload") and isinstance(event.payload, dict):
44
- provider = event.payload.get("provider_name")
45
- model = event.payload.get("model") or event.payload.get("model_name")
46
- if not provider:
47
- provider = getattr(event, "provider_name", None)
48
- if not provider:
49
- provider = getattr(event, "driver_name", None)
50
- if not provider:
51
- provider = "LLM"
52
- if not model:
53
- model = getattr(event, "model", None)
54
- if not model:
55
- model = getattr(event, "model_name", None)
56
- if not model:
57
- model = "?"
58
- self.console.print(
59
- f"[bold cyan]Waiting for {provider} (model: {model})...[/bold cyan]", end=""
60
- )
61
-
62
- def on_ResponseReceived(self, event):
63
- parts = event.parts if hasattr(event, "parts") else None
64
- if not parts:
65
- self.console.print("[No response parts to display]")
66
- self.console.file.flush()
67
- return
68
- for part in parts:
69
- if isinstance(part, message_parts.TextMessagePart):
70
- self.console.print(Markdown(part.content))
71
- self.console.file.flush()
72
-
73
- def delete_current_line(self):
74
- """
75
- Clears the entire current line in the terminal and returns the cursor to column 1.
76
- """
77
- sys.stdout.write("\033[2K\r")
78
- sys.stdout.flush()
79
-
80
- def on_RequestFinished(self, event):
81
- self.delete_current_line()
82
- self._waiting_printed = False
83
- response = getattr(event, "response", None)
84
- error = getattr(event, "error", None)
85
- exception = getattr(event, "exception", None)
86
-
87
- # Print error and exception if present
88
- if error:
89
- self.console.print(f"[bold red]Error:[/] {error}")
90
- self.console.file.flush()
91
- if exception:
92
- self.console.print(f"[red]Exception:[/] {exception}")
93
- self.console.file.flush()
94
-
95
- if response is not None:
96
- if self.raw_mode:
97
- self.console.print(Pretty(response, expand_all=True))
98
- self.console.file.flush()
99
- # Check for 'code' and 'event' fields in the response
100
- code = None
101
- event_field = None
102
- if isinstance(response, dict):
103
- code = response.get("code")
104
- event_field = response.get("event")
105
- if event_field is not None:
106
- self.console.print(f"[bold yellow]Event:[/] {event_field}")
107
- self.console.file.flush()
108
- # No output if not raw_mode or if response is None
109
-
110
- def on_ToolCallError(self, event):
111
- # Optionally handle tool call errors in a user-friendly way
112
- error = getattr(event, "error", None)
113
- tool = getattr(event, "tool_name", None)
114
- if error and tool:
115
- self.console.print(f"[bold red]Tool Error ({tool}):[/] {error}")
116
- self.console.file.flush()
117
-
118
- def on_ReportEvent(self, event):
119
- # Special handling for security-related report events
120
- subtype = getattr(event, "subtype", None)
121
- msg = getattr(event, "message", None)
122
- action = getattr(event, "action", None)
123
- tool = getattr(event, "tool", None)
124
- context = getattr(event, "context", None)
125
- if (
126
- subtype == ReportSubtype.ERROR
127
- and msg
128
- and "[SECURITY] Path access denied" in msg
129
- ):
130
- # Highlight security errors with a distinct style
131
- self.console.print(
132
- Panel(f"{msg}", title="[red]SECURITY VIOLATION[/red]", style="bold red")
133
- )
134
- self.console.file.flush()
135
- return
136
-
137
- msg = event.message if hasattr(event, "message") else None
138
- subtype = event.subtype if hasattr(event, "subtype") else None
139
- if not msg or not subtype:
140
- return
141
- if subtype == ReportSubtype.ACTION_INFO:
142
- # Use orange for all write/modification actions
143
- modification_actions = (
144
- getattr(ReportAction, "UPDATE", None),
145
- getattr(ReportAction, "WRITE", None),
146
- getattr(ReportAction, "DELETE", None),
147
- getattr(ReportAction, "CREATE", None),
148
- )
149
- style = (
150
- "orange1"
151
- if getattr(event, "action", None) in modification_actions
152
- else "cyan"
153
- )
154
- self.console.print(Text(msg, style=style), end="")
155
- self.console.file.flush()
156
- elif subtype in (
157
- ReportSubtype.SUCCESS,
158
- ReportSubtype.ERROR,
159
- ReportSubtype.WARNING,
160
- ):
161
- self.console.print(msg)
162
- self.console.file.flush()
163
- elif subtype == ReportSubtype.STDOUT:
164
- self.console.print(msg)
165
- self.console.file.flush()
166
- elif subtype == ReportSubtype.STDERR:
167
- self.console.print(Text(msg, style="on red"))
168
- self.console.file.flush()
169
- else:
170
- self.console.print(msg)
171
- self.console.file.flush()
1
+ from rich.console import Console
2
+ from rich.markdown import Markdown
3
+ from rich.pretty import Pretty
4
+ from rich.panel import Panel
5
+ from rich.text import Text
6
+ from janito.event_bus.handler import EventHandlerBase
7
+ import janito.driver_events as driver_events
8
+ from janito.report_events import ReportSubtype, ReportAction
9
+ from janito.event_bus.bus import event_bus
10
+ from janito.llm import message_parts
11
+ import janito.agent_events as agent_events
12
+
13
+
14
+ import sys
15
+
16
+
17
+ class RichTerminalReporter(EventHandlerBase):
18
+ """
19
+ Handles UI rendering for janito events using Rich.
20
+
21
+ - For ResponseReceived events, iterates over the 'parts' field and displays each part appropriately:
22
+ - TextMessagePart: rendered as Markdown (uses 'content' field)
23
+ - Other MessageParts: displayed using Pretty or a suitable Rich representation
24
+ - For RequestFinished events, output is printed only if raw mode is enabled (using Pretty formatting).
25
+ - Report events (info, success, error, etc.) are always printed with appropriate styling.
26
+ """
27
+
28
+ def __init__(self, raw_mode=False):
29
+ from janito.cli.console import shared_console
30
+
31
+ self.console = shared_console
32
+ self.raw_mode = raw_mode
33
+ import janito.report_events as report_events
34
+
35
+ import janito.tools.tool_events as tool_events
36
+
37
+ super().__init__(driver_events, report_events, tool_events, agent_events)
38
+ self._waiting_printed = False
39
+
40
+ def on_RequestStarted(self, event):
41
+ # Print waiting message with provider and model name
42
+ provider = None
43
+ model = None
44
+ if hasattr(event, "payload") and isinstance(event.payload, dict):
45
+ provider = event.payload.get("provider_name")
46
+ model = event.payload.get("model") or event.payload.get("model_name")
47
+ if not provider:
48
+ provider = getattr(event, "provider_name", None)
49
+ if not provider:
50
+ provider = getattr(event, "driver_name", None)
51
+ if not provider:
52
+ provider = "LLM"
53
+ if not model:
54
+ model = getattr(event, "model", None)
55
+ if not model:
56
+ model = getattr(event, "model_name", None)
57
+ if not model:
58
+ model = "?"
59
+ self.console.print(
60
+ f"[bold cyan]Waiting for {provider} (model: {model})...[/bold cyan]", end=""
61
+ )
62
+ self._waiting_printed = True
63
+
64
+ def on_AgentWaitingForResponse(self, event):
65
+ # Agent waiting - set flag but don't print anything
66
+ self._waiting_printed = True
67
+
68
+ def on_ResponseReceived(self, event):
69
+ parts = event.parts if hasattr(event, "parts") else None
70
+ if not parts:
71
+ self.console.print("[No response parts to display]")
72
+ self.console.file.flush()
73
+ return
74
+ for part in parts:
75
+ if isinstance(part, message_parts.TextMessagePart):
76
+ self.console.print(Markdown(part.content))
77
+ self.console.file.flush()
78
+
79
+ def delete_current_line(self):
80
+ """
81
+ Clears the entire current line in the terminal and returns the cursor to column 1.
82
+ """
83
+ # Use raw ANSI escape sequences but write directly to the underlying file
84
+ # to bypass Rich's escaping/interpretation
85
+ if hasattr(self.console, 'file') and hasattr(self.console.file, 'write'):
86
+ self.console.file.write("\r\033[2K")
87
+ self.console.file.flush()
88
+ else:
89
+ # Fallback to sys.stdout if console.file is not available
90
+ import sys
91
+ sys.stdout.write("\r\033[2K")
92
+ sys.stdout.flush()
93
+
94
+ def on_RequestFinished(self, event):
95
+ if self._waiting_printed:
96
+ self.delete_current_line()
97
+ self._waiting_printed = False
98
+
99
+ def on_AgentReceivedResponse(self, event):
100
+ # Clear any waiting message when agent receives response
101
+ if self._waiting_printed:
102
+ self.delete_current_line()
103
+ self._waiting_printed = False
104
+
105
+ def on_ToolCallError(self, event):
106
+ # Optionally handle tool call errors in a user-friendly way
107
+ error = getattr(event, "error", None)
108
+ tool = getattr(event, "tool_name", None)
109
+ if error and tool:
110
+ self.console.print(f"[bold red]Tool Error ({tool}):[/] {error}")
111
+ self.console.file.flush()
112
+
113
+ def on_ReportEvent(self, event):
114
+ # Special handling for security-related report events
115
+ subtype = getattr(event, "subtype", None)
116
+ msg = getattr(event, "message", None)
117
+ action = getattr(event, "action", None)
118
+ tool = getattr(event, "tool", None)
119
+ context = getattr(event, "context", None)
120
+ if (
121
+ subtype == ReportSubtype.ERROR
122
+ and msg
123
+ and "[SECURITY] Path access denied" in msg
124
+ ):
125
+ # Highlight security errors with a distinct style
126
+ self.console.print(
127
+ Panel(f"{msg}", title="[red]SECURITY VIOLATION[/red]", style="bold red")
128
+ )
129
+ self.console.file.flush()
130
+ return
131
+
132
+ msg = event.message if hasattr(event, "message") else None
133
+ subtype = event.subtype if hasattr(event, "subtype") else None
134
+ if not msg or not subtype:
135
+ return
136
+ if subtype == ReportSubtype.ACTION_INFO:
137
+ # Clear any waiting message before showing action info
138
+ if self._waiting_printed:
139
+ self.delete_current_line()
140
+ self._waiting_printed = False
141
+ # Use orange for all write/modification actions
142
+ modification_actions = (
143
+ getattr(ReportAction, "UPDATE", None),
144
+ getattr(ReportAction, "WRITE", None),
145
+ getattr(ReportAction, "DELETE", None),
146
+ getattr(ReportAction, "CREATE", None),
147
+ )
148
+ style = (
149
+ "orange1"
150
+ if getattr(event, "action", None) in modification_actions
151
+ else "cyan"
152
+ )
153
+ self.console.print(Text(msg, style=style), end="")
154
+ self.console.file.flush()
155
+ elif subtype in (
156
+ ReportSubtype.SUCCESS,
157
+ ReportSubtype.ERROR,
158
+ ReportSubtype.WARNING,
159
+ ):
160
+ self.console.print(msg)
161
+ self.console.file.flush()
162
+ elif subtype == ReportSubtype.STDOUT:
163
+ self.console.print(msg)
164
+ self.console.file.flush()
165
+ elif subtype == ReportSubtype.STDERR:
166
+ self.console.print(Text(msg, style="on red"))
167
+ self.console.file.flush()
168
+ else:
169
+ self.console.print(msg)
170
+ self.console.file.flush()
@@ -45,6 +45,25 @@ class PromptHandler:
45
45
  def handle(self) -> None:
46
46
  import traceback
47
47
 
48
+ # Check if interactive mode is requested - if so, switch to chat mode
49
+ if getattr(self.args, "interactive", False):
50
+ from janito.cli.chat_mode.session import ChatSession
51
+ from rich.console import Console
52
+
53
+ console = Console()
54
+ session = ChatSession(
55
+ console,
56
+ self.provider_instance,
57
+ self.llm_driver_config,
58
+ role=self.role,
59
+ args=self.args,
60
+ verbose_tools=getattr(self.args, "verbose_tools", False),
61
+ verbose_agent=getattr(self.args, "verbose_agent", False),
62
+ allowed_permissions=getattr(self, 'allowed_permissions', None),
63
+ )
64
+ session.run()
65
+ return
66
+
48
67
  user_prompt = " ".join(getattr(self.args, "user_prompt", [])).strip()
49
68
  # UTF-8 sanitize user_prompt
50
69
  sanitized = user_prompt
janito/llm/agent.py CHANGED
@@ -4,6 +4,17 @@ from janito.conversation_history import LLMConversationHistory
4
4
  from janito.tools.tools_adapter import ToolsAdapterBase
5
5
  from queue import Queue, Empty
6
6
  from janito.driver_events import RequestStatus
7
+ from janito.agent_events import (
8
+ AgentInitialized,
9
+ AgentChatStarted,
10
+ AgentChatFinished,
11
+ AgentProcessingResponse,
12
+ AgentToolCallStarted,
13
+ AgentToolCallFinished,
14
+ AgentWaitingForResponse,
15
+ AgentReceivedResponse,
16
+ AgentShutdown
17
+ )
7
18
  from typing import Any, Optional, List, Iterator, Union
8
19
  import threading
9
20
  import logging
@@ -53,6 +64,9 @@ class LLMAgent:
53
64
  self._latest_event = None
54
65
  self.verbose_agent = verbose_agent
55
66
  self.driver = None # Will be set by setup_agent if available
67
+
68
+ # Emit agent initialized event
69
+ event_bus.publish(AgentInitialized(agent_name=self.agent_name))
56
70
 
57
71
  def get_provider_name(self):
58
72
  # Try to get provider name from driver, fallback to llm_provider, else '?'
@@ -178,11 +192,17 @@ class LLMAgent:
178
192
  Wait for a single event from the output queue (with timeout), process it, and return the result.
179
193
  This function is intended to be called from the main agent loop, which controls the overall flow.
180
194
  """
195
+ # Emit agent waiting for response event
196
+ event_bus.publish(AgentWaitingForResponse(agent_name=self.agent_name))
197
+
181
198
  if getattr(self, "verbose_agent", False):
182
199
  print("[agent] [DEBUG] Entered _process_next_response")
183
200
  elapsed = 0.0
184
201
  if getattr(self, "verbose_agent", False):
185
202
  print("[agent] [DEBUG] Waiting for event from output_queue...")
203
+ # Show initial wait message
204
+ if getattr(self, "verbose_agent", False):
205
+ print(f"[agent] [DEBUG] Starting to wait for LLM response... (timeout: {max_wait_time}s)")
186
206
  # Let KeyboardInterrupt propagate to caller
187
207
  return self._poll_for_event(poll_timeout, max_wait_time)
188
208
 
@@ -197,7 +217,14 @@ class LLMAgent:
197
217
  print(error_msg)
198
218
  print("[DEBUG] Exiting _process_next_response due to timeout")
199
219
  return None, False
220
+ # Show elapsed time info while waiting
221
+ if getattr(self, "verbose_agent", False):
222
+ print(f"[agent] [DEBUG] Waiting for LLM response... ({elapsed:.1f}s elapsed)")
200
223
  continue
224
+
225
+ # Emit agent received response event
226
+ event_bus.publish(AgentReceivedResponse(agent_name=self.agent_name, response=event))
227
+
201
228
  if getattr(self, "verbose_agent", False):
202
229
  print(f"[agent] [DEBUG] Received event from output_queue: {event}")
203
230
  event_bus.publish(event)
@@ -227,6 +254,10 @@ class LLMAgent:
227
254
  """
228
255
  if getattr(self, "verbose_agent", False):
229
256
  print("[agent] [INFO] Handling ResponseReceived event.")
257
+
258
+ # Emit agent processing response event
259
+ event_bus.publish(AgentProcessingResponse(agent_name=self.agent_name, response=event))
260
+
230
261
  from janito.llm.message_parts import FunctionCallMessagePart
231
262
 
232
263
  # Skip tool processing if no tools adapter is available
@@ -243,6 +274,15 @@ class LLMAgent:
243
274
  print(
244
275
  f"[agent] [DEBUG] Tool call detected: {getattr(part, 'name', repr(part))} with arguments: {getattr(part, 'arguments', None)}"
245
276
  )
277
+
278
+ # Emit agent tool call started event
279
+ event_bus.publish(AgentToolCallStarted(
280
+ agent_name=self.agent_name,
281
+ tool_call_id=getattr(part, 'tool_call_id', None),
282
+ name=getattr(part, 'name', None),
283
+ arguments=getattr(part, 'arguments', None)
284
+ ))
285
+
246
286
  tool_calls.append(part)
247
287
  try:
248
288
  result = self.tools_adapter.execute_function_call_message_part(part)
@@ -251,6 +291,14 @@ class LLMAgent:
251
291
  # instead of letting it propagate to the user
252
292
  result = str(e)
253
293
  tool_results.append(result)
294
+
295
+ # Emit agent tool call finished event
296
+ event_bus.publish(AgentToolCallFinished(
297
+ agent_name=self.agent_name,
298
+ tool_call_id=getattr(part, 'tool_call_id', None),
299
+ name=getattr(part, 'name', None),
300
+ result=result
301
+ ))
254
302
  if tool_calls:
255
303
  # Prepare tool_calls message for assistant
256
304
  tool_calls_list = []
@@ -310,6 +358,14 @@ class LLMAgent:
310
358
  role: str = "user",
311
359
  config=None,
312
360
  ):
361
+ # Emit agent chat started event
362
+ event_bus.publish(AgentChatStarted(
363
+ agent_name=self.agent_name,
364
+ prompt=prompt,
365
+ messages=messages,
366
+ role=role
367
+ ))
368
+
313
369
  self._clear_driver_queues()
314
370
  self._validate_and_update_history(prompt, messages, role)
315
371
  self._ensure_system_prompt()
@@ -333,6 +389,12 @@ class LLMAgent:
333
389
  f"[agent] [DEBUG] Returned from _process_next_response: result={result}, added_tool_results={added_tool_results}"
334
390
  )
335
391
  if self._should_exit_chat_loop(result, added_tool_results):
392
+ # Emit agent chat finished event
393
+ event_bus.publish(AgentChatFinished(
394
+ agent_name=self.agent_name,
395
+ result=result,
396
+ loop_count=loop_count
397
+ ))
336
398
  return result
337
399
  loop_count += 1
338
400
 
@@ -496,6 +558,9 @@ class LLMAgent:
496
558
  :param timeout: Optional timeout in seconds.
497
559
  Handles KeyboardInterrupt gracefully.
498
560
  """
561
+ # Emit agent shutdown event
562
+ event_bus.publish(AgentShutdown(agent_name=self.agent_name))
563
+
499
564
  if (
500
565
  hasattr(self, "driver")
501
566
  and self.driver
@@ -3,9 +3,9 @@ from janito.tools.path_utils import expand_path
3
3
  from janito.i18n import tr
4
4
  from janito.tools.tool_base import ToolBase, ToolPermissions
5
5
  from janito.report_events import ReportAction
6
- from janito.tools.adapters.local.adapter import register_local_tool
6
+ from janito.plugins.tools.local.adapter import register_local_tool
7
7
  from janito.tools.tool_utils import display_path
8
- from janito.tools.adapters.local.adapter import register_local_tool as register_tool
8
+ from janito.plugins.tools.local.adapter import register_local_tool as register_tool
9
9
 
10
10
  from .python_validator import validate_python
11
11
  from .json_validator import validate_json
@@ -17,6 +17,7 @@ from .markdown_validator import validate_markdown
17
17
  from .js_validator import validate_js
18
18
  from .css_validator import validate_css
19
19
  from .jinja2_validator import validate_jinja2
20
+ from .txt_validator import validate_txt
20
21
  from janito.tools.loop_protection_decorator import protect_against_loops
21
22
 
22
23
 
@@ -37,6 +38,8 @@ def _get_validator(ext):
37
38
  ".css": validate_css,
38
39
  ".j2": validate_jinja2,
39
40
  ".jinja2": validate_jinja2,
41
+ ".txt": validate_txt,
42
+ ".text": validate_txt,
40
43
  }
41
44
  return mapping.get(ext)
42
45
 
@@ -79,6 +82,7 @@ class ValidateFileSyntaxTool(ToolBase):
79
82
  - Markdown (.md)
80
83
  - JavaScript (.js)
81
84
  - Jinja2 templates (.j2, .jinja2)
85
+ - Text files (.txt, .text) [UTF-8 validation]
82
86
 
83
87
  Args:
84
88
  path (str): Path to the file to validate.
@@ -86,6 +90,7 @@ class ValidateFileSyntaxTool(ToolBase):
86
90
  str: Validation status message. Example:
87
91
  - "✅ Syntax OK"
88
92
  - "⚠️ Warning: Syntax error: <error message>"
93
+ - "⚠️ Warning: UTF-8 decoding error: <error details>"
89
94
  - "⚠️ Warning: Unsupported file extension: <ext>"
90
95
  """
91
96
 
@@ -0,0 +1,28 @@
1
+ """Text file validator for UTF-8 encoding validation."""
2
+
3
+ import codecs
4
+ from pathlib import Path
5
+
6
+
7
+ def validate_txt(path: str) -> str:
8
+ """
9
+ Validate a text file for UTF-8 encoding issues.
10
+
11
+ Args:
12
+ path (str): Path to the text file to validate
13
+
14
+ Returns:
15
+ str: Validation status message
16
+ - "✅ Syntax OK" if file is valid UTF-8
17
+ - "⚠️ Warning: UTF-8 decoding error: <error details>" if invalid
18
+ """
19
+ try:
20
+ # Try to read the file with UTF-8 encoding
21
+ with codecs.open(path, 'r', encoding='utf-8') as f:
22
+ # Read the entire file to trigger any decoding errors
23
+ f.read()
24
+ return "✅ Syntax OK"
25
+ except UnicodeDecodeError as e:
26
+ return f"⚠️ Warning: UTF-8 decoding error: {e}"
27
+ except Exception as e:
28
+ return f"⚠️ Warning: File read error: {e}"
janito/plugins/manager.py CHANGED
@@ -14,7 +14,7 @@ from .base import Plugin, PluginMetadata
14
14
  from .discovery import discover_plugins
15
15
  from .config import load_plugins_config, get_user_plugins_dir
16
16
  from .builtin import BuiltinPluginRegistry, load_builtin_plugin
17
- from janito.tools.adapters.local import LocalToolsAdapter
17
+ from janito.plugins.tools.local import LocalToolsAdapter
18
18
 
19
19
  logger = logging.getLogger(__name__)
20
20
 
@@ -30,6 +30,7 @@ from .show_image_grid import ShowImageGridTool
30
30
  from janito.tools.tool_base import ToolPermissions
31
31
  import os
32
32
  from janito.tools.permissions import get_global_allowed_permissions
33
+ from janito.platform_discovery import PlatformDiscovery
33
34
 
34
35
  # Singleton tools adapter with all standard tools registered
35
36
  local_tools_adapter = LocalToolsAdapter(workdir=os.getcwd())
@@ -40,6 +41,9 @@ def get_local_tools_adapter(workdir=None):
40
41
 
41
42
 
42
43
  # Register tools
44
+ pd = PlatformDiscovery()
45
+ is_powershell = pd.detect_shell().startswith("PowerShell")
46
+
43
47
  for tool_class in [
44
48
  AskUserTool,
45
49
  CopyFileTool,
@@ -68,6 +72,9 @@ for tool_class in [
68
72
  ShowImageTool,
69
73
  ShowImageGridTool,
70
74
  ]:
75
+ # Skip bash tools when running in PowerShell
76
+ if is_powershell and tool_class.__name__ in ["RunBashCommandTool"]:
77
+ continue
71
78
  local_tools_adapter.register_tool(tool_class)
72
79
 
73
80
  # DEBUG: Print registered tools at startup
@@ -207,7 +207,7 @@ def register_local_tool(tool=None):
207
207
  # Register the tool on a *fresh* adapter instance to avoid circular
208
208
  # import issues during package initialisation. This keeps behaviour
209
209
  # identical to the original implementation while still allowing
210
- # immediate use via the singleton in janito.tools.adapters.local.
210
+ # immediate use via the singleton in janito.plugins.tools.local.
211
211
  LocalToolsAdapter().register_tool(cls)
212
212
  return cls
213
213
 
@@ -1,5 +1,5 @@
1
1
  from janito.tools.tool_base import ToolBase, ToolPermissions
2
- from janito.tools.adapters.local.adapter import register_local_tool
2
+ from janito.plugins.tools.local.adapter import register_local_tool
3
3
  from janito.tools.loop_protection_decorator import protect_against_loops
4
4
 
5
5
  from rich import print as rich_print
@@ -2,7 +2,7 @@ import os
2
2
  from janito.tools.path_utils import expand_path
3
3
  import shutil
4
4
  from typing import List, Union
5
- from janito.tools.adapters.local.adapter import register_local_tool
5
+ from janito.plugins.tools.local.adapter import register_local_tool
6
6
  from janito.tools.tool_base import ToolBase, ToolPermissions
7
7
  from janito.tools.tool_utils import display_path
8
8
  from janito.report_events import ReportAction