janito 1.8.1__py3-none-any.whl → 1.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.
- janito/__init__.py +1 -1
- janito/agent/api_exceptions.py +4 -0
- janito/agent/config.py +1 -1
- janito/agent/config_defaults.py +2 -3
- janito/agent/config_utils.py +0 -9
- janito/agent/conversation.py +177 -114
- janito/agent/conversation_api.py +179 -159
- janito/agent/conversation_tool_calls.py +11 -8
- janito/agent/llm_conversation_history.py +70 -0
- janito/agent/openai_client.py +44 -21
- janito/agent/openai_schema_generator.py +164 -128
- janito/agent/platform_discovery.py +134 -77
- janito/agent/profile_manager.py +5 -5
- janito/agent/rich_message_handler.py +80 -31
- janito/agent/templates/profiles/system_prompt_template_base.txt.j2 +9 -8
- janito/agent/test_openai_schema_generator.py +93 -0
- janito/agent/tool_base.py +7 -2
- janito/agent/tool_executor.py +63 -50
- janito/agent/tool_registry.py +5 -2
- janito/agent/tool_use_tracker.py +42 -5
- janito/agent/tools/__init__.py +13 -12
- janito/agent/tools/create_directory.py +9 -6
- janito/agent/tools/create_file.py +35 -54
- janito/agent/tools/delete_text_in_file.py +97 -0
- janito/agent/tools/fetch_url.py +50 -5
- janito/agent/tools/find_files.py +40 -26
- janito/agent/tools/get_file_outline/__init__.py +1 -0
- janito/agent/tools/{outline_file/__init__.py → get_file_outline/core.py} +14 -18
- janito/agent/tools/get_file_outline/python_outline.py +134 -0
- janito/agent/tools/{search_outline.py → get_file_outline/search_outline.py} +11 -0
- janito/agent/tools/get_lines.py +21 -12
- janito/agent/tools/move_file.py +13 -12
- janito/agent/tools/present_choices.py +3 -1
- janito/agent/tools/python_command_runner.py +150 -0
- janito/agent/tools/python_file_runner.py +148 -0
- janito/agent/tools/python_stdin_runner.py +154 -0
- janito/agent/tools/remove_directory.py +4 -2
- janito/agent/tools/remove_file.py +15 -13
- janito/agent/tools/replace_file.py +72 -0
- janito/agent/tools/replace_text_in_file.py +7 -5
- janito/agent/tools/run_bash_command.py +29 -72
- janito/agent/tools/run_powershell_command.py +142 -102
- janito/agent/tools/search_text.py +177 -131
- janito/agent/tools/validate_file_syntax/__init__.py +1 -0
- janito/agent/tools/validate_file_syntax/core.py +94 -0
- janito/agent/tools/validate_file_syntax/css_validator.py +35 -0
- janito/agent/tools/validate_file_syntax/html_validator.py +77 -0
- janito/agent/tools/validate_file_syntax/js_validator.py +27 -0
- janito/agent/tools/validate_file_syntax/json_validator.py +6 -0
- janito/agent/tools/validate_file_syntax/markdown_validator.py +66 -0
- janito/agent/tools/validate_file_syntax/ps1_validator.py +32 -0
- janito/agent/tools/validate_file_syntax/python_validator.py +5 -0
- janito/agent/tools/validate_file_syntax/xml_validator.py +11 -0
- janito/agent/tools/validate_file_syntax/yaml_validator.py +6 -0
- janito/agent/tools_utils/__init__.py +1 -0
- janito/agent/tools_utils/action_type.py +7 -0
- janito/agent/tools_utils/dir_walk_utils.py +24 -0
- janito/agent/tools_utils/formatting.py +49 -0
- janito/agent/tools_utils/gitignore_utils.py +69 -0
- janito/agent/tools_utils/test_gitignore_utils.py +46 -0
- janito/agent/tools_utils/utils.py +30 -0
- janito/cli/_livereload_log_utils.py +13 -0
- janito/cli/_print_config.py +63 -61
- janito/cli/arg_parser.py +57 -14
- janito/cli/cli_main.py +270 -0
- janito/cli/livereload_starter.py +60 -0
- janito/cli/main.py +166 -99
- janito/cli/one_shot.py +80 -0
- janito/cli/termweb_starter.py +2 -2
- janito/i18n/__init__.py +1 -1
- janito/livereload/app.py +25 -0
- janito/rich_utils.py +41 -25
- janito/{cli_chat_shell → shell}/commands/__init__.py +19 -14
- janito/{cli_chat_shell → shell}/commands/config.py +4 -4
- janito/shell/commands/conversation_restart.py +74 -0
- janito/shell/commands/edit.py +24 -0
- janito/shell/commands/history_view.py +18 -0
- janito/{cli_chat_shell → shell}/commands/lang.py +3 -0
- janito/shell/commands/livelogs.py +42 -0
- janito/{cli_chat_shell → shell}/commands/prompt.py +16 -6
- janito/shell/commands/session.py +35 -0
- janito/{cli_chat_shell → shell}/commands/session_control.py +3 -5
- janito/{cli_chat_shell → shell}/commands/termweb_log.py +18 -10
- janito/shell/commands/tools.py +26 -0
- janito/shell/commands/track.py +36 -0
- janito/shell/commands/utility.py +28 -0
- janito/{cli_chat_shell → shell}/commands/verbose.py +4 -5
- janito/shell/commands.py +40 -0
- janito/shell/input_history.py +62 -0
- janito/shell/main.py +257 -0
- janito/{cli_chat_shell/shell_command_completer.py → shell/prompt/completer.py} +1 -1
- janito/{cli_chat_shell/chat_ui.py → shell/prompt/session_setup.py} +19 -5
- janito/shell/session/manager.py +101 -0
- janito/{cli_chat_shell/ui.py → shell/ui/interactive.py} +23 -17
- janito/termweb/app.py +3 -3
- janito/termweb/static/editor.css +142 -0
- janito/termweb/static/editor.css.bak +27 -0
- janito/termweb/static/editor.html +15 -213
- janito/termweb/static/editor.html.bak +16 -215
- janito/termweb/static/editor.js +209 -0
- janito/termweb/static/editor.js.bak +227 -0
- janito/termweb/static/index.html +2 -3
- janito/termweb/static/index.html.bak +2 -3
- janito/termweb/static/termweb.css.bak +33 -84
- janito/termweb/static/termweb.js +15 -34
- janito/termweb/static/termweb.js.bak +18 -36
- janito/tests/test_rich_utils.py +44 -0
- janito/web/app.py +0 -75
- {janito-1.8.1.dist-info → janito-1.10.0.dist-info}/METADATA +62 -42
- janito-1.10.0.dist-info/RECORD +158 -0
- {janito-1.8.1.dist-info → janito-1.10.0.dist-info}/WHEEL +1 -1
- janito/agent/tools/dir_walk_utils.py +0 -16
- janito/agent/tools/gitignore_utils.py +0 -46
- janito/agent/tools/memory.py +0 -48
- janito/agent/tools/outline_file/formatting.py +0 -20
- janito/agent/tools/outline_file/python_outline.py +0 -71
- janito/agent/tools/present_choices_test.py +0 -18
- janito/agent/tools/rich_live.py +0 -44
- janito/agent/tools/run_python_command.py +0 -163
- janito/agent/tools/tools_utils.py +0 -56
- janito/agent/tools/utils.py +0 -33
- janito/agent/tools/validate_file_syntax.py +0 -163
- janito/cli/runner/cli_main.py +0 -180
- janito/cli_chat_shell/chat_loop.py +0 -163
- janito/cli_chat_shell/chat_state.py +0 -38
- janito/cli_chat_shell/commands/history_start.py +0 -37
- janito/cli_chat_shell/commands/session.py +0 -48
- janito/cli_chat_shell/commands/sum.py +0 -49
- janito/cli_chat_shell/commands/utility.py +0 -32
- janito/cli_chat_shell/session_manager.py +0 -72
- janito-1.8.1.dist-info/RECORD +0 -127
- /janito/agent/tools/{outline_file → get_file_outline}/markdown_outline.py +0 -0
- /janito/cli/{runner/_termweb_log_utils.py → _termweb_log_utils.py} +0 -0
- /janito/cli/{runner/config.py → config_runner.py} +0 -0
- /janito/cli/{runner/formatting.py → formatting_runner.py} +0 -0
- /janito/{cli/runner → shell}/__init__.py +0 -0
- /janito/{cli_chat_shell → shell/prompt}/load_prompt.py +0 -0
- /janito/{cli_chat_shell/config_shell.py → shell/session/config.py} +0 -0
- /janito/{cli_chat_shell/__init__.py → shell/session/history.py} +0 -0
- {janito-1.8.1.dist-info → janito-1.10.0.dist-info}/entry_points.txt +0 -0
- {janito-1.8.1.dist-info → janito-1.10.0.dist-info}/licenses/LICENSE +0 -0
- {janito-1.8.1.dist-info → janito-1.10.0.dist-info}/top_level.txt +0 -0
@@ -1,78 +1,59 @@
|
|
1
1
|
import os
|
2
|
-
import shutil
|
3
2
|
from janito.agent.tool_registry import register_tool
|
4
|
-
|
3
|
+
|
4
|
+
# from janito.agent.tools_utils.expand_path import expand_path
|
5
|
+
from janito.agent.tools_utils.utils import display_path
|
5
6
|
from janito.agent.tool_base import ToolBase
|
7
|
+
from janito.agent.tools_utils.action_type import ActionType
|
6
8
|
from janito.i18n import tr
|
7
9
|
|
8
10
|
|
11
|
+
from janito.agent.tools.validate_file_syntax.core import validate_file_syntax
|
12
|
+
|
13
|
+
|
9
14
|
@register_tool(name="create_file")
|
10
15
|
class CreateFileTool(ToolBase):
|
11
16
|
"""
|
12
|
-
Create a new file with the given content
|
17
|
+
Create a new file with the given content.
|
13
18
|
Args:
|
14
|
-
file_path (str): Path to the file to create
|
19
|
+
file_path (str): Path to the file to create.
|
15
20
|
content (str): Content to write to the file.
|
16
|
-
overwrite (bool, optional): If True, overwrite the file if it exists. Defaults to False.
|
17
|
-
CRITICAL: If you use overwrite=True, you MUST provide the full content for the file. Using placeholders or partial content will result in file corruption. Before overwriting, read the full original file.
|
18
21
|
Returns:
|
19
22
|
str: Status message indicating the result. Example:
|
20
23
|
- "✅ Successfully created the file at ..."
|
24
|
+
|
25
|
+
Note: Syntax validation is automatically performed after this operation.
|
21
26
|
"""
|
22
27
|
|
23
|
-
def run(self, file_path: str, content: str
|
24
|
-
expanded_file_path =
|
28
|
+
def run(self, file_path: str, content: str) -> str:
|
29
|
+
expanded_file_path = file_path # Using file_path as is
|
25
30
|
disp_path = display_path(expanded_file_path)
|
26
31
|
file_path = expanded_file_path
|
27
|
-
backup_path = None
|
28
32
|
if os.path.exists(file_path):
|
29
|
-
if not overwrite:
|
30
|
-
return tr(
|
31
|
-
"⚠️ File already exists at '{disp_path}'. Use overwrite=True to overwrite.",
|
32
|
-
disp_path=disp_path,
|
33
|
-
)
|
34
|
-
# Check ToolUseTracker for full read before overwrite
|
35
33
|
try:
|
36
|
-
|
37
|
-
|
38
|
-
tracker = ToolUseTracker()
|
39
|
-
if not tracker.file_fully_read(file_path):
|
40
|
-
self.report_error(
|
41
|
-
"❌ Refusing to overwrite file: full file has not been read with get_lines."
|
42
|
-
)
|
43
|
-
return tr(
|
44
|
-
"❌ Refusing to overwrite file: full file has not been read with get_lines."
|
45
|
-
)
|
34
|
+
with open(file_path, "r", encoding="utf-8", errors="replace") as f:
|
35
|
+
existing_content = f.read()
|
46
36
|
except Exception as e:
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
tr("📝 Updating file: '{disp_path}' ...", disp_path=disp_path)
|
53
|
-
)
|
54
|
-
mode = "w"
|
55
|
-
updated = True
|
56
|
-
else:
|
57
|
-
dir_name = os.path.dirname(file_path)
|
58
|
-
if dir_name:
|
59
|
-
os.makedirs(dir_name, exist_ok=True)
|
60
|
-
self.report_info(
|
61
|
-
tr("📝 Creating file: '{disp_path}' ...", disp_path=disp_path)
|
37
|
+
existing_content = f"[Error reading file: {e}]"
|
38
|
+
return tr(
|
39
|
+
"❗ Cannot create file: file already exists at '{disp_path}'.\n--- Current file content ---\n{existing_content}",
|
40
|
+
disp_path=disp_path,
|
41
|
+
existing_content=existing_content,
|
62
42
|
)
|
63
|
-
|
64
|
-
|
65
|
-
|
43
|
+
dir_name = os.path.dirname(file_path)
|
44
|
+
if dir_name:
|
45
|
+
os.makedirs(dir_name, exist_ok=True)
|
46
|
+
self.report_info(
|
47
|
+
ActionType.WRITE,
|
48
|
+
tr("📝 Creating file '{disp_path}' ...", disp_path=disp_path),
|
49
|
+
)
|
50
|
+
with open(file_path, "w", encoding="utf-8", errors="replace") as f:
|
66
51
|
f.write(content)
|
67
52
|
new_lines = content.count("\n") + 1 if content else 0
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
return msg
|
76
|
-
else:
|
77
|
-
self.report_success(tr("✅ ({new_lines} lines).", new_lines=new_lines))
|
78
|
-
return tr("✅ Created file ({new_lines} lines).", new_lines=new_lines)
|
53
|
+
self.report_success(tr("✅ {new_lines} lines", new_lines=new_lines))
|
54
|
+
# Perform syntax validation and append result
|
55
|
+
validation_result = validate_file_syntax(file_path)
|
56
|
+
return (
|
57
|
+
tr("✅ Created file {new_lines} lines.", new_lines=new_lines)
|
58
|
+
+ f"\n{validation_result}"
|
59
|
+
)
|
@@ -0,0 +1,97 @@
|
|
1
|
+
from janito.agent.tool_base import ToolBase
|
2
|
+
from janito.agent.tool_registry import register_tool
|
3
|
+
from janito.i18n import tr
|
4
|
+
|
5
|
+
|
6
|
+
@register_tool(name="delete_text_in_file")
|
7
|
+
class DeleteTextInFileTool(ToolBase):
|
8
|
+
"""
|
9
|
+
Delete all occurrences of text between start_marker and end_marker (inclusive) in a file, using exact string markers.
|
10
|
+
|
11
|
+
Args:
|
12
|
+
file_path (str): Path to the file to modify.
|
13
|
+
start_marker (str): The starting delimiter string.
|
14
|
+
end_marker (str): The ending delimiter string.
|
15
|
+
backup (bool, optional): If True, create a backup (.bak) before deleting. Defaults to False.
|
16
|
+
Returns:
|
17
|
+
str: Status message indicating the result.
|
18
|
+
"""
|
19
|
+
|
20
|
+
def run(
|
21
|
+
self,
|
22
|
+
file_path: str,
|
23
|
+
start_marker: str,
|
24
|
+
end_marker: str,
|
25
|
+
backup: bool = False,
|
26
|
+
) -> str:
|
27
|
+
import shutil
|
28
|
+
from janito.agent.tools_utils.utils import display_path
|
29
|
+
|
30
|
+
disp_path = display_path(file_path)
|
31
|
+
backup_path = file_path + ".bak"
|
32
|
+
backup_msg = ""
|
33
|
+
try:
|
34
|
+
with open(file_path, "r", encoding="utf-8", errors="replace") as f:
|
35
|
+
content = f.read()
|
36
|
+
except Exception as e:
|
37
|
+
self.report_error(tr(" ❌ Error reading file: {error}", error=e))
|
38
|
+
return tr("Error reading file: {error}", error=e)
|
39
|
+
|
40
|
+
start_count = content.count(start_marker)
|
41
|
+
if start_count > 1:
|
42
|
+
self.report_error("Need more context for start_marker")
|
43
|
+
return (
|
44
|
+
f"Error: start_marker is not unique in {disp_path}. "
|
45
|
+
"Try including the next line(s) for more context."
|
46
|
+
)
|
47
|
+
|
48
|
+
end_count = content.count(end_marker)
|
49
|
+
if end_count > 1:
|
50
|
+
self.report_error("Need more context for end_marker")
|
51
|
+
return (
|
52
|
+
f"Error: end_marker is not unique in {disp_path}. "
|
53
|
+
"Try including the previous line(s) for more context."
|
54
|
+
)
|
55
|
+
|
56
|
+
count = 0
|
57
|
+
new_content = content
|
58
|
+
while True:
|
59
|
+
start_idx = new_content.find(start_marker)
|
60
|
+
if start_idx == -1:
|
61
|
+
break
|
62
|
+
end_idx = new_content.find(end_marker, start_idx + len(start_marker))
|
63
|
+
if end_idx == -1:
|
64
|
+
break
|
65
|
+
# Remove from start_marker to end_marker (inclusive)
|
66
|
+
new_content = (
|
67
|
+
new_content[:start_idx] + new_content[end_idx + len(end_marker) :]
|
68
|
+
)
|
69
|
+
count += 1
|
70
|
+
|
71
|
+
if count == 0:
|
72
|
+
self.report_warning(tr("ℹ️ No blocks found between markers."))
|
73
|
+
return tr(
|
74
|
+
"No blocks found between markers in {file_path}.", file_path=file_path
|
75
|
+
)
|
76
|
+
|
77
|
+
if backup:
|
78
|
+
shutil.copy2(file_path, backup_path)
|
79
|
+
backup_msg = f" (A backup was saved to {backup_path})"
|
80
|
+
with open(file_path, "w", encoding="utf-8", errors="replace") as f:
|
81
|
+
f.write(new_content)
|
82
|
+
|
83
|
+
self.report_success(
|
84
|
+
tr(
|
85
|
+
"Deleted {count} block(s) between markers in {disp_path}.",
|
86
|
+
count=count,
|
87
|
+
disp_path=disp_path,
|
88
|
+
)
|
89
|
+
)
|
90
|
+
return (
|
91
|
+
tr(
|
92
|
+
"Deleted {count} block(s) between markers in {file_path}.",
|
93
|
+
count=count,
|
94
|
+
file_path=file_path,
|
95
|
+
)
|
96
|
+
+ backup_msg
|
97
|
+
)
|
janito/agent/tools/fetch_url.py
CHANGED
@@ -2,7 +2,9 @@ import requests
|
|
2
2
|
from bs4 import BeautifulSoup
|
3
3
|
from janito.agent.tool_registry import register_tool
|
4
4
|
from janito.agent.tool_base import ToolBase
|
5
|
+
from janito.agent.tools_utils.action_type import ActionType
|
5
6
|
from janito.i18n import tr
|
7
|
+
from janito.agent.tools_utils.utils import pluralize
|
6
8
|
|
7
9
|
|
8
10
|
@register_tool(name="fetch_url")
|
@@ -21,11 +23,47 @@ class FetchUrlTool(ToolBase):
|
|
21
23
|
|
22
24
|
def run(self, url: str, search_strings: list[str] = None) -> str:
|
23
25
|
if not url.strip():
|
24
|
-
self.report_warning(tr("
|
26
|
+
self.report_warning(tr("ℹ️ Empty URL provided."))
|
25
27
|
return tr("Warning: Empty URL provided. Operation skipped.")
|
26
|
-
self.report_info(tr("🌐 Fetching URL
|
27
|
-
|
28
|
-
|
28
|
+
self.report_info(ActionType.READ, tr("🌐 Fetching URL '{url}' ...", url=url))
|
29
|
+
try:
|
30
|
+
response = requests.get(url, timeout=10)
|
31
|
+
response.raise_for_status()
|
32
|
+
except requests.exceptions.HTTPError as http_err:
|
33
|
+
status_code = http_err.response.status_code if http_err.response else None
|
34
|
+
if status_code and 400 <= status_code < 500:
|
35
|
+
self.report_error(
|
36
|
+
tr(
|
37
|
+
"❗ HTTP {status_code} error for URL: {url}",
|
38
|
+
status_code=status_code,
|
39
|
+
url=url,
|
40
|
+
)
|
41
|
+
)
|
42
|
+
return tr(
|
43
|
+
"Warning: HTTP {status_code} error for URL: {url}",
|
44
|
+
status_code=status_code,
|
45
|
+
url=url,
|
46
|
+
)
|
47
|
+
else:
|
48
|
+
self.report_error(
|
49
|
+
tr(
|
50
|
+
"❗ HTTP error for URL: {url}: {err}",
|
51
|
+
url=url,
|
52
|
+
err=str(http_err),
|
53
|
+
)
|
54
|
+
)
|
55
|
+
return tr(
|
56
|
+
"Warning: HTTP error for URL: {url}: {err}",
|
57
|
+
url=url,
|
58
|
+
err=str(http_err),
|
59
|
+
)
|
60
|
+
except Exception as err:
|
61
|
+
self.report_error(
|
62
|
+
tr("❗ Error fetching URL: {url}: {err}", url=url, err=str(err))
|
63
|
+
)
|
64
|
+
return tr(
|
65
|
+
"Warning: Error fetching URL: {url}: {err}", url=url, err=str(err)
|
66
|
+
)
|
29
67
|
self.update_progress(
|
30
68
|
{
|
31
69
|
"event": "progress",
|
@@ -49,5 +87,12 @@ class FetchUrlTool(ToolBase):
|
|
49
87
|
text = "\n...\n".join(filtered)
|
50
88
|
else:
|
51
89
|
text = tr("No lines found for the provided search strings.")
|
52
|
-
|
90
|
+
num_lines = len(text.splitlines())
|
91
|
+
self.report_success(
|
92
|
+
tr(
|
93
|
+
"✅ {num_lines} {line_word}",
|
94
|
+
num_lines=num_lines,
|
95
|
+
line_word=pluralize("line", num_lines),
|
96
|
+
)
|
97
|
+
)
|
53
98
|
return text
|
janito/agent/tools/find_files.py
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
from janito.agent.tool_base import ToolBase
|
2
|
+
from janito.agent.tools_utils.action_type import ActionType
|
2
3
|
from janito.agent.tool_registry import register_tool
|
3
|
-
from janito.agent.
|
4
|
-
from janito.agent.
|
4
|
+
from janito.agent.tools_utils.utils import pluralize, display_path
|
5
|
+
from janito.agent.tools_utils.dir_walk_utils import walk_dir_with_gitignore
|
5
6
|
from janito.i18n import tr
|
6
7
|
import fnmatch
|
7
8
|
import os
|
@@ -10,11 +11,12 @@ import os
|
|
10
11
|
@register_tool(name="find_files")
|
11
12
|
class FindFilesTool(ToolBase):
|
12
13
|
"""
|
13
|
-
Find files in one or more directories matching a pattern. Respects .gitignore.
|
14
|
+
Find files or directories in one or more directories matching a pattern. Respects .gitignore.
|
14
15
|
Args:
|
15
16
|
paths (str): String of one or more paths (space-separated) to search in. Each path can be a directory.
|
16
17
|
pattern (str): File pattern(s) to match. Multiple patterns can be separated by spaces. Uses Unix shell-style wildcards (fnmatch), e.g. '*.py', 'data_??.csv', '[a-z]*.txt'.
|
17
|
-
|
18
|
+
- If the pattern ends with '/' or '\\', only matching directory names (with trailing slash) are returned, not the files within those directories. For example, pattern '*/' will return only directories at the specified depth.
|
19
|
+
max_depth (int, optional): Maximum directory depth to search. If None, unlimited recursion. If 0, only the top-level directory. If 1, only the root directory (matches 'find . -maxdepth 1').
|
18
20
|
max_results (int, optional): Maximum number of results to return. 0 means no limit (default).
|
19
21
|
Returns:
|
20
22
|
str: Newline-separated list of matching file paths. Example:
|
@@ -23,47 +25,59 @@ class FindFilesTool(ToolBase):
|
|
23
25
|
If max_results is reached, appends a note to the output.
|
24
26
|
"""
|
25
27
|
|
26
|
-
def run(self, paths: str, pattern: str, max_depth: int =
|
28
|
+
def run(self, paths: str, pattern: str, max_depth: int = None) -> str:
|
27
29
|
if not pattern:
|
28
|
-
self.report_warning(
|
29
|
-
tr("⚠️ Warning: Empty file pattern provided. Operation skipped.")
|
30
|
-
)
|
30
|
+
self.report_warning(tr("ℹ️ Empty file pattern provided."))
|
31
31
|
return tr("Warning: Empty file pattern provided. Operation skipped.")
|
32
|
-
output = set()
|
33
32
|
patterns = pattern.split()
|
33
|
+
results = []
|
34
34
|
for directory in paths.split():
|
35
35
|
disp_path = display_path(directory)
|
36
36
|
depth_msg = (
|
37
37
|
tr(" (max depth: {max_depth})", max_depth=max_depth)
|
38
|
-
if max_depth > 0
|
38
|
+
if max_depth is not None and max_depth > 0
|
39
39
|
else ""
|
40
40
|
)
|
41
41
|
self.report_info(
|
42
|
+
ActionType.READ,
|
42
43
|
tr(
|
43
44
|
"🔍 Searching for files '{pattern}' in '{disp_path}'{depth_msg} ...",
|
44
45
|
pattern=pattern,
|
45
46
|
disp_path=disp_path,
|
46
47
|
depth_msg=depth_msg,
|
47
|
-
)
|
48
|
+
),
|
48
49
|
)
|
50
|
+
dir_output = set()
|
49
51
|
for root, dirs, files in walk_dir_with_gitignore(
|
50
52
|
directory, max_depth=max_depth
|
51
53
|
):
|
52
54
|
for pat in patterns:
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
55
|
+
# Directory matching: pattern ends with '/' or '\'
|
56
|
+
if pat.endswith("/") or pat.endswith("\\"):
|
57
|
+
dir_pat = pat.rstrip("/\\")
|
58
|
+
for d in dirs:
|
59
|
+
if fnmatch.fnmatch(d, dir_pat):
|
60
|
+
dir_output.add(os.path.join(root, d) + os.sep)
|
61
|
+
else:
|
62
|
+
# Match files
|
63
|
+
for filename in fnmatch.filter(files, pat):
|
64
|
+
dir_output.add(os.path.join(root, filename))
|
65
|
+
# Also match directories (without trailing slash)
|
66
|
+
for d in fnmatch.filter(dirs, pat):
|
67
|
+
dir_output.add(os.path.join(root, d))
|
68
|
+
self.report_success(
|
69
|
+
tr(
|
70
|
+
" ✅ {count} {file_word}",
|
71
|
+
count=len(dir_output),
|
72
|
+
file_word=pluralize("file", len(dir_output)),
|
73
|
+
)
|
60
74
|
)
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
result = "\n".join(
|
75
|
+
# If searching in '.', strip leading './' from results
|
76
|
+
if directory.strip() == ".":
|
77
|
+
dir_output = {
|
78
|
+
p[2:] if (p.startswith("./") or p.startswith(".\\")) else p
|
79
|
+
for p in dir_output
|
80
|
+
}
|
81
|
+
results.extend(sorted(dir_output))
|
82
|
+
result = "\n".join(results)
|
69
83
|
return result
|
@@ -0,0 +1 @@
|
|
1
|
+
# Outline tools and parsers package
|
@@ -1,14 +1,15 @@
|
|
1
1
|
from janito.agent.tool_registry import register_tool
|
2
2
|
from .python_outline import parse_python_outline
|
3
3
|
from .markdown_outline import parse_markdown_outline
|
4
|
-
from .formatting import
|
4
|
+
from janito.agent.tools_utils.formatting import OutlineFormatter
|
5
5
|
import os
|
6
6
|
from janito.agent.tool_base import ToolBase
|
7
|
-
from janito.agent.
|
7
|
+
from janito.agent.tools_utils.action_type import ActionType
|
8
|
+
from janito.agent.tools_utils.utils import display_path, pluralize
|
8
9
|
from janito.i18n import tr
|
9
10
|
|
10
11
|
|
11
|
-
@register_tool(name="
|
12
|
+
@register_tool(name="get_file_outline")
|
12
13
|
class GetFileOutlineTool(ToolBase):
|
13
14
|
"""
|
14
15
|
Get an outline of a file's structure. Supports Python and Markdown files.
|
@@ -20,10 +21,11 @@ class GetFileOutlineTool(ToolBase):
|
|
20
21
|
def run(self, file_path: str) -> str:
|
21
22
|
try:
|
22
23
|
self.report_info(
|
24
|
+
ActionType.READ,
|
23
25
|
tr(
|
24
|
-
"📄 Outlining file
|
26
|
+
"📄 Outlining file '{disp_path}' ...",
|
25
27
|
disp_path=display_path(file_path),
|
26
|
-
)
|
28
|
+
),
|
27
29
|
)
|
28
30
|
ext = os.path.splitext(file_path)[1].lower()
|
29
31
|
with open(file_path, "r", encoding="utf-8", errors="replace") as f:
|
@@ -31,12 +33,12 @@ class GetFileOutlineTool(ToolBase):
|
|
31
33
|
if ext == ".py":
|
32
34
|
outline_items = parse_python_outline(lines)
|
33
35
|
outline_type = "python"
|
34
|
-
table = format_outline_table(outline_items)
|
36
|
+
table = OutlineFormatter.format_outline_table(outline_items)
|
35
37
|
self.report_success(
|
36
38
|
tr(
|
37
|
-
"✅ {count}
|
39
|
+
"✅ Outlined {count} {item_word}",
|
38
40
|
count=len(outline_items),
|
39
|
-
|
41
|
+
item_word=pluralize("item", len(outline_items)),
|
40
42
|
)
|
41
43
|
)
|
42
44
|
return (
|
@@ -50,12 +52,12 @@ class GetFileOutlineTool(ToolBase):
|
|
50
52
|
elif ext == ".md":
|
51
53
|
outline_items = parse_markdown_outline(lines)
|
52
54
|
outline_type = "markdown"
|
53
|
-
table = format_markdown_outline_table(outline_items)
|
55
|
+
table = OutlineFormatter.format_markdown_outline_table(outline_items)
|
54
56
|
self.report_success(
|
55
57
|
tr(
|
56
|
-
"✅ {count}
|
58
|
+
"✅ Outlined {count} {item_word}",
|
57
59
|
count=len(outline_items),
|
58
|
-
|
60
|
+
item_word=pluralize("item", len(outline_items)),
|
59
61
|
)
|
60
62
|
)
|
61
63
|
return (
|
@@ -68,13 +70,7 @@ class GetFileOutlineTool(ToolBase):
|
|
68
70
|
)
|
69
71
|
else:
|
70
72
|
outline_type = "default"
|
71
|
-
self.report_success(
|
72
|
-
tr(
|
73
|
-
"✅ {count} lines ({outline_type})",
|
74
|
-
count=len(lines),
|
75
|
-
outline_type=outline_type,
|
76
|
-
)
|
77
|
-
)
|
73
|
+
self.report_success(tr("✅ Outlined {count} items", count=len(lines)))
|
78
74
|
return tr(
|
79
75
|
"Outline: {count} lines ({outline_type})\nFile has {count} lines.",
|
80
76
|
count=len(lines),
|
@@ -0,0 +1,134 @@
|
|
1
|
+
import re
|
2
|
+
from typing import List
|
3
|
+
|
4
|
+
|
5
|
+
def parse_python_outline(lines: List[str]):
|
6
|
+
class_pat = re.compile(r"^(\s*)class\s+(\w+)")
|
7
|
+
func_pat = re.compile(r"^(\s*)def\s+(\w+)")
|
8
|
+
assign_pat = re.compile(r"^(\s*)([A-Za-z_][A-Za-z0-9_]*)\s*=.*")
|
9
|
+
main_pat = re.compile(r"^\s*if\s+__name__\s*==\s*[\'\"]__main__[\'\"]\s*:")
|
10
|
+
outline = []
|
11
|
+
stack = [] # (type, name, indent, start, parent)
|
12
|
+
obj_ranges = [] # (type, name, start, end, parent, indent)
|
13
|
+
last_top_obj = None
|
14
|
+
for idx, line in enumerate(lines):
|
15
|
+
class_match = class_pat.match(line)
|
16
|
+
func_match = func_pat.match(line)
|
17
|
+
assign_match = assign_pat.match(line)
|
18
|
+
indent = len(line) - len(line.lstrip())
|
19
|
+
# If a new top-level class or function starts, close the previous one
|
20
|
+
if (class_match or func_match) and indent == 0 and last_top_obj:
|
21
|
+
# Only close if still open
|
22
|
+
if last_top_obj in stack:
|
23
|
+
stack.remove(last_top_obj)
|
24
|
+
obj_ranges.append(
|
25
|
+
(
|
26
|
+
last_top_obj[0],
|
27
|
+
last_top_obj[1],
|
28
|
+
last_top_obj[3],
|
29
|
+
idx,
|
30
|
+
last_top_obj[4],
|
31
|
+
last_top_obj[2],
|
32
|
+
)
|
33
|
+
)
|
34
|
+
last_top_obj = None
|
35
|
+
if class_match:
|
36
|
+
name = class_match.group(2)
|
37
|
+
parent = stack[-1][1] if stack and stack[-1][0] == "class" else ""
|
38
|
+
obj = ("class", name, indent, idx + 1, parent)
|
39
|
+
stack.append(obj)
|
40
|
+
if indent == 0:
|
41
|
+
last_top_obj = obj
|
42
|
+
elif func_match:
|
43
|
+
name = func_match.group(2)
|
44
|
+
parent = ""
|
45
|
+
for s in reversed(stack):
|
46
|
+
if s[0] == "class" and indent > s[2]:
|
47
|
+
parent = s[1]
|
48
|
+
break
|
49
|
+
obj = ("function", name, indent, idx + 1, parent)
|
50
|
+
stack.append(obj)
|
51
|
+
if indent == 0:
|
52
|
+
last_top_obj = obj
|
53
|
+
elif assign_match and indent == 0:
|
54
|
+
var_name = assign_match.group(2)
|
55
|
+
var_type = "const" if var_name.isupper() else "var"
|
56
|
+
outline.append(
|
57
|
+
{
|
58
|
+
"type": var_type,
|
59
|
+
"name": var_name,
|
60
|
+
"start": idx + 1,
|
61
|
+
"end": idx + 1,
|
62
|
+
"parent": "",
|
63
|
+
"docstring": "",
|
64
|
+
}
|
65
|
+
)
|
66
|
+
main_match = main_pat.match(line)
|
67
|
+
if main_match:
|
68
|
+
outline.append(
|
69
|
+
{
|
70
|
+
"type": "main",
|
71
|
+
"name": "__main__",
|
72
|
+
"start": idx + 1,
|
73
|
+
"end": idx + 1,
|
74
|
+
"parent": "",
|
75
|
+
"docstring": "",
|
76
|
+
}
|
77
|
+
)
|
78
|
+
while stack and indent < stack[-1][2]:
|
79
|
+
popped = stack.pop()
|
80
|
+
obj_ranges.append(
|
81
|
+
(popped[0], popped[1], popped[3], idx, popped[4], popped[2])
|
82
|
+
)
|
83
|
+
# Close any remaining open objects
|
84
|
+
for popped in stack:
|
85
|
+
obj_ranges.append(
|
86
|
+
(popped[0], popped[1], popped[3], len(lines), popped[4], popped[2])
|
87
|
+
)
|
88
|
+
|
89
|
+
# Now, extract docstrings for classes, functions, and methods
|
90
|
+
for obj in obj_ranges:
|
91
|
+
obj_type, name, start, end, parent, indent = obj
|
92
|
+
# Determine if this is a method
|
93
|
+
if obj_type == "function" and parent:
|
94
|
+
outline_type = "method"
|
95
|
+
elif obj_type == "function":
|
96
|
+
outline_type = "function"
|
97
|
+
else:
|
98
|
+
outline_type = obj_type
|
99
|
+
docstring = extract_docstring(lines, start, end)
|
100
|
+
outline.append(
|
101
|
+
{
|
102
|
+
"type": outline_type,
|
103
|
+
"name": name,
|
104
|
+
"start": start,
|
105
|
+
"end": end,
|
106
|
+
"parent": parent,
|
107
|
+
"docstring": docstring,
|
108
|
+
}
|
109
|
+
)
|
110
|
+
return outline
|
111
|
+
|
112
|
+
|
113
|
+
def extract_docstring(lines, start_idx, end_idx):
|
114
|
+
"""Extracts a docstring from lines[start_idx:end_idx] if present."""
|
115
|
+
for i in range(start_idx, min(end_idx, len(lines))):
|
116
|
+
line = lines[i].lstrip()
|
117
|
+
if not line:
|
118
|
+
continue
|
119
|
+
if line.startswith('"""') or line.startswith("'''"):
|
120
|
+
quote = line[:3]
|
121
|
+
doc = line[3:]
|
122
|
+
if doc.strip().endswith(quote):
|
123
|
+
return doc.strip()[:-3].strip()
|
124
|
+
docstring_lines = [doc]
|
125
|
+
for j in range(i + 1, min(end_idx, len(lines))):
|
126
|
+
line = lines[j]
|
127
|
+
if line.strip().endswith(quote):
|
128
|
+
docstring_lines.append(line.strip()[:-3])
|
129
|
+
return "\n".join([d.strip() for d in docstring_lines]).strip()
|
130
|
+
docstring_lines.append(line)
|
131
|
+
break
|
132
|
+
else:
|
133
|
+
break
|
134
|
+
return ""
|
@@ -1,4 +1,5 @@
|
|
1
1
|
from janito.agent.tool_base import ToolBase
|
2
|
+
from janito.agent.tools_utils.action_type import ActionType
|
2
3
|
from janito.agent.tool_registry import register_tool
|
3
4
|
|
4
5
|
|
@@ -9,6 +10,16 @@ class SearchOutlineTool(ToolBase):
|
|
9
10
|
"""
|
10
11
|
|
11
12
|
def run(self, file_path: str) -> str:
|
13
|
+
from janito.agent.tools_utils.utils import display_path
|
14
|
+
from janito.i18n import tr
|
15
|
+
|
16
|
+
self.report_info(
|
17
|
+
ActionType.READ,
|
18
|
+
tr(
|
19
|
+
"🔍 Searching for outline in '{disp_path}'",
|
20
|
+
disp_path=display_path(file_path),
|
21
|
+
),
|
22
|
+
)
|
12
23
|
# ... rest of implementation ...
|
13
24
|
# Example warnings and successes:
|
14
25
|
# self.report_warning(tr("No files found with supported extensions."))
|