janito 1.6.0__py3-none-any.whl → 1.7.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 +2 -2
- janito/agent/config_defaults.py +1 -0
- janito/agent/conversation.py +4 -1
- janito/agent/openai_client.py +2 -0
- janito/agent/platform_discovery.py +90 -0
- janito/agent/profile_manager.py +83 -91
- janito/agent/rich_message_handler.py +72 -64
- janito/agent/templates/profiles/system_prompt_template_base.toml +76 -0
- janito/agent/templates/profiles/system_prompt_template_default.toml +3 -0
- janito/agent/templates/profiles/system_prompt_template_technical.toml +13 -0
- janito/agent/tests/test_prompt_toml.py +61 -0
- janito/agent/tools/__init__.py +4 -0
- janito/agent/tools/ask_user.py +8 -2
- janito/agent/tools/create_directory.py +27 -10
- janito/agent/tools/find_files.py +2 -10
- janito/agent/tools/get_file_outline.py +29 -0
- janito/agent/tools/get_lines.py +9 -10
- janito/agent/tools/memory.py +68 -0
- janito/agent/tools/run_bash_command.py +79 -60
- janito/agent/tools/run_powershell_command.py +153 -0
- janito/agent/tools/run_python_command.py +4 -0
- janito/agent/tools/search_files.py +0 -6
- janito/cli/_print_config.py +1 -1
- janito/cli/config_commands.py +1 -1
- janito/cli/main.py +1 -1
- janito/cli/runner/__init__.py +0 -2
- janito/cli/runner/cli_main.py +3 -13
- janito/cli/runner/config.py +4 -2
- janito/cli/runner/scan.py +22 -9
- janito/cli_chat_shell/chat_loop.py +13 -9
- janito/cli_chat_shell/chat_ui.py +2 -2
- janito/cli_chat_shell/commands/__init__.py +2 -0
- janito/cli_chat_shell/commands/sum.py +49 -0
- janito/cli_chat_shell/load_prompt.py +47 -8
- janito/cli_chat_shell/ui.py +8 -2
- janito/web/app.py +6 -9
- {janito-1.6.0.dist-info → janito-1.7.0.dist-info}/METADATA +17 -9
- {janito-1.6.0.dist-info → janito-1.7.0.dist-info}/RECORD +43 -35
- {janito-1.6.0.dist-info → janito-1.7.0.dist-info}/WHEEL +0 -0
- {janito-1.6.0.dist-info → janito-1.7.0.dist-info}/entry_points.txt +0 -0
- {janito-1.6.0.dist-info → janito-1.7.0.dist-info}/licenses/LICENSE +0 -0
- {janito-1.6.0.dist-info → janito-1.7.0.dist-info}/top_level.txt +0 -0
janito/agent/tools/__init__.py
CHANGED
@@ -13,10 +13,12 @@ from . import remove_file
|
|
13
13
|
from . import replace_text_in_file
|
14
14
|
from . import rich_live
|
15
15
|
from . import run_bash_command
|
16
|
+
from . import run_powershell_command
|
16
17
|
from . import run_python_command
|
17
18
|
from . import search_files
|
18
19
|
from . import tools_utils
|
19
20
|
from . import replace_file
|
21
|
+
from . import memory
|
20
22
|
|
21
23
|
__all__ = [
|
22
24
|
"ask_user",
|
@@ -34,8 +36,10 @@ __all__ = [
|
|
34
36
|
"replace_text_in_file",
|
35
37
|
"rich_live",
|
36
38
|
"run_bash_command",
|
39
|
+
"run_powershell_command",
|
37
40
|
"run_python_command",
|
38
41
|
"search_files",
|
39
42
|
"tools_utils",
|
40
43
|
"replace_file",
|
44
|
+
"memory",
|
41
45
|
]
|
janito/agent/tools/ask_user.py
CHANGED
@@ -34,12 +34,18 @@ class AskUserTool(ToolBase):
|
|
34
34
|
def _(event):
|
35
35
|
pass
|
36
36
|
|
37
|
+
# F12 instruction rotation
|
38
|
+
_f12_instructions = ["proceed", "go ahead", "continue", "next", "okay"]
|
39
|
+
_f12_index = {"value": 0}
|
40
|
+
|
37
41
|
@bindings.add("f12")
|
38
42
|
def _(event):
|
39
|
-
"""When F12 is pressed,
|
43
|
+
"""When F12 is pressed, rotate through a set of short instructions."""
|
40
44
|
buf = event.app.current_buffer
|
41
|
-
|
45
|
+
idx = _f12_index["value"]
|
46
|
+
buf.text = _f12_instructions[idx]
|
42
47
|
buf.validate_and_handle()
|
48
|
+
_f12_index["value"] = (idx + 1) % len(_f12_instructions)
|
43
49
|
|
44
50
|
style = Style.from_dict(
|
45
51
|
{
|
@@ -1,6 +1,8 @@
|
|
1
1
|
from janito.agent.tool_registry import register_tool
|
2
2
|
from janito.agent.tools.utils import expand_path, display_path
|
3
3
|
from janito.agent.tool_base import ToolBase
|
4
|
+
import os
|
5
|
+
import shutil
|
4
6
|
|
5
7
|
|
6
8
|
@register_tool(name="create_directory")
|
@@ -21,13 +23,28 @@ class CreateDirectoryTool(ToolBase):
|
|
21
23
|
original_path = path
|
22
24
|
path = expand_path(path)
|
23
25
|
disp_path = display_path(original_path, path)
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
26
|
+
self.report_info(
|
27
|
+
f"\U0001f4c1 Creating directory: '{disp_path}' (overwrite={overwrite}) ... "
|
28
|
+
)
|
29
|
+
try:
|
30
|
+
if os.path.exists(path):
|
31
|
+
if not os.path.isdir(path):
|
32
|
+
self.report_error(
|
33
|
+
f"\u274c Path '{disp_path}' exists and is not a directory."
|
34
|
+
)
|
35
|
+
return f"\u274c Path '{disp_path}' exists and is not a directory."
|
36
|
+
if not overwrite:
|
37
|
+
self.report_error(
|
38
|
+
f"\u2757 Directory '{disp_path}' already exists (overwrite=False)"
|
39
|
+
)
|
40
|
+
return (
|
41
|
+
f"\u2757 Cannot create directory: '{disp_path}' already exists."
|
42
|
+
)
|
43
|
+
# Overwrite: remove existing directory
|
44
|
+
shutil.rmtree(path)
|
45
|
+
os.makedirs(path, exist_ok=True)
|
46
|
+
self.report_success(f"\u2705 Directory created at '{disp_path}'")
|
47
|
+
return f"\u2705 Successfully created the directory at '{disp_path}'."
|
48
|
+
except Exception as e:
|
49
|
+
self.report_error(f"\u274c Error creating directory '{disp_path}': {e}")
|
50
|
+
return f"\u274c Cannot create directory: {e}"
|
janito/agent/tools/find_files.py
CHANGED
@@ -14,8 +14,7 @@ class FindFilesTool(ToolBase):
|
|
14
14
|
Args:
|
15
15
|
directories (list[str]): List of directories to search in.
|
16
16
|
pattern (str): File pattern to match. Uses Unix shell-style wildcards (fnmatch), e.g. '*.py', 'data_??.csv', '[a-z]*.txt'.
|
17
|
-
recursive (bool, optional): Whether to search recursively in subdirectories. Defaults to
|
18
|
-
max_depth (int, optional): Maximum directory depth to search (0 = only top-level). If None, unlimited. Defaults to None.
|
17
|
+
recursive (bool, optional): Whether to search recursively in subdirectories. Defaults to True.
|
19
18
|
Returns:
|
20
19
|
str: Newline-separated list of matching file paths. Example:
|
21
20
|
"/path/to/file1.py\n/path/to/file2.py"
|
@@ -26,8 +25,7 @@ class FindFilesTool(ToolBase):
|
|
26
25
|
self,
|
27
26
|
directories: list[str],
|
28
27
|
pattern: str,
|
29
|
-
recursive: bool =
|
30
|
-
max_depth: int = None,
|
28
|
+
recursive: bool = True,
|
31
29
|
) -> str:
|
32
30
|
import os
|
33
31
|
|
@@ -43,15 +41,9 @@ class FindFilesTool(ToolBase):
|
|
43
41
|
disp_path = display_path(directory)
|
44
42
|
self.report_info(f"🔍 Searching for files '{pattern}' in '{disp_path}'")
|
45
43
|
for root, dirs, files in os.walk(directory):
|
46
|
-
# Calculate depth
|
47
44
|
rel_path = os.path.relpath(root, directory)
|
48
45
|
depth = 0 if rel_path == "." else rel_path.count(os.sep) + 1
|
49
|
-
if max_depth is not None and depth > max_depth:
|
50
|
-
# Prune traversal
|
51
|
-
dirs[:] = []
|
52
|
-
continue
|
53
46
|
if not recursive and depth > 0:
|
54
|
-
# Only top-level if not recursive
|
55
47
|
break
|
56
48
|
dirs, files = filter_ignored(root, dirs, files)
|
57
49
|
for filename in fnmatch.filter(files, pattern):
|
@@ -39,6 +39,12 @@ class GetFileOutlineTool(ToolBase):
|
|
39
39
|
table = self._format_outline_table(outline_items)
|
40
40
|
self.report_success(f"✅ {len(outline_items)} items ({outline_type})")
|
41
41
|
return f"Outline: {len(outline_items)} items ({outline_type})\n" + table
|
42
|
+
elif ext == ".md":
|
43
|
+
outline_items = self._parse_markdown_outline(lines)
|
44
|
+
outline_type = "markdown"
|
45
|
+
table = self._format_markdown_outline_table(outline_items)
|
46
|
+
self.report_success(f"✅ {len(outline_items)} items ({outline_type})")
|
47
|
+
return f"Outline: {len(outline_items)} items ({outline_type})\n" + table
|
42
48
|
else:
|
43
49
|
outline_type = "default"
|
44
50
|
self.report_success(f"✅ {len(lines)} lines ({outline_type})")
|
@@ -105,6 +111,29 @@ class GetFileOutlineTool(ToolBase):
|
|
105
111
|
)
|
106
112
|
return outline
|
107
113
|
|
114
|
+
def _parse_markdown_outline(self, lines: List[str]):
|
115
|
+
# Extract Markdown headers (e.g., #, ##, ###)
|
116
|
+
header_pat = re.compile(r"^(#+)\s+(.*)")
|
117
|
+
outline = []
|
118
|
+
for idx, line in enumerate(lines):
|
119
|
+
match = header_pat.match(line)
|
120
|
+
if match:
|
121
|
+
level = len(match.group(1))
|
122
|
+
title = match.group(2).strip()
|
123
|
+
outline.append({"level": level, "title": title, "line": idx + 1})
|
124
|
+
return outline
|
125
|
+
|
126
|
+
def _format_markdown_outline_table(self, outline_items):
|
127
|
+
if not outline_items:
|
128
|
+
return "No headers found."
|
129
|
+
header = "| Level | Header | Line |\n|-------|----------------------------------|------|"
|
130
|
+
rows = []
|
131
|
+
for item in outline_items:
|
132
|
+
rows.append(
|
133
|
+
f"| {item['level']:<5} | {item['title']:<32} | {item['line']:<4} |"
|
134
|
+
)
|
135
|
+
return header + "\n" + "\n".join(rows)
|
136
|
+
|
108
137
|
def _format_outline_table(self, outline_items):
|
109
138
|
if not outline_items:
|
110
139
|
return "No classes or functions found."
|
janito/agent/tools/get_lines.py
CHANGED
@@ -37,32 +37,31 @@ class GetLinesTool(ToolBase):
|
|
37
37
|
]
|
38
38
|
selected_len = len(selected)
|
39
39
|
total_lines = len(lines)
|
40
|
+
at_end = False
|
40
41
|
if from_line and to_line:
|
41
42
|
requested = to_line - from_line + 1
|
42
|
-
if selected_len < requested:
|
43
|
+
if to_line >= total_lines or selected_len < requested:
|
44
|
+
at_end = True
|
45
|
+
if at_end:
|
43
46
|
self.report_success(
|
44
|
-
f" ✅ {selected_len} {pluralize('line', selected_len)} (end
|
47
|
+
f" ✅ {selected_len} {pluralize('line', selected_len)} (end)"
|
45
48
|
)
|
46
49
|
elif to_line < total_lines:
|
47
50
|
self.report_success(
|
48
51
|
f" ✅ {selected_len} {pluralize('line', selected_len)} ({total_lines - to_line} lines to end)"
|
49
52
|
)
|
50
|
-
else:
|
51
|
-
self.report_success(
|
52
|
-
f" ✅ {selected_len} {pluralize('line', selected_len)} (end at line {total_lines})"
|
53
|
-
)
|
54
53
|
else:
|
55
54
|
self.report_success(
|
56
55
|
f" ✅ {selected_len} {pluralize('line', selected_len)} (full file)"
|
57
56
|
)
|
58
57
|
# Prepare header
|
59
58
|
if from_line and to_line:
|
60
|
-
|
61
|
-
|
62
|
-
|
59
|
+
if to_line >= total_lines or selected_len < (to_line - from_line + 1):
|
60
|
+
header = f"---\nFile: {disp_path} | Lines: {from_line}-{to_line} (end)\n---\n"
|
61
|
+
else:
|
62
|
+
header = f"---\nFile: {disp_path} | Lines: {from_line}-{to_line} (of {total_lines})\n---\n"
|
63
63
|
elif from_line:
|
64
64
|
header = f"---\nFile: {disp_path} | Lines: {from_line}-END (of {total_lines})\n---\n"
|
65
|
-
header = f"---\nFile: {disp_path} | Lines: {from_line}-END (end at line {total_lines})\n---\n"
|
66
65
|
else:
|
67
66
|
header = (
|
68
67
|
f"---\nFile: {disp_path} | All lines (total: {total_lines})\n---\n"
|
@@ -0,0 +1,68 @@
|
|
1
|
+
"""
|
2
|
+
In-memory memory tools for storing and retrieving reusable information during an agent session.
|
3
|
+
These tools allow the agent to remember and recall arbitrary key-value pairs for the duration of the process.
|
4
|
+
"""
|
5
|
+
|
6
|
+
from janito.agent.tool_base import ToolBase
|
7
|
+
from janito.agent.tool_registry import register_tool
|
8
|
+
|
9
|
+
# Simple in-memory store (process-local, not persistent)
|
10
|
+
_memory_store = {}
|
11
|
+
|
12
|
+
|
13
|
+
@register_tool(name="store_memory")
|
14
|
+
class StoreMemoryTool(ToolBase):
|
15
|
+
"""
|
16
|
+
Store a value for later retrieval using a key. Use this tool to remember information that may be useful in future steps or requests.
|
17
|
+
|
18
|
+
Args:
|
19
|
+
key (str): The identifier for the value to store.
|
20
|
+
value (str): The value to store for later retrieval.
|
21
|
+
Returns:
|
22
|
+
str: Status message indicating success or error. Example:
|
23
|
+
- "✅ Stored value for key: 'foo'"
|
24
|
+
- "❗ Error storing value: ..."
|
25
|
+
"""
|
26
|
+
|
27
|
+
def call(self, key: str, value: str) -> str:
|
28
|
+
self.report_info(f"Storing value for key: '{key}'")
|
29
|
+
try:
|
30
|
+
_memory_store[key] = value
|
31
|
+
msg = f"✅ Stored value for key: '{key}'"
|
32
|
+
self.report_success(msg)
|
33
|
+
return msg
|
34
|
+
except Exception as e:
|
35
|
+
msg = f"❗ Error storing value: {e}"
|
36
|
+
self.report_error(msg)
|
37
|
+
return msg
|
38
|
+
|
39
|
+
|
40
|
+
@register_tool(name="retrieve_memory")
|
41
|
+
class RetrieveMemoryTool(ToolBase):
|
42
|
+
"""
|
43
|
+
Retrieve a value previously stored using a key. Use this tool to recall information remembered earlier in the session.
|
44
|
+
|
45
|
+
Args:
|
46
|
+
key (str): The identifier for the value to retrieve.
|
47
|
+
Returns:
|
48
|
+
str: The stored value, or a warning message if not found. Example:
|
49
|
+
- "🔎 Retrieved value for key: 'foo': bar"
|
50
|
+
- "⚠️ No value found for key: 'notfound'"
|
51
|
+
"""
|
52
|
+
|
53
|
+
def call(self, key: str) -> str:
|
54
|
+
self.report_info(f"Retrieving value for key: '{key}'")
|
55
|
+
try:
|
56
|
+
if key in _memory_store:
|
57
|
+
value = _memory_store[key]
|
58
|
+
msg = f"🔎 Retrieved value for key: '{key}': {value}"
|
59
|
+
self.report_success(msg)
|
60
|
+
return msg
|
61
|
+
else:
|
62
|
+
msg = f"⚠️ No value found for key: '{key}'"
|
63
|
+
self.report_warning(msg)
|
64
|
+
return msg
|
65
|
+
except Exception as e:
|
66
|
+
msg = f"❗ Error retrieving value: {e}"
|
67
|
+
self.report_error(msg)
|
68
|
+
return msg
|
@@ -4,6 +4,7 @@ from janito.agent.tool_registry import register_tool
|
|
4
4
|
import subprocess
|
5
5
|
import tempfile
|
6
6
|
import sys
|
7
|
+
import os
|
7
8
|
|
8
9
|
|
9
10
|
@register_tool(name="run_bash_command")
|
@@ -33,18 +34,6 @@ class RunBashCommandTool(ToolBase):
|
|
33
34
|
"""
|
34
35
|
Execute a bash command and capture live output.
|
35
36
|
|
36
|
-
Args:
|
37
|
-
command (str): The bash command to execute.
|
38
|
-
timeout (int, optional): Timeout in seconds for the command. Defaults to 60.
|
39
|
-
require_confirmation (bool, optional): If True, require user confirmation before running. Defaults to False.
|
40
|
-
interactive (bool, optional): If True, warns that the command may require user interaction. Defaults to False.
|
41
|
-
|
42
|
-
Returns:
|
43
|
-
str: Output and status message.
|
44
|
-
"""
|
45
|
-
"""
|
46
|
-
Execute a bash command and capture live output.
|
47
|
-
|
48
37
|
Args:
|
49
38
|
command (str): The bash command to execute.
|
50
39
|
timeout (int, optional): Timeout in seconds for the command. Defaults to 60.
|
@@ -62,7 +51,6 @@ class RunBashCommandTool(ToolBase):
|
|
62
51
|
self.report_info(
|
63
52
|
"⚠️ Warning: This command might be interactive, require user input, and might hang."
|
64
53
|
)
|
65
|
-
|
66
54
|
sys.stdout.flush()
|
67
55
|
|
68
56
|
try:
|
@@ -74,81 +62,112 @@ class RunBashCommandTool(ToolBase):
|
|
74
62
|
mode="w+", prefix="run_bash_stderr_", delete=False, encoding="utf-8"
|
75
63
|
) as stderr_file,
|
76
64
|
):
|
77
|
-
|
65
|
+
env = os.environ.copy()
|
66
|
+
env["PYTHONIOENCODING"] = "utf-8"
|
67
|
+
env["LC_ALL"] = "C.UTF-8"
|
68
|
+
env["LANG"] = "C.UTF-8"
|
69
|
+
|
78
70
|
process = subprocess.Popen(
|
79
71
|
["bash", "-c", command],
|
80
|
-
stdout=
|
81
|
-
stderr=
|
72
|
+
stdout=subprocess.PIPE,
|
73
|
+
stderr=subprocess.PIPE,
|
82
74
|
text=True,
|
75
|
+
encoding="utf-8",
|
76
|
+
bufsize=1, # line-buffered
|
77
|
+
env=env,
|
83
78
|
)
|
79
|
+
|
80
|
+
stdout_lines = 0
|
81
|
+
stderr_lines = 0
|
82
|
+
stdout_content = []
|
83
|
+
stderr_content = []
|
84
|
+
max_lines = 100
|
85
|
+
|
86
|
+
import threading
|
87
|
+
|
88
|
+
def stream_reader(
|
89
|
+
stream, file_handle, report_func, content_list, line_counter
|
90
|
+
):
|
91
|
+
for line in iter(stream.readline, ""):
|
92
|
+
file_handle.write(line)
|
93
|
+
file_handle.flush()
|
94
|
+
report_func(line)
|
95
|
+
content_list.append(line)
|
96
|
+
line_counter[0] += 1
|
97
|
+
stream.close()
|
98
|
+
|
99
|
+
stdout_counter = [0]
|
100
|
+
stderr_counter = [0]
|
101
|
+
stdout_thread = threading.Thread(
|
102
|
+
target=stream_reader,
|
103
|
+
args=(
|
104
|
+
process.stdout,
|
105
|
+
stdout_file,
|
106
|
+
self.report_stdout,
|
107
|
+
stdout_content,
|
108
|
+
stdout_counter,
|
109
|
+
),
|
110
|
+
)
|
111
|
+
stderr_thread = threading.Thread(
|
112
|
+
target=stream_reader,
|
113
|
+
args=(
|
114
|
+
process.stderr,
|
115
|
+
stderr_file,
|
116
|
+
self.report_stderr,
|
117
|
+
stderr_content,
|
118
|
+
stderr_counter,
|
119
|
+
),
|
120
|
+
)
|
121
|
+
stdout_thread.start()
|
122
|
+
stderr_thread.start()
|
123
|
+
|
84
124
|
try:
|
85
|
-
|
125
|
+
process.wait(timeout=timeout)
|
86
126
|
except subprocess.TimeoutExpired:
|
87
127
|
process.kill()
|
88
128
|
self.report_error(f" ❌ Timed out after {timeout} seconds.")
|
89
129
|
return f"Command timed out after {timeout} seconds."
|
90
130
|
|
91
|
-
|
92
|
-
|
93
|
-
stderr_file.flush()
|
94
|
-
with open(
|
95
|
-
stdout_file.name, "r", encoding="utf-8", errors="replace"
|
96
|
-
) as out_f:
|
97
|
-
out_f.seek(0)
|
98
|
-
for line in out_f:
|
99
|
-
self.report_stdout(line)
|
100
|
-
with open(
|
101
|
-
stderr_file.name, "r", encoding="utf-8", errors="replace"
|
102
|
-
) as err_f:
|
103
|
-
err_f.seek(0)
|
104
|
-
for line in err_f:
|
105
|
-
self.report_stderr(line)
|
131
|
+
stdout_thread.join()
|
132
|
+
stderr_thread.join()
|
106
133
|
|
107
134
|
# Count lines
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
with open(
|
113
|
-
stderr_file.name, "r", encoding="utf-8", errors="replace"
|
114
|
-
) as err_f:
|
115
|
-
stderr_lines = sum(1 for _ in err_f)
|
116
|
-
|
117
|
-
self.report_success(f" ✅ return code {return_code}")
|
135
|
+
stdout_lines = stdout_counter[0]
|
136
|
+
stderr_lines = stderr_counter[0]
|
137
|
+
|
138
|
+
self.report_success(f" ✅ return code {process.returncode}")
|
118
139
|
warning_msg = ""
|
119
140
|
if interactive:
|
120
141
|
warning_msg = "⚠️ Warning: This command might be interactive, require user input, and might hang.\n"
|
121
142
|
|
122
|
-
# Read output contents
|
123
|
-
with open(
|
124
|
-
stdout_file.name, "r", encoding="utf-8", errors="replace"
|
125
|
-
) as out_f:
|
126
|
-
stdout_content = out_f.read()
|
127
|
-
with open(
|
128
|
-
stderr_file.name, "r", encoding="utf-8", errors="replace"
|
129
|
-
) as err_f:
|
130
|
-
stderr_content = err_f.read()
|
131
|
-
|
132
|
-
# Thresholds
|
133
|
-
max_lines = 100
|
143
|
+
# Read output contents if small
|
134
144
|
if stdout_lines <= max_lines and stderr_lines <= max_lines:
|
145
|
+
# Read files from disk to ensure all content is included
|
146
|
+
with open(
|
147
|
+
stdout_file.name, "r", encoding="utf-8", errors="replace"
|
148
|
+
) as out_f:
|
149
|
+
stdout_content_str = out_f.read()
|
150
|
+
with open(
|
151
|
+
stderr_file.name, "r", encoding="utf-8", errors="replace"
|
152
|
+
) as err_f:
|
153
|
+
stderr_content_str = err_f.read()
|
135
154
|
result = (
|
136
155
|
warning_msg
|
137
|
-
+ f"Return code: {
|
156
|
+
+ f"Return code: {process.returncode}\n--- STDOUT ---\n{stdout_content_str}"
|
138
157
|
)
|
139
|
-
if
|
140
|
-
result += f"\n--- STDERR ---\n{
|
158
|
+
if stderr_content_str.strip():
|
159
|
+
result += f"\n--- STDERR ---\n{stderr_content_str}"
|
141
160
|
return result
|
142
161
|
else:
|
143
162
|
result = (
|
144
163
|
warning_msg
|
145
|
-
+ f"
|
164
|
+
+ f"[LARGE OUTPUT]\nstdout_file: {stdout_file.name} (lines: {stdout_lines})\n"
|
146
165
|
)
|
147
|
-
if stderr_lines > 0
|
166
|
+
if stderr_lines > 0:
|
148
167
|
result += (
|
149
168
|
f"stderr_file: {stderr_file.name} (lines: {stderr_lines})\n"
|
150
169
|
)
|
151
|
-
result += f"returncode: {
|
170
|
+
result += f"returncode: {process.returncode}\nUse the get_lines tool to inspect the contents of these files when needed."
|
152
171
|
return result
|
153
172
|
except Exception as e:
|
154
173
|
self.report_error(f" ❌ Error: {e}")
|
@@ -0,0 +1,153 @@
|
|
1
|
+
from janito.agent.tool_base import ToolBase
|
2
|
+
from janito.agent.tool_registry import register_tool
|
3
|
+
|
4
|
+
import subprocess
|
5
|
+
import tempfile
|
6
|
+
|
7
|
+
|
8
|
+
@register_tool(name="run_powershell_command")
|
9
|
+
class RunPowerShellCommandTool(ToolBase):
|
10
|
+
"""
|
11
|
+
Execute a non-interactive command using the PowerShell shell and capture live output.
|
12
|
+
|
13
|
+
This tool explicitly invokes 'powershell.exe' (on Windows) or 'pwsh' (on other platforms if available).
|
14
|
+
|
15
|
+
All commands are automatically prepended with UTF-8 output encoding:
|
16
|
+
$OutputEncoding = [Console]::OutputEncoding = [System.Text.Encoding]::UTF8;
|
17
|
+
|
18
|
+
For file output, it is recommended to use -Encoding utf8 in your PowerShell commands (e.g., Out-File -Encoding utf8) to ensure correct file encoding.
|
19
|
+
|
20
|
+
Args:
|
21
|
+
command (str): The PowerShell command to execute. This string is passed directly to PowerShell using the --Command argument (not as a script file).
|
22
|
+
timeout (int, optional): Timeout in seconds for the command. Defaults to 60.
|
23
|
+
require_confirmation (bool, optional): If True, require user confirmation before running. Defaults to False.
|
24
|
+
interactive (bool, optional): If True, warns that the command may require user interaction. Defaults to False. Non-interactive commands are preferred for automation and reliability.
|
25
|
+
|
26
|
+
Returns:
|
27
|
+
str: Output and status message, or file paths/line counts if output is large.
|
28
|
+
"""
|
29
|
+
|
30
|
+
def call(
|
31
|
+
self,
|
32
|
+
command: str,
|
33
|
+
timeout: int = 60,
|
34
|
+
require_confirmation: bool = False,
|
35
|
+
interactive: bool = False,
|
36
|
+
) -> str:
|
37
|
+
if not command.strip():
|
38
|
+
self.report_warning("⚠️ Warning: Empty command provided. Operation skipped.")
|
39
|
+
return "Warning: Empty command provided. Operation skipped."
|
40
|
+
# Prepend UTF-8 output encoding
|
41
|
+
encoding_prefix = "$OutputEncoding = [Console]::OutputEncoding = [System.Text.Encoding]::UTF8; "
|
42
|
+
command_with_encoding = encoding_prefix + command
|
43
|
+
self.report_info(f"🖥️ Running PowerShell command: {command}\n")
|
44
|
+
if interactive:
|
45
|
+
self.report_info(
|
46
|
+
"⚠️ Warning: This command might be interactive, require user input, and might hang."
|
47
|
+
)
|
48
|
+
if require_confirmation:
|
49
|
+
confirmed = self.ask_user_confirmation(
|
50
|
+
f"About to run PowerShell command: {command}\nContinue?"
|
51
|
+
)
|
52
|
+
if not confirmed:
|
53
|
+
self.report_warning("Execution cancelled by user.")
|
54
|
+
return "❌ Command execution cancelled by user."
|
55
|
+
from janito.agent.platform_discovery import is_windows
|
56
|
+
|
57
|
+
shell_exe = "powershell.exe" if is_windows() else "pwsh"
|
58
|
+
try:
|
59
|
+
with (
|
60
|
+
tempfile.NamedTemporaryFile(
|
61
|
+
mode="w+",
|
62
|
+
prefix="run_powershell_stdout_",
|
63
|
+
delete=False,
|
64
|
+
encoding="utf-8",
|
65
|
+
) as stdout_file,
|
66
|
+
tempfile.NamedTemporaryFile(
|
67
|
+
mode="w+",
|
68
|
+
prefix="run_powershell_stderr_",
|
69
|
+
delete=False,
|
70
|
+
encoding="utf-8",
|
71
|
+
) as stderr_file,
|
72
|
+
):
|
73
|
+
process = subprocess.Popen(
|
74
|
+
[
|
75
|
+
shell_exe,
|
76
|
+
"-NoProfile",
|
77
|
+
"-ExecutionPolicy",
|
78
|
+
"Bypass",
|
79
|
+
"-Command",
|
80
|
+
command_with_encoding,
|
81
|
+
],
|
82
|
+
stdout=stdout_file,
|
83
|
+
stderr=stderr_file,
|
84
|
+
text=True,
|
85
|
+
)
|
86
|
+
try:
|
87
|
+
return_code = process.wait(timeout=timeout)
|
88
|
+
except subprocess.TimeoutExpired:
|
89
|
+
process.kill()
|
90
|
+
self.report_error(f" ❌ Timed out after {timeout} seconds.")
|
91
|
+
return f"Command timed out after {timeout} seconds."
|
92
|
+
# Print live output to user
|
93
|
+
stdout_file.flush()
|
94
|
+
stderr_file.flush()
|
95
|
+
with open(
|
96
|
+
stdout_file.name, "r", encoding="utf-8", errors="replace"
|
97
|
+
) as out_f:
|
98
|
+
out_f.seek(0)
|
99
|
+
for line in out_f:
|
100
|
+
self.report_stdout(line)
|
101
|
+
with open(
|
102
|
+
stderr_file.name, "r", encoding="utf-8", errors="replace"
|
103
|
+
) as err_f:
|
104
|
+
err_f.seek(0)
|
105
|
+
for line in err_f:
|
106
|
+
self.report_stderr(line)
|
107
|
+
# Count lines
|
108
|
+
with open(
|
109
|
+
stdout_file.name, "r", encoding="utf-8", errors="replace"
|
110
|
+
) as out_f:
|
111
|
+
stdout_lines = sum(1 for _ in out_f)
|
112
|
+
with open(
|
113
|
+
stderr_file.name, "r", encoding="utf-8", errors="replace"
|
114
|
+
) as err_f:
|
115
|
+
stderr_lines = sum(1 for _ in err_f)
|
116
|
+
self.report_success(f" ✅ return code {return_code}")
|
117
|
+
warning_msg = ""
|
118
|
+
if interactive:
|
119
|
+
warning_msg = "⚠️ Warning: This command might be interactive, require user input, and might hang.\n"
|
120
|
+
# Read output contents
|
121
|
+
with open(
|
122
|
+
stdout_file.name, "r", encoding="utf-8", errors="replace"
|
123
|
+
) as out_f:
|
124
|
+
stdout_content = out_f.read()
|
125
|
+
with open(
|
126
|
+
stderr_file.name, "r", encoding="utf-8", errors="replace"
|
127
|
+
) as err_f:
|
128
|
+
stderr_content = err_f.read()
|
129
|
+
# Thresholds
|
130
|
+
max_lines = 100
|
131
|
+
if stdout_lines <= max_lines and stderr_lines <= max_lines:
|
132
|
+
result = (
|
133
|
+
warning_msg
|
134
|
+
+ f"Return code: {return_code}\n--- STDOUT ---\n{stdout_content}"
|
135
|
+
)
|
136
|
+
if stderr_content.strip():
|
137
|
+
result += f"\n--- STDERR ---\n{stderr_content}"
|
138
|
+
return result
|
139
|
+
else:
|
140
|
+
result = (
|
141
|
+
warning_msg
|
142
|
+
+ f"stdout_file: {stdout_file.name} (lines: {stdout_lines})\n"
|
143
|
+
)
|
144
|
+
if stderr_lines > 0 and stderr_content.strip():
|
145
|
+
result += (
|
146
|
+
f"stderr_file: {stderr_file.name} (lines: {stderr_lines})\n"
|
147
|
+
)
|
148
|
+
result += f"returncode: {return_code}\nUse the get_lines tool to inspect the contents of these files when needed."
|
149
|
+
return result
|
150
|
+
except Exception as e:
|
151
|
+
self.report_error(f" ❌ Error: {e}")
|
152
|
+
return f"Error running command: {e}"
|
153
|
+
# No temp script file to clean up anymore
|
@@ -1,6 +1,7 @@
|
|
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
|
6
7
|
|
@@ -64,11 +65,14 @@ class RunPythonCommandTool(ToolBase):
|
|
64
65
|
):
|
65
66
|
code_file.write(code)
|
66
67
|
code_file.flush()
|
68
|
+
env = os.environ.copy()
|
69
|
+
env["PYTHONIOENCODING"] = "utf-8"
|
67
70
|
process = subprocess.Popen(
|
68
71
|
[sys.executable, code_file.name],
|
69
72
|
stdout=stdout_file,
|
70
73
|
stderr=stderr_file,
|
71
74
|
text=True,
|
75
|
+
env=env,
|
72
76
|
)
|
73
77
|
try:
|
74
78
|
return_code = process.wait(timeout=timeout)
|