janito 1.8.0__py3-none-any.whl → 1.9.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 (119) hide show
  1. janito/__init__.py +1 -1
  2. janito/agent/config_defaults.py +23 -0
  3. janito/agent/config_utils.py +0 -9
  4. janito/agent/conversation.py +31 -9
  5. janito/agent/conversation_api.py +32 -2
  6. janito/agent/conversation_history.py +53 -0
  7. janito/agent/conversation_tool_calls.py +11 -8
  8. janito/agent/openai_client.py +11 -3
  9. janito/agent/openai_schema_generator.py +9 -6
  10. janito/agent/providers.py +77 -0
  11. janito/agent/rich_message_handler.py +1 -1
  12. janito/agent/templates/profiles/system_prompt_template_base.txt.j2 +8 -8
  13. janito/agent/tool_executor.py +18 -10
  14. janito/agent/tool_use_tracker.py +16 -0
  15. janito/agent/tools/__init__.py +7 -9
  16. janito/agent/tools/create_directory.py +7 -6
  17. janito/agent/tools/create_file.py +29 -54
  18. janito/agent/tools/delete_text_in_file.py +97 -0
  19. janito/agent/tools/fetch_url.py +11 -3
  20. janito/agent/tools/find_files.py +37 -25
  21. janito/agent/tools/get_file_outline/__init__.py +1 -0
  22. janito/agent/tools/{outline_file/__init__.py → get_file_outline/core.py} +12 -15
  23. janito/agent/tools/get_file_outline/python_outline.py +134 -0
  24. janito/agent/tools/{search_outline.py → get_file_outline/search_outline.py} +9 -0
  25. janito/agent/tools/get_lines.py +15 -11
  26. janito/agent/tools/move_file.py +10 -11
  27. janito/agent/tools/remove_directory.py +2 -2
  28. janito/agent/tools/remove_file.py +11 -13
  29. janito/agent/tools/replace_file.py +62 -0
  30. janito/agent/tools/replace_text_in_file.py +3 -3
  31. janito/agent/tools/run_bash_command.py +3 -7
  32. janito/agent/tools/run_powershell_command.py +39 -28
  33. janito/agent/tools/run_python_command.py +3 -5
  34. janito/agent/tools/search_text.py +10 -14
  35. janito/agent/tools/validate_file_syntax/__init__.py +1 -0
  36. janito/agent/tools/validate_file_syntax/core.py +92 -0
  37. janito/agent/tools/validate_file_syntax/css_validator.py +35 -0
  38. janito/agent/tools/validate_file_syntax/html_validator.py +77 -0
  39. janito/agent/tools/validate_file_syntax/js_validator.py +27 -0
  40. janito/agent/tools/validate_file_syntax/json_validator.py +6 -0
  41. janito/agent/tools/validate_file_syntax/markdown_validator.py +66 -0
  42. janito/agent/tools/validate_file_syntax/ps1_validator.py +32 -0
  43. janito/agent/tools/validate_file_syntax/python_validator.py +5 -0
  44. janito/agent/tools/validate_file_syntax/xml_validator.py +11 -0
  45. janito/agent/tools/validate_file_syntax/yaml_validator.py +6 -0
  46. janito/agent/tools_utils/__init__.py +1 -0
  47. janito/agent/tools_utils/dir_walk_utils.py +23 -0
  48. janito/agent/{tools/outline_file → tools_utils}/formatting.py +5 -2
  49. janito/agent/{tools → tools_utils}/gitignore_utils.py +0 -3
  50. janito/agent/tools_utils/utils.py +30 -0
  51. janito/cli/_livereload_log_utils.py +13 -0
  52. janito/cli/arg_parser.py +45 -3
  53. janito/cli/{runner/cli_main.py → cli_main.py} +120 -20
  54. janito/cli/livereload_starter.py +60 -0
  55. janito/cli/main.py +110 -21
  56. janito/cli/one_shot.py +66 -0
  57. janito/cli/termweb_starter.py +2 -2
  58. janito/livereload/app.py +25 -0
  59. janito/rich_utils.py +0 -22
  60. janito/{cli_chat_shell → shell}/commands/__init__.py +18 -11
  61. janito/{cli_chat_shell → shell}/commands/config.py +4 -4
  62. janito/shell/commands/conversation_restart.py +72 -0
  63. janito/shell/commands/edit.py +21 -0
  64. janito/shell/commands/history_view.py +18 -0
  65. janito/shell/commands/livelogs.py +40 -0
  66. janito/{cli_chat_shell → shell}/commands/prompt.py +10 -6
  67. janito/shell/commands/session.py +32 -0
  68. janito/{cli_chat_shell → shell}/commands/session_control.py +2 -7
  69. janito/{cli_chat_shell → shell}/commands/sum.py +6 -6
  70. janito/{cli_chat_shell → shell}/commands/termweb_log.py +10 -10
  71. janito/shell/commands/tools.py +23 -0
  72. janito/{cli_chat_shell → shell}/commands/utility.py +5 -4
  73. janito/{cli_chat_shell → shell}/commands/verbose.py +1 -1
  74. janito/shell/commands.py +40 -0
  75. janito/shell/main.py +321 -0
  76. janito/{cli_chat_shell/shell_command_completer.py → shell/prompt/completer.py} +1 -1
  77. janito/{cli_chat_shell/chat_ui.py → shell/prompt/session_setup.py} +19 -5
  78. janito/{cli_chat_shell/session_manager.py → shell/session/manager.py} +53 -3
  79. janito/{cli_chat_shell/ui.py → shell/ui/interactive.py} +23 -15
  80. janito/termweb/app.py +3 -3
  81. janito/termweb/static/editor.css +146 -0
  82. janito/termweb/static/editor.css.bak +27 -0
  83. janito/termweb/static/editor.html +15 -213
  84. janito/termweb/static/editor.html.bak +16 -215
  85. janito/termweb/static/editor.js +209 -0
  86. janito/termweb/static/editor.js.bak +227 -0
  87. janito/termweb/static/index.html +2 -3
  88. janito/termweb/static/index.html.bak +2 -3
  89. janito/termweb/static/termweb.css.bak +33 -84
  90. janito/termweb/static/termweb.js +15 -34
  91. janito/termweb/static/termweb.js.bak +18 -36
  92. {janito-1.8.0.dist-info → janito-1.9.0.dist-info}/METADATA +6 -3
  93. janito-1.9.0.dist-info/RECORD +151 -0
  94. {janito-1.8.0.dist-info → janito-1.9.0.dist-info}/WHEEL +1 -1
  95. janito/agent/tools/dir_walk_utils.py +0 -16
  96. janito/agent/tools/memory.py +0 -48
  97. janito/agent/tools/outline_file/python_outline.py +0 -71
  98. janito/agent/tools/present_choices_test.py +0 -18
  99. janito/agent/tools/rich_live.py +0 -44
  100. janito/agent/tools/tools_utils.py +0 -56
  101. janito/agent/tools/utils.py +0 -33
  102. janito/agent/tools/validate_file_syntax.py +0 -163
  103. janito/cli_chat_shell/chat_loop.py +0 -163
  104. janito/cli_chat_shell/chat_state.py +0 -38
  105. janito/cli_chat_shell/commands/history_start.py +0 -37
  106. janito/cli_chat_shell/commands/session.py +0 -48
  107. janito-1.8.0.dist-info/RECORD +0 -127
  108. /janito/agent/tools/{outline_file → get_file_outline}/markdown_outline.py +0 -0
  109. /janito/cli/{runner/_termweb_log_utils.py → _termweb_log_utils.py} +0 -0
  110. /janito/cli/{runner/config.py → config_runner.py} +0 -0
  111. /janito/cli/{runner/formatting.py → formatting_runner.py} +0 -0
  112. /janito/{cli/runner → shell}/__init__.py +0 -0
  113. /janito/{cli_chat_shell → shell}/commands/lang.py +0 -0
  114. /janito/{cli_chat_shell → shell/prompt}/load_prompt.py +0 -0
  115. /janito/{cli_chat_shell/config_shell.py → shell/session/config.py} +0 -0
  116. /janito/{cli_chat_shell/__init__.py → shell/session/history.py} +0 -0
  117. {janito-1.8.0.dist-info → janito-1.9.0.dist-info}/entry_points.txt +0 -0
  118. {janito-1.8.0.dist-info → janito-1.9.0.dist-info}/licenses/LICENSE +0 -0
  119. {janito-1.8.0.dist-info → janito-1.9.0.dist-info}/top_level.txt +0 -0
@@ -1,78 +1,53 @@
1
1
  import os
2
- import shutil
3
2
  from janito.agent.tool_registry import register_tool
4
- from janito.agent.tools.utils import expand_path, display_path
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
6
7
  from janito.i18n import tr
7
8
 
8
9
 
10
+ from janito.agent.tools.validate_file_syntax.core import validate_file_syntax
11
+
12
+
9
13
  @register_tool(name="create_file")
10
14
  class CreateFileTool(ToolBase):
11
15
  """
12
- Create a new file with the given content, or overwrite if specified.
16
+ Create a new file with the given content.
13
17
  Args:
14
- file_path (str): Path to the file to create or overwrite.
18
+ file_path (str): Path to the file to create.
15
19
  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
20
  Returns:
19
21
  str: Status message indicating the result. Example:
20
22
  - "✅ Successfully created the file at ..."
21
23
  """
22
24
 
23
- def run(self, file_path: str, content: str, overwrite: bool = False) -> str:
24
- expanded_file_path = expand_path(file_path)
25
+ def run(self, file_path: str, content: str) -> str:
26
+ expanded_file_path = file_path # Using file_path as is
25
27
  disp_path = display_path(expanded_file_path)
26
28
  file_path = expanded_file_path
27
- backup_path = None
28
29
  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
30
  try:
36
- from janito.agent.tool_use_tracker import ToolUseTracker
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
- )
31
+ with open(file_path, "r", encoding="utf-8", errors="replace") as f:
32
+ existing_content = f.read()
46
33
  except Exception as e:
47
- self.report_error(f"[ToolUseTracker] Error: {e}")
48
- return tr("[ToolUseTracker] Error: {e}")
49
- backup_path = file_path + ".bak"
50
- shutil.copy2(file_path, backup_path)
51
- self.report_info(
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)
34
+ existing_content = f"[Error reading file: {e}]"
35
+ return tr(
36
+ "❗ Cannot create file: file already exists at '{disp_path}'.\n--- Current file content ---\n{existing_content}",
37
+ disp_path=disp_path,
38
+ existing_content=existing_content,
62
39
  )
63
- mode = "w"
64
- updated = False
65
- with open(file_path, mode, encoding="utf-8", errors="replace") as f:
40
+ dir_name = os.path.dirname(file_path)
41
+ if dir_name:
42
+ os.makedirs(dir_name, exist_ok=True)
43
+ self.report_info(tr("📝 Creating file '{disp_path}' ...", disp_path=disp_path))
44
+ with open(file_path, "w", encoding="utf-8", errors="replace") as f:
66
45
  f.write(content)
67
46
  new_lines = content.count("\n") + 1 if content else 0
68
- if updated:
69
- self.report_success(tr("✅ ({new_lines} lines).", new_lines=new_lines))
70
- msg = tr(
71
- "✅ Updated file ({new_lines} lines, backup at {backup_path}).",
72
- new_lines=new_lines,
73
- backup_path=backup_path,
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)
47
+ self.report_success(tr("✅ {new_lines} lines", new_lines=new_lines))
48
+ # Perform syntax validation and append result
49
+ validation_result = validate_file_syntax(file_path)
50
+ return (
51
+ tr("✅ Created file {new_lines} lines.", new_lines=new_lines)
52
+ + f"\n{validation_result}"
53
+ )
@@ -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
+ )
@@ -3,6 +3,7 @@ from bs4 import BeautifulSoup
3
3
  from janito.agent.tool_registry import register_tool
4
4
  from janito.agent.tool_base import ToolBase
5
5
  from janito.i18n import tr
6
+ from janito.agent.tools_utils.utils import pluralize
6
7
 
7
8
 
8
9
  @register_tool(name="fetch_url")
@@ -21,9 +22,9 @@ class FetchUrlTool(ToolBase):
21
22
 
22
23
  def run(self, url: str, search_strings: list[str] = None) -> str:
23
24
  if not url.strip():
24
- self.report_warning(tr("⚠️ Warning: Empty URL provided. Operation skipped."))
25
+ self.report_warning(tr("ℹ️ Empty URL provided."))
25
26
  return tr("Warning: Empty URL provided. Operation skipped.")
26
- self.report_info(tr("🌐 Fetching URL: {url} ...", url=url))
27
+ self.report_info(tr("🌐 Fetching URL '{url}' ...", url=url))
27
28
  response = requests.get(url, timeout=10)
28
29
  response.raise_for_status()
29
30
  self.update_progress(
@@ -49,5 +50,12 @@ class FetchUrlTool(ToolBase):
49
50
  text = "\n...\n".join(filtered)
50
51
  else:
51
52
  text = tr("No lines found for the provided search strings.")
52
- self.report_success(tr("✅ Result"))
53
+ num_lines = len(text.splitlines())
54
+ self.report_success(
55
+ tr(
56
+ "✅ {num_lines} {line_word}",
57
+ num_lines=num_lines,
58
+ line_word=pluralize("line", num_lines),
59
+ )
60
+ )
53
61
  return text
@@ -1,7 +1,7 @@
1
1
  from janito.agent.tool_base import ToolBase
2
2
  from janito.agent.tool_registry import register_tool
3
- from janito.agent.tools.tools_utils import pluralize, display_path
4
- from janito.agent.tools.dir_walk_utils import walk_dir_with_gitignore
3
+ from janito.agent.tools_utils.utils import pluralize, display_path
4
+ from janito.agent.tools_utils.dir_walk_utils import walk_dir_with_gitignore
5
5
  from janito.i18n import tr
6
6
  import fnmatch
7
7
  import os
@@ -10,11 +10,12 @@ import os
10
10
  @register_tool(name="find_files")
11
11
  class FindFilesTool(ToolBase):
12
12
  """
13
- Find files in one or more directories matching a pattern. Respects .gitignore.
13
+ Find files or directories in one or more directories matching a pattern. Respects .gitignore.
14
14
  Args:
15
15
  paths (str): String of one or more paths (space-separated) to search in. Each path can be a directory.
16
16
  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
- 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).
17
+ - 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.
18
+ 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
19
  max_results (int, optional): Maximum number of results to return. 0 means no limit (default).
19
20
  Returns:
20
21
  str: Newline-separated list of matching file paths. Example:
@@ -23,19 +24,17 @@ class FindFilesTool(ToolBase):
23
24
  If max_results is reached, appends a note to the output.
24
25
  """
25
26
 
26
- def run(self, paths: str, pattern: str, max_depth: int = 0) -> str:
27
+ def run(self, paths: str, pattern: str, max_depth: int = None) -> str:
27
28
  if not pattern:
28
- self.report_warning(
29
- tr("⚠️ Warning: Empty file pattern provided. Operation skipped.")
30
- )
29
+ self.report_warning(tr("ℹ️ Empty file pattern provided."))
31
30
  return tr("Warning: Empty file pattern provided. Operation skipped.")
32
- output = set()
33
31
  patterns = pattern.split()
32
+ results = []
34
33
  for directory in paths.split():
35
34
  disp_path = display_path(directory)
36
35
  depth_msg = (
37
36
  tr(" (max depth: {max_depth})", max_depth=max_depth)
38
- if max_depth > 0
37
+ if max_depth is not None and max_depth > 0
39
38
  else ""
40
39
  )
41
40
  self.report_info(
@@ -46,24 +45,37 @@ class FindFilesTool(ToolBase):
46
45
  depth_msg=depth_msg,
47
46
  )
48
47
  )
48
+ dir_output = set()
49
49
  for root, dirs, files in walk_dir_with_gitignore(
50
50
  directory, max_depth=max_depth
51
51
  ):
52
52
  for pat in patterns:
53
- for filename in fnmatch.filter(files, pat):
54
- output.add(os.path.join(root, filename))
55
- self.report_success(
56
- tr(
57
- " {count} {file_word} found",
58
- count=len(output),
59
- file_word=pluralize("file", len(output)),
53
+ # Directory matching: pattern ends with '/' or '\'
54
+ if pat.endswith("/") or pat.endswith("\\"):
55
+ dir_pat = pat.rstrip("/\\")
56
+ for d in dirs:
57
+ if fnmatch.fnmatch(d, dir_pat):
58
+ dir_output.add(os.path.join(root, d) + os.sep)
59
+ else:
60
+ # Match files
61
+ for filename in fnmatch.filter(files, pat):
62
+ dir_output.add(os.path.join(root, filename))
63
+ # Also match directories (without trailing slash)
64
+ for d in fnmatch.filter(dirs, pat):
65
+ dir_output.add(os.path.join(root, d))
66
+ self.report_success(
67
+ tr(
68
+ " ✅ {count} {file_word}",
69
+ count=len(dir_output),
70
+ file_word=pluralize("file", len(dir_output)),
71
+ )
60
72
  )
61
- )
62
- # If searching in '.', strip leading './' from results
63
- if paths.strip() == ".":
64
- output = {
65
- p[2:] if (p.startswith("./") or p.startswith(".\\")) else p
66
- for p in output
67
- }
68
- result = "\n".join(sorted(output))
73
+ # If searching in '.', strip leading './' from results
74
+ if directory.strip() == ".":
75
+ dir_output = {
76
+ p[2:] if (p.startswith("./") or p.startswith(".\\")) else p
77
+ for p in dir_output
78
+ }
79
+ results.extend(sorted(dir_output))
80
+ result = "\n".join(results)
69
81
  return result
@@ -0,0 +1 @@
1
+ # Outline tools and parsers package
@@ -1,14 +1,17 @@
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 format_outline_table, format_markdown_outline_table
4
+ from janito.agent.tools_utils.formatting import (
5
+ format_outline_table,
6
+ format_markdown_outline_table,
7
+ )
5
8
  import os
6
9
  from janito.agent.tool_base import ToolBase
7
- from janito.agent.tools.tools_utils import display_path
10
+ from janito.agent.tools_utils.utils import display_path, pluralize
8
11
  from janito.i18n import tr
9
12
 
10
13
 
11
- @register_tool(name="outline_file")
14
+ @register_tool(name="get_file_outline")
12
15
  class GetFileOutlineTool(ToolBase):
13
16
  """
14
17
  Get an outline of a file's structure. Supports Python and Markdown files.
@@ -21,7 +24,7 @@ class GetFileOutlineTool(ToolBase):
21
24
  try:
22
25
  self.report_info(
23
26
  tr(
24
- "📄 Outlining file: '{disp_path}' ...",
27
+ "📄 Outlining file '{disp_path}' ...",
25
28
  disp_path=display_path(file_path),
26
29
  )
27
30
  )
@@ -34,9 +37,9 @@ class GetFileOutlineTool(ToolBase):
34
37
  table = format_outline_table(outline_items)
35
38
  self.report_success(
36
39
  tr(
37
- "✅ {count} items ({outline_type})",
40
+ "✅ Outlined {count} {item_word}",
38
41
  count=len(outline_items),
39
- outline_type=outline_type,
42
+ item_word=pluralize("item", len(outline_items)),
40
43
  )
41
44
  )
42
45
  return (
@@ -53,9 +56,9 @@ class GetFileOutlineTool(ToolBase):
53
56
  table = format_markdown_outline_table(outline_items)
54
57
  self.report_success(
55
58
  tr(
56
- "✅ {count} items ({outline_type})",
59
+ "✅ Outlined {count} {item_word}",
57
60
  count=len(outline_items),
58
- outline_type=outline_type,
61
+ item_word=pluralize("item", len(outline_items)),
59
62
  )
60
63
  )
61
64
  return (
@@ -68,13 +71,7 @@ class GetFileOutlineTool(ToolBase):
68
71
  )
69
72
  else:
70
73
  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
- )
74
+ self.report_success(tr("✅ Outlined {count} items", count=len(lines)))
78
75
  return tr(
79
76
  "Outline: {count} lines ({outline_type})\nFile has {count} lines.",
80
77
  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 ""
@@ -9,6 +9,15 @@ class SearchOutlineTool(ToolBase):
9
9
  """
10
10
 
11
11
  def run(self, file_path: str) -> str:
12
+ from janito.agent.tools_utils.utils import display_path
13
+ from janito.i18n import tr
14
+
15
+ self.report_info(
16
+ tr(
17
+ "🔍 Searching for outline in '{disp_path}'",
18
+ disp_path=display_path(file_path),
19
+ )
20
+ )
12
21
  # ... rest of implementation ...
13
22
  # Example warnings and successes:
14
23
  # self.report_warning(tr("No files found with supported extensions."))
@@ -1,40 +1,44 @@
1
1
  from janito.agent.tool_base import ToolBase
2
2
  from janito.agent.tool_registry import register_tool
3
- from janito.agent.tools.tools_utils import pluralize
3
+ from janito.agent.tools_utils.utils import pluralize
4
4
  from janito.i18n import tr
5
5
 
6
6
 
7
7
  @register_tool(name="get_lines")
8
8
  class GetLinesTool(ToolBase):
9
9
  """
10
- Read lines from a file. Returns specific lines if a range is provided, or the entire file if no range is given. If both from_line and to_line are None, the entire file is returned in one call—no need to chunk or split requests when reading the full file.
10
+ Read lines from a file. You can specify a line range, or read the entire file by simply omitting the from_line and to_line parameters.
11
+
11
12
  Args:
12
13
  file_path (str): Path to the file to read lines from.
13
- from_line (int, optional): Starting line number (1-based). If None, starts from the first line.
14
- to_line (int, optional): Ending line number (1-based). If None, reads to the end of the file. If both are None, the entire file is returned.
14
+ from_line (int, optional): Starting line number (1-based). Omit to start from the first line.
15
+ to_line (int, optional): Ending line number (1-based). Omit to read to the end of the file.
16
+
17
+ To read the full file, just provide file_path and leave from_line and to_line unset.
18
+
15
19
  Returns:
16
20
  str: File content with a header indicating the file name and line range. Example:
17
21
  - "---\nFile: /path/to/file.py | Lines: 1-10 (of 100)\n---\n<lines...>"
18
- - "---\nFile: /path/to/file.py | All lines (total: 100)\n---\n<all lines...>"
22
+ - "---\nFile: /path/to/file.py | All lines (total: 100 (all))\n---\n<all lines...>"
19
23
  - "Error reading file: <error message>"
20
24
  - "❗ not found"
21
25
  """
22
26
 
23
27
  def run(self, file_path: str, from_line: int = None, to_line: int = None) -> str:
24
- from janito.agent.tools.tools_utils import display_path
28
+ from janito.agent.tools_utils.utils import display_path
25
29
 
26
30
  disp_path = display_path(file_path)
27
31
  if from_line and to_line:
28
32
  self.report_info(
29
33
  tr(
30
- "📖 Reading {disp_path} {from_line}-{to_line}",
34
+ "📖 Reading file '{disp_path}' {from_line}-{to_line}",
31
35
  disp_path=disp_path,
32
36
  from_line=from_line,
33
37
  to_line=to_line,
34
38
  )
35
39
  )
36
40
  else:
37
- self.report_info(tr("📖 Reading {disp_path} all", disp_path=disp_path))
41
+ self.report_info(tr("📖 Reading file '{disp_path}'", disp_path=disp_path))
38
42
  try:
39
43
  with open(file_path, "r", encoding="utf-8", errors="replace") as f:
40
44
  lines = f.readlines()
@@ -59,7 +63,7 @@ class GetLinesTool(ToolBase):
59
63
  elif to_line < total_lines:
60
64
  self.report_success(
61
65
  tr(
62
- " ✅ {selected_len} {line_word} ({remaining} to eof)",
66
+ " ✅ {selected_len} {line_word} ({remaining} to end)",
63
67
  selected_len=selected_len,
64
68
  line_word=pluralize("line", selected_len),
65
69
  remaining=total_lines - to_line,
@@ -68,7 +72,7 @@ class GetLinesTool(ToolBase):
68
72
  else:
69
73
  self.report_success(
70
74
  tr(
71
- " ✅ {selected_len} {line_word}",
75
+ " ✅ {selected_len} {line_word} (all)",
72
76
  selected_len=selected_len,
73
77
  line_word=pluralize("line", selected_len),
74
78
  )
@@ -98,7 +102,7 @@ class GetLinesTool(ToolBase):
98
102
  )
99
103
  else:
100
104
  header = tr(
101
- "---\n{disp_path} All lines (total: {total_lines})\n---\n",
105
+ "---\n{disp_path} All lines (total: {total_lines} (all))\n---\n",
102
106
  disp_path=disp_path,
103
107
  total_lines=total_lines,
104
108
  )