janito 1.8.0__py3-none-any.whl → 1.9.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. janito/__init__.py +1 -1
  2. janito/agent/config_defaults.py +23 -0
  3. janito/agent/config_utils.py +0 -9
  4. janito/agent/conversation.py +31 -9
  5. janito/agent/conversation_api.py +32 -2
  6. janito/agent/conversation_history.py +53 -0
  7. janito/agent/conversation_tool_calls.py +11 -8
  8. janito/agent/openai_client.py +11 -3
  9. janito/agent/openai_schema_generator.py +9 -6
  10. janito/agent/providers.py +77 -0
  11. janito/agent/rich_message_handler.py +1 -1
  12. janito/agent/templates/profiles/system_prompt_template_base.txt.j2 +8 -8
  13. janito/agent/tool_executor.py +18 -10
  14. janito/agent/tool_use_tracker.py +16 -0
  15. janito/agent/tools/__init__.py +7 -9
  16. janito/agent/tools/create_directory.py +7 -6
  17. janito/agent/tools/create_file.py +29 -54
  18. janito/agent/tools/delete_text_in_file.py +97 -0
  19. janito/agent/tools/fetch_url.py +11 -3
  20. janito/agent/tools/find_files.py +37 -25
  21. janito/agent/tools/get_file_outline/__init__.py +1 -0
  22. janito/agent/tools/{outline_file/__init__.py → get_file_outline/core.py} +12 -15
  23. janito/agent/tools/get_file_outline/python_outline.py +134 -0
  24. janito/agent/tools/{search_outline.py → get_file_outline/search_outline.py} +9 -0
  25. janito/agent/tools/get_lines.py +15 -11
  26. janito/agent/tools/move_file.py +10 -11
  27. janito/agent/tools/remove_directory.py +2 -2
  28. janito/agent/tools/remove_file.py +11 -13
  29. janito/agent/tools/replace_file.py +62 -0
  30. janito/agent/tools/replace_text_in_file.py +3 -3
  31. janito/agent/tools/run_bash_command.py +3 -7
  32. janito/agent/tools/run_powershell_command.py +39 -28
  33. janito/agent/tools/run_python_command.py +3 -5
  34. janito/agent/tools/search_text.py +10 -14
  35. janito/agent/tools/validate_file_syntax/__init__.py +1 -0
  36. janito/agent/tools/validate_file_syntax/core.py +92 -0
  37. janito/agent/tools/validate_file_syntax/css_validator.py +35 -0
  38. janito/agent/tools/validate_file_syntax/html_validator.py +77 -0
  39. janito/agent/tools/validate_file_syntax/js_validator.py +27 -0
  40. janito/agent/tools/validate_file_syntax/json_validator.py +6 -0
  41. janito/agent/tools/validate_file_syntax/markdown_validator.py +66 -0
  42. janito/agent/tools/validate_file_syntax/ps1_validator.py +32 -0
  43. janito/agent/tools/validate_file_syntax/python_validator.py +5 -0
  44. janito/agent/tools/validate_file_syntax/xml_validator.py +11 -0
  45. janito/agent/tools/validate_file_syntax/yaml_validator.py +6 -0
  46. janito/agent/tools_utils/__init__.py +1 -0
  47. janito/agent/tools_utils/dir_walk_utils.py +23 -0
  48. janito/agent/{tools/outline_file → tools_utils}/formatting.py +5 -2
  49. janito/agent/{tools → tools_utils}/gitignore_utils.py +0 -3
  50. janito/agent/tools_utils/utils.py +30 -0
  51. janito/cli/_livereload_log_utils.py +13 -0
  52. janito/cli/arg_parser.py +45 -3
  53. janito/cli/{runner/cli_main.py → cli_main.py} +120 -20
  54. janito/cli/livereload_starter.py +60 -0
  55. janito/cli/main.py +110 -21
  56. janito/cli/one_shot.py +66 -0
  57. janito/cli/termweb_starter.py +2 -2
  58. janito/livereload/app.py +25 -0
  59. janito/rich_utils.py +0 -22
  60. janito/{cli_chat_shell → shell}/commands/__init__.py +18 -11
  61. janito/{cli_chat_shell → shell}/commands/config.py +4 -4
  62. janito/shell/commands/conversation_restart.py +72 -0
  63. janito/shell/commands/edit.py +21 -0
  64. janito/shell/commands/history_view.py +18 -0
  65. janito/shell/commands/livelogs.py +40 -0
  66. janito/{cli_chat_shell → shell}/commands/prompt.py +10 -6
  67. janito/shell/commands/session.py +32 -0
  68. janito/{cli_chat_shell → shell}/commands/session_control.py +2 -7
  69. janito/{cli_chat_shell → shell}/commands/sum.py +6 -6
  70. janito/{cli_chat_shell → shell}/commands/termweb_log.py +10 -10
  71. janito/shell/commands/tools.py +23 -0
  72. janito/{cli_chat_shell → shell}/commands/utility.py +5 -4
  73. janito/{cli_chat_shell → shell}/commands/verbose.py +1 -1
  74. janito/shell/commands.py +40 -0
  75. janito/shell/main.py +321 -0
  76. janito/{cli_chat_shell/shell_command_completer.py → shell/prompt/completer.py} +1 -1
  77. janito/{cli_chat_shell/chat_ui.py → shell/prompt/session_setup.py} +19 -5
  78. janito/{cli_chat_shell/session_manager.py → shell/session/manager.py} +53 -3
  79. janito/{cli_chat_shell/ui.py → shell/ui/interactive.py} +23 -15
  80. janito/termweb/app.py +3 -3
  81. janito/termweb/static/editor.css +146 -0
  82. janito/termweb/static/editor.css.bak +27 -0
  83. janito/termweb/static/editor.html +15 -213
  84. janito/termweb/static/editor.html.bak +16 -215
  85. janito/termweb/static/editor.js +209 -0
  86. janito/termweb/static/editor.js.bak +227 -0
  87. janito/termweb/static/index.html +2 -3
  88. janito/termweb/static/index.html.bak +2 -3
  89. janito/termweb/static/termweb.css.bak +33 -84
  90. janito/termweb/static/termweb.js +15 -34
  91. janito/termweb/static/termweb.js.bak +18 -36
  92. {janito-1.8.0.dist-info → janito-1.9.0.dist-info}/METADATA +6 -3
  93. janito-1.9.0.dist-info/RECORD +151 -0
  94. {janito-1.8.0.dist-info → janito-1.9.0.dist-info}/WHEEL +1 -1
  95. janito/agent/tools/dir_walk_utils.py +0 -16
  96. janito/agent/tools/memory.py +0 -48
  97. janito/agent/tools/outline_file/python_outline.py +0 -71
  98. janito/agent/tools/present_choices_test.py +0 -18
  99. janito/agent/tools/rich_live.py +0 -44
  100. janito/agent/tools/tools_utils.py +0 -56
  101. janito/agent/tools/utils.py +0 -33
  102. janito/agent/tools/validate_file_syntax.py +0 -163
  103. janito/cli_chat_shell/chat_loop.py +0 -163
  104. janito/cli_chat_shell/chat_state.py +0 -38
  105. janito/cli_chat_shell/commands/history_start.py +0 -37
  106. janito/cli_chat_shell/commands/session.py +0 -48
  107. janito-1.8.0.dist-info/RECORD +0 -127
  108. /janito/agent/tools/{outline_file → get_file_outline}/markdown_outline.py +0 -0
  109. /janito/cli/{runner/_termweb_log_utils.py → _termweb_log_utils.py} +0 -0
  110. /janito/cli/{runner/config.py → config_runner.py} +0 -0
  111. /janito/cli/{runner/formatting.py → formatting_runner.py} +0 -0
  112. /janito/{cli/runner → shell}/__init__.py +0 -0
  113. /janito/{cli_chat_shell → shell}/commands/lang.py +0 -0
  114. /janito/{cli_chat_shell → shell/prompt}/load_prompt.py +0 -0
  115. /janito/{cli_chat_shell/config_shell.py → shell/session/config.py} +0 -0
  116. /janito/{cli_chat_shell/__init__.py → shell/session/history.py} +0 -0
  117. {janito-1.8.0.dist-info → janito-1.9.0.dist-info}/entry_points.txt +0 -0
  118. {janito-1.8.0.dist-info → janito-1.9.0.dist-info}/licenses/LICENSE +0 -0
  119. {janito-1.8.0.dist-info → janito-1.9.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,6 @@
1
+ def validate_json(file_path: str) -> str:
2
+ import json
3
+
4
+ with open(file_path, "r", encoding="utf-8") as f:
5
+ json.load(f)
6
+ return "✅ Syntax valid"
@@ -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,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
- "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 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
- 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",
@@ -219,6 +256,11 @@ def create_parser():
219
256
  "-i",
220
257
  "--info",
221
258
  action="store_true",
222
- help="Exibe informações básicas do programa e sai (útil para execução única em shell)",
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
- # --- Liteweb integration ---
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
- print(
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
- print(f"[red]Port {requested_port} is not available for termweb.[/red]")
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
- if not getattr(args, "prompt", None):
122
- from janito.cli_chat_shell.chat_loop import start_chat_shell
123
-
124
- start_chat_shell(
125
- profile_manager,
126
- continue_session=getattr(args, "continue_session", False),
127
- termweb_stdout_path=(
128
- termweb_stdout_path if "termweb_stdout_path" in locals() else None
129
- ),
130
- termweb_stderr_path=(
131
- termweb_stderr_path if "termweb_stderr_path" in locals() else None
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
- sys.exit(0)
208
+ sys.exit(0)
135
209
  # --- Prompt mode ---
136
- prompt = args.prompt
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 = 50
159
- profile_manager.agent.chat(
160
- messages,
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