moma-cli 0.0.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.
- moma_cli-0.0.1/.gitignore +7 -0
- moma_cli-0.0.1/PKG-INFO +108 -0
- moma_cli-0.0.1/README.md +83 -0
- moma_cli-0.0.1/asset/image.png +0 -0
- moma_cli-0.0.1/moma/__init__.py +4 -0
- moma_cli-0.0.1/moma/agent.py +360 -0
- moma_cli-0.0.1/moma/cli.py +607 -0
- moma_cli-0.0.1/moma/config.py +233 -0
- moma_cli-0.0.1/moma/diff_applier.py +285 -0
- moma_cli-0.0.1/moma/security.py +272 -0
- moma_cli-0.0.1/moma/tools.py +691 -0
- moma_cli-0.0.1/plan.md +52 -0
- moma_cli-0.0.1/pyproject.toml +49 -0
moma_cli-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: moma-cli
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: MOMA CLI - A terminal-based AI coding assistant
|
|
5
|
+
Author-email: MOMA CLI <dev@moma-cli.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: ai,cli,coding-assistant,llm
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Requires-Dist: litellm>=1.0.0
|
|
18
|
+
Requires-Dist: rich>=13.0.0
|
|
19
|
+
Requires-Dist: typer[all]>=0.9.0
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
|
|
22
|
+
Requires-Dist: pytest>=7.0.0; extra == 'dev'
|
|
23
|
+
Requires-Dist: ruff>=0.1.0; extra == 'dev'
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# moma-cli
|
|
27
|
+
|
|
28
|
+
A terminal‑based AI coding assistant powered by `litellm`. It lets you chat with your codebase, automatically read context, and apply changes manually.
|
|
29
|
+
|
|
30
|
+
## Overview
|
|
31
|
+
|
|
32
|
+
<img src="asset/image.png" alt="moma-cli screenshot" width="800">
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install moma-cli # or install from the repository
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Quick start
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# Start an interactive chat (default command)
|
|
44
|
+
moma
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The CLI will launch a welcome screen and you can start typing messages. Commands are prefixed with `/`:
|
|
48
|
+
|
|
49
|
+
| Command | Description |
|
|
50
|
+
|---------|-------------|
|
|
51
|
+
| `/clear` | Clear conversation history |
|
|
52
|
+
| `/model list` | List available models |
|
|
53
|
+
| `/model change` | Choose a different model |
|
|
54
|
+
| `/security` | Change security mode (smart/strict/permissive) |
|
|
55
|
+
| `/status` | Show current session status |
|
|
56
|
+
| `/config …` | View or edit configuration |
|
|
57
|
+
| `/help` | Show help panel |
|
|
58
|
+
| `/quit` or `Ctrl‑D` | Exit the program |
|
|
59
|
+
|
|
60
|
+
## Chat command
|
|
61
|
+
|
|
62
|
+
You can also invoke the chat directly with arguments:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
moma chat "Explain this function" # send an initial message
|
|
66
|
+
moma chat -m gpt-4 "Generate a unit test" # override model for this session
|
|
67
|
+
moma chat -c # clear history before starting
|
|
68
|
+
moma chat -t 0.7 -max-tokens 500 # set temperature and max tokens
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Configuration
|
|
72
|
+
|
|
73
|
+
Configuration is stored in `~/.moma-cli/config.json`. Manage it via the `config` sub‑command:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
# Show all configuration
|
|
77
|
+
moma config show
|
|
78
|
+
|
|
79
|
+
# Set a value (e.g., default model)
|
|
80
|
+
moma config set default_model ollama/llama3
|
|
81
|
+
|
|
82
|
+
# Set your API key
|
|
83
|
+
moma config set api_key sk-xxxxxxxxxxxx
|
|
84
|
+
|
|
85
|
+
# Reset system prompt to default
|
|
86
|
+
moma config reset
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Security modes
|
|
90
|
+
|
|
91
|
+
- **smart** – read‑only shell commands are auto‑executed, mutating commands require confirmation.
|
|
92
|
+
- **strict** – all shell commands require confirmation.
|
|
93
|
+
- **permissive** – all commands run without prompts.
|
|
94
|
+
|
|
95
|
+
Change the mode with:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
moma /security
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Example workflow
|
|
102
|
+
|
|
103
|
+
1. **Start chat** – `moma`
|
|
104
|
+
2. **Ask a question** – e.g., `How can I improve this function?`
|
|
105
|
+
3. **Review suggestions** – the assistant may propose a diff.
|
|
106
|
+
4. **Apply changes** – confirm the diff when prompted.
|
|
107
|
+
|
|
108
|
+
Enjoy coding with AI assistance!
|
moma_cli-0.0.1/README.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# moma-cli
|
|
2
|
+
|
|
3
|
+
A terminal‑based AI coding assistant powered by `litellm`. It lets you chat with your codebase, automatically read context, and apply changes manually.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
<img src="asset/image.png" alt="moma-cli screenshot" width="800">
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install moma-cli # or install from the repository
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# Start an interactive chat (default command)
|
|
19
|
+
moma
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The CLI will launch a welcome screen and you can start typing messages. Commands are prefixed with `/`:
|
|
23
|
+
|
|
24
|
+
| Command | Description |
|
|
25
|
+
|---------|-------------|
|
|
26
|
+
| `/clear` | Clear conversation history |
|
|
27
|
+
| `/model list` | List available models |
|
|
28
|
+
| `/model change` | Choose a different model |
|
|
29
|
+
| `/security` | Change security mode (smart/strict/permissive) |
|
|
30
|
+
| `/status` | Show current session status |
|
|
31
|
+
| `/config …` | View or edit configuration |
|
|
32
|
+
| `/help` | Show help panel |
|
|
33
|
+
| `/quit` or `Ctrl‑D` | Exit the program |
|
|
34
|
+
|
|
35
|
+
## Chat command
|
|
36
|
+
|
|
37
|
+
You can also invoke the chat directly with arguments:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
moma chat "Explain this function" # send an initial message
|
|
41
|
+
moma chat -m gpt-4 "Generate a unit test" # override model for this session
|
|
42
|
+
moma chat -c # clear history before starting
|
|
43
|
+
moma chat -t 0.7 -max-tokens 500 # set temperature and max tokens
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Configuration
|
|
47
|
+
|
|
48
|
+
Configuration is stored in `~/.moma-cli/config.json`. Manage it via the `config` sub‑command:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# Show all configuration
|
|
52
|
+
moma config show
|
|
53
|
+
|
|
54
|
+
# Set a value (e.g., default model)
|
|
55
|
+
moma config set default_model ollama/llama3
|
|
56
|
+
|
|
57
|
+
# Set your API key
|
|
58
|
+
moma config set api_key sk-xxxxxxxxxxxx
|
|
59
|
+
|
|
60
|
+
# Reset system prompt to default
|
|
61
|
+
moma config reset
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Security modes
|
|
65
|
+
|
|
66
|
+
- **smart** – read‑only shell commands are auto‑executed, mutating commands require confirmation.
|
|
67
|
+
- **strict** – all shell commands require confirmation.
|
|
68
|
+
- **permissive** – all commands run without prompts.
|
|
69
|
+
|
|
70
|
+
Change the mode with:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
moma /security
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Example workflow
|
|
77
|
+
|
|
78
|
+
1. **Start chat** – `moma`
|
|
79
|
+
2. **Ask a question** – e.g., `How can I improve this function?`
|
|
80
|
+
3. **Review suggestions** – the assistant may propose a diff.
|
|
81
|
+
4. **Apply changes** – confirm the diff when prompted.
|
|
82
|
+
|
|
83
|
+
Enjoy coding with AI assistance!
|
|
Binary file
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
"""Agent module for MOMA CLI.
|
|
2
|
+
|
|
3
|
+
Manages conversation history, system prompts, and handles the litellm
|
|
4
|
+
completion loop including tool calling/function execution.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Dict, List, Optional, Generator
|
|
10
|
+
|
|
11
|
+
import litellm
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from rich.prompt import Prompt
|
|
14
|
+
|
|
15
|
+
from moma.config import ConfigManager, get_config
|
|
16
|
+
from moma.security import SecurityPolicy, confirm_command
|
|
17
|
+
from moma.tools import TOOLS, execute_tool_call, TOOL_FUNCTIONS
|
|
18
|
+
from moma.diff_applier import propose_file_write, CodeProposal, apply_proposal
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ConversationHistory:
|
|
22
|
+
"""Manages the conversation history with the LLM."""
|
|
23
|
+
|
|
24
|
+
def __init__(self):
|
|
25
|
+
"""Initialize the conversation history."""
|
|
26
|
+
self.messages: List[Dict[str, Any]] = []
|
|
27
|
+
|
|
28
|
+
def add_system(self, prompt: str) -> None:
|
|
29
|
+
"""Add a system message."""
|
|
30
|
+
self.messages.append({"role": "system", "content": prompt})
|
|
31
|
+
|
|
32
|
+
def add_user(self, content: str) -> None:
|
|
33
|
+
"""Add a user message."""
|
|
34
|
+
self.messages.append({"role": "user", "content": content})
|
|
35
|
+
|
|
36
|
+
def add_assistant(self, content: Optional[str], tool_calls: Optional[List[Dict]] = None) -> None:
|
|
37
|
+
"""Add an assistant message."""
|
|
38
|
+
msg: Dict[str, Any] = {"role": "assistant"}
|
|
39
|
+
msg["content"] = content if content is not None else ""
|
|
40
|
+
if tool_calls:
|
|
41
|
+
msg["tool_calls"] = tool_calls
|
|
42
|
+
self.messages.append(msg)
|
|
43
|
+
|
|
44
|
+
def add_tool_result(self, tool_call_id: str, name: str, content: str, is_error: bool = False) -> None:
|
|
45
|
+
"""Add a tool result message."""
|
|
46
|
+
self.messages.append({
|
|
47
|
+
"role": "tool",
|
|
48
|
+
"tool_call_id": tool_call_id,
|
|
49
|
+
"name": name,
|
|
50
|
+
"content": content,
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
def get_messages(self) -> List[Dict[str, Any]]:
|
|
54
|
+
"""Get all messages."""
|
|
55
|
+
return self.messages.copy()
|
|
56
|
+
|
|
57
|
+
def clear(self) -> None:
|
|
58
|
+
"""Clear all messages (keeps system message if any)."""
|
|
59
|
+
system_msgs = [m for m in self.messages if m["role"] == "system"]
|
|
60
|
+
self.messages.clear()
|
|
61
|
+
for msg in system_msgs:
|
|
62
|
+
self.messages.append(msg)
|
|
63
|
+
|
|
64
|
+
def __len__(self) -> int:
|
|
65
|
+
return len(self.messages)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class Agent:
|
|
69
|
+
"""The main agent that handles LLM interaction."""
|
|
70
|
+
|
|
71
|
+
MAX_TOOL_DEPTH = 10
|
|
72
|
+
|
|
73
|
+
def __init__(self, config: Optional[ConfigManager] = None, console: Optional[Console] = None):
|
|
74
|
+
"""Initialize the agent."""
|
|
75
|
+
self.config = config or get_config()
|
|
76
|
+
self.console = console or Console()
|
|
77
|
+
self.history = ConversationHistory()
|
|
78
|
+
self.security = SecurityPolicy(mode=self.config.get_security_mode())
|
|
79
|
+
self._task_completed = False
|
|
80
|
+
self._tools_supported = True # flipped off if backend rejects tool_choice
|
|
81
|
+
self._setup()
|
|
82
|
+
|
|
83
|
+
def _setup(self) -> None:
|
|
84
|
+
"""Set up the agent with system prompt and defaults."""
|
|
85
|
+
system_prompt = self.config.get_system_prompt()
|
|
86
|
+
self.history.add_system(system_prompt)
|
|
87
|
+
|
|
88
|
+
def _get_model(self) -> str:
|
|
89
|
+
"""Get the LLM model to use, auto-prefixing with 'openai/' for the proxy."""
|
|
90
|
+
model = self.config.get("default_model", "openai/gpt-4o")
|
|
91
|
+
if "/" not in model:
|
|
92
|
+
model = f"openai/{model}"
|
|
93
|
+
return model
|
|
94
|
+
|
|
95
|
+
def _get_temperature(self) -> float:
|
|
96
|
+
"""Get the temperature setting."""
|
|
97
|
+
return self.config.get("temperature", 0.7)
|
|
98
|
+
|
|
99
|
+
def _get_max_tokens(self) -> int:
|
|
100
|
+
"""Get the max tokens setting."""
|
|
101
|
+
return self.config.get("max_tokens", 4096)
|
|
102
|
+
|
|
103
|
+
def _resolve_api_key(self) -> str:
|
|
104
|
+
"""Return the configured API key, falling back to env vars."""
|
|
105
|
+
import os
|
|
106
|
+
key = self.config.get("api_key")
|
|
107
|
+
if key:
|
|
108
|
+
return key
|
|
109
|
+
return os.environ.get("LITELLM_API_KEY") or os.environ.get("OPENAI_API_KEY") or "sk-no-key"
|
|
110
|
+
|
|
111
|
+
def _build_environment_details(self) -> str:
|
|
112
|
+
"""Build environment context with the current directory file tree."""
|
|
113
|
+
cwd = Path.cwd()
|
|
114
|
+
IGNORE = {"__pycache__", ".git", "node_modules", ".venv", "venv", "env",
|
|
115
|
+
"dist", "build", ".pytest_cache", ".mypy_cache", ".ruff_cache"}
|
|
116
|
+
try:
|
|
117
|
+
entries = []
|
|
118
|
+
for item in sorted(cwd.rglob("*")):
|
|
119
|
+
parts = item.relative_to(cwd).parts
|
|
120
|
+
if any(p in IGNORE or p.startswith(".") for p in parts):
|
|
121
|
+
continue
|
|
122
|
+
rel = str(item.relative_to(cwd))
|
|
123
|
+
entries.append(f"{rel}{'/' if item.is_dir() else ''}")
|
|
124
|
+
file_list = "\n".join(entries[:500])
|
|
125
|
+
return (
|
|
126
|
+
f"<environment_details>\n"
|
|
127
|
+
f"Current Working Directory: {cwd}\n\n"
|
|
128
|
+
f"File Tree:\n{file_list}\n"
|
|
129
|
+
f"</environment_details>"
|
|
130
|
+
)
|
|
131
|
+
except Exception:
|
|
132
|
+
return f"<environment_details>\nCurrent Working Directory: {cwd}\n</environment_details>"
|
|
133
|
+
|
|
134
|
+
def chat(self, user_message: str) -> Generator[str, None, None]:
|
|
135
|
+
"""Process a user message and yield the response.
|
|
136
|
+
|
|
137
|
+
Yields:
|
|
138
|
+
Text chunks of the response.
|
|
139
|
+
"""
|
|
140
|
+
# Inject file tree context on the first user message
|
|
141
|
+
if len(self.history.messages) <= 1:
|
|
142
|
+
env = self._build_environment_details()
|
|
143
|
+
full_message = f"{user_message}\n\n{env}"
|
|
144
|
+
else:
|
|
145
|
+
full_message = user_message
|
|
146
|
+
self.history.add_user(full_message)
|
|
147
|
+
self._task_completed = False
|
|
148
|
+
self.console.print("\n[bold blue]🤔 MOMA Assistant is thinking...[/bold blue]")
|
|
149
|
+
yield from self._run_round()
|
|
150
|
+
|
|
151
|
+
def _run_round(self, depth: int = 0) -> Generator[str, None, None]:
|
|
152
|
+
"""Make one LLM API call and handle the full response including tool calls.
|
|
153
|
+
|
|
154
|
+
Recurses up to MAX_TOOL_DEPTH times to handle chained tool use.
|
|
155
|
+
"""
|
|
156
|
+
if depth >= self.MAX_TOOL_DEPTH:
|
|
157
|
+
self.console.print("\n[yellow]⚠️ Maximum tool call depth reached.[/yellow]")
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
model = self._get_model()
|
|
161
|
+
temperature = self._get_temperature()
|
|
162
|
+
max_tokens = self._get_max_tokens()
|
|
163
|
+
api_key = self._resolve_api_key()
|
|
164
|
+
api_base = self.config.get("api_base")
|
|
165
|
+
messages = self.history.get_messages()
|
|
166
|
+
|
|
167
|
+
def _call_llm(with_tools: bool):
|
|
168
|
+
return litellm.completion(
|
|
169
|
+
model=model,
|
|
170
|
+
messages=messages,
|
|
171
|
+
temperature=temperature,
|
|
172
|
+
max_tokens=max_tokens,
|
|
173
|
+
api_key=api_key,
|
|
174
|
+
api_base=api_base,
|
|
175
|
+
stream=True,
|
|
176
|
+
tools=TOOLS if with_tools else None,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
try:
|
|
180
|
+
try:
|
|
181
|
+
response = _call_llm(self._tools_supported)
|
|
182
|
+
except litellm.BadRequestError as e:
|
|
183
|
+
if self._tools_supported and "tool choice" in str(e).lower():
|
|
184
|
+
self._tools_supported = False
|
|
185
|
+
self.console.print("[yellow]⚠️ Tool calling disabled (model doesn't support it)[/yellow]")
|
|
186
|
+
response = _call_llm(False)
|
|
187
|
+
else:
|
|
188
|
+
raise
|
|
189
|
+
|
|
190
|
+
response_content = ""
|
|
191
|
+
response_tool_calls: List[Dict] = []
|
|
192
|
+
has_tool_calls = False
|
|
193
|
+
|
|
194
|
+
for chunk in response:
|
|
195
|
+
if chunk.choices:
|
|
196
|
+
choice = chunk.choices[0]
|
|
197
|
+
if choice.delta.content:
|
|
198
|
+
response_content += choice.delta.content
|
|
199
|
+
yield choice.delta.content
|
|
200
|
+
if choice.delta.tool_calls:
|
|
201
|
+
has_tool_calls = True
|
|
202
|
+
for tc in choice.delta.tool_calls:
|
|
203
|
+
if tc.index is not None:
|
|
204
|
+
while len(response_tool_calls) <= tc.index:
|
|
205
|
+
response_tool_calls.append({"id": "", "function": {"name": "", "arguments": ""}})
|
|
206
|
+
# Capture id whenever it appears (not just on first/name chunk)
|
|
207
|
+
if tc.id:
|
|
208
|
+
response_tool_calls[tc.index]["id"] = tc.id
|
|
209
|
+
if tc.function:
|
|
210
|
+
if tc.function.name:
|
|
211
|
+
response_tool_calls[tc.index]["function"]["name"] = tc.function.name
|
|
212
|
+
if tc.function.arguments:
|
|
213
|
+
response_tool_calls[tc.index]["function"]["arguments"] += tc.function.arguments
|
|
214
|
+
|
|
215
|
+
if has_tool_calls:
|
|
216
|
+
# API protocol: assistant message with tool_calls must precede tool result messages
|
|
217
|
+
formatted_tool_calls = [
|
|
218
|
+
{"id": tc["id"], "type": "function", "function": tc["function"]}
|
|
219
|
+
for tc in response_tool_calls
|
|
220
|
+
]
|
|
221
|
+
self.history.add_assistant(response_content, tool_calls=formatted_tool_calls)
|
|
222
|
+
|
|
223
|
+
self._process_tool_calls(response_tool_calls)
|
|
224
|
+
|
|
225
|
+
if not self._task_completed:
|
|
226
|
+
self.console.print("\n[bold blue]🔄 Processing tool results...[/bold blue]")
|
|
227
|
+
yield from self._run_round(depth + 1)
|
|
228
|
+
else:
|
|
229
|
+
self.history.add_assistant(response_content)
|
|
230
|
+
|
|
231
|
+
except litellm.exceptions.AuthenticationError as e:
|
|
232
|
+
self.console.print(f"\n[bold red]❌ Authentication Error:[/bold red] {e}")
|
|
233
|
+
except litellm.exceptions.ContextWindowExceededError:
|
|
234
|
+
self.console.print("\n[bold red]❌ Context window exceeded.[/bold red] Try /clear to reset history.")
|
|
235
|
+
except Exception as e:
|
|
236
|
+
self.console.print(f"\n[bold red]❌ Error:[/bold red] {e}")
|
|
237
|
+
|
|
238
|
+
def _process_tool_calls(self, response_tool_calls: List[Dict]) -> None:
|
|
239
|
+
"""Execute tool calls and record results in history.
|
|
240
|
+
|
|
241
|
+
Sets self._task_completed = True and returns early if attempt_completion is called.
|
|
242
|
+
"""
|
|
243
|
+
for tc in response_tool_calls:
|
|
244
|
+
tool_call = {
|
|
245
|
+
"id": tc.get("id", ""),
|
|
246
|
+
"name": tc["function"]["name"],
|
|
247
|
+
"arguments": tc["function"]["arguments"],
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
try:
|
|
251
|
+
args = json.loads(tc["function"]["arguments"]) if tc["function"]["arguments"] else {}
|
|
252
|
+
except json.JSONDecodeError:
|
|
253
|
+
args = {}
|
|
254
|
+
|
|
255
|
+
tool_name = tool_call["name"]
|
|
256
|
+
tool_result = ""
|
|
257
|
+
|
|
258
|
+
if tool_name == "execute_command":
|
|
259
|
+
command = args.get("command", "")
|
|
260
|
+
requires_approval = args.get("requires_approval", True)
|
|
261
|
+
|
|
262
|
+
self.console.print(f"\n[bold]🔧 Executing command:[/bold] [cyan]{command}[/cyan]")
|
|
263
|
+
|
|
264
|
+
validation = self.security.validate_command(command)
|
|
265
|
+
|
|
266
|
+
if not validation["allowed"]:
|
|
267
|
+
# Hard block — never execute regardless of user input
|
|
268
|
+
self.console.print(f"\n[bold red]⛔ BLOCKED:[/bold red] Security policy forbids: [cyan]{command}[/cyan]")
|
|
269
|
+
tool_result = json.dumps({
|
|
270
|
+
"success": False,
|
|
271
|
+
"error": "Command is blocked by security policy",
|
|
272
|
+
"command": command,
|
|
273
|
+
})
|
|
274
|
+
elif requires_approval and validation["requires_confirmation"]:
|
|
275
|
+
confirmed = confirm_command(command, validation.get("category", "mutating"))
|
|
276
|
+
if not confirmed:
|
|
277
|
+
tool_result = json.dumps({
|
|
278
|
+
"success": False,
|
|
279
|
+
"error": "Command was rejected by user",
|
|
280
|
+
"command": command,
|
|
281
|
+
})
|
|
282
|
+
else:
|
|
283
|
+
result = TOOL_FUNCTIONS["execute_command"](command=command, requires_approval=False)
|
|
284
|
+
tool_result = json.dumps(result)
|
|
285
|
+
else:
|
|
286
|
+
result = TOOL_FUNCTIONS["execute_command"](command=command, requires_approval=False)
|
|
287
|
+
tool_result = json.dumps(result)
|
|
288
|
+
|
|
289
|
+
elif tool_name == "propose_file_write":
|
|
290
|
+
file_path = args.get("path", "")
|
|
291
|
+
content = args.get("content", "")
|
|
292
|
+
|
|
293
|
+
if file_path and content:
|
|
294
|
+
confirmed = propose_file_write(file_path, content, _console=self.console)
|
|
295
|
+
if confirmed:
|
|
296
|
+
# Actually write the file after user approval
|
|
297
|
+
write_result = apply_proposal(CodeProposal(file_path, content))
|
|
298
|
+
tool_result = json.dumps(write_result)
|
|
299
|
+
else:
|
|
300
|
+
tool_result = json.dumps({
|
|
301
|
+
"success": False,
|
|
302
|
+
"error": "Changes were rejected by user",
|
|
303
|
+
})
|
|
304
|
+
else:
|
|
305
|
+
tool_result = json.dumps({
|
|
306
|
+
"success": False,
|
|
307
|
+
"error": "Missing path or content",
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
elif tool_name == "attempt_completion":
|
|
311
|
+
result_text = args.get("result", "")
|
|
312
|
+
self.console.print("\n[bold green]✅ Task completed:[/bold green]")
|
|
313
|
+
if result_text:
|
|
314
|
+
from rich.markdown import Markdown as _Markdown
|
|
315
|
+
self.console.print(_Markdown(result_text))
|
|
316
|
+
self._task_completed = True
|
|
317
|
+
return # Don't add to history; stop processing further tool calls
|
|
318
|
+
|
|
319
|
+
elif tool_name == "ask_followup_question":
|
|
320
|
+
question = args.get("question", "")
|
|
321
|
+
options = args.get("options")
|
|
322
|
+
|
|
323
|
+
if options:
|
|
324
|
+
self.console.print(f"\n[bold]{question}[/bold]")
|
|
325
|
+
for i, opt in enumerate(options, 1):
|
|
326
|
+
self.console.print(f" [{i}] {opt}")
|
|
327
|
+
answer = Prompt.ask("Select option", console=self.console, choices=[str(i) for i in range(1, len(options) + 1)])
|
|
328
|
+
answer = options[int(answer) - 1]
|
|
329
|
+
else:
|
|
330
|
+
self.console.print(f"\n[bold]{question}[/bold]")
|
|
331
|
+
answer = Prompt.ask("Your answer", console=self.console)
|
|
332
|
+
|
|
333
|
+
tool_result = json.dumps({"success": True, "answer": answer})
|
|
334
|
+
|
|
335
|
+
else:
|
|
336
|
+
result = execute_tool_call(tool_call)
|
|
337
|
+
tool_result = result["content"]
|
|
338
|
+
|
|
339
|
+
# Derive is_error from the "success" key (not the absent "is_error" key)
|
|
340
|
+
try:
|
|
341
|
+
is_error = not json.loads(tool_result).get("success", True) if tool_result.startswith("{") else False
|
|
342
|
+
except (json.JSONDecodeError, AttributeError):
|
|
343
|
+
is_error = False
|
|
344
|
+
|
|
345
|
+
self.history.add_tool_result(tool_call["id"], tool_name, tool_result, is_error=is_error)
|
|
346
|
+
|
|
347
|
+
def clear_history(self) -> None:
|
|
348
|
+
"""Clear the conversation history."""
|
|
349
|
+
self.history.clear()
|
|
350
|
+
self.console.print("[green]✅ Conversation history cleared.[/green]")
|
|
351
|
+
|
|
352
|
+
def get_status(self) -> Dict[str, Any]:
|
|
353
|
+
"""Get the current agent status."""
|
|
354
|
+
return {
|
|
355
|
+
"model": self._get_model(),
|
|
356
|
+
"security_mode": self.config.get_security_mode(),
|
|
357
|
+
"history_length": len(self.history),
|
|
358
|
+
"temperature": self._get_temperature(),
|
|
359
|
+
"max_tokens": self._get_max_tokens(),
|
|
360
|
+
}
|