janito 1.10.0__py3-none-any.whl → 1.11.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 (57) hide show
  1. janito/__init__.py +1 -1
  2. janito/agent/conversation_api.py +178 -90
  3. janito/agent/conversation_ui.py +1 -1
  4. janito/agent/llm_conversation_history.py +12 -0
  5. janito/agent/templates/profiles/system_prompt_template_base.txt.j2 +19 -4
  6. janito/agent/tools/__init__.py +2 -0
  7. janito/agent/tools/create_directory.py +1 -1
  8. janito/agent/tools/create_file.py +1 -1
  9. janito/agent/tools/fetch_url.py +1 -1
  10. janito/agent/tools/find_files.py +26 -13
  11. janito/agent/tools/get_file_outline/core.py +1 -1
  12. janito/agent/tools/get_file_outline/python_outline.py +139 -95
  13. janito/agent/tools/get_lines.py +92 -63
  14. janito/agent/tools/move_file.py +58 -32
  15. janito/agent/tools/open_url.py +31 -0
  16. janito/agent/tools/python_command_runner.py +85 -86
  17. janito/agent/tools/python_file_runner.py +85 -86
  18. janito/agent/tools/python_stdin_runner.py +87 -88
  19. janito/agent/tools/remove_directory.py +1 -1
  20. janito/agent/tools/remove_file.py +1 -1
  21. janito/agent/tools/replace_file.py +2 -2
  22. janito/agent/tools/replace_text_in_file.py +193 -149
  23. janito/agent/tools/run_bash_command.py +1 -1
  24. janito/agent/tools/run_powershell_command.py +4 -0
  25. janito/agent/tools/search_text/__init__.py +1 -0
  26. janito/agent/tools/search_text/core.py +176 -0
  27. janito/agent/tools/search_text/match_lines.py +58 -0
  28. janito/agent/tools/search_text/pattern_utils.py +65 -0
  29. janito/agent/tools/search_text/traverse_directory.py +132 -0
  30. janito/agent/tools/validate_file_syntax/core.py +41 -30
  31. janito/agent/tools/validate_file_syntax/html_validator.py +21 -5
  32. janito/agent/tools/validate_file_syntax/markdown_validator.py +77 -34
  33. janito/agent/tools_utils/gitignore_utils.py +25 -2
  34. janito/agent/tools_utils/utils.py +7 -1
  35. janito/cli/config_commands.py +112 -109
  36. janito/shell/main.py +51 -8
  37. janito/shell/session/config.py +83 -75
  38. janito/shell/ui/interactive.py +97 -73
  39. janito/termweb/static/editor.css +32 -29
  40. janito/termweb/static/editor.css.bak +140 -22
  41. janito/termweb/static/editor.html +12 -7
  42. janito/termweb/static/editor.html.bak +16 -11
  43. janito/termweb/static/editor.js +94 -40
  44. janito/termweb/static/editor.js.bak +97 -65
  45. janito/termweb/static/index.html +1 -2
  46. janito/termweb/static/index.html.bak +1 -1
  47. janito/termweb/static/termweb.css +1 -22
  48. janito/termweb/static/termweb.css.bak +6 -4
  49. janito/termweb/static/termweb.js +0 -6
  50. janito/termweb/static/termweb.js.bak +1 -2
  51. {janito-1.10.0.dist-info → janito-1.11.1.dist-info}/METADATA +1 -1
  52. {janito-1.10.0.dist-info → janito-1.11.1.dist-info}/RECORD +56 -51
  53. {janito-1.10.0.dist-info → janito-1.11.1.dist-info}/WHEEL +1 -1
  54. janito/agent/tools/search_text.py +0 -254
  55. {janito-1.10.0.dist-info → janito-1.11.1.dist-info}/entry_points.txt +0 -0
  56. {janito-1.10.0.dist-info → janito-1.11.1.dist-info}/licenses/LICENSE +0 -0
  57. {janito-1.10.0.dist-info → janito-1.11.1.dist-info}/top_level.txt +0 -0
@@ -4,12 +4,11 @@ from janito.agent.runtime_config import unified_config, runtime_config
4
4
  from janito.agent.config_defaults import CONFIG_DEFAULTS
5
5
  from rich import print
6
6
  from ._utils import home_shorten
7
+ import os
8
+ from pathlib import Path
7
9
 
8
10
 
9
- def handle_config_commands(args):
10
- """Handle --set-local-config, --set-global-config, --show-config. Exit if any are used."""
11
- did_something = False
12
-
11
+ def handle_run_config(args):
13
12
  if args.run_config:
14
13
  for run_item in args.run_config:
15
14
  try:
@@ -24,6 +23,11 @@ def handle_config_commands(args):
24
23
  )
25
24
  sys.exit(1)
26
25
  runtime_config.set(key, val.strip())
26
+ return True
27
+ return False
28
+
29
+
30
+ def handle_set_local_config(args):
27
31
  if args.set_local_config:
28
32
  try:
29
33
  key, val = args.set_local_config.split("=", 1)
@@ -40,8 +44,11 @@ def handle_config_commands(args):
40
44
  local_config.save()
41
45
  runtime_config.set(key, val.strip())
42
46
  print(f"Local config updated: {key} = {val.strip()}")
43
- did_something = True
47
+ return True
48
+ return False
49
+
44
50
 
51
+ def handle_set_global_config(args):
45
52
  if args.set_global_config:
46
53
  try:
47
54
  key, val = args.set_global_config.split("=", 1)
@@ -65,129 +72,111 @@ def handle_config_commands(args):
65
72
  del global_config._data[key]
66
73
  runtime_config.set("template", template_dict)
67
74
  print(f"Global config updated: template.{subkey} = {val.strip()}")
68
- did_something = True
75
+ return True
69
76
  else:
70
77
  global_config.set(key, val.strip())
71
78
  global_config.save()
72
79
  runtime_config.set(key, val.strip())
73
80
  print(f"Global config updated: {key} = {val.strip()}")
74
- did_something = True
81
+ return True
82
+ return False
75
83
 
84
+
85
+ def handle_set_api_key(args):
76
86
  if args.set_api_key:
77
- # Merge: load full config, update api_key, save all
78
87
  existing = dict(global_config.all())
79
88
  existing["api_key"] = args.set_api_key.strip()
80
89
  global_config._data = existing
81
90
  global_config.save()
82
91
  runtime_config.set("api_key", args.set_api_key.strip())
83
92
  print("Global API key saved.")
84
- did_something = True
93
+ return True
94
+ return False
95
+
85
96
 
97
+ def handle_show_config(args):
86
98
  if args.show_config:
87
- local_items = {}
88
- global_items = {}
89
-
90
- # Collect and group keys
91
- local_keys = set(local_config.all().keys())
92
- global_keys = set(global_config.all().keys())
93
- if not (local_keys or global_keys):
94
- print("No configuration found.")
95
- else:
96
- # Imports previously inside block to avoid circular import at module level
97
- # Handle template as nested dict
98
- for key in sorted(local_keys):
99
- if key == "template":
100
- template_dict = local_config.get("template", {})
101
- if template_dict:
102
- local_items["template"] = f"({len(template_dict)} keys set)"
103
- for tkey, tval in template_dict.items():
104
- local_items[f" template.{tkey}"] = tval
105
- continue
106
- if key.startswith("template."):
107
- # Skip legacy flat keys
108
- continue
109
- if key == "api_key":
110
- value = local_config.get("api_key")
111
- value = (
112
- value[:4] + "..." + value[-4:]
113
- if value and len(value) > 8
114
- else ("***" if value else None)
115
- )
116
- else:
117
- value = unified_config.get(key)
118
- local_items[key] = value
119
- for key in sorted(global_keys - local_keys):
120
- if key == "template":
121
- template_dict = global_config.get("template", {})
122
- if template_dict:
123
- global_items["template"] = f"({len(template_dict)} keys set)"
124
- for tkey, tval in template_dict.items():
125
- global_items[f" template.{tkey}"] = tval
126
- continue
127
- if key.startswith("template."):
128
- continue
129
- if key == "api_key":
130
- value = global_config.get("api_key")
131
- value = (
132
- value[:4] + "..." + value[-4:]
133
- if value and len(value) > 8
134
- else ("***" if value else None)
135
- )
136
- else:
137
- value = unified_config.get(key)
138
- global_items[key] = value
139
-
140
- # Mask API key
141
- for cfg in (local_items, global_items):
142
- if "api_key" in cfg and cfg["api_key"]:
143
- val = cfg["api_key"]
144
- cfg["api_key"] = (
145
- val[:4] + "..." + val[-4:] if len(val) > 8 else "***"
146
- )
147
-
148
- # Print local config
149
- from ._print_config import print_config_items
150
-
151
- print_config_items(
152
- local_items, color_label="[cyan]🏠 Local Configuration[/cyan]"
153
- )
99
+ local_items = _collect_config_items(local_config, unified_config, True)
100
+ global_items = _collect_config_items(
101
+ global_config, unified_config, False, set(local_items.keys())
102
+ )
103
+ _mask_api_keys(local_items)
104
+ _mask_api_keys(global_items)
105
+ _print_config_items(local_items, global_items)
106
+ _print_default_items(local_items, global_items)
107
+ return True
108
+ return False
154
109
 
155
- # Print global config
156
- print_config_items(
157
- global_items, color_label="[yellow]🌐 Global Configuration[/yellow]"
110
+
111
+ def _collect_config_items(config, unified_config, is_local, exclude_keys=None):
112
+ items = {}
113
+ keys = set(config.all().keys())
114
+ if exclude_keys:
115
+ keys = keys - set(exclude_keys)
116
+ for key in sorted(keys):
117
+ if key == "template":
118
+ template_dict = config.get("template", {})
119
+ if template_dict:
120
+ items["template"] = f"({len(template_dict)} keys set)"
121
+ for tkey, tval in template_dict.items():
122
+ items[f" template.{tkey}"] = tval
123
+ continue
124
+ if key.startswith("template."):
125
+ continue
126
+ if key == "api_key":
127
+ value = config.get("api_key")
128
+ value = (
129
+ value[:4] + "..." + value[-4:]
130
+ if value and len(value) > 8
131
+ else ("***" if value else None)
158
132
  )
133
+ else:
134
+ value = unified_config.get(key)
135
+ items[key] = value
136
+ return items
137
+
138
+
139
+ def _mask_api_keys(cfg):
140
+ if "api_key" in cfg and cfg["api_key"]:
141
+ val = cfg["api_key"]
142
+ cfg["api_key"] = val[:4] + "..." + val[-4:] if len(val) > 8 else "***"
159
143
 
160
- # Show defaults for unset keys
161
- shown_keys = set(local_items.keys()) | set(global_items.keys())
162
- default_items = {
163
- k: v
164
- for k, v in CONFIG_DEFAULTS.items()
165
- if k not in shown_keys and k != "api_key"
166
- }
167
- if default_items:
168
- print("[green]🟢 Defaults (not set in config files)[/green]")
169
- for key, value in default_items.items():
170
- # Special case for system_prompt: show template file if None
171
- if key == "system_prompt" and value is None:
172
- from pathlib import Path
173
-
174
- template_path = (
175
- Path(__file__).parent
176
- / "agent"
177
- / "templates"
178
- / "system_prompt_template_default.j2"
179
- )
180
- print(
181
- f"{key} = (default template path: {home_shorten(str(template_path))})"
182
- )
183
- else:
184
- print(f"{key} = {value}")
185
- print()
186
- did_something = True
187
-
188
- import os
189
- from pathlib import Path
190
144
 
145
+ def _print_config_items(local_items, global_items):
146
+ from ._print_config import print_config_items
147
+
148
+ print_config_items(local_items, color_label="[cyan]🏠 Local Configuration[/cyan]")
149
+ print_config_items(
150
+ global_items, color_label="[yellow]🌐 Global Configuration[/yellow]"
151
+ )
152
+
153
+
154
+ def _print_default_items(local_items, global_items):
155
+ shown_keys = set(local_items.keys()) | set(global_items.keys())
156
+ default_items = {
157
+ k: v
158
+ for k, v in CONFIG_DEFAULTS.items()
159
+ if k not in shown_keys and k != "api_key"
160
+ }
161
+ if default_items:
162
+ print("[green]🟢 Defaults (not set in config files)[/green]")
163
+ for key, value in default_items.items():
164
+ if key == "system_prompt" and value is None:
165
+ template_path = (
166
+ Path(__file__).parent
167
+ / "agent"
168
+ / "templates"
169
+ / "system_prompt_template_default.j2"
170
+ )
171
+ print(
172
+ f"{key} = (default template path: {home_shorten(str(template_path))})"
173
+ )
174
+ else:
175
+ print(f"{key} = {value}")
176
+ print()
177
+
178
+
179
+ def handle_config_reset_local(args):
191
180
  if getattr(args, "config_reset_local", False):
192
181
  local_path = Path(".janito/config.json")
193
182
  if local_path.exists():
@@ -196,6 +185,9 @@ def handle_config_commands(args):
196
185
  else:
197
186
  print(f"Local config file does not exist: {local_path}")
198
187
  sys.exit(0)
188
+
189
+
190
+ def handle_config_reset_global(args):
199
191
  if getattr(args, "config_reset_global", False):
200
192
  global_path = Path.home() / ".janito/config.json"
201
193
  if global_path.exists():
@@ -204,5 +196,16 @@ def handle_config_commands(args):
204
196
  else:
205
197
  print(f"Global config file does not exist: {global_path}")
206
198
  sys.exit(0)
199
+
200
+
201
+ def handle_config_commands(args):
202
+ did_something = False
203
+ did_something |= handle_run_config(args)
204
+ did_something |= handle_set_local_config(args)
205
+ did_something |= handle_set_global_config(args)
206
+ did_something |= handle_set_api_key(args)
207
+ did_something |= handle_show_config(args)
208
+ handle_config_reset_local(args)
209
+ handle_config_reset_global(args)
207
210
  if did_something:
208
211
  sys.exit(0)
janito/shell/main.py CHANGED
@@ -158,6 +158,50 @@ def handle_prompt_loop(
158
158
  save_conversation_history(conversation_history, session_id)
159
159
 
160
160
 
161
+ def handle_keyboard_interrupt(conversation_history, message_handler):
162
+ removed_count = 0
163
+ while (
164
+ conversation_history.last_message()
165
+ and conversation_history.last_message().get("role") != "assistant"
166
+ ):
167
+ conversation_history.remove_last_message()
168
+ removed_count += 1
169
+ # Remove the assistant message itself, if present
170
+ if (
171
+ conversation_history.last_message()
172
+ and conversation_history.last_message().get("role") == "assistant"
173
+ ):
174
+ conversation_history.remove_last_message()
175
+ removed_count += 1
176
+ message_handler.handle_message(
177
+ {
178
+ "type": "info",
179
+ "message": f"\nLast turn cleared due to interruption. Returning to prompt. (removed {removed_count} last msgs)\n",
180
+ }
181
+ )
182
+
183
+
184
+ def handle_chat_error(message_handler, error_type, error):
185
+ if error_type == "ProviderError":
186
+ message_handler.handle_message(
187
+ {"type": "error", "message": f"Provider error: {error}"}
188
+ )
189
+ elif error_type == "EmptyResponseError":
190
+ message_handler.handle_message({"type": "error", "message": f"Error: {error}"})
191
+ elif error_type == "ApiError":
192
+ message_handler.handle_message({"type": "error", "message": str(error)})
193
+ elif error_type == "Exception":
194
+ import traceback
195
+
196
+ tb = traceback.format_exc()
197
+ message_handler.handle_message(
198
+ {
199
+ "type": "error",
200
+ "message": f"Unexpected error: {error}\n{tb}\nReturning to prompt.",
201
+ }
202
+ )
203
+
204
+
161
205
  def handle_chat(shell_state, profile_manager, agent, max_rounds, session_id):
162
206
  conversation_history = shell_state.conversation_history
163
207
  message_handler = RichMessageHandler()
@@ -171,20 +215,19 @@ def handle_chat(shell_state, profile_manager, agent, max_rounds, session_id):
171
215
  spinner=True,
172
216
  )
173
217
  except KeyboardInterrupt:
174
- message_handler.handle_message(
175
- {"type": "info", "message": "Request interrupted. Returning to prompt."}
176
- )
218
+ handle_keyboard_interrupt(conversation_history, message_handler)
177
219
  return
178
220
  except ProviderError as e:
179
- message_handler.handle_message(
180
- {"type": "error", "message": f"Provider error: {e}"}
181
- )
221
+ handle_chat_error(message_handler, "ProviderError", e)
182
222
  return
183
223
  except EmptyResponseError as e:
184
- message_handler.handle_message({"type": "error", "message": f"Error: {e}"})
224
+ handle_chat_error(message_handler, "EmptyResponseError", e)
185
225
  return
186
226
  except ApiError as e:
187
- message_handler.handle_message({"type": "error", "message": str(e)})
227
+ handle_chat_error(message_handler, "ApiError", e)
228
+ return
229
+ except Exception as e:
230
+ handle_chat_error(message_handler, "Exception", e)
188
231
  return
189
232
  shell_state.last_elapsed = time.time() - start_time
190
233
  usage = response.get("usage")
@@ -12,90 +12,98 @@ def handle_config_shell(console, *args, **kwargs):
12
12
  /config reset global
13
13
  """
14
14
  if not args or args[0] not in ("show", "set", "reset"):
15
- console.print(
16
- "[bold red]Usage:[/bold red] /config show | /config set local|global key=value | /config reset local|global"
17
- )
15
+ _print_usage(console)
18
16
  return
19
-
20
17
  if args[0] == "show":
21
- # Show config, unified with CLI
22
- from janito.cli._print_config import print_full_config
23
-
24
- print_full_config(
25
- local_config,
26
- global_config,
27
- unified_config,
28
- CONFIG_DEFAULTS,
29
- console=console,
30
- )
18
+ _show_config(console)
31
19
  return
32
-
33
20
  if args[0] == "reset":
34
- if len(args) < 2 or args[1] not in ("local", "global"):
35
- console.print("[bold red]Usage:[/bold red] /config reset local|global")
36
- return
37
- import os
38
- from pathlib import Path
21
+ _reset_config(console, args)
22
+ return
23
+ if args[0] == "set":
24
+ _set_config(console, args)
25
+ return
39
26
 
40
- scope = args[1]
41
- if scope == "local":
42
- local_path = Path(".janito/config.json")
43
- if local_path.exists():
44
- os.remove(local_path)
45
- console.print(f"[green]Removed local config file:[/green] {local_path}")
46
- else:
47
- console.print(
48
- f"[yellow]Local config file does not exist:[/yellow] {local_path}"
49
- )
50
- elif scope == "global":
51
- global_path = Path.home() / ".janito/config.json"
52
- if global_path.exists():
53
- os.remove(global_path)
54
- console.print(
55
- f"[green]Removed global config file:[/green] {global_path}"
56
- )
57
- else:
58
- console.print(
59
- f"[yellow]Global config file does not exist:[/yellow] {global_path}"
60
- )
61
- console.print(
62
- "[bold yellow]Please use /restart for changes to take full effect.[/bold yellow]"
63
- )
27
+
28
+ def _print_usage(console):
29
+ console.print(
30
+ "[bold red]Usage:[/bold red] /config show | /config set local|global key=value | /config reset local|global"
31
+ )
32
+
33
+
34
+ def _show_config(console):
35
+ from janito.cli._print_config import print_full_config
36
+
37
+ print_full_config(
38
+ local_config,
39
+ global_config,
40
+ unified_config,
41
+ CONFIG_DEFAULTS,
42
+ console=console,
43
+ )
44
+
45
+
46
+ def _reset_config(console, args):
47
+ if len(args) < 2 or args[1] not in ("local", "global"):
48
+ console.print("[bold red]Usage:[/bold red] /config reset local|global")
64
49
  return
50
+ import os
51
+ from pathlib import Path
65
52
 
66
- if args[0] == "set":
67
- if len(args) < 3 or args[1] not in ("local", "global"):
68
- console.print(
69
- "[bold red]Usage:[/bold red] /config set local|global key=value"
70
- )
71
- return
72
- scope = args[1]
73
- try:
74
- key, val = args[2].split("=", 1)
75
- except ValueError:
76
- console.print("[bold red]Invalid format, expected key=val[/bold red]")
77
- return
78
- key = key.strip()
79
- if key not in CONFIG_OPTIONS and not key.startswith("template."):
80
- console.print(
81
- f"[bold red]Invalid config key: '{key}'. Supported keys are: {', '.join(CONFIG_OPTIONS.keys())}"
82
- )
83
- return
84
- val = val.strip()
85
- if scope == "local":
86
- local_config.set(key, val)
87
- local_config.save()
88
- runtime_config.set(key, val)
89
- console.print(f"[green]Local config updated:[/green] {key} = {val}")
53
+ scope = args[1]
54
+ if scope == "local":
55
+ local_path = Path(".janito/config.json")
56
+ if local_path.exists():
57
+ os.remove(local_path)
58
+ console.print(f"[green]Removed local config file:[/green] {local_path}")
59
+ else:
90
60
  console.print(
91
- "[bold yellow]Please use /restart for changes to take full effect.[/bold yellow]"
61
+ f"[yellow]Local config file does not exist:[/yellow] {local_path}"
92
62
  )
93
- elif scope == "global":
94
- global_config.set(key, val)
95
- global_config.save()
96
- runtime_config.set(key, val)
97
- console.print(f"[green]Global config updated:[/green] {key} = {val}")
63
+ elif scope == "global":
64
+ global_path = Path.home() / ".janito/config.json"
65
+ if global_path.exists():
66
+ os.remove(global_path)
67
+ console.print(f"[green]Removed global config file:[/green] {global_path}")
68
+ else:
98
69
  console.print(
99
- "[bold yellow]Please use /restart for changes to take full effect.[/bold yellow]"
70
+ f"[yellow]Global config file does not exist:[/yellow] {global_path}"
100
71
  )
72
+ console.print(
73
+ "[bold yellow]Please use /restart for changes to take full effect.[/bold yellow]"
74
+ )
75
+
76
+
77
+ def _set_config(console, args):
78
+ if len(args) < 3 or args[1] not in ("local", "global"):
79
+ console.print("[bold red]Usage:[/bold red] /config set local|global key=value")
101
80
  return
81
+ scope = args[1]
82
+ try:
83
+ key, val = args[2].split("=", 1)
84
+ except ValueError:
85
+ console.print("[bold red]Invalid format, expected key=val[/bold red]")
86
+ return
87
+ key = key.strip()
88
+ if key not in CONFIG_OPTIONS and not key.startswith("template."):
89
+ console.print(
90
+ f"[bold red]Invalid config key: '{key}'. Supported keys are: {', '.join(CONFIG_OPTIONS.keys())}"
91
+ )
92
+ return
93
+ val = val.strip()
94
+ if scope == "local":
95
+ local_config.set(key, val)
96
+ local_config.save()
97
+ runtime_config.set(key, val)
98
+ console.print(f"[green]Local config updated:[/green] {key} = {val}")
99
+ console.print(
100
+ "[bold yellow]Please use /restart for changes to take full effect.[/bold yellow]"
101
+ )
102
+ elif scope == "global":
103
+ global_config.set(key, val)
104
+ global_config.save()
105
+ runtime_config.set(key, val)
106
+ console.print(f"[green]Global config updated:[/green] {key} = {val}")
107
+ console.print(
108
+ "[bold yellow]Please use /restart for changes to take full effect.[/bold yellow]"
109
+ )