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
@@ -0,0 +1,451 @@
1
+ """Command handlers — auto-split from commands.py."""
2
+
3
+ from __future__ import annotations
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+
8
+ def register_commands(r: Any) -> None:
9
+ """Register this group's commands on the registry."""
10
+ # ── Sessions ────────────────────────────────────────────────────────────
11
+ # ── Sessions ───────────────────────────────────────────────────────
12
+
13
+ @r.register("/save", "Save session", "session")
14
+ def cmd_save(arg: str, ctx: dict) -> bool:
15
+ print(f"Saved: {ctx['agent'].save_session(arg)}")
16
+ return True
17
+
18
+ @r.register("/sessions", "List all sessions", "session")
19
+ @r.register("/history", "Search/browse history", "session")
20
+ def cmd_history(arg: str, ctx: dict) -> bool:
21
+ sm = ctx.get("session_mgr")
22
+ if not sm:
23
+ print("Session manager not available.")
24
+ return True
25
+
26
+ # Get current workspace for filtering
27
+ agent = ctx["agent"]
28
+ ws = getattr(agent.tools, "workspace", None)
29
+ workspace = str(ws) if ws else None
30
+
31
+ if arg:
32
+ # Try to resume by index number
33
+ if arg.isdigit():
34
+ sessions = sm.list_sessions(limit=50, workspace=workspace)
35
+ idx = int(arg) - 1
36
+ if 0 <= idx < len(sessions):
37
+ meta = sessions[idx]
38
+ msgs = sm.load(meta.id)
39
+ if msgs:
40
+ agent._state.messages = msgs
41
+ print(f"Resumed: {meta.id}")
42
+ print(f" {meta.summary[:100]}")
43
+ print(f" Messages: {len(msgs)}, Tools: {meta.tool_call_count}")
44
+ return True
45
+ print(f"No session at index {arg} (found {len(sessions)} sessions)")
46
+ return True
47
+
48
+ # Try to resume by session ID
49
+ msgs = sm.load(arg)
50
+ if msgs:
51
+ agent._state.messages = msgs
52
+ meta = sm.get_meta(arg)
53
+ print(f"Resumed: {arg} ({len(msgs)} msgs)")
54
+ if meta:
55
+ print(f" {meta.summary[:100]}")
56
+ else:
57
+ # Search by keyword
58
+ results = sm.search_sessions(arg, workspace=workspace)
59
+ if results:
60
+ print(f"Search '{arg}': {len(results)} matches")
61
+ for i, meta in enumerate(results[:10], 1):
62
+ date = meta.created[:10] if meta.created else "?"
63
+ print(f" [{i}] {date} | {meta.skill:15s} | {meta.summary[:60]}")
64
+ else:
65
+ print(f"No matches for: {arg}")
66
+ return True
67
+
68
+ # No args — list recent sessions for this workspace
69
+ sessions = sm.list_sessions(limit=20, workspace=workspace)
70
+ ws_name = Path(workspace).name if workspace else "all"
71
+
72
+ if not sessions:
73
+ print(f"No sessions for workspace: {ws_name}")
74
+ print("Try /history <keyword> to search all sessions.")
75
+ return True
76
+
77
+ print(f"History ({ws_name}/):")
78
+ for i, meta in enumerate(sessions, 1):
79
+ date = meta.created[:10] if meta.created else "?"
80
+ icon = {"general-coder": "💻", "debugger": "🐛", "code-reviewer": "🔍",
81
+ "architect": "🏗️", "test-writer": "🧪"}.get(meta.skill, "📝")
82
+ print(f" [{i}] {icon} {date} | {meta.skill:15s} | {meta.summary[:60]}")
83
+ if meta.tool_call_count:
84
+ print(f" {meta.message_count} msgs, {meta.tool_call_count} tools")
85
+ print("\n/history <number> to resume, /history <keyword> to search")
86
+ return True
87
+
88
+ @r.register("/resume", "Resume or list sessions", "session")
89
+ def cmd_resume(arg: str, ctx: dict) -> bool:
90
+ """
91
+ /resume → list sessions in current workspace
92
+ /resume <hash> → resume a specific session by ID or hash
93
+ """
94
+ sm = ctx.get("session_mgr")
95
+ if not sm:
96
+ print("Session storage not available.")
97
+ return True
98
+
99
+ # ── No arg: list sessions in current workspace ───────────────
100
+ if not arg:
101
+ import hashlib
102
+ try:
103
+ workspace = str(ctx["agent"].tools.workspace) if ctx.get("agent") else ""
104
+ except Exception:
105
+ workspace = ""
106
+ ws_hash = hashlib.sha256(workspace.encode()).hexdigest()[:8]
107
+ sessions = sm.list_sessions(limit=50)
108
+ matches = [s for s in sessions if s.id.startswith(ws_hash)]
109
+
110
+ if not matches:
111
+ print(f"No sessions in this workspace.\n hash: {ws_hash}")
112
+ return True
113
+
114
+ print(f"\nSessions ({workspace}):\n")
115
+ for i, s in enumerate(matches):
116
+ parts = s.id.split("-")
117
+ resume_hash = parts[-1] if len(parts) >= 3 else s.id[-8:]
118
+ title = s.summary[:60] if s.summary else "(untitled)"
119
+ print(f" [{i+1}] {title}")
120
+ print(f" ata --resume {resume_hash}")
121
+ print(f" {s.updated} {s.message_count} msgs\n")
122
+ return True
123
+
124
+ # ── Hash arg: resume a specific session ─────────────────────
125
+ msgs = sm.load(arg)
126
+ if msgs:
127
+ ctx["agent"]._state.messages = msgs
128
+ ctx["agent"]._current_session_id = sm.resolve_session_id(arg) or arg
129
+ print(f"Resumed: {arg} ({len(msgs)} msgs)")
130
+ else:
131
+ print(f"Not found: {arg}")
132
+ return True
133
+
134
+ @r.register("/export", "Export session", "session")
135
+ def cmd_export(arg: str, ctx: dict) -> bool:
136
+ sm = ctx.get("session_mgr")
137
+ if not sm or not arg:
138
+ print("Usage: /export <id> [path]")
139
+ return True
140
+ parts = arg.split(maxsplit=1)
141
+ sid = parts[0]
142
+ out = parts[1] if len(parts) > 1 else None
143
+ md = sm.export_markdown(sid, out)
144
+ if md:
145
+ print(f"Exported {sid}" + (f" to {out}" if out else ""))
146
+ else:
147
+ print(f"Not found: {sid}")
148
+ return True
149
+
150
+
151
+ # ── Git ────────────────────────────────────────────────────────────
152
+ # ── Git ────────────────────────────────────────────────────────────
153
+
154
+ @r.register("/git", "Git operations", "git")
155
+ def cmd_git(arg: str, ctx: dict) -> bool:
156
+ git = ctx["agent"].git
157
+ if not git:
158
+ print("Not available.")
159
+ return True
160
+ if arg == "status" or not arg:
161
+ s = git.get_status()
162
+ print(f"Branch: {s.branch}\nStatus: {s.summary()}")
163
+ elif arg == "diff":
164
+ print(git.get_diff())
165
+ elif arg == "log":
166
+ print(git.get_log())
167
+ elif arg.startswith("commit"):
168
+ ok, out = git.commit(arg[6:].strip())
169
+ print(out)
170
+ elif arg.startswith("branch "):
171
+ ok, out = git.create_branch(arg[7:].strip())
172
+ print(out)
173
+ elif arg == "branch" or arg == "branches":
174
+ print(git.list_branches())
175
+ elif arg == "undo":
176
+ ok, out = git.undo_commit()
177
+ print(out)
178
+ elif arg == "stash":
179
+ git.stash()
180
+ print("Stashed.")
181
+ elif arg == "unstash":
182
+ git.stash_pop()
183
+ print("Unstashed.")
184
+ elif arg == "summary":
185
+ print(git.session_summary())
186
+ else:
187
+ print("/git [status|diff|log|commit|branch|undo|branches|stash|unstash|summary]")
188
+ return True
189
+
190
+
191
+ # ── Review & Fix ────────────────────────────────────────────────────────────
192
+ # ── Review & Fix ────────────────────────────────────────────────────
193
+
194
+ @r.register("/review", "AI code review of current changes", "review")
195
+ async def cmd_review(arg: str, ctx: dict) -> bool:
196
+ agent = ctx["agent"]
197
+ git = agent.git
198
+ diff_text = git.get_diff() if git else "(no git repo)"
199
+ if not diff_text or diff_text == "(no changes)":
200
+ print("No changes to review.")
201
+ return True
202
+ task = (
203
+ "Review the following code changes. Output a structured report:\n"
204
+ "## Issues Found\n"
205
+ "For each: severity (critical/high/medium/low), file, line, problem, fix\n\n"
206
+ f"```diff\n{diff_text[:8000]}\n```"
207
+ )
208
+ print("Reviewing changes...\n")
209
+ await agent.run(task, stream=True)
210
+ return True
211
+
212
+ @r.register("/fix", "AI apply review suggestions", "review")
213
+ async def cmd_fix(arg: str, ctx: dict) -> bool:
214
+ agent = ctx["agent"]
215
+ git = agent.git
216
+ diff_text = git.get_diff() if git else "(no git repo)"
217
+ if not diff_text or diff_text == "(no changes)":
218
+ print("No changes to fix.")
219
+ return True
220
+ severity = arg if arg else "all"
221
+ task = (
222
+ f"Review this diff and fix issues. Focus on {severity} severity issues.\n"
223
+ "Apply the fixes directly to the files.\n\n"
224
+ f"```diff\n{diff_text[:8000]}\n```"
225
+ )
226
+ print(f"Fixing {severity} issues...\n")
227
+ await agent.run(task, stream=True)
228
+ return True
229
+
230
+
231
+ # ── Planner ────────────────────────────────────────────────────────────
232
+ # ── Planner ────────────────────────────────────────────────────────
233
+
234
+ @r.register("/plan", "Task plan", "plan")
235
+ def cmd_plan(arg: str, ctx: dict) -> bool:
236
+ p = ctx["agent"].planner
237
+ if arg:
238
+ agent = ctx["agent"]
239
+ plan = p.decompose(arg, llm_client=agent.llm)
240
+ print(plan.to_prompt())
241
+ elif p.current_plan:
242
+ print(p.current_plan.to_prompt())
243
+ else:
244
+ print("Usage: /plan <task>")
245
+ return True
246
+
247
+ @r.register("/tasks", "List plan tasks", "plan")
248
+ def cmd_tasks(arg: str, ctx: dict) -> bool:
249
+ p = ctx["agent"].planner
250
+ if not p.current_plan:
251
+ print("No active plan.")
252
+ return True
253
+ print(p.current_plan.progress_bar())
254
+ icons = {"pending": "[ ]", "in_progress": "[>]", "completed": "[x]", "failed": "[!]", "skipped": "[-]"}
255
+ for t in p.current_plan.subtasks:
256
+ print(f" {icons.get(t.status.value, '[?]')} #{t.id} {t.subject}")
257
+ return True
258
+
259
+ @r.register("/plan-next", "Start next task", "plan")
260
+ def cmd_plan_next(arg: str, ctx: dict) -> bool:
261
+ t = ctx["agent"].planner.auto_advance()
262
+ print(f"Starting: #{t.id} {t.subject}" if t else "No pending tasks.")
263
+ return True
264
+
265
+ @r.register("/plan-done", "Complete task", "plan")
266
+ def cmd_plan_done(arg: str, ctx: dict) -> bool:
267
+ p = ctx["agent"].planner
268
+ try:
269
+ tid = int(arg) if arg else 0
270
+ except ValueError:
271
+ tid = 0
272
+ if tid and p.current_plan:
273
+ t = p.complete_task(tid)
274
+ elif p.current_plan and p.current_plan.current:
275
+ t = p.complete_task(p.current_plan.current.id)
276
+ else:
277
+ print("No task to complete.")
278
+ return True
279
+ print(f"Completed: #{t.id} {t.subject}" if t else "Failed.")
280
+ return True
281
+
282
+ @r.register("/retry", "Self-correct stats", "plan")
283
+ def cmd_retry(arg: str, ctx: dict) -> bool:
284
+ sc = ctx["agent"].self_correct
285
+ s = sc.stats
286
+ print(f"Retries: {s['total_retries']} Successful: {s['successful_retries']} "
287
+ f"Auto-fix rate: {s['auto_fix_rate']}")
288
+ return True
289
+
290
+
291
+ # ── Extensions ────────────────────────────────────────────────────────────
292
+ # ── Extensions ─────────────────────────────────────────────────────
293
+
294
+ @r.register("/extensions", "List extensions", "extension")
295
+ def cmd_extensions(arg: str, ctx: dict) -> bool:
296
+ agent = ctx["agent"]
297
+ if not hasattr(agent, "ext_mgr") or not agent.ext_mgr:
298
+ print("Extension manager not available.")
299
+ return True
300
+ active_names = {e.meta.name for e in agent.ext_mgr.list_active()}
301
+ for ext in agent.ext_mgr.list_extensions():
302
+ status = "[active]" if ext.meta.name in active_names else "[loaded]"
303
+ print(f" {status} {ext.meta.name} v{ext.meta.version} — {ext.meta.description[:60]}")
304
+ return True
305
+
306
+ @r.register("/ext-activate", "Activate extension", "extension")
307
+ def cmd_ext_activate(arg: str, ctx: dict) -> bool:
308
+ if not arg:
309
+ print("Usage: /ext-activate <name>")
310
+ return True
311
+ agent = ctx["agent"]
312
+ if not hasattr(agent, "ext_mgr") or not agent.ext_mgr:
313
+ print("Extension manager not available.")
314
+ return True
315
+ ok = agent.ext_mgr.activate(arg)
316
+ print(f"Activated: {arg}" if ok else f"Failed: {arg}")
317
+ return True
318
+
319
+ @r.register("/ext-deactivate", "Deactivate extension", "extension")
320
+ def cmd_ext_deactivate(arg: str, ctx: dict) -> bool:
321
+ if not arg:
322
+ print("Usage: /ext-deactivate <name>")
323
+ return True
324
+ agent = ctx["agent"]
325
+ if not hasattr(agent, "ext_mgr") or not agent.ext_mgr:
326
+ print("Extension manager not available.")
327
+ return True
328
+ ok = agent.ext_mgr.deactivate(arg)
329
+ print(f"Deactivated: {arg}" if ok else f"Failed: {arg}")
330
+ return True
331
+
332
+
333
+ # ── Sub-agents ────────────────────────────────────────────────────────────
334
+ # ── Sub-agents ─────────────────────────────────────────────────────
335
+
336
+ @r.register("/sub-agents", "List sub-agents", "subagent")
337
+ def cmd_sub_agents(arg: str, ctx: dict) -> bool:
338
+ agent = ctx["agent"]
339
+ mgr = getattr(agent, "_sub_agent_mgr", None)
340
+ if not mgr:
341
+ print("SubAgentManager not available.")
342
+ return True
343
+ agents = mgr.list_all()
344
+ if not agents:
345
+ print("No sub-agents.")
346
+ return True
347
+ icons = {"running": "R", "done": "D", "failed": "F", "cancelled": "C", "idle": "I"}
348
+ for a in agents:
349
+ icon = icons.get(a.status, "?")
350
+ print(f" [{icon}] {a.id} — {a.status} (tool_calls={a.tool_call_count})")
351
+ if a.status == "done" and a.result:
352
+ print(f" result: {a.result[:100]}...")
353
+ return True
354
+
355
+ @r.register("/sub-cancel", "Cancel sub-agent", "subagent")
356
+ def cmd_sub_cancel(arg: str, ctx: dict) -> bool:
357
+ if not arg:
358
+ print("Usage: /sub-cancel <agent_id|all>")
359
+ return True
360
+ agent = ctx["agent"]
361
+ mgr = getattr(agent, "_sub_agent_mgr", None)
362
+ if not mgr:
363
+ print("SubAgentManager not available.")
364
+ return True
365
+ if arg == "all":
366
+ mgr.cancel_all()
367
+ print("All sub-agents cancelled.")
368
+ else:
369
+ ok = mgr.cancel(arg)
370
+ print(f"Cancelled: {arg}" if ok else f"Not found: {arg}")
371
+ return True
372
+
373
+ @r.register("/config", "Show current configuration", "settings")
374
+ def cmd_config(arg: str, ctx: dict) -> bool:
375
+ agent = ctx["agent"]
376
+ cfg = agent.config
377
+ print(f" Model: {cfg.llm.model}")
378
+ print(f" API Base: {cfg.llm.base_url}")
379
+ print(f" Workspace: {cfg.agent.workspace_dir}")
380
+ print(f" Max Tokens: {cfg.llm.max_output_tokens}")
381
+ print(f" Temperature: {cfg.llm.temperature}")
382
+ print(f" Thinking: {cfg.llm.thinking_strength}")
383
+ print(f" Max Context: {cfg.agent.max_context_tokens:,}")
384
+ print(f" Max Tools: {cfg.agent.max_tool_calls}")
385
+ print(f" Session: {agent.session_id}")
386
+ token_est = agent.get_token_estimate()
387
+ print(f" Tokens used: ~{token_est:,}")
388
+ print(f" Anthropic: {'yes' if getattr(agent, '_use_anthropic', False) else 'no'}")
389
+ if agent.skills and agent.skills.active_skill:
390
+ print(f" Active skill: {agent.skills.active_skill.name}")
391
+ return True
392
+
393
+ @r.register("/status", "Show conversation window", "basic")
394
+ def cmd_status(arg: str, ctx: dict) -> bool:
395
+ agent = ctx["agent"]
396
+ ctx["ui"].show_context(
397
+ total_messages=len(agent._state.messages),
398
+ tool_calls=agent._state.tool_call_count,
399
+ skill=agent.skills.active_skill.name if agent.skills and agent.skills.active_skill else "default",
400
+ model=ctx["config"].llm.model,
401
+ estimated_tokens=agent.get_token_estimate(),
402
+ max_tokens=ctx["config"].agent.max_context_tokens,
403
+ )
404
+ return True
405
+
406
+ @r.register("/vision", "Analyze image with multimodal vision", "settings")
407
+ def cmd_vision(arg: str, ctx: dict) -> bool:
408
+ if not arg:
409
+ print("Usage: /vision <image_path> [prompt]")
410
+ print(" Analyze an image using the configured vision model.")
411
+ print(" Configure in ~/.ata_coder/settings.json:")
412
+ print(' {"vision": {"model": "...", "api_base": "...", "api_key": "..."}}')
413
+ print(" Or set VISION_MODEL / VISION_API_KEY env vars.")
414
+ print(" Falls back to main API config if not set.")
415
+ return True
416
+ parts = arg.split(maxsplit=2)
417
+ image_path = parts[0]
418
+ prompt = parts[1] if len(parts) > 1 else "Describe this image in detail."
419
+ agent = ctx["agent"]
420
+ result = agent.tools._tool_analyze_image(image_path, prompt)
421
+ if result.success:
422
+ print(result.output)
423
+ else:
424
+ print(f"Error: {result.error}")
425
+ return True
426
+
427
+ @r.register("/auto-skill", "Smart skill detection (LLM router)", "skill")
428
+ def cmd_auto_skill(arg: str, ctx: dict) -> bool:
429
+ if not arg:
430
+ print("Usage: /auto-skill <task description>")
431
+ print(" Uses LLM to intelligently route to the best skill.")
432
+ return True
433
+ agent = ctx["agent"]
434
+ skill_mgr = ctx.get("skill_mgr")
435
+ if not skill_mgr or not agent:
436
+ print("Skills or agent not available.")
437
+ return True
438
+ results = skill_mgr.detect_skills_smart(arg, max_results=5, llm_client=agent.llm)
439
+ if not results:
440
+ print("No matching skills found.")
441
+ return True
442
+ print(f"Smart routing for: {arg[:80]}...")
443
+ print(f"{'Skill':<22} {'Confidence':>10}")
444
+ print("-" * 34)
445
+ for skill, conf in results:
446
+ bar = "█" * int(conf * 10) + "░" * (10 - int(conf * 10))
447
+ print(f"{skill.name:<22} {bar} {conf:.0%}")
448
+ return True
449
+
450
+ return r
451
+