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.
Files changed (118) hide show
  1. ata_coder/__init__.py +1 -0
  2. ata_coder/agent.py +874 -0
  3. ata_coder/agent_compact.py +190 -0
  4. ata_coder/agent_controller.py +218 -0
  5. ata_coder/agent_extension.py +69 -0
  6. ata_coder/agent_routing.py +105 -0
  7. ata_coder/agent_subsystems.py +72 -0
  8. ata_coder/agent_tools.py +318 -0
  9. ata_coder/agent_undo.py +63 -0
  10. ata_coder/anthropic_client.py +465 -0
  11. ata_coder/change_tracker.py +368 -0
  12. ata_coder/clawd_integration.py +574 -0
  13. ata_coder/commands/__init__.py +128 -0
  14. ata_coder/commands/_core.py +184 -0
  15. ata_coder/commands/_safety.py +95 -0
  16. ata_coder/commands/_settings.py +241 -0
  17. ata_coder/commands/_workflow.py +451 -0
  18. ata_coder/commands.py +974 -0
  19. ata_coder/config.py +257 -0
  20. ata_coder/core/__init__.py +35 -0
  21. ata_coder/core/events.py +73 -0
  22. ata_coder/core/queue.py +85 -0
  23. ata_coder/core/state.py +17 -0
  24. ata_coder/event_queue.py +5 -0
  25. ata_coder/extension.py +654 -0
  26. ata_coder/extensions/__init__.py +1 -0
  27. ata_coder/extensions/hello_skill.py +47 -0
  28. ata_coder/fool_proof.py +295 -0
  29. ata_coder/git_workflow.py +371 -0
  30. ata_coder/gui.py +511 -0
  31. ata_coder/llm_client.py +543 -0
  32. ata_coder/main.py +814 -0
  33. ata_coder/mcp_client.py +1095 -0
  34. ata_coder/memory.py +539 -0
  35. ata_coder/model_registry.py +134 -0
  36. ata_coder/model_router.py +105 -0
  37. ata_coder/permissions.py +274 -0
  38. ata_coder/privilege.py +464 -0
  39. ata_coder/project.py +273 -0
  40. ata_coder/prompt_template.py +423 -0
  41. ata_coder/prompts/auto-mode.md +7 -0
  42. ata_coder/prompts/coding-rules.md +40 -0
  43. ata_coder/prompts/execution-guardrails.md +14 -0
  44. ata_coder/prompts/memory-system.md +24 -0
  45. ata_coder/prompts/output-style.md +23 -0
  46. ata_coder/prompts/safety.md +17 -0
  47. ata_coder/prompts/slash-commands.md +24 -0
  48. ata_coder/prompts/sub-agents.md +38 -0
  49. ata_coder/prompts/system-reminders.md +17 -0
  50. ata_coder/prompts/system.md +105 -0
  51. ata_coder/prompts/tool-policy.md +46 -0
  52. ata_coder/repl_theme.py +99 -0
  53. ata_coder/repl_tracker.py +89 -0
  54. ata_coder/repl_ui.py +1214 -0
  55. ata_coder/safety_guard.py +434 -0
  56. ata_coder/self_correct.py +346 -0
  57. ata_coder/server.py +882 -0
  58. ata_coder/server_session.py +159 -0
  59. ata_coder/server_shell.py +129 -0
  60. ata_coder/session.py +431 -0
  61. ata_coder/settings.py +439 -0
  62. ata_coder/setup_wizard.py +136 -0
  63. ata_coder/skill_extension.py +92 -0
  64. ata_coder/skills/architect/SKILL.md +42 -0
  65. ata_coder/skills/code-reviewer/SKILL.md +37 -0
  66. ata_coder/skills/codecraft/SKILL.md +452 -0
  67. ata_coder/skills/debugger/SKILL.md +45 -0
  68. ata_coder/skills/doc-writer/SKILL.md +36 -0
  69. ata_coder/skills/general-coder/SKILL.md +76 -0
  70. ata_coder/skills/math-calculator/README.md +40 -0
  71. ata_coder/skills/math-calculator/SKILL.md +59 -0
  72. ata_coder/skills/math-calculator/handler.py +103 -0
  73. ata_coder/skills/math-calculator/prompts/system.md +8 -0
  74. ata_coder/skills/math-calculator/requirements.txt +2 -0
  75. ata_coder/skills/math-calculator/resources/constants.json +8 -0
  76. ata_coder/skills/math-calculator/tests/test_handler.py +53 -0
  77. ata_coder/skills/security-auditor/SKILL.md +40 -0
  78. ata_coder/skills/test-writer/SKILL.md +36 -0
  79. ata_coder/skills/weather-skill/README.md +45 -0
  80. ata_coder/skills/weather-skill/handler.py +76 -0
  81. ata_coder/skills/weather-skill/manifest.json +48 -0
  82. ata_coder/skills/weather-skill/prompts/system_prompt.txt +9 -0
  83. ata_coder/skills/weather-skill/prompts/user_prompt_template.txt +3 -0
  84. ata_coder/skills/weather-skill/requirements.txt +1 -0
  85. ata_coder/skills/weather-skill/resources/city_list.json +17 -0
  86. ata_coder/skills/weather-skill/resources/error_messages.json +7 -0
  87. ata_coder/skills/weather-skill/tests/test_handler.py +28 -0
  88. ata_coder/skills/weather-skill/weather_utils.py +50 -0
  89. ata_coder/skills.py +1014 -0
  90. ata_coder/sub_agent.py +273 -0
  91. ata_coder/sub_agent_manager.py +203 -0
  92. ata_coder/system_prompt_builder.py +146 -0
  93. ata_coder/task_planner.py +391 -0
  94. ata_coder/terminal.py +318 -0
  95. ata_coder/test_runner.py +219 -0
  96. ata_coder/thread_supervisor.py +195 -0
  97. ata_coder/tool_defs.py +335 -0
  98. ata_coder/tools/__init__.py +11 -0
  99. ata_coder/tools/definitions.py +335 -0
  100. ata_coder/tools/executor.py +1036 -0
  101. ata_coder/tools/result.py +26 -0
  102. ata_coder/tools/subagent.py +332 -0
  103. ata_coder/tools/web.py +361 -0
  104. ata_coder/tools.py +1576 -0
  105. ata_coder/types.py +92 -0
  106. ata_coder/utils.py +113 -0
  107. ata_coder/web/css/style.css +180 -0
  108. ata_coder/web/index.html +84 -0
  109. ata_coder/web/js/app.js +489 -0
  110. ata_coder/web/package-lock.json +25 -0
  111. ata_coder/web/package.json +10 -0
  112. ata_coder/web/tsconfig.json +13 -0
  113. ata_coder-2.4.2.dist-info/METADATA +799 -0
  114. ata_coder-2.4.2.dist-info/RECORD +118 -0
  115. ata_coder-2.4.2.dist-info/WHEEL +5 -0
  116. ata_coder-2.4.2.dist-info/entry_points.txt +2 -0
  117. ata_coder-2.4.2.dist-info/licenses/LICENSE +21 -0
  118. 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()]