aru-code 0.5.0__tar.gz → 0.6.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.5.0 → aru_code-0.6.0}/PKG-INFO +9 -2
- {aru_code-0.5.0 → aru_code-0.6.0}/README.md +8 -1
- aru_code-0.6.0/aru/__init__.py +1 -0
- aru_code-0.6.0/aru/agent_factory.py +66 -0
- aru_code-0.6.0/aru/cli.py +568 -0
- aru_code-0.6.0/aru/commands.py +102 -0
- aru_code-0.6.0/aru/completers.py +300 -0
- {aru_code-0.5.0 → aru_code-0.6.0}/aru/config.py +8 -0
- aru_code-0.6.0/aru/display.py +334 -0
- aru_code-0.6.0/aru/runner.py +448 -0
- aru_code-0.6.0/aru/session.py +475 -0
- {aru_code-0.5.0 → aru_code-0.6.0}/aru_code.egg-info/PKG-INFO +9 -2
- {aru_code-0.5.0 → aru_code-0.6.0}/aru_code.egg-info/SOURCES.txt +6 -0
- {aru_code-0.5.0 → aru_code-0.6.0}/pyproject.toml +1 -1
- {aru_code-0.5.0 → aru_code-0.6.0}/tests/test_cli_advanced.py +3 -3
- {aru_code-0.5.0 → aru_code-0.6.0}/tests/test_cli_shell.py +17 -17
- {aru_code-0.5.0 → aru_code-0.6.0}/tests/test_config.py +24 -7
- aru_code-0.5.0/aru/__init__.py +0 -1
- aru_code-0.5.0/aru/cli.py +0 -2270
- {aru_code-0.5.0 → aru_code-0.6.0}/LICENSE +0 -0
- {aru_code-0.5.0 → aru_code-0.6.0}/aru/agents/__init__.py +0 -0
- {aru_code-0.5.0 → aru_code-0.6.0}/aru/agents/base.py +0 -0
- {aru_code-0.5.0 → aru_code-0.6.0}/aru/agents/executor.py +0 -0
- {aru_code-0.5.0 → aru_code-0.6.0}/aru/agents/planner.py +0 -0
- {aru_code-0.5.0 → aru_code-0.6.0}/aru/context.py +0 -0
- {aru_code-0.5.0 → aru_code-0.6.0}/aru/permissions.py +0 -0
- {aru_code-0.5.0 → aru_code-0.6.0}/aru/providers.py +0 -0
- {aru_code-0.5.0 → aru_code-0.6.0}/aru/tools/__init__.py +0 -0
- {aru_code-0.5.0 → aru_code-0.6.0}/aru/tools/ast_tools.py +0 -0
- {aru_code-0.5.0 → aru_code-0.6.0}/aru/tools/codebase.py +0 -0
- {aru_code-0.5.0 → aru_code-0.6.0}/aru/tools/gitignore.py +0 -0
- {aru_code-0.5.0 → aru_code-0.6.0}/aru/tools/mcp_client.py +0 -0
- {aru_code-0.5.0 → aru_code-0.6.0}/aru/tools/ranker.py +0 -0
- {aru_code-0.5.0 → aru_code-0.6.0}/aru/tools/tasklist.py +0 -0
- {aru_code-0.5.0 → aru_code-0.6.0}/aru_code.egg-info/dependency_links.txt +0 -0
- {aru_code-0.5.0 → aru_code-0.6.0}/aru_code.egg-info/entry_points.txt +0 -0
- {aru_code-0.5.0 → aru_code-0.6.0}/aru_code.egg-info/requires.txt +0 -0
- {aru_code-0.5.0 → aru_code-0.6.0}/aru_code.egg-info/top_level.txt +0 -0
- {aru_code-0.5.0 → aru_code-0.6.0}/setup.cfg +0 -0
- {aru_code-0.5.0 → aru_code-0.6.0}/tests/test_agents_base.py +0 -0
- {aru_code-0.5.0 → aru_code-0.6.0}/tests/test_ast_tools.py +0 -0
- {aru_code-0.5.0 → aru_code-0.6.0}/tests/test_cli.py +0 -0
- {aru_code-0.5.0 → aru_code-0.6.0}/tests/test_cli_base.py +0 -0
- {aru_code-0.5.0 → aru_code-0.6.0}/tests/test_cli_completers.py +0 -0
- {aru_code-0.5.0 → aru_code-0.6.0}/tests/test_cli_new.py +0 -0
- {aru_code-0.5.0 → aru_code-0.6.0}/tests/test_cli_run_cli.py +0 -0
- {aru_code-0.5.0 → aru_code-0.6.0}/tests/test_cli_session.py +0 -0
- {aru_code-0.5.0 → aru_code-0.6.0}/tests/test_codebase.py +0 -0
- {aru_code-0.5.0 → aru_code-0.6.0}/tests/test_context.py +0 -0
- {aru_code-0.5.0 → aru_code-0.6.0}/tests/test_executor.py +0 -0
- {aru_code-0.5.0 → aru_code-0.6.0}/tests/test_gitignore.py +0 -0
- {aru_code-0.5.0 → aru_code-0.6.0}/tests/test_main.py +0 -0
- {aru_code-0.5.0 → aru_code-0.6.0}/tests/test_mcp_client.py +0 -0
- {aru_code-0.5.0 → aru_code-0.6.0}/tests/test_permissions.py +0 -0
- {aru_code-0.5.0 → aru_code-0.6.0}/tests/test_planner.py +0 -0
- {aru_code-0.5.0 → aru_code-0.6.0}/tests/test_providers.py +0 -0
- {aru_code-0.5.0 → aru_code-0.6.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.6.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
|
|
@@ -452,7 +452,14 @@ Aru can load tools from MCP servers. Configure in `.aru/mcp_config.json`:
|
|
|
452
452
|
```
|
|
453
453
|
aru-code/
|
|
454
454
|
├── aru/
|
|
455
|
-
│ ├── cli.py #
|
|
455
|
+
│ ├── cli.py # Main REPL loop, argument parsing, and entry point
|
|
456
|
+
│ ├── agent_factory.py # Agent instantiation (general and custom agents)
|
|
457
|
+
│ ├── commands.py # Slash commands, help display, shell execution
|
|
458
|
+
│ ├── completers.py # Input completions, paste detection, @file mentions
|
|
459
|
+
│ ├── context.py # Token optimization (pruning, truncation, compaction)
|
|
460
|
+
│ ├── display.py # Terminal display (logo, status bar, streaming output)
|
|
461
|
+
│ ├── runner.py # Agent execution orchestration with streaming
|
|
462
|
+
│ ├── session.py # Session state, persistence, plan tracking
|
|
456
463
|
│ ├── config.py # Configuration loader (AGENTS.md, .agents/)
|
|
457
464
|
│ ├── providers.py # Multi-provider LLM abstraction
|
|
458
465
|
│ ├── permissions.py # Granular permission system (allow/ask/deny)
|
|
@@ -405,7 +405,14 @@ Aru can load tools from MCP servers. Configure in `.aru/mcp_config.json`:
|
|
|
405
405
|
```
|
|
406
406
|
aru-code/
|
|
407
407
|
├── aru/
|
|
408
|
-
│ ├── cli.py #
|
|
408
|
+
│ ├── cli.py # Main REPL loop, argument parsing, and entry point
|
|
409
|
+
│ ├── agent_factory.py # Agent instantiation (general and custom agents)
|
|
410
|
+
│ ├── commands.py # Slash commands, help display, shell execution
|
|
411
|
+
│ ├── completers.py # Input completions, paste detection, @file mentions
|
|
412
|
+
│ ├── context.py # Token optimization (pruning, truncation, compaction)
|
|
413
|
+
│ ├── display.py # Terminal display (logo, status bar, streaming output)
|
|
414
|
+
│ ├── runner.py # Agent execution orchestration with streaming
|
|
415
|
+
│ ├── session.py # Session state, persistence, plan tracking
|
|
409
416
|
│ ├── config.py # Configuration loader (AGENTS.md, .agents/)
|
|
410
417
|
│ ├── providers.py # Multi-provider LLM abstraction
|
|
411
418
|
│ ├── permissions.py # Granular permission system (allow/ask/deny)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.6.0"
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Agent creation: general-purpose and custom agent instantiation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from aru.agents.base import build_instructions as _build_instructions
|
|
6
|
+
from aru.config import AgentConfig, CustomAgent
|
|
7
|
+
from aru.providers import create_model
|
|
8
|
+
from aru.session import Session
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def create_general_agent(session: Session, config: AgentConfig | None = None):
|
|
12
|
+
"""Create the general-purpose agent."""
|
|
13
|
+
from agno.agent import Agent
|
|
14
|
+
from agno.compression.manager import CompressionManager
|
|
15
|
+
|
|
16
|
+
from aru.tools.codebase import GENERAL_TOOLS, _get_small_model_ref
|
|
17
|
+
|
|
18
|
+
extra = config.get_extra_instructions() if config else ""
|
|
19
|
+
|
|
20
|
+
return Agent(
|
|
21
|
+
name="Aru",
|
|
22
|
+
model=create_model(session.model_ref, max_tokens=8192),
|
|
23
|
+
tools=GENERAL_TOOLS,
|
|
24
|
+
instructions=_build_instructions("general", extra),
|
|
25
|
+
markdown=True,
|
|
26
|
+
compress_tool_results=True,
|
|
27
|
+
compression_manager=CompressionManager(
|
|
28
|
+
model=create_model(_get_small_model_ref(), max_tokens=1024),
|
|
29
|
+
compress_tool_results=True,
|
|
30
|
+
compress_tool_results_limit=7,
|
|
31
|
+
),
|
|
32
|
+
tool_call_limit=20,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def create_custom_agent_instance(agent_def: CustomAgent, session: Session,
|
|
37
|
+
config: AgentConfig | None = None):
|
|
38
|
+
"""Create an Agno Agent from a CustomAgent definition."""
|
|
39
|
+
from agno.agent import Agent
|
|
40
|
+
from agno.compression.manager import CompressionManager
|
|
41
|
+
from aru.agents.base import BASE_INSTRUCTIONS
|
|
42
|
+
from aru.tools.codebase import resolve_tools, _get_small_model_ref
|
|
43
|
+
|
|
44
|
+
model_ref = agent_def.model or session.model_ref
|
|
45
|
+
tools = resolve_tools(agent_def.tools)
|
|
46
|
+
|
|
47
|
+
extra = config.get_extra_instructions() if config else ""
|
|
48
|
+
parts = [agent_def.system_prompt, BASE_INSTRUCTIONS]
|
|
49
|
+
if extra:
|
|
50
|
+
parts.append(extra)
|
|
51
|
+
instructions = "\n\n".join(parts)
|
|
52
|
+
|
|
53
|
+
return Agent(
|
|
54
|
+
name=agent_def.name,
|
|
55
|
+
model=create_model(model_ref, max_tokens=8192),
|
|
56
|
+
tools=tools,
|
|
57
|
+
instructions=instructions,
|
|
58
|
+
markdown=True,
|
|
59
|
+
compress_tool_results=True,
|
|
60
|
+
compression_manager=CompressionManager(
|
|
61
|
+
model=create_model(_get_small_model_ref(), max_tokens=1024),
|
|
62
|
+
compress_tool_results=True,
|
|
63
|
+
compress_tool_results_limit=7,
|
|
64
|
+
),
|
|
65
|
+
tool_call_limit=agent_def.max_turns or 20,
|
|
66
|
+
)
|
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
"""Interactive CLI for aru - a Claude Code clone.
|
|
2
|
+
|
|
3
|
+
This module is the slim orchestrator: REPL loop, arg parsing, and entrypoint.
|
|
4
|
+
All domain logic lives in dedicated modules; public names are re-exported here
|
|
5
|
+
for backward compatibility.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import io as _io
|
|
12
|
+
import logging as _logging
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
from rich.markdown import Markdown
|
|
17
|
+
|
|
18
|
+
# ── Re-exports for backward compatibility ─────────────────────────────
|
|
19
|
+
# Tests and external code import these from aru.cli; keep them accessible.
|
|
20
|
+
|
|
21
|
+
from aru.session import ( # noqa: F401
|
|
22
|
+
DEFAULT_MODEL,
|
|
23
|
+
PlanStep,
|
|
24
|
+
Session,
|
|
25
|
+
SessionStore,
|
|
26
|
+
SESSIONS_DIR,
|
|
27
|
+
_generate_session_id,
|
|
28
|
+
parse_plan_steps,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
from aru.display import ( # noqa: F401
|
|
32
|
+
StatusBar,
|
|
33
|
+
StreamingDisplay,
|
|
34
|
+
ToolTracker,
|
|
35
|
+
THINKING_PHRASES,
|
|
36
|
+
TOOL_DISPLAY_NAMES,
|
|
37
|
+
TOOL_PRIMARY_ARG,
|
|
38
|
+
_build_logo_with_shadow,
|
|
39
|
+
_format_tool_label,
|
|
40
|
+
_render_home,
|
|
41
|
+
_render_input_separator,
|
|
42
|
+
_sanitize_input,
|
|
43
|
+
aru_logo,
|
|
44
|
+
console,
|
|
45
|
+
format_duration,
|
|
46
|
+
neon_green,
|
|
47
|
+
shadow_green,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
from aru.completers import ( # noqa: F401
|
|
51
|
+
AruCompleter,
|
|
52
|
+
FileMentionCompleter,
|
|
53
|
+
PasteState,
|
|
54
|
+
SlashCommandCompleter,
|
|
55
|
+
TIPS,
|
|
56
|
+
_MENTION_RE,
|
|
57
|
+
_create_prompt_session,
|
|
58
|
+
_extract_agent_mention,
|
|
59
|
+
_resolve_mentions,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
from aru.commands import ( # noqa: F401
|
|
63
|
+
SLASH_COMMANDS,
|
|
64
|
+
_show_help,
|
|
65
|
+
ask_yes_no,
|
|
66
|
+
run_shell,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
from aru.runner import ( # noqa: F401
|
|
70
|
+
AgentRunResult,
|
|
71
|
+
_MUTATION_TOOLS,
|
|
72
|
+
_build_file_context,
|
|
73
|
+
_extract_plan_file_paths,
|
|
74
|
+
execute_plan_steps,
|
|
75
|
+
run_agent_capture,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
from aru.agent_factory import ( # noqa: F401
|
|
79
|
+
create_custom_agent_instance,
|
|
80
|
+
create_general_agent,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# ── Platform setup ─────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
if sys.platform == "win32" and not hasattr(sys, "_called_from_test"):
|
|
86
|
+
sys.stdout = _io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
|
|
87
|
+
sys.stderr = _io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8")
|
|
88
|
+
|
|
89
|
+
_logging.getLogger("agno").setLevel(_logging.WARNING)
|
|
90
|
+
|
|
91
|
+
# ── Imports used only in this module ───────────────────────────────────
|
|
92
|
+
|
|
93
|
+
from aru.agents.planner import create_planner, review_plan
|
|
94
|
+
from aru.config import load_config, render_command_template, render_skill_template
|
|
95
|
+
from aru.permissions import get_skip_permissions
|
|
96
|
+
from aru.providers import (
|
|
97
|
+
MODEL_ALIASES,
|
|
98
|
+
list_providers,
|
|
99
|
+
resolve_model_ref,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# ── Main REPL ──────────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
async def run_cli(skip_permissions: bool = False, resume_id: str | None = None):
|
|
106
|
+
"""Main REPL loop."""
|
|
107
|
+
from aru.tools.codebase import set_model_id, set_small_model_ref, set_on_file_mutation
|
|
108
|
+
from aru.permissions import (
|
|
109
|
+
set_config as set_perm_config,
|
|
110
|
+
set_skip_permissions,
|
|
111
|
+
set_console as perm_set_console,
|
|
112
|
+
reset_session as perm_reset_session,
|
|
113
|
+
parse_permission_config,
|
|
114
|
+
)
|
|
115
|
+
from aru.tools.codebase import set_console
|
|
116
|
+
set_console(console)
|
|
117
|
+
perm_set_console(console)
|
|
118
|
+
set_skip_permissions(skip_permissions)
|
|
119
|
+
|
|
120
|
+
store = SessionStore()
|
|
121
|
+
|
|
122
|
+
def _sync_model(sess: Session):
|
|
123
|
+
"""Sync the model IDs to the tools module from the session's model_ref."""
|
|
124
|
+
set_model_id(sess.model_id)
|
|
125
|
+
small_ref = config.model_aliases.get("small") if config else None
|
|
126
|
+
if not small_ref:
|
|
127
|
+
provider_key, _ = resolve_model_ref(sess.model_ref)
|
|
128
|
+
_small_defaults = {
|
|
129
|
+
"anthropic": "anthropic/claude-haiku-4-5",
|
|
130
|
+
"openai": "openai/gpt-4o-mini",
|
|
131
|
+
"groq": "groq/llama-3.1-8b-instant",
|
|
132
|
+
"deepseek": "deepseek/deepseek-chat",
|
|
133
|
+
"ollama": "ollama/llama3.1",
|
|
134
|
+
}
|
|
135
|
+
small_ref = _small_defaults.get(provider_key, sess.model_ref)
|
|
136
|
+
set_small_model_ref(small_ref)
|
|
137
|
+
|
|
138
|
+
# Load project configuration
|
|
139
|
+
config = load_config()
|
|
140
|
+
if config.agents_md:
|
|
141
|
+
console.print("[dim]Loaded AGENTS.md[/dim]")
|
|
142
|
+
if config.commands:
|
|
143
|
+
console.print(f"[dim]Loaded {len(config.commands)} custom command(s): {', '.join(f'/{k}' for k in config.commands)}[/dim]")
|
|
144
|
+
if config.skills:
|
|
145
|
+
console.print(f"[dim]Loaded {len(config.skills)} skill(s): {', '.join(config.skills.keys())}[/dim]")
|
|
146
|
+
if config.custom_agents:
|
|
147
|
+
primary = [k for k, v in config.custom_agents.items() if v.mode == "primary"]
|
|
148
|
+
subagents = [k for k, v in config.custom_agents.items() if v.mode == "subagent"]
|
|
149
|
+
parts = []
|
|
150
|
+
if primary:
|
|
151
|
+
parts.append(", ".join(f"/{k}" for k in primary))
|
|
152
|
+
if subagents:
|
|
153
|
+
parts.append(f"{len(subagents)} subagent(s)")
|
|
154
|
+
console.print(f"[dim]Loaded {len(config.custom_agents)} custom agent(s): {', '.join(parts)}[/dim]")
|
|
155
|
+
from aru.tools.codebase import set_custom_agents
|
|
156
|
+
set_custom_agents(config.custom_agents)
|
|
157
|
+
if config.permissions:
|
|
158
|
+
perm_config = parse_permission_config(config.permissions)
|
|
159
|
+
set_perm_config(perm_config)
|
|
160
|
+
console.print("[dim]Loaded permission config[/dim]")
|
|
161
|
+
|
|
162
|
+
extra_instructions = config.get_extra_instructions()
|
|
163
|
+
|
|
164
|
+
# Resume or create session
|
|
165
|
+
if resume_id:
|
|
166
|
+
if resume_id == "last":
|
|
167
|
+
session = store.load_last()
|
|
168
|
+
else:
|
|
169
|
+
session = store.load(resume_id)
|
|
170
|
+
if session is None:
|
|
171
|
+
console.print(f"[red]Session not found: {resume_id}[/red]")
|
|
172
|
+
return
|
|
173
|
+
console.print(Markdown(f"# aru - Resuming session `{session.session_id}`"))
|
|
174
|
+
console.print(f"[dim]Title: {session.title}[/dim]")
|
|
175
|
+
console.print(f"[dim]Messages: {len(session.history)} | Created: {session.created_at}[/dim]")
|
|
176
|
+
if session.history:
|
|
177
|
+
console.print(f"[green]Session loaded — {len(session.history)} messages restored.[/green]")
|
|
178
|
+
if session.current_plan:
|
|
179
|
+
console.print(f"[dim]Active plan: {session.plan_task}[/dim]")
|
|
180
|
+
if session.plan_steps:
|
|
181
|
+
completed = sum(1 for s in session.plan_steps if s.status == "completed")
|
|
182
|
+
console.print(f"[dim]Steps: {completed}/{len(session.plan_steps)} completed[/dim]")
|
|
183
|
+
_sync_model(session)
|
|
184
|
+
else:
|
|
185
|
+
session = Session()
|
|
186
|
+
if config.default_model:
|
|
187
|
+
session.model_ref = config.default_model
|
|
188
|
+
_sync_model(session)
|
|
189
|
+
_render_home(session, skip_permissions)
|
|
190
|
+
|
|
191
|
+
# Wire file-mutation callback
|
|
192
|
+
set_on_file_mutation(session.invalidate_context_cache)
|
|
193
|
+
|
|
194
|
+
planner = None
|
|
195
|
+
executor = None
|
|
196
|
+
paste_state = PasteState()
|
|
197
|
+
prompt_session = _create_prompt_session(paste_state, config)
|
|
198
|
+
|
|
199
|
+
# Startup: load MCP tools
|
|
200
|
+
from aru.tools.codebase import load_mcp_tools
|
|
201
|
+
await load_mcp_tools()
|
|
202
|
+
|
|
203
|
+
while True:
|
|
204
|
+
try:
|
|
205
|
+
paste_state.clear()
|
|
206
|
+
_render_input_separator()
|
|
207
|
+
model_tb = session.model_display
|
|
208
|
+
from prompt_toolkit.formatted_text import HTML
|
|
209
|
+
user_text = (
|
|
210
|
+
await asyncio.to_thread(
|
|
211
|
+
prompt_session.prompt,
|
|
212
|
+
HTML('<b><ansigreen>❯</ansigreen></b> '),
|
|
213
|
+
multiline=False,
|
|
214
|
+
bottom_toolbar=HTML(
|
|
215
|
+
f' <style fg="ansigray">{model_tb}</style>'
|
|
216
|
+
f' <style fg="ansigray">│</style>'
|
|
217
|
+
f' <style fg="ansigray">/help</style>'
|
|
218
|
+
f' <style fg="ansigray">│</style>'
|
|
219
|
+
f' <style fg="ansigray">Esc+Enter newline</style>'
|
|
220
|
+
),
|
|
221
|
+
)
|
|
222
|
+
).strip()
|
|
223
|
+
_render_input_separator()
|
|
224
|
+
except (EOFError, KeyboardInterrupt, asyncio.CancelledError):
|
|
225
|
+
store.save(session)
|
|
226
|
+
console.print(f"\n[dim]Session saved: {session.session_id}[/dim]")
|
|
227
|
+
console.print(f"[dim]Resume with:[/dim] [bold cyan]aru --resume {session.session_id}[/bold cyan]")
|
|
228
|
+
console.print("[dim]Bye![/dim]")
|
|
229
|
+
from aru.tools.mcp_client import cleanup_mcp
|
|
230
|
+
await cleanup_mcp()
|
|
231
|
+
break
|
|
232
|
+
|
|
233
|
+
user_input = _sanitize_input(paste_state.build_message(user_text))
|
|
234
|
+
|
|
235
|
+
# Resolve @file mentions (skip known agent names)
|
|
236
|
+
_agent_names = set(config.custom_agents.keys()) if config.custom_agents else set()
|
|
237
|
+
resolved, injected = _resolve_mentions(user_input, os.getcwd(), _agent_names)
|
|
238
|
+
if resolved != user_input:
|
|
239
|
+
console.print(f"[dim]Attached {injected} file(s) from @ mentions[/dim]")
|
|
240
|
+
user_input = resolved
|
|
241
|
+
|
|
242
|
+
if paste_state.pasted_content and user_text:
|
|
243
|
+
console.print(
|
|
244
|
+
f"[dim] {paste_state.line_count} lines pasted[/dim] [cyan]{user_text}[/cyan]"
|
|
245
|
+
)
|
|
246
|
+
elif paste_state.pasted_content:
|
|
247
|
+
console.print(
|
|
248
|
+
f"[dim] {paste_state.line_count} lines pasted[/dim]"
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
if not user_input:
|
|
252
|
+
continue
|
|
253
|
+
|
|
254
|
+
# Reset "allow all" approvals for each new user message
|
|
255
|
+
perm_reset_session()
|
|
256
|
+
|
|
257
|
+
if user_input.lower() in ("/quit", "/exit", "quit", "exit"):
|
|
258
|
+
store.save(session)
|
|
259
|
+
console.print(f"\n[dim]Session saved: {session.session_id}[/dim]")
|
|
260
|
+
console.print(f"[dim]Resume with:[/dim] [bold cyan]aru --resume {session.session_id}[/bold cyan]")
|
|
261
|
+
console.print("[dim]Bye![/dim]")
|
|
262
|
+
from aru.tools.mcp_client import cleanup_mcp
|
|
263
|
+
await cleanup_mcp()
|
|
264
|
+
break
|
|
265
|
+
|
|
266
|
+
if user_input == "/model" or user_input.startswith("/model "):
|
|
267
|
+
arg = user_input[6:].strip()
|
|
268
|
+
if not arg:
|
|
269
|
+
console.print(f"[bold]Current model:[/bold] {session.model_display} ({session.model_id})")
|
|
270
|
+
console.print()
|
|
271
|
+
if config.model_aliases:
|
|
272
|
+
console.print("[bold]Model aliases (aru.json):[/bold]")
|
|
273
|
+
for alias, ref in config.model_aliases.items():
|
|
274
|
+
console.print(f" [cyan]{alias}[/cyan] → {ref}")
|
|
275
|
+
console.print()
|
|
276
|
+
console.print("[bold]Aliases:[/bold]")
|
|
277
|
+
for alias, ref in MODEL_ALIASES.items():
|
|
278
|
+
console.print(f" [cyan]{alias}[/cyan] → {ref}")
|
|
279
|
+
console.print()
|
|
280
|
+
console.print("[bold]Providers:[/bold]")
|
|
281
|
+
for pkey, pconfig in list_providers().items():
|
|
282
|
+
dflt = pconfig.default_model or "—"
|
|
283
|
+
console.print(f" [cyan]{pkey}[/cyan] ({pconfig.name}) — default: {dflt}")
|
|
284
|
+
console.print()
|
|
285
|
+
console.print("[dim]Usage: /model <provider/model> (e.g., /model ollama/llama3.1, /model openai/gpt-4o)[/dim]")
|
|
286
|
+
else:
|
|
287
|
+
arg_lower = arg.lower()
|
|
288
|
+
try:
|
|
289
|
+
resolved_ref = config.model_aliases.get(arg_lower, arg_lower) if config.model_aliases else arg_lower
|
|
290
|
+
provider_key, model_name = resolve_model_ref(resolved_ref)
|
|
291
|
+
from aru.providers import get_provider
|
|
292
|
+
provider = get_provider(provider_key)
|
|
293
|
+
if provider is None:
|
|
294
|
+
available = ", ".join(sorted(list_providers().keys()))
|
|
295
|
+
console.print(f"[yellow]Unknown provider '{provider_key}'. Available: {available}[/yellow]")
|
|
296
|
+
else:
|
|
297
|
+
session.model_ref = resolved_ref if "/" in resolved_ref else (
|
|
298
|
+
MODEL_ALIASES.get(resolved_ref, resolved_ref)
|
|
299
|
+
)
|
|
300
|
+
_sync_model(session)
|
|
301
|
+
planner = None
|
|
302
|
+
executor = None
|
|
303
|
+
console.print(f"[bold green]Switched to {session.model_display}[/bold green] ({session.model_id})")
|
|
304
|
+
except Exception as e:
|
|
305
|
+
console.print(f"[yellow]Error: {e}[/yellow]")
|
|
306
|
+
continue
|
|
307
|
+
|
|
308
|
+
if user_input.lower() in ("/sessions", "/list"):
|
|
309
|
+
sessions = store.list_sessions()
|
|
310
|
+
if not sessions:
|
|
311
|
+
console.print("[dim]No saved sessions.[/dim]")
|
|
312
|
+
else:
|
|
313
|
+
console.print("[bold]Recent sessions:[/bold]\n")
|
|
314
|
+
for s in sessions:
|
|
315
|
+
sid = s["session_id"]
|
|
316
|
+
title = s["title"][:50]
|
|
317
|
+
msgs = s["messages"]
|
|
318
|
+
updated = s["updated_at"]
|
|
319
|
+
model = s["model"]
|
|
320
|
+
is_current = " [green](current)[/green]" if sid == session.session_id else ""
|
|
321
|
+
console.print(f" [bold cyan]{sid}[/bold cyan] {title} [dim]({msgs} msgs, {model}, {updated})[/dim]{is_current}")
|
|
322
|
+
console.print(f"\n[dim]Resume with: aru --resume <id>[/dim]")
|
|
323
|
+
continue
|
|
324
|
+
|
|
325
|
+
if user_input.lower() == "/commands":
|
|
326
|
+
if not config.commands:
|
|
327
|
+
console.print("[dim]No custom commands found. Add .md files to .agents/commands/[/dim]")
|
|
328
|
+
else:
|
|
329
|
+
console.print("[bold]Custom commands:[/bold]\n")
|
|
330
|
+
for name, cmd_def in config.commands.items():
|
|
331
|
+
console.print(f" [bold cyan]/{name}[/bold cyan] [dim]{cmd_def.description}[/dim]")
|
|
332
|
+
console.print(f"\n[dim]Source: .agents/commands/[/dim]")
|
|
333
|
+
continue
|
|
334
|
+
|
|
335
|
+
if user_input.lower() == "/skills":
|
|
336
|
+
if not config.skills:
|
|
337
|
+
console.print("[dim]No skills found. Create skills/<name>/SKILL.md in .agents/ or .claude/[/dim]")
|
|
338
|
+
else:
|
|
339
|
+
console.print("[bold]Available skills:[/bold]\n")
|
|
340
|
+
for name, skill in config.skills.items():
|
|
341
|
+
invocable = "" if skill.user_invocable else " [dim](model-only)[/dim]"
|
|
342
|
+
hint = f" [dim]{skill.argument_hint}[/dim]" if skill.argument_hint else ""
|
|
343
|
+
console.print(f" [bold cyan]/{name}[/bold cyan]{hint} {skill.description}{invocable}")
|
|
344
|
+
console.print(f"\n[dim]Invoke with: /skill-name <arguments>[/dim]")
|
|
345
|
+
continue
|
|
346
|
+
|
|
347
|
+
if user_input.lower() == "/agents":
|
|
348
|
+
if not config.custom_agents:
|
|
349
|
+
console.print("[dim]No custom agents found. Add .md files to .agents/agents/[/dim]")
|
|
350
|
+
else:
|
|
351
|
+
console.print("[bold]Custom agents:[/bold]\n")
|
|
352
|
+
for name, agent_def in config.custom_agents.items():
|
|
353
|
+
mode_tag = " [dim](subagent)[/dim]" if agent_def.mode == "subagent" else ""
|
|
354
|
+
model_tag = f" [dim]({agent_def.model})[/dim]" if agent_def.model else ""
|
|
355
|
+
console.print(f" [bold cyan]/{name}[/bold cyan] {agent_def.description}{mode_tag}{model_tag}")
|
|
356
|
+
console.print(f"\n[dim]Source: .agents/agents/*.md[/dim]")
|
|
357
|
+
continue
|
|
358
|
+
|
|
359
|
+
if user_input.lower() == "/mcp":
|
|
360
|
+
from aru.tools.codebase import ALL_TOOLS
|
|
361
|
+
from agno.tools import Function
|
|
362
|
+
mcp_tools = [t for t in ALL_TOOLS if isinstance(t, Function) and getattr(t, "name", "").count("__") > 0]
|
|
363
|
+
if not mcp_tools:
|
|
364
|
+
console.print("[dim]No MCP tools loaded. Check aru.mcp.json config.[/dim]")
|
|
365
|
+
else:
|
|
366
|
+
console.print(f"[bold]Loaded MCP Tools ({len(mcp_tools)}):[/bold]\n")
|
|
367
|
+
for t in mcp_tools:
|
|
368
|
+
console.print(f" [bold cyan]{t.name}[/bold cyan] [dim]{t.description}[/dim]")
|
|
369
|
+
continue
|
|
370
|
+
|
|
371
|
+
if user_input.lower() == "/help":
|
|
372
|
+
_show_help(config)
|
|
373
|
+
continue
|
|
374
|
+
|
|
375
|
+
if user_input.startswith("! "):
|
|
376
|
+
cmd = user_input[2:].strip()
|
|
377
|
+
if not cmd:
|
|
378
|
+
console.print("[yellow]Usage: ! <command>[/yellow]")
|
|
379
|
+
continue
|
|
380
|
+
run_shell(cmd)
|
|
381
|
+
|
|
382
|
+
elif user_input.startswith("/plan "):
|
|
383
|
+
task = user_input[6:].strip()
|
|
384
|
+
if not task:
|
|
385
|
+
console.print("[yellow]Usage: /plan <task description>[/yellow]")
|
|
386
|
+
continue
|
|
387
|
+
|
|
388
|
+
console.print("[bold magenta]Planning...[/bold magenta]")
|
|
389
|
+
if planner is None:
|
|
390
|
+
planner = create_planner(session.model_ref, extra_instructions)
|
|
391
|
+
|
|
392
|
+
prompt = task
|
|
393
|
+
|
|
394
|
+
plan_result = await run_agent_capture(planner, prompt, session, lightweight=True)
|
|
395
|
+
plan_content = plan_result.content
|
|
396
|
+
|
|
397
|
+
if plan_content and config and config.plan_reviewer:
|
|
398
|
+
console.print("[dim]Reviewing scope...[/dim]")
|
|
399
|
+
reviewed = await review_plan(task, plan_content)
|
|
400
|
+
if reviewed != plan_content:
|
|
401
|
+
plan_content = reviewed
|
|
402
|
+
console.print(Markdown(plan_content))
|
|
403
|
+
|
|
404
|
+
if plan_content:
|
|
405
|
+
session.set_plan(task, plan_content)
|
|
406
|
+
session.add_message("user", f"/plan {task}")
|
|
407
|
+
session.add_message("assistant", f"[Plan]\n{plan_content}")
|
|
408
|
+
|
|
409
|
+
if session.plan_steps:
|
|
410
|
+
console.print(f"\n[bold]{len(session.plan_steps)} steps detected.[/bold]")
|
|
411
|
+
|
|
412
|
+
if get_skip_permissions() or ask_yes_no("Execute this plan?"):
|
|
413
|
+
console.print("[bold green]Executing plan...[/bold green]")
|
|
414
|
+
|
|
415
|
+
from aru.agents.executor import create_executor
|
|
416
|
+
light_instructions = config.get_extra_instructions(lightweight=True) if config else ""
|
|
417
|
+
|
|
418
|
+
def make_executor():
|
|
419
|
+
return create_executor(session.model_ref, light_instructions)
|
|
420
|
+
|
|
421
|
+
result = await execute_plan_steps(session, make_executor)
|
|
422
|
+
if result:
|
|
423
|
+
session.add_message("assistant", f"[Execution]\n{result}")
|
|
424
|
+
|
|
425
|
+
session.clear_plan()
|
|
426
|
+
|
|
427
|
+
elif user_input.startswith("/") and not user_input.startswith("//"):
|
|
428
|
+
parts = user_input[1:].split(None, 1)
|
|
429
|
+
cmd_name = parts[0].lower()
|
|
430
|
+
cmd_args = parts[1] if len(parts) > 1 else ""
|
|
431
|
+
|
|
432
|
+
if cmd_name in config.commands:
|
|
433
|
+
cmd_def = config.commands[cmd_name]
|
|
434
|
+
prompt = render_command_template(cmd_def.template, cmd_args)
|
|
435
|
+
console.print(f"[bold magenta]Running /{cmd_name}...[/bold magenta]")
|
|
436
|
+
|
|
437
|
+
agent = create_general_agent(session, config)
|
|
438
|
+
session.add_message("user", user_input)
|
|
439
|
+
run_result = await run_agent_capture(agent, prompt, session)
|
|
440
|
+
if run_result.content:
|
|
441
|
+
session.add_message("assistant", run_result.with_tools_summary())
|
|
442
|
+
elif cmd_name in config.skills:
|
|
443
|
+
skill = config.skills[cmd_name]
|
|
444
|
+
if not skill.user_invocable:
|
|
445
|
+
console.print(f"[yellow]Skill '{cmd_name}' is not user-invocable[/yellow]")
|
|
446
|
+
else:
|
|
447
|
+
prompt = render_skill_template(skill.content, cmd_args)
|
|
448
|
+
console.print(f"[bold magenta]Running skill /{cmd_name}...[/bold magenta]")
|
|
449
|
+
|
|
450
|
+
agent = create_general_agent(session, config)
|
|
451
|
+
session.add_message("user", user_input)
|
|
452
|
+
run_result = await run_agent_capture(agent, prompt, session)
|
|
453
|
+
if run_result.content:
|
|
454
|
+
session.add_message("assistant", run_result.with_tools_summary())
|
|
455
|
+
elif cmd_name in config.custom_agents:
|
|
456
|
+
agent_def = config.custom_agents[cmd_name]
|
|
457
|
+
if agent_def.mode == "subagent":
|
|
458
|
+
console.print(f"[yellow]Agent '{cmd_name}' is a subagent — invoke via delegate_task only[/yellow]")
|
|
459
|
+
else:
|
|
460
|
+
from aru.permissions import permission_scope
|
|
461
|
+
console.print(f"[bold magenta]Running agent /{cmd_name}...[/bold magenta]")
|
|
462
|
+
agent = create_custom_agent_instance(agent_def, session, config)
|
|
463
|
+
session.add_message("user", user_input)
|
|
464
|
+
with permission_scope(agent_def.permission):
|
|
465
|
+
run_result = await run_agent_capture(agent, cmd_args or user_input, session)
|
|
466
|
+
if run_result.content:
|
|
467
|
+
session.add_message("assistant", run_result.with_tools_summary())
|
|
468
|
+
else:
|
|
469
|
+
console.print(f"[yellow]Unknown command: /{cmd_name}[/yellow]")
|
|
470
|
+
console.print(f"[dim]Built-in: /plan, /model, /sessions, /commands, /skills, /agents, /quit[/dim]")
|
|
471
|
+
if config.commands:
|
|
472
|
+
console.print(f"[dim]Custom: {', '.join(f'/{k}' for k in config.commands)}[/dim]")
|
|
473
|
+
if config.skills:
|
|
474
|
+
invocable = [k for k, v in config.skills.items() if v.user_invocable]
|
|
475
|
+
if invocable:
|
|
476
|
+
console.print(f"[dim]Skills: {', '.join(f'/{k}' for k in invocable)}[/dim]")
|
|
477
|
+
if config.custom_agents:
|
|
478
|
+
primary = [k for k, v in config.custom_agents.items() if v.mode == "primary"]
|
|
479
|
+
if primary:
|
|
480
|
+
console.print(f"[dim]Agents: {', '.join(f'/{k}' for k in primary)}[/dim]")
|
|
481
|
+
|
|
482
|
+
else:
|
|
483
|
+
# Check for @agent mention anywhere in message
|
|
484
|
+
agent_mention = _extract_agent_mention(user_input, config.custom_agents)
|
|
485
|
+
if agent_mention:
|
|
486
|
+
agent_name, message_text = agent_mention
|
|
487
|
+
agent_def = config.custom_agents[agent_name]
|
|
488
|
+
from aru.permissions import permission_scope
|
|
489
|
+
console.print(f"[bold magenta]Routing to @{agent_name}...[/bold magenta]")
|
|
490
|
+
agent = create_custom_agent_instance(agent_def, session, config)
|
|
491
|
+
session.add_message("user", user_input)
|
|
492
|
+
with permission_scope(agent_def.permission):
|
|
493
|
+
run_result = await run_agent_capture(agent, message_text, session)
|
|
494
|
+
if run_result.content:
|
|
495
|
+
session.add_message("assistant", run_result.with_tools_summary())
|
|
496
|
+
else:
|
|
497
|
+
agent = create_general_agent(session, config)
|
|
498
|
+
session.add_message("user", user_input)
|
|
499
|
+
run_result = await run_agent_capture(agent, user_input, session)
|
|
500
|
+
if run_result.content:
|
|
501
|
+
session.add_message("assistant", run_result.with_tools_summary())
|
|
502
|
+
|
|
503
|
+
# Show token usage and auto-save
|
|
504
|
+
if session.token_summary:
|
|
505
|
+
console.print(f"[dim]{session.token_summary}[/dim]")
|
|
506
|
+
store.save(session)
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
# ── CLI entrypoint ─────────────────────────────────────────────────────
|
|
510
|
+
|
|
511
|
+
def _list_sessions_and_exit():
|
|
512
|
+
"""Print saved sessions and exit."""
|
|
513
|
+
store = SessionStore()
|
|
514
|
+
sessions = store.list_sessions()
|
|
515
|
+
if not sessions:
|
|
516
|
+
console.print("[dim]No saved sessions.[/dim]")
|
|
517
|
+
return
|
|
518
|
+
console.print("[bold]Recent sessions:[/bold]\n")
|
|
519
|
+
for s in sessions:
|
|
520
|
+
sid = s["session_id"]
|
|
521
|
+
title = s["title"][:50]
|
|
522
|
+
msgs = s["messages"]
|
|
523
|
+
updated = s["updated_at"]
|
|
524
|
+
model = s["model"]
|
|
525
|
+
console.print(f" [bold cyan]{sid}[/bold cyan] {title} [dim]({msgs} msgs, {model}, {updated})[/dim]")
|
|
526
|
+
console.print(f"\n[dim]Resume with: aru --resume <id>[/dim]")
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def main():
|
|
530
|
+
"""Entry point for the aru CLI."""
|
|
531
|
+
from dotenv import load_dotenv
|
|
532
|
+
|
|
533
|
+
load_dotenv()
|
|
534
|
+
args = sys.argv[1:]
|
|
535
|
+
skip_permissions = "--dangerously-skip-permissions" in args
|
|
536
|
+
|
|
537
|
+
if "--list" in args:
|
|
538
|
+
_list_sessions_and_exit()
|
|
539
|
+
return
|
|
540
|
+
|
|
541
|
+
resume_id = None
|
|
542
|
+
if "--resume" in args:
|
|
543
|
+
idx = args.index("--resume")
|
|
544
|
+
if idx + 1 < len(args) and not args[idx + 1].startswith("--"):
|
|
545
|
+
resume_id = args[idx + 1]
|
|
546
|
+
else:
|
|
547
|
+
resume_id = "last"
|
|
548
|
+
|
|
549
|
+
try:
|
|
550
|
+
asyncio.run(run_cli(skip_permissions=skip_permissions, resume_id=resume_id))
|
|
551
|
+
except (KeyboardInterrupt, asyncio.CancelledError, SystemExit):
|
|
552
|
+
_graceful_exit()
|
|
553
|
+
except Exception as e:
|
|
554
|
+
from rich.markup import escape
|
|
555
|
+
console.print(f"\n[bold red]Fatal error: {escape(str(e))}[/bold red]")
|
|
556
|
+
_graceful_exit()
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def _graceful_exit():
|
|
560
|
+
"""Save session and show resume hint on exit."""
|
|
561
|
+
try:
|
|
562
|
+
store = SessionStore()
|
|
563
|
+
last = store.load_last()
|
|
564
|
+
if last:
|
|
565
|
+
console.print(f"\n[dim]Session saved: {last.session_id}[/dim]")
|
|
566
|
+
console.print(f"[dim]Resume with:[/dim] [bold cyan]aru --resume {last.session_id}[/bold cyan]")
|
|
567
|
+
except Exception:
|
|
568
|
+
pass
|