janito 2.28.0__py3-none-any.whl → 2.30.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 (32) hide show
  1. janito/README.md +3 -3
  2. janito/agent/setup_agent.py +21 -35
  3. janito/agent/templates/profiles/system_prompt_template_Developer_with_Python_Tools.txt.j2 +6 -0
  4. janito/agent/templates/profiles/system_prompt_template_developer.txt.j2 +6 -0
  5. janito/agent/templates/profiles/system_prompt_template_market_analyst.txt.j2 +7 -1
  6. janito/agent/templates/profiles/system_prompt_template_model_conversation_without_tools_or_context.txt.j2 +7 -1
  7. janito/cli/chat_mode/session.py +154 -96
  8. janito/cli/cli_commands/list_plugins.py +99 -75
  9. janito/cli/cli_commands/show_system_prompt.py +8 -3
  10. janito/cli/core/runner.py +2 -2
  11. janito/cli/main_cli.py +9 -15
  12. janito/cli/prompt_core.py +0 -2
  13. janito/cli/rich_terminal_reporter.py +2 -1
  14. janito/cli/single_shot_mode/handler.py +0 -2
  15. janito/llm/agent.py +6 -1
  16. janito/provider_registry.py +1 -1
  17. janito/providers/openai/provider.py +1 -1
  18. janito/tools/adapters/local/ask_user.py +3 -1
  19. janito/tools/adapters/local/fetch_url.py +20 -28
  20. janito/tools/adapters/local/replace_text_in_file.py +9 -3
  21. janito/tools/adapters/local/search_text/core.py +2 -2
  22. janito/tools/loop_protection_decorator.py +12 -16
  23. janito/tools/tools_adapter.py +18 -4
  24. janito-2.30.0.dist-info/METADATA +83 -0
  25. {janito-2.28.0.dist-info → janito-2.30.0.dist-info}/RECORD +29 -30
  26. janito-2.30.0.dist-info/licenses/LICENSE +201 -0
  27. janito/cli/chat_mode/session_profile_select.py +0 -182
  28. janito-2.28.0.dist-info/METADATA +0 -431
  29. janito-2.28.0.dist-info/licenses/LICENSE +0 -21
  30. {janito-2.28.0.dist-info → janito-2.30.0.dist-info}/WHEEL +0 -0
  31. {janito-2.28.0.dist-info → janito-2.30.0.dist-info}/entry_points.txt +0 -0
  32. {janito-2.28.0.dist-info → janito-2.30.0.dist-info}/top_level.txt +0 -0
@@ -35,6 +35,7 @@ def _prepare_context(args, agent_role, allowed_permissions):
35
35
  context["role"] = agent_role or "developer"
36
36
  context["profile"] = getattr(args, "profile", None)
37
37
  context["allowed_permissions"] = allowed_permissions
38
+ context["emoji_enabled"] = getattr(args, "emoji", False)
38
39
  if allowed_permissions and "x" in allowed_permissions:
39
40
  pd = PlatformDiscovery()
40
41
  context["platform"] = pd.get_platform_name()
@@ -122,6 +123,10 @@ def handle_show_system_prompt(args):
122
123
  if profile is None and getattr(args, "market", False):
123
124
  profile = "Market Analyst"
124
125
 
126
+ # Handle --developer flag mapping to Developer With Python Tools profile
127
+ if profile is None and getattr(args, "developer", False):
128
+ profile = "Developer With Python Tools"
129
+
125
130
  if not profile:
126
131
  print(
127
132
  "[janito] No profile specified. The main agent runs without a system prompt template.\n"
@@ -152,9 +157,9 @@ def handle_show_system_prompt(args):
152
157
  system_prompt = template.render(**context)
153
158
  system_prompt = re.sub(r"\n{3,}", "\n\n", system_prompt)
154
159
 
155
- print(
156
- f"\n--- System Prompt (resolved, profile: {getattr(args, 'profile', 'main')}) ---\n"
157
- )
160
+ # Use the actual profile name for display, not the resolved value
161
+ display_profile = profile or "main"
162
+ print(f"\n--- System Prompt (resolved, profile: {display_profile}) ---\n")
158
163
  print(system_prompt)
159
164
  print("\n-------------------------------\n")
160
165
  if agent_role:
janito/cli/core/runner.py CHANGED
@@ -109,8 +109,8 @@ def prepare_llm_driver_config(args, modifiers):
109
109
  llm_driver_config = LLMDriverConfig(**driver_config_data)
110
110
  if getattr(llm_driver_config, "verbose_api", None):
111
111
  pass
112
- # If both --role and --profile are provided, --role takes precedence for agent_role
113
- agent_role = modifiers.get("role") or modifiers.get("profile") or "developer"
112
+
113
+ agent_role = modifiers.get("profile") or "developer"
114
114
  return provider, llm_driver_config, agent_role
115
115
 
116
116
 
janito/cli/main_cli.py CHANGED
@@ -51,14 +51,6 @@ definition = [
51
51
  "help": "Start with the Market Analyst profile (equivalent to --profile 'Market Analyst')",
52
52
  },
53
53
  ),
54
- (
55
- ["--role"],
56
- {
57
- "metavar": "ROLE",
58
- "help": "Select the developer role name (overrides profile, e.g. 'python-expert').",
59
- "default": None,
60
- },
61
- ),
62
54
  (
63
55
  ["-W", "--workdir"],
64
56
  {
@@ -205,6 +197,13 @@ definition = [
205
197
  "help": "Set the reasoning effort for models that support it (low, medium, high, none)",
206
198
  },
207
199
  ),
200
+ (
201
+ ["--emoji"],
202
+ {
203
+ "action": "store_true",
204
+ "help": "Enable emoji usage in responses to make output more engaging and expressive",
205
+ },
206
+ ),
208
207
  (["user_prompt"], {"nargs": argparse.REMAINDER, "help": "Prompt to submit"}),
209
208
  (
210
209
  ["-e", "--event-log"],
@@ -244,7 +243,6 @@ definition = [
244
243
  MODIFIER_KEYS = [
245
244
  "provider",
246
245
  "model",
247
- "role",
248
246
  "profile",
249
247
  "developer",
250
248
  "market",
@@ -257,6 +255,7 @@ MODIFIER_KEYS = [
257
255
  "exec",
258
256
  "read",
259
257
  "write",
258
+ "emoji",
260
259
  ]
261
260
  SETTER_KEYS = ["set", "set_provider", "set_api_key", "unset"]
262
261
  GETTER_KEYS = [
@@ -372,9 +371,7 @@ class JanitoCLI:
372
371
  for k in MODIFIER_KEYS
373
372
  if getattr(self.args, k, None) is not None
374
373
  }
375
- # If --role is provided, override role in modifiers
376
- if getattr(self.args, "role", None):
377
- modifiers["role"] = getattr(self.args, "role")
374
+
378
375
  return modifiers
379
376
 
380
377
  def classify(self):
@@ -420,9 +417,6 @@ class JanitoCLI:
420
417
  self.args.exec = True
421
418
  # Remove the /rwx prefix from the prompt
422
419
  self.args.user_prompt = self.args.user_prompt[1:]
423
- elif self.args.user_prompt and self.args.user_prompt[0].startswith("/"):
424
- # Skip LLM processing for other commands that start with /
425
- return
426
420
 
427
421
  # If running in single shot mode and --profile is not provided, default to 'developer' profile
428
422
  # Skip profile selection for list commands that don't need it
janito/cli/prompt_core.py CHANGED
@@ -216,8 +216,6 @@ class PromptHandler:
216
216
  if on_event and final_event is not None:
217
217
  on_event(final_event)
218
218
  global_event_bus.publish(final_event)
219
- # Terminal bell moved to token summary printing in session.py and handler.py
220
- pass # print('\a', end='', flush=True)
221
219
  except KeyboardInterrupt:
222
220
  # Capture user interrupt / cancellation
223
221
  self.console.print("[red]Interrupted by the user.[/red]")
@@ -139,11 +139,12 @@ class RichTerminalReporter(EventHandlerBase):
139
139
  if not msg or not subtype:
140
140
  return
141
141
  if subtype == ReportSubtype.ACTION_INFO:
142
- # Use orange for modification actions, cyan otherwise
142
+ # Use orange for all write/modification actions
143
143
  modification_actions = (
144
144
  getattr(ReportAction, "UPDATE", None),
145
145
  getattr(ReportAction, "WRITE", None),
146
146
  getattr(ReportAction, "DELETE", None),
147
+ getattr(ReportAction, "CREATE", None),
147
148
  )
148
149
  style = (
149
150
  "orange1"
@@ -125,8 +125,6 @@ class PromptHandler:
125
125
  print_token_message_summary(
126
126
  shared_console, msg_count=1, usage=usage, elapsed=elapsed
127
127
  )
128
- # Send terminal bell character to trigger TUI bell after printing token summary
129
- print("\a", end="", flush=True)
130
128
  self._cleanup_driver_and_console()
131
129
 
132
130
  def _cleanup_driver_and_console(self):
janito/llm/agent.py CHANGED
@@ -244,7 +244,12 @@ class LLMAgent:
244
244
  f"[agent] [DEBUG] Tool call detected: {getattr(part, 'name', repr(part))} with arguments: {getattr(part, 'arguments', None)}"
245
245
  )
246
246
  tool_calls.append(part)
247
- result = self.tools_adapter.execute_function_call_message_part(part)
247
+ try:
248
+ result = self.tools_adapter.execute_function_call_message_part(part)
249
+ except Exception as e:
250
+ # Catch any exception during tool execution and return as string
251
+ # instead of letting it propagate to the user
252
+ result = str(e)
248
253
  tool_results.append(result)
249
254
  if tool_calls:
250
255
  # Prepare tool_calls message for assistant
@@ -41,7 +41,7 @@ class ProviderRegistry:
41
41
  rows.append(info[:3])
42
42
 
43
43
  # Group providers by openness (open-source first, then proprietary)
44
- open_providers = {"cerebras", "deepseek", "alibaba", "moonshotai", "zai"}
44
+ open_providers = {"cerebras", "deepseek", "alibaba", "moonshot", "zai"}
45
45
 
46
46
  def sort_key(row):
47
47
  provider_name = row[0]
@@ -18,7 +18,7 @@ class OpenAIProvider(LLMProvider):
18
18
  MAINTAINER = "João Pinto <janito@ikignosis.org>"
19
19
  MODEL_SPECS = MODEL_SPECS
20
20
  DEFAULT_MODEL = (
21
- "gpt-4.1" # Options: gpt-4.1, gpt-4o, o3-mini, o4-mini, gpt-5, gpt-5-nano
21
+ "gpt-5" # Options: gpt-4.1, gpt-4o, o3-mini, o4-mini, gpt-5, gpt-5-nano
22
22
  )
23
23
 
24
24
  def __init__(
@@ -5,6 +5,7 @@ from janito.tools.loop_protection_decorator import protect_against_loops
5
5
  from rich import print as rich_print
6
6
  from janito.i18n import tr
7
7
  from rich.panel import Panel
8
+ from rich.markdown import Markdown
8
9
  from prompt_toolkit import PromptSession
9
10
  from prompt_toolkit.key_binding import KeyBindings
10
11
  from prompt_toolkit.enums import EditingMode
@@ -36,7 +37,7 @@ class AskUserTool(ToolBase):
36
37
  def run(self, question: str) -> str:
37
38
 
38
39
  print() # Print an empty line before the question panel
39
- rich_print(Panel.fit(question, title=tr("Question"), style="cyan"))
40
+ rich_print(Panel.fit(Markdown(question), title=tr("Question"), style="cyan"))
40
41
 
41
42
  bindings = KeyBindings()
42
43
  mode = {"multiline": False}
@@ -107,4 +108,5 @@ class AskUserTool(ToolBase):
107
108
  rich_print(
108
109
  "[yellow]Warning: Some characters in your input were not valid UTF-8 and have been replaced.[/yellow]"
109
110
  )
111
+ print("\a", end="", flush=True) # Print bell character
110
112
  return sanitized
@@ -196,25 +196,15 @@ class FetchUrlTool(ToolBase):
196
196
  whitelist_manager = get_url_whitelist_manager()
197
197
 
198
198
  if not whitelist_manager.is_url_allowed(url):
199
- error_message = tr(
200
- "Warning: URL blocked by whitelist: {url}",
201
- url=url,
202
- )
199
+ error_message = tr("Blocked")
203
200
  self.report_error(
204
- tr(
205
- "❗ URL blocked by whitelist: {url}",
206
- url=url,
207
- ),
201
+ tr("❗ Blocked"),
208
202
  ReportAction.READ,
209
203
  )
210
204
  return error_message
211
205
 
212
206
  # Check session cache first
213
207
  if url in self.session_cache:
214
- self.report_warning(
215
- tr("ℹ️ Using session cache"),
216
- ReportAction.READ,
217
- )
218
208
  return self.session_cache[url]
219
209
 
220
210
  # Check persistent cache for known errors
@@ -258,9 +248,8 @@ class FetchUrlTool(ToolBase):
258
248
  status_code = http_err.response.status_code if http_err.response else None
259
249
  if status_code and 400 <= status_code < 500:
260
250
  error_message = tr(
261
- "Warning: HTTP {status_code} error for URL: {url}",
251
+ "HTTP {status_code}",
262
252
  status_code=status_code,
263
- url=url,
264
253
  )
265
254
  # Cache 403 and 404 errors
266
255
  if status_code in [403, 404]:
@@ -268,9 +257,8 @@ class FetchUrlTool(ToolBase):
268
257
 
269
258
  self.report_error(
270
259
  tr(
271
- "❗ HTTP {status_code} error for URL: {url}",
260
+ "❗ HTTP {status_code}",
272
261
  status_code=status_code,
273
- url=url,
274
262
  ),
275
263
  ReportAction.READ,
276
264
  )
@@ -278,25 +266,21 @@ class FetchUrlTool(ToolBase):
278
266
  else:
279
267
  self.report_error(
280
268
  tr(
281
- "❗ HTTP error for URL: {url}: {err}",
282
- url=url,
283
- err=str(http_err),
269
+ "❗ HTTP {status_code}",
270
+ status_code=status_code or "Error",
284
271
  ),
285
272
  ReportAction.READ,
286
273
  )
287
274
  return tr(
288
- "Warning: HTTP error for URL: {url}: {err}",
289
- url=url,
290
- err=str(http_err),
275
+ "HTTP {status_code}",
276
+ status_code=status_code or "Error",
291
277
  )
292
278
  except Exception as err:
293
279
  self.report_error(
294
- tr("❗ Error fetching URL: {url}: {err}", url=url, err=str(err)),
280
+ tr("❗ Error"),
295
281
  ReportAction.READ,
296
282
  )
297
- return tr(
298
- "Warning: Error fetching URL: {url}: {err}", url=url, err=str(err)
299
- )
283
+ return tr("Error")
300
284
 
301
285
  def _extract_and_clean_text(self, html_content: str) -> str:
302
286
  """Extract and clean text from HTML content."""
@@ -370,7 +354,11 @@ class FetchUrlTool(ToolBase):
370
354
  cookies=cookies,
371
355
  follow_redirects=follow_redirects,
372
356
  )
373
- if html_content.startswith("Warning:"):
357
+ if (
358
+ html_content.startswith("HTTP ")
359
+ or html_content == "Error"
360
+ or html_content == "Blocked"
361
+ ):
374
362
  return html_content
375
363
 
376
364
  try:
@@ -399,7 +387,11 @@ class FetchUrlTool(ToolBase):
399
387
  cookies=cookies,
400
388
  follow_redirects=follow_redirects,
401
389
  )
402
- if html_content.startswith("Warning:"):
390
+ if (
391
+ html_content.startswith("HTTP ")
392
+ or html_content == "Error"
393
+ or html_content == "Blocked"
394
+ ):
403
395
  return html_content
404
396
 
405
397
  # Extract and clean text
@@ -159,16 +159,22 @@ class ReplaceTextInFileTool(ToolBase):
159
159
  )
160
160
  return warning, concise_warning
161
161
 
162
- def _report_success(self, match_lines):
162
+ def _report_success(self, match_lines, line_delta_str=""):
163
163
  """Report success with line numbers where replacements occurred."""
164
164
  if match_lines:
165
165
  lines_str = ", ".join(str(line_no) for line_no in match_lines)
166
166
  self.report_success(
167
- tr(" ✅ replaced at {lines_str}", lines_str=lines_str),
167
+ tr(
168
+ " ✅ replaced at {lines_str}{delta}",
169
+ lines_str=lines_str,
170
+ delta=line_delta_str,
171
+ ),
168
172
  ReportAction.CREATE,
169
173
  )
170
174
  else:
171
- self.report_success(tr(" ✅ replaced (lines unknown)"), ReportAction.CREATE)
175
+ self.report_success(
176
+ tr(" ✅ replaced{delta}", delta=line_delta_str), ReportAction.CREATE
177
+ )
172
178
 
173
179
  def _get_line_delta_str(self, content, new_content):
174
180
  """Return a string describing the net line change after replacement."""
@@ -98,7 +98,7 @@ class SearchTextTool(ToolBase):
98
98
  if max_depth > 0:
99
99
  info_str += tr(" [max_depth={max_depth}]", max_depth=max_depth)
100
100
  if count_only:
101
- info_str += " [count-only]"
101
+ info_str += " [count]"
102
102
  self.report_action(info_str, ReportAction.READ)
103
103
  if os.path.isfile(search_path):
104
104
  dir_output, dir_limit_reached, per_file_counts = self._handle_file(
@@ -144,7 +144,7 @@ class SearchTextTool(ToolBase):
144
144
  file_word_max = file_word + (" (max)" if dir_limit_reached else "")
145
145
  self.report_success(
146
146
  tr(
147
- " ✅ {count} {file_word} from {num_files} {file_label}",
147
+ " ✅ {count} {file_word}/{num_files} {file_label}",
148
148
  count=count,
149
149
  file_word=file_word_max,
150
150
  num_files=num_files,
@@ -119,22 +119,18 @@ def protect_against_loops(
119
119
  current_time - timestamp <= time_window
120
120
  for timestamp in _decorator_call_tracker[op_name]
121
121
  ):
122
- # Define the error reporting function
123
- def _report_error_and_raise(args, operation_type):
124
- # Get the tool instance to access report_error method if available
125
- tool_instance = args[0] if args else None
126
- error_msg = f"Loop protection: Too many {operation_type} operations in a short time period ({max_calls} calls in {time_window}s)"
127
-
128
- # Try to report the error through the tool's reporting mechanism
129
- if hasattr(tool_instance, "report_error"):
130
- try:
131
- tool_instance.report_error(error_msg)
132
- except Exception:
133
- pass # If reporting fails, we still raise the error
134
-
135
- raise RuntimeError(error_msg)
136
-
137
- _report_error_and_raise(args, op_name)
122
+ # Return loop protection message as string instead of raising exception
123
+ error_msg = f"Loop protection: Too many {op_name} operations in a short time period ({max_calls} calls in {time_window}s). Please try a different approach or wait before retrying."
124
+
125
+ # Try to report the error through the tool's reporting mechanism
126
+ tool_instance = args[0] if args else None
127
+ if hasattr(tool_instance, "report_error"):
128
+ try:
129
+ tool_instance.report_error(error_msg)
130
+ except Exception:
131
+ pass # If reporting fails, we still return the message
132
+
133
+ return error_msg
138
134
 
139
135
  # Record this call
140
136
  if op_name not in _decorator_call_tracker:
@@ -460,9 +460,9 @@ class ToolsAdapterBase:
460
460
  raise ToolCallException(tool_name, error_msg, arguments=arguments)
461
461
 
462
462
  def _handle_execution_error(self, tool_name, request_id, exception, arguments):
463
- # Check if this is a loop protection error that should be returned as a string
463
+ # Check if this is a loop protection error that should trigger a new strategy
464
464
  if isinstance(exception, RuntimeError) and "Loop protection:" in str(exception):
465
- error_msg = str(exception) # Return the loop protection message directly
465
+ error_msg = str(exception)
466
466
  if self._event_bus:
467
467
  self._event_bus.publish(
468
468
  ToolCallError(
@@ -473,8 +473,22 @@ class ToolsAdapterBase:
473
473
  arguments=arguments,
474
474
  )
475
475
  )
476
- # Return the error message instead of raising an exception
477
- return error_msg
476
+ # Return the loop protection message as string to trigger new strategy
477
+ return f"Loop protection triggered - requesting new strategy: {error_msg}"
478
+
479
+ # Check if this is a string return from loop protection (new behavior)
480
+ if isinstance(exception, str) and "Loop protection:" in exception:
481
+ error_msg = str(exception)
482
+ if self._event_bus:
483
+ self._event_bus.publish(
484
+ ToolCallError(
485
+ tool_name=tool_name,
486
+ request_id=request_id,
487
+ error=error_msg,
488
+ arguments=arguments,
489
+ )
490
+ )
491
+ return f"Loop protection triggered - requesting new strategy: {error_msg}"
478
492
 
479
493
  error_msg = f"Exception during execution of tool '{tool_name}': {exception}"
480
494
  if self._event_bus:
@@ -0,0 +1,83 @@
1
+ Metadata-Version: 2.4
2
+ Name: janito
3
+ Version: 2.30.0
4
+ Summary: A new Python package called janito.
5
+ Author-email: João Pinto <janito@ikignosis.org>
6
+ Project-URL: Homepage, https://github.com/ikignosis/janito
7
+ Requires-Python: >=3.7
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: attrs==25.3.0
11
+ Requires-Dist: rich==14.0.0
12
+ Requires-Dist: pathspec==0.12.1
13
+ Requires-Dist: setuptools>=61.0
14
+ Requires-Dist: pyyaml>=6.0
15
+ Requires-Dist: jinja2>=3.0.0
16
+ Requires-Dist: prompt_toolkit>=3.0.51
17
+ Requires-Dist: lxml>=5.4.0
18
+ Requires-Dist: requests>=2.32.4
19
+ Requires-Dist: bs4>=0.0.2
20
+ Requires-Dist: questionary>=2.0.1
21
+ Requires-Dist: openai>=1.68.0
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest; extra == "dev"
24
+ Requires-Dist: pre-commit; extra == "dev"
25
+ Requires-Dist: ruff==0.11.9; extra == "dev"
26
+ Requires-Dist: detect-secrets==1.4.0; extra == "dev"
27
+ Requires-Dist: codespell==2.4.1; extra == "dev"
28
+ Requires-Dist: black; extra == "dev"
29
+ Requires-Dist: questionary>=2.0.1; extra == "dev"
30
+ Requires-Dist: setuptools_scm>=8.0; extra == "dev"
31
+ Provides-Extra: coder
32
+ Requires-Dist: janito-coder; extra == "coder"
33
+ Dynamic: license-file
34
+
35
+ # nctl
36
+
37
+ ```bash
38
+ $ nctl --help
39
+ Usage: nctl <command>
40
+
41
+ Interact with Nine API resources. See https://docs.nineapis.ch for the full API docs.
42
+
43
+ Run "nctl <command> --help" for more information on a command.
44
+ ```
45
+
46
+ ## Setup
47
+
48
+ ```bash
49
+ # If you have go already installed
50
+ go install github.com/ninech/nctl@latest
51
+
52
+ # Homebrew
53
+ brew install ninech/taps/nctl
54
+
55
+ # Debian/Ubuntu
56
+ echo "deb [trusted=yes] https://repo.nine.ch/deb/ /" | sudo tee /etc/apt/sources.list.d/repo.nine.ch.list
57
+ sudo apt-get update
58
+ sudo apt-get install nctl
59
+
60
+ # Fedora/RHEL
61
+ cat <<EOF > /etc/yum.repos.d/repo.nine.ch.repo
62
+ [repo.nine.ch]
63
+ name=Nine Repo
64
+ baseurl=https://repo.nine.ch/yum/
65
+ enabled=1
66
+ gpgcheck=0
67
+ EOF
68
+ dnf install nctl
69
+
70
+ # Arch
71
+ # Install yay: https://github.com/Jguer/yay#binary
72
+ yay --version
73
+ yay -S nctl-bin
74
+ ```
75
+
76
+ For Windows users, nctl is also built for arm64 and amd64. You can download the
77
+ latest exe file from the [releases](https://github.com/ninech/nctl/releases) and
78
+ install it.
79
+
80
+ ## Getting started
81
+
82
+ * login to the API using `nctl auth login`
83
+ * run `nctl --help` to get a list of all available commands