janito 0.11.0__py3-none-any.whl → 0.13.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 (50) hide show
  1. janito/__init__.py +1 -1
  2. janito/__main__.py +6 -204
  3. janito/callbacks.py +34 -132
  4. janito/cli/__init__.py +6 -0
  5. janito/cli/agent.py +400 -0
  6. janito/cli/app.py +94 -0
  7. janito/cli/commands.py +329 -0
  8. janito/cli/output.py +29 -0
  9. janito/cli/utils.py +22 -0
  10. janito/config.py +358 -121
  11. janito/data/instructions_template.txt +28 -0
  12. janito/token_report.py +154 -145
  13. janito/tools/__init__.py +38 -21
  14. janito/tools/bash/bash.py +84 -0
  15. janito/tools/bash/unix_persistent_bash.py +184 -0
  16. janito/tools/bash/win_persistent_bash.py +308 -0
  17. janito/tools/decorators.py +2 -13
  18. janito/tools/delete_file.py +27 -9
  19. janito/tools/fetch_webpage/__init__.py +34 -0
  20. janito/tools/fetch_webpage/chunking.py +76 -0
  21. janito/tools/fetch_webpage/core.py +155 -0
  22. janito/tools/fetch_webpage/extractors.py +276 -0
  23. janito/tools/fetch_webpage/news.py +137 -0
  24. janito/tools/fetch_webpage/utils.py +108 -0
  25. janito/tools/find_files.py +106 -44
  26. janito/tools/move_file.py +72 -0
  27. janito/tools/prompt_user.py +37 -6
  28. janito/tools/replace_file.py +31 -4
  29. janito/tools/rich_console.py +176 -0
  30. janito/tools/search_text.py +35 -22
  31. janito/tools/str_replace_editor/editor.py +7 -4
  32. janito/tools/str_replace_editor/handlers/__init__.py +16 -0
  33. janito/tools/str_replace_editor/handlers/create.py +60 -0
  34. janito/tools/str_replace_editor/handlers/insert.py +100 -0
  35. janito/tools/str_replace_editor/handlers/str_replace.py +94 -0
  36. janito/tools/str_replace_editor/handlers/undo.py +64 -0
  37. janito/tools/str_replace_editor/handlers/view.py +159 -0
  38. janito/tools/str_replace_editor/utils.py +0 -1
  39. janito/tools/usage_tracker.py +136 -0
  40. janito-0.13.0.dist-info/METADATA +300 -0
  41. janito-0.13.0.dist-info/RECORD +47 -0
  42. janito/chat_history.py +0 -117
  43. janito/data/instructions.txt +0 -4
  44. janito/tools/bash.py +0 -22
  45. janito/tools/str_replace_editor/handlers.py +0 -335
  46. janito-0.11.0.dist-info/METADATA +0 -86
  47. janito-0.11.0.dist-info/RECORD +0 -26
  48. {janito-0.11.0.dist-info → janito-0.13.0.dist-info}/WHEEL +0 -0
  49. {janito-0.11.0.dist-info → janito-0.13.0.dist-info}/entry_points.txt +0 -0
  50. {janito-0.11.0.dist-info → janito-0.13.0.dist-info}/licenses/LICENSE +0 -0
janito/cli/agent.py ADDED
@@ -0,0 +1,400 @@
1
+ """
2
+ Agent initialization and query handling for Janito CLI.
3
+ """
4
+ import os
5
+ import sys
6
+ import json
7
+ import anthropic
8
+ import claudine
9
+ import typer
10
+ import datetime
11
+ from typing import Optional
12
+ from rich.console import Console
13
+ from pathlib import Path
14
+ from jinja2 import Template
15
+ import importlib.resources as pkg_resources
16
+
17
+ from janito.config import get_config, Config
18
+ from janito.callbacks import text_callback
19
+ from janito.token_report import generate_token_report
20
+ from janito.tools import str_replace_editor
21
+ from janito.tools.bash.bash import bash_tool
22
+ from janito.cli.output import display_generation_params
23
+
24
+
25
+ console = Console()
26
+
27
+ def get_api_key() -> str:
28
+ """
29
+ Get the API key from global config, environment variable, or user input.
30
+
31
+ Returns:
32
+ str: The API key
33
+ """
34
+ # Get API key from global config, environment variable, or ask the user
35
+ api_key = Config.get_api_key()
36
+
37
+ # If not found in global config, try environment variable
38
+ if not api_key:
39
+ api_key = os.environ.get("ANTHROPIC_API_KEY")
40
+
41
+ # If still not found, prompt the user
42
+ if not api_key:
43
+ console.print("[bold yellow]⚠️ Warning:[/bold yellow] API key not found in global config or ANTHROPIC_API_KEY environment variable.")
44
+ console.print("🔑 Please set it using --set-api-key or provide your API key now:")
45
+ api_key = typer.prompt("Anthropic API Key", hide_input=True)
46
+
47
+ return api_key
48
+
49
+ def load_instructions() -> str:
50
+ """
51
+ Load instructions template and render it with variables.
52
+
53
+ Returns:
54
+ str: The rendered instructions
55
+ """
56
+ import platform
57
+
58
+ try:
59
+ # For Python 3.9+
60
+ try:
61
+ from importlib.resources import files
62
+ template_content = files('janito.data').joinpath('instructions_template.txt').read_text(encoding='utf-8')
63
+ # Fallback for older Python versions
64
+ except (ImportError, AttributeError):
65
+ template_content = pkg_resources.read_text('janito.data', 'instructions_template.txt', encoding='utf-8')
66
+
67
+ # Create template variables
68
+ template_variables = {
69
+ 'platform': platform.system(),
70
+ 'role': get_config().role,
71
+ # Add any other variables you want to pass to the template here
72
+ }
73
+
74
+ # Create template and render
75
+ template = Template(template_content)
76
+ instructions = template.render(**template_variables)
77
+
78
+ except Exception as e:
79
+ console.print(f"[bold red]❌ Error loading instructions template:[/bold red] {str(e)}")
80
+ # Try to fall back to regular instructions.txt
81
+ try:
82
+ # For Python 3.9+
83
+ try:
84
+ from importlib.resources import files
85
+ instructions = files('janito.data').joinpath('instructions.txt').read_text(encoding='utf-8')
86
+ # Fallback for older Python versions
87
+ except (ImportError, AttributeError):
88
+ instructions = pkg_resources.read_text('janito.data', 'instructions.txt', encoding='utf-8')
89
+ except Exception as e2:
90
+ console.print(f"[bold red]❌ Error loading fallback instructions:[/bold red] {str(e2)}")
91
+ instructions = "You are Janito, an AI assistant."
92
+
93
+ return instructions
94
+
95
+ def initialize_agent(temperature: float, verbose: bool) -> claudine.Agent:
96
+ """
97
+ Initialize the Claude agent with tools and configuration.
98
+
99
+ Args:
100
+ temperature: Temperature value for model generation
101
+ verbose: Whether to enable verbose mode
102
+
103
+ Returns:
104
+ claudine.Agent: The initialized agent
105
+ """
106
+ # Get API key
107
+ api_key = get_api_key()
108
+
109
+ # Load instructions
110
+ instructions = load_instructions()
111
+
112
+ # Get tools
113
+ from janito.tools import get_tools, reset_tracker
114
+ tools_list = get_tools()
115
+
116
+ # Reset usage tracker before each query
117
+ reset_tracker()
118
+
119
+ # Use command line parameters if provided (not default values), otherwise use config
120
+ temp_to_use = temperature if temperature != 0.0 else get_config().temperature
121
+
122
+ # Get profile parameters if a profile is set
123
+ config = get_config()
124
+ profile_data = None
125
+ if config.profile:
126
+ profile_data = config.get_available_profiles()[config.profile]
127
+
128
+ # Display generation parameters if verbose mode is enabled
129
+ if verbose:
130
+ display_generation_params(temp_to_use, profile_data, temperature)
131
+
132
+ # Create config_params dictionary with generation parameters
133
+ config_params = {
134
+ "temperature": temp_to_use
135
+ }
136
+
137
+ # Add top_k and top_p from profile if available
138
+ if profile_data:
139
+ if "top_k" in profile_data and profile_data["top_k"] != 0:
140
+ config_params["top_k"] = profile_data["top_k"]
141
+ if "top_p" in profile_data and profile_data["top_p"] != 0.0:
142
+ config_params["top_p"] = profile_data["top_p"]
143
+
144
+ # Initialize the agent
145
+ agent = claudine.Agent(
146
+ api_key=api_key,
147
+ system_prompt=instructions,
148
+ callbacks={"text": text_callback},
149
+ text_editor_tool=str_replace_editor,
150
+ bash_tool=bash_tool,
151
+ tools=tools_list,
152
+ verbose=verbose,
153
+ max_tokens=8126,
154
+ max_tool_rounds=100,
155
+ config_params=config_params,
156
+ )
157
+
158
+ return agent
159
+
160
+ def generate_message_id():
161
+ """
162
+ Generate a message ID based on timestamp with seconds granularity
163
+
164
+ Returns:
165
+ str: A timestamp-based message ID
166
+ """
167
+ timestamp = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
168
+ return timestamp
169
+
170
+ def save_messages(agent):
171
+ """
172
+ Save agent messages to .janito/last_messages/{message_id}.json
173
+
174
+ Args:
175
+ agent: The claudine agent instance
176
+
177
+ Returns:
178
+ str: The message ID used for saving
179
+ """
180
+ try:
181
+ # Get the workspace directory
182
+ workspace_dir = Path(get_config().workspace_dir)
183
+
184
+ # Create .janito directory if it doesn't exist
185
+ janito_dir = workspace_dir / ".janito"
186
+ janito_dir.mkdir(exist_ok=True)
187
+
188
+ # Create last_messages directory if it doesn't exist
189
+ messages_dir = janito_dir / "last_messages"
190
+ messages_dir.mkdir(exist_ok=True)
191
+
192
+ # Generate a unique message ID
193
+ message_id = generate_message_id()
194
+
195
+ # Get messages from the agent
196
+ messages = agent.get_messages()
197
+
198
+ # Create a message object with metadata
199
+ message_object = {
200
+ "id": message_id,
201
+ "timestamp": datetime.datetime.now().isoformat(),
202
+ "messages": messages
203
+ }
204
+
205
+ # Save messages to file
206
+ message_file = messages_dir / f"{message_id}.json"
207
+ with open(message_file, "w", encoding="utf-8") as f:
208
+ json.dump(message_object, f, ensure_ascii=False, indent=2)
209
+
210
+ # No longer saving to last_message.json for backward compatibility
211
+
212
+ if get_config().verbose:
213
+ console.print(f"[bold green]✅ Conversation saved to {message_file}[/bold green]")
214
+
215
+ return message_id
216
+ except Exception as e:
217
+ console.print(f"[bold red]❌ Error saving conversation:[/bold red] {str(e)}")
218
+ return None
219
+
220
+ def load_messages(message_id=None):
221
+ """
222
+ Load messages from .janito/last_messages/{message_id}.json or the latest message file
223
+
224
+ Args:
225
+ message_id: Optional message ID to load specific conversation
226
+
227
+ Returns:
228
+ List of message dictionaries or None if file doesn't exist
229
+ """
230
+ try:
231
+ # Get the workspace directory
232
+ workspace_dir = Path(get_config().workspace_dir)
233
+ janito_dir = workspace_dir / ".janito"
234
+ messages_dir = janito_dir / "last_messages"
235
+
236
+ # If message_id is provided, try to load that specific conversation
237
+ if message_id:
238
+ # Check if the message ID is a file name or just the ID
239
+ if message_id.endswith('.json'):
240
+ message_file = messages_dir / message_id
241
+ else:
242
+ message_file = messages_dir / f"{message_id}.json"
243
+
244
+ if not message_file.exists():
245
+ console.print(f"[bold yellow]⚠️ No conversation found with ID {message_id}[/bold yellow]")
246
+ return None
247
+
248
+ # Load messages from file
249
+ with open(message_file, "r", encoding="utf-8") as f:
250
+ message_object = json.load(f)
251
+
252
+ # Extract messages from the message object
253
+ if isinstance(message_object, dict) and "messages" in message_object:
254
+ messages = message_object["messages"]
255
+ else:
256
+ # Handle legacy format
257
+ messages = message_object
258
+
259
+ if get_config().verbose:
260
+ console.print(f"[bold green]✅ Loaded conversation from {message_file}[/bold green]")
261
+ console.print(f"[dim]📝 Conversation has {len(messages)} messages[/dim]")
262
+
263
+ return messages
264
+
265
+ # If no message_id is provided, try to load the latest message from last_messages directory
266
+ if not messages_dir.exists() or not any(messages_dir.iterdir()):
267
+ console.print("[bold yellow]⚠️ No previous conversation found[/bold yellow]")
268
+ return None
269
+
270
+ # Find the latest message file (based on filename which is a timestamp)
271
+ latest_file = max(
272
+ [f for f in messages_dir.iterdir() if f.is_file() and f.suffix == '.json'],
273
+ key=lambda x: x.stem
274
+ )
275
+
276
+ # Load messages from the latest file
277
+ with open(latest_file, "r", encoding="utf-8") as f:
278
+ message_object = json.load(f)
279
+
280
+ # Extract messages from the message object
281
+ if isinstance(message_object, dict) and "messages" in message_object:
282
+ messages = message_object["messages"]
283
+ else:
284
+ # Handle legacy format
285
+ messages = message_object
286
+
287
+ if get_config().verbose:
288
+ console.print(f"[bold green]✅ Loaded latest conversation from {latest_file}[/bold green]")
289
+ console.print(f"[dim]📝 Conversation has {len(messages)} messages[/dim]")
290
+
291
+ return messages
292
+ except Exception as e:
293
+ console.print(f"[bold red]❌ Error loading conversation:[/bold red] {str(e)}")
294
+ return None
295
+
296
+ def handle_query(query: str, temperature: float, verbose: bool, show_tokens: bool, continue_conversation: Optional[str] = None) -> None:
297
+ """
298
+ Handle a query by initializing the agent and sending the query.
299
+
300
+ Args:
301
+ query: The query to send to the agent
302
+ temperature: Temperature value for model generation
303
+ verbose: Whether to enable verbose mode
304
+ show_tokens: Whether to show detailed token usage
305
+ continue_conversation: Optional message ID to continue a specific conversation
306
+ """
307
+ # Initialize the agent
308
+ agent = initialize_agent(temperature, verbose)
309
+
310
+ # Load previous messages if continuing conversation
311
+ if continue_conversation is not None:
312
+ # If continue_conversation is an empty string (from flag with no value), use default behavior
313
+ message_id = None if continue_conversation == "" else continue_conversation
314
+ messages = load_messages(message_id)
315
+ if messages:
316
+ agent.set_messages(messages)
317
+ if message_id:
318
+ console.print(f"[bold blue]🔄 Continuing conversation with ID: {message_id}[/bold blue]")
319
+ else:
320
+ console.print("[bold blue]🔄 Continuing previous conversation[/bold blue]")
321
+
322
+ # Send the query to the agent
323
+ try:
324
+ agent.query(query)
325
+
326
+ # Save messages after successful query and get the message ID
327
+ message_id = save_messages(agent)
328
+
329
+ # Print token usage report
330
+ if show_tokens:
331
+ generate_token_report(agent, verbose=True, interrupted=False)
332
+ else:
333
+ # Show basic token usage
334
+ generate_token_report(agent, verbose=False, interrupted=False)
335
+
336
+ # Print tool usage statistics
337
+ from janito.tools import print_usage_stats
338
+ print_usage_stats()
339
+
340
+ # Show message about continuing this conversation
341
+ if message_id:
342
+ script_name = "janito"
343
+ try:
344
+ # Check if we're running from the entry point script or as a module
345
+ if sys.argv[0].endswith(('janito', 'janito.exe')):
346
+ console.print(f"[bold green]💬 This conversation can be continued with:[/bold green] {script_name} --continue {message_id} <request>")
347
+ else:
348
+ console.print(f"[bold green]💬 This conversation can be continued with:[/bold green] python -m janito --continue {message_id} <request>")
349
+ except:
350
+ # Fallback message
351
+ console.print(f"[bold green]💬 This conversation can be continued with:[/bold green] --continue {message_id} <request>")
352
+
353
+ except KeyboardInterrupt:
354
+ # Handle Ctrl+C by printing token and tool usage information
355
+ console.print("\n[bold yellow]⚠️ Query interrupted by user (Ctrl+C)[/bold yellow]")
356
+
357
+ # Save messages even if interrupted
358
+ message_id = save_messages(agent)
359
+
360
+ # Print token usage report (even if interrupted)
361
+ try:
362
+ if show_tokens:
363
+ generate_token_report(agent, verbose=True, interrupted=True)
364
+ else:
365
+ # Show basic token usage
366
+ generate_token_report(agent, verbose=False, interrupted=True)
367
+
368
+ # Print tool usage statistics
369
+ from janito.tools import print_usage_stats
370
+ print_usage_stats()
371
+
372
+ # Show message about continuing this conversation
373
+ if message_id:
374
+ script_name = "janito"
375
+ try:
376
+ # Check if we're running from the entry point script or as a module
377
+ if sys.argv[0].endswith(('janito', 'janito.exe')):
378
+ console.print(f"[bold green]💬 This conversation can be continued with:[/bold green] {script_name} --continue {message_id} <request>")
379
+ else:
380
+ console.print(f"[bold green]💬 This conversation can be continued with:[/bold green] python -m janito --continue {message_id} <request>")
381
+ except:
382
+ # Fallback message
383
+ console.print(f"[bold green]💬 This conversation can be continued with:[/bold green] --continue {message_id} <request>")
384
+ except Exception as e:
385
+ console.print(f"[bold red]❌ Error generating usage report:[/bold red] {str(e)}")
386
+ if verbose:
387
+ import traceback
388
+ console.print(traceback.format_exc())
389
+
390
+ # Exit with non-zero status to indicate interruption
391
+ sys.exit(130) # 130 is the standard exit code for SIGINT
392
+
393
+ except anthropic.APIError as e:
394
+ console.print(f"[bold red]❌ Anthropic API Error:[/bold red] {str(e)}")
395
+
396
+ except Exception as e:
397
+ console.print(f"[bold red]❌ Error:[/bold red] {str(e)}")
398
+ if verbose:
399
+ import traceback
400
+ console.print(traceback.format_exc())
janito/cli/app.py ADDED
@@ -0,0 +1,94 @@
1
+ """
2
+ Main CLI application for Janito.
3
+ """
4
+ import sys
5
+ from typing import Optional
6
+ import typer
7
+ from rich.console import Console
8
+ import importlib.metadata
9
+
10
+ from janito.config import get_config
11
+ from janito.cli.commands import handle_config_commands, validate_parameters
12
+ from janito.cli.agent import handle_query
13
+ from janito.cli.utils import get_stdin_termination_hint
14
+
15
+ app = typer.Typer()
16
+ console = Console()
17
+
18
+ @app.callback(invoke_without_command=True)
19
+ def main(ctx: typer.Context,
20
+ query: Optional[str] = typer.Argument(None, help="Query to send to the claudine agent"),
21
+ verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable verbose mode with detailed output"),
22
+ show_tokens: bool = typer.Option(False, "--show-tokens", "--tokens", help="Show detailed token usage and pricing information"),
23
+ workspace: Optional[str] = typer.Option(None, "--workspace", "-w", help="Set the workspace directory"),
24
+ config_str: Optional[str] = typer.Option(None, "--set-config", help="Configuration string in format 'key=value', e.g., 'temperature=0.7' or 'profile=technical'"),
25
+ show_config: bool = typer.Option(False, "--show-config", help="Show current configuration"),
26
+ reset_config: bool = typer.Option(False, "--reset-config", help="Reset configuration by removing the config file"),
27
+ set_api_key: Optional[str] = typer.Option(None, "--set-api-key", help="Set the Anthropic API key globally in the user's home directory"),
28
+ ask: bool = typer.Option(False, "--ask", help="Enable ask mode which disables tools that perform changes"),
29
+ trust: bool = typer.Option(False, "--trust", "-t", help="Enable trust mode which suppresses tool outputs for a more concise execution (per-session setting)"),
30
+ temperature: float = typer.Option(0.0, "--temperature", help="Set the temperature for model generation (0.0 to 1.0)"),
31
+ profile: Optional[str] = typer.Option(None, "--profile", help="Use a predefined parameter profile (precise, balanced, conversational, creative, technical)"),
32
+ role: Optional[str] = typer.Option(None, "--role", help="Set the assistant's role (default: 'software engineer')"),
33
+ version: bool = typer.Option(False, "--version", help="Show the version and exit"),
34
+ continue_conversation: Optional[str] = typer.Option(None, "--continue", "-c", help="Continue a previous conversation, optionally with a specific message ID")):
35
+ """
36
+ Janito CLI tool. If a query is provided without a command, it will be sent to the claudine agent.
37
+ """
38
+ # Set verbose mode in config
39
+ get_config().verbose = verbose
40
+
41
+ # Set ask mode in config
42
+ get_config().ask_mode = ask
43
+
44
+ # Set trust mode in config
45
+ get_config().trust_mode = trust
46
+
47
+ # Show a message if ask mode is enabled
48
+ if ask:
49
+ console.print("[bold yellow]⚠️ Ask Mode enabled:[/bold yellow] 🔒 Tools that perform changes are disabled")
50
+
51
+ # Show a message if trust mode is enabled
52
+ if trust:
53
+ console.print("[bold blue]⚡ Trust Mode enabled:[/bold blue] Tool outputs are suppressed for concise execution (per-session setting)")
54
+
55
+ # Show version and exit if requested
56
+ if version:
57
+ try:
58
+ version_str = importlib.metadata.version("janito")
59
+ console.print(f"🚀 Janito version: {version_str}")
60
+ except importlib.metadata.PackageNotFoundError:
61
+ console.print("🚀 Janito version: [italic]development[/italic]")
62
+ sys.exit(0)
63
+
64
+ # Validate temperature
65
+ validate_parameters(temperature)
66
+
67
+ # Handle configuration-related commands
68
+ exit_after_config = handle_config_commands(
69
+ ctx,
70
+ reset_config,
71
+ workspace,
72
+ show_config,
73
+ profile,
74
+ role,
75
+ set_api_key,
76
+ config_str,
77
+ query,
78
+ continue_conversation
79
+ )
80
+
81
+ if exit_after_config:
82
+ sys.exit(0)
83
+
84
+ # Handle query if no subcommand was invoked
85
+ if ctx.invoked_subcommand is None:
86
+ # If no query provided in command line, read from stdin
87
+ if not query:
88
+ console.print("[bold blue]📝 No query provided in command line. Reading from stdin...[/bold blue]")
89
+ console.print(get_stdin_termination_hint())
90
+ query = sys.stdin.read().strip()
91
+
92
+ # Only proceed if we have a query (either from command line or stdin)
93
+ if query:
94
+ handle_query(query, temperature, verbose, show_tokens, continue_conversation)