janito 2.5.1__py3-none-any.whl → 2.6.1__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 (68) hide show
  1. janito/__init__.py +7 -6
  2. janito/__main__.py +5 -4
  3. janito/_version.py +58 -57
  4. janito/agent/setup_agent.py +241 -223
  5. janito/agent/templates/profiles/system_prompt_template_software developer.txt.j2 +39 -0
  6. janito/cli/__init__.py +10 -9
  7. janito/cli/chat_mode/bindings.py +38 -62
  8. janito/cli/chat_mode/chat_entry.py +23 -22
  9. janito/cli/chat_mode/prompt_style.py +25 -24
  10. janito/cli/chat_mode/script_runner.py +154 -153
  11. janito/cli/chat_mode/session.py +32 -44
  12. janito/cli/chat_mode/session_profile_select.py +125 -55
  13. janito/cli/chat_mode/shell/commands/tools.py +51 -48
  14. janito/cli/chat_mode/toolbar.py +42 -68
  15. janito/cli/cli_commands/list_tools.py +41 -56
  16. janito/cli/cli_commands/show_system_prompt.py +70 -49
  17. janito/cli/core/runner.py +6 -1
  18. janito/cli/core/setters.py +43 -34
  19. janito/cli/main_cli.py +25 -1
  20. janito/cli/prompt_core.py +76 -69
  21. janito/cli/rich_terminal_reporter.py +22 -1
  22. janito/cli/single_shot_mode/handler.py +95 -94
  23. janito/drivers/driver_registry.py +27 -29
  24. janito/drivers/openai/driver.py +436 -494
  25. janito/llm/agent.py +54 -68
  26. janito/provider_registry.py +178 -178
  27. janito/providers/anthropic/model_info.py +41 -22
  28. janito/providers/anthropic/provider.py +80 -67
  29. janito/providers/provider_static_info.py +18 -17
  30. janito/tools/adapters/local/__init__.py +66 -65
  31. janito/tools/adapters/local/adapter.py +79 -18
  32. janito/tools/adapters/local/create_directory.py +9 -9
  33. janito/tools/adapters/local/create_file.py +12 -12
  34. janito/tools/adapters/local/delete_text_in_file.py +16 -16
  35. janito/tools/adapters/local/find_files.py +2 -2
  36. janito/tools/adapters/local/get_file_outline/core.py +5 -5
  37. janito/tools/adapters/local/get_file_outline/search_outline.py +4 -4
  38. janito/tools/adapters/local/open_html_in_browser.py +15 -15
  39. janito/tools/adapters/local/python_file_run.py +4 -4
  40. janito/tools/adapters/local/read_files.py +40 -0
  41. janito/tools/adapters/local/remove_directory.py +5 -5
  42. janito/tools/adapters/local/remove_file.py +4 -4
  43. janito/tools/adapters/local/replace_text_in_file.py +21 -21
  44. janito/tools/adapters/local/run_bash_command.py +1 -1
  45. janito/tools/adapters/local/search_text/pattern_utils.py +2 -2
  46. janito/tools/adapters/local/search_text/traverse_directory.py +10 -10
  47. janito/tools/adapters/local/validate_file_syntax/core.py +7 -7
  48. janito/tools/adapters/local/validate_file_syntax/css_validator.py +2 -2
  49. janito/tools/adapters/local/validate_file_syntax/html_validator.py +7 -7
  50. janito/tools/adapters/local/validate_file_syntax/js_validator.py +2 -2
  51. janito/tools/adapters/local/validate_file_syntax/json_validator.py +2 -2
  52. janito/tools/adapters/local/validate_file_syntax/markdown_validator.py +2 -2
  53. janito/tools/adapters/local/validate_file_syntax/ps1_validator.py +2 -2
  54. janito/tools/adapters/local/validate_file_syntax/python_validator.py +2 -2
  55. janito/tools/adapters/local/validate_file_syntax/xml_validator.py +2 -2
  56. janito/tools/adapters/local/validate_file_syntax/yaml_validator.py +2 -2
  57. janito/tools/adapters/local/view_file.py +12 -12
  58. janito/tools/path_security.py +204 -0
  59. janito/tools/tool_use_tracker.py +12 -12
  60. janito/tools/tools_adapter.py +66 -34
  61. {janito-2.5.1.dist-info → janito-2.6.1.dist-info}/METADATA +417 -412
  62. {janito-2.5.1.dist-info → janito-2.6.1.dist-info}/RECORD +66 -65
  63. janito/drivers/anthropic/driver.py +0 -113
  64. janito/tools/adapters/local/get_file_outline/python_outline_v2.py +0 -156
  65. {janito-2.5.1.dist-info → janito-2.6.1.dist-info}/WHEEL +0 -0
  66. {janito-2.5.1.dist-info → janito-2.6.1.dist-info}/entry_points.txt +0 -0
  67. {janito-2.5.1.dist-info → janito-2.6.1.dist-info}/licenses/LICENSE +0 -0
  68. {janito-2.5.1.dist-info → janito-2.6.1.dist-info}/top_level.txt +0 -0
@@ -3,16 +3,49 @@ CLI Command: List available tools
3
3
  """
4
4
 
5
5
 
6
+ def _group_tools_by_permission(tools, tool_instances):
7
+ read_only_tools = []
8
+ write_only_tools = []
9
+ read_write_tools = []
10
+ exec_tools = []
11
+ import inspect
12
+ for tool in tools:
13
+ inst = tool_instances.get(tool, None)
14
+ param_names = []
15
+ if inst and hasattr(inst, "run"):
16
+ sig = inspect.signature(inst.run)
17
+ param_names = [p for p in sig.parameters if p != "self"]
18
+ info = {
19
+ "name": tool,
20
+ "params": ", ".join(param_names),
21
+ }
22
+ perms = getattr(inst, "permissions", None)
23
+ if perms and perms.execute:
24
+ exec_tools.append(info)
25
+ elif perms and perms.read and perms.write:
26
+ read_write_tools.append(info)
27
+ elif perms and perms.read:
28
+ read_only_tools.append(info)
29
+ elif perms and perms.write:
30
+ write_only_tools.append(info)
31
+ return read_only_tools, write_only_tools, read_write_tools, exec_tools
32
+
33
+ def _print_tools_table(console, title, tools_info):
34
+ from rich.table import Table
35
+ table = Table(title=title, show_header=True, header_style="bold", show_lines=False, box=None)
36
+ table.add_column("Name", style="cyan", no_wrap=True)
37
+ table.add_column("Parameters", style="yellow")
38
+ for info in tools_info:
39
+ table.add_row(info["name"], info["params"] or "-")
40
+ console.print(table)
41
+
6
42
  def handle_list_tools(args=None):
7
43
  from janito.tools.adapters.local.adapter import LocalToolsAdapter
8
44
  import janito.tools # Ensure all tools are registered
9
-
10
- # Determine permissions from args (default: all False)
11
45
  from janito.tools.tool_base import ToolPermissions
12
46
  read = getattr(args, "read", False) if args else False
13
47
  write = getattr(args, "write", False) if args else False
14
48
  execute = getattr(args, "exec", False) if args else False
15
- # If no permissions are specified, assume user wants to list all tools
16
49
  if not (read or write or execute):
17
50
  read = write = execute = True
18
51
  from janito.tools.permissions import set_global_allowed_permissions
@@ -20,67 +53,19 @@ def handle_list_tools(args=None):
20
53
  registry = janito.tools.get_local_tools_adapter()
21
54
  tools = registry.list_tools()
22
55
  if tools:
23
- from rich.table import Table
24
56
  from rich.console import Console
25
57
  console = Console()
26
- # Get tool instances to check provides_execution and get info
27
58
  tool_instances = {t.tool_name: t for t in registry.get_tools()}
28
- read_only_tools = []
29
- write_only_tools = []
30
- read_write_tools = []
31
- exec_tools = []
32
- for tool in tools:
33
- inst = tool_instances.get(tool, None)
34
- # Extract parameter names from run signature
35
- param_names = []
36
- if inst and hasattr(inst, "run"):
37
- import inspect
38
- sig = inspect.signature(inst.run)
39
- param_names = [p for p in sig.parameters if p != "self"]
40
- info = {
41
- "name": tool,
42
- "params": ", ".join(param_names),
43
- }
44
- perms = getattr(inst, "permissions", None)
45
- if perms and perms.execute:
46
- exec_tools.append(info)
47
- elif perms and perms.read and perms.write:
48
- read_write_tools.append(info)
49
- elif perms and perms.read:
50
- read_only_tools.append(info)
51
- elif perms and perms.write:
52
- write_only_tools.append(info)
53
- # Print each group if not empty
59
+ read_only_tools, write_only_tools, read_write_tools, exec_tools = _group_tools_by_permission(tools, tool_instances)
54
60
  if read_only_tools:
55
- table = Table(title="Read-only tools (-r)", show_header=True, header_style="bold", show_lines=False, box=None)
56
- table.add_column("Name", style="cyan", no_wrap=True)
57
- table.add_column("Parameters", style="yellow")
58
- for info in read_only_tools:
59
- table.add_row(info["name"], info["params"] or "-")
60
- console.print(table)
61
+ _print_tools_table(console, "Read-only tools (-r)", read_only_tools)
61
62
  if write_only_tools:
62
- table = Table(title="Write-only tools (-w)", show_header=True, header_style="bold", show_lines=False, box=None)
63
- table.add_column("Name", style="cyan", no_wrap=True)
64
- table.add_column("Parameters", style="yellow")
65
- for info in write_only_tools:
66
- table.add_row(info["name"], info["params"] or "-")
67
- console.print(table)
63
+ _print_tools_table(console, "Write-only tools (-w)", write_only_tools)
68
64
  if read_write_tools:
69
- table = Table(title="Read-Write tools (-rw)", show_header=True, header_style="bold", show_lines=False, box=None)
70
- table.add_column("Name", style="cyan", no_wrap=True)
71
- table.add_column("Parameters", style="yellow")
72
- for info in read_write_tools:
73
- table.add_row(info["name"], info["params"] or "-")
74
- console.print(table)
65
+ _print_tools_table(console, "Read-Write tools (-rw)", read_write_tools)
75
66
  if exec_tools:
76
- exec_table = Table(title="Execution tools (-x)", show_header=True, header_style="bold", show_lines=False, box=None)
77
- exec_table.add_column("Name", style="cyan", no_wrap=True)
78
- exec_table.add_column("Parameters", style="yellow")
79
- for info in exec_tools:
80
- exec_table.add_row(info["name"], info["params"] or "-")
81
- console.print(exec_table)
67
+ _print_tools_table(console, "Execution tools (-x)", exec_tools)
82
68
  else:
83
69
  print("No tools registered.")
84
70
  import sys
85
-
86
71
  sys.exit(0)
@@ -9,25 +9,10 @@ from janito.platform_discovery import PlatformDiscovery
9
9
  from pathlib import Path
10
10
  from jinja2 import Template
11
11
  import importlib.resources
12
+ import re
12
13
 
13
14
 
14
- def handle_show_system_prompt(args):
15
- # Collect modifiers as in JanitoCLI
16
- from janito.cli.main_cli import MODIFIER_KEYS
17
-
18
- modifiers = {
19
- k: getattr(args, k) for k in MODIFIER_KEYS if getattr(args, k, None) is not None
20
- }
21
- provider, llm_driver_config, agent_role = prepare_llm_driver_config(args, modifiers)
22
- if provider is None or llm_driver_config is None:
23
- print("Error: Could not resolve provider or LLM driver config.")
24
- return
25
-
26
- # Prepare context for Jinja2 rendering
27
- context = {}
28
- context["role"] = agent_role or "developer"
29
- context["profile"] = getattr(args, "profile", None)
30
- # Compute allowed_permissions from CLI args (as in agent setup)
15
+ def _compute_permission_string(args):
31
16
  from janito.tools.tool_base import ToolPermissions
32
17
  read = getattr(args, "read", False)
33
18
  write = getattr(args, "write", False)
@@ -40,63 +25,99 @@ def handle_show_system_prompt(args):
40
25
  perm_str += "w"
41
26
  if allowed.execute:
42
27
  perm_str += "x"
43
- allowed_permissions = perm_str or None
28
+ return perm_str or None
29
+
30
+
31
+ def _prepare_context(args, agent_role, allowed_permissions):
32
+ context = {}
33
+ context["role"] = agent_role or "developer"
34
+ context["profile"] = getattr(args, "profile", None)
44
35
  context["allowed_permissions"] = allowed_permissions
45
- # DEBUG: Show permissions/context before rendering
46
- from rich import print as rich_print
47
- debug_flag = False
48
- import sys
49
- try:
50
- debug_flag = (hasattr(sys, 'argv') and ('--debug' in sys.argv or '--verbose' in sys.argv or '-v' in sys.argv))
51
- except Exception:
52
- pass
53
- if debug_flag:
54
- rich_print(f"[bold magenta][DEBUG][/bold magenta] Rendering system prompt template '[cyan]{template_filename}[/cyan]' with allowed_permissions: [yellow]{allowed_permissions}[/yellow]")
55
- rich_print(f"[bold magenta][DEBUG][/bold magenta] Template context: [green]{context}[/green]")
56
36
  if allowed_permissions and 'x' in allowed_permissions:
57
37
  pd = PlatformDiscovery()
58
38
  context["platform"] = pd.get_platform_name()
59
39
  context["python_version"] = pd.get_python_version()
60
40
  context["shell_info"] = pd.detect_shell()
41
+ return context
61
42
 
62
- # Locate and load the system prompt template
63
- templates_dir = (
64
- Path(__file__).parent.parent.parent / "agent" / "templates" / "profiles"
65
- )
66
- profile = getattr(args, "profile", None)
43
+
44
+ def _load_template(profile, templates_dir):
67
45
  if profile:
68
46
  template_filename = f"system_prompt_template_{profile}.txt.j2"
69
47
  template_path = templates_dir / template_filename
70
48
  else:
71
- # No profile specified means the main agent has no dedicated system prompt template.
72
- print("[janito] No profile specified. The main agent runs without a system prompt template.\n"
73
- "Use --profile PROFILE to view a profile-specific system prompt.")
74
- return
49
+ return None, None
75
50
  template_content = None
76
51
  if template_path and template_path.exists():
77
52
  with open(template_path, "r", encoding="utf-8") as file:
78
53
  template_content = file.read()
79
54
  else:
80
- # Try package import fallback
81
55
  try:
82
56
  with importlib.resources.files("janito.agent.templates.profiles").joinpath(
83
57
  template_filename
84
58
  ).open("r", encoding="utf-8") as file:
85
59
  template_content = file.read()
86
60
  except (FileNotFoundError, ModuleNotFoundError, AttributeError):
87
- if profile:
88
- raise FileNotFoundError(
89
- f"[janito] Could not find profile-specific template '{template_filename}' in {template_path} nor in janito.agent.templates.profiles package."
90
- )
91
- else:
92
- print(
93
- f"[janito] Could not find {template_filename} in {template_path} nor in janito.agent.templates.profiles package."
94
- )
95
- print("No system prompt is set or resolved for this configuration.")
96
- return
61
+ return template_filename, None
62
+ return template_filename, template_content
63
+
64
+
65
+ def _print_debug_info(debug_flag, template_filename, allowed_permissions, context):
66
+ if debug_flag:
67
+ from rich import print as rich_print
68
+ rich_print(f"[bold magenta][DEBUG][/bold magenta] Rendering system prompt template '[cyan]{template_filename}[/cyan]' with allowed_permissions: [yellow]{allowed_permissions}[/yellow]")
69
+ rich_print(f"[bold magenta][DEBUG][/bold magenta] Template context: [green]{context}[/green]")
70
+
71
+
72
+ def handle_show_system_prompt(args):
73
+ from janito.cli.main_cli import MODIFIER_KEYS
74
+
75
+ modifiers = {
76
+ k: getattr(args, k) for k in MODIFIER_KEYS if getattr(args, k, None) is not None
77
+ }
78
+ provider, llm_driver_config, agent_role = prepare_llm_driver_config(args, modifiers)
79
+ if provider is None or llm_driver_config is None:
80
+ print("Error: Could not resolve provider or LLM driver config.")
81
+ return
82
+
83
+ allowed_permissions = _compute_permission_string(args)
84
+ context = _prepare_context(args, agent_role, allowed_permissions)
85
+
86
+ # Debug flag detection
87
+ import sys
88
+ debug_flag = False
89
+ try:
90
+ debug_flag = (hasattr(sys, 'argv') and ('--debug' in sys.argv or '--verbose' in sys.argv or '-v' in sys.argv))
91
+ except Exception:
92
+ pass
93
+
94
+ templates_dir = (
95
+ Path(__file__).parent.parent.parent / "agent" / "templates" / "profiles"
96
+ )
97
+ profile = getattr(args, "profile", None)
98
+ if not profile:
99
+ print("[janito] No profile specified. The main agent runs without a system prompt template.\n"
100
+ "Use --profile PROFILE to view a profile-specific system prompt.")
101
+ return
102
+
103
+ template_filename, template_content = _load_template(profile, templates_dir)
104
+ _print_debug_info(debug_flag, template_filename, allowed_permissions, context)
105
+
106
+ if not template_content:
107
+ if profile:
108
+ raise FileNotFoundError(
109
+ f"[janito] Could not find profile-specific template '{template_filename}' in {templates_dir / template_filename} nor in janito.agent.templates.profiles package."
110
+ )
111
+ else:
112
+ print(
113
+ f"[janito] Could not find {template_filename} in {templates_dir / template_filename} nor in janito.agent.templates.profiles package."
114
+ )
115
+ print("No system prompt is set or resolved for this configuration.")
116
+ return
97
117
 
98
118
  template = Template(template_content)
99
119
  system_prompt = template.render(**context)
120
+ system_prompt = re.sub(r'\n{3,}', '\n\n', system_prompt)
100
121
 
101
122
  print(f"\n--- System Prompt (resolved, profile: {getattr(args, 'profile', 'main')}) ---\n")
102
123
  print(system_prompt)
janito/cli/core/runner.py CHANGED
@@ -92,7 +92,8 @@ def prepare_llm_driver_config(args, modifiers):
92
92
  llm_driver_config = LLMDriverConfig(**driver_config_data)
93
93
  if getattr(llm_driver_config, "verbose_api", None):
94
94
  pass
95
- agent_role = modifiers.get("role", "developer")
95
+ # If both --role and --profile are provided, --role takes precedence for agent_role
96
+ agent_role = modifiers.get("role") or modifiers.get("profile") or "developer"
96
97
  return provider, llm_driver_config, agent_role
97
98
 
98
99
 
@@ -116,7 +117,11 @@ def handle_runner(args, provider, llm_driver_config, agent_role, verbose_tools=F
116
117
  # Store the default permissions for later restoration (e.g., on /restart)
117
118
  from janito.tools.permissions import set_default_allowed_permissions
118
119
  set_default_allowed_permissions(allowed_permissions)
120
+ unrestricted_paths = getattr(args, "unrestricted_paths", False)
119
121
  adapter = janito.tools.get_local_tools_adapter(workdir=getattr(args, "workdir", None))
122
+ if unrestricted_paths:
123
+ # Patch: disable path security enforcement for this adapter instance
124
+ setattr(adapter, "unrestricted_paths", True)
120
125
 
121
126
  # Print allowed permissions in verbose mode
122
127
  if getattr(args, "verbose", False):
@@ -18,47 +18,56 @@ def handle_set(args, config_mgr=None):
18
18
  if not set_arg:
19
19
  return False
20
20
  try:
21
- if "=" not in set_arg:
22
- print(
23
- "Error: --set requires KEY=VALUE (e.g., --set provider=provider_name)."
24
- )
21
+ if not _validate_set_arg_format(set_arg):
25
22
  return True
26
- key, value = set_arg.split("=", 1)
27
- key, value = key.strip(), value.strip()
23
+ key, value = _parse_set_arg(set_arg)
28
24
  key = key.replace("-", "_")
25
+ return _dispatch_set_key(key, value)
26
+ except Exception as e:
27
+ print(f"Error parsing --set value: {e}")
28
+ return True
29
29
 
30
- if key == "provider":
31
- return _handle_set_config_provider(value)
32
- if key == "model":
33
- return _handle_set_global_model(value)
34
- if "." in key and key.endswith(".model"):
35
- return _handle_set_provider_model(key, value)
36
- if key == "max_tokens":
37
- return _handle_set_max_tokens(value)
38
- if key == "base_url":
39
- return _handle_set_base_url(value)
40
- if key in ["azure_deployment_name", "azure-deployment-name"]:
41
- global_config.file_set("azure_deployment_name", value)
42
- print(f"Azure deployment name set to '{value}'.")
43
- return True
44
- if ".max_tokens" in key or ".base_url" in key:
45
- return _handle_set_provider_level_setting(key, value)
46
- # Tool permissions support: janito set tool_permissions=rwx
47
- if key == "tool_permissions":
48
- from janito.tools.permissions_parse import parse_permissions_string
49
- from janito.tools.permissions import set_global_allowed_permissions
50
- perms = parse_permissions_string(value)
51
- global_config.file_set("tool_permissions", value)
52
- set_global_allowed_permissions(perms)
53
- print(f"Tool permissions set to '{value}' (parsed: {perms})")
54
- return True
30
+ def _validate_set_arg_format(set_arg):
31
+ if "=" not in set_arg:
55
32
  print(
56
- f"Error: Unknown config key '{key}'. Supported: provider, model, <provider>.model, max_tokens, base_url, azure_deployment_name, <provider>.max_tokens, <provider>.base_url, <provider>.<model>.max_tokens, <provider>.<model>.base_url, tool_permissions"
33
+ "Error: --set requires KEY=VALUE (e.g., --set provider=provider_name)."
57
34
  )
35
+ return False
36
+ return True
37
+
38
+ def _parse_set_arg(set_arg):
39
+ key, value = set_arg.split("=", 1)
40
+ return key.strip(), value.strip()
41
+
42
+ def _dispatch_set_key(key, value):
43
+ if key == "provider":
44
+ return _handle_set_config_provider(value)
45
+ if key == "model":
46
+ return _handle_set_global_model(value)
47
+ if "." in key and key.endswith(".model"):
48
+ return _handle_set_provider_model(key, value)
49
+ if key == "max_tokens":
50
+ return _handle_set_max_tokens(value)
51
+ if key == "base_url":
52
+ return _handle_set_base_url(value)
53
+ if key in ["azure_deployment_name", "azure-deployment-name"]:
54
+ global_config.file_set("azure_deployment_name", value)
55
+ print(f"Azure deployment name set to '{value}'.")
58
56
  return True
59
- except Exception as e:
60
- print(f"Error parsing --set value: {e}")
57
+ if ".max_tokens" in key or ".base_url" in key:
58
+ return _handle_set_provider_level_setting(key, value)
59
+ if key == "tool_permissions":
60
+ from janito.tools.permissions_parse import parse_permissions_string
61
+ from janito.tools.permissions import set_global_allowed_permissions
62
+ perms = parse_permissions_string(value)
63
+ global_config.file_set("tool_permissions", value)
64
+ set_global_allowed_permissions(perms)
65
+ print(f"Tool permissions set to '{value}' (parsed: {perms})")
61
66
  return True
67
+ print(
68
+ f"Error: Unknown config key '{key}'. Supported: provider, model, <provider>.model, max_tokens, base_url, azure_deployment_name, <provider>.max_tokens, <provider>.base_url, <provider>.<model>.max_tokens, <provider>.<model>.base_url, tool_permissions"
69
+ )
70
+ return True
62
71
 
63
72
 
64
73
  def _handle_set_max_tokens(value):
janito/cli/main_cli.py CHANGED
@@ -14,6 +14,14 @@ from janito.cli.core.event_logger import (
14
14
  )
15
15
 
16
16
  definition = [
17
+ (
18
+ ["-u", "--unrestricted-paths"],
19
+ {
20
+ "action": "store_true",
21
+ "help": "Disable path security: allow tool arguments to use any file/directory path (DANGEROUS)",
22
+ },
23
+ ),
24
+
17
25
  (
18
26
  ["--profile"],
19
27
  {
@@ -22,6 +30,14 @@ definition = [
22
30
  "default": None,
23
31
  },
24
32
  ),
33
+ (
34
+ ["--role"],
35
+ {
36
+ "metavar": "ROLE",
37
+ "help": "Select the developer role name (overrides profile, e.g. 'python-expert').",
38
+ "default": None,
39
+ },
40
+ ),
25
41
  (
26
42
  ["-W", "--workdir"],
27
43
  {
@@ -165,6 +181,7 @@ MODIFIER_KEYS = [
165
181
  "provider",
166
182
  "model",
167
183
  "role",
184
+ "profile",
168
185
  "system",
169
186
  "temperature",
170
187
 
@@ -240,11 +257,15 @@ class JanitoCLI:
240
257
  setattr(self.args, key, None)
241
258
 
242
259
  def collect_modifiers(self):
243
- return {
260
+ modifiers = {
244
261
  k: getattr(self.args, k)
245
262
  for k in MODIFIER_KEYS
246
263
  if getattr(self.args, k, None) is not None
247
264
  }
265
+ # If --role is provided, override role in modifiers
266
+ if getattr(self.args, "role", None):
267
+ modifiers["role"] = getattr(self.args, "role")
268
+ return modifiers
248
269
 
249
270
  def classify(self):
250
271
  if any(getattr(self.args, k, None) for k in SETTER_KEYS):
@@ -273,6 +294,9 @@ class JanitoCLI:
273
294
  self._maybe_print_verbose_provider_model()
274
295
  handle_getter(self.args)
275
296
  return
297
+ # If running in single shot mode and --profile is not provided, default to 'developer' profile
298
+ if get_prompt_mode(self.args) == "single_shot" and not getattr(self.args, "profile", None):
299
+ self.args.profile = "developer"
276
300
  provider = self._get_provider_or_default()
277
301
  if provider is None:
278
302
  print(
janito/cli/prompt_core.py CHANGED
@@ -49,87 +49,94 @@ class PromptHandler:
49
49
  on_event(inner_event)
50
50
  from janito.tools.tool_events import ToolCallFinished
51
51
 
52
- # Print tool result if ToolCallFinished event is received
53
52
  if isinstance(inner_event, ToolCallFinished):
54
- # Print result if verbose_tools is enabled or always for user visibility
55
- if hasattr(self.args, "verbose_tools") and self.args.verbose_tools:
56
- self.console.print(
57
- f"[cyan][tools-adapter] Tool '{inner_event.tool_name}' result:[/cyan] {inner_event.result}"
58
- )
59
- else:
60
- self.console.print(inner_event.result)
61
- return None
53
+ return self._handle_tool_call_finished(inner_event)
62
54
  if isinstance(inner_event, RateLimitRetry):
63
- status.update(f"[yellow]Rate limited. Waiting {inner_event.retry_delay:.0f}s before retry (attempt {inner_event.attempt}).[yellow]")
64
- return None
55
+ return self._handle_rate_limit_retry(inner_event, status)
65
56
  if isinstance(inner_event, RequestFinished):
57
+ if getattr(inner_event, "status", None) == "error":
58
+ return self._handle_request_finished_error(inner_event, status)
59
+ if getattr(inner_event, "status", None) in (RequestStatus.EMPTY_RESPONSE, RequestStatus.TIMEOUT):
60
+ return self._handle_empty_or_timeout(inner_event, status)
66
61
  status.update("[bold green]Received response![bold green]")
67
62
  return "break"
68
- elif (
69
- isinstance(inner_event, RequestFinished)
70
- and getattr(inner_event, "status", None) == "error"
71
- ): # noqa
72
- error_msg = (
73
- inner_event.error if hasattr(inner_event, "error") else "Unknown error"
63
+ if isinstance(inner_event, ToolCallError):
64
+ return self._handle_tool_call_error(inner_event, status)
65
+ event_type = type(inner_event).__name__
66
+ self.console.print(
67
+ f"[yellow]Warning: Unknown event type encountered: {event_type}[yellow]"
68
+ )
69
+ return None
70
+
71
+ def _handle_tool_call_finished(self, inner_event):
72
+ if hasattr(self.args, "verbose_tools") and self.args.verbose_tools:
73
+ self.console.print(
74
+ f"[cyan][tools-adapter] Tool '{inner_event.tool_name}' result:[/cyan] {inner_event.result}"
75
+ )
76
+ else:
77
+ self.console.print(inner_event.result)
78
+ return None
79
+
80
+ def _handle_rate_limit_retry(self, inner_event, status):
81
+ status.update(f"[yellow]Rate limited. Waiting {inner_event.retry_delay:.0f}s before retry (attempt {inner_event.attempt}).[yellow]")
82
+ return None
83
+
84
+ def _handle_request_finished_error(self, inner_event, status):
85
+ error_msg = (
86
+ inner_event.error if hasattr(inner_event, "error") else "Unknown error"
87
+ )
88
+ if (
89
+ "Status 429" in error_msg
90
+ and "Service tier capacity exceeded for this model" in error_msg
91
+ ):
92
+ status.update(
93
+ "[yellow]Service tier capacity exceeded, retrying...[yellow]"
74
94
  )
75
- if (
76
- "Status 429" in error_msg
77
- and "Service tier capacity exceeded for this model" in error_msg
78
- ):
79
- status.update(
80
- "[yellow]Service tier capacity exceeded, retrying...[yellow]"
81
- )
82
- return "break"
83
- status.update(f"[bold red]Error: {error_msg}[bold red]")
84
- self.console.print(f"[red]Error: {error_msg}[red]")
85
95
  return "break"
86
- elif isinstance(inner_event, ToolCallError):
87
- error_msg = (
88
- inner_event.error
89
- if hasattr(inner_event, "error")
90
- else "Unknown tool error"
96
+ status.update(f"[bold red]Error: {error_msg}[bold red]")
97
+ self.console.print(f"[red]Error: {error_msg}[red]")
98
+ return "break"
99
+
100
+ def _handle_tool_call_error(self, inner_event, status):
101
+ error_msg = (
102
+ inner_event.error
103
+ if hasattr(inner_event, "error")
104
+ else "Unknown tool error"
105
+ )
106
+ tool_name = (
107
+ inner_event.tool_name
108
+ if hasattr(inner_event, "tool_name")
109
+ else "unknown"
110
+ )
111
+ status.update(
112
+ f"[bold red]Tool Error in '{tool_name}': {error_msg}[bold red]"
113
+ )
114
+ self.console.print(f"[red]Tool Error in '{tool_name}': {error_msg}[red]")
115
+ return "break"
116
+
117
+ def _handle_empty_or_timeout(self, inner_event, status):
118
+ details = getattr(inner_event, "details", None) or {}
119
+ block_reason = details.get("block_reason")
120
+ block_msg = details.get("block_reason_message")
121
+ msg = details.get(
122
+ "message", "LLM returned an empty or incomplete response."
123
+ )
124
+ driver_name = getattr(inner_event, "driver_name", "unknown driver")
125
+ if block_reason or block_msg:
126
+ status.update(
127
+ f"[bold yellow]Blocked by driver: {driver_name} | {block_reason or ''} {block_msg or ''}[bold yellow]"
91
128
  )
92
- tool_name = (
93
- inner_event.tool_name
94
- if hasattr(inner_event, "tool_name")
95
- else "unknown"
129
+ self.console.print(
130
+ f"[yellow]Blocked by driver: {driver_name} (empty response): {block_reason or ''}\n{block_msg or ''}[/yellow]"
96
131
  )
132
+ else:
97
133
  status.update(
98
- f"[bold red]Tool Error in '{tool_name}': {error_msg}[bold red]"
134
+ f"[yellow]LLM produced no output for this request (driver: {driver_name}).[/yellow]"
99
135
  )
100
- self.console.print(f"[red]Tool Error in '{tool_name}': {error_msg}[red]")
101
- return "break"
102
- elif isinstance(inner_event, RequestFinished) and getattr(
103
- inner_event, "status", None
104
- ) in (RequestStatus.EMPTY_RESPONSE, RequestStatus.TIMEOUT):
105
- details = getattr(inner_event, "details", None) or {}
106
- block_reason = details.get("block_reason")
107
- block_msg = details.get("block_reason_message")
108
- msg = details.get(
109
- "message", "LLM returned an empty or incomplete response."
136
+ self.console.print(
137
+ f"[yellow]Warning: {msg} (driver: {driver_name})[/yellow]"
110
138
  )
111
- driver_name = getattr(inner_event, "driver_name", "unknown driver")
112
- if block_reason or block_msg:
113
- status.update(
114
- f"[bold yellow]Blocked by driver: {driver_name} | {block_reason or ''} {block_msg or ''}[bold yellow]"
115
- )
116
- self.console.print(
117
- f"[yellow]Blocked by driver: {driver_name} (empty response): {block_reason or ''}\n{block_msg or ''}[/yellow]"
118
- )
119
- else:
120
- status.update(
121
- f"[yellow]LLM produced no output for this request (driver: {driver_name}).[/yellow]"
122
- )
123
- self.console.print(
124
- f"[yellow]Warning: {msg} (driver: {driver_name})[/yellow]"
125
- )
126
- return "break"
127
- # Report unknown event types
128
- event_type = type(inner_event).__name__
129
- self.console.print(
130
- f"[yellow]Warning: Unknown event type encountered: {event_type}[yellow]"
131
- )
132
- return None
139
+ return "break"
133
140
 
134
141
  def _process_event_iter(self, event_iter, on_event):
135
142
  for event in event_iter:
@@ -30,7 +30,8 @@ class RichTerminalReporter(EventHandlerBase):
30
30
  self.raw_mode = raw_mode
31
31
  import janito.report_events as report_events
32
32
 
33
- super().__init__(driver_events, report_events)
33
+ import janito.tools.tool_events as tool_events
34
+ super().__init__(driver_events, report_events, tool_events)
34
35
  self._waiting_printed = False
35
36
 
36
37
  def on_RequestStarted(self, event):
@@ -102,7 +103,27 @@ class RichTerminalReporter(EventHandlerBase):
102
103
  self.console.file.flush()
103
104
  # No output if not raw_mode or if response is None
104
105
 
106
+ def on_ToolCallError(self, event):
107
+ # Optionally handle tool call errors in a user-friendly way
108
+ error = getattr(event, "error", None)
109
+ tool = getattr(event, "tool_name", None)
110
+ if error and tool:
111
+ self.console.print(f"[bold red]Tool Error ({tool}):[/] {error}")
112
+ self.console.file.flush()
113
+
105
114
  def on_ReportEvent(self, event):
115
+ # Special handling for security-related report events
116
+ subtype = getattr(event, "subtype", None)
117
+ msg = getattr(event, "message", None)
118
+ action = getattr(event, "action", None)
119
+ tool = getattr(event, "tool", None)
120
+ context = getattr(event, "context", None)
121
+ if subtype == ReportSubtype.ERROR and msg and "[SECURITY] Path access denied" in msg:
122
+ # Highlight security errors with a distinct style
123
+ self.console.print(Panel(f"{msg}", title="[red]SECURITY VIOLATION[/red]", style="bold red"))
124
+ self.console.file.flush()
125
+ return
126
+
106
127
  msg = event.message if hasattr(event, "message") else None
107
128
  subtype = event.subtype if hasattr(event, "subtype") else None
108
129
  if not msg or not subtype: