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.
Files changed (142) 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 -3
  5. janito/agent/config_utils.py +0 -9
  6. janito/agent/conversation.py +177 -114
  7. janito/agent/conversation_api.py +179 -159
  8. janito/agent/conversation_tool_calls.py +11 -8
  9. janito/agent/llm_conversation_history.py +70 -0
  10. janito/agent/openai_client.py +44 -21
  11. janito/agent/openai_schema_generator.py +164 -128
  12. janito/agent/platform_discovery.py +134 -77
  13. janito/agent/profile_manager.py +5 -5
  14. janito/agent/rich_message_handler.py +80 -31
  15. janito/agent/templates/profiles/system_prompt_template_base.txt.j2 +9 -8
  16. janito/agent/test_openai_schema_generator.py +93 -0
  17. janito/agent/tool_base.py +7 -2
  18. janito/agent/tool_executor.py +63 -50
  19. janito/agent/tool_registry.py +5 -2
  20. janito/agent/tool_use_tracker.py +42 -5
  21. janito/agent/tools/__init__.py +13 -12
  22. janito/agent/tools/create_directory.py +9 -6
  23. janito/agent/tools/create_file.py +35 -54
  24. janito/agent/tools/delete_text_in_file.py +97 -0
  25. janito/agent/tools/fetch_url.py +50 -5
  26. janito/agent/tools/find_files.py +40 -26
  27. janito/agent/tools/get_file_outline/__init__.py +1 -0
  28. janito/agent/tools/{outline_file/__init__.py → get_file_outline/core.py} +14 -18
  29. janito/agent/tools/get_file_outline/python_outline.py +134 -0
  30. janito/agent/tools/{search_outline.py → get_file_outline/search_outline.py} +11 -0
  31. janito/agent/tools/get_lines.py +21 -12
  32. janito/agent/tools/move_file.py +13 -12
  33. janito/agent/tools/present_choices.py +3 -1
  34. janito/agent/tools/python_command_runner.py +150 -0
  35. janito/agent/tools/python_file_runner.py +148 -0
  36. janito/agent/tools/python_stdin_runner.py +154 -0
  37. janito/agent/tools/remove_directory.py +4 -2
  38. janito/agent/tools/remove_file.py +15 -13
  39. janito/agent/tools/replace_file.py +72 -0
  40. janito/agent/tools/replace_text_in_file.py +7 -5
  41. janito/agent/tools/run_bash_command.py +29 -72
  42. janito/agent/tools/run_powershell_command.py +142 -102
  43. janito/agent/tools/search_text.py +177 -131
  44. janito/agent/tools/validate_file_syntax/__init__.py +1 -0
  45. janito/agent/tools/validate_file_syntax/core.py +94 -0
  46. janito/agent/tools/validate_file_syntax/css_validator.py +35 -0
  47. janito/agent/tools/validate_file_syntax/html_validator.py +77 -0
  48. janito/agent/tools/validate_file_syntax/js_validator.py +27 -0
  49. janito/agent/tools/validate_file_syntax/json_validator.py +6 -0
  50. janito/agent/tools/validate_file_syntax/markdown_validator.py +66 -0
  51. janito/agent/tools/validate_file_syntax/ps1_validator.py +32 -0
  52. janito/agent/tools/validate_file_syntax/python_validator.py +5 -0
  53. janito/agent/tools/validate_file_syntax/xml_validator.py +11 -0
  54. janito/agent/tools/validate_file_syntax/yaml_validator.py +6 -0
  55. janito/agent/tools_utils/__init__.py +1 -0
  56. janito/agent/tools_utils/action_type.py +7 -0
  57. janito/agent/tools_utils/dir_walk_utils.py +24 -0
  58. janito/agent/tools_utils/formatting.py +49 -0
  59. janito/agent/tools_utils/gitignore_utils.py +69 -0
  60. janito/agent/tools_utils/test_gitignore_utils.py +46 -0
  61. janito/agent/tools_utils/utils.py +30 -0
  62. janito/cli/_livereload_log_utils.py +13 -0
  63. janito/cli/_print_config.py +63 -61
  64. janito/cli/arg_parser.py +57 -14
  65. janito/cli/cli_main.py +270 -0
  66. janito/cli/livereload_starter.py +60 -0
  67. janito/cli/main.py +166 -99
  68. janito/cli/one_shot.py +80 -0
  69. janito/cli/termweb_starter.py +2 -2
  70. janito/i18n/__init__.py +1 -1
  71. janito/livereload/app.py +25 -0
  72. janito/rich_utils.py +41 -25
  73. janito/{cli_chat_shell → shell}/commands/__init__.py +19 -14
  74. janito/{cli_chat_shell → shell}/commands/config.py +4 -4
  75. janito/shell/commands/conversation_restart.py +74 -0
  76. janito/shell/commands/edit.py +24 -0
  77. janito/shell/commands/history_view.py +18 -0
  78. janito/{cli_chat_shell → shell}/commands/lang.py +3 -0
  79. janito/shell/commands/livelogs.py +42 -0
  80. janito/{cli_chat_shell → shell}/commands/prompt.py +16 -6
  81. janito/shell/commands/session.py +35 -0
  82. janito/{cli_chat_shell → shell}/commands/session_control.py +3 -5
  83. janito/{cli_chat_shell → shell}/commands/termweb_log.py +18 -10
  84. janito/shell/commands/tools.py +26 -0
  85. janito/shell/commands/track.py +36 -0
  86. janito/shell/commands/utility.py +28 -0
  87. janito/{cli_chat_shell → shell}/commands/verbose.py +4 -5
  88. janito/shell/commands.py +40 -0
  89. janito/shell/input_history.py +62 -0
  90. janito/shell/main.py +257 -0
  91. janito/{cli_chat_shell/shell_command_completer.py → shell/prompt/completer.py} +1 -1
  92. janito/{cli_chat_shell/chat_ui.py → shell/prompt/session_setup.py} +19 -5
  93. janito/shell/session/manager.py +101 -0
  94. janito/{cli_chat_shell/ui.py → shell/ui/interactive.py} +23 -17
  95. janito/termweb/app.py +3 -3
  96. janito/termweb/static/editor.css +142 -0
  97. janito/termweb/static/editor.css.bak +27 -0
  98. janito/termweb/static/editor.html +15 -213
  99. janito/termweb/static/editor.html.bak +16 -215
  100. janito/termweb/static/editor.js +209 -0
  101. janito/termweb/static/editor.js.bak +227 -0
  102. janito/termweb/static/index.html +2 -3
  103. janito/termweb/static/index.html.bak +2 -3
  104. janito/termweb/static/termweb.css.bak +33 -84
  105. janito/termweb/static/termweb.js +15 -34
  106. janito/termweb/static/termweb.js.bak +18 -36
  107. janito/tests/test_rich_utils.py +44 -0
  108. janito/web/app.py +0 -75
  109. {janito-1.8.1.dist-info → janito-1.10.0.dist-info}/METADATA +62 -42
  110. janito-1.10.0.dist-info/RECORD +158 -0
  111. {janito-1.8.1.dist-info → janito-1.10.0.dist-info}/WHEEL +1 -1
  112. janito/agent/tools/dir_walk_utils.py +0 -16
  113. janito/agent/tools/gitignore_utils.py +0 -46
  114. janito/agent/tools/memory.py +0 -48
  115. janito/agent/tools/outline_file/formatting.py +0 -20
  116. janito/agent/tools/outline_file/python_outline.py +0 -71
  117. janito/agent/tools/present_choices_test.py +0 -18
  118. janito/agent/tools/rich_live.py +0 -44
  119. janito/agent/tools/run_python_command.py +0 -163
  120. janito/agent/tools/tools_utils.py +0 -56
  121. janito/agent/tools/utils.py +0 -33
  122. janito/agent/tools/validate_file_syntax.py +0 -163
  123. janito/cli/runner/cli_main.py +0 -180
  124. janito/cli_chat_shell/chat_loop.py +0 -163
  125. janito/cli_chat_shell/chat_state.py +0 -38
  126. janito/cli_chat_shell/commands/history_start.py +0 -37
  127. janito/cli_chat_shell/commands/session.py +0 -48
  128. janito/cli_chat_shell/commands/sum.py +0 -49
  129. janito/cli_chat_shell/commands/utility.py +0 -32
  130. janito/cli_chat_shell/session_manager.py +0 -72
  131. janito-1.8.1.dist-info/RECORD +0 -127
  132. /janito/agent/tools/{outline_file → get_file_outline}/markdown_outline.py +0 -0
  133. /janito/cli/{runner/_termweb_log_utils.py → _termweb_log_utils.py} +0 -0
  134. /janito/cli/{runner/config.py → config_runner.py} +0 -0
  135. /janito/cli/{runner/formatting.py → formatting_runner.py} +0 -0
  136. /janito/{cli/runner → shell}/__init__.py +0 -0
  137. /janito/{cli_chat_shell → shell/prompt}/load_prompt.py +0 -0
  138. /janito/{cli_chat_shell/config_shell.py → shell/session/config.py} +0 -0
  139. /janito/{cli_chat_shell/__init__.py → shell/session/history.py} +0 -0
  140. {janito-1.8.1.dist-info → janito-1.10.0.dist-info}/entry_points.txt +0 -0
  141. {janito-1.8.1.dist-info → janito-1.10.0.dist-info}/licenses/LICENSE +0 -0
  142. {janito-1.8.1.dist-info → janito-1.10.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,66 @@
1
+ from janito.i18n import tr
2
+ import re
3
+
4
+
5
+ def validate_markdown(file_path: str) -> str:
6
+ with open(file_path, "r", encoding="utf-8") as f:
7
+ content = f.read()
8
+ errors = []
9
+ lines = content.splitlines()
10
+ # Header space check
11
+ for i, line in enumerate(lines, 1):
12
+ if re.match(r"^#+[^ #]", line):
13
+ errors.append(f"Line {i}: Header missing space after # | {line.strip()}")
14
+ # Unclosed code block
15
+ if content.count("```") % 2 != 0:
16
+ errors.append("Unclosed code block (```) detected")
17
+ # Unclosed link or image
18
+ for i, line in enumerate(lines, 1):
19
+ if re.search(r"\[[^\]]*\]\([^)]+$", line):
20
+ errors.append(
21
+ f"Line {i}: Unclosed link or image (missing closing parenthesis) | {line.strip()}"
22
+ )
23
+ # List item formatting and blank line before new list (bulleted and numbered)
24
+ for i, line in enumerate(lines, 1):
25
+ # Skip table lines
26
+ if line.lstrip().startswith("|"):
27
+ continue
28
+ # List item missing space after bullet
29
+ if re.match(r"^[-*+][^ \n]", line):
30
+ stripped = line.strip()
31
+ if not (
32
+ stripped.startswith("*")
33
+ and stripped.endswith("*")
34
+ and len(stripped) > 2
35
+ ):
36
+ errors.append(
37
+ f"Line {i}: List item missing space after bullet | {line.strip()}"
38
+ )
39
+ # Blank line before first item of a new bulleted list
40
+ if re.match(r"^\s*[-*+] ", line):
41
+ if i > 1:
42
+ prev_line = lines[i - 2]
43
+ prev_is_list = bool(re.match(r"^\s*[-*+] ", prev_line))
44
+ if not prev_is_list and prev_line.strip() != "":
45
+ errors.append(
46
+ f"Line {i}: List should be preceded by a blank line for compatibility with MkDocs and other Markdown parsers | {line.strip()}"
47
+ )
48
+ # Blank line before first item of a new numbered list
49
+ if re.match(r"^\s*\d+\. ", line):
50
+ if i > 1:
51
+ prev_line = lines[i - 2]
52
+ prev_is_numbered_list = bool(re.match(r"^\s*\d+\. ", prev_line))
53
+ if not prev_is_numbered_list and prev_line.strip() != "":
54
+ errors.append(
55
+ f"Line {i}: Numbered list should be preceded by a blank line for compatibility with MkDocs and other Markdown parsers | {line.strip()}"
56
+ )
57
+ # Unclosed inline code
58
+ if content.count("`") % 2 != 0:
59
+ errors.append("Unclosed inline code (`) detected")
60
+ if errors:
61
+ msg = tr(
62
+ "⚠️ Warning: Markdown syntax issues found:\n{errors}",
63
+ errors="\n".join(errors),
64
+ )
65
+ return msg
66
+ return "✅ Syntax valid"
@@ -0,0 +1,32 @@
1
+ from janito.i18n import tr
2
+ import re
3
+
4
+
5
+ def validate_ps1(file_path: str) -> str:
6
+ with open(file_path, "r", encoding="utf-8") as f:
7
+ content = f.read()
8
+ errors = []
9
+ # Unmatched curly braces
10
+ if content.count("{") != content.count("}"):
11
+ errors.append("Unmatched curly braces { }")
12
+ # Unmatched parentheses
13
+ if content.count("(") != content.count(")"):
14
+ errors.append("Unmatched parentheses ( )")
15
+ # Unmatched brackets
16
+ if content.count("[") != content.count("]"):
17
+ errors.append("Unmatched brackets [ ]")
18
+ # Unclosed string literals
19
+ for quote in ["'", '"']:
20
+ unescaped = re.findall(rf"(?<!\\){quote}", content)
21
+ if len(unescaped) % 2 != 0:
22
+ errors.append(f"Unclosed string literal ({quote}) detected")
23
+ # Unclosed block comments <# ... #>
24
+ if content.count("<#") != content.count("#>"):
25
+ errors.append("Unclosed block comment (<# ... #>)")
26
+ if errors:
27
+ msg = tr(
28
+ "⚠️ Warning: PowerShell syntax issues found:\n{errors}",
29
+ errors="\n".join(errors),
30
+ )
31
+ return msg
32
+ return "✅ Syntax valid"
@@ -0,0 +1,5 @@
1
+ def validate_python(file_path: str) -> str:
2
+ import py_compile
3
+
4
+ py_compile.compile(file_path, doraise=True)
5
+ return "✅ Syntax valid"
@@ -0,0 +1,11 @@
1
+ from janito.i18n import tr
2
+
3
+
4
+ def validate_xml(file_path: str) -> str:
5
+ try:
6
+ from lxml import etree
7
+ except ImportError:
8
+ return tr("⚠️ lxml not installed. Cannot validate XML.")
9
+ with open(file_path, "rb") as f:
10
+ etree.parse(f)
11
+ return "✅ Syntax valid"
@@ -0,0 +1,6 @@
1
+ def validate_yaml(file_path: str) -> str:
2
+ import yaml
3
+
4
+ with open(file_path, "r", encoding="utf-8") as f:
5
+ yaml.safe_load(f)
6
+ return "✅ Syntax valid"
@@ -0,0 +1 @@
1
+ # tools_utils package init
@@ -0,0 +1,7 @@
1
+ from enum import Enum, auto
2
+
3
+
4
+ class ActionType(Enum):
5
+ READ = auto()
6
+ WRITE = auto()
7
+ EXECUTE = auto()
@@ -0,0 +1,24 @@
1
+ import os
2
+ from .gitignore_utils import GitignoreFilter
3
+
4
+
5
+ def walk_dir_with_gitignore(root_dir, max_depth=None):
6
+ """
7
+ Walks the directory tree starting at root_dir, yielding (root, dirs, files) tuples,
8
+ with .gitignore rules applied.
9
+ - If max_depth is None, unlimited recursion.
10
+ - If max_depth=0, only the top-level directory (flat, no recursion).
11
+ - If max_depth=1, only the root directory (matches 'find . -maxdepth 1').
12
+ - If max_depth=N (N>1), yields files in root and up to N-1 levels below root (matches 'find . -maxdepth N').
13
+ """
14
+ gitignore = GitignoreFilter()
15
+ for root, dirs, files in os.walk(root_dir):
16
+ rel_path = os.path.relpath(root, root_dir)
17
+ depth = 0 if rel_path == "." else rel_path.count(os.sep) + 1
18
+ if max_depth is not None:
19
+ if depth >= max_depth:
20
+ # For max_depth=1, only root (depth=0). For max_depth=2, root and one level below (depth=0,1).
21
+ if depth > 0:
22
+ continue
23
+ dirs, files = gitignore.filter_ignored(root, dirs, files)
24
+ yield root, dirs, files
@@ -0,0 +1,49 @@
1
+ class OutlineFormatter:
2
+ """
3
+ Utility class for formatting code and markdown outlines into human-readable tables.
4
+ """
5
+
6
+ @staticmethod
7
+ def format_outline_table(outline_items):
8
+ """
9
+ Format a list of code outline items (classes, functions, variables) into a table.
10
+
11
+ Args:
12
+ outline_items (list of dict): Each dict should contain keys: 'type', 'name', 'start', 'end', 'parent', 'docstring'.
13
+
14
+ Returns:
15
+ str: Formatted table as a string.
16
+ """
17
+ if not outline_items:
18
+ return "No classes, functions, or variables found."
19
+ header = "| Type | Name | Start | End | Parent | Docstring |\n|---------|-------------|-------|-----|----------|--------------------------|"
20
+ rows = []
21
+ for item in outline_items:
22
+ docstring = item.get("docstring", "").replace("\n", " ")
23
+ if len(docstring) > 24:
24
+ docstring = docstring[:21] + "..."
25
+ rows.append(
26
+ f"| {item['type']:<7} | {item['name']:<11} | {item['start']:<5} | {item['end']:<3} | {item['parent']:<8} | {docstring:<24} |"
27
+ )
28
+ return header + "\n" + "\n".join(rows)
29
+
30
+ @staticmethod
31
+ def format_markdown_outline_table(outline_items):
32
+ """
33
+ Format a list of markdown outline items (headers) into a table.
34
+
35
+ Args:
36
+ outline_items (list of dict): Each dict should contain keys: 'level', 'title', 'line'.
37
+
38
+ Returns:
39
+ str: Formatted table as a string.
40
+ """
41
+ if not outline_items:
42
+ return "No headers found."
43
+ header = "| Level | Header | Line |\n|-------|----------------------------------|------|"
44
+ rows = []
45
+ for item in outline_items:
46
+ rows.append(
47
+ f"| {item['level']:<5} | {item['title']:<32} | {item['line']:<4} |"
48
+ )
49
+ return header + "\n" + "\n".join(rows)
@@ -0,0 +1,69 @@
1
+ import os
2
+ import pathspec
3
+
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.
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
+ def __init__(self, gitignore_path: str = ".gitignore"):
22
+ self.gitignore_path = os.path.abspath(gitignore_path)
23
+ self.base_dir = os.path.dirname(self.gitignore_path)
24
+ lines = []
25
+ if not os.path.exists(self.gitignore_path):
26
+ self._spec = pathspec.PathSpec.from_lines("gitwildmatch", [])
27
+ else:
28
+ with open(
29
+ self.gitignore_path, "r", encoding="utf-8", errors="replace"
30
+ ) as f:
31
+ lines = f.readlines()
32
+ self._spec = pathspec.PathSpec.from_lines("gitwildmatch", lines)
33
+ # Collect directory patterns (ending with /)
34
+ self.dir_patterns = [
35
+ line.strip() for line in lines if line.strip().endswith("/")
36
+ ]
37
+
38
+ def is_ignored(self, path: str) -> bool:
39
+ """Return True if the given path is ignored by the loaded .gitignore patterns."""
40
+ abs_path = os.path.abspath(path)
41
+ rel_path = os.path.relpath(abs_path, self.base_dir).replace(os.sep, "/")
42
+ return self._spec.match_file(rel_path)
43
+
44
+ def filter_ignored(self, root: str, dirs: list, files: list) -> tuple[list, list]:
45
+ """
46
+ Filter out ignored directories and files from the provided lists.
47
+ Always ignores the .git directory (like git does).
48
+ """
49
+
50
+ def dir_is_ignored(d):
51
+ abs_path = os.path.abspath(os.path.join(root, d))
52
+ rel_path = os.path.relpath(abs_path, self.base_dir).replace(os.sep, "/")
53
+ if rel_path == ".git" or rel_path.startswith(".git/"):
54
+ return True
55
+ # Remove directory if it matches a directory pattern
56
+ for pat in self.dir_patterns:
57
+ pat_clean = pat.rstrip("/")
58
+ if rel_path == pat_clean or rel_path.startswith(pat_clean + "/"):
59
+ return True
60
+ return self._spec.match_file(rel_path)
61
+
62
+ def file_is_ignored(f):
63
+ abs_path = os.path.abspath(os.path.join(root, f))
64
+ rel_path = os.path.relpath(abs_path, self.base_dir).replace(os.sep, "/")
65
+ return self._spec.match_file(rel_path)
66
+
67
+ dirs[:] = [d for d in dirs if not dir_is_ignored(d)]
68
+ files = [f for f in files if not file_is_ignored(f)]
69
+ 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
@@ -0,0 +1,30 @@
1
+ import os
2
+ import urllib.parse
3
+ from janito.agent.runtime_config import runtime_config
4
+
5
+
6
+ def display_path(path):
7
+ """
8
+ Returns a display-friendly path. If runtime_config['termweb_port'] is set, injects an ANSI hyperlink to the local web file viewer.
9
+ Args:
10
+ path (str): Path to display.
11
+ Returns:
12
+ str: Display path, optionally as an ANSI hyperlink.
13
+ """
14
+ if os.path.isabs(path):
15
+ disp = path
16
+ else:
17
+ disp = os.path.relpath(path)
18
+ port = runtime_config.get("termweb_port")
19
+ if port:
20
+ url = f"http://localhost:{port}/?path={urllib.parse.quote(path)}"
21
+ # Use Rich markup for hyperlinks
22
+ return f"[link={url}]{disp}[/link]"
23
+ return disp
24
+
25
+
26
+ def pluralize(word: str, count: int) -> str:
27
+ """Return the pluralized form of word if count != 1, unless word already ends with 's'."""
28
+ if count == 1 or word.endswith("s"):
29
+ return word
30
+ return word + "s"
@@ -0,0 +1,13 @@
1
+ def print_livereload_logs(stdout_path, stderr_path):
2
+ print("\n[LiveReload stdout log]")
3
+ try:
4
+ with open(stdout_path, encoding="utf-8") as f:
5
+ print(f.read())
6
+ except Exception as e:
7
+ print(f"[Error reading stdout log: {e}]")
8
+ print("\n[LiveReload stderr log]")
9
+ try:
10
+ with open(stderr_path, encoding="utf-8") as f:
11
+ print(f.read())
12
+ except Exception as e:
13
+ print(f"[Error reading stderr log: {e}]")
@@ -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
@@ -8,8 +8,33 @@ def create_parser():
8
8
  parser = argparse.ArgumentParser(
9
9
  description="OpenRouter API call using OpenAI Python SDK"
10
10
  )
11
+ # The positional argument is interpreted as either a prompt or session_id depending on context
11
12
  parser.add_argument(
12
- "prompt", type=str, nargs="?", help="Prompt to send to the model"
13
+ "input_arg",
14
+ type=str,
15
+ nargs="?",
16
+ help="Prompt to send to the model, or session ID if --continue is used.",
17
+ )
18
+
19
+ parser.add_argument(
20
+ "--list",
21
+ nargs="?",
22
+ type=int,
23
+ const=10,
24
+ default=None,
25
+ help="List the last N sessions (default: 10) and exit.",
26
+ )
27
+ parser.add_argument(
28
+ "--view",
29
+ type=str,
30
+ default=None,
31
+ help="View the content of a conversation history by session id and exit.",
32
+ )
33
+ parser.add_argument(
34
+ "--set-provider-config",
35
+ nargs=3,
36
+ metavar=("NAME", "KEY", "VALUE"),
37
+ help="Set a provider config parameter (e.g., --set-provider-config openai api_key sk-xxx).",
13
38
  )
14
39
  parser.add_argument(
15
40
  "--lang",
@@ -18,11 +43,16 @@ def create_parser():
18
43
  help="Language for interface messages (e.g., en, pt). Overrides config if set.",
19
44
  )
20
45
 
46
+ parser.add_argument(
47
+ "--app-shell",
48
+ action="store_true",
49
+ help="Use the new prompt_toolkit Application-based chat shell (experimental)",
50
+ )
21
51
  parser.add_argument(
22
52
  "--max-tokens",
23
53
  type=int,
24
54
  default=None,
25
- help="Maximum tokens for model response (overrides config, default: 200000)",
55
+ help="Maximum tokens for model response (overrides config, default: 32000)",
26
56
  )
27
57
  parser.add_argument(
28
58
  "--max-tools",
@@ -154,12 +184,19 @@ def create_parser():
154
184
  )
155
185
  parser.add_argument(
156
186
  "--continue-session",
187
+ "--continue",
157
188
  action="store_true",
158
- help="Continue from the last saved conversation",
189
+ default=False,
190
+ help="Continue from a saved conversation. Uses the session ID from the positional argument if provided, otherwise resumes the most recent session.",
159
191
  )
160
192
  parser.add_argument(
161
193
  "--web", action="store_true", help="Launch the Janito web server instead of CLI"
162
194
  )
195
+ parser.add_argument(
196
+ "--live",
197
+ action="store_true",
198
+ help="Launch the Janito live reload server for web development",
199
+ )
163
200
  parser.add_argument(
164
201
  "--config-reset-local",
165
202
  action="store_true",
@@ -175,6 +212,11 @@ def create_parser():
175
212
  action="store_true",
176
213
  help="Print all agent events before dispatching to the message handler (for debugging)",
177
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
+ )
178
220
  parser.add_argument(
179
221
  "-V",
180
222
  "--vanilla",
@@ -194,16 +236,6 @@ def create_parser():
194
236
  default=None,
195
237
  help="Agent Profile name (only 'base' is supported)",
196
238
  )
197
- parser.add_argument(
198
- "--stream",
199
- action="store_true",
200
- help="Enable OpenAI streaming mode (yields tokens as they arrive)",
201
- )
202
- parser.add_argument(
203
- "--verbose-stream",
204
- action="store_true",
205
- help="Print raw chunks as they are fetched from OpenAI (for debugging)",
206
- )
207
239
  parser.add_argument(
208
240
  "--no-termweb",
209
241
  action="store_true",
@@ -219,6 +251,17 @@ def create_parser():
219
251
  "-i",
220
252
  "--info",
221
253
  action="store_true",
222
- help="Exibe informações básicas do programa e sai (útil para execução única em shell)",
254
+ help="Show basic program info and exit (useful for one-shot shell execution)",
255
+ )
256
+ parser.add_argument(
257
+ "--ntt",
258
+ action="store_true",
259
+ help="Disable tool call reason tracking (no tools tracking)",
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.",
223
266
  )
224
267
  return parser