nc1709 1.15.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nc1709/__init__.py +13 -0
- nc1709/agent/__init__.py +36 -0
- nc1709/agent/core.py +505 -0
- nc1709/agent/mcp_bridge.py +245 -0
- nc1709/agent/permissions.py +298 -0
- nc1709/agent/tools/__init__.py +21 -0
- nc1709/agent/tools/base.py +440 -0
- nc1709/agent/tools/bash_tool.py +367 -0
- nc1709/agent/tools/file_tools.py +454 -0
- nc1709/agent/tools/notebook_tools.py +516 -0
- nc1709/agent/tools/search_tools.py +322 -0
- nc1709/agent/tools/task_tool.py +284 -0
- nc1709/agent/tools/web_tools.py +555 -0
- nc1709/agents/__init__.py +17 -0
- nc1709/agents/auto_fix.py +506 -0
- nc1709/agents/test_generator.py +507 -0
- nc1709/checkpoints.py +372 -0
- nc1709/cli.py +3380 -0
- nc1709/cli_ui.py +1080 -0
- nc1709/cognitive/__init__.py +149 -0
- nc1709/cognitive/anticipation.py +594 -0
- nc1709/cognitive/context_engine.py +1046 -0
- nc1709/cognitive/council.py +824 -0
- nc1709/cognitive/learning.py +761 -0
- nc1709/cognitive/router.py +583 -0
- nc1709/cognitive/system.py +519 -0
- nc1709/config.py +155 -0
- nc1709/custom_commands.py +300 -0
- nc1709/executor.py +333 -0
- nc1709/file_controller.py +354 -0
- nc1709/git_integration.py +308 -0
- nc1709/github_integration.py +477 -0
- nc1709/image_input.py +446 -0
- nc1709/linting.py +519 -0
- nc1709/llm_adapter.py +667 -0
- nc1709/logger.py +192 -0
- nc1709/mcp/__init__.py +18 -0
- nc1709/mcp/client.py +370 -0
- nc1709/mcp/manager.py +407 -0
- nc1709/mcp/protocol.py +210 -0
- nc1709/mcp/server.py +473 -0
- nc1709/memory/__init__.py +20 -0
- nc1709/memory/embeddings.py +325 -0
- nc1709/memory/indexer.py +474 -0
- nc1709/memory/sessions.py +432 -0
- nc1709/memory/vector_store.py +451 -0
- nc1709/models/__init__.py +86 -0
- nc1709/models/detector.py +377 -0
- nc1709/models/formats.py +315 -0
- nc1709/models/manager.py +438 -0
- nc1709/models/registry.py +497 -0
- nc1709/performance/__init__.py +343 -0
- nc1709/performance/cache.py +705 -0
- nc1709/performance/pipeline.py +611 -0
- nc1709/performance/tiering.py +543 -0
- nc1709/plan_mode.py +362 -0
- nc1709/plugins/__init__.py +17 -0
- nc1709/plugins/agents/__init__.py +18 -0
- nc1709/plugins/agents/django_agent.py +912 -0
- nc1709/plugins/agents/docker_agent.py +623 -0
- nc1709/plugins/agents/fastapi_agent.py +887 -0
- nc1709/plugins/agents/git_agent.py +731 -0
- nc1709/plugins/agents/nextjs_agent.py +867 -0
- nc1709/plugins/base.py +359 -0
- nc1709/plugins/manager.py +411 -0
- nc1709/plugins/registry.py +337 -0
- nc1709/progress.py +443 -0
- nc1709/prompts/__init__.py +22 -0
- nc1709/prompts/agent_system.py +180 -0
- nc1709/prompts/task_prompts.py +340 -0
- nc1709/prompts/unified_prompt.py +133 -0
- nc1709/reasoning_engine.py +541 -0
- nc1709/remote_client.py +266 -0
- nc1709/shell_completions.py +349 -0
- nc1709/slash_commands.py +649 -0
- nc1709/task_classifier.py +408 -0
- nc1709/version_check.py +177 -0
- nc1709/web/__init__.py +8 -0
- nc1709/web/server.py +950 -0
- nc1709/web/templates/index.html +1127 -0
- nc1709-1.15.4.dist-info/METADATA +858 -0
- nc1709-1.15.4.dist-info/RECORD +86 -0
- nc1709-1.15.4.dist-info/WHEEL +5 -0
- nc1709-1.15.4.dist-info/entry_points.txt +2 -0
- nc1709-1.15.4.dist-info/licenses/LICENSE +9 -0
- nc1709-1.15.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom Slash Commands for NC1709
|
|
3
|
+
|
|
4
|
+
Allows users to define their own slash commands in:
|
|
5
|
+
- ~/.nc1709/commands/ (personal commands)
|
|
6
|
+
- .nc1709/commands/ (project commands)
|
|
7
|
+
|
|
8
|
+
Similar to Claude Code's custom command system.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Optional, List, Dict, Any
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class CustomCommand:
|
|
19
|
+
"""A user-defined custom command"""
|
|
20
|
+
name: str # Command name (without /)
|
|
21
|
+
content: str # Command content/prompt
|
|
22
|
+
description: str # Optional description (first line of file)
|
|
23
|
+
file_path: Path # Path to the command file
|
|
24
|
+
scope: str # "personal" or "project"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class CustomCommandManager:
|
|
28
|
+
"""
|
|
29
|
+
Manages custom slash commands defined in markdown files.
|
|
30
|
+
|
|
31
|
+
Commands are loaded from:
|
|
32
|
+
- ~/.nc1709/commands/*.md (personal commands, available everywhere)
|
|
33
|
+
- .nc1709/commands/*.md (project commands, available in current project)
|
|
34
|
+
|
|
35
|
+
The filename becomes the command name (e.g., fix-bug.md -> /project:fix-bug)
|
|
36
|
+
The first line starting with # is used as the description.
|
|
37
|
+
The rest is the command content/prompt.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, project_path: Optional[str] = None):
|
|
41
|
+
"""
|
|
42
|
+
Initialize the custom command manager.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
project_path: Path to current project (defaults to cwd)
|
|
46
|
+
"""
|
|
47
|
+
self.project_path = Path(project_path) if project_path else Path.cwd()
|
|
48
|
+
self.personal_dir = Path.home() / ".nc1709" / "commands"
|
|
49
|
+
self.project_dir = self.project_path / ".nc1709" / "commands"
|
|
50
|
+
|
|
51
|
+
# Ensure personal commands directory exists
|
|
52
|
+
self.personal_dir.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
|
|
54
|
+
# Cache loaded commands
|
|
55
|
+
self._commands: Dict[str, CustomCommand] = {}
|
|
56
|
+
self._loaded = False
|
|
57
|
+
|
|
58
|
+
def _load_commands(self) -> None:
|
|
59
|
+
"""Load all custom commands from disk"""
|
|
60
|
+
self._commands = {}
|
|
61
|
+
|
|
62
|
+
# Load personal commands
|
|
63
|
+
if self.personal_dir.exists():
|
|
64
|
+
for file_path in self.personal_dir.glob("*.md"):
|
|
65
|
+
cmd = self._load_command_file(file_path, scope="personal")
|
|
66
|
+
if cmd:
|
|
67
|
+
self._commands[cmd.name] = cmd
|
|
68
|
+
|
|
69
|
+
# Load project commands (override personal if same name)
|
|
70
|
+
if self.project_dir.exists():
|
|
71
|
+
for file_path in self.project_dir.glob("*.md"):
|
|
72
|
+
cmd = self._load_command_file(file_path, scope="project")
|
|
73
|
+
if cmd:
|
|
74
|
+
# Use project: prefix to avoid collisions
|
|
75
|
+
self._commands[f"project:{cmd.name}"] = cmd
|
|
76
|
+
|
|
77
|
+
self._loaded = True
|
|
78
|
+
|
|
79
|
+
def _load_command_file(self, file_path: Path, scope: str) -> Optional[CustomCommand]:
|
|
80
|
+
"""Load a single command file"""
|
|
81
|
+
try:
|
|
82
|
+
content = file_path.read_text(encoding='utf-8')
|
|
83
|
+
lines = content.strip().split('\n')
|
|
84
|
+
|
|
85
|
+
# Extract description from first heading line
|
|
86
|
+
description = ""
|
|
87
|
+
content_start = 0
|
|
88
|
+
|
|
89
|
+
for i, line in enumerate(lines):
|
|
90
|
+
if line.startswith('#'):
|
|
91
|
+
# Extract text after # (the description)
|
|
92
|
+
description = line.lstrip('#').strip()
|
|
93
|
+
content_start = i + 1
|
|
94
|
+
break
|
|
95
|
+
elif line.strip():
|
|
96
|
+
# First non-empty, non-heading line - no description
|
|
97
|
+
break
|
|
98
|
+
|
|
99
|
+
# Command name is filename without extension
|
|
100
|
+
name = file_path.stem
|
|
101
|
+
|
|
102
|
+
# Content is everything after the description
|
|
103
|
+
command_content = '\n'.join(lines[content_start:]).strip()
|
|
104
|
+
|
|
105
|
+
return CustomCommand(
|
|
106
|
+
name=name,
|
|
107
|
+
content=command_content,
|
|
108
|
+
description=description or f"Custom command: {name}",
|
|
109
|
+
file_path=file_path,
|
|
110
|
+
scope=scope
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
except Exception:
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
def reload(self) -> None:
|
|
117
|
+
"""Force reload all commands"""
|
|
118
|
+
self._loaded = False
|
|
119
|
+
self._load_commands()
|
|
120
|
+
|
|
121
|
+
def get_command(self, name: str) -> Optional[CustomCommand]:
|
|
122
|
+
"""
|
|
123
|
+
Get a custom command by name.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
name: Command name (with or without /)
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
CustomCommand if found, None otherwise
|
|
130
|
+
"""
|
|
131
|
+
if not self._loaded:
|
|
132
|
+
self._load_commands()
|
|
133
|
+
|
|
134
|
+
# Remove leading / if present
|
|
135
|
+
if name.startswith('/'):
|
|
136
|
+
name = name[1:]
|
|
137
|
+
|
|
138
|
+
return self._commands.get(name)
|
|
139
|
+
|
|
140
|
+
def list_commands(self) -> List[CustomCommand]:
|
|
141
|
+
"""Get all available custom commands"""
|
|
142
|
+
if not self._loaded:
|
|
143
|
+
self._load_commands()
|
|
144
|
+
|
|
145
|
+
return list(self._commands.values())
|
|
146
|
+
|
|
147
|
+
def get_commands_for_autocomplete(self) -> List[Dict[str, str]]:
|
|
148
|
+
"""Get commands formatted for autocomplete"""
|
|
149
|
+
if not self._loaded:
|
|
150
|
+
self._load_commands()
|
|
151
|
+
|
|
152
|
+
return [
|
|
153
|
+
{
|
|
154
|
+
"name": cmd.name,
|
|
155
|
+
"description": cmd.description,
|
|
156
|
+
"scope": cmd.scope
|
|
157
|
+
}
|
|
158
|
+
for cmd in self._commands.values()
|
|
159
|
+
]
|
|
160
|
+
|
|
161
|
+
def create_command(
|
|
162
|
+
self,
|
|
163
|
+
name: str,
|
|
164
|
+
content: str,
|
|
165
|
+
description: str = "",
|
|
166
|
+
scope: str = "personal"
|
|
167
|
+
) -> CustomCommand:
|
|
168
|
+
"""
|
|
169
|
+
Create a new custom command.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
name: Command name (without /)
|
|
173
|
+
content: Command content/prompt
|
|
174
|
+
description: Optional description
|
|
175
|
+
scope: "personal" or "project"
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
The created CustomCommand
|
|
179
|
+
"""
|
|
180
|
+
# Determine target directory
|
|
181
|
+
if scope == "project":
|
|
182
|
+
target_dir = self.project_dir
|
|
183
|
+
else:
|
|
184
|
+
target_dir = self.personal_dir
|
|
185
|
+
|
|
186
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
187
|
+
|
|
188
|
+
# Create the file content
|
|
189
|
+
file_content = ""
|
|
190
|
+
if description:
|
|
191
|
+
file_content = f"# {description}\n\n"
|
|
192
|
+
file_content += content
|
|
193
|
+
|
|
194
|
+
# Write the file
|
|
195
|
+
file_path = target_dir / f"{name}.md"
|
|
196
|
+
file_path.write_text(file_content, encoding='utf-8')
|
|
197
|
+
|
|
198
|
+
# Create and cache the command
|
|
199
|
+
cmd = CustomCommand(
|
|
200
|
+
name=name if scope == "personal" else f"project:{name}",
|
|
201
|
+
content=content,
|
|
202
|
+
description=description or f"Custom command: {name}",
|
|
203
|
+
file_path=file_path,
|
|
204
|
+
scope=scope
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
self._commands[cmd.name] = cmd
|
|
208
|
+
return cmd
|
|
209
|
+
|
|
210
|
+
def delete_command(self, name: str) -> bool:
|
|
211
|
+
"""
|
|
212
|
+
Delete a custom command.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
name: Command name
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
True if deleted, False if not found
|
|
219
|
+
"""
|
|
220
|
+
cmd = self.get_command(name)
|
|
221
|
+
if cmd and cmd.file_path.exists():
|
|
222
|
+
cmd.file_path.unlink()
|
|
223
|
+
del self._commands[cmd.name]
|
|
224
|
+
return True
|
|
225
|
+
return False
|
|
226
|
+
|
|
227
|
+
def get_example_commands(self) -> str:
|
|
228
|
+
"""Get example custom commands for users"""
|
|
229
|
+
return """# Example Custom Commands
|
|
230
|
+
|
|
231
|
+
Create markdown files in ~/.nc1709/commands/ or .nc1709/commands/
|
|
232
|
+
|
|
233
|
+
## Example: fix-bug.md
|
|
234
|
+
|
|
235
|
+
```markdown
|
|
236
|
+
# Fix a bug in the codebase
|
|
237
|
+
|
|
238
|
+
Look at the error message or description provided.
|
|
239
|
+
Find the relevant code files.
|
|
240
|
+
Analyze the root cause.
|
|
241
|
+
Propose and implement a fix.
|
|
242
|
+
Test that the fix works.
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## Example: add-tests.md
|
|
246
|
+
|
|
247
|
+
```markdown
|
|
248
|
+
# Add unit tests for a file
|
|
249
|
+
|
|
250
|
+
Read the specified file.
|
|
251
|
+
Identify all public functions and classes.
|
|
252
|
+
Generate comprehensive unit tests.
|
|
253
|
+
Include edge cases and error handling tests.
|
|
254
|
+
Write tests to a corresponding test file.
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## Example: review-pr.md
|
|
258
|
+
|
|
259
|
+
```markdown
|
|
260
|
+
# Review a pull request
|
|
261
|
+
|
|
262
|
+
Check the diff for:
|
|
263
|
+
- Code style and best practices
|
|
264
|
+
- Potential bugs or edge cases
|
|
265
|
+
- Security vulnerabilities
|
|
266
|
+
- Performance issues
|
|
267
|
+
- Missing tests
|
|
268
|
+
|
|
269
|
+
Provide actionable feedback.
|
|
270
|
+
```
|
|
271
|
+
"""
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
# Global custom command manager
|
|
275
|
+
_custom_command_manager: Optional[CustomCommandManager] = None
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def get_custom_command_manager() -> CustomCommandManager:
|
|
279
|
+
"""Get or create the global custom command manager"""
|
|
280
|
+
global _custom_command_manager
|
|
281
|
+
if _custom_command_manager is None:
|
|
282
|
+
_custom_command_manager = CustomCommandManager()
|
|
283
|
+
return _custom_command_manager
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def execute_custom_command(name: str) -> Optional[str]:
|
|
287
|
+
"""
|
|
288
|
+
Get the prompt content for a custom command.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
name: Command name
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
Command prompt content if found, None otherwise
|
|
295
|
+
"""
|
|
296
|
+
manager = get_custom_command_manager()
|
|
297
|
+
cmd = manager.get_command(name)
|
|
298
|
+
if cmd:
|
|
299
|
+
return cmd.content
|
|
300
|
+
return None
|
nc1709/executor.py
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Execution Sandbox for Safe Command Execution
|
|
3
|
+
Validates and executes shell commands with safety checks
|
|
4
|
+
"""
|
|
5
|
+
import subprocess
|
|
6
|
+
import shlex
|
|
7
|
+
import os
|
|
8
|
+
from typing import Tuple, List, Optional
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from .config import get_config
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CommandExecutor:
|
|
15
|
+
"""Executes shell commands with safety validation"""
|
|
16
|
+
|
|
17
|
+
def __init__(self):
|
|
18
|
+
"""Initialize the command executor"""
|
|
19
|
+
self.config = get_config()
|
|
20
|
+
self.execution_log: List[dict] = []
|
|
21
|
+
|
|
22
|
+
def execute(
|
|
23
|
+
self,
|
|
24
|
+
command: str,
|
|
25
|
+
cwd: Optional[str] = None,
|
|
26
|
+
timeout: Optional[int] = None,
|
|
27
|
+
confirm: bool = True
|
|
28
|
+
) -> Tuple[int, str, str]:
|
|
29
|
+
"""Execute a shell command with safety checks
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
command: Command to execute
|
|
33
|
+
cwd: Working directory (default: current directory)
|
|
34
|
+
timeout: Timeout in seconds (default: from config)
|
|
35
|
+
confirm: Whether to ask for confirmation
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Tuple of (return_code, stdout, stderr)
|
|
39
|
+
|
|
40
|
+
Raises:
|
|
41
|
+
ValueError: If command is not allowed
|
|
42
|
+
subprocess.TimeoutExpired: If command times out
|
|
43
|
+
"""
|
|
44
|
+
# Validate command
|
|
45
|
+
if not self._is_command_allowed(command):
|
|
46
|
+
raise ValueError(f"Command not allowed: {command}")
|
|
47
|
+
|
|
48
|
+
# Check for destructive operations
|
|
49
|
+
if self._is_destructive(command):
|
|
50
|
+
if not self._confirm_destructive(command):
|
|
51
|
+
return (-1, "", "Command cancelled by user")
|
|
52
|
+
|
|
53
|
+
# Confirm execution if required
|
|
54
|
+
if confirm and self.config.get("safety.confirm_commands", True):
|
|
55
|
+
print(f"\n💻 About to execute: {command}")
|
|
56
|
+
if cwd:
|
|
57
|
+
print(f" Working directory: {cwd}")
|
|
58
|
+
response = input("Execute this command? [y/N]: ").strip().lower()
|
|
59
|
+
if response != 'y':
|
|
60
|
+
print("Execution cancelled.")
|
|
61
|
+
return (-1, "", "Command cancelled by user")
|
|
62
|
+
|
|
63
|
+
# Set timeout
|
|
64
|
+
if timeout is None:
|
|
65
|
+
timeout = self.config.get("execution.command_timeout", 60)
|
|
66
|
+
|
|
67
|
+
# Set working directory
|
|
68
|
+
if cwd is None:
|
|
69
|
+
cwd = os.getcwd()
|
|
70
|
+
|
|
71
|
+
# Execute command
|
|
72
|
+
try:
|
|
73
|
+
print(f"\n🔄 Executing: {command}")
|
|
74
|
+
|
|
75
|
+
result = subprocess.run(
|
|
76
|
+
command,
|
|
77
|
+
shell=True,
|
|
78
|
+
cwd=cwd,
|
|
79
|
+
timeout=timeout,
|
|
80
|
+
capture_output=True,
|
|
81
|
+
text=True
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Log execution
|
|
85
|
+
self._log_execution(command, cwd, result.returncode, result.stdout, result.stderr)
|
|
86
|
+
|
|
87
|
+
# Print output
|
|
88
|
+
if result.stdout:
|
|
89
|
+
print("\n📤 Output:")
|
|
90
|
+
print(result.stdout)
|
|
91
|
+
|
|
92
|
+
if result.stderr:
|
|
93
|
+
print("\n⚠️ Errors/Warnings:")
|
|
94
|
+
print(result.stderr)
|
|
95
|
+
|
|
96
|
+
if result.returncode == 0:
|
|
97
|
+
print("✅ Command completed successfully")
|
|
98
|
+
else:
|
|
99
|
+
print(f"❌ Command failed with exit code {result.returncode}")
|
|
100
|
+
|
|
101
|
+
return (result.returncode, result.stdout, result.stderr)
|
|
102
|
+
|
|
103
|
+
except subprocess.TimeoutExpired:
|
|
104
|
+
error_msg = f"Command timed out after {timeout} seconds"
|
|
105
|
+
print(f"\n⏱️ {error_msg}")
|
|
106
|
+
self._log_execution(command, cwd, -1, "", error_msg)
|
|
107
|
+
return (-1, "", error_msg)
|
|
108
|
+
|
|
109
|
+
except Exception as e:
|
|
110
|
+
error_msg = f"Execution error: {str(e)}"
|
|
111
|
+
print(f"\n❌ {error_msg}")
|
|
112
|
+
self._log_execution(command, cwd, -1, "", error_msg)
|
|
113
|
+
return (-1, "", error_msg)
|
|
114
|
+
|
|
115
|
+
def execute_multiple(
|
|
116
|
+
self,
|
|
117
|
+
commands: List[str],
|
|
118
|
+
cwd: Optional[str] = None,
|
|
119
|
+
stop_on_error: bool = True
|
|
120
|
+
) -> List[Tuple[int, str, str]]:
|
|
121
|
+
"""Execute multiple commands in sequence
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
commands: List of commands to execute
|
|
125
|
+
cwd: Working directory
|
|
126
|
+
stop_on_error: Whether to stop if a command fails
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
List of (return_code, stdout, stderr) tuples
|
|
130
|
+
"""
|
|
131
|
+
results = []
|
|
132
|
+
|
|
133
|
+
for i, command in enumerate(commands, 1):
|
|
134
|
+
print(f"\n{'='*60}")
|
|
135
|
+
print(f"Command {i}/{len(commands)}")
|
|
136
|
+
print(f"{'='*60}")
|
|
137
|
+
|
|
138
|
+
result = self.execute(command, cwd=cwd, confirm=False)
|
|
139
|
+
results.append(result)
|
|
140
|
+
|
|
141
|
+
# Stop on error if requested
|
|
142
|
+
if stop_on_error and result[0] != 0:
|
|
143
|
+
print(f"\n⚠️ Stopping execution due to error in command {i}")
|
|
144
|
+
break
|
|
145
|
+
|
|
146
|
+
return results
|
|
147
|
+
|
|
148
|
+
def _is_command_allowed(self, command: str) -> bool:
|
|
149
|
+
"""Check if a command is allowed
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
command: Command to check
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
True if command is allowed
|
|
156
|
+
"""
|
|
157
|
+
# Parse command to get the base command
|
|
158
|
+
try:
|
|
159
|
+
parts = shlex.split(command)
|
|
160
|
+
if not parts:
|
|
161
|
+
return False
|
|
162
|
+
base_command = parts[0]
|
|
163
|
+
except ValueError:
|
|
164
|
+
# If parsing fails, be conservative and reject
|
|
165
|
+
print("⛔ Command parsing failed - rejecting for safety")
|
|
166
|
+
return False
|
|
167
|
+
|
|
168
|
+
# Normalize the command for security checks
|
|
169
|
+
normalized_cmd = command.lower().replace("\\", "")
|
|
170
|
+
|
|
171
|
+
# Check against blocked commands with improved pattern matching
|
|
172
|
+
blocked = self.config.get("execution.blocked_commands", [])
|
|
173
|
+
for blocked_cmd in blocked:
|
|
174
|
+
blocked_lower = blocked_cmd.lower()
|
|
175
|
+
# Check for exact match or as part of a command chain
|
|
176
|
+
if (blocked_lower in normalized_cmd or
|
|
177
|
+
normalized_cmd.startswith(blocked_lower) or
|
|
178
|
+
f"; {blocked_lower}" in normalized_cmd or
|
|
179
|
+
f"&& {blocked_lower}" in normalized_cmd or
|
|
180
|
+
f"| {blocked_lower}" in normalized_cmd or
|
|
181
|
+
f"|| {blocked_lower}" in normalized_cmd):
|
|
182
|
+
print(f"⛔ Blocked command detected: {blocked_cmd}")
|
|
183
|
+
return False
|
|
184
|
+
|
|
185
|
+
# Check for shell injection patterns
|
|
186
|
+
dangerous_patterns = [
|
|
187
|
+
"$(", "`", # Command substitution
|
|
188
|
+
"${", # Variable expansion that could be exploited
|
|
189
|
+
"; rm", "&& rm", # Chained destructive commands
|
|
190
|
+
"| sh", "| bash", # Piping to shell
|
|
191
|
+
"> /dev/sd", # Writing to block devices
|
|
192
|
+
"eval ", "exec ", # Dangerous execution primitives
|
|
193
|
+
]
|
|
194
|
+
for pattern in dangerous_patterns:
|
|
195
|
+
if pattern in command:
|
|
196
|
+
print(f"⛔ Potentially dangerous pattern detected: {pattern}")
|
|
197
|
+
return False
|
|
198
|
+
|
|
199
|
+
# Check against allowed commands (if whitelist is enabled)
|
|
200
|
+
allowed = self.config.get("execution.allowed_commands", [])
|
|
201
|
+
if allowed:
|
|
202
|
+
# Extract just the command name (without path)
|
|
203
|
+
cmd_name = os.path.basename(base_command)
|
|
204
|
+
if cmd_name not in allowed and base_command not in allowed:
|
|
205
|
+
print(f"⛔ Command not in whitelist: {base_command}")
|
|
206
|
+
print(f" Allowed commands: {', '.join(allowed)}")
|
|
207
|
+
return False
|
|
208
|
+
|
|
209
|
+
return True
|
|
210
|
+
|
|
211
|
+
def _is_destructive(self, command: str) -> bool:
|
|
212
|
+
"""Check if a command is potentially destructive
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
command: Command to check
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
True if command is destructive
|
|
219
|
+
"""
|
|
220
|
+
destructive_patterns = [
|
|
221
|
+
"rm ", "rmdir", "del ", "format", "mkfs",
|
|
222
|
+
"dd ", ">", "truncate", "shred"
|
|
223
|
+
]
|
|
224
|
+
|
|
225
|
+
return any(pattern in command for pattern in destructive_patterns)
|
|
226
|
+
|
|
227
|
+
def _confirm_destructive(self, command: str) -> bool:
|
|
228
|
+
"""Ask for confirmation for destructive commands
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
command: Command to confirm
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
True if user confirms
|
|
235
|
+
"""
|
|
236
|
+
if not self.config.get("safety.confirm_destructive", True):
|
|
237
|
+
return True
|
|
238
|
+
|
|
239
|
+
print(f"\n⚠️ WARNING: Potentially destructive command detected!")
|
|
240
|
+
print(f" Command: {command}")
|
|
241
|
+
print(f" This command may delete or modify data.")
|
|
242
|
+
response = input("Are you absolutely sure you want to execute this? [yes/NO]: ").strip().lower()
|
|
243
|
+
|
|
244
|
+
return response == "yes"
|
|
245
|
+
|
|
246
|
+
def _log_execution(
|
|
247
|
+
self,
|
|
248
|
+
command: str,
|
|
249
|
+
cwd: str,
|
|
250
|
+
return_code: int,
|
|
251
|
+
stdout: str,
|
|
252
|
+
stderr: str
|
|
253
|
+
) -> None:
|
|
254
|
+
"""Log command execution
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
command: Executed command
|
|
258
|
+
cwd: Working directory
|
|
259
|
+
return_code: Exit code
|
|
260
|
+
stdout: Standard output
|
|
261
|
+
stderr: Standard error
|
|
262
|
+
"""
|
|
263
|
+
from datetime import datetime
|
|
264
|
+
|
|
265
|
+
log_entry = {
|
|
266
|
+
"timestamp": datetime.now().isoformat(),
|
|
267
|
+
"command": command,
|
|
268
|
+
"cwd": cwd,
|
|
269
|
+
"return_code": return_code,
|
|
270
|
+
"stdout_length": len(stdout),
|
|
271
|
+
"stderr_length": len(stderr),
|
|
272
|
+
"success": return_code == 0
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
self.execution_log.append(log_entry)
|
|
276
|
+
|
|
277
|
+
# Keep only last N entries
|
|
278
|
+
max_log_size = 1000
|
|
279
|
+
if len(self.execution_log) > max_log_size:
|
|
280
|
+
self.execution_log = self.execution_log[-max_log_size:]
|
|
281
|
+
|
|
282
|
+
def get_execution_history(self, limit: int = 10) -> List[dict]:
|
|
283
|
+
"""Get recent command execution history
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
limit: Number of recent entries to return
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
List of execution log entries
|
|
290
|
+
"""
|
|
291
|
+
return self.execution_log[-limit:]
|
|
292
|
+
|
|
293
|
+
def validate_command(self, command: str) -> Tuple[bool, str]:
|
|
294
|
+
"""Validate a command without executing it
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
command: Command to validate
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
Tuple of (is_valid, message)
|
|
301
|
+
"""
|
|
302
|
+
if not command.strip():
|
|
303
|
+
return (False, "Empty command")
|
|
304
|
+
|
|
305
|
+
if not self._is_command_allowed(command):
|
|
306
|
+
return (False, "Command not allowed by security policy")
|
|
307
|
+
|
|
308
|
+
if self._is_destructive(command):
|
|
309
|
+
return (True, "Command is valid but potentially destructive")
|
|
310
|
+
|
|
311
|
+
return (True, "Command is valid")
|
|
312
|
+
|
|
313
|
+
def suggest_safe_alternative(self, command: str) -> Optional[str]:
|
|
314
|
+
"""Suggest a safer alternative for a command
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
command: Original command
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
Suggested alternative command or None
|
|
321
|
+
"""
|
|
322
|
+
# Common dangerous patterns and their safer alternatives
|
|
323
|
+
alternatives = {
|
|
324
|
+
"rm -rf /": "# This command would delete your entire system! Never run this.",
|
|
325
|
+
"rm -rf": "rm -ri", # Interactive mode
|
|
326
|
+
"dd if=/dev/zero": "# This would wipe data. Use with extreme caution.",
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
for pattern, alternative in alternatives.items():
|
|
330
|
+
if pattern in command:
|
|
331
|
+
return alternative
|
|
332
|
+
|
|
333
|
+
return None
|