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
ata_coder/commands.py
ADDED
|
@@ -0,0 +1,974 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Slash command registry — replaces the 400+ line _handle_command function.
|
|
3
|
+
|
|
4
|
+
Each command is a small self-contained function registered with a decorator.
|
|
5
|
+
Keeps the main loop clean and each command independently testable.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import Any, Callable
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class CommandContext:
|
|
14
|
+
"""Typed context passed to every slash-command handler.
|
|
15
|
+
|
|
16
|
+
Supports both attribute access (``ctx.agent``) and dict-style access
|
|
17
|
+
(``ctx["agent"]``) for backward compatibility with existing handlers.
|
|
18
|
+
"""
|
|
19
|
+
agent: Any = None # CoderAgent instance
|
|
20
|
+
config: Any = None # AppConfig instance
|
|
21
|
+
ui: Any = None # ClaudeCodeUI instance
|
|
22
|
+
skill_mgr: Any = None # SkillManager
|
|
23
|
+
memory_store: Any = None # MemoryStore
|
|
24
|
+
session_mgr: Any = None # SessionManager
|
|
25
|
+
mcp_client: Any = None # MCPClient
|
|
26
|
+
template_mgr: Any = None # TemplateManager
|
|
27
|
+
permission_store: Any = None # PermissionStore
|
|
28
|
+
auto_skill_state: dict = field(default_factory=lambda: {"value": True})
|
|
29
|
+
|
|
30
|
+
def __getitem__(self, key: str) -> Any:
|
|
31
|
+
"""Dict-style access for backward compatibility."""
|
|
32
|
+
return getattr(self, key)
|
|
33
|
+
|
|
34
|
+
def __setitem__(self, key: str, value: Any) -> None:
|
|
35
|
+
"""Dict-style mutation for backward compatibility."""
|
|
36
|
+
setattr(self, key, value)
|
|
37
|
+
|
|
38
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
39
|
+
"""dict.get() compatibility."""
|
|
40
|
+
return getattr(self, key, default)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class Command:
|
|
45
|
+
name: str
|
|
46
|
+
handler: Callable[..., bool] # (arg: str, ctx: CommandContext) -> continue_running
|
|
47
|
+
help_text: str
|
|
48
|
+
category: str = "general"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class CommandRegistry:
|
|
52
|
+
"""Registry of slash commands with dispatch."""
|
|
53
|
+
|
|
54
|
+
def __init__(self):
|
|
55
|
+
self._commands: dict[str, Command] = {}
|
|
56
|
+
|
|
57
|
+
def register(self, name: str, help_text: str, category: str = "general"):
|
|
58
|
+
"""Decorator to register a command handler."""
|
|
59
|
+
def decorator(fn: Callable[..., bool]):
|
|
60
|
+
self._commands[name] = Command(name=name, handler=fn, help_text=help_text, category=category)
|
|
61
|
+
return fn
|
|
62
|
+
return decorator
|
|
63
|
+
|
|
64
|
+
async def dispatch(self, cmd: str, arg: str, ctx: dict) -> bool | None:
|
|
65
|
+
"""Dispatch a command. Returns: True=continue, False=quit, None=unknown.
|
|
66
|
+
|
|
67
|
+
Accepts a dict for backward compatibility; wraps it in a
|
|
68
|
+
CommandContext before passing to the handler.
|
|
69
|
+
|
|
70
|
+
Supports both sync and async command handlers.
|
|
71
|
+
"""
|
|
72
|
+
command = self._commands.get(cmd)
|
|
73
|
+
if command is None:
|
|
74
|
+
return None
|
|
75
|
+
# Wrap dict in typed context if not already
|
|
76
|
+
if isinstance(ctx, dict):
|
|
77
|
+
ctx = CommandContext(**{k: ctx.get(k) for k in CommandContext.__dataclass_fields__})
|
|
78
|
+
import asyncio
|
|
79
|
+
if asyncio.iscoroutinefunction(command.handler):
|
|
80
|
+
return await command.handler(arg, ctx)
|
|
81
|
+
return command.handler(arg, ctx)
|
|
82
|
+
|
|
83
|
+
def list_all(self) -> list[Command]:
|
|
84
|
+
return sorted(self._commands.values(), key=lambda c: c.name)
|
|
85
|
+
|
|
86
|
+
def list_by_category(self) -> dict[str, list[Command]]:
|
|
87
|
+
cats: dict[str, list[Command]] = {}
|
|
88
|
+
for c in self._commands.values():
|
|
89
|
+
cats.setdefault(c.category, []).append(c)
|
|
90
|
+
return cats
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
94
|
+
# Build the registry
|
|
95
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
96
|
+
|
|
97
|
+
def build_registry() -> CommandRegistry:
|
|
98
|
+
r = CommandRegistry()
|
|
99
|
+
|
|
100
|
+
# ── Basic ──────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
@r.register("/help", "Show help", "basic")
|
|
103
|
+
def cmd_help(arg: str, ctx: dict) -> bool:
|
|
104
|
+
ctx["ui"].show_help()
|
|
105
|
+
return True
|
|
106
|
+
|
|
107
|
+
@r.register("/quit", "Exit", "basic")
|
|
108
|
+
@r.register("/exit", "Exit", "basic")
|
|
109
|
+
@r.register("/q", "Exit", "basic")
|
|
110
|
+
def cmd_quit(arg: str, ctx: dict) -> bool:
|
|
111
|
+
print("Goodbye!")
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
@r.register("/clear", "Clear conversation", "basic")
|
|
115
|
+
def cmd_clear(arg: str, ctx: dict) -> bool:
|
|
116
|
+
ctx["agent"].reset()
|
|
117
|
+
print("Conversation cleared.")
|
|
118
|
+
return True
|
|
119
|
+
|
|
120
|
+
@r.register("/context", "Show token usage", "basic")
|
|
121
|
+
def cmd_context(arg: str, ctx: dict) -> bool:
|
|
122
|
+
agent = ctx["agent"]
|
|
123
|
+
tokens = agent.get_token_estimate()
|
|
124
|
+
ctx["ui"].show_context(
|
|
125
|
+
total_messages=len(agent._state.messages),
|
|
126
|
+
tool_calls=agent._state.tool_call_count,
|
|
127
|
+
skill=agent.skills.active_skill.name if agent.skills and agent.skills.active_skill else "default",
|
|
128
|
+
model=ctx["config"].llm.model,
|
|
129
|
+
estimated_tokens=tokens,
|
|
130
|
+
max_tokens=ctx["config"].agent.max_context_tokens,
|
|
131
|
+
)
|
|
132
|
+
return True
|
|
133
|
+
|
|
134
|
+
@r.register("/compact", "Compact conversation", "basic")
|
|
135
|
+
def cmd_compact(arg: str, ctx: dict) -> bool:
|
|
136
|
+
print(ctx["agent"].compact())
|
|
137
|
+
return True
|
|
138
|
+
|
|
139
|
+
@r.register("/cost", "Estimate cost", "basic")
|
|
140
|
+
def cmd_cost(arg: str, ctx: dict) -> bool:
|
|
141
|
+
from .model_registry import estimate_cost
|
|
142
|
+
agent = ctx["agent"]
|
|
143
|
+
tokens = agent.get_token_estimate()
|
|
144
|
+
model = ctx["config"].llm.model
|
|
145
|
+
cost = estimate_cost(tokens, model)
|
|
146
|
+
print(f"Estimated: ~${cost:.4f} (~{tokens:,} tokens, {model})")
|
|
147
|
+
return True
|
|
148
|
+
|
|
149
|
+
@r.register("/summary", "Conversation summary", "basic")
|
|
150
|
+
def cmd_summary(arg: str, ctx: dict) -> bool:
|
|
151
|
+
print(ctx["agent"].get_conversation_summary())
|
|
152
|
+
return True
|
|
153
|
+
|
|
154
|
+
# ── Skills ─────────────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
@r.register("/skills", "List skills", "skill")
|
|
157
|
+
def cmd_skills(arg: str, ctx: dict) -> bool:
|
|
158
|
+
sm = ctx.get("skill_mgr")
|
|
159
|
+
if not sm:
|
|
160
|
+
print("Skills not loaded.")
|
|
161
|
+
return True
|
|
162
|
+
for s in sm.list_skills():
|
|
163
|
+
marker = " [active]" if sm.active_skill and sm.active_skill.name == s.name else ""
|
|
164
|
+
print(f" {s.name}{marker}: {s.description[:80]}")
|
|
165
|
+
return True
|
|
166
|
+
|
|
167
|
+
@r.register("/skill", "Switch skill", "skill")
|
|
168
|
+
def cmd_skill(arg: str, ctx: dict) -> bool:
|
|
169
|
+
sm = ctx.get("skill_mgr")
|
|
170
|
+
if not sm:
|
|
171
|
+
print("Skills not loaded.")
|
|
172
|
+
return True
|
|
173
|
+
if arg:
|
|
174
|
+
s = sm.activate(arg)
|
|
175
|
+
print(f"Skill: {s.name}" if s else f"Not found: {arg}")
|
|
176
|
+
else:
|
|
177
|
+
a = sm.active_skill
|
|
178
|
+
print(f"Active: {a.name} - {a.description}" if a else "No active skill.")
|
|
179
|
+
return True
|
|
180
|
+
|
|
181
|
+
@r.register("/skill-auto", "Toggle skill auto-detect", "skill")
|
|
182
|
+
def cmd_skill_auto(arg: str, ctx: dict) -> bool:
|
|
183
|
+
if arg.lower() in ("off", "false", "0"):
|
|
184
|
+
ctx["auto_skill_state"]["value"] = False
|
|
185
|
+
print("Auto-skill: off")
|
|
186
|
+
else:
|
|
187
|
+
ctx["auto_skill_state"]["value"] = True
|
|
188
|
+
print("Auto-skill: on")
|
|
189
|
+
return True
|
|
190
|
+
|
|
191
|
+
# ── Memory ─────────────────────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
@r.register("/remember", "Save a memory", "memory")
|
|
194
|
+
def cmd_remember(arg: str, ctx: dict) -> bool:
|
|
195
|
+
store = ctx.get("memory_store")
|
|
196
|
+
if not store:
|
|
197
|
+
print("Memory not loaded.")
|
|
198
|
+
return True
|
|
199
|
+
parts = arg.split("|", 1)
|
|
200
|
+
if len(parts) < 2:
|
|
201
|
+
print("Usage: /remember type/name description | content")
|
|
202
|
+
return True
|
|
203
|
+
header = parts[0].strip()
|
|
204
|
+
content = parts[1].strip()
|
|
205
|
+
header_parts = header.split(maxsplit=1)
|
|
206
|
+
type_name = header_parts[0]
|
|
207
|
+
description = header_parts[1] if len(header_parts) > 1 else ""
|
|
208
|
+
|
|
209
|
+
if "/" in type_name:
|
|
210
|
+
mem_type, name = type_name.split("/", 1)
|
|
211
|
+
else:
|
|
212
|
+
mem_type, name = "reference", type_name
|
|
213
|
+
|
|
214
|
+
from .memory import Memory
|
|
215
|
+
store.add(Memory(name=name, description=description, content=content, metadata={"type": mem_type}))
|
|
216
|
+
print(f"Saved: [{mem_type}] {name}")
|
|
217
|
+
return True
|
|
218
|
+
|
|
219
|
+
@r.register("/recall", "Search memories", "memory")
|
|
220
|
+
def cmd_recall(arg: str, ctx: dict) -> bool:
|
|
221
|
+
store = ctx.get("memory_store")
|
|
222
|
+
if not store:
|
|
223
|
+
print("Memory not loaded.")
|
|
224
|
+
return True
|
|
225
|
+
if not arg:
|
|
226
|
+
print("Usage: /recall <query>")
|
|
227
|
+
return True
|
|
228
|
+
results = store.search(arg)
|
|
229
|
+
if not results:
|
|
230
|
+
print("No matches.")
|
|
231
|
+
return True
|
|
232
|
+
for m in results[:5]:
|
|
233
|
+
print(f"\n[{m.memory_type}] {m.description}\n{m.content[:300]}")
|
|
234
|
+
return True
|
|
235
|
+
|
|
236
|
+
@r.register("/memories", "List memories", "memory")
|
|
237
|
+
def cmd_memories(arg: str, ctx: dict) -> bool:
|
|
238
|
+
store = ctx.get("memory_store")
|
|
239
|
+
if not store:
|
|
240
|
+
print("Memory not loaded.")
|
|
241
|
+
return True
|
|
242
|
+
memories = store.list_all(arg if arg else None)
|
|
243
|
+
if not memories:
|
|
244
|
+
print("No memories.")
|
|
245
|
+
return True
|
|
246
|
+
for m in memories:
|
|
247
|
+
print(f" [{m.memory_type}] {m.name} - {m.description} ({str(m.updated)[:10]})")
|
|
248
|
+
return True
|
|
249
|
+
|
|
250
|
+
@r.register("/forget", "Delete a memory", "memory")
|
|
251
|
+
def cmd_forget(arg: str, ctx: dict) -> bool:
|
|
252
|
+
store = ctx.get("memory_store")
|
|
253
|
+
if not store:
|
|
254
|
+
print("Memory not loaded.")
|
|
255
|
+
return True
|
|
256
|
+
if not arg:
|
|
257
|
+
print("Usage: /forget <name>")
|
|
258
|
+
return True
|
|
259
|
+
ok = store.delete(arg)
|
|
260
|
+
print(f"Deleted: {arg}" if ok else f"Not found: {arg}")
|
|
261
|
+
return True
|
|
262
|
+
|
|
263
|
+
# ── Safety ─────────────────────────────────────────────────────────
|
|
264
|
+
|
|
265
|
+
@r.register("/undo", "Undo changes", "safety")
|
|
266
|
+
def cmd_undo(arg: str, ctx: dict) -> bool:
|
|
267
|
+
agent = ctx["agent"]
|
|
268
|
+
if arg.lower() == "all":
|
|
269
|
+
print(agent.undo_all())
|
|
270
|
+
else:
|
|
271
|
+
try:
|
|
272
|
+
n = int(arg) if arg else 1
|
|
273
|
+
except ValueError:
|
|
274
|
+
n = 1
|
|
275
|
+
print(agent.undo(n))
|
|
276
|
+
return True
|
|
277
|
+
|
|
278
|
+
@r.register("/redo", "Re-apply reverted change", "safety")
|
|
279
|
+
def cmd_redo(arg: str, ctx: dict) -> bool:
|
|
280
|
+
try:
|
|
281
|
+
n = int(arg) if arg else 1
|
|
282
|
+
except ValueError:
|
|
283
|
+
print("Usage: /redo <change-id>")
|
|
284
|
+
return True
|
|
285
|
+
print(ctx["agent"].restore_change(n))
|
|
286
|
+
return True
|
|
287
|
+
|
|
288
|
+
@r.register("/changes", "List file changes", "safety")
|
|
289
|
+
def cmd_changes(arg: str, ctx: dict) -> bool:
|
|
290
|
+
print(ctx["agent"].list_changes())
|
|
291
|
+
return True
|
|
292
|
+
|
|
293
|
+
@r.register("/diff-changes", "Show change diffs", "safety")
|
|
294
|
+
def cmd_diff_changes(arg: str, ctx: dict) -> bool:
|
|
295
|
+
try:
|
|
296
|
+
n = int(arg) if arg else 3
|
|
297
|
+
except ValueError:
|
|
298
|
+
n = 3
|
|
299
|
+
print(ctx["agent"].show_change_diff(n))
|
|
300
|
+
return True
|
|
301
|
+
|
|
302
|
+
@r.register("/dry-run", "Toggle dry-run mode", "safety")
|
|
303
|
+
def cmd_dry_run(arg: str, ctx: dict) -> bool:
|
|
304
|
+
enable = None if not arg else arg.lower() in ("on", "true", "1", "yes")
|
|
305
|
+
print(ctx["agent"].toggle_dry_run(enable))
|
|
306
|
+
return True
|
|
307
|
+
|
|
308
|
+
@r.register("/stats", "Safety stats", "safety")
|
|
309
|
+
def cmd_stats(arg: str, ctx: dict) -> bool:
|
|
310
|
+
a = ctx["agent"]
|
|
311
|
+
if a.fool_proof:
|
|
312
|
+
s = a.fool_proof.stats
|
|
313
|
+
print(f"Blocks: {s['blocks']} Confirmations: {s['confirmations']} "
|
|
314
|
+
f"Changes: {s['tracker_changes']} active "
|
|
315
|
+
f"Dry-run: {'ON' if a.change_tracker and a.change_tracker.dry_run else 'OFF'}")
|
|
316
|
+
return True
|
|
317
|
+
|
|
318
|
+
# ── Dangerous mode ─────────────────────────────────────────────────
|
|
319
|
+
|
|
320
|
+
@r.register("/dangerous", "Dangerous mode", "danger")
|
|
321
|
+
def cmd_dangerous(arg: str, ctx: dict) -> bool:
|
|
322
|
+
pm = ctx["agent"].privilege_mgr
|
|
323
|
+
if not pm:
|
|
324
|
+
print("Not available.")
|
|
325
|
+
return True
|
|
326
|
+
al = arg.lower()
|
|
327
|
+
if al in ("on", "enable", "1", "yes"):
|
|
328
|
+
print(pm.enable_dangerous_mode("user-command", timeout_minutes=15))
|
|
329
|
+
elif al in ("off", "disable", "0", "no"):
|
|
330
|
+
print(pm.disable_dangerous_mode())
|
|
331
|
+
elif al == "audit":
|
|
332
|
+
print(pm.get_audit_log())
|
|
333
|
+
elif al == "elevate":
|
|
334
|
+
print(pm.get_elevation_instructions())
|
|
335
|
+
else:
|
|
336
|
+
print(pm.status())
|
|
337
|
+
return True
|
|
338
|
+
|
|
339
|
+
@r.register("/elevate", "Elevation guide", "danger")
|
|
340
|
+
def cmd_elevate(arg: str, ctx: dict) -> bool:
|
|
341
|
+
pm = ctx["agent"].privilege_mgr
|
|
342
|
+
print(pm.get_elevation_instructions() if pm else "Not available.")
|
|
343
|
+
return True
|
|
344
|
+
|
|
345
|
+
# ── Think ──────────────────────────────────────────────────────────
|
|
346
|
+
|
|
347
|
+
@r.register("/think", "Thinking mode", "settings")
|
|
348
|
+
def cmd_think(arg: str, ctx: dict) -> bool:
|
|
349
|
+
cfg = ctx["config"]
|
|
350
|
+
strengths = ["off", "low", "medium", "high", "xhigh", "max"]
|
|
351
|
+
if not arg:
|
|
352
|
+
current = cfg.llm.thinking_strength or "off"
|
|
353
|
+
print(f"Thinking: {current} ({' | '.join(strengths)})")
|
|
354
|
+
elif arg.lower() == "off":
|
|
355
|
+
cfg.llm.thinking_strength = ""
|
|
356
|
+
print("Thinking: OFF")
|
|
357
|
+
elif arg.lower() in strengths:
|
|
358
|
+
cfg.llm.thinking_strength = arg.lower()
|
|
359
|
+
print(f"Thinking: {arg.upper()}")
|
|
360
|
+
else:
|
|
361
|
+
print(f"Invalid. Choose: {' | '.join(strengths)}")
|
|
362
|
+
return True
|
|
363
|
+
|
|
364
|
+
# ── Settings ───────────────────────────────────────────────────────
|
|
365
|
+
|
|
366
|
+
@r.register("/model", "Change model", "settings")
|
|
367
|
+
def cmd_model(arg: str, ctx: dict) -> bool:
|
|
368
|
+
agent = ctx["agent"]
|
|
369
|
+
if not arg:
|
|
370
|
+
print(f"Model: {agent.llm.config.model}")
|
|
371
|
+
return True
|
|
372
|
+
agent.llm.set_model(arg)
|
|
373
|
+
agent.llm.register_tools(agent._all_tools)
|
|
374
|
+
print(f"Model: {arg}")
|
|
375
|
+
return True
|
|
376
|
+
|
|
377
|
+
@r.register("/effort", "Set effort: low/medium/high/xhigh/max", "settings")
|
|
378
|
+
def cmd_effort(arg: str, ctx: dict) -> bool:
|
|
379
|
+
valid = {"low", "medium", "high", "xhigh", "max"}
|
|
380
|
+
if not arg or arg.lower() not in valid:
|
|
381
|
+
current = getattr(ctx.get("config", None), "effort", "medium")
|
|
382
|
+
print(f"Effort: {current} (low / medium / high / xhigh / max)")
|
|
383
|
+
print(f" low = haiku, 4K tokens, thinking disabled")
|
|
384
|
+
print(f" medium = default, 16K tokens, no thinking")
|
|
385
|
+
print(f" high = default, 32K tokens, reasoning_effort=high")
|
|
386
|
+
print(f" xhigh = opus, 48K tokens, reasoning_effort=xhigh")
|
|
387
|
+
print(f" max = opus, 64K tokens, reasoning_effort=max")
|
|
388
|
+
return True
|
|
389
|
+
level = arg.lower()
|
|
390
|
+
ctx["config"].effort = level
|
|
391
|
+
agent = ctx["agent"]
|
|
392
|
+
if level == "low":
|
|
393
|
+
agent.llm.config.max_tokens = 4096
|
|
394
|
+
agent.llm.config.thinking_strength = "off"
|
|
395
|
+
elif level == "medium":
|
|
396
|
+
agent.llm.config.max_tokens = 16384
|
|
397
|
+
agent.llm.config.thinking_strength = ""
|
|
398
|
+
elif level == "high":
|
|
399
|
+
agent.llm.config.max_tokens = 32768
|
|
400
|
+
agent.llm.config.thinking_strength = "high"
|
|
401
|
+
elif level == "xhigh":
|
|
402
|
+
agent.llm.config.max_tokens = 49152
|
|
403
|
+
agent.llm.config.thinking_strength = "xhigh"
|
|
404
|
+
elif level == "max":
|
|
405
|
+
agent.llm.config.max_tokens = 65536
|
|
406
|
+
agent.llm.config.thinking_strength = "max"
|
|
407
|
+
print(f"Effort: {level}")
|
|
408
|
+
return True
|
|
409
|
+
|
|
410
|
+
@r.register("/models", "List models from API", "settings")
|
|
411
|
+
def cmd_models(arg: str, ctx: dict) -> bool:
|
|
412
|
+
from .model_registry import fetch_available_models
|
|
413
|
+
cfg = ctx["config"]
|
|
414
|
+
models = fetch_available_models(cfg.llm.base_url, cfg.llm.api_key)
|
|
415
|
+
if not models:
|
|
416
|
+
print("Failed to fetch models.")
|
|
417
|
+
return True
|
|
418
|
+
current = cfg.llm.model
|
|
419
|
+
print(f"\n{len(models)} model(s) (current: {current}):")
|
|
420
|
+
for mid in sorted(models):
|
|
421
|
+
print(f" {mid}{' << current' if mid == current else ''}")
|
|
422
|
+
return True
|
|
423
|
+
|
|
424
|
+
@r.register("/workspace", "Change workspace", "settings")
|
|
425
|
+
def cmd_workspace(arg: str, ctx: dict) -> bool:
|
|
426
|
+
from pathlib import Path
|
|
427
|
+
import os
|
|
428
|
+
|
|
429
|
+
cfg = ctx["config"]
|
|
430
|
+
agent = ctx["agent"]
|
|
431
|
+
|
|
432
|
+
if not arg:
|
|
433
|
+
print(f"Workspace: {cfg.agent.workspace_dir}")
|
|
434
|
+
return True
|
|
435
|
+
|
|
436
|
+
new_path = os.path.abspath(os.path.expanduser(arg))
|
|
437
|
+
if not os.path.isdir(new_path):
|
|
438
|
+
print(f"Not found: {arg}")
|
|
439
|
+
return True
|
|
440
|
+
|
|
441
|
+
cfg.agent.workspace_dir = new_path
|
|
442
|
+
agent.tools.workspace = Path(new_path)
|
|
443
|
+
agent.tools.config.workspace_dir = new_path
|
|
444
|
+
print(f"Workspace: {new_path}")
|
|
445
|
+
return True
|
|
446
|
+
|
|
447
|
+
@r.register("/permissions", "Permission rules", "settings")
|
|
448
|
+
def cmd_permissions(arg: str, ctx: dict) -> bool:
|
|
449
|
+
ps = ctx.get("permission_store")
|
|
450
|
+
print(ps.describe() if ps else "Not loaded.")
|
|
451
|
+
return True
|
|
452
|
+
|
|
453
|
+
@r.register("/mcp", "MCP status", "settings")
|
|
454
|
+
def cmd_mcp(arg: str, ctx: dict) -> bool:
|
|
455
|
+
mcp = ctx.get("mcp_client")
|
|
456
|
+
if not mcp:
|
|
457
|
+
print("MCP not configured.")
|
|
458
|
+
return True
|
|
459
|
+
for name in mcp.connected_servers:
|
|
460
|
+
count = sum(
|
|
461
|
+
1 for t in mcp.get_tools()
|
|
462
|
+
if t["function"]["name"].startswith(f"mcp__{name}__")
|
|
463
|
+
)
|
|
464
|
+
print(f" {name}: {count} tools")
|
|
465
|
+
return True
|
|
466
|
+
|
|
467
|
+
@r.register("/mcp-tools", "List MCP tools", "settings")
|
|
468
|
+
def cmd_mcp_tools(arg: str, ctx: dict) -> bool:
|
|
469
|
+
mcp = ctx.get("mcp_client")
|
|
470
|
+
if not mcp:
|
|
471
|
+
print("MCP not configured.")
|
|
472
|
+
return True
|
|
473
|
+
for t in mcp.get_tools():
|
|
474
|
+
fn = t["function"]
|
|
475
|
+
print(f" {fn['name']}: {fn['description'][:100]}")
|
|
476
|
+
return True
|
|
477
|
+
|
|
478
|
+
@r.register("/mcp search", "Search MCP tools/resources", "settings")
|
|
479
|
+
def cmd_mcp_search(arg: str, ctx: dict) -> bool:
|
|
480
|
+
"""Search MCP tools and resources by keyword. Usage: /mcp search <keyword>"""
|
|
481
|
+
mcp = ctx.get("mcp_client")
|
|
482
|
+
if not mcp:
|
|
483
|
+
print("MCP not configured.")
|
|
484
|
+
return True
|
|
485
|
+
if not arg:
|
|
486
|
+
print("Usage: /mcp search <keyword>")
|
|
487
|
+
print(" Searches tool names, descriptions, and resource URIs.")
|
|
488
|
+
return True
|
|
489
|
+
|
|
490
|
+
# Search tools
|
|
491
|
+
tools = mcp.search_tools(arg, limit=15)
|
|
492
|
+
if tools:
|
|
493
|
+
print(f"\n Tools matching '{arg}' ({len(tools)}):")
|
|
494
|
+
for t in tools:
|
|
495
|
+
name = t.get("name", "?")
|
|
496
|
+
desc = (t.get("description") or "")[:80]
|
|
497
|
+
server = t.get("_mcp_server", "?")
|
|
498
|
+
print(f" \033[1m{name}\033[0m @{server}")
|
|
499
|
+
if desc:
|
|
500
|
+
print(f" {desc}")
|
|
501
|
+
|
|
502
|
+
# Search resources
|
|
503
|
+
resources = mcp.search_resources(arg, limit=15)
|
|
504
|
+
if resources:
|
|
505
|
+
print(f"\n Resources matching '{arg}' ({len(resources)}):")
|
|
506
|
+
for r in resources:
|
|
507
|
+
uri = r.get("uri", "?")
|
|
508
|
+
name = r.get("name", "")
|
|
509
|
+
server = r.get("_mcp_server", "?")
|
|
510
|
+
label = f"{name} ({uri})" if name else uri
|
|
511
|
+
print(f" \033[1m{label}\033[0m @{server}")
|
|
512
|
+
|
|
513
|
+
if not tools and not resources:
|
|
514
|
+
print(f" No tools or resources found matching '{arg}'.")
|
|
515
|
+
return True
|
|
516
|
+
|
|
517
|
+
@r.register("/mcp resources", "List/search MCP resources", "settings")
|
|
518
|
+
def cmd_mcp_resources(arg: str, ctx: dict) -> bool:
|
|
519
|
+
"""List or search MCP resources. Usage: /mcp resources [keyword]"""
|
|
520
|
+
mcp = ctx.get("mcp_client")
|
|
521
|
+
if not mcp:
|
|
522
|
+
print("MCP not configured.")
|
|
523
|
+
return True
|
|
524
|
+
|
|
525
|
+
if arg:
|
|
526
|
+
resources = mcp.search_resources(arg)
|
|
527
|
+
label = f"matching '{arg}'"
|
|
528
|
+
else:
|
|
529
|
+
resources = mcp.get_all_resources()
|
|
530
|
+
label = "available"
|
|
531
|
+
|
|
532
|
+
if not resources:
|
|
533
|
+
print(f" No resources {label}.")
|
|
534
|
+
return True
|
|
535
|
+
|
|
536
|
+
print(f"\n MCP resources {label} ({len(resources)}):")
|
|
537
|
+
for r in resources:
|
|
538
|
+
uri = r.get("uri", "?")
|
|
539
|
+
name = r.get("name", "")
|
|
540
|
+
desc = (r.get("description") or "")[:80]
|
|
541
|
+
server = r.get("_mcp_server", "?")
|
|
542
|
+
display = name or uri
|
|
543
|
+
print(f" \033[1m{display}\033[0m @{server}")
|
|
544
|
+
if desc:
|
|
545
|
+
print(f" {desc}")
|
|
546
|
+
if name:
|
|
547
|
+
print(f" uri: {uri}")
|
|
548
|
+
return True
|
|
549
|
+
|
|
550
|
+
@r.register("/templates", "List templates", "settings")
|
|
551
|
+
def cmd_templates(arg: str, ctx: dict) -> bool:
|
|
552
|
+
tm = ctx.get("template_mgr")
|
|
553
|
+
if not tm:
|
|
554
|
+
print("Not loaded.")
|
|
555
|
+
return True
|
|
556
|
+
for t in tm.list_templates():
|
|
557
|
+
print(f" {t}")
|
|
558
|
+
return True
|
|
559
|
+
|
|
560
|
+
@r.register("/template", "Render template", "settings")
|
|
561
|
+
def cmd_template(arg: str, ctx: dict) -> bool:
|
|
562
|
+
tm = ctx.get("template_mgr")
|
|
563
|
+
if not tm:
|
|
564
|
+
print("Not loaded.")
|
|
565
|
+
return True
|
|
566
|
+
if not arg:
|
|
567
|
+
print("Usage: /template <name>")
|
|
568
|
+
return True
|
|
569
|
+
r = tm.render(arg)
|
|
570
|
+
print(r if r else f"Not found: {arg}")
|
|
571
|
+
return True
|
|
572
|
+
|
|
573
|
+
# ── Sessions ───────────────────────────────────────────────────────
|
|
574
|
+
|
|
575
|
+
@r.register("/save", "Save session", "session")
|
|
576
|
+
def cmd_save(arg: str, ctx: dict) -> bool:
|
|
577
|
+
print(f"Saved: {ctx['agent'].save_session(arg)}")
|
|
578
|
+
return True
|
|
579
|
+
|
|
580
|
+
@r.register("/sessions", "List all sessions", "session")
|
|
581
|
+
@r.register("/history", "Search/browse history", "session")
|
|
582
|
+
def cmd_history(arg: str, ctx: dict) -> bool:
|
|
583
|
+
sm = ctx.get("session_mgr")
|
|
584
|
+
if not sm:
|
|
585
|
+
print("Session manager not available.")
|
|
586
|
+
return True
|
|
587
|
+
|
|
588
|
+
# Get current workspace for filtering
|
|
589
|
+
agent = ctx["agent"]
|
|
590
|
+
ws = getattr(agent.tools, "workspace", None)
|
|
591
|
+
workspace = str(ws) if ws else None
|
|
592
|
+
|
|
593
|
+
if arg:
|
|
594
|
+
# Try to resume by index number
|
|
595
|
+
if arg.isdigit():
|
|
596
|
+
sessions = sm.list_sessions(limit=50, workspace=workspace)
|
|
597
|
+
idx = int(arg) - 1
|
|
598
|
+
if 0 <= idx < len(sessions):
|
|
599
|
+
meta = sessions[idx]
|
|
600
|
+
msgs = sm.load(meta.id)
|
|
601
|
+
if msgs:
|
|
602
|
+
agent._state.messages = msgs
|
|
603
|
+
print(f"Resumed: {meta.id}")
|
|
604
|
+
print(f" {meta.summary[:100]}")
|
|
605
|
+
print(f" Messages: {len(msgs)}, Tools: {meta.tool_call_count}")
|
|
606
|
+
return True
|
|
607
|
+
print(f"No session at index {arg} (found {len(sessions)} sessions)")
|
|
608
|
+
return True
|
|
609
|
+
|
|
610
|
+
# Try to resume by session ID
|
|
611
|
+
msgs = sm.load(arg)
|
|
612
|
+
if msgs:
|
|
613
|
+
agent._state.messages = msgs
|
|
614
|
+
meta = sm.get_meta(arg)
|
|
615
|
+
print(f"Resumed: {arg} ({len(msgs)} msgs)")
|
|
616
|
+
if meta:
|
|
617
|
+
print(f" {meta.summary[:100]}")
|
|
618
|
+
else:
|
|
619
|
+
# Search by keyword
|
|
620
|
+
results = sm.search_sessions(arg, workspace=workspace)
|
|
621
|
+
if results:
|
|
622
|
+
print(f"Search '{arg}': {len(results)} matches")
|
|
623
|
+
for i, meta in enumerate(results[:10], 1):
|
|
624
|
+
date = meta.created[:10] if meta.created else "?"
|
|
625
|
+
print(f" [{i}] {date} | {meta.skill:15s} | {meta.summary[:60]}")
|
|
626
|
+
else:
|
|
627
|
+
print(f"No matches for: {arg}")
|
|
628
|
+
return True
|
|
629
|
+
|
|
630
|
+
# No args — list recent sessions for this workspace
|
|
631
|
+
sessions = sm.list_sessions(limit=20, workspace=workspace)
|
|
632
|
+
ws_name = Path(workspace).name if workspace else "all"
|
|
633
|
+
|
|
634
|
+
if not sessions:
|
|
635
|
+
print(f"No sessions for workspace: {ws_name}")
|
|
636
|
+
print("Try /history <keyword> to search all sessions.")
|
|
637
|
+
return True
|
|
638
|
+
|
|
639
|
+
print(f"History ({ws_name}/):")
|
|
640
|
+
for i, meta in enumerate(sessions, 1):
|
|
641
|
+
date = meta.created[:10] if meta.created else "?"
|
|
642
|
+
icon = {"general-coder": "💻", "debugger": "🐛", "code-reviewer": "🔍",
|
|
643
|
+
"architect": "🏗️", "test-writer": "🧪"}.get(meta.skill, "📝")
|
|
644
|
+
print(f" [{i}] {icon} {date} | {meta.skill:15s} | {meta.summary[:60]}")
|
|
645
|
+
if meta.tool_call_count:
|
|
646
|
+
print(f" {meta.message_count} msgs, {meta.tool_call_count} tools")
|
|
647
|
+
print(f"\n/history <number> to resume, /history <keyword> to search")
|
|
648
|
+
return True
|
|
649
|
+
|
|
650
|
+
@r.register("/resume", "Resume session by ID", "session")
|
|
651
|
+
def cmd_resume(arg: str, ctx: dict) -> bool:
|
|
652
|
+
sm = ctx.get("session_mgr")
|
|
653
|
+
if not sm or not arg:
|
|
654
|
+
print("Usage: /resume <id>")
|
|
655
|
+
return True
|
|
656
|
+
msgs = sm.load(arg)
|
|
657
|
+
if msgs:
|
|
658
|
+
ctx["agent"]._state.messages = msgs
|
|
659
|
+
print(f"Resumed: {arg} ({len(msgs)} msgs)")
|
|
660
|
+
else:
|
|
661
|
+
print(f"Not found: {arg}")
|
|
662
|
+
return True
|
|
663
|
+
|
|
664
|
+
@r.register("/export", "Export session", "session")
|
|
665
|
+
def cmd_export(arg: str, ctx: dict) -> bool:
|
|
666
|
+
sm = ctx.get("session_mgr")
|
|
667
|
+
if not sm or not arg:
|
|
668
|
+
print("Usage: /export <id> [path]")
|
|
669
|
+
return True
|
|
670
|
+
parts = arg.split(maxsplit=1)
|
|
671
|
+
sid = parts[0]
|
|
672
|
+
out = parts[1] if len(parts) > 1 else None
|
|
673
|
+
md = sm.export_markdown(sid, out)
|
|
674
|
+
if md:
|
|
675
|
+
print(f"Exported {sid}" + (f" to {out}" if out else ""))
|
|
676
|
+
else:
|
|
677
|
+
print(f"Not found: {sid}")
|
|
678
|
+
return True
|
|
679
|
+
|
|
680
|
+
# ── Git ────────────────────────────────────────────────────────────
|
|
681
|
+
|
|
682
|
+
@r.register("/git", "Git operations", "git")
|
|
683
|
+
def cmd_git(arg: str, ctx: dict) -> bool:
|
|
684
|
+
git = ctx["agent"].git
|
|
685
|
+
if not git:
|
|
686
|
+
print("Not available.")
|
|
687
|
+
return True
|
|
688
|
+
if arg == "status" or not arg:
|
|
689
|
+
s = git.get_status()
|
|
690
|
+
print(f"Branch: {s.branch}\nStatus: {s.summary()}")
|
|
691
|
+
elif arg == "diff":
|
|
692
|
+
print(git.get_diff())
|
|
693
|
+
elif arg == "log":
|
|
694
|
+
print(git.get_log())
|
|
695
|
+
elif arg.startswith("commit"):
|
|
696
|
+
ok, out = git.commit(arg[6:].strip())
|
|
697
|
+
print(out)
|
|
698
|
+
elif arg.startswith("branch "):
|
|
699
|
+
ok, out = git.create_branch(arg[7:].strip())
|
|
700
|
+
print(out)
|
|
701
|
+
elif arg == "branch" or arg == "branches":
|
|
702
|
+
print(git.list_branches())
|
|
703
|
+
elif arg == "undo":
|
|
704
|
+
ok, out = git.undo_commit()
|
|
705
|
+
print(out)
|
|
706
|
+
elif arg == "stash":
|
|
707
|
+
git.stash()
|
|
708
|
+
print("Stashed.")
|
|
709
|
+
elif arg == "unstash":
|
|
710
|
+
git.stash_pop()
|
|
711
|
+
print("Unstashed.")
|
|
712
|
+
elif arg == "summary":
|
|
713
|
+
print(git.session_summary())
|
|
714
|
+
else:
|
|
715
|
+
print("/git [status|diff|log|commit|branch|undo|branches|stash|unstash|summary]")
|
|
716
|
+
return True
|
|
717
|
+
|
|
718
|
+
# ── Review & Fix ────────────────────────────────────────────────────
|
|
719
|
+
|
|
720
|
+
@r.register("/review", "AI code review of current changes", "review")
|
|
721
|
+
async def cmd_review(arg: str, ctx: dict) -> bool:
|
|
722
|
+
agent = ctx["agent"]
|
|
723
|
+
git = agent.git
|
|
724
|
+
diff_text = git.get_diff() if git else "(no git repo)"
|
|
725
|
+
if not diff_text or diff_text == "(no changes)":
|
|
726
|
+
print("No changes to review.")
|
|
727
|
+
return True
|
|
728
|
+
task = (
|
|
729
|
+
"Review the following code changes. Output a structured report:\n"
|
|
730
|
+
"## Issues Found\n"
|
|
731
|
+
"For each: severity (critical/high/medium/low), file, line, problem, fix\n\n"
|
|
732
|
+
f"```diff\n{diff_text[:8000]}\n```"
|
|
733
|
+
)
|
|
734
|
+
print("Reviewing changes...\n")
|
|
735
|
+
await agent.run(task, stream=True)
|
|
736
|
+
return True
|
|
737
|
+
|
|
738
|
+
@r.register("/fix", "AI apply review suggestions", "review")
|
|
739
|
+
async def cmd_fix(arg: str, ctx: dict) -> bool:
|
|
740
|
+
agent = ctx["agent"]
|
|
741
|
+
git = agent.git
|
|
742
|
+
diff_text = git.get_diff() if git else "(no git repo)"
|
|
743
|
+
if not diff_text or diff_text == "(no changes)":
|
|
744
|
+
print("No changes to fix.")
|
|
745
|
+
return True
|
|
746
|
+
severity = arg if arg else "all"
|
|
747
|
+
task = (
|
|
748
|
+
f"Review this diff and fix issues. Focus on {severity} severity issues.\n"
|
|
749
|
+
"Apply the fixes directly to the files.\n\n"
|
|
750
|
+
f"```diff\n{diff_text[:8000]}\n```"
|
|
751
|
+
)
|
|
752
|
+
print(f"Fixing {severity} issues...\n")
|
|
753
|
+
await agent.run(task, stream=True)
|
|
754
|
+
return True
|
|
755
|
+
|
|
756
|
+
# ── Planner ────────────────────────────────────────────────────────
|
|
757
|
+
|
|
758
|
+
@r.register("/plan", "Task plan", "plan")
|
|
759
|
+
def cmd_plan(arg: str, ctx: dict) -> bool:
|
|
760
|
+
p = ctx["agent"].planner
|
|
761
|
+
if arg:
|
|
762
|
+
agent = ctx["agent"]
|
|
763
|
+
plan = p.decompose(arg, llm_client=agent.llm)
|
|
764
|
+
print(plan.to_prompt())
|
|
765
|
+
elif p.current_plan:
|
|
766
|
+
print(p.current_plan.to_prompt())
|
|
767
|
+
else:
|
|
768
|
+
print("Usage: /plan <task>")
|
|
769
|
+
return True
|
|
770
|
+
|
|
771
|
+
@r.register("/tasks", "List plan tasks", "plan")
|
|
772
|
+
def cmd_tasks(arg: str, ctx: dict) -> bool:
|
|
773
|
+
p = ctx["agent"].planner
|
|
774
|
+
if not p.current_plan:
|
|
775
|
+
print("No active plan.")
|
|
776
|
+
return True
|
|
777
|
+
print(p.current_plan.progress_bar())
|
|
778
|
+
icons = {"pending": "[ ]", "in_progress": "[>]", "completed": "[x]", "failed": "[!]", "skipped": "[-]"}
|
|
779
|
+
for t in p.current_plan.subtasks:
|
|
780
|
+
print(f" {icons.get(t.status.value, '[?]')} #{t.id} {t.subject}")
|
|
781
|
+
return True
|
|
782
|
+
|
|
783
|
+
@r.register("/plan-next", "Start next task", "plan")
|
|
784
|
+
def cmd_plan_next(arg: str, ctx: dict) -> bool:
|
|
785
|
+
t = ctx["agent"].planner.auto_advance()
|
|
786
|
+
print(f"Starting: #{t.id} {t.subject}" if t else "No pending tasks.")
|
|
787
|
+
return True
|
|
788
|
+
|
|
789
|
+
@r.register("/plan-done", "Complete task", "plan")
|
|
790
|
+
def cmd_plan_done(arg: str, ctx: dict) -> bool:
|
|
791
|
+
p = ctx["agent"].planner
|
|
792
|
+
try: tid = int(arg) if arg else 0
|
|
793
|
+
except ValueError: tid = 0
|
|
794
|
+
if tid and p.current_plan:
|
|
795
|
+
t = p.complete_task(tid)
|
|
796
|
+
elif p.current_plan and p.current_plan.current:
|
|
797
|
+
t = p.complete_task(p.current_plan.current.id)
|
|
798
|
+
else:
|
|
799
|
+
print("No task to complete.")
|
|
800
|
+
return True
|
|
801
|
+
print(f"Completed: #{t.id} {t.subject}" if t else "Failed.")
|
|
802
|
+
return True
|
|
803
|
+
|
|
804
|
+
@r.register("/retry", "Self-correct stats", "plan")
|
|
805
|
+
def cmd_retry(arg: str, ctx: dict) -> bool:
|
|
806
|
+
sc = ctx["agent"].self_correct
|
|
807
|
+
s = sc.stats
|
|
808
|
+
print(f"Retries: {s['total_retries']} Successful: {s['successful_retries']} "
|
|
809
|
+
f"Auto-fix rate: {s['auto_fix_rate']}")
|
|
810
|
+
return True
|
|
811
|
+
|
|
812
|
+
# ── Extensions ─────────────────────────────────────────────────────
|
|
813
|
+
|
|
814
|
+
@r.register("/extensions", "List extensions", "extension")
|
|
815
|
+
def cmd_extensions(arg: str, ctx: dict) -> bool:
|
|
816
|
+
agent = ctx["agent"]
|
|
817
|
+
if not hasattr(agent, "ext_mgr") or not agent.ext_mgr:
|
|
818
|
+
print("Extension manager not available.")
|
|
819
|
+
return True
|
|
820
|
+
active_names = {e.meta.name for e in agent.ext_mgr.list_active()}
|
|
821
|
+
for ext in agent.ext_mgr.list_extensions():
|
|
822
|
+
status = "[active]" if ext.meta.name in active_names else "[loaded]"
|
|
823
|
+
print(f" {status} {ext.meta.name} v{ext.meta.version} — {ext.meta.description[:60]}")
|
|
824
|
+
return True
|
|
825
|
+
|
|
826
|
+
@r.register("/ext-activate", "Activate extension", "extension")
|
|
827
|
+
def cmd_ext_activate(arg: str, ctx: dict) -> bool:
|
|
828
|
+
if not arg:
|
|
829
|
+
print("Usage: /ext-activate <name>")
|
|
830
|
+
return True
|
|
831
|
+
agent = ctx["agent"]
|
|
832
|
+
if not hasattr(agent, "ext_mgr") or not agent.ext_mgr:
|
|
833
|
+
print("Extension manager not available.")
|
|
834
|
+
return True
|
|
835
|
+
ok = agent.ext_mgr.activate(arg)
|
|
836
|
+
print(f"Activated: {arg}" if ok else f"Failed: {arg}")
|
|
837
|
+
return True
|
|
838
|
+
|
|
839
|
+
@r.register("/ext-deactivate", "Deactivate extension", "extension")
|
|
840
|
+
def cmd_ext_deactivate(arg: str, ctx: dict) -> bool:
|
|
841
|
+
if not arg:
|
|
842
|
+
print("Usage: /ext-deactivate <name>")
|
|
843
|
+
return True
|
|
844
|
+
agent = ctx["agent"]
|
|
845
|
+
if not hasattr(agent, "ext_mgr") or not agent.ext_mgr:
|
|
846
|
+
print("Extension manager not available.")
|
|
847
|
+
return True
|
|
848
|
+
ok = agent.ext_mgr.deactivate(arg)
|
|
849
|
+
print(f"Deactivated: {arg}" if ok else f"Failed: {arg}")
|
|
850
|
+
return True
|
|
851
|
+
|
|
852
|
+
# ── Sub-agents ─────────────────────────────────────────────────────
|
|
853
|
+
|
|
854
|
+
@r.register("/sub-agents", "List sub-agents", "subagent")
|
|
855
|
+
def cmd_sub_agents(arg: str, ctx: dict) -> bool:
|
|
856
|
+
agent = ctx["agent"]
|
|
857
|
+
mgr = getattr(agent, "_sub_agent_mgr", None)
|
|
858
|
+
if not mgr:
|
|
859
|
+
print("SubAgentManager not available.")
|
|
860
|
+
return True
|
|
861
|
+
agents = mgr.list_all()
|
|
862
|
+
if not agents:
|
|
863
|
+
print("No sub-agents.")
|
|
864
|
+
return True
|
|
865
|
+
icons = {"running": "R", "done": "D", "failed": "F", "cancelled": "C", "idle": "I"}
|
|
866
|
+
for a in agents:
|
|
867
|
+
icon = icons.get(a.status, "?")
|
|
868
|
+
print(f" [{icon}] {a.id} — {a.status} (tool_calls={a.tool_call_count})")
|
|
869
|
+
if a.status == "done" and a.result:
|
|
870
|
+
print(f" result: {a.result[:100]}...")
|
|
871
|
+
return True
|
|
872
|
+
|
|
873
|
+
@r.register("/sub-cancel", "Cancel sub-agent", "subagent")
|
|
874
|
+
def cmd_sub_cancel(arg: str, ctx: dict) -> bool:
|
|
875
|
+
if not arg:
|
|
876
|
+
print("Usage: /sub-cancel <agent_id|all>")
|
|
877
|
+
return True
|
|
878
|
+
agent = ctx["agent"]
|
|
879
|
+
mgr = getattr(agent, "_sub_agent_mgr", None)
|
|
880
|
+
if not mgr:
|
|
881
|
+
print("SubAgentManager not available.")
|
|
882
|
+
return True
|
|
883
|
+
if arg == "all":
|
|
884
|
+
mgr.cancel_all()
|
|
885
|
+
print("All sub-agents cancelled.")
|
|
886
|
+
else:
|
|
887
|
+
ok = mgr.cancel(arg)
|
|
888
|
+
print(f"Cancelled: {arg}" if ok else f"Not found: {arg}")
|
|
889
|
+
return True
|
|
890
|
+
|
|
891
|
+
@r.register("/config", "Show current configuration", "settings")
|
|
892
|
+
def cmd_config(arg: str, ctx: dict) -> bool:
|
|
893
|
+
agent = ctx["agent"]
|
|
894
|
+
cfg = agent.config
|
|
895
|
+
print(f" Model: {cfg.llm.model}")
|
|
896
|
+
print(f" API Base: {cfg.llm.base_url}")
|
|
897
|
+
print(f" Workspace: {cfg.agent.workspace_dir}")
|
|
898
|
+
print(f" Max Tokens: {cfg.llm.max_output_tokens}")
|
|
899
|
+
print(f" Temperature: {cfg.llm.temperature}")
|
|
900
|
+
print(f" Thinking: {cfg.llm.thinking_strength}")
|
|
901
|
+
print(f" Max Context: {cfg.agent.max_context_tokens:,}")
|
|
902
|
+
print(f" Max Tools: {cfg.agent.max_tool_calls}")
|
|
903
|
+
print(f" Session: {agent.session_id}")
|
|
904
|
+
token_est = agent.get_token_estimate()
|
|
905
|
+
print(f" Tokens used: ~{token_est:,}")
|
|
906
|
+
print(f" Anthropic: {'yes' if getattr(agent, '_use_anthropic', False) else 'no'}")
|
|
907
|
+
if agent.skills and agent.skills.active_skill:
|
|
908
|
+
print(f" Active skill: {agent.skills.active_skill.name}")
|
|
909
|
+
return True
|
|
910
|
+
|
|
911
|
+
@r.register("/status", "Show agent status (alias for /context)", "basic")
|
|
912
|
+
def cmd_status(arg: str, ctx: dict) -> bool:
|
|
913
|
+
return cmd_context(arg, ctx)
|
|
914
|
+
|
|
915
|
+
@r.register("/vision", "Analyze image with multimodal vision", "settings")
|
|
916
|
+
def cmd_vision(arg: str, ctx: dict) -> bool:
|
|
917
|
+
if not arg:
|
|
918
|
+
print("Usage: /vision <image_path> [prompt]")
|
|
919
|
+
print(" Analyze an image using the configured vision model.")
|
|
920
|
+
print(" Configure in ~/.ata_coder/settings.json:")
|
|
921
|
+
print(' {"vision": {"model": "...", "api_base": "...", "api_key": "..."}}')
|
|
922
|
+
print(" Or set VISION_MODEL / VISION_API_KEY env vars.")
|
|
923
|
+
print(" Falls back to main API config if not set.")
|
|
924
|
+
return True
|
|
925
|
+
parts = arg.split(maxsplit=2)
|
|
926
|
+
image_path = parts[0]
|
|
927
|
+
prompt = parts[1] if len(parts) > 1 else "Describe this image in detail."
|
|
928
|
+
agent = ctx["agent"]
|
|
929
|
+
result = agent.tools._tool_analyze_image(image_path, prompt)
|
|
930
|
+
if result.success:
|
|
931
|
+
print(result.output)
|
|
932
|
+
else:
|
|
933
|
+
print(f"Error: {result.error}")
|
|
934
|
+
return True
|
|
935
|
+
|
|
936
|
+
@r.register("/auto-skill", "Smart skill detection (LLM router)", "skill")
|
|
937
|
+
def cmd_auto_skill(arg: str, ctx: dict) -> bool:
|
|
938
|
+
if not arg:
|
|
939
|
+
print("Usage: /auto-skill <task description>")
|
|
940
|
+
print(" Uses LLM to intelligently route to the best skill.")
|
|
941
|
+
return True
|
|
942
|
+
agent = ctx["agent"]
|
|
943
|
+
skill_mgr = ctx.get("skill_mgr")
|
|
944
|
+
if not skill_mgr or not agent:
|
|
945
|
+
print("Skills or agent not available.")
|
|
946
|
+
return True
|
|
947
|
+
results = skill_mgr.detect_skills_smart(arg, max_results=5, llm_client=agent.llm)
|
|
948
|
+
if not results:
|
|
949
|
+
print("No matching skills found.")
|
|
950
|
+
return True
|
|
951
|
+
print(f"Smart routing for: {arg[:80]}...")
|
|
952
|
+
print(f"{'Skill':<22} {'Confidence':>10}")
|
|
953
|
+
print("-" * 34)
|
|
954
|
+
for skill, conf in results:
|
|
955
|
+
bar = "█" * int(conf * 10) + "░" * (10 - int(conf * 10))
|
|
956
|
+
print(f"{skill.name:<22} {bar} {conf:.0%}")
|
|
957
|
+
return True
|
|
958
|
+
|
|
959
|
+
return r
|
|
960
|
+
|
|
961
|
+
|
|
962
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
963
|
+
# Command list for readline completion (auto-generated from registry)
|
|
964
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
965
|
+
|
|
966
|
+
_REGISTRY: CommandRegistry | None = None
|
|
967
|
+
|
|
968
|
+
|
|
969
|
+
def get_command_list() -> list[tuple[str, str]]:
|
|
970
|
+
"""Return list of (name, description) for all slash commands."""
|
|
971
|
+
global _REGISTRY
|
|
972
|
+
if _REGISTRY is None:
|
|
973
|
+
_REGISTRY = build_registry()
|
|
974
|
+
return [(c.name, c.help_text) for c in _REGISTRY.list_all()]
|