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.
- janito/__init__.py +1 -1
- janito/agent/config_defaults.py +23 -0
- janito/agent/config_utils.py +0 -9
- janito/agent/conversation.py +31 -9
- janito/agent/conversation_api.py +32 -2
- janito/agent/conversation_history.py +53 -0
- janito/agent/conversation_tool_calls.py +11 -8
- janito/agent/openai_client.py +11 -3
- janito/agent/openai_schema_generator.py +9 -6
- janito/agent/providers.py +77 -0
- janito/agent/rich_message_handler.py +1 -1
- janito/agent/templates/profiles/system_prompt_template_base.txt.j2 +8 -8
- janito/agent/tool_executor.py +18 -10
- janito/agent/tool_use_tracker.py +16 -0
- janito/agent/tools/__init__.py +7 -9
- janito/agent/tools/create_directory.py +7 -6
- janito/agent/tools/create_file.py +29 -54
- janito/agent/tools/delete_text_in_file.py +97 -0
- janito/agent/tools/fetch_url.py +11 -3
- janito/agent/tools/find_files.py +37 -25
- janito/agent/tools/get_file_outline/__init__.py +1 -0
- janito/agent/tools/{outline_file/__init__.py → get_file_outline/core.py} +12 -15
- janito/agent/tools/get_file_outline/python_outline.py +134 -0
- janito/agent/tools/{search_outline.py → get_file_outline/search_outline.py} +9 -0
- janito/agent/tools/get_lines.py +15 -11
- janito/agent/tools/move_file.py +10 -11
- janito/agent/tools/remove_directory.py +2 -2
- janito/agent/tools/remove_file.py +11 -13
- janito/agent/tools/replace_file.py +62 -0
- janito/agent/tools/replace_text_in_file.py +3 -3
- janito/agent/tools/run_bash_command.py +3 -7
- janito/agent/tools/run_powershell_command.py +39 -28
- janito/agent/tools/run_python_command.py +3 -5
- janito/agent/tools/search_text.py +10 -14
- janito/agent/tools/validate_file_syntax/__init__.py +1 -0
- janito/agent/tools/validate_file_syntax/core.py +92 -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/dir_walk_utils.py +23 -0
- janito/agent/{tools/outline_file → tools_utils}/formatting.py +5 -2
- janito/agent/{tools → tools_utils}/gitignore_utils.py +0 -3
- janito/agent/tools_utils/utils.py +30 -0
- janito/cli/_livereload_log_utils.py +13 -0
- janito/cli/arg_parser.py +45 -3
- janito/cli/{runner/cli_main.py → cli_main.py} +120 -20
- janito/cli/livereload_starter.py +60 -0
- janito/cli/main.py +110 -21
- janito/cli/one_shot.py +66 -0
- janito/cli/termweb_starter.py +2 -2
- janito/livereload/app.py +25 -0
- janito/rich_utils.py +0 -22
- janito/{cli_chat_shell → shell}/commands/__init__.py +18 -11
- janito/{cli_chat_shell → shell}/commands/config.py +4 -4
- janito/shell/commands/conversation_restart.py +72 -0
- janito/shell/commands/edit.py +21 -0
- janito/shell/commands/history_view.py +18 -0
- janito/shell/commands/livelogs.py +40 -0
- janito/{cli_chat_shell → shell}/commands/prompt.py +10 -6
- janito/shell/commands/session.py +32 -0
- janito/{cli_chat_shell → shell}/commands/session_control.py +2 -7
- janito/{cli_chat_shell → shell}/commands/sum.py +6 -6
- janito/{cli_chat_shell → shell}/commands/termweb_log.py +10 -10
- janito/shell/commands/tools.py +23 -0
- janito/{cli_chat_shell → shell}/commands/utility.py +5 -4
- janito/{cli_chat_shell → shell}/commands/verbose.py +1 -1
- janito/shell/commands.py +40 -0
- janito/shell/main.py +321 -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/{cli_chat_shell/session_manager.py → shell/session/manager.py} +53 -3
- janito/{cli_chat_shell/ui.py → shell/ui/interactive.py} +23 -15
- janito/termweb/app.py +3 -3
- janito/termweb/static/editor.css +146 -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-1.8.0.dist-info → janito-1.9.0.dist-info}/METADATA +6 -3
- janito-1.9.0.dist-info/RECORD +151 -0
- {janito-1.8.0.dist-info → janito-1.9.0.dist-info}/WHEEL +1 -1
- janito/agent/tools/dir_walk_utils.py +0 -16
- janito/agent/tools/memory.py +0 -48
- 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/tools_utils.py +0 -56
- janito/agent/tools/utils.py +0 -33
- janito/agent/tools/validate_file_syntax.py +0 -163
- 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-1.8.0.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}/commands/lang.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.0.dist-info → janito-1.9.0.dist-info}/entry_points.txt +0 -0
- {janito-1.8.0.dist-info → janito-1.9.0.dist-info}/licenses/LICENSE +0 -0
- {janito-1.8.0.dist-info → janito-1.9.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,23 @@
|
|
1
|
+
import os
|
2
|
+
from .gitignore_utils import filter_ignored
|
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
|
+
for root, dirs, files in os.walk(root_dir):
|
15
|
+
rel_path = os.path.relpath(root, root_dir)
|
16
|
+
depth = 0 if rel_path == "." else rel_path.count(os.sep) + 1
|
17
|
+
if max_depth is not None:
|
18
|
+
if depth >= max_depth:
|
19
|
+
# For max_depth=1, only root (depth=0). For max_depth=2, root and one level below (depth=0,1).
|
20
|
+
if depth > 0:
|
21
|
+
continue
|
22
|
+
dirs, files = filter_ignored(root, dirs, files)
|
23
|
+
yield root, dirs, files
|
@@ -1,11 +1,14 @@
|
|
1
1
|
def format_outline_table(outline_items):
|
2
2
|
if not outline_items:
|
3
3
|
return "No classes, functions, or variables found."
|
4
|
-
header = "| Type | Name | Start | End | Parent |\n
|
4
|
+
header = "| Type | Name | Start | End | Parent | Docstring |\n|---------|-------------|-------|-----|----------|--------------------------|"
|
5
5
|
rows = []
|
6
6
|
for item in outline_items:
|
7
|
+
docstring = item.get("docstring", "").replace("\n", " ")
|
8
|
+
if len(docstring) > 24:
|
9
|
+
docstring = docstring[:21] + "..."
|
7
10
|
rows.append(
|
8
|
-
f"| {item['type']:<7} | {item['name']:<11} | {item['start']:<5} | {item['end']:<3} | {item['parent']:<8} |"
|
11
|
+
f"| {item['type']:<7} | {item['name']:<11} | {item['start']:<5} | {item['end']:<3} | {item['parent']:<8} | {docstring:<24} |"
|
9
12
|
)
|
10
13
|
return header + "\n" + "\n".join(rows)
|
11
14
|
|
@@ -1,13 +1,11 @@
|
|
1
1
|
import os
|
2
2
|
import pathspec
|
3
|
-
from janito.agent.tools.utils import expand_path
|
4
3
|
|
5
4
|
_spec = None
|
6
5
|
|
7
6
|
|
8
7
|
def load_gitignore_patterns(gitignore_path=".gitignore"):
|
9
8
|
global _spec
|
10
|
-
gitignore_path = expand_path(gitignore_path)
|
11
9
|
if not os.path.exists(gitignore_path):
|
12
10
|
_spec = pathspec.PathSpec.from_lines("gitwildmatch", [])
|
13
11
|
return _spec
|
@@ -19,7 +17,6 @@ def load_gitignore_patterns(gitignore_path=".gitignore"):
|
|
19
17
|
|
20
18
|
def is_ignored(path):
|
21
19
|
global _spec
|
22
|
-
path = expand_path(path)
|
23
20
|
if _spec is None:
|
24
21
|
_spec = load_gitignore_patterns()
|
25
22
|
# Normalize path to be relative and use forward slashes
|
@@ -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/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 openrouter.ai api_key sk-xxx).",
|
13
38
|
)
|
14
39
|
parser.add_argument(
|
15
40
|
"--lang",
|
@@ -18,6 +43,11 @@ 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,
|
@@ -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",
|
@@ -219,6 +256,11 @@ def create_parser():
|
|
219
256
|
"-i",
|
220
257
|
"--info",
|
221
258
|
action="store_true",
|
222
|
-
help="
|
259
|
+
help="Show basic program info and exit (useful for one-shot shell execution)",
|
260
|
+
)
|
261
|
+
parser.add_argument(
|
262
|
+
"--ntt",
|
263
|
+
action="store_true",
|
264
|
+
help="Disable tool call reason tracking (no tools tracking)",
|
223
265
|
)
|
224
266
|
return parser
|
@@ -9,6 +9,7 @@ from janito.agent.conversation_exceptions import (
|
|
9
9
|
EmptyResponseError,
|
10
10
|
ProviderError,
|
11
11
|
)
|
12
|
+
from janito.shell.main import start_chat_shell
|
12
13
|
|
13
14
|
|
14
15
|
def is_port_free(port):
|
@@ -25,6 +26,9 @@ def run_cli(args):
|
|
25
26
|
if getattr(args, "vanilla", False):
|
26
27
|
runtime_config.set("vanilla_mode", True)
|
27
28
|
|
29
|
+
# Set no_tools_tracking if --ntt is passed
|
30
|
+
if getattr(args, "ntt", False):
|
31
|
+
runtime_config.set("no_tools_tracking", True)
|
28
32
|
# Normalize all verbose flags into runtime_config
|
29
33
|
for flag in [
|
30
34
|
"verbose_http",
|
@@ -77,13 +81,14 @@ def run_cli(args):
|
|
77
81
|
if getattr(args, "verbose_reason", False):
|
78
82
|
runtime_config.set("verbose_reason", True)
|
79
83
|
|
80
|
-
# ---
|
84
|
+
# --- termweb integration ---
|
81
85
|
termweb_proc = None
|
82
86
|
selected_port = None
|
83
87
|
if (
|
84
88
|
not getattr(args, "no_termweb", False)
|
85
89
|
and interaction_mode == "chat"
|
86
90
|
and not runtime_config.get("vanilla_mode", False)
|
91
|
+
and not getattr(args, "input_arg", None) # Prevent termweb in one-shot mode
|
87
92
|
):
|
88
93
|
default_port = 8088
|
89
94
|
max_port = 8100
|
@@ -94,13 +99,21 @@ def run_cli(args):
|
|
94
99
|
selected_port = port
|
95
100
|
break
|
96
101
|
if selected_port is None:
|
97
|
-
|
102
|
+
from rich.console import Console
|
103
|
+
|
104
|
+
console = Console()
|
105
|
+
console.print(
|
98
106
|
f"[red]No free port found for termweb in range {default_port}-{max_port}.[/red]"
|
99
107
|
)
|
100
108
|
sys.exit(1)
|
101
109
|
else:
|
102
110
|
if not is_port_free(requested_port):
|
103
|
-
|
111
|
+
from rich.console import Console
|
112
|
+
|
113
|
+
console = Console()
|
114
|
+
console.print(
|
115
|
+
f"[red]Port {requested_port} is not available for termweb.[/red]"
|
116
|
+
)
|
104
117
|
sys.exit(1)
|
105
118
|
selected_port = requested_port
|
106
119
|
runtime_config.set("termweb_port", selected_port)
|
@@ -118,22 +131,83 @@ def run_cli(args):
|
|
118
131
|
|
119
132
|
# --- End termweb integration ---
|
120
133
|
try:
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
134
|
+
livereload_stdout_path = None
|
135
|
+
livereload_stderr_path = None
|
136
|
+
continue_session = False
|
137
|
+
session_id = None
|
138
|
+
if getattr(args, "input_arg", None):
|
139
|
+
from janito.cli.one_shot import run_oneshot_mode
|
140
|
+
|
141
|
+
run_oneshot_mode(args, profile_manager, runtime_config)
|
142
|
+
return
|
143
|
+
if not getattr(args, "input_arg", None) or getattr(
|
144
|
+
args, "continue_session", False
|
145
|
+
):
|
146
|
+
# Determine continue_session and session_id
|
147
|
+
_cont = getattr(args, "continue_session", False)
|
148
|
+
if _cont:
|
149
|
+
continue_session = True
|
150
|
+
session_id = getattr(args, "input_arg", None)
|
151
|
+
if session_id is None:
|
152
|
+
# Find the most recent session id from .janito/chat_history/*.json
|
153
|
+
import os
|
154
|
+
import glob
|
155
|
+
|
156
|
+
chat_hist_dir = (
|
157
|
+
os.path.join(os.path.expanduser("~"), ".janito", "chat_history")
|
158
|
+
if not os.path.isabs(".janito")
|
159
|
+
else os.path.join(".janito", "chat_history")
|
160
|
+
)
|
161
|
+
if not os.path.exists(chat_hist_dir):
|
162
|
+
session_id = None
|
163
|
+
else:
|
164
|
+
files = glob.glob(os.path.join(chat_hist_dir, "*.json"))
|
165
|
+
if files:
|
166
|
+
latest = max(files, key=os.path.getmtime)
|
167
|
+
session_id = os.path.splitext(os.path.basename(latest))[0]
|
168
|
+
else:
|
169
|
+
session_id = None
|
170
|
+
else:
|
171
|
+
continue_session = False
|
172
|
+
session_id = None
|
173
|
+
import time
|
174
|
+
|
175
|
+
info_start_time = None
|
176
|
+
if getattr(args, "info", False):
|
177
|
+
info_start_time = time.time()
|
178
|
+
usage_info = start_chat_shell(
|
179
|
+
profile_manager,
|
180
|
+
continue_session=continue_session,
|
181
|
+
session_id=session_id,
|
182
|
+
termweb_stdout_path=(
|
183
|
+
termweb_stdout_path if "termweb_stdout_path" in locals() else None
|
184
|
+
),
|
185
|
+
termweb_stderr_path=(
|
186
|
+
termweb_stderr_path if "termweb_stderr_path" in locals() else None
|
187
|
+
),
|
188
|
+
livereload_stdout_path=(
|
189
|
+
livereload_stdout_path if "livereload_stdout_path" in locals() else None
|
190
|
+
),
|
191
|
+
livereload_stderr_path=(
|
192
|
+
livereload_stderr_path if "livereload_stderr_path" in locals() else None
|
193
|
+
),
|
194
|
+
)
|
195
|
+
if (
|
196
|
+
getattr(args, "info", False)
|
197
|
+
and usage_info is not None
|
198
|
+
and info_start_time is not None
|
199
|
+
):
|
200
|
+
elapsed = time.time() - info_start_time
|
201
|
+
from rich.console import Console
|
202
|
+
|
203
|
+
console = Console()
|
204
|
+
total_tokens = usage_info.get("total_tokens")
|
205
|
+
console.print(
|
206
|
+
f"[bold green]Total tokens used:[/] [yellow]{total_tokens}[/yellow] [bold green]| Elapsed time:[/] [yellow]{elapsed:.2f}s[/yellow]"
|
133
207
|
)
|
134
|
-
|
208
|
+
sys.exit(0)
|
135
209
|
# --- Prompt mode ---
|
136
|
-
prompt = args
|
210
|
+
prompt = getattr(args, "input_arg", None)
|
137
211
|
from rich.console import Console
|
138
212
|
from janito.agent.rich_message_handler import RichMessageHandler
|
139
213
|
|
@@ -154,15 +228,41 @@ def run_cli(args):
|
|
154
228
|
{"role": "system", "content": profile_manager.system_prompt_template}
|
155
229
|
)
|
156
230
|
messages.append({"role": "user", "content": prompt})
|
231
|
+
import time
|
232
|
+
|
233
|
+
info_start_time = None
|
234
|
+
if getattr(args, "info", False):
|
235
|
+
info_start_time = time.time()
|
157
236
|
try:
|
158
|
-
max_rounds =
|
159
|
-
|
160
|
-
|
237
|
+
max_rounds = 100
|
238
|
+
from janito.agent.conversation_history import ConversationHistory
|
239
|
+
|
240
|
+
result = profile_manager.agent.chat(
|
241
|
+
ConversationHistory(messages),
|
161
242
|
message_handler=message_handler,
|
162
243
|
spinner=True,
|
163
244
|
max_rounds=max_rounds,
|
164
245
|
stream=getattr(args, "stream", False),
|
165
246
|
)
|
247
|
+
if (
|
248
|
+
getattr(args, "info", False)
|
249
|
+
and info_start_time is not None
|
250
|
+
and result is not None
|
251
|
+
):
|
252
|
+
usage_info = result.get("usage")
|
253
|
+
total_tokens = usage_info.get("total_tokens") if usage_info else None
|
254
|
+
prompt_tokens = usage_info.get("prompt_tokens") if usage_info else None
|
255
|
+
completion_tokens = (
|
256
|
+
usage_info.get("completion_tokens") if usage_info else None
|
257
|
+
)
|
258
|
+
elapsed = time.time() - info_start_time
|
259
|
+
from rich.console import Console
|
260
|
+
|
261
|
+
console = Console()
|
262
|
+
console.print(
|
263
|
+
f"[bold green]Total tokens:[/] [yellow]{total_tokens}[/yellow] [bold green]| Input:[/] [cyan]{prompt_tokens}[/cyan] [bold green]| Output:[/] [magenta]{completion_tokens}[/magenta] [bold green]| Elapsed:[/] [yellow]{elapsed:.2f}s[/yellow]",
|
264
|
+
style="dim",
|
265
|
+
)
|
166
266
|
except MaxRoundsExceededError:
|
167
267
|
console.print("[red]Max conversation rounds exceeded.[/red]")
|
168
268
|
except ProviderError as e:
|
@@ -0,0 +1,60 @@
|
|
1
|
+
import sys
|
2
|
+
import subprocess
|
3
|
+
import tempfile
|
4
|
+
import time
|
5
|
+
import http.client
|
6
|
+
import os
|
7
|
+
from rich.console import Console
|
8
|
+
|
9
|
+
|
10
|
+
def wait_for_livereload(port, timeout=3.0):
|
11
|
+
deadline = time.time() + timeout
|
12
|
+
while time.time() < deadline:
|
13
|
+
try:
|
14
|
+
conn = http.client.HTTPConnection("localhost", port, timeout=0.5)
|
15
|
+
conn.request("GET", "/")
|
16
|
+
resp = conn.getresponse()
|
17
|
+
if resp.status in (200, 404):
|
18
|
+
return True
|
19
|
+
except Exception:
|
20
|
+
pass
|
21
|
+
time.sleep(0.1)
|
22
|
+
return False
|
23
|
+
|
24
|
+
|
25
|
+
def start_livereload(selected_port):
|
26
|
+
console = Console()
|
27
|
+
with console.status("[cyan]Starting live reload server...", spinner="dots"):
|
28
|
+
app_py_path = os.path.join(
|
29
|
+
os.path.dirname(__file__), "..", "livereload", "app.py"
|
30
|
+
)
|
31
|
+
app_py_path = os.path.abspath(app_py_path)
|
32
|
+
if not os.path.isfile(app_py_path):
|
33
|
+
console.print("[red]Could not find livereload app.py![/red]")
|
34
|
+
return None, False, None, None
|
35
|
+
livereload_stdout = tempfile.NamedTemporaryFile(
|
36
|
+
delete=False, mode="w+", encoding="utf-8"
|
37
|
+
)
|
38
|
+
livereload_stderr = tempfile.NamedTemporaryFile(
|
39
|
+
delete=False, mode="w+", encoding="utf-8"
|
40
|
+
)
|
41
|
+
livereload_proc = subprocess.Popen(
|
42
|
+
[sys.executable, app_py_path, "--port", str(selected_port)],
|
43
|
+
stdout=livereload_stdout,
|
44
|
+
stderr=livereload_stderr,
|
45
|
+
)
|
46
|
+
if wait_for_livereload(selected_port, timeout=3.0):
|
47
|
+
console.print(
|
48
|
+
f"LiveReload started... Available at http://localhost:{selected_port}"
|
49
|
+
)
|
50
|
+
return livereload_proc, True, livereload_stdout.name, livereload_stderr.name
|
51
|
+
else:
|
52
|
+
livereload_proc.terminate()
|
53
|
+
livereload_proc.wait()
|
54
|
+
from janito.cli._livereload_log_utils import print_livereload_logs
|
55
|
+
|
56
|
+
console.print(
|
57
|
+
f"[red]Failed to start LiveReload on port {selected_port}. Check logs for details.[/red]"
|
58
|
+
)
|
59
|
+
print_livereload_logs(livereload_stdout.name, livereload_stderr.name)
|
60
|
+
return None, False, livereload_stdout.name, livereload_stderr.name
|