ata-coder 2.4.2__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.
- ata_coder/__init__.py +1 -0
- ata_coder/agent.py +874 -0
- ata_coder/agent_compact.py +190 -0
- ata_coder/agent_controller.py +218 -0
- ata_coder/agent_extension.py +69 -0
- ata_coder/agent_routing.py +105 -0
- ata_coder/agent_subsystems.py +72 -0
- ata_coder/agent_tools.py +318 -0
- ata_coder/agent_undo.py +63 -0
- ata_coder/anthropic_client.py +465 -0
- ata_coder/change_tracker.py +368 -0
- ata_coder/clawd_integration.py +574 -0
- ata_coder/commands/__init__.py +128 -0
- ata_coder/commands/_core.py +184 -0
- ata_coder/commands/_safety.py +95 -0
- ata_coder/commands/_settings.py +241 -0
- ata_coder/commands/_workflow.py +451 -0
- ata_coder/commands.py +974 -0
- ata_coder/config.py +257 -0
- ata_coder/core/__init__.py +35 -0
- ata_coder/core/events.py +73 -0
- ata_coder/core/queue.py +85 -0
- ata_coder/core/state.py +17 -0
- ata_coder/event_queue.py +5 -0
- ata_coder/extension.py +654 -0
- ata_coder/extensions/__init__.py +1 -0
- ata_coder/extensions/hello_skill.py +47 -0
- ata_coder/fool_proof.py +295 -0
- ata_coder/git_workflow.py +371 -0
- ata_coder/gui.py +511 -0
- ata_coder/llm_client.py +543 -0
- ata_coder/main.py +814 -0
- ata_coder/mcp_client.py +1095 -0
- ata_coder/memory.py +539 -0
- ata_coder/model_registry.py +134 -0
- ata_coder/model_router.py +105 -0
- ata_coder/permissions.py +274 -0
- ata_coder/privilege.py +464 -0
- ata_coder/project.py +273 -0
- ata_coder/prompt_template.py +423 -0
- ata_coder/prompts/auto-mode.md +7 -0
- ata_coder/prompts/coding-rules.md +40 -0
- ata_coder/prompts/execution-guardrails.md +14 -0
- ata_coder/prompts/memory-system.md +24 -0
- ata_coder/prompts/output-style.md +23 -0
- ata_coder/prompts/safety.md +17 -0
- ata_coder/prompts/slash-commands.md +24 -0
- ata_coder/prompts/sub-agents.md +38 -0
- ata_coder/prompts/system-reminders.md +17 -0
- ata_coder/prompts/system.md +105 -0
- ata_coder/prompts/tool-policy.md +46 -0
- ata_coder/repl_theme.py +99 -0
- ata_coder/repl_tracker.py +89 -0
- ata_coder/repl_ui.py +1214 -0
- ata_coder/safety_guard.py +434 -0
- ata_coder/self_correct.py +346 -0
- ata_coder/server.py +882 -0
- ata_coder/server_session.py +159 -0
- ata_coder/server_shell.py +129 -0
- ata_coder/session.py +431 -0
- ata_coder/settings.py +439 -0
- ata_coder/setup_wizard.py +136 -0
- ata_coder/skill_extension.py +92 -0
- ata_coder/skills/architect/SKILL.md +42 -0
- ata_coder/skills/code-reviewer/SKILL.md +37 -0
- ata_coder/skills/codecraft/SKILL.md +452 -0
- ata_coder/skills/debugger/SKILL.md +45 -0
- ata_coder/skills/doc-writer/SKILL.md +36 -0
- ata_coder/skills/general-coder/SKILL.md +76 -0
- ata_coder/skills/math-calculator/README.md +40 -0
- ata_coder/skills/math-calculator/SKILL.md +59 -0
- ata_coder/skills/math-calculator/handler.py +103 -0
- ata_coder/skills/math-calculator/prompts/system.md +8 -0
- ata_coder/skills/math-calculator/requirements.txt +2 -0
- ata_coder/skills/math-calculator/resources/constants.json +8 -0
- ata_coder/skills/math-calculator/tests/test_handler.py +53 -0
- ata_coder/skills/security-auditor/SKILL.md +40 -0
- ata_coder/skills/test-writer/SKILL.md +36 -0
- ata_coder/skills/weather-skill/README.md +45 -0
- ata_coder/skills/weather-skill/handler.py +76 -0
- ata_coder/skills/weather-skill/manifest.json +48 -0
- ata_coder/skills/weather-skill/prompts/system_prompt.txt +9 -0
- ata_coder/skills/weather-skill/prompts/user_prompt_template.txt +3 -0
- ata_coder/skills/weather-skill/requirements.txt +1 -0
- ata_coder/skills/weather-skill/resources/city_list.json +17 -0
- ata_coder/skills/weather-skill/resources/error_messages.json +7 -0
- ata_coder/skills/weather-skill/tests/test_handler.py +28 -0
- ata_coder/skills/weather-skill/weather_utils.py +50 -0
- ata_coder/skills.py +1014 -0
- ata_coder/sub_agent.py +273 -0
- ata_coder/sub_agent_manager.py +203 -0
- ata_coder/system_prompt_builder.py +146 -0
- ata_coder/task_planner.py +391 -0
- ata_coder/terminal.py +318 -0
- ata_coder/test_runner.py +219 -0
- ata_coder/thread_supervisor.py +195 -0
- ata_coder/tool_defs.py +335 -0
- ata_coder/tools/__init__.py +11 -0
- ata_coder/tools/definitions.py +335 -0
- ata_coder/tools/executor.py +1036 -0
- ata_coder/tools/result.py +26 -0
- ata_coder/tools/subagent.py +332 -0
- ata_coder/tools/web.py +361 -0
- ata_coder/tools.py +1576 -0
- ata_coder/types.py +92 -0
- ata_coder/utils.py +113 -0
- ata_coder/web/css/style.css +180 -0
- ata_coder/web/index.html +84 -0
- ata_coder/web/js/app.js +489 -0
- ata_coder/web/package-lock.json +25 -0
- ata_coder/web/package.json +10 -0
- ata_coder/web/tsconfig.json +13 -0
- ata_coder-2.4.2.dist-info/METADATA +799 -0
- ata_coder-2.4.2.dist-info/RECORD +118 -0
- ata_coder-2.4.2.dist-info/WHEEL +5 -0
- ata_coder-2.4.2.dist-info/entry_points.txt +2 -0
- ata_coder-2.4.2.dist-info/licenses/LICENSE +21 -0
- ata_coder-2.4.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Example extension — demonstrates the Extension API.
|
|
4
|
+
|
|
5
|
+
This is a minimal example showing how to create a custom extension
|
|
6
|
+
that contributes a system prompt and a tool.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
# Auto-discovered from extensions/ directory
|
|
10
|
+
# Or register manually:
|
|
11
|
+
from ata_coder.extension import get_extension_manager
|
|
12
|
+
from extensions.hello_skill import HelloSkill
|
|
13
|
+
get_extension_manager().register(HelloSkill())
|
|
14
|
+
get_extension_manager().activate("hello-skill")
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from ata_coder.extension import Extension, extension
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@extension(
|
|
21
|
+
name="hello-skill",
|
|
22
|
+
version="1.0.0",
|
|
23
|
+
description="A friendly companion skill that greets you",
|
|
24
|
+
tags=["skill", "example"],
|
|
25
|
+
priority=90,
|
|
26
|
+
)
|
|
27
|
+
class HelloSkill(Extension):
|
|
28
|
+
"""Example skill extension that adds a friendly tone to responses."""
|
|
29
|
+
|
|
30
|
+
def get_prompt(self) -> str:
|
|
31
|
+
return (
|
|
32
|
+
"You are a friendly and encouraging coding assistant. "
|
|
33
|
+
"Always start your responses with a warm greeting and "
|
|
34
|
+
"end with an encouraging note."
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def on_activate(self) -> None:
|
|
38
|
+
"""Called when this extension is activated."""
|
|
39
|
+
import logging
|
|
40
|
+
logger = logging.getLogger(__name__)
|
|
41
|
+
logger.info("HelloSkill activated — be friendly!")
|
|
42
|
+
|
|
43
|
+
def on_deactivate(self) -> None:
|
|
44
|
+
"""Called when this extension is deactivated."""
|
|
45
|
+
import logging
|
|
46
|
+
logger = logging.getLogger(__name__)
|
|
47
|
+
logger.info("HelloSkill deactivated — back to normal.")
|
ata_coder/fool_proof.py
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Fool-proof integration layer — ties together safety guards, change tracking,
|
|
3
|
+
dry-run preview, and interactive confirmation.
|
|
4
|
+
|
|
5
|
+
Provides a single unified interface for "check before execute":
|
|
6
|
+
|
|
7
|
+
check = guard.evaluate(tool_name, arguments)
|
|
8
|
+
if check.needs_confirmation:
|
|
9
|
+
ui.show_confirmation(check) # interactive prompt
|
|
10
|
+
if check.allowed:
|
|
11
|
+
result = execute(...)
|
|
12
|
+
tracker.capture(result)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from enum import Enum
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from .safety_guard import SafetyGuard, SafetyCheck, RiskLevel
|
|
22
|
+
from .change_tracker import ChangeTracker, FileChange
|
|
23
|
+
from .permissions import PermissionStore, PermissionMode, tool_category
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
29
|
+
# Unified check result
|
|
30
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
31
|
+
|
|
32
|
+
class ActionRequired(Enum):
|
|
33
|
+
PROCEED = "proceed" # No confirmation needed
|
|
34
|
+
CONFIRM = "confirm" # Show confirmation prompt (y/n/a/d)
|
|
35
|
+
WARN_CONFIRM = "warn_confirm" # Show warning + confirmation
|
|
36
|
+
TYPE_CONFIRM = "type_confirm" # User must type a phrase to confirm
|
|
37
|
+
BLOCKED = "blocked" # Hard blocked, cannot proceed
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class OperationCheck:
|
|
42
|
+
"""Complete pre-execution check result."""
|
|
43
|
+
tool_name: str
|
|
44
|
+
arguments: dict[str, Any]
|
|
45
|
+
category: str = ""
|
|
46
|
+
allowed: bool = True
|
|
47
|
+
action: ActionRequired = ActionRequired.PROCEED
|
|
48
|
+
risk: RiskLevel = RiskLevel.SAFE
|
|
49
|
+
safety: SafetyCheck | None = None
|
|
50
|
+
warnings: list[str] = field(default_factory=list)
|
|
51
|
+
confirm_message: str = "" # What to show the user
|
|
52
|
+
confirm_phrase: str = "" # Phrase user must type (for TYPE_CONFIRM)
|
|
53
|
+
dry_run_preview: str = "" # What would happen in dry-run
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
57
|
+
# Fool-proof engine
|
|
58
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
59
|
+
|
|
60
|
+
class FoolProofEngine:
|
|
61
|
+
"""
|
|
62
|
+
Unified "fool-proof" engine that combines:
|
|
63
|
+
- Safety guards (pattern-based blocking)
|
|
64
|
+
- Permission system (user-configurable allow/deny)
|
|
65
|
+
- Change tracking (backup + undo)
|
|
66
|
+
- Dry-run preview
|
|
67
|
+
- Interactive confirmation
|
|
68
|
+
|
|
69
|
+
Usage:
|
|
70
|
+
engine = FoolProofEngine(workspace, permission_store, change_tracker)
|
|
71
|
+
|
|
72
|
+
# Before executing a tool:
|
|
73
|
+
check = engine.evaluate("run_shell", {"command": "rm file.txt"})
|
|
74
|
+
if check.action == ActionRequired.BLOCKED:
|
|
75
|
+
print(f"Blocked: {check.warnings}")
|
|
76
|
+
elif check.action in (ActionRequired.CONFIRM, ActionRequired.WARN_CONFIRM):
|
|
77
|
+
if ui.confirm(check):
|
|
78
|
+
execute_tool()
|
|
79
|
+
else:
|
|
80
|
+
print("Cancelled.")
|
|
81
|
+
|
|
82
|
+
# After executing:
|
|
83
|
+
engine.capture("write_file", {"file_path": "x.py"}, result)
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
def __init__(self,
|
|
87
|
+
workspace: str | Path,
|
|
88
|
+
permission_store: PermissionStore | None = None,
|
|
89
|
+
change_tracker: ChangeTracker | None = None,
|
|
90
|
+
safety_guard: SafetyGuard | None = None):
|
|
91
|
+
self.workspace = Path(workspace).resolve()
|
|
92
|
+
self.permissions = permission_store
|
|
93
|
+
self.tracker = change_tracker
|
|
94
|
+
self.guard = safety_guard or SafetyGuard(workspace)
|
|
95
|
+
|
|
96
|
+
# Stats
|
|
97
|
+
self._blocks = 0
|
|
98
|
+
self._confirmations = 0
|
|
99
|
+
self._dry_runs = 0
|
|
100
|
+
|
|
101
|
+
# ── Evaluate before execution ────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
def evaluate(self, tool_name: str, arguments: dict[str, Any]) -> OperationCheck:
|
|
104
|
+
"""
|
|
105
|
+
Evaluate a tool call BEFORE execution.
|
|
106
|
+
Returns an OperationCheck indicating whether and how to proceed.
|
|
107
|
+
"""
|
|
108
|
+
category = tool_category(tool_name)
|
|
109
|
+
|
|
110
|
+
check = OperationCheck(
|
|
111
|
+
tool_name=tool_name,
|
|
112
|
+
arguments=arguments,
|
|
113
|
+
category=category,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# 1. Safety guard check
|
|
117
|
+
safety = self._run_safety_check(tool_name, arguments)
|
|
118
|
+
check.safety = safety
|
|
119
|
+
check.risk = safety.risk
|
|
120
|
+
check.warnings = safety.warnings
|
|
121
|
+
|
|
122
|
+
if not safety.allowed:
|
|
123
|
+
check.allowed = False
|
|
124
|
+
check.action = ActionRequired.BLOCKED
|
|
125
|
+
check.confirm_message = safety.reason
|
|
126
|
+
self._blocks += 1
|
|
127
|
+
return check
|
|
128
|
+
|
|
129
|
+
# 2. Read tools — always safe
|
|
130
|
+
if category == "read":
|
|
131
|
+
check.allowed = True
|
|
132
|
+
check.action = ActionRequired.PROCEED
|
|
133
|
+
return check
|
|
134
|
+
|
|
135
|
+
# 3. For write/shell/mcp — check permissions + risk
|
|
136
|
+
# NOTE: safety.risk == CRITICAL is already caught by safety.allowed above;
|
|
137
|
+
# no need for a redundant check here.
|
|
138
|
+
if safety.risk == RiskLevel.DANGER:
|
|
139
|
+
# Check if user already allowed this category
|
|
140
|
+
if self.permissions:
|
|
141
|
+
cat_mode = self.permissions.get_category_mode(category)
|
|
142
|
+
if cat_mode == PermissionMode.ALLOW:
|
|
143
|
+
check.action = ActionRequired.PROCEED
|
|
144
|
+
elif cat_mode == PermissionMode.DENY:
|
|
145
|
+
check.action = ActionRequired.BLOCKED
|
|
146
|
+
check.allowed = False
|
|
147
|
+
else:
|
|
148
|
+
check.action = ActionRequired.WARN_CONFIRM
|
|
149
|
+
else:
|
|
150
|
+
check.action = ActionRequired.WARN_CONFIRM
|
|
151
|
+
|
|
152
|
+
check.confirm_message = self._format_danger_message(tool_name, arguments, safety)
|
|
153
|
+
|
|
154
|
+
elif safety.risk == RiskLevel.CAUTION:
|
|
155
|
+
if self.permissions:
|
|
156
|
+
cat_mode = self.permissions.get_category_mode(category)
|
|
157
|
+
if cat_mode == PermissionMode.ALLOW:
|
|
158
|
+
check.action = ActionRequired.PROCEED
|
|
159
|
+
elif cat_mode == PermissionMode.DENY:
|
|
160
|
+
check.action = ActionRequired.BLOCKED
|
|
161
|
+
check.allowed = False
|
|
162
|
+
else:
|
|
163
|
+
check.action = ActionRequired.CONFIRM
|
|
164
|
+
else:
|
|
165
|
+
check.action = ActionRequired.CONFIRM
|
|
166
|
+
|
|
167
|
+
check.confirm_message = self._format_caution_message(tool_name, arguments, safety)
|
|
168
|
+
|
|
169
|
+
else:
|
|
170
|
+
check.action = ActionRequired.PROCEED
|
|
171
|
+
|
|
172
|
+
# 4. Generate dry-run preview
|
|
173
|
+
if self.tracker and self.tracker.dry_run:
|
|
174
|
+
check.dry_run_preview = self._preview_dry_run(tool_name, arguments)
|
|
175
|
+
|
|
176
|
+
# Track statistics
|
|
177
|
+
if check.action in (ActionRequired.CONFIRM, ActionRequired.WARN_CONFIRM,
|
|
178
|
+
ActionRequired.TYPE_CONFIRM):
|
|
179
|
+
self._confirmations += 1
|
|
180
|
+
|
|
181
|
+
return check
|
|
182
|
+
|
|
183
|
+
# ── Capture after execution ──────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
def capture(self, tool_name: str, arguments: dict[str, Any],
|
|
186
|
+
result: Any, old_content: str = "") -> FileChange | None:
|
|
187
|
+
"""Record a completed operation in the change tracker."""
|
|
188
|
+
if not self.tracker:
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
if tool_name == "write_file":
|
|
192
|
+
file_path = arguments.get("file_path", "")
|
|
193
|
+
content = arguments.get("content", "")
|
|
194
|
+
if file_path:
|
|
195
|
+
return self.tracker.capture_write(file_path, content)
|
|
196
|
+
|
|
197
|
+
elif tool_name == "edit_file":
|
|
198
|
+
file_path = arguments.get("file_path", "")
|
|
199
|
+
if file_path and old_content:
|
|
200
|
+
old_str = arguments.get("old_string", "")
|
|
201
|
+
new_str = arguments.get("new_string", "")
|
|
202
|
+
if old_str:
|
|
203
|
+
# Reconstruct new content from edit args (avoids re-reading from disk)
|
|
204
|
+
new_content = old_content.replace(old_str, new_str, 1)
|
|
205
|
+
else:
|
|
206
|
+
# Fallback: read from disk (old_string not in args — test/legacy path)
|
|
207
|
+
try:
|
|
208
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
209
|
+
new_content = f.read()
|
|
210
|
+
except Exception:
|
|
211
|
+
new_content = old_content # keep old content if read fails
|
|
212
|
+
return self.tracker.capture_edit(file_path, old_content, new_content)
|
|
213
|
+
|
|
214
|
+
return None
|
|
215
|
+
|
|
216
|
+
# ── Safety check dispatch ────────────────────────────────────────────
|
|
217
|
+
|
|
218
|
+
def _run_safety_check(self, tool_name: str, arguments: dict) -> SafetyCheck:
|
|
219
|
+
if tool_name == "read_file":
|
|
220
|
+
return self.guard.check_read_file(arguments.get("file_path", ""))
|
|
221
|
+
elif tool_name == "write_file":
|
|
222
|
+
return self.guard.check_write_file(
|
|
223
|
+
arguments.get("file_path", ""),
|
|
224
|
+
arguments.get("content", ""),
|
|
225
|
+
)
|
|
226
|
+
elif tool_name == "edit_file":
|
|
227
|
+
return self.guard.check_edit_file(
|
|
228
|
+
arguments.get("file_path", ""),
|
|
229
|
+
arguments.get("old_string", ""),
|
|
230
|
+
arguments.get("new_string", ""),
|
|
231
|
+
)
|
|
232
|
+
elif tool_name == "run_shell":
|
|
233
|
+
return self.guard.check_shell(arguments.get("command", ""))
|
|
234
|
+
elif tool_name.startswith("mcp__"):
|
|
235
|
+
return SafetyCheck(allowed=True, risk=RiskLevel.CAUTION,
|
|
236
|
+
warnings=["MCP tool — verify on server side"])
|
|
237
|
+
else:
|
|
238
|
+
# Unknown tool — be conservative, not permissive.
|
|
239
|
+
# Defaulting to SAFE would silently skip safety checks for any
|
|
240
|
+
# newly-added tool that wasn't wired into this dispatch.
|
|
241
|
+
return SafetyCheck(allowed=True, risk=RiskLevel.CAUTION,
|
|
242
|
+
warnings=[f"Unknown tool type: {tool_name} — no safety rules defined"])
|
|
243
|
+
|
|
244
|
+
# ── Message formatting ───────────────────────────────────────────────
|
|
245
|
+
|
|
246
|
+
def _format_danger_message(self, tool_name: str, arguments: dict,
|
|
247
|
+
safety: SafetyCheck) -> str:
|
|
248
|
+
lines = [f"[DANGER] {tool_name}"]
|
|
249
|
+
if tool_name == "run_shell" and "command" in arguments:
|
|
250
|
+
lines.append(f" Command: {arguments['command'][:200]}")
|
|
251
|
+
elif "file_path" in arguments:
|
|
252
|
+
lines.append(f" File: {arguments['file_path']}")
|
|
253
|
+
for w in safety.warnings:
|
|
254
|
+
lines.append(f" ! {w}")
|
|
255
|
+
return "\n".join(lines)
|
|
256
|
+
|
|
257
|
+
def _format_caution_message(self, tool_name: str, arguments: dict,
|
|
258
|
+
safety: SafetyCheck) -> str:
|
|
259
|
+
lines = [f"{tool_name}"]
|
|
260
|
+
if "file_path" in arguments:
|
|
261
|
+
lines.append(f" File: {arguments['file_path']}")
|
|
262
|
+
elif "command" in arguments:
|
|
263
|
+
lines.append(f" Cmd: {arguments['command'][:120]}")
|
|
264
|
+
for w in safety.warnings:
|
|
265
|
+
lines.append(f" ! {w}")
|
|
266
|
+
return "\n".join(lines)
|
|
267
|
+
|
|
268
|
+
def _preview_dry_run(self, tool_name: str, arguments: dict) -> str:
|
|
269
|
+
"""Generate a dry-run preview string."""
|
|
270
|
+
if tool_name == "write_file":
|
|
271
|
+
fp = arguments.get("file_path", "")
|
|
272
|
+
content = arguments.get("content", "")
|
|
273
|
+
lines = content.count("\n") + 1
|
|
274
|
+
return f"Would WRITE {fp} ({lines} lines, {len(content)} bytes)"
|
|
275
|
+
elif tool_name == "edit_file":
|
|
276
|
+
fp = arguments.get("file_path", "")
|
|
277
|
+
old = arguments.get("old_string", "")
|
|
278
|
+
new = arguments.get("new_string", "")
|
|
279
|
+
return f"Would EDIT {fp}:\n - {old[:80]}\n + {new[:80]}"
|
|
280
|
+
elif tool_name == "run_shell":
|
|
281
|
+
return f"Would RUN: {arguments.get('command', '')[:200]}"
|
|
282
|
+
return ""
|
|
283
|
+
|
|
284
|
+
# ── Stats ────────────────────────────────────────────────────────────
|
|
285
|
+
|
|
286
|
+
@property
|
|
287
|
+
def stats(self) -> dict:
|
|
288
|
+
return {
|
|
289
|
+
"blocks": self._blocks,
|
|
290
|
+
"confirmations": self._confirmations,
|
|
291
|
+
"dry_runs": self._dry_runs,
|
|
292
|
+
"safety_guard": self.guard.stats,
|
|
293
|
+
"tracker_changes": self.tracker.count_active() if self.tracker else 0,
|
|
294
|
+
"tracker_total": self.tracker.count_all() if self.tracker else 0,
|
|
295
|
+
}
|