aru-code 0.10.0__tar.gz → 0.11.0__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.
- {aru_code-0.10.0/aru_code.egg-info → aru_code-0.11.0}/PKG-INFO +33 -4
- {aru_code-0.10.0 → aru_code-0.11.0}/README.md +32 -3
- aru_code-0.11.0/aru/__init__.py +1 -0
- {aru_code-0.10.0 → aru_code-0.11.0}/aru/cli.py +14 -8
- {aru_code-0.10.0 → aru_code-0.11.0}/aru/completers.py +29 -8
- {aru_code-0.10.0 → aru_code-0.11.0}/aru/runner.py +8 -3
- {aru_code-0.10.0 → aru_code-0.11.0}/aru/tools/codebase.py +17 -4
- {aru_code-0.10.0 → aru_code-0.11.0/aru_code.egg-info}/PKG-INFO +33 -4
- {aru_code-0.10.0 → aru_code-0.11.0}/pyproject.toml +1 -1
- {aru_code-0.10.0 → aru_code-0.11.0}/tests/test_cli.py +5 -5
- {aru_code-0.10.0 → aru_code-0.11.0}/tests/test_cli_completers.py +97 -1
- aru_code-0.10.0/aru/__init__.py +0 -1
- {aru_code-0.10.0 → aru_code-0.11.0}/LICENSE +0 -0
- {aru_code-0.10.0 → aru_code-0.11.0}/aru/agent_factory.py +0 -0
- {aru_code-0.10.0 → aru_code-0.11.0}/aru/agents/__init__.py +0 -0
- {aru_code-0.10.0 → aru_code-0.11.0}/aru/agents/base.py +0 -0
- {aru_code-0.10.0 → aru_code-0.11.0}/aru/agents/executor.py +0 -0
- {aru_code-0.10.0 → aru_code-0.11.0}/aru/agents/planner.py +0 -0
- {aru_code-0.10.0 → aru_code-0.11.0}/aru/commands.py +0 -0
- {aru_code-0.10.0 → aru_code-0.11.0}/aru/config.py +0 -0
- {aru_code-0.10.0 → aru_code-0.11.0}/aru/context.py +0 -0
- {aru_code-0.10.0 → aru_code-0.11.0}/aru/display.py +0 -0
- {aru_code-0.10.0 → aru_code-0.11.0}/aru/permissions.py +0 -0
- {aru_code-0.10.0 → aru_code-0.11.0}/aru/providers.py +0 -0
- {aru_code-0.10.0 → aru_code-0.11.0}/aru/runtime.py +0 -0
- {aru_code-0.10.0 → aru_code-0.11.0}/aru/session.py +0 -0
- {aru_code-0.10.0 → aru_code-0.11.0}/aru/tools/__init__.py +0 -0
- {aru_code-0.10.0 → aru_code-0.11.0}/aru/tools/ast_tools.py +0 -0
- {aru_code-0.10.0 → aru_code-0.11.0}/aru/tools/gitignore.py +0 -0
- {aru_code-0.10.0 → aru_code-0.11.0}/aru/tools/mcp_client.py +0 -0
- {aru_code-0.10.0 → aru_code-0.11.0}/aru/tools/ranker.py +0 -0
- {aru_code-0.10.0 → aru_code-0.11.0}/aru/tools/tasklist.py +0 -0
- {aru_code-0.10.0 → aru_code-0.11.0}/aru_code.egg-info/SOURCES.txt +0 -0
- {aru_code-0.10.0 → aru_code-0.11.0}/aru_code.egg-info/dependency_links.txt +0 -0
- {aru_code-0.10.0 → aru_code-0.11.0}/aru_code.egg-info/entry_points.txt +0 -0
- {aru_code-0.10.0 → aru_code-0.11.0}/aru_code.egg-info/requires.txt +0 -0
- {aru_code-0.10.0 → aru_code-0.11.0}/aru_code.egg-info/top_level.txt +0 -0
- {aru_code-0.10.0 → aru_code-0.11.0}/setup.cfg +0 -0
- {aru_code-0.10.0 → aru_code-0.11.0}/tests/test_agents_base.py +0 -0
- {aru_code-0.10.0 → aru_code-0.11.0}/tests/test_ast_tools.py +0 -0
- {aru_code-0.10.0 → aru_code-0.11.0}/tests/test_cli_advanced.py +0 -0
- {aru_code-0.10.0 → aru_code-0.11.0}/tests/test_cli_base.py +0 -0
- {aru_code-0.10.0 → aru_code-0.11.0}/tests/test_cli_new.py +0 -0
- {aru_code-0.10.0 → aru_code-0.11.0}/tests/test_cli_run_cli.py +0 -0
- {aru_code-0.10.0 → aru_code-0.11.0}/tests/test_cli_session.py +0 -0
- {aru_code-0.10.0 → aru_code-0.11.0}/tests/test_cli_shell.py +0 -0
- {aru_code-0.10.0 → aru_code-0.11.0}/tests/test_codebase.py +0 -0
- {aru_code-0.10.0 → aru_code-0.11.0}/tests/test_config.py +0 -0
- {aru_code-0.10.0 → aru_code-0.11.0}/tests/test_context.py +0 -0
- {aru_code-0.10.0 → aru_code-0.11.0}/tests/test_executor.py +0 -0
- {aru_code-0.10.0 → aru_code-0.11.0}/tests/test_gitignore.py +0 -0
- {aru_code-0.10.0 → aru_code-0.11.0}/tests/test_main.py +0 -0
- {aru_code-0.10.0 → aru_code-0.11.0}/tests/test_mcp_client.py +0 -0
- {aru_code-0.10.0 → aru_code-0.11.0}/tests/test_permissions.py +0 -0
- {aru_code-0.10.0 → aru_code-0.11.0}/tests/test_planner.py +0 -0
- {aru_code-0.10.0 → aru_code-0.11.0}/tests/test_providers.py +0 -0
- {aru_code-0.10.0 → aru_code-0.11.0}/tests/test_ranker.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aru-code
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.0
|
|
4
4
|
Summary: A Claude Code clone built with Agno agents
|
|
5
5
|
Author-email: Estevao <estevaofon@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -55,6 +55,7 @@ An intelligent coding assistant for the terminal, powered by LLMs and [Agno](htt
|
|
|
55
55
|
|
|
56
56
|
- **Multi-Agent Architecture** — Specialized agents for planning, execution, and conversation
|
|
57
57
|
- **Interactive CLI** — Streaming responses, multi-line paste, session management
|
|
58
|
+
- **Image Support** — Attach images via `@` mentions for multimodal analysis (Claude, GPT-4o, Gemini)
|
|
58
59
|
- **16 Integrated Tools** — File operations, code search, shell, web search, task delegation
|
|
59
60
|
- **Task Planning** — Break down complex tasks into steps with automatic execution
|
|
60
61
|
- **Multi-Provider** — Anthropic, OpenAI, Ollama, Groq, OpenRouter, DeepSeek, and others via custom configuration
|
|
@@ -69,7 +70,7 @@ An intelligent coding assistant for the terminal, powered by LLMs and [Agno](htt
|
|
|
69
70
|
pip install aru-code
|
|
70
71
|
```
|
|
71
72
|
|
|
72
|
-
> **Requirements:** Python 3.
|
|
73
|
+
> **Requirements:** Python 3.11+
|
|
73
74
|
|
|
74
75
|
### 2. Configure the API Key
|
|
75
76
|
|
|
@@ -131,6 +132,23 @@ aru> ! pytest tests/ -v
|
|
|
131
132
|
aru> /model ollama/codellama
|
|
132
133
|
```
|
|
133
134
|
|
|
135
|
+
### Image Support
|
|
136
|
+
|
|
137
|
+
Attach images to your messages using the same `@` mention syntax used for files. Aru detects image files by extension and sends them to the LLM as visual content for multimodal analysis.
|
|
138
|
+
|
|
139
|
+
**Supported formats:** `.png`, `.jpg`, `.jpeg`, `.gif`, `.webp`, `.bmp`
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
aru> describe @screenshot.png
|
|
143
|
+
aru> compare @before.png and @after.png
|
|
144
|
+
aru> review @code.py and explain the diagram in @architecture.png
|
|
145
|
+
aru> analyze @D:/full/path/to/image.jpg
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Images are sent natively to the model via the provider's multimodal API — no base64 text is injected into the conversation. Works with any multimodal model (Claude Opus/Sonnet, GPT-4o, Gemini, etc.). The autocomplete shows an `[image]` label for image files.
|
|
149
|
+
|
|
150
|
+
> **Note:** Images require a multimodal model. Local models via Ollama may not support image input. Maximum file size: 20MB.
|
|
151
|
+
|
|
134
152
|
## Configuration
|
|
135
153
|
|
|
136
154
|
### Models and Providers
|
|
@@ -374,11 +392,22 @@ performance, and readability. Do NOT modify files.
|
|
|
374
392
|
|
|
375
393
|
#### Invocation
|
|
376
394
|
|
|
395
|
+
There are three ways to invoke a custom agent:
|
|
396
|
+
|
|
397
|
+
| Method | Syntax | When to use |
|
|
398
|
+
|--------|--------|-------------|
|
|
399
|
+
| **Slash command** | `/reviewer src/auth.py` | Directly invoke a `primary` agent by name |
|
|
400
|
+
| **@mention** | `@reviewer check this function` | Mention an agent anywhere in your message |
|
|
401
|
+
| **delegate_task** | Automatic (subagents only) | Subagent names and descriptions are injected into the `delegate_task` tool description, so the LLM sees them and can call `delegate_task(task="...", agent="name")` on its own when it judges the task fits |
|
|
402
|
+
|
|
377
403
|
```
|
|
378
|
-
aru> /reviewer src/auth.py
|
|
379
|
-
aru>
|
|
404
|
+
aru> /reviewer src/auth.py # slash command (primary agents)
|
|
405
|
+
aru> @reviewer check the auth module # @mention (primary or subagent)
|
|
406
|
+
aru> /agents # list all custom agents
|
|
380
407
|
```
|
|
381
408
|
|
|
409
|
+
> **Note:** Slash commands (`/name`) are only available for `primary` agents — subagents are blocked with a warning. `@mention` works for any agent regardless of mode. Subagents can be invoked in two ways: automatically by the LLM via `delegate_task`, or manually by the user via `@name`.
|
|
410
|
+
|
|
382
411
|
#### Discovery paths
|
|
383
412
|
|
|
384
413
|
Agents are discovered from multiple locations (later overrides earlier):
|
|
@@ -8,6 +8,7 @@ An intelligent coding assistant for the terminal, powered by LLMs and [Agno](htt
|
|
|
8
8
|
|
|
9
9
|
- **Multi-Agent Architecture** — Specialized agents for planning, execution, and conversation
|
|
10
10
|
- **Interactive CLI** — Streaming responses, multi-line paste, session management
|
|
11
|
+
- **Image Support** — Attach images via `@` mentions for multimodal analysis (Claude, GPT-4o, Gemini)
|
|
11
12
|
- **16 Integrated Tools** — File operations, code search, shell, web search, task delegation
|
|
12
13
|
- **Task Planning** — Break down complex tasks into steps with automatic execution
|
|
13
14
|
- **Multi-Provider** — Anthropic, OpenAI, Ollama, Groq, OpenRouter, DeepSeek, and others via custom configuration
|
|
@@ -22,7 +23,7 @@ An intelligent coding assistant for the terminal, powered by LLMs and [Agno](htt
|
|
|
22
23
|
pip install aru-code
|
|
23
24
|
```
|
|
24
25
|
|
|
25
|
-
> **Requirements:** Python 3.
|
|
26
|
+
> **Requirements:** Python 3.11+
|
|
26
27
|
|
|
27
28
|
### 2. Configure the API Key
|
|
28
29
|
|
|
@@ -84,6 +85,23 @@ aru> ! pytest tests/ -v
|
|
|
84
85
|
aru> /model ollama/codellama
|
|
85
86
|
```
|
|
86
87
|
|
|
88
|
+
### Image Support
|
|
89
|
+
|
|
90
|
+
Attach images to your messages using the same `@` mention syntax used for files. Aru detects image files by extension and sends them to the LLM as visual content for multimodal analysis.
|
|
91
|
+
|
|
92
|
+
**Supported formats:** `.png`, `.jpg`, `.jpeg`, `.gif`, `.webp`, `.bmp`
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
aru> describe @screenshot.png
|
|
96
|
+
aru> compare @before.png and @after.png
|
|
97
|
+
aru> review @code.py and explain the diagram in @architecture.png
|
|
98
|
+
aru> analyze @D:/full/path/to/image.jpg
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Images are sent natively to the model via the provider's multimodal API — no base64 text is injected into the conversation. Works with any multimodal model (Claude Opus/Sonnet, GPT-4o, Gemini, etc.). The autocomplete shows an `[image]` label for image files.
|
|
102
|
+
|
|
103
|
+
> **Note:** Images require a multimodal model. Local models via Ollama may not support image input. Maximum file size: 20MB.
|
|
104
|
+
|
|
87
105
|
## Configuration
|
|
88
106
|
|
|
89
107
|
### Models and Providers
|
|
@@ -327,11 +345,22 @@ performance, and readability. Do NOT modify files.
|
|
|
327
345
|
|
|
328
346
|
#### Invocation
|
|
329
347
|
|
|
348
|
+
There are three ways to invoke a custom agent:
|
|
349
|
+
|
|
350
|
+
| Method | Syntax | When to use |
|
|
351
|
+
|--------|--------|-------------|
|
|
352
|
+
| **Slash command** | `/reviewer src/auth.py` | Directly invoke a `primary` agent by name |
|
|
353
|
+
| **@mention** | `@reviewer check this function` | Mention an agent anywhere in your message |
|
|
354
|
+
| **delegate_task** | Automatic (subagents only) | Subagent names and descriptions are injected into the `delegate_task` tool description, so the LLM sees them and can call `delegate_task(task="...", agent="name")` on its own when it judges the task fits |
|
|
355
|
+
|
|
330
356
|
```
|
|
331
|
-
aru> /reviewer src/auth.py
|
|
332
|
-
aru>
|
|
357
|
+
aru> /reviewer src/auth.py # slash command (primary agents)
|
|
358
|
+
aru> @reviewer check the auth module # @mention (primary or subagent)
|
|
359
|
+
aru> /agents # list all custom agents
|
|
333
360
|
```
|
|
334
361
|
|
|
362
|
+
> **Note:** Slash commands (`/name`) are only available for `primary` agents — subagents are blocked with a warning. `@mention` works for any agent regardless of mode. Subagents can be invoked in two ways: automatically by the LLM via `delegate_task`, or manually by the user via `@name`.
|
|
363
|
+
|
|
335
364
|
#### Discovery paths
|
|
336
365
|
|
|
337
366
|
Agents are discovered from multiple locations (later overrides earlier):
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.11.0"
|
|
@@ -230,9 +230,15 @@ async def run_cli(skip_permissions: bool = False, resume_id: str | None = None):
|
|
|
230
230
|
|
|
231
231
|
# Resolve @file mentions (skip known agent names)
|
|
232
232
|
_agent_names = set(config.custom_agents.keys()) if config.custom_agents else set()
|
|
233
|
-
resolved, injected = _resolve_mentions(user_input, os.getcwd(), _agent_names)
|
|
234
|
-
if
|
|
235
|
-
|
|
233
|
+
resolved, injected, attached_images = _resolve_mentions(user_input, os.getcwd(), _agent_names)
|
|
234
|
+
if injected > 0:
|
|
235
|
+
parts = []
|
|
236
|
+
text_count = injected - len(attached_images)
|
|
237
|
+
if text_count > 0:
|
|
238
|
+
parts.append(f"{text_count} file(s)")
|
|
239
|
+
if attached_images:
|
|
240
|
+
parts.append(f"{len(attached_images)} image(s)")
|
|
241
|
+
console.print(f"[dim]Attached {', '.join(parts)} from @ mentions[/dim]")
|
|
236
242
|
user_input = resolved
|
|
237
243
|
|
|
238
244
|
if paste_state.pasted_content and user_text:
|
|
@@ -441,7 +447,7 @@ async def run_cli(skip_permissions: bool = False, resume_id: str | None = None):
|
|
|
441
447
|
else:
|
|
442
448
|
agent = create_general_agent(session, config)
|
|
443
449
|
session.add_message("user", user_input)
|
|
444
|
-
run_result = await run_agent_capture(agent, prompt, session)
|
|
450
|
+
run_result = await run_agent_capture(agent, prompt, session, images=attached_images or None)
|
|
445
451
|
if run_result.content:
|
|
446
452
|
session.add_message("assistant", run_result.with_tools_summary())
|
|
447
453
|
elif cmd_name in config.skills:
|
|
@@ -454,7 +460,7 @@ async def run_cli(skip_permissions: bool = False, resume_id: str | None = None):
|
|
|
454
460
|
|
|
455
461
|
agent = create_general_agent(session, config)
|
|
456
462
|
session.add_message("user", user_input)
|
|
457
|
-
run_result = await run_agent_capture(agent, prompt, session)
|
|
463
|
+
run_result = await run_agent_capture(agent, prompt, session, images=attached_images or None)
|
|
458
464
|
if run_result.content:
|
|
459
465
|
session.add_message("assistant", run_result.with_tools_summary())
|
|
460
466
|
elif cmd_name in config.custom_agents:
|
|
@@ -467,7 +473,7 @@ async def run_cli(skip_permissions: bool = False, resume_id: str | None = None):
|
|
|
467
473
|
agent = create_custom_agent_instance(agent_def, session, config)
|
|
468
474
|
session.add_message("user", user_input)
|
|
469
475
|
with permission_scope(agent_def.permission):
|
|
470
|
-
run_result = await run_agent_capture(agent, cmd_args or user_input, session)
|
|
476
|
+
run_result = await run_agent_capture(agent, cmd_args or user_input, session, images=attached_images or None)
|
|
471
477
|
if run_result.content:
|
|
472
478
|
session.add_message("assistant", run_result.with_tools_summary())
|
|
473
479
|
else:
|
|
@@ -495,13 +501,13 @@ async def run_cli(skip_permissions: bool = False, resume_id: str | None = None):
|
|
|
495
501
|
agent = create_custom_agent_instance(agent_def, session, config)
|
|
496
502
|
session.add_message("user", user_input)
|
|
497
503
|
with permission_scope(agent_def.permission):
|
|
498
|
-
run_result = await run_agent_capture(agent, message_text, session)
|
|
504
|
+
run_result = await run_agent_capture(agent, message_text, session, images=attached_images or None)
|
|
499
505
|
if run_result.content:
|
|
500
506
|
session.add_message("assistant", run_result.with_tools_summary())
|
|
501
507
|
else:
|
|
502
508
|
agent = create_general_agent(session, config)
|
|
503
509
|
session.add_message("user", user_input)
|
|
504
|
-
run_result = await run_agent_capture(agent, user_input, session)
|
|
510
|
+
run_result = await run_agent_capture(agent, user_input, session, images=attached_images or None)
|
|
505
511
|
if run_result.content:
|
|
506
512
|
session.add_message("assistant", run_result.with_tools_summary())
|
|
507
513
|
|
|
@@ -12,25 +12,31 @@ from prompt_toolkit.formatted_text import HTML
|
|
|
12
12
|
from prompt_toolkit.key_binding import KeyBindings
|
|
13
13
|
from prompt_toolkit.keys import Keys
|
|
14
14
|
|
|
15
|
+
from agno.media import Image
|
|
16
|
+
|
|
15
17
|
from aru.commands import SLASH_COMMANDS
|
|
16
18
|
from aru.config import AgentConfig
|
|
17
19
|
|
|
18
|
-
_MENTION_RE = re.compile(r'(?<!\S)@([a-zA-Z0-9_
|
|
20
|
+
_MENTION_RE = re.compile(r'(?<!\S)@([a-zA-Z0-9_./\\:-]+)')
|
|
19
21
|
_MENTION_MAX_SIZE = 30_000 # bytes, same limit as read_file
|
|
22
|
+
_IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp"}
|
|
23
|
+
_IMAGE_MAX_SIZE = 20 * 1024 * 1024 # 20MB
|
|
20
24
|
|
|
21
25
|
|
|
22
|
-
def _resolve_mentions(text: str, cwd: str, agent_names: set[str] | None = None) -> tuple[str, int]:
|
|
26
|
+
def _resolve_mentions(text: str, cwd: str, agent_names: set[str] | None = None) -> tuple[str, int, list[Image]]:
|
|
23
27
|
"""Resolve @file mentions by appending file contents to the message.
|
|
24
28
|
|
|
29
|
+
Image files (png, jpg, etc.) are returned as Image objects instead of text.
|
|
25
30
|
Skips @mentions that match known agent names.
|
|
26
|
-
Returns (resolved_text, number_of_files_attached).
|
|
31
|
+
Returns (resolved_text, number_of_files_attached, images).
|
|
27
32
|
"""
|
|
28
33
|
agent_names = agent_names or set()
|
|
29
34
|
matches = list(_MENTION_RE.finditer(text))
|
|
30
35
|
if not matches:
|
|
31
|
-
return text, 0
|
|
36
|
+
return text, 0, []
|
|
32
37
|
|
|
33
38
|
appendix_parts = []
|
|
39
|
+
images: list[Image] = []
|
|
34
40
|
seen = set()
|
|
35
41
|
for m in matches:
|
|
36
42
|
rel_path = m.group(1)
|
|
@@ -39,9 +45,21 @@ def _resolve_mentions(text: str, cwd: str, agent_names: set[str] | None = None)
|
|
|
39
45
|
if rel_path in seen:
|
|
40
46
|
continue
|
|
41
47
|
seen.add(rel_path)
|
|
42
|
-
abs_path = os.path.join(cwd, rel_path)
|
|
48
|
+
abs_path = rel_path if os.path.isabs(rel_path) else os.path.join(cwd, rel_path)
|
|
43
49
|
if not os.path.isfile(abs_path):
|
|
44
50
|
continue
|
|
51
|
+
|
|
52
|
+
ext = os.path.splitext(rel_path)[1].lower()
|
|
53
|
+
if ext in _IMAGE_EXTENSIONS:
|
|
54
|
+
try:
|
|
55
|
+
size = os.path.getsize(abs_path)
|
|
56
|
+
if size > _IMAGE_MAX_SIZE:
|
|
57
|
+
continue
|
|
58
|
+
images.append(Image(filepath=abs_path, id=rel_path))
|
|
59
|
+
except OSError:
|
|
60
|
+
pass
|
|
61
|
+
continue
|
|
62
|
+
|
|
45
63
|
try:
|
|
46
64
|
size = os.path.getsize(abs_path)
|
|
47
65
|
with open(abs_path, "r", encoding="utf-8", errors="replace") as f:
|
|
@@ -57,9 +75,10 @@ def _resolve_mentions(text: str, cwd: str, agent_names: set[str] | None = None)
|
|
|
57
75
|
except OSError:
|
|
58
76
|
continue
|
|
59
77
|
|
|
78
|
+
attached = len(appendix_parts) + len(images)
|
|
60
79
|
if appendix_parts:
|
|
61
|
-
return text + "".join(appendix_parts),
|
|
62
|
-
return text,
|
|
80
|
+
return text + "".join(appendix_parts), attached, images
|
|
81
|
+
return text, attached, images
|
|
63
82
|
|
|
64
83
|
|
|
65
84
|
def _extract_agent_mention(
|
|
@@ -206,7 +225,9 @@ class FileMentionCompleter(Completer):
|
|
|
206
225
|
|
|
207
226
|
is_dir = os.path.isdir(full_path)
|
|
208
227
|
display_text = rel_prefix + entry + ("/" if is_dir else "")
|
|
209
|
-
|
|
228
|
+
file_ext = os.path.splitext(entry)[1].lower()
|
|
229
|
+
is_image = not is_dir and file_ext in _IMAGE_EXTENSIONS
|
|
230
|
+
meta = "dir" if is_dir else ("image" if is_image else "")
|
|
210
231
|
|
|
211
232
|
yield Completion(
|
|
212
233
|
display_text,
|
|
@@ -43,7 +43,8 @@ class AgentRunResult:
|
|
|
43
43
|
return f"{self.content}\n\n[Tools]\n{tools_section}"
|
|
44
44
|
|
|
45
45
|
|
|
46
|
-
async def run_agent_capture(agent, message: str, session=None, lightweight: bool = False
|
|
46
|
+
async def run_agent_capture(agent, message: str, session=None, lightweight: bool = False,
|
|
47
|
+
images: list | None = None) -> AgentRunResult:
|
|
47
48
|
"""Run agent with async streaming display and parallel tool execution.
|
|
48
49
|
|
|
49
50
|
Args:
|
|
@@ -51,6 +52,7 @@ async def run_agent_capture(agent, message: str, session=None, lightweight: bool
|
|
|
51
52
|
message: The user message/prompt.
|
|
52
53
|
session: Optional session for history and context.
|
|
53
54
|
lightweight: If True, skip tree/git/plan context and history (for executor steps).
|
|
55
|
+
images: Optional list of agno.media.Image objects to attach.
|
|
54
56
|
|
|
55
57
|
Returns:
|
|
56
58
|
AgentRunResult with text output and list of tool call labels.
|
|
@@ -116,7 +118,7 @@ async def run_agent_capture(agent, message: str, session=None, lightweight: bool
|
|
|
116
118
|
|
|
117
119
|
# Combine: history messages + current enriched message
|
|
118
120
|
if history_messages:
|
|
119
|
-
history_messages.append(Message(role="user", content=run_message))
|
|
121
|
+
history_messages.append(Message(role="user", content=run_message, images=images or None))
|
|
120
122
|
agent_input = history_messages
|
|
121
123
|
else:
|
|
122
124
|
agent_input = run_message
|
|
@@ -130,7 +132,10 @@ async def run_agent_capture(agent, message: str, session=None, lightweight: bool
|
|
|
130
132
|
_stall_counter = 0
|
|
131
133
|
_stalled = False
|
|
132
134
|
_STALL_LIMIT = 20
|
|
133
|
-
|
|
135
|
+
arun_kwargs = dict(stream=True, stream_events=True, yield_run_output=True)
|
|
136
|
+
if isinstance(agent_input, str) and images:
|
|
137
|
+
arun_kwargs["images"] = images
|
|
138
|
+
async for event in agent.arun(agent_input, **arun_kwargs):
|
|
134
139
|
if isinstance(event, RunOutput):
|
|
135
140
|
run_output = event
|
|
136
141
|
break
|
|
@@ -1023,8 +1023,19 @@ async def delegate_task(task: str, context: str = "", agent: str = "") -> str:
|
|
|
1023
1023
|
|
|
1024
1024
|
agent_perm = None
|
|
1025
1025
|
custom_agent_defs = get_ctx().custom_agent_defs
|
|
1026
|
-
|
|
1027
|
-
|
|
1026
|
+
# Agno may pass the caller Agent object instead of a string — coerce to str
|
|
1027
|
+
agent_name = str(agent) if agent and isinstance(agent, str) else ""
|
|
1028
|
+
|
|
1029
|
+
# Print delegation info so the user sees what's happening
|
|
1030
|
+
from rich.console import Console
|
|
1031
|
+
_console = Console()
|
|
1032
|
+
if agent_name and agent_name in custom_agent_defs:
|
|
1033
|
+
_console.print(f"[dim] → Delegating to agent [bold]{agent_name}[/bold] (task: {task[:80]}{'...' if len(task) > 80 else ''})[/dim]")
|
|
1034
|
+
else:
|
|
1035
|
+
_console.print(f"[dim] → Delegating to sub-agent #{agent_id} (task: {task[:80]}{'...' if len(task) > 80 else ''})[/dim]")
|
|
1036
|
+
|
|
1037
|
+
if agent_name and agent_name in custom_agent_defs:
|
|
1038
|
+
agent_def = custom_agent_defs[agent_name]
|
|
1028
1039
|
agent_perm = agent_def.permission
|
|
1029
1040
|
tools = resolve_tools(agent_def.tools) if agent_def.tools else list(_SUBAGENT_TOOLS)
|
|
1030
1041
|
tools = [t for t in tools if t is not delegate_task]
|
|
@@ -1186,13 +1197,15 @@ def _update_delegate_task_docstring():
|
|
|
1186
1197
|
Args:
|
|
1187
1198
|
task: What the sub-agent should do.
|
|
1188
1199
|
context: Optional extra context (file paths, constraints).
|
|
1189
|
-
agent:
|
|
1200
|
+
agent: Name of a specialized agent to use. ALWAYS prefer a specialized agent when one matches the task."""
|
|
1190
1201
|
|
|
1191
1202
|
custom_agent_defs = get_ctx().custom_agent_defs
|
|
1192
1203
|
if custom_agent_defs:
|
|
1193
|
-
lines = [f"\n\n
|
|
1204
|
+
lines = [f"\n\n IMPORTANT: When a specialized agent matches the task, you MUST pass its name in the agent parameter."]
|
|
1205
|
+
lines.append(f" Available specialized agents:")
|
|
1194
1206
|
for name, agent_def in custom_agent_defs.items():
|
|
1195
1207
|
lines.append(f" - agent=\"{name}\": {agent_def.description}")
|
|
1208
|
+
lines.append(f"\n If no specialized agent fits, omit the agent parameter to use a generic sub-agent.")
|
|
1196
1209
|
base_doc += "\n".join(lines)
|
|
1197
1210
|
|
|
1198
1211
|
delegate_task.__doc__ = base_doc
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aru-code
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.0
|
|
4
4
|
Summary: A Claude Code clone built with Agno agents
|
|
5
5
|
Author-email: Estevao <estevaofon@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -55,6 +55,7 @@ An intelligent coding assistant for the terminal, powered by LLMs and [Agno](htt
|
|
|
55
55
|
|
|
56
56
|
- **Multi-Agent Architecture** — Specialized agents for planning, execution, and conversation
|
|
57
57
|
- **Interactive CLI** — Streaming responses, multi-line paste, session management
|
|
58
|
+
- **Image Support** — Attach images via `@` mentions for multimodal analysis (Claude, GPT-4o, Gemini)
|
|
58
59
|
- **16 Integrated Tools** — File operations, code search, shell, web search, task delegation
|
|
59
60
|
- **Task Planning** — Break down complex tasks into steps with automatic execution
|
|
60
61
|
- **Multi-Provider** — Anthropic, OpenAI, Ollama, Groq, OpenRouter, DeepSeek, and others via custom configuration
|
|
@@ -69,7 +70,7 @@ An intelligent coding assistant for the terminal, powered by LLMs and [Agno](htt
|
|
|
69
70
|
pip install aru-code
|
|
70
71
|
```
|
|
71
72
|
|
|
72
|
-
> **Requirements:** Python 3.
|
|
73
|
+
> **Requirements:** Python 3.11+
|
|
73
74
|
|
|
74
75
|
### 2. Configure the API Key
|
|
75
76
|
|
|
@@ -131,6 +132,23 @@ aru> ! pytest tests/ -v
|
|
|
131
132
|
aru> /model ollama/codellama
|
|
132
133
|
```
|
|
133
134
|
|
|
135
|
+
### Image Support
|
|
136
|
+
|
|
137
|
+
Attach images to your messages using the same `@` mention syntax used for files. Aru detects image files by extension and sends them to the LLM as visual content for multimodal analysis.
|
|
138
|
+
|
|
139
|
+
**Supported formats:** `.png`, `.jpg`, `.jpeg`, `.gif`, `.webp`, `.bmp`
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
aru> describe @screenshot.png
|
|
143
|
+
aru> compare @before.png and @after.png
|
|
144
|
+
aru> review @code.py and explain the diagram in @architecture.png
|
|
145
|
+
aru> analyze @D:/full/path/to/image.jpg
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Images are sent natively to the model via the provider's multimodal API — no base64 text is injected into the conversation. Works with any multimodal model (Claude Opus/Sonnet, GPT-4o, Gemini, etc.). The autocomplete shows an `[image]` label for image files.
|
|
149
|
+
|
|
150
|
+
> **Note:** Images require a multimodal model. Local models via Ollama may not support image input. Maximum file size: 20MB.
|
|
151
|
+
|
|
134
152
|
## Configuration
|
|
135
153
|
|
|
136
154
|
### Models and Providers
|
|
@@ -374,11 +392,22 @@ performance, and readability. Do NOT modify files.
|
|
|
374
392
|
|
|
375
393
|
#### Invocation
|
|
376
394
|
|
|
395
|
+
There are three ways to invoke a custom agent:
|
|
396
|
+
|
|
397
|
+
| Method | Syntax | When to use |
|
|
398
|
+
|--------|--------|-------------|
|
|
399
|
+
| **Slash command** | `/reviewer src/auth.py` | Directly invoke a `primary` agent by name |
|
|
400
|
+
| **@mention** | `@reviewer check this function` | Mention an agent anywhere in your message |
|
|
401
|
+
| **delegate_task** | Automatic (subagents only) | Subagent names and descriptions are injected into the `delegate_task` tool description, so the LLM sees them and can call `delegate_task(task="...", agent="name")` on its own when it judges the task fits |
|
|
402
|
+
|
|
377
403
|
```
|
|
378
|
-
aru> /reviewer src/auth.py
|
|
379
|
-
aru>
|
|
404
|
+
aru> /reviewer src/auth.py # slash command (primary agents)
|
|
405
|
+
aru> @reviewer check the auth module # @mention (primary or subagent)
|
|
406
|
+
aru> /agents # list all custom agents
|
|
380
407
|
```
|
|
381
408
|
|
|
409
|
+
> **Note:** Slash commands (`/name`) are only available for `primary` agents — subagents are blocked with a warning. `@mention` works for any agent regardless of mode. Subagents can be invoked in two ways: automatically by the LLM via `delegate_task`, or manually by the user via `@name`.
|
|
410
|
+
|
|
382
411
|
#### Discovery paths
|
|
383
412
|
|
|
384
413
|
Agents are discovered from multiple locations (later overrides earlier):
|
|
@@ -46,32 +46,32 @@ class TestSanitizeInput:
|
|
|
46
46
|
|
|
47
47
|
class TestResolveMentions:
|
|
48
48
|
def test_no_mentions(self, tmp_path):
|
|
49
|
-
result, count = _resolve_mentions("hello world", str(tmp_path))
|
|
49
|
+
result, count, _imgs = _resolve_mentions("hello world", str(tmp_path))
|
|
50
50
|
assert result == "hello world"
|
|
51
51
|
assert count == 0
|
|
52
52
|
|
|
53
53
|
def test_resolves_file_mention(self, tmp_path):
|
|
54
54
|
(tmp_path / "config.py").write_text("DEBUG = True")
|
|
55
|
-
result, count = _resolve_mentions("check @config.py", str(tmp_path))
|
|
55
|
+
result, count, _imgs = _resolve_mentions("check @config.py", str(tmp_path))
|
|
56
56
|
assert "DEBUG = True" in result
|
|
57
57
|
assert "Contents of config.py" in result
|
|
58
58
|
assert count == 1
|
|
59
59
|
|
|
60
60
|
def test_nonexistent_file_ignored(self, tmp_path):
|
|
61
|
-
result, count = _resolve_mentions("check @missing.py", str(tmp_path))
|
|
61
|
+
result, count, _imgs = _resolve_mentions("check @missing.py", str(tmp_path))
|
|
62
62
|
assert result == "check @missing.py"
|
|
63
63
|
assert count == 0
|
|
64
64
|
|
|
65
65
|
def test_deduplicates_mentions(self, tmp_path):
|
|
66
66
|
(tmp_path / "file.py").write_text("code")
|
|
67
|
-
result, count = _resolve_mentions("@file.py and @file.py", str(tmp_path))
|
|
67
|
+
result, count, _imgs = _resolve_mentions("@file.py and @file.py", str(tmp_path))
|
|
68
68
|
assert result.count("Contents of file.py") == 1
|
|
69
69
|
assert count == 1
|
|
70
70
|
|
|
71
71
|
def test_multiple_files(self, tmp_path):
|
|
72
72
|
(tmp_path / "a.py").write_text("aaa")
|
|
73
73
|
(tmp_path / "b.py").write_text("bbb")
|
|
74
|
-
result, count = _resolve_mentions("@a.py and @b.py", str(tmp_path))
|
|
74
|
+
result, count, _imgs = _resolve_mentions("@a.py and @b.py", str(tmp_path))
|
|
75
75
|
assert "Contents of a.py" in result
|
|
76
76
|
assert "Contents of b.py" in result
|
|
77
77
|
assert count == 2
|
|
@@ -7,13 +7,17 @@ from unittest.mock import Mock, patch
|
|
|
7
7
|
import pytest
|
|
8
8
|
from prompt_toolkit.document import Document
|
|
9
9
|
|
|
10
|
+
from agno.media import Image
|
|
11
|
+
|
|
10
12
|
from aru.cli import (
|
|
11
13
|
SlashCommandCompleter,
|
|
12
14
|
FileMentionCompleter,
|
|
13
15
|
AruCompleter,
|
|
14
16
|
SLASH_COMMANDS,
|
|
15
17
|
_extract_agent_mention,
|
|
18
|
+
_resolve_mentions,
|
|
16
19
|
)
|
|
20
|
+
from aru.completers import _IMAGE_EXTENSIONS
|
|
17
21
|
from aru.config import CustomAgent, CustomCommand
|
|
18
22
|
|
|
19
23
|
|
|
@@ -593,4 +597,96 @@ class TestExtractAgentMention:
|
|
|
593
597
|
def test_agent_not_matched_if_attached_to_word(self):
|
|
594
598
|
agents = self._make_agents()
|
|
595
599
|
# @reviewer preceded by non-whitespace should not match
|
|
596
|
-
assert _extract_agent_mention("email@reviewer", agents) is None
|
|
600
|
+
assert _extract_agent_mention("email@reviewer", agents) is None
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
# ── Image mention support ──────────────────────────────────────────
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
class TestImageMentions:
|
|
607
|
+
"""Tests for image file detection in @mentions."""
|
|
608
|
+
|
|
609
|
+
def test_resolve_mentions_returns_three_tuple(self, tmp_path):
|
|
610
|
+
result = _resolve_mentions("hello", str(tmp_path))
|
|
611
|
+
assert len(result) == 3
|
|
612
|
+
text, count, images = result
|
|
613
|
+
assert text == "hello"
|
|
614
|
+
assert count == 0
|
|
615
|
+
assert images == []
|
|
616
|
+
|
|
617
|
+
def test_resolve_mentions_image_file(self, tmp_path):
|
|
618
|
+
img = tmp_path / "screenshot.png"
|
|
619
|
+
img.write_bytes(b"\x89PNG\r\n\x1a\n" + b"\x00" * 100)
|
|
620
|
+
|
|
621
|
+
text, count, images = _resolve_mentions(
|
|
622
|
+
"analyze @screenshot.png", str(tmp_path)
|
|
623
|
+
)
|
|
624
|
+
assert count == 1
|
|
625
|
+
assert len(images) == 1
|
|
626
|
+
assert isinstance(images[0], Image)
|
|
627
|
+
assert images[0].id == "screenshot.png"
|
|
628
|
+
# Image content should NOT be appended as text
|
|
629
|
+
assert "```" not in text
|
|
630
|
+
|
|
631
|
+
def test_resolve_mentions_mixed_files_and_images(self, tmp_path):
|
|
632
|
+
(tmp_path / "code.py").write_text("print('hello')", encoding="utf-8")
|
|
633
|
+
(tmp_path / "diagram.jpg").write_bytes(b"\xff\xd8\xff" + b"\x00" * 100)
|
|
634
|
+
|
|
635
|
+
text, count, images = _resolve_mentions(
|
|
636
|
+
"review @code.py and @diagram.jpg", str(tmp_path)
|
|
637
|
+
)
|
|
638
|
+
assert count == 2
|
|
639
|
+
assert len(images) == 1
|
|
640
|
+
assert images[0].id == "diagram.jpg"
|
|
641
|
+
# Text file content should be appended
|
|
642
|
+
assert "print('hello')" in text
|
|
643
|
+
|
|
644
|
+
def test_resolve_mentions_multiple_images(self, tmp_path):
|
|
645
|
+
(tmp_path / "a.png").write_bytes(b"\x89PNG" + b"\x00" * 100)
|
|
646
|
+
(tmp_path / "b.webp").write_bytes(b"RIFF" + b"\x00" * 100)
|
|
647
|
+
|
|
648
|
+
text, count, images = _resolve_mentions(
|
|
649
|
+
"compare @a.png @b.webp", str(tmp_path)
|
|
650
|
+
)
|
|
651
|
+
assert count == 2
|
|
652
|
+
assert len(images) == 2
|
|
653
|
+
|
|
654
|
+
def test_resolve_mentions_image_too_large(self, tmp_path):
|
|
655
|
+
img = tmp_path / "huge.png"
|
|
656
|
+
# Write just over the 20MB limit header
|
|
657
|
+
img.write_bytes(b"\x89PNG" + b"\x00" * (20 * 1024 * 1024 + 1))
|
|
658
|
+
|
|
659
|
+
text, count, images = _resolve_mentions(
|
|
660
|
+
"analyze @huge.png", str(tmp_path)
|
|
661
|
+
)
|
|
662
|
+
assert count == 0
|
|
663
|
+
assert len(images) == 0
|
|
664
|
+
|
|
665
|
+
def test_resolve_mentions_all_image_extensions(self, tmp_path):
|
|
666
|
+
for ext in _IMAGE_EXTENSIONS:
|
|
667
|
+
fname = f"test{ext}"
|
|
668
|
+
(tmp_path / fname).write_bytes(b"\x00" * 100)
|
|
669
|
+
|
|
670
|
+
mentions = " ".join(f"@test{ext}" for ext in _IMAGE_EXTENSIONS)
|
|
671
|
+
text, count, images = _resolve_mentions(mentions, str(tmp_path))
|
|
672
|
+
assert len(images) == len(_IMAGE_EXTENSIONS)
|
|
673
|
+
|
|
674
|
+
def test_image_completer_shows_image_metadata(self, tmp_path):
|
|
675
|
+
(tmp_path / "photo.png").touch()
|
|
676
|
+
(tmp_path / "code.py").touch()
|
|
677
|
+
|
|
678
|
+
completer = FileMentionCompleter()
|
|
679
|
+
doc = Document("@")
|
|
680
|
+
with patch("os.getcwd", return_value=str(tmp_path)):
|
|
681
|
+
with patch("aru.tools.gitignore.is_ignored", return_value=False):
|
|
682
|
+
completions = list(completer.get_completions(doc, Mock()))
|
|
683
|
+
|
|
684
|
+
by_name = {c.text: c for c in completions}
|
|
685
|
+
assert "photo.png" in by_name
|
|
686
|
+
assert "code.py" in by_name
|
|
687
|
+
# Image should have "image" in metadata
|
|
688
|
+
photo_meta = str(by_name["photo.png"].display_meta)
|
|
689
|
+
assert "image" in photo_meta
|
|
690
|
+
# Code file should NOT have "image" in metadata
|
|
691
|
+
code_meta = str(by_name["code.py"].display_meta)
|
|
692
|
+
assert "image" not in code_meta
|
aru_code-0.10.0/aru/__init__.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.10.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|