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
@@ -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,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 @@
|
|
1
|
+
# tools_utils package init
|
@@ -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}]")
|
janito/cli/_print_config.py
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
import os
|
2
|
-
from janito.rich_utils import
|
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
|
-
|
20
|
-
|
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
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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]
|
90
|
+
local_items, color_label="[cyan]\U0001f3e0 Local Configuration[/cyan]"
|
66
91
|
)
|
67
92
|
print_config_items(
|
68
|
-
global_items, color_label="[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
|
-
|
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
|
-
"
|
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:
|
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
|
-
|
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="
|
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
|