janito 1.3.2__tar.gz → 1.4.1__tar.gz

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 (73) hide show
  1. {janito-1.3.2 → janito-1.4.1}/PKG-INFO +6 -7
  2. {janito-1.3.2 → janito-1.4.1}/README.md +5 -5
  3. janito-1.4.1/janito/__init__.py +1 -0
  4. {janito-1.3.2 → janito-1.4.1}/janito/agent/conversation.py +7 -1
  5. {janito-1.3.2 → janito-1.4.1}/janito/agent/templates/system_instructions.j2 +4 -8
  6. {janito-1.3.2 → janito-1.4.1}/janito/agent/tool_handler.py +62 -95
  7. janito-1.4.1/janito/agent/tools/__init__.py +9 -0
  8. janito-1.4.1/janito/agent/tools/ask_user.py +61 -0
  9. janito-1.4.1/janito/agent/tools/fetch_url.py +35 -0
  10. janito-1.4.1/janito/agent/tools/file_ops.py +60 -0
  11. janito-1.4.1/janito/agent/tools/find_files.py +31 -0
  12. janito-1.4.1/janito/agent/tools/get_file_outline.py +22 -0
  13. janito-1.4.1/janito/agent/tools/get_lines.py +37 -0
  14. {janito-1.3.2 → janito-1.4.1}/janito/agent/tools/gitignore_utils.py +4 -1
  15. janito-1.4.1/janito/agent/tools/py_compile.py +23 -0
  16. janito-1.4.1/janito/agent/tools/python_exec.py +54 -0
  17. janito-1.4.1/janito/agent/tools/remove_directory.py +24 -0
  18. janito-1.4.1/janito/agent/tools/replace_text_in_file.py +67 -0
  19. {janito-1.3.2 → janito-1.4.1}/janito/agent/tools/rich_utils.py +6 -6
  20. janito-1.4.1/janito/agent/tools/run_bash_command.py +80 -0
  21. janito-1.4.1/janito/agent/tools/search_files.py +26 -0
  22. {janito-1.3.2 → janito-1.4.1}/janito/agent/tools/tool_base.py +4 -2
  23. janito-1.4.1/janito/agent/tools/utils.py +31 -0
  24. {janito-1.3.2 → janito-1.4.1}/janito.egg-info/PKG-INFO +6 -7
  25. {janito-1.3.2 → janito-1.4.1}/janito.egg-info/SOURCES.txt +2 -0
  26. {janito-1.3.2 → janito-1.4.1}/pyproject.toml +2 -2
  27. janito-1.3.2/janito/__init__.py +0 -1
  28. janito-1.3.2/janito/agent/tools/__init__.py +0 -12
  29. janito-1.3.2/janito/agent/tools/ask_user.py +0 -64
  30. janito-1.3.2/janito/agent/tools/fetch_url.py +0 -40
  31. janito-1.3.2/janito/agent/tools/file_ops.py +0 -72
  32. janito-1.3.2/janito/agent/tools/find_files.py +0 -58
  33. janito-1.3.2/janito/agent/tools/get_lines.py +0 -58
  34. janito-1.3.2/janito/agent/tools/py_compile.py +0 -26
  35. janito-1.3.2/janito/agent/tools/python_exec.py +0 -47
  36. janito-1.3.2/janito/agent/tools/remove_directory.py +0 -38
  37. janito-1.3.2/janito/agent/tools/replace_text_in_file.py +0 -67
  38. janito-1.3.2/janito/agent/tools/run_bash_command.py +0 -134
  39. janito-1.3.2/janito/agent/tools/search_files.py +0 -52
  40. {janito-1.3.2 → janito-1.4.1}/LICENSE +0 -0
  41. {janito-1.3.2 → janito-1.4.1}/janito/__main__.py +0 -0
  42. {janito-1.3.2 → janito-1.4.1}/janito/agent/__init__.py +0 -0
  43. {janito-1.3.2 → janito-1.4.1}/janito/agent/agent.py +0 -0
  44. {janito-1.3.2 → janito-1.4.1}/janito/agent/config.py +0 -0
  45. {janito-1.3.2 → janito-1.4.1}/janito/agent/config_defaults.py +0 -0
  46. {janito-1.3.2 → janito-1.4.1}/janito/agent/config_utils.py +0 -0
  47. {janito-1.3.2 → janito-1.4.1}/janito/agent/queued_tool_handler.py +0 -0
  48. {janito-1.3.2 → janito-1.4.1}/janito/agent/runtime_config.py +0 -0
  49. {janito-1.3.2 → janito-1.4.1}/janito/agent/tools/rich_live.py +0 -0
  50. {janito-1.3.2 → janito-1.4.1}/janito/cli/__init__.py +0 -0
  51. {janito-1.3.2 → janito-1.4.1}/janito/cli/_print_config.py +0 -0
  52. {janito-1.3.2 → janito-1.4.1}/janito/cli/_utils.py +0 -0
  53. {janito-1.3.2 → janito-1.4.1}/janito/cli/arg_parser.py +0 -0
  54. {janito-1.3.2 → janito-1.4.1}/janito/cli/config_commands.py +0 -0
  55. {janito-1.3.2 → janito-1.4.1}/janito/cli/logging_setup.py +0 -0
  56. {janito-1.3.2 → janito-1.4.1}/janito/cli/main.py +0 -0
  57. {janito-1.3.2 → janito-1.4.1}/janito/cli/runner.py +0 -0
  58. {janito-1.3.2 → janito-1.4.1}/janito/cli_chat_shell/__init__.py +0 -0
  59. {janito-1.3.2 → janito-1.4.1}/janito/cli_chat_shell/chat_loop.py +0 -0
  60. {janito-1.3.2 → janito-1.4.1}/janito/cli_chat_shell/commands.py +0 -0
  61. {janito-1.3.2 → janito-1.4.1}/janito/cli_chat_shell/config_shell.py +0 -0
  62. {janito-1.3.2 → janito-1.4.1}/janito/cli_chat_shell/load_prompt.py +0 -0
  63. {janito-1.3.2 → janito-1.4.1}/janito/cli_chat_shell/session_manager.py +0 -0
  64. {janito-1.3.2 → janito-1.4.1}/janito/cli_chat_shell/ui.py +0 -0
  65. {janito-1.3.2 → janito-1.4.1}/janito/render_prompt.py +0 -0
  66. {janito-1.3.2 → janito-1.4.1}/janito/web/__init__.py +0 -0
  67. {janito-1.3.2 → janito-1.4.1}/janito/web/__main__.py +0 -0
  68. {janito-1.3.2 → janito-1.4.1}/janito/web/app.py +0 -0
  69. {janito-1.3.2 → janito-1.4.1}/janito.egg-info/dependency_links.txt +0 -0
  70. {janito-1.3.2 → janito-1.4.1}/janito.egg-info/entry_points.txt +0 -0
  71. {janito-1.3.2 → janito-1.4.1}/janito.egg-info/requires.txt +0 -0
  72. {janito-1.3.2 → janito-1.4.1}/janito.egg-info/top_level.txt +0 -0
  73. {janito-1.3.2 → janito-1.4.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: janito
3
- Version: 1.3.2
3
+ Version: 1.4.1
4
4
  Summary: A Natural Programming Language Agent,
5
5
  Author-email: João Pinto <joao.pinto@gmail.com>
6
6
  License: MIT
@@ -8,7 +8,6 @@ Project-URL: homepage, https://github.com/joaompinto/janito
8
8
  Project-URL: repository, https://github.com/joaompinto/janito
9
9
  Keywords: agent,framework,tools,automation
10
10
  Classifier: Programming Language :: Python :: 3.10
11
- Classifier: License :: OSI Approved :: MIT License
12
11
  Classifier: Operating System :: OS Independent
13
12
  Requires-Python: >=3.10
14
13
  Description-Content-Type: text/markdown
@@ -23,7 +22,7 @@ Requires-Dist: requests
23
22
  Requires-Dist: rich
24
23
  Dynamic: license-file
25
24
 
26
- # 🚀 Janito: Agent
25
+ # 🚀 Janito: Natural Programming Language Agent
27
26
 
28
27
  Janito is an AI-powered assistant for the command line and web that interprets natural language instructions to edit code, manage files, and analyze projects using patterns and tools designed by experienced software engineers. It prioritizes transparency, interactive clarification, and precise, reviewable changes.
29
28
 
@@ -60,7 +59,7 @@ python -m janito.web
60
59
  - `replace_text_in_file`: Replace exact text fragments in files.
61
60
  - `search_files`: Search for text patterns across files.
62
61
  - `python_exec`: Execute Python code and capture output.
63
- - And more, see `janito/agent/tools/` for the full list.
62
+ - And more built-in operations for code and file management.
64
63
  - 🌐 **Web Interface (In Development):** Upcoming simple web UI for streaming responses and tool progress.
65
64
 
66
65
  ---
@@ -79,9 +78,9 @@ Below are the supported configuration parameters and CLI flags. Some options can
79
78
 
80
79
  | Key / Flag | Description | How to set | Default |
81
80
  |---------------------------|---------------------------------------------------------------------------------------------|-----------------------------------------------------------------|--------------------------------------------|
82
- | `api_key` | API key for OpenAI-compatible service | `--set-api-key`, config file | _None_ (required) |
83
- | `model` | Model name to use for this session | `--model` (session only), `--set-local-config model=...`, or `--set-global-config` | `openai/gpt-4.1` |
84
- | `base_url` | API base URL (OpenAI-compatible endpoint) | `--set-local-config base_url=...` or `--set-global-config` | `https://openrouter.ai/api/v1` |
81
+ | `api_key` | API key for a compatible language model service | `--set-api-key`, config file | _None_ (required) |
82
+ | `model` | Model name to use for this session | `--model` (session only), `--set-local-config model=...`, or `--set-global-config` | _(example: gpt-4)_ |
83
+ | `base_url` | API base URL for your language model service | `--set-local-config base_url=...` or `--set-global-config` | _(example: https://api.your-model.com)_ |
85
84
  | `role` | Role description for the system prompt | `--role` or config | "software engineer" |
86
85
  | `system_prompt` | Override the entire system prompt as a raw string | `--system-prompt` or config | _Default prompt_ |
87
86
  | `system_file` | Use a plain text file as the system prompt (takes precedence over `system_prompt`) | `--system-file` (CLI only) | _None_ |
@@ -1,4 +1,4 @@
1
- # 🚀 Janito: Agent
1
+ # 🚀 Janito: Natural Programming Language Agent
2
2
 
3
3
  Janito is an AI-powered assistant for the command line and web that interprets natural language instructions to edit code, manage files, and analyze projects using patterns and tools designed by experienced software engineers. It prioritizes transparency, interactive clarification, and precise, reviewable changes.
4
4
 
@@ -35,7 +35,7 @@ python -m janito.web
35
35
  - `replace_text_in_file`: Replace exact text fragments in files.
36
36
  - `search_files`: Search for text patterns across files.
37
37
  - `python_exec`: Execute Python code and capture output.
38
- - And more, see `janito/agent/tools/` for the full list.
38
+ - And more built-in operations for code and file management.
39
39
  - 🌐 **Web Interface (In Development):** Upcoming simple web UI for streaming responses and tool progress.
40
40
 
41
41
  ---
@@ -54,9 +54,9 @@ Below are the supported configuration parameters and CLI flags. Some options can
54
54
 
55
55
  | Key / Flag | Description | How to set | Default |
56
56
  |---------------------------|---------------------------------------------------------------------------------------------|-----------------------------------------------------------------|--------------------------------------------|
57
- | `api_key` | API key for OpenAI-compatible service | `--set-api-key`, config file | _None_ (required) |
58
- | `model` | Model name to use for this session | `--model` (session only), `--set-local-config model=...`, or `--set-global-config` | `openai/gpt-4.1` |
59
- | `base_url` | API base URL (OpenAI-compatible endpoint) | `--set-local-config base_url=...` or `--set-global-config` | `https://openrouter.ai/api/v1` |
57
+ | `api_key` | API key for a compatible language model service | `--set-api-key`, config file | _None_ (required) |
58
+ | `model` | Model name to use for this session | `--model` (session only), `--set-local-config model=...`, or `--set-global-config` | _(example: gpt-4)_ |
59
+ | `base_url` | API base URL for your language model service | `--set-local-config base_url=...` or `--set-global-config` | _(example: https://api.your-model.com)_ |
60
60
  | `role` | Role description for the system prompt | `--role` or config | "software engineer" |
61
61
  | `system_prompt` | Override the entire system prompt as a raw string | `--system-prompt` or config | _Default prompt_ |
62
62
  | `system_file` | Use a plain text file as the system prompt (takes precedence over `system_prompt`) | `--system-file` (CLI only) | _None_ |
@@ -0,0 +1 @@
1
+ __version__ = "1.4.1"
@@ -45,7 +45,13 @@ class ConversationHandler:
45
45
  if spinner:
46
46
  # Calculate word count for all messages
47
47
  word_count = sum(len(str(m.get('content', '')).split()) for m in messages if 'content' in m)
48
- spinner_msg = f"[bold green]Waiting for AI response... ({word_count} words in conversation)"
48
+ def format_count(n):
49
+ if n >= 1_000_000:
50
+ return f"{n/1_000_000:.1f}m"
51
+ elif n >= 1_000:
52
+ return f"{n/1_000:.1f}k"
53
+ return str(n)
54
+ spinner_msg = f"[bold green]Waiting for AI response... ({format_count(word_count)} words in conversation)"
49
55
  with console.status(spinner_msg, spinner="dots") as status:
50
56
  response = self.client.chat.completions.create(
51
57
  model=self.model,
@@ -4,6 +4,7 @@ You are an assistant for a analysis and development tool that operates on files
4
4
  directories using text-based operations.
5
5
 
6
6
  Provide a concise plan before calling any tool.
7
+ Always execute the plan immediately after presenting it, unless the user requests otherwise.
7
8
 
8
9
  <context>
9
10
  Always review `README_structure.txt` before conducting file-specific searches.
@@ -12,25 +13,20 @@ Explore files that might be relevant to the current task.
12
13
  </context>
13
14
 
14
15
  <analysis>
15
- When analyzing issues, you might want to look into the git history for clues.
16
+ In case of missing code or functions, look into the .bak files and check git diff/history for recent changes.
16
17
  </analysis>
17
18
 
18
19
  <editing>
19
20
  If in doubt during editing, use the `ask_user` function to get additional information; otherwise, proceed and inform the user of the decision made.
20
21
 
21
22
  When you need to make changes to a file, consider the following:
22
-
23
- - Use the `edit_file` tool when you want to update or fix specific text fragments within a file without altering the rest of its content. It is preferred over full file replacement when:
24
- - Only small, targeted changes are needed.
25
- - You want to avoid the risk of accidentally overwriting unrelated content.
26
- - The file is large, and rewriting the entire file would be inefficient.
27
- - You want to preserve formatting, comments, or code structure outside the replaced text.
28
-
23
+ - It is preferred to replace exact text occurrences over file overwriting.
29
24
  - When replacing files, review their current content before requesting the update.
30
25
  - When reorganizing, moving files, or functions, search for references in other files that might need to be updated accordingly.
31
26
  </editing>
32
27
 
33
28
  <finishing>
29
+ - When asked to commit and no message is provided, check the git diff and summarize the changes in the commit message.
34
30
  - Review the README content if there are user-exposed or public API changes.
35
31
  - Update `README_structure.txt` considering discovered, created, or modified files.
36
32
  </finishing>
@@ -1,102 +1,40 @@
1
1
  import os
2
2
  import json
3
3
  import traceback
4
+ from janito.agent.tools.tool_base import ToolBase
4
5
 
5
6
  class ToolHandler:
6
7
  _tool_registry = {}
7
8
 
8
- @classmethod
9
- def register_tool(cls, func):
10
- import inspect
11
- import typing
12
- from typing import get_origin, get_args
13
-
14
- name = func.__name__
15
- description = func.__doc__ or ""
16
-
17
- sig = inspect.signature(func)
18
- params_schema = {
19
- "type": "object",
20
- "properties": {},
21
- "required": []
22
- }
23
-
24
- for param_name, param in sig.parameters.items():
25
- if param.annotation is param.empty:
26
- raise TypeError(f"Parameter '{param_name}' in tool '{name}' is missing a type hint.")
27
- param_type = param.annotation
28
- schema = {}
29
-
30
- # Handle typing.Optional, typing.List, typing.Literal, etc.
31
- origin = get_origin(param_type)
32
- args = get_args(param_type)
33
-
34
- if origin is typing.Union and type(None) in args:
35
- # Optional[...] type
36
- main_type = args[0] if args[1] is type(None) else args[1]
37
- origin = get_origin(main_type)
38
- args = get_args(main_type)
39
- param_type = main_type
40
- else:
41
- main_type = param_type
42
-
43
- if origin is list or origin is typing.List:
44
- item_type = args[0] if args else str
45
- item_schema = {"type": _pytype_to_json_type(item_type)}
46
- schema = {"type": "array", "items": item_schema}
47
- elif origin is typing.Literal:
48
- schema = {"type": _pytype_to_json_type(type(args[0])), "enum": list(args)}
49
- elif main_type == int:
50
- schema = {"type": "integer"}
51
- elif main_type == float:
52
- schema = {"type": "number"}
53
- elif main_type == bool:
54
- schema = {"type": "boolean"}
55
- elif main_type == dict:
56
- schema = {"type": "object"}
57
- elif main_type == list:
58
- schema = {"type": "array", "items": {"type": "string"}}
59
- else:
60
- schema = {"type": "string"}
61
-
62
- # Optionally add description if available in docstring (not implemented here)
63
- params_schema["properties"][param_name] = schema
64
- if param.default is param.empty:
65
- params_schema["required"].append(param_name)
66
-
67
- cls._tool_registry[name] = {
68
- "function": func,
69
- "description": description,
70
- "parameters": params_schema
71
- }
72
- return func
73
-
74
- def _pytype_to_json_type(pytype):
75
- import typing
76
- if pytype == int:
77
- return "integer"
78
- elif pytype == float:
79
- return "number"
80
- elif pytype == bool:
81
- return "boolean"
82
- elif pytype == dict:
83
- return "object"
84
- elif pytype == list or pytype == typing.List:
85
- return "array"
86
- else:
87
- return "string"
88
-
89
- class ToolHandler:
90
- _tool_registry = {}
9
+ def __init__(self, verbose=False, enable_tools=True):
10
+ self.verbose = verbose
11
+ self.tools = []
12
+ self.enable_tools = enable_tools
91
13
 
92
14
  @classmethod
93
- def register_tool(cls, func):
15
+ def register_tool(cls, tool=None, *, name: str = None):
16
+ """
17
+ Register a tool class derived from ToolBase.
18
+ Args:
19
+ tool: The tool class (must inherit from ToolBase).
20
+ name: Optional override for the tool name.
21
+ Raises:
22
+ TypeError: If the tool is not a subclass of ToolBase.
23
+ """
24
+ if tool is None:
25
+ return lambda t: cls.register_tool(t, name=name)
94
26
  import inspect
95
27
  import typing
96
28
  from typing import get_origin, get_args
97
29
 
98
- name = func.__name__
99
- description = func.__doc__ or ""
30
+ override_name = name
31
+ if not (isinstance(tool, type) and issubclass(tool, ToolBase)):
32
+ raise TypeError("Tool must be a class derived from ToolBase.")
33
+ instance = tool()
34
+ func = instance.call
35
+ default_name = tool.__name__
36
+ name = override_name or default_name
37
+ description = tool.__doc__ or func.__doc__ or ""
100
38
 
101
39
  sig = inspect.signature(func)
102
40
  params_schema = {
@@ -143,22 +81,30 @@ class ToolHandler:
143
81
  else:
144
82
  schema = {"type": "string"}
145
83
 
146
- # Optionally add description if available in docstring (not implemented here)
84
+ # Add description from call method docstring if available (Google-style Args parsing)
85
+ if func.__doc__:
86
+ import re
87
+ doc = func.__doc__
88
+ args_section = re.search(r"Args:\s*(.*?)(?:\n\s*\w|Returns:|$)", doc, re.DOTALL)
89
+ param_descs = {}
90
+ if args_section:
91
+ args_text = args_section.group(1)
92
+ for match in re.finditer(r"(\w+) \([^)]+\): ([^\n]+)", args_text):
93
+ pname, pdesc = match.groups()
94
+ param_descs[pname] = pdesc.strip()
95
+ if param_name in param_descs:
96
+ schema["description"] = param_descs[param_name]
147
97
  params_schema["properties"][param_name] = schema
148
98
  if param.default is param.empty:
149
99
  params_schema["required"].append(param_name)
150
100
 
101
+ # register the bound call function
151
102
  cls._tool_registry[name] = {
152
103
  "function": func,
153
104
  "description": description,
154
105
  "parameters": params_schema
155
106
  }
156
- return func
157
-
158
- def __init__(self, verbose=False, enable_tools=True):
159
- self.verbose = verbose
160
- self.tools = []
161
- self.enable_tools = enable_tools
107
+ return tool
162
108
 
163
109
  def register(self, func):
164
110
  self.tools.append(func)
@@ -194,6 +140,11 @@ class ToolHandler:
194
140
  print(f"[Tool Call] {tool_call.function.name} called with arguments: {args}")
195
141
  import inspect
196
142
  sig = inspect.signature(func)
143
+ # Set progress callback on tool instance if possible
144
+ instance = None
145
+ if hasattr(func, '__self__') and isinstance(func.__self__, ToolBase):
146
+ instance = func.__self__
147
+ instance._progress_callback = on_progress
197
148
  if on_progress:
198
149
  on_progress({
199
150
  'event': 'start',
@@ -201,8 +152,6 @@ class ToolHandler:
201
152
  'tool': tool_call.function.name,
202
153
  'args': args
203
154
  })
204
- if 'on_progress' in sig.parameters and on_progress is not None:
205
- args['on_progress'] = on_progress
206
155
  try:
207
156
  result = func(**args)
208
157
  except Exception as e:
@@ -226,4 +175,22 @@ class ToolHandler:
226
175
  'args': args,
227
176
  'result': result
228
177
  })
178
+ # Clean up progress callback
179
+ if instance is not None:
180
+ instance._progress_callback = None
229
181
  return result
182
+
183
+ def _pytype_to_json_type(pytype):
184
+ import typing
185
+ if pytype == int:
186
+ return "integer"
187
+ elif pytype == float:
188
+ return "number"
189
+ elif pytype == bool:
190
+ return "boolean"
191
+ elif pytype == dict:
192
+ return "object"
193
+ elif pytype == list or pytype == typing.List:
194
+ return "array"
195
+ else:
196
+ return "string"
@@ -0,0 +1,9 @@
1
+ import importlib
2
+ import os
3
+
4
+ # Dynamically import all tool modules in this directory (except __init__.py and tool_base.py)
5
+ _tool_dir = os.path.dirname(__file__)
6
+ for fname in os.listdir(_tool_dir):
7
+ if fname.endswith('.py') and fname not in ('__init__.py', 'tool_base.py'):
8
+ modname = fname[:-3]
9
+ importlib.import_module(f'janito.agent.tools.{modname}')
@@ -0,0 +1,61 @@
1
+ from janito.agent.tools.tool_base import ToolBase
2
+ from janito.agent.tools.rich_utils import print_info, print_success
3
+
4
+ class AskUserTool(ToolBase):
5
+ """Ask the user a question and return their response."""
6
+ def call(self, question: str) -> str:
7
+ from rich import print as rich_print
8
+ from rich.panel import Panel
9
+ from prompt_toolkit import PromptSession
10
+ from prompt_toolkit.key_binding import KeyBindings
11
+ from prompt_toolkit.enums import EditingMode
12
+ from prompt_toolkit.formatted_text import HTML
13
+ from prompt_toolkit.styles import Style
14
+
15
+ rich_print(Panel.fit(question, title="Question", style="cyan"))
16
+
17
+ bindings = KeyBindings()
18
+ mode = {'multiline': False}
19
+
20
+ @bindings.add('c-r')
21
+ def _(event):
22
+ pass
23
+
24
+ style = Style.from_dict({
25
+ 'bottom-toolbar': 'bg:#333333 #ffffff',
26
+ 'b': 'bold',
27
+ 'prompt': 'bold bg:#000080 #ffffff',
28
+ })
29
+
30
+ def get_toolbar():
31
+ if mode['multiline']:
32
+ return HTML('<b>Multiline mode (Esc+Enter to submit). Type /single to switch.</b>')
33
+ else:
34
+ return HTML('<b>Single-line mode (Enter to submit). Type /multi for multiline.</b>')
35
+
36
+ session = PromptSession(
37
+ multiline=False,
38
+ key_bindings=bindings,
39
+ editing_mode=EditingMode.EMACS,
40
+ bottom_toolbar=get_toolbar,
41
+ style=style
42
+ )
43
+
44
+ prompt_icon = HTML('<prompt>💬 </prompt>')
45
+
46
+ while True:
47
+ response = session.prompt(prompt_icon)
48
+ if not mode['multiline'] and response.strip() == '/multi':
49
+ mode['multiline'] = True
50
+ session.multiline = True
51
+ continue
52
+ elif mode['multiline'] and response.strip() == '/single':
53
+ mode['multiline'] = False
54
+ session.multiline = False
55
+ continue
56
+ else:
57
+ return response
58
+
59
+
60
+ from janito.agent.tool_handler import ToolHandler
61
+ ToolHandler.register_tool(AskUserTool, name="ask_user")
@@ -0,0 +1,35 @@
1
+ import requests
2
+ from typing import Optional
3
+ from bs4 import BeautifulSoup
4
+ from janito.agent.tool_handler import ToolHandler
5
+ from janito.agent.tools.rich_utils import print_info, print_success, print_error
6
+ from janito.agent.tools.tool_base import ToolBase
7
+
8
+ class FetchUrlTool(ToolBase):
9
+ """Fetch the content of a web page and extract its text."""
10
+ def call(self, url: str, search_strings: list[str] = None) -> str:
11
+ print_info(f"🌐 Fetching URL: {url} ... ", end="")
12
+ response = requests.get(url, timeout=10)
13
+ response.raise_for_status()
14
+ self.update_progress(f"Fetched URL with status {response.status_code}")
15
+ soup = BeautifulSoup(response.text, 'html.parser')
16
+ text = soup.get_text(separator='\n')
17
+
18
+ if search_strings:
19
+ filtered = []
20
+ for s in search_strings:
21
+ idx = text.find(s)
22
+ if idx != -1:
23
+ start = max(0, idx - 200)
24
+ end = min(len(text), idx + len(s) + 200)
25
+ snippet = text[start:end]
26
+ filtered.append(snippet)
27
+ if filtered:
28
+ text = '\n...\n'.join(filtered)
29
+ else:
30
+ text = "No matches found for the provided search strings."
31
+
32
+ print_success("✅ Success")
33
+ return text
34
+
35
+ ToolHandler.register_tool(FetchUrlTool, name="fetch_url")
@@ -0,0 +1,60 @@
1
+ import os
2
+ import shutil
3
+ from janito.agent.tool_handler import ToolHandler
4
+ from janito.agent.tools.rich_utils import print_info, print_success, print_error
5
+ from janito.agent.tools.utils import expand_path, display_path
6
+ from janito.agent.tools.tool_base import ToolBase
7
+
8
+ class CreateFileTool(ToolBase):
9
+ """
10
+ Create a new file or update an existing file with the given content.
11
+ """
12
+ def call(self, path: str, content: str, overwrite: bool = False) -> str:
13
+ original_path = path
14
+ path = expand_path(path)
15
+ updating = os.path.exists(path) and not os.path.isdir(path)
16
+ disp_path = display_path(original_path, path)
17
+ if os.path.exists(path):
18
+ if os.path.isdir(path):
19
+ print_error(f"❌ Error: is a directory")
20
+ return f"❌ Cannot create file: '{disp_path}' is an existing directory."
21
+ if not overwrite:
22
+ print_error(f"❗ Error: file '{disp_path}' exists and overwrite is False")
23
+ return f"❗ Cannot create file: '{disp_path}' already exists and overwrite is False."
24
+ if updating and overwrite:
25
+ print_info(f"📝 Updating file: '{disp_path}' ... ")
26
+ else:
27
+ print_info(f"📝 Creating file: '{disp_path}' ... ")
28
+ old_lines = None
29
+ if updating and overwrite:
30
+ with open(path, "r", encoding="utf-8") as f:
31
+ old_lines = sum(1 for _ in f)
32
+ with open(path, "w", encoding="utf-8") as f:
33
+ f.write(content)
34
+ new_lines = content.count('\n') + 1 if content else 0
35
+ if old_lines is not None:
36
+ print_success(f"✅ Successfully updated the file at '{disp_path}' ({old_lines} > {new_lines} lines).")
37
+ return f"✅ Successfully updated the file at '{disp_path}' ({old_lines} > {new_lines} lines)."
38
+ print_success(f"✅ Successfully created the file at '{disp_path}' ({new_lines} lines).")
39
+ return f"✅ Successfully created the file at '{disp_path}' ({new_lines} lines)."
40
+
41
+ class CreateDirectoryTool(ToolBase):
42
+ """
43
+ Create a new directory at the specified path.
44
+ """
45
+ def call(self, path: str, overwrite: bool = False) -> str:
46
+ """
47
+ Create a new directory at the specified path.
48
+ Args:
49
+ path (str): Path to the directory to create.
50
+ overwrite (bool): Whether to remove the directory if it exists.
51
+ Returns:
52
+ str: Result message.
53
+ """
54
+ original_path = path
55
+ path = expand_path(path)
56
+ disp_path = display_path(original_path, path)
57
+ if os.path.exists(path):
58
+ if not os.path.isdir(path):
59
+ print_error(f"❌ Path '{disp_path}' exists and is not a directory.")
60
+ return f"❌ Path '{disp_path}' exists and is not a directory."
@@ -0,0 +1,31 @@
1
+ from janito.agent.tools.tool_base import ToolBase
2
+ from janito.agent.tool_handler import ToolHandler
3
+ from janito.agent.tools.rich_utils import print_info, print_success
4
+ import os
5
+ import fnmatch
6
+
7
+ class FindFilesTool(ToolBase):
8
+ """Find files in a directory matching a pattern."""
9
+ def call(self, directory: str, pattern: str, recursive: bool=False, max_results: int=100) -> str:
10
+ import os
11
+ def _display_path(path):
12
+ import os
13
+ if os.path.isabs(path):
14
+ return path
15
+ return os.path.relpath(path)
16
+ disp_path = _display_path(directory)
17
+ rec = "recursively" if recursive else "non-recursively"
18
+ print_info(f"\U0001F50D Searching '{disp_path}' for pattern '{pattern}' ({rec}, max {max_results})", end="")
19
+ self.update_progress(f"Searching for files in {directory} matching {pattern}")
20
+ matches = []
21
+ for root, dirs, files in os.walk(directory):
22
+ for filename in fnmatch.filter(files, pattern):
23
+ matches.append(os.path.join(root, filename))
24
+ if len(matches) >= max_results:
25
+ break
26
+ if not recursive:
27
+ break
28
+ print_success(f"✅ {len(matches)} found")
29
+ return "\n".join(matches)
30
+
31
+ ToolHandler.register_tool(FindFilesTool, name="find_files")
@@ -0,0 +1,22 @@
1
+ from janito.agent.tools.tool_base import ToolBase
2
+ from janito.agent.tool_handler import ToolHandler
3
+
4
+ from janito.agent.tools.rich_utils import print_info, print_success, print_error
5
+
6
+ class GetFileOutlineTool(ToolBase):
7
+ """Get an outline of a file's structure."""
8
+ def call(self, file_path: str) -> str:
9
+ print_info(f"\U0001F4C4 Getting outline for: {file_path}", end="")
10
+ self.update_progress(f"Getting outline for: {file_path}")
11
+ try:
12
+ with open(file_path, 'r', encoding='utf-8') as f:
13
+ lines = f.readlines()
14
+ outline = [line.strip() for line in lines if line.strip()]
15
+ num_items = len(outline)
16
+ print_success(f"✅ Outline generated ({num_items} items)")
17
+ return f"Outline: {num_items} items\n" + '\n'.join(outline)
18
+ except Exception as e:
19
+ print_error(f"\u274c Error reading file: {e}")
20
+ return f"Error reading file: {e}"
21
+
22
+ ToolHandler.register_tool(GetFileOutlineTool, name="get_file_outline")
@@ -0,0 +1,37 @@
1
+ from janito.agent.tools.tool_base import ToolBase
2
+ from janito.agent.tool_handler import ToolHandler
3
+ from janito.agent.tools.rich_utils import print_info, print_success, print_error
4
+
5
+ class GetLinesTool(ToolBase):
6
+ """Get specific lines from a file."""
7
+ def call(self, file_path: str, from_line: int=None, to_line: int=None) -> str:
8
+ import os
9
+ def _display_path(path):
10
+ import os
11
+ if os.path.isabs(path):
12
+ return path
13
+ return os.path.relpath(path)
14
+ disp_path = _display_path(file_path)
15
+ if from_line and to_line:
16
+ count = to_line - from_line + 1
17
+ print_info(f"📄 Reading {disp_path}:{from_line} ({count} lines)", end="")
18
+ else:
19
+ print_info(f"📄 Reading {disp_path} (all lines)", end="")
20
+ self.update_progress(f"Getting lines {from_line} to {to_line} from {file_path}")
21
+ try:
22
+ with open(file_path, 'r', encoding='utf-8') as f:
23
+ lines = f.readlines()
24
+ selected = lines[(from_line-1 if from_line else 0):(to_line if to_line else None)]
25
+ if from_line and to_line:
26
+ print_success(f" ✅ {to_line - from_line + 1} lines read")
27
+ else:
28
+ print_success(f" ✅ {len(lines)} lines read")
29
+ return ''.join(selected)
30
+ except Exception as e:
31
+ if isinstance(e, FileNotFoundError):
32
+ print_error(f"❗ not found")
33
+ return "❗ not found"
34
+ print_error(f" ❌ Error: {e}")
35
+ return f"Error reading file: {e}"
36
+
37
+ ToolHandler.register_tool(GetLinesTool, name="get_lines")
@@ -1,15 +1,17 @@
1
1
  import os
2
2
  import pathspec
3
+ from janito.agent.tools.utils import expand_path
3
4
 
4
5
  _spec = None
5
6
 
6
7
 
7
8
  def load_gitignore_patterns(gitignore_path='.gitignore'):
8
9
  global _spec
10
+ gitignore_path = expand_path(gitignore_path)
9
11
  if not os.path.exists(gitignore_path):
10
12
  _spec = pathspec.PathSpec.from_lines('gitwildmatch', [])
11
13
  return _spec
12
- with open(gitignore_path, 'r') as f:
14
+ with open(gitignore_path, 'r', encoding='utf-8') as f:
13
15
  lines = f.readlines()
14
16
  _spec = pathspec.PathSpec.from_lines('gitwildmatch', lines)
15
17
  return _spec
@@ -17,6 +19,7 @@ def load_gitignore_patterns(gitignore_path='.gitignore'):
17
19
 
18
20
  def is_ignored(path):
19
21
  global _spec
22
+ path = expand_path(path)
20
23
  if _spec is None:
21
24
  _spec = load_gitignore_patterns()
22
25
  # Normalize path to be relative and use forward slashes
@@ -0,0 +1,23 @@
1
+ from janito.agent.tools.tool_base import ToolBase
2
+ from janito.agent.tool_handler import ToolHandler
3
+ from janito.agent.tools.rich_utils import print_info, print_success, print_error
4
+ from typing import Optional
5
+ import py_compile
6
+
7
+ class PyCompileTool(ToolBase):
8
+ """Validate a Python file by compiling it with py_compile."""
9
+ def call(self, file_path: str, doraise: Optional[bool] = True) -> str:
10
+ print_info(f"[py_compile] Compiling Python file: {file_path}", end="")
11
+ self.update_progress(f"Compiling Python file: {file_path}")
12
+ try:
13
+ py_compile.compile(file_path, doraise=doraise)
14
+ print_success(f"[py_compile] Compiled successfully: {file_path}")
15
+ return f"Compiled successfully: {file_path}"
16
+ except py_compile.PyCompileError as e:
17
+ print_error(f"[py_compile] Compile error: {e}")
18
+ return f"Compile error: {e}"
19
+ except Exception as e:
20
+ print_error(f"[py_compile] Error: {e}")
21
+ return f"Error: {e}"
22
+
23
+ ToolHandler.register_tool(PyCompileTool, name="py_compile_file")