janito 1.9.0__py3-none-any.whl → 1.11.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 (106) hide show
  1. janito/__init__.py +1 -1
  2. janito/agent/api_exceptions.py +4 -0
  3. janito/agent/config.py +1 -1
  4. janito/agent/config_defaults.py +2 -26
  5. janito/agent/conversation.py +163 -122
  6. janito/agent/conversation_api.py +246 -168
  7. janito/agent/conversation_ui.py +1 -1
  8. janito/agent/{conversation_history.py → llm_conversation_history.py} +30 -1
  9. janito/agent/openai_client.py +38 -23
  10. janito/agent/openai_schema_generator.py +162 -129
  11. janito/agent/platform_discovery.py +134 -77
  12. janito/agent/profile_manager.py +5 -5
  13. janito/agent/rich_message_handler.py +80 -31
  14. janito/agent/templates/profiles/system_prompt_template_base.txt.j2 +20 -4
  15. janito/agent/test_openai_schema_generator.py +93 -0
  16. janito/agent/tool_base.py +7 -2
  17. janito/agent/tool_executor.py +54 -49
  18. janito/agent/tool_registry.py +5 -2
  19. janito/agent/tool_use_tracker.py +26 -5
  20. janito/agent/tools/__init__.py +8 -3
  21. janito/agent/tools/create_directory.py +3 -1
  22. janito/agent/tools/create_file.py +7 -1
  23. janito/agent/tools/fetch_url.py +40 -3
  24. janito/agent/tools/find_files.py +29 -14
  25. janito/agent/tools/get_file_outline/core.py +7 -8
  26. janito/agent/tools/get_file_outline/python_outline.py +139 -95
  27. janito/agent/tools/get_file_outline/search_outline.py +3 -1
  28. janito/agent/tools/get_lines.py +98 -64
  29. janito/agent/tools/move_file.py +59 -31
  30. janito/agent/tools/open_url.py +31 -0
  31. janito/agent/tools/present_choices.py +3 -1
  32. janito/agent/tools/python_command_runner.py +149 -0
  33. janito/agent/tools/python_file_runner.py +147 -0
  34. janito/agent/tools/python_stdin_runner.py +153 -0
  35. janito/agent/tools/remove_directory.py +3 -1
  36. janito/agent/tools/remove_file.py +5 -1
  37. janito/agent/tools/replace_file.py +12 -2
  38. janito/agent/tools/replace_text_in_file.py +195 -149
  39. janito/agent/tools/run_bash_command.py +30 -69
  40. janito/agent/tools/run_powershell_command.py +138 -105
  41. janito/agent/tools/search_text/__init__.py +1 -0
  42. janito/agent/tools/search_text/core.py +176 -0
  43. janito/agent/tools/search_text/match_lines.py +58 -0
  44. janito/agent/tools/search_text/pattern_utils.py +65 -0
  45. janito/agent/tools/search_text/traverse_directory.py +127 -0
  46. janito/agent/tools/validate_file_syntax/core.py +43 -30
  47. janito/agent/tools/validate_file_syntax/html_validator.py +21 -5
  48. janito/agent/tools/validate_file_syntax/markdown_validator.py +77 -34
  49. janito/agent/tools_utils/action_type.py +7 -0
  50. janito/agent/tools_utils/dir_walk_utils.py +3 -2
  51. janito/agent/tools_utils/formatting.py +47 -21
  52. janito/agent/tools_utils/gitignore_utils.py +89 -40
  53. janito/agent/tools_utils/test_gitignore_utils.py +46 -0
  54. janito/agent/tools_utils/utils.py +7 -1
  55. janito/cli/_print_config.py +63 -61
  56. janito/cli/arg_parser.py +13 -12
  57. janito/cli/cli_main.py +137 -147
  58. janito/cli/config_commands.py +112 -109
  59. janito/cli/main.py +152 -174
  60. janito/cli/one_shot.py +40 -26
  61. janito/i18n/__init__.py +1 -1
  62. janito/rich_utils.py +46 -8
  63. janito/shell/commands/__init__.py +2 -4
  64. janito/shell/commands/conversation_restart.py +3 -1
  65. janito/shell/commands/edit.py +3 -0
  66. janito/shell/commands/history_view.py +3 -3
  67. janito/shell/commands/lang.py +3 -0
  68. janito/shell/commands/livelogs.py +5 -3
  69. janito/shell/commands/prompt.py +6 -0
  70. janito/shell/commands/session.py +3 -0
  71. janito/shell/commands/session_control.py +3 -0
  72. janito/shell/commands/termweb_log.py +8 -0
  73. janito/shell/commands/tools.py +3 -0
  74. janito/shell/commands/track.py +36 -0
  75. janito/shell/commands/utility.py +13 -18
  76. janito/shell/commands/verbose.py +3 -4
  77. janito/shell/input_history.py +62 -0
  78. janito/shell/main.py +160 -181
  79. janito/shell/session/config.py +83 -75
  80. janito/shell/session/manager.py +0 -21
  81. janito/shell/ui/interactive.py +97 -75
  82. janito/termweb/static/editor.css +32 -33
  83. janito/termweb/static/editor.css.bak +140 -22
  84. janito/termweb/static/editor.html +12 -7
  85. janito/termweb/static/editor.html.bak +16 -11
  86. janito/termweb/static/editor.js +94 -40
  87. janito/termweb/static/editor.js.bak +97 -65
  88. janito/termweb/static/index.html +1 -2
  89. janito/termweb/static/index.html.bak +1 -1
  90. janito/termweb/static/termweb.css +1 -22
  91. janito/termweb/static/termweb.css.bak +6 -4
  92. janito/termweb/static/termweb.js +0 -6
  93. janito/termweb/static/termweb.js.bak +1 -2
  94. janito/tests/test_rich_utils.py +44 -0
  95. janito/web/app.py +0 -75
  96. {janito-1.9.0.dist-info → janito-1.11.0.dist-info}/METADATA +61 -42
  97. janito-1.11.0.dist-info/RECORD +163 -0
  98. {janito-1.9.0.dist-info → janito-1.11.0.dist-info}/WHEEL +1 -1
  99. janito/agent/providers.py +0 -77
  100. janito/agent/tools/run_python_command.py +0 -161
  101. janito/agent/tools/search_text.py +0 -204
  102. janito/shell/commands/sum.py +0 -49
  103. janito-1.9.0.dist-info/RECORD +0 -151
  104. {janito-1.9.0.dist-info → janito-1.11.0.dist-info}/entry_points.txt +0 -0
  105. {janito-1.9.0.dist-info → janito-1.11.0.dist-info}/licenses/LICENSE +0 -0
  106. {janito-1.9.0.dist-info → janito-1.11.0.dist-info}/top_level.txt +0 -0
@@ -1,43 +1,92 @@
1
1
  import os
2
2
  import pathspec
3
3
 
4
- _spec = None
5
-
6
-
7
- def load_gitignore_patterns(gitignore_path=".gitignore"):
8
- global _spec
9
- if not os.path.exists(gitignore_path):
10
- _spec = pathspec.PathSpec.from_lines("gitwildmatch", [])
11
- return _spec
12
- with open(gitignore_path, "r", encoding="utf-8", errors="replace") as f:
13
- lines = f.readlines()
14
- _spec = pathspec.PathSpec.from_lines("gitwildmatch", lines)
15
- return _spec
16
-
17
-
18
- def is_ignored(path):
19
- global _spec
20
- if _spec is None:
21
- _spec = load_gitignore_patterns()
22
- # Normalize path to be relative and use forward slashes
23
- rel_path = os.path.relpath(path).replace(os.sep, "/")
24
- return _spec.match_file(rel_path)
25
-
26
-
27
- def filter_ignored(root, dirs, files, spec=None):
28
- if spec is None:
29
- global _spec
30
- if _spec is None:
31
- _spec = load_gitignore_patterns()
32
- spec = _spec
33
-
34
- def not_ignored(p):
35
- rel_path = os.path.relpath(os.path.join(root, p)).replace(os.sep, "/")
36
- # Always ignore .git directory (like git does)
37
- if rel_path == ".git" or rel_path.startswith(".git/"):
38
- return False
39
- return not spec.match_file(rel_path)
40
-
41
- dirs[:] = [d for d in dirs if not_ignored(d)]
42
- files = [f for f in files if not_ignored(f)]
43
- return dirs, files
4
+
5
+ class GitignoreFilter:
6
+ """
7
+ Utility class for loading, interpreting, and applying .gitignore patterns to file and directory paths.
8
+
9
+ Methods
10
+ -------
11
+ __init__(self, gitignore_path: str = ".gitignore")
12
+ Loads and parses .gitignore patterns from the specified path or finds the nearest .gitignore if a directory is given.
13
+
14
+ is_ignored(self, path: str) -> bool
15
+ Returns True if the given path matches any of the loaded .gitignore patterns.
16
+
17
+ filter_ignored(self, root: str, dirs: list, files: list) -> tuple[list, list]
18
+ Filters out ignored directories and files from the provided lists, returning only those not ignored.
19
+ """
20
+
21
+ @staticmethod
22
+ def find_nearest_gitignore(start_path):
23
+ """
24
+ Search upward from start_path for the nearest .gitignore file.
25
+ Returns the path to the found .gitignore, or the default .gitignore in start_path if none found.
26
+ """
27
+ current_dir = os.path.abspath(start_path)
28
+ if os.path.isfile(current_dir):
29
+ current_dir = os.path.dirname(current_dir)
30
+ while True:
31
+ candidate = os.path.join(current_dir, ".gitignore")
32
+ if os.path.isfile(candidate):
33
+ return candidate
34
+ parent = os.path.dirname(current_dir)
35
+ if parent == current_dir:
36
+ # Reached filesystem root, return default .gitignore path (may not exist)
37
+ return os.path.join(start_path, ".gitignore")
38
+ current_dir = parent
39
+
40
+ def __init__(self, gitignore_path: str = ".gitignore"):
41
+ # If a directory is passed, find the nearest .gitignore up the tree
42
+ if os.path.isdir(gitignore_path):
43
+ self.gitignore_path = self.find_nearest_gitignore(gitignore_path)
44
+ else:
45
+ self.gitignore_path = os.path.abspath(gitignore_path)
46
+ self.base_dir = os.path.dirname(self.gitignore_path)
47
+ lines = []
48
+ if not os.path.exists(self.gitignore_path):
49
+ self._spec = pathspec.PathSpec.from_lines("gitwildmatch", [])
50
+ else:
51
+ with open(
52
+ self.gitignore_path, "r", encoding="utf-8", errors="replace"
53
+ ) as f:
54
+ lines = f.readlines()
55
+ self._spec = pathspec.PathSpec.from_lines("gitwildmatch", lines)
56
+ # Collect directory patterns (ending with /)
57
+ self.dir_patterns = [
58
+ line.strip() for line in lines if line.strip().endswith("/")
59
+ ]
60
+
61
+ def is_ignored(self, path: str) -> bool:
62
+ """Return True if the given path is ignored by the loaded .gitignore patterns."""
63
+ abs_path = os.path.abspath(path)
64
+ rel_path = os.path.relpath(abs_path, self.base_dir).replace(os.sep, "/")
65
+ return self._spec.match_file(rel_path)
66
+
67
+ def filter_ignored(self, root: str, dirs: list, files: list) -> tuple[list, list]:
68
+ """
69
+ Filter out ignored directories and files from the provided lists.
70
+ Always ignores the .git directory (like git does).
71
+ """
72
+
73
+ def dir_is_ignored(d):
74
+ abs_path = os.path.abspath(os.path.join(root, d))
75
+ rel_path = os.path.relpath(abs_path, self.base_dir).replace(os.sep, "/")
76
+ if rel_path == ".git" or rel_path.startswith(".git/"):
77
+ return True
78
+ # Remove directory if it matches a directory pattern
79
+ for pat in self.dir_patterns:
80
+ pat_clean = pat.rstrip("/")
81
+ if rel_path == pat_clean or rel_path.startswith(pat_clean + "/"):
82
+ return True
83
+ return self._spec.match_file(rel_path)
84
+
85
+ def file_is_ignored(f):
86
+ abs_path = os.path.abspath(os.path.join(root, f))
87
+ rel_path = os.path.relpath(abs_path, self.base_dir).replace(os.sep, "/")
88
+ return self._spec.match_file(rel_path)
89
+
90
+ dirs[:] = [d for d in dirs if not dir_is_ignored(d)]
91
+ files = [f for f in files if not file_is_ignored(f)]
92
+ return dirs, files
@@ -0,0 +1,46 @@
1
+ import os
2
+ import tempfile
3
+ import shutil
4
+ import pytest
5
+ from janito.agent.tools_utils.gitignore_utils import GitignoreFilter
6
+
7
+
8
+ def test_gitignore_filter_basic(tmp_path):
9
+ # Create a .gitignore file
10
+ gitignore_content = """
11
+ ignored_file.txt
12
+ ignored_dir/
13
+ *.log
14
+ """
15
+ gitignore_path = tmp_path / ".gitignore"
16
+ gitignore_path.write_text(gitignore_content)
17
+
18
+ # Create files and directories
19
+ (tmp_path / "ignored_file.txt").write_text("should be ignored")
20
+ (tmp_path / "not_ignored.txt").write_text("should not be ignored")
21
+ (tmp_path / "ignored_dir").mkdir()
22
+ (tmp_path / "ignored_dir" / "file.txt").write_text("should be ignored")
23
+ (tmp_path / "not_ignored_dir").mkdir()
24
+ (tmp_path / "not_ignored_dir" / "file.txt").write_text("should not be ignored")
25
+ (tmp_path / "file.log").write_text("should be ignored")
26
+
27
+ gi = GitignoreFilter(str(gitignore_path))
28
+
29
+ assert gi.is_ignored(str(tmp_path / "ignored_file.txt"))
30
+ assert not gi.is_ignored(str(tmp_path / "not_ignored.txt"))
31
+ # Directory itself is not ignored, only its contents
32
+ assert not gi.is_ignored(str(tmp_path / "ignored_dir"))
33
+ assert gi.is_ignored(str(tmp_path / "ignored_dir" / "file.txt"))
34
+ assert not gi.is_ignored(str(tmp_path / "not_ignored_dir"))
35
+ assert not gi.is_ignored(str(tmp_path / "not_ignored_dir" / "file.txt"))
36
+ assert gi.is_ignored(str(tmp_path / "file.log"))
37
+
38
+ # Test filter_ignored
39
+ dirs = ["ignored_dir", "not_ignored_dir"]
40
+ files = ["ignored_file.txt", "not_ignored.txt", "file.log"]
41
+ filtered_dirs, filtered_files = gi.filter_ignored(str(tmp_path), dirs, files)
42
+ assert "ignored_dir" not in filtered_dirs
43
+ assert "not_ignored_dir" in filtered_dirs
44
+ assert "ignored_file.txt" not in filtered_files
45
+ assert "file.log" not in filtered_files
46
+ assert "not_ignored.txt" in filtered_files
@@ -12,7 +12,13 @@ def display_path(path):
12
12
  str: Display path, optionally as an ANSI hyperlink.
13
13
  """
14
14
  if os.path.isabs(path):
15
- disp = path
15
+ cwd = os.path.abspath(os.getcwd())
16
+ abs_path = os.path.abspath(path)
17
+ # Check if the absolute path is within the current working directory
18
+ if abs_path.startswith(cwd + os.sep):
19
+ disp = os.path.relpath(abs_path, cwd)
20
+ else:
21
+ disp = path
16
22
  else:
17
23
  disp = os.path.relpath(path)
18
24
  port = runtime_config.get("termweb_port")
@@ -1,5 +1,7 @@
1
1
  import os
2
- from janito.rich_utils import print_info, print_warning, print_magenta
2
+ from janito.rich_utils import RichPrinter
3
+
4
+ _rich_printer = RichPrinter()
3
5
  from ._utils import home_shorten
4
6
 
5
7
 
@@ -7,17 +9,65 @@ def print_config_items(items, color_label=None):
7
9
  if not items:
8
10
  return
9
11
  if color_label:
10
- print_info(color_label)
12
+ _rich_printer.print_info(color_label)
11
13
  home = os.path.expanduser("~")
12
14
  for key, value in items.items():
13
15
  if key == "system_prompt_template" and isinstance(value, str):
14
16
  if value.startswith(home):
15
17
  print(f"{key} = {home_shorten(value)}")
16
18
  else:
17
- print_info(f"{key} = {value}")
19
+ _rich_printer.print_info(f"{key} = {value}")
20
+ else:
21
+ _rich_printer.print_info(f"{key} = {value}")
22
+ _rich_printer.print_info("")
23
+
24
+
25
+ def _mask_api_key(value):
26
+ if value and len(value) > 8:
27
+ return value[:4] + "..." + value[-4:]
28
+ elif value:
29
+ return "***"
30
+ return None
31
+
32
+
33
+ def _collect_config_items(config, unified_config, keys):
34
+ items = {}
35
+ for key in sorted(keys):
36
+ if key == "api_key":
37
+ value = config.get("api_key")
38
+ value = _mask_api_key(value)
18
39
  else:
19
- print_info(f"{key} = {value}")
20
- print_info("")
40
+ value = unified_config.get(key)
41
+ items[key] = value
42
+ return items
43
+
44
+
45
+ def _print_defaults(config_defaults, shown_keys):
46
+ default_items = {
47
+ k: v
48
+ for k, v in config_defaults.items()
49
+ if k not in shown_keys and k != "api_key"
50
+ }
51
+ if default_items:
52
+ _rich_printer.print_magenta(
53
+ "[green]\U0001f7e2 Defaults (not set in config files)[/green]"
54
+ )
55
+ from pathlib import Path
56
+
57
+ template_path = (
58
+ Path(__file__).parent
59
+ / "agent"
60
+ / "templates"
61
+ / "system_prompt_template_default.j2"
62
+ )
63
+ for key, value in default_items.items():
64
+ if key == "system_prompt_template" and value is None:
65
+ _rich_printer.print_info(
66
+ f"{key} = (default template path: {home_shorten(str(template_path))})"
67
+ )
68
+ else:
69
+ _rich_printer.print_info(f"{key} = {value}")
70
+ _rich_printer.print_info("")
21
71
 
22
72
 
23
73
  def print_full_config(
@@ -27,68 +77,20 @@ def print_full_config(
27
77
  Print local, global, and default config values in a unified way.
28
78
  Handles masking API keys and showing the template file for system_prompt_template if not set.
29
79
  """
30
- local_items = {}
31
- global_items = {}
32
80
  local_keys = set(local_config.all().keys())
33
81
  global_keys = set(global_config.all().keys())
34
82
  if not (local_keys or global_keys):
35
- print_warning("No configuration found.")
83
+ _rich_printer.print_warning("No configuration found.")
36
84
  else:
37
- for key in sorted(local_keys):
38
- if key == "api_key":
39
- value = local_config.get("api_key")
40
- value = (
41
- value[:4] + "..." + value[-4:]
42
- if value and len(value) > 8
43
- else ("***" if value else None)
44
- )
45
- else:
46
- value = unified_config.get(key)
47
- local_items[key] = value
48
- for key in sorted(global_keys - local_keys):
49
- if key == "api_key":
50
- value = global_config.get("api_key")
51
- value = (
52
- value[:4] + "..." + value[-4:]
53
- if value and len(value) > 8
54
- else ("***" if value else None)
55
- )
56
- else:
57
- value = unified_config.get(key)
58
- global_items[key] = value
59
- # Mask API key
60
- for cfg in (local_items, global_items):
61
- if "api_key" in cfg and cfg["api_key"]:
62
- val = cfg["api_key"]
63
- cfg["api_key"] = val[:4] + "..." + val[-4:] if len(val) > 8 else "***"
85
+ local_items = _collect_config_items(local_config, unified_config, local_keys)
86
+ global_items = _collect_config_items(
87
+ global_config, unified_config, global_keys - local_keys
88
+ )
64
89
  print_config_items(
65
- local_items, color_label="[cyan]🏠 Local Configuration[/cyan]"
90
+ local_items, color_label="[cyan]\U0001f3e0 Local Configuration[/cyan]"
66
91
  )
67
92
  print_config_items(
68
- global_items, color_label="[yellow]🌐 Global Configuration[/yellow]"
93
+ global_items, color_label="[yellow]\U0001f310 Global Configuration[/yellow]"
69
94
  )
70
- # Show defaults for unset keys
71
95
  shown_keys = set(local_items.keys()) | set(global_items.keys())
72
- default_items = {
73
- k: v
74
- for k, v in config_defaults.items()
75
- if k not in shown_keys and k != "api_key"
76
- }
77
- if default_items:
78
- print_magenta("[green]🟢 Defaults (not set in config files)[/green]")
79
- from pathlib import Path
80
-
81
- template_path = (
82
- Path(__file__).parent
83
- / "agent"
84
- / "templates"
85
- / "system_prompt_template_default.j2"
86
- )
87
- for key, value in default_items.items():
88
- if key == "system_prompt_template" and value is None:
89
- print_info(
90
- f"{key} = (default template path: {home_shorten(str(template_path))})"
91
- )
92
- else:
93
- print_info(f"{key} = {value}")
94
- print_info("")
96
+ _print_defaults(config_defaults, shown_keys)
janito/cli/arg_parser.py CHANGED
@@ -34,7 +34,7 @@ def create_parser():
34
34
  "--set-provider-config",
35
35
  nargs=3,
36
36
  metavar=("NAME", "KEY", "VALUE"),
37
- help="Set a provider config parameter (e.g., --set-provider-config openrouter.ai api_key sk-xxx).",
37
+ help="Set a provider config parameter (e.g., --set-provider-config openai api_key sk-xxx).",
38
38
  )
39
39
  parser.add_argument(
40
40
  "--lang",
@@ -52,7 +52,7 @@ def create_parser():
52
52
  "--max-tokens",
53
53
  type=int,
54
54
  default=None,
55
- help="Maximum tokens for model response (overrides config, default: 200000)",
55
+ help="Maximum tokens for model response (overrides config, default: 32000)",
56
56
  )
57
57
  parser.add_argument(
58
58
  "--max-tools",
@@ -212,6 +212,11 @@ def create_parser():
212
212
  action="store_true",
213
213
  help="Print all agent events before dispatching to the message handler (for debugging)",
214
214
  )
215
+ parser.add_argument(
216
+ "--verbose-messages",
217
+ action="store_true",
218
+ help="Print every new message added to the conversation history with a colored background.",
219
+ )
215
220
  parser.add_argument(
216
221
  "-V",
217
222
  "--vanilla",
@@ -231,16 +236,6 @@ def create_parser():
231
236
  default=None,
232
237
  help="Agent Profile name (only 'base' is supported)",
233
238
  )
234
- parser.add_argument(
235
- "--stream",
236
- action="store_true",
237
- help="Enable OpenAI streaming mode (yields tokens as they arrive)",
238
- )
239
- parser.add_argument(
240
- "--verbose-stream",
241
- action="store_true",
242
- help="Print raw chunks as they are fetched from OpenAI (for debugging)",
243
- )
244
239
  parser.add_argument(
245
240
  "--no-termweb",
246
241
  action="store_true",
@@ -263,4 +258,10 @@ def create_parser():
263
258
  action="store_true",
264
259
  help="Disable tool call reason tracking (no tools tracking)",
265
260
  )
261
+ parser.add_argument(
262
+ "--tool-user",
263
+ action="store_true",
264
+ default=False,
265
+ help="When set, tool responses will use role 'user' instead of 'tool' in the conversation history.",
266
+ )
266
267
  return parser