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/main.py
ADDED
|
@@ -0,0 +1,814 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
ATA Coder — Claude Code-compatible CLI.
|
|
4
|
+
|
|
5
|
+
A full-featured AI coding assistant with OpenAI-compatible APIs.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
ata # Interactive mode
|
|
9
|
+
ata run "Add type hints" # Single task
|
|
10
|
+
ata server # Start API server
|
|
11
|
+
ata --skill debugger # Interactive with forced skill
|
|
12
|
+
ata --resume <session-id> # Resume session
|
|
13
|
+
"""
|
|
14
|
+
import sys
|
|
15
|
+
if sys.version_info < (3, 10):
|
|
16
|
+
sys.exit("Python 3.10 or higher is required for ATA Coder.")
|
|
17
|
+
|
|
18
|
+
# ── Kill GBK _readerthread errors on Windows ─────────────────────────────
|
|
19
|
+
# CPython's subprocess module spawns a daemon _readerthread that uses the
|
|
20
|
+
# system locale (GBK on Chinese Windows) when text=True. Any non-ASCII
|
|
21
|
+
# output crashes the thread on interpreter shutdown with UnicodeDecodeError.
|
|
22
|
+
# We monkey-patch subprocess.Popen to default encoding='utf-8',errors='replace'.
|
|
23
|
+
if sys.platform == 'win32':
|
|
24
|
+
# Windows subprocess defaults to the system ANSI code page (e.g. GBK on
|
|
25
|
+
# Chinese Windows) for text-mode pipes, which corrupts UTF-8 output from
|
|
26
|
+
# modern CLI tools. This monkey-patch forces utf-8 + errors='replace' on
|
|
27
|
+
# every subprocess.Popen call in the process. It is deliberately placed
|
|
28
|
+
# BEFORE any other imports so that ALL downstream Popen usage is covered.
|
|
29
|
+
#
|
|
30
|
+
# Risk: global side-effect that affects third-party libraries using Popen.
|
|
31
|
+
# Mitigation: binary-mode calls (no encoding/text/universal_newlines) are
|
|
32
|
+
# left untouched, so the patch is a no-op for the common binary-pipe case.
|
|
33
|
+
import subprocess as _sp
|
|
34
|
+
_orig_init = _sp.Popen.__init__
|
|
35
|
+
def _patched_init(self, *a, **kw):
|
|
36
|
+
if 'encoding' not in kw and 'text' not in kw and 'universal_newlines' not in kw:
|
|
37
|
+
pass # binary mode — no encoding needed
|
|
38
|
+
else:
|
|
39
|
+
kw.setdefault('encoding', 'utf-8')
|
|
40
|
+
kw.setdefault('errors', 'replace')
|
|
41
|
+
_orig_init(self, *a, **kw)
|
|
42
|
+
_sp.Popen.__init__ = _patched_init
|
|
43
|
+
|
|
44
|
+
__version__ = "2.4.2"
|
|
45
|
+
|
|
46
|
+
import asyncio
|
|
47
|
+
import logging
|
|
48
|
+
import os
|
|
49
|
+
import platform
|
|
50
|
+
import signal
|
|
51
|
+
import sys
|
|
52
|
+
from pathlib import Path
|
|
53
|
+
|
|
54
|
+
import click
|
|
55
|
+
|
|
56
|
+
# Allow running directly (python main.py) without pip install -e .
|
|
57
|
+
# When the package IS installed, this is a harmless no-op.
|
|
58
|
+
_PKG_DIR = str(Path(__file__).parent.resolve())
|
|
59
|
+
if _PKG_DIR not in sys.path:
|
|
60
|
+
sys.path.insert(0, _PKG_DIR)
|
|
61
|
+
|
|
62
|
+
from .config import AppConfig, get_config
|
|
63
|
+
from .tools import ToolExecutor, TOOL_DEFINITIONS
|
|
64
|
+
from .skills import get_skill_manager
|
|
65
|
+
from .memory import get_memory_store
|
|
66
|
+
from .setup_wizard import ensure_first_run as _ensure_first_run
|
|
67
|
+
from .session import SessionManager
|
|
68
|
+
from .project import ProjectDetector
|
|
69
|
+
from .permissions import PermissionStore, PermissionMode
|
|
70
|
+
from .repl_ui import ClaudeCodeUI, HAS_RICH
|
|
71
|
+
from .agent import CoderAgent
|
|
72
|
+
from .agent_subsystems import AgentSubsystems
|
|
73
|
+
from .clawd_integration import create_clawd_permission_handler, get_clawd
|
|
74
|
+
|
|
75
|
+
logger = logging.getLogger(__name__)
|
|
76
|
+
|
|
77
|
+
_cleanup_handlers: list = []
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def register_cleanup(handler) -> None:
|
|
81
|
+
_cleanup_handlers.append(handler)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _signal_handler(sig, frame):
|
|
85
|
+
print("\n[Interrupted]")
|
|
86
|
+
# Clawd: notify desktop pet before exit
|
|
87
|
+
try:
|
|
88
|
+
get_clawd().shutdown()
|
|
89
|
+
except Exception:
|
|
90
|
+
pass
|
|
91
|
+
for handler in _cleanup_handlers:
|
|
92
|
+
try:
|
|
93
|
+
handler()
|
|
94
|
+
except Exception:
|
|
95
|
+
pass
|
|
96
|
+
sys.exit(1)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
signal.signal(signal.SIGINT, _signal_handler)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# ── Subsystem init ──────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
class SubsystemInitError(Exception):
|
|
105
|
+
"""Critical subsystem failed to initialize — agent cannot start."""
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _init_subsystems(config, **kwargs) -> dict:
|
|
109
|
+
"""Initialize all subsystems.
|
|
110
|
+
|
|
111
|
+
Critical subsystems (skills, memory, permissions) raise on failure.
|
|
112
|
+
Non-critical subsystems log a warning and continue with None.
|
|
113
|
+
"""
|
|
114
|
+
result = {
|
|
115
|
+
"skills": None, "memory": None, "mcp": None,
|
|
116
|
+
"templates": None, "sessions": None, "project": None,
|
|
117
|
+
"permissions": None,
|
|
118
|
+
}
|
|
119
|
+
workspace = config.agent.workspace_dir
|
|
120
|
+
errors: list[str] = []
|
|
121
|
+
|
|
122
|
+
# ── Critical: agent cannot function without these ──────────────────
|
|
123
|
+
for name, factory in [
|
|
124
|
+
("skills", lambda: get_skill_manager(kwargs.get("skills_dir"))),
|
|
125
|
+
("memory", lambda: get_memory_store(kwargs.get("memory_dir"))),
|
|
126
|
+
("permissions", lambda: PermissionStore()),
|
|
127
|
+
]:
|
|
128
|
+
try:
|
|
129
|
+
result[name] = factory()
|
|
130
|
+
except Exception as e:
|
|
131
|
+
logger.exception("%s init failed", name)
|
|
132
|
+
errors.append(f" {name}: {e}")
|
|
133
|
+
|
|
134
|
+
# ── Non-critical: nice-to-have, degrade gracefully ─────────────────
|
|
135
|
+
for name, factory in [
|
|
136
|
+
("sessions", SessionManager),
|
|
137
|
+
("templates", lambda: _try_init_templates(kwargs.get("prompts_dir"))),
|
|
138
|
+
("project", lambda: ProjectDetector(workspace).detect()),
|
|
139
|
+
]:
|
|
140
|
+
try:
|
|
141
|
+
result[name] = factory()
|
|
142
|
+
except Exception as e:
|
|
143
|
+
logger.warning("%s unavailable: %s", name, e)
|
|
144
|
+
result[name] = None
|
|
145
|
+
|
|
146
|
+
# MCP is special: only init if config provided
|
|
147
|
+
result["mcp"] = _try_init_mcp(kwargs.get("mcp_config"))
|
|
148
|
+
|
|
149
|
+
if errors:
|
|
150
|
+
raise SubsystemInitError(
|
|
151
|
+
"Critical subsystems failed to initialize:\n"
|
|
152
|
+
+ "\n".join(errors)
|
|
153
|
+
+ "\n\nCheck your installation or environment variables."
|
|
154
|
+
)
|
|
155
|
+
return result
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _try_init_templates(prompts_dir: str | None):
|
|
159
|
+
from .prompt_template import TemplateManager
|
|
160
|
+
return TemplateManager(prompts_dir)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _try_init_mcp(mcp_config: str | None):
|
|
164
|
+
if not mcp_config:
|
|
165
|
+
return None
|
|
166
|
+
from .mcp_client import MCPClient, load_mcp_config
|
|
167
|
+
return MCPClient(load_mcp_config(mcp_config))
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# ── Config override ─────────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
def _apply_config_overrides(config: AppConfig, kwargs: dict) -> str:
|
|
173
|
+
explicit_model = ""
|
|
174
|
+
if kwargs.get("model"):
|
|
175
|
+
config.llm.model = kwargs["model"]
|
|
176
|
+
explicit_model = kwargs["model"]
|
|
177
|
+
if kwargs.get("api_key"):
|
|
178
|
+
config.llm.api_key = kwargs["api_key"]
|
|
179
|
+
if kwargs.get("base_url"):
|
|
180
|
+
config.llm.base_url = kwargs["base_url"]
|
|
181
|
+
if kwargs.get("workspace"):
|
|
182
|
+
config.agent.workspace_dir = os.path.abspath(
|
|
183
|
+
os.path.expanduser(kwargs["workspace"]))
|
|
184
|
+
if kwargs.get("max_tool_calls"):
|
|
185
|
+
config.agent.max_tool_calls = kwargs["max_tool_calls"]
|
|
186
|
+
if kwargs.get("think"):
|
|
187
|
+
config.llm.thinking_strength = kwargs["think"]
|
|
188
|
+
if kwargs.get("anthropic"):
|
|
189
|
+
config.llm.use_anthropic = True
|
|
190
|
+
return explicit_model
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# ── Startup banner / First-run setup → setup_wizard.py
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
# ── Interactive mode ────────────────────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
async def run_interactive_async(config: AppConfig, **kwargs):
|
|
201
|
+
"""Async REPL loop — runs on the asyncio event loop."""
|
|
202
|
+
ui = ClaudeCodeUI()
|
|
203
|
+
|
|
204
|
+
from .commands import get_command_list
|
|
205
|
+
ui.setup_command_completion(get_command_list())
|
|
206
|
+
|
|
207
|
+
explicit_model = kwargs.get("model", "") or ""
|
|
208
|
+
subsystems = _init_subsystems(config, **kwargs)
|
|
209
|
+
|
|
210
|
+
skill_mgr = subsystems["skills"]
|
|
211
|
+
memory_store = subsystems["memory"]
|
|
212
|
+
mcp_client = subsystems["mcp"]
|
|
213
|
+
template_mgr = subsystems["templates"]
|
|
214
|
+
session_mgr = subsystems["sessions"]
|
|
215
|
+
project_info = subsystems["project"]
|
|
216
|
+
permission_store = subsystems["permissions"]
|
|
217
|
+
|
|
218
|
+
def on_permission_change(action: str, target: str):
|
|
219
|
+
if action == "allow_category":
|
|
220
|
+
permission_store.set_category_rule(target, PermissionMode.ALLOW)
|
|
221
|
+
if HAS_RICH:
|
|
222
|
+
ui.console.print(f"[dim]Allowed all {target} commands for this session.[/dim]")
|
|
223
|
+
elif action == "deny_category":
|
|
224
|
+
permission_store.set_category_rule(target, PermissionMode.DENY)
|
|
225
|
+
if HAS_RICH:
|
|
226
|
+
ui.console.print(f"[dim]Denied all {target} commands for this session.[/dim]")
|
|
227
|
+
|
|
228
|
+
ui.set_permission_callback(on_permission_change)
|
|
229
|
+
|
|
230
|
+
# Wrap the built-in permission prompt with Clawd bubble support.
|
|
231
|
+
# When Clawd is running, permission decisions go through its
|
|
232
|
+
# interactive bubble UI (Y/N/A/D). Falls back to the built-in
|
|
233
|
+
# terminal prompt when Clawd is unreachable.
|
|
234
|
+
_clawd_perm = create_clawd_permission_handler()
|
|
235
|
+
_builtin_prompt = ui.permission_prompt
|
|
236
|
+
|
|
237
|
+
def _combined_permission(tool_name: str, arguments: dict, category: str) -> bool:
|
|
238
|
+
if _clawd_perm is not None:
|
|
239
|
+
result = _clawd_perm(tool_name, arguments, category)
|
|
240
|
+
if result is not None:
|
|
241
|
+
return result
|
|
242
|
+
return _builtin_prompt(tool_name, arguments, category)
|
|
243
|
+
|
|
244
|
+
permission_store.set_prompt_callback(_combined_permission)
|
|
245
|
+
|
|
246
|
+
if kwargs.get("allow_all"):
|
|
247
|
+
permission_store.set_category_rule("shell", PermissionMode.ALLOW)
|
|
248
|
+
permission_store.set_category_rule("write", PermissionMode.ALLOW)
|
|
249
|
+
if kwargs.get("deny_shell"):
|
|
250
|
+
permission_store.set_category_rule("shell", PermissionMode.DENY)
|
|
251
|
+
|
|
252
|
+
active_skill = kwargs.get("skill") or "general-coder"
|
|
253
|
+
if skill_mgr:
|
|
254
|
+
skill_mgr.activate(active_skill)
|
|
255
|
+
auto_skill_state = {"value": not kwargs.get("no_skill_auto", False)}
|
|
256
|
+
|
|
257
|
+
resume_id = kwargs.get("resume")
|
|
258
|
+
resume_messages = None
|
|
259
|
+
if resume_id and session_mgr:
|
|
260
|
+
resume_messages = session_mgr.load(resume_id)
|
|
261
|
+
if resume_messages:
|
|
262
|
+
if HAS_RICH:
|
|
263
|
+
ui.console.print(f"[green]Resumed session: {resume_id}[/green]")
|
|
264
|
+
else:
|
|
265
|
+
print(f"Resumed: {resume_id}")
|
|
266
|
+
|
|
267
|
+
mcp_names = mcp_client.connected_servers if mcp_client else []
|
|
268
|
+
ui.show_welcome(
|
|
269
|
+
config.llm.model, config.agent.workspace_dir,
|
|
270
|
+
active_skill, project_info, mcp_names,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
tool_exec = ToolExecutor(config.agent)
|
|
274
|
+
tool_exec.on_edit(ui.track_edit)
|
|
275
|
+
tool_exec.setup_file_cache(Path(config.agent.workspace_dir) / ".ata_coder" / "files")
|
|
276
|
+
|
|
277
|
+
# ── Create AgentController (runs agent on background thread) ──────────
|
|
278
|
+
from .agent_controller import AgentController
|
|
279
|
+
|
|
280
|
+
subsys = AgentSubsystems(
|
|
281
|
+
skills=skill_mgr, memory=memory_store, mcp=mcp_client,
|
|
282
|
+
templates=template_mgr, permissions=permission_store,
|
|
283
|
+
project_info=project_info, sessions=session_mgr,
|
|
284
|
+
)
|
|
285
|
+
controller = AgentController(
|
|
286
|
+
config=config, subsystems=subsys, tool_executor=tool_exec,
|
|
287
|
+
)
|
|
288
|
+
await controller.start()
|
|
289
|
+
|
|
290
|
+
# Clawd: SessionStart — one session per REPL, not per task
|
|
291
|
+
_clawd = get_clawd()
|
|
292
|
+
workspace_str = str(controller.agent.tools.workspace) if controller.agent else os.getcwd()
|
|
293
|
+
_clawd.start(session_id=controller.agent.session_id if controller.agent else "", cwd=workspace_str)
|
|
294
|
+
|
|
295
|
+
# Wire usage tracking (events go through EventQueue, not callback)
|
|
296
|
+
if controller.agent:
|
|
297
|
+
controller.agent.llm.on_usage(ui.track_usage)
|
|
298
|
+
|
|
299
|
+
if resume_messages and controller.agent:
|
|
300
|
+
controller.agent._state.messages = resume_messages
|
|
301
|
+
# Reuse the resumed session ID so auto-save updates the same session
|
|
302
|
+
controller.agent._current_session_id = resume_id
|
|
303
|
+
|
|
304
|
+
running = True
|
|
305
|
+
while running:
|
|
306
|
+
try:
|
|
307
|
+
session_info = ""
|
|
308
|
+
agent_ref = controller.agent
|
|
309
|
+
if agent_ref and agent_ref.session_id:
|
|
310
|
+
tokens = agent_ref.get_token_estimate()
|
|
311
|
+
parts = [f"tokens=~{tokens:,}"]
|
|
312
|
+
if agent_ref.git:
|
|
313
|
+
gs = agent_ref.git.get_status()
|
|
314
|
+
if gs.is_dirty():
|
|
315
|
+
parts.append(f"git:{gs.summary()}")
|
|
316
|
+
session_info = " ".join(parts)
|
|
317
|
+
|
|
318
|
+
dangerous = (
|
|
319
|
+
agent_ref.privilege_mgr.is_dangerous
|
|
320
|
+
if agent_ref and agent_ref.privilege_mgr else False
|
|
321
|
+
)
|
|
322
|
+
user_input = await ui.get_input(session_info, dangerous=dangerous)
|
|
323
|
+
except (KeyboardInterrupt, EOFError):
|
|
324
|
+
sid = getattr(agent_ref, "_current_session_id", "") if agent_ref else ""
|
|
325
|
+
hash_suffix = sid.rsplit("-", 1)[-1] if "-" in sid else sid
|
|
326
|
+
if hash_suffix and len(hash_suffix) >= 6:
|
|
327
|
+
print(f"\nResume this session with:\n ata --resume {hash_suffix}")
|
|
328
|
+
else:
|
|
329
|
+
print("\nGoodbye!")
|
|
330
|
+
break
|
|
331
|
+
|
|
332
|
+
if not user_input:
|
|
333
|
+
continue
|
|
334
|
+
|
|
335
|
+
cmd, arg = _parse_command(user_input)
|
|
336
|
+
|
|
337
|
+
if cmd is None:
|
|
338
|
+
print()
|
|
339
|
+
# ── Task execution via AgentController ──────────────────────
|
|
340
|
+
skill_to_use = None
|
|
341
|
+
if auto_skill_state["value"] and skill_mgr:
|
|
342
|
+
detected = skill_mgr.detect_skill(user_input)
|
|
343
|
+
if detected and detected.name != active_skill:
|
|
344
|
+
skill_to_use = detected.name
|
|
345
|
+
skill_mgr.activate(skill_to_use)
|
|
346
|
+
if HAS_RICH:
|
|
347
|
+
ui.console.print(f" [yellow][auto-skill] {skill_to_use}[/yellow]")
|
|
348
|
+
else:
|
|
349
|
+
print(f" [auto-skill] {skill_to_use}")
|
|
350
|
+
|
|
351
|
+
# Clawd: notify the pet that the user submitted a new task
|
|
352
|
+
get_clawd().user_prompt(prompt=user_input)
|
|
353
|
+
|
|
354
|
+
# Submit to background thread
|
|
355
|
+
await controller.submit(
|
|
356
|
+
user_input, skill_name=skill_to_use,
|
|
357
|
+
explicit_model=explicit_model, stream=True,
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
# Drain events while agent is busy (keeps UI responsive)
|
|
361
|
+
try:
|
|
362
|
+
while controller.is_busy():
|
|
363
|
+
for event in await controller.event_queue.drain():
|
|
364
|
+
ui.on_event(event)
|
|
365
|
+
# Check for interrupt
|
|
366
|
+
if controller._cancel.is_set():
|
|
367
|
+
break
|
|
368
|
+
await asyncio.sleep(0.05)
|
|
369
|
+
# Drain remaining events after completion
|
|
370
|
+
for event in await controller.event_queue.drain():
|
|
371
|
+
ui.on_event(event)
|
|
372
|
+
except KeyboardInterrupt:
|
|
373
|
+
await controller.cancel()
|
|
374
|
+
print("\n[Interrupted]")
|
|
375
|
+
ui.reset_stream()
|
|
376
|
+
# Drain remaining events
|
|
377
|
+
for event in await controller.event_queue.drain():
|
|
378
|
+
ui.on_event(event)
|
|
379
|
+
continue
|
|
380
|
+
except Exception as e:
|
|
381
|
+
if HAS_RICH:
|
|
382
|
+
from rich.markup import escape as rich_escape
|
|
383
|
+
ui.console.print(f"\n[red bold]Error:[/red bold] [red]{rich_escape(str(e))}[/red]")
|
|
384
|
+
else:
|
|
385
|
+
print(f"\nError: {e}")
|
|
386
|
+
logger.exception("Agent run failed")
|
|
387
|
+
finally:
|
|
388
|
+
# Save session
|
|
389
|
+
if session_mgr and controller.agent and controller.agent.session_id:
|
|
390
|
+
try:
|
|
391
|
+
controller.agent.save_session()
|
|
392
|
+
except Exception:
|
|
393
|
+
pass
|
|
394
|
+
continue
|
|
395
|
+
|
|
396
|
+
running = await _dispatch_command(cmd, arg, controller.agent, config, ui,
|
|
397
|
+
skill_mgr, memory_store, session_mgr,
|
|
398
|
+
mcp_client, template_mgr, permission_store,
|
|
399
|
+
auto_skill_state)
|
|
400
|
+
|
|
401
|
+
if session_mgr and controller.agent and controller.agent._state.messages:
|
|
402
|
+
try:
|
|
403
|
+
controller.agent.save_session()
|
|
404
|
+
except Exception:
|
|
405
|
+
pass
|
|
406
|
+
tool_exec.clear_file_cache()
|
|
407
|
+
|
|
408
|
+
# Clawd: SessionEnd — await final event delivery before loop stops
|
|
409
|
+
await get_clawd().shutdown_async()
|
|
410
|
+
|
|
411
|
+
await controller.shutdown()
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
# ── Single task ─────────────────────────────────────────────────────────
|
|
415
|
+
|
|
416
|
+
async def run_single_task_async(task: str, config: AppConfig, **kwargs):
|
|
417
|
+
ui = ClaudeCodeUI()
|
|
418
|
+
subsystems = _init_subsystems(config, **kwargs)
|
|
419
|
+
permission_store = subsystems["permissions"]
|
|
420
|
+
|
|
421
|
+
# Wrap the built-in permission prompt with Clawd bubble support.
|
|
422
|
+
_clawd_perm_st = create_clawd_permission_handler()
|
|
423
|
+
_builtin_prompt_st = ui.permission_prompt
|
|
424
|
+
|
|
425
|
+
def _combined_permission_st(tool_name: str, arguments: dict, category: str) -> bool:
|
|
426
|
+
if _clawd_perm_st is not None:
|
|
427
|
+
result = _clawd_perm_st(tool_name, arguments, category)
|
|
428
|
+
if result is not None:
|
|
429
|
+
return result
|
|
430
|
+
return _builtin_prompt_st(tool_name, arguments, category)
|
|
431
|
+
|
|
432
|
+
permission_store.set_prompt_callback(_combined_permission_st)
|
|
433
|
+
|
|
434
|
+
if kwargs.get("allow_all"):
|
|
435
|
+
permission_store.set_category_rule("shell", PermissionMode.ALLOW)
|
|
436
|
+
permission_store.set_category_rule("write", PermissionMode.ALLOW)
|
|
437
|
+
|
|
438
|
+
explicit_model = kwargs.get("model", "") or ""
|
|
439
|
+
tool_exec = ToolExecutor(config.agent)
|
|
440
|
+
tool_exec.on_edit(ui.track_edit)
|
|
441
|
+
tool_exec.setup_file_cache(Path(config.agent.workspace_dir) / ".ata_coder" / "files")
|
|
442
|
+
|
|
443
|
+
agent = CoderAgent(
|
|
444
|
+
config=config, tool_executor=tool_exec,
|
|
445
|
+
subsystems=AgentSubsystems(
|
|
446
|
+
skills=subsystems["skills"], memory=subsystems["memory"],
|
|
447
|
+
mcp=subsystems["mcp"], templates=subsystems["templates"],
|
|
448
|
+
permissions=permission_store, project_info=subsystems["project"],
|
|
449
|
+
sessions=subsystems["sessions"],
|
|
450
|
+
),
|
|
451
|
+
)
|
|
452
|
+
agent.on_event(ui.on_event)
|
|
453
|
+
agent.llm.on_usage(ui.track_usage)
|
|
454
|
+
|
|
455
|
+
skill_name = kwargs.get("skill")
|
|
456
|
+
no_stream = kwargs.get("no_stream", False)
|
|
457
|
+
|
|
458
|
+
# Clawd: for single-task mode, the session IS this task
|
|
459
|
+
_clawd_st = get_clawd()
|
|
460
|
+
_clawd_st.start(session_id="", cwd=str(agent.tools.workspace), title=task)
|
|
461
|
+
_clawd_st.user_prompt(prompt=task)
|
|
462
|
+
|
|
463
|
+
try:
|
|
464
|
+
await agent.run(task, stream=not no_stream, skill_name=skill_name,
|
|
465
|
+
explicit_model=explicit_model)
|
|
466
|
+
print()
|
|
467
|
+
if subsystems["sessions"]:
|
|
468
|
+
mid = agent.save_session()
|
|
469
|
+
print(f"Session: {mid}")
|
|
470
|
+
return 0
|
|
471
|
+
except KeyboardInterrupt:
|
|
472
|
+
print("\nInterrupted.")
|
|
473
|
+
return 1
|
|
474
|
+
except Exception as e:
|
|
475
|
+
print(f"\nError: {e}")
|
|
476
|
+
logger.exception("Agent run failed")
|
|
477
|
+
return 1
|
|
478
|
+
finally:
|
|
479
|
+
tool_exec.clear_file_cache()
|
|
480
|
+
# Clawd: goodbye animation first
|
|
481
|
+
_clawd_st.shutdown()
|
|
482
|
+
await agent.shutdown()
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
# ── Command dispatch ────────────────────────────────────────────────────
|
|
486
|
+
|
|
487
|
+
_registry = None
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
async def _dispatch_command(cmd, arg, agent, config, ui, skill_mgr, memory_store,
|
|
491
|
+
session_mgr, mcp_client, template_mgr,
|
|
492
|
+
permission_store, auto_skill_state) -> bool:
|
|
493
|
+
global _registry
|
|
494
|
+
if _registry is None:
|
|
495
|
+
from .commands import build_registry
|
|
496
|
+
_registry = build_registry()
|
|
497
|
+
|
|
498
|
+
from .commands import CommandContext
|
|
499
|
+
ctx = CommandContext(
|
|
500
|
+
agent=agent, config=config, ui=ui,
|
|
501
|
+
skill_mgr=skill_mgr, memory_store=memory_store,
|
|
502
|
+
session_mgr=session_mgr, mcp_client=mcp_client,
|
|
503
|
+
template_mgr=template_mgr, permission_store=permission_store,
|
|
504
|
+
auto_skill_state=auto_skill_state,
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
result = await _registry.dispatch(cmd, arg, ctx)
|
|
508
|
+
if result is None:
|
|
509
|
+
from .commands import get_command_list
|
|
510
|
+
all_cmds = get_command_list()
|
|
511
|
+
matches = [(n, d) for n, d in all_cmds if n.startswith(cmd)]
|
|
512
|
+
if matches:
|
|
513
|
+
click.echo(f"\n Unknown: {cmd} — Did you mean?")
|
|
514
|
+
for name, desc in matches[:10]:
|
|
515
|
+
click.echo(f" {name:<18} {desc}")
|
|
516
|
+
else:
|
|
517
|
+
click.echo(f"\n Unknown: {cmd} — Available commands:")
|
|
518
|
+
shown = set()
|
|
519
|
+
for name, desc in sorted(all_cmds):
|
|
520
|
+
if name not in shown:
|
|
521
|
+
shown.add(name)
|
|
522
|
+
click.echo(f" {name:<18} {desc}")
|
|
523
|
+
click.echo()
|
|
524
|
+
return True
|
|
525
|
+
return result
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def _parse_command(user_input: str) -> tuple:
|
|
529
|
+
if user_input.startswith("/"):
|
|
530
|
+
parts = user_input.split(maxsplit=1)
|
|
531
|
+
return parts[0].lower(), parts[1] if len(parts) > 1 else ""
|
|
532
|
+
return None, user_input
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
# ═════════════════════════════════════════════════════════════════════════
|
|
536
|
+
# Click CLI
|
|
537
|
+
# ═════════════════════════════════════════════════════════════════════════
|
|
538
|
+
|
|
539
|
+
def _setup(config, kwargs):
|
|
540
|
+
"""Shared bootstrap: first-run check, UTF-8, logging, config, validation."""
|
|
541
|
+
import io
|
|
542
|
+
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
|
|
543
|
+
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace")
|
|
544
|
+
|
|
545
|
+
log_level = logging.DEBUG if kwargs.get("verbose") else logging.WARNING
|
|
546
|
+
logging.basicConfig(
|
|
547
|
+
level=log_level,
|
|
548
|
+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
from .settings import init_settings
|
|
552
|
+
init_settings()
|
|
553
|
+
|
|
554
|
+
_apply_config_overrides(config, kwargs)
|
|
555
|
+
|
|
556
|
+
errors = config.llm.validate()
|
|
557
|
+
if errors:
|
|
558
|
+
click.echo("\n[!] Configuration:", err=True)
|
|
559
|
+
for e in errors:
|
|
560
|
+
click.echo(f" - {e}", err=True)
|
|
561
|
+
click.echo(" Run 'ata' in interactive mode to set up your API key.\n", err=True)
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
# Shared click options
|
|
565
|
+
_GLOBAL_OPTIONS = [
|
|
566
|
+
click.option("--model", "-m", default=None, help="Model name"),
|
|
567
|
+
click.option("--api-key", "-k", default=None, help="API key"),
|
|
568
|
+
click.option("--base-url", "-b", default=None, help="API base URL"),
|
|
569
|
+
click.option("--workspace", "-w", default=None, help="Workspace directory"),
|
|
570
|
+
click.option("--verbose", is_flag=True, help="Verbose logging"),
|
|
571
|
+
click.option("--max-tool-calls", type=int, help="Max tool calls per task"),
|
|
572
|
+
click.option("--skill", "-s", default=None, help="Force a skill"),
|
|
573
|
+
click.option("--skills-dir", default=None, help="Custom skills directory"),
|
|
574
|
+
click.option("--no-skill-auto", is_flag=True, help="Disable skill auto-detection"),
|
|
575
|
+
click.option("--memory-dir", default=None, help="Custom memory directory"),
|
|
576
|
+
click.option("--mcp-config", default=None, help="MCP config JSON file"),
|
|
577
|
+
click.option("--resume", "-r", default=None, help="Resume a saved session"),
|
|
578
|
+
click.option("--prompts-dir", default=None, help="Custom prompts directory"),
|
|
579
|
+
click.option("--allow-all", "-A", is_flag=True, help="Allow all shell/write without prompting"),
|
|
580
|
+
click.option("--deny-shell", is_flag=True, help="Deny all shell commands"),
|
|
581
|
+
click.option("--think", type=click.Choice(["low", "medium", "high", "xhigh", "max"]),
|
|
582
|
+
help="Enable thinking mode"),
|
|
583
|
+
click.option("--anthropic", is_flag=True, help="Use Anthropic Messages API format"),
|
|
584
|
+
click.option("--no-stream", "-n", is_flag=True, help="Disable streaming"),
|
|
585
|
+
click.option("--version", "-v", is_flag=True, help="Show version and detailed info"),
|
|
586
|
+
]
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def _global_options(f):
|
|
590
|
+
for opt in reversed(_GLOBAL_OPTIONS):
|
|
591
|
+
f = opt(f)
|
|
592
|
+
return f
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def _display_width(s: str) -> int:
|
|
596
|
+
"""Calculate the terminal display width of a string (CJK ≈ 2 cells)."""
|
|
597
|
+
import unicodedata
|
|
598
|
+
w = 0
|
|
599
|
+
for ch in s:
|
|
600
|
+
ea = unicodedata.east_asian_width(ch)
|
|
601
|
+
w += 2 if ea in ("W", "F", "A") else 1 # A=Ambiguous, treated wide on CJK terminals
|
|
602
|
+
return w
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def _pad(s: str, target: int) -> str:
|
|
606
|
+
"""Pad *s* with spaces so its display width equals *target*."""
|
|
607
|
+
return s + " " * max(0, target - _display_width(s))
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def _print_version() -> None:
|
|
611
|
+
"""Print detailed version information and exit."""
|
|
612
|
+
try:
|
|
613
|
+
from importlib.metadata import version as pkg_version
|
|
614
|
+
pkg_ver = pkg_version("ata-coder")
|
|
615
|
+
except Exception:
|
|
616
|
+
pkg_ver = __version__
|
|
617
|
+
|
|
618
|
+
# Count tests dynamically
|
|
619
|
+
try:
|
|
620
|
+
test_dir = Path(__file__).parent / "tests"
|
|
621
|
+
test_files = list(test_dir.glob("test_*.py"))
|
|
622
|
+
test_count = sum(
|
|
623
|
+
len([l for l in f.read_text(encoding="utf-8").splitlines()
|
|
624
|
+
if l.strip().startswith("def test_")])
|
|
625
|
+
for f in test_files
|
|
626
|
+
)
|
|
627
|
+
except Exception:
|
|
628
|
+
test_count = "?"
|
|
629
|
+
|
|
630
|
+
# Count source files
|
|
631
|
+
src_files = len(list(Path(__file__).parent.glob("*.py")))
|
|
632
|
+
|
|
633
|
+
W = 40 # total content width inside borders
|
|
634
|
+
|
|
635
|
+
info = [
|
|
636
|
+
("Version", pkg_ver),
|
|
637
|
+
("Python", platform.python_version()),
|
|
638
|
+
("Platform", platform.system()),
|
|
639
|
+
("Source", f"{src_files} modules, ~{test_count} tests"),
|
|
640
|
+
("Tools", f"{len(TOOL_DEFINITIONS)} built-in"),
|
|
641
|
+
("License", "MIT"),
|
|
642
|
+
("Repo", "github.com/jiaheng0815/ata-coder"),
|
|
643
|
+
]
|
|
644
|
+
|
|
645
|
+
lines = ["┌" + "─" * (W + 2) + "┐"]
|
|
646
|
+
lines.append("│ " + _pad("ATA Coder", W) + "│")
|
|
647
|
+
lines.append("│" + " " * (W + 2) + "│")
|
|
648
|
+
for label, value in info:
|
|
649
|
+
line = f" {label}: {value}"
|
|
650
|
+
lines.append("│" + _pad(line, W + 2) + "│")
|
|
651
|
+
lines.append("└" + "─" * (W + 2) + "┘")
|
|
652
|
+
|
|
653
|
+
click.echo("\n".join(lines))
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
@click.group(invoke_without_command=True)
|
|
657
|
+
@_global_options
|
|
658
|
+
@click.pass_context
|
|
659
|
+
def cli(ctx, **kwargs):
|
|
660
|
+
"""ATA Coder — AI-powered coding assistant.
|
|
661
|
+
|
|
662
|
+
\b
|
|
663
|
+
Interactive: ata
|
|
664
|
+
Single task: ata run "your task here"
|
|
665
|
+
Server: ata server
|
|
666
|
+
"""
|
|
667
|
+
# Version flag — print and exit before anything else
|
|
668
|
+
if kwargs.get("version"):
|
|
669
|
+
_print_version()
|
|
670
|
+
return
|
|
671
|
+
|
|
672
|
+
if ctx.invoked_subcommand is not None:
|
|
673
|
+
return
|
|
674
|
+
|
|
675
|
+
# First-run setup BEFORE config validation
|
|
676
|
+
_ensure_first_run()
|
|
677
|
+
|
|
678
|
+
config = get_config()
|
|
679
|
+
_setup(config, kwargs)
|
|
680
|
+
ctx.obj = {"config": config, "kwargs": kwargs}
|
|
681
|
+
|
|
682
|
+
asyncio.run(run_interactive_async(config, **kwargs))
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
@cli.command("init")
|
|
686
|
+
def init_cmd():
|
|
687
|
+
"""Force re-run the setup wizard (overwrites existing config)."""
|
|
688
|
+
_ensure_first_run(force=True)
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
@cli.command("run")
|
|
692
|
+
@click.argument("task", required=True)
|
|
693
|
+
@click.pass_context
|
|
694
|
+
def run_cmd(ctx, task):
|
|
695
|
+
"""Run a single task."""
|
|
696
|
+
_ensure_first_run()
|
|
697
|
+
config = get_config()
|
|
698
|
+
kwargs = ctx.obj.get("kwargs", {}) if ctx.obj else {}
|
|
699
|
+
_setup(config, kwargs)
|
|
700
|
+
ctx.exit(asyncio.run(run_single_task_async(task, config, **kwargs)))
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
@cli.command("server")
|
|
704
|
+
@click.option("--port", "-p", type=int, default=8000, help="Server port")
|
|
705
|
+
@click.option("--host", default="127.0.0.1", help="Server host")
|
|
706
|
+
@click.pass_context
|
|
707
|
+
def server_cmd(ctx, port, host, **kwargs):
|
|
708
|
+
"""Start HTTP API server."""
|
|
709
|
+
_ensure_first_run()
|
|
710
|
+
config = get_config()
|
|
711
|
+
group_kwargs = ctx.obj.get("kwargs", {}) if ctx.obj else {}
|
|
712
|
+
group_kwargs.update(kwargs)
|
|
713
|
+
_setup(config, group_kwargs)
|
|
714
|
+
|
|
715
|
+
if group_kwargs.get("allow_all"):
|
|
716
|
+
os.environ["ATA_CODER_ALLOW_ALL"] = "1"
|
|
717
|
+
|
|
718
|
+
from .model_registry import fetch_available_models
|
|
719
|
+
models_list = [config.llm.model]
|
|
720
|
+
click.echo(f"Fetching models from {config.llm.base_url} ...")
|
|
721
|
+
fetched = fetch_available_models(config.llm.base_url, config.llm.api_key)
|
|
722
|
+
if fetched:
|
|
723
|
+
models_list = fetched
|
|
724
|
+
click.echo(f" {len(models_list)} model(s): {', '.join(models_list[:10])}")
|
|
725
|
+
else:
|
|
726
|
+
click.echo(f" Could not fetch models, using configured: {config.llm.model}")
|
|
727
|
+
|
|
728
|
+
os.environ["ATA_CODER_MODELS_CACHE"] = ",".join(models_list)
|
|
729
|
+
|
|
730
|
+
from .server import create_server
|
|
731
|
+
srv = create_server(config, host, port)
|
|
732
|
+
|
|
733
|
+
click.echo(f"""
|
|
734
|
+
ATA Coder API Server
|
|
735
|
+
URL: http://{host}:{port}
|
|
736
|
+
Model: {config.llm.model}
|
|
737
|
+
Models: {len(models_list)} available
|
|
738
|
+
Tools: {len(TOOL_DEFINITIONS)}
|
|
739
|
+
""")
|
|
740
|
+
|
|
741
|
+
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
|
742
|
+
try:
|
|
743
|
+
srv.serve_forever()
|
|
744
|
+
except KeyboardInterrupt:
|
|
745
|
+
pass
|
|
746
|
+
click.echo("\nServer stopped.")
|
|
747
|
+
srv.shutdown()
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
@cli.command("gui")
|
|
751
|
+
@click.option("--skill", "-s", default=None, help="Force a skill")
|
|
752
|
+
@click.option("--port", "-p", type=int, default=0, help="Server port (0=auto)")
|
|
753
|
+
@click.option("--no-browser", is_flag=True, help="Don't open browser")
|
|
754
|
+
@click.pass_context
|
|
755
|
+
def gui_cmd(ctx, skill, port, no_browser, **kwargs):
|
|
756
|
+
"""Launch web-based GUI (opens browser)."""
|
|
757
|
+
import socket
|
|
758
|
+
import webbrowser
|
|
759
|
+
_ensure_first_run()
|
|
760
|
+
config = get_config()
|
|
761
|
+
group_kwargs = ctx.obj.get("kwargs", {}) if ctx.obj else {}
|
|
762
|
+
group_kwargs.update(kwargs)
|
|
763
|
+
_setup(config, group_kwargs)
|
|
764
|
+
|
|
765
|
+
overrides = ctx.obj.get("kwargs", {}) if ctx.obj else {}
|
|
766
|
+
_apply_config_overrides(config, overrides)
|
|
767
|
+
if skill:
|
|
768
|
+
config.skill = skill
|
|
769
|
+
|
|
770
|
+
# Show agent and server events, but suppress noisy internal modules
|
|
771
|
+
logging.getLogger().setLevel(logging.WARNING)
|
|
772
|
+
logging.getLogger("ata_coder.server").setLevel(logging.INFO)
|
|
773
|
+
logging.getLogger("ata_coder.agent").setLevel(logging.INFO)
|
|
774
|
+
logging.getLogger("httpx").setLevel(logging.WARNING)
|
|
775
|
+
logging.getLogger("ata_coder.skills").setLevel(logging.WARNING)
|
|
776
|
+
logging.getLogger("ata_coder.extension").setLevel(logging.WARNING)
|
|
777
|
+
logging.getLogger("ata_coder.skill_extension").setLevel(logging.WARNING)
|
|
778
|
+
|
|
779
|
+
# Find available port
|
|
780
|
+
if port == 0:
|
|
781
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
782
|
+
s.bind(("127.0.0.1", 0))
|
|
783
|
+
port = s.getsockname()[1]
|
|
784
|
+
|
|
785
|
+
from .server import create_server
|
|
786
|
+
srv = create_server(config, "127.0.0.1", port)
|
|
787
|
+
url = f"http://127.0.0.1:{port}"
|
|
788
|
+
|
|
789
|
+
click.echo(f"""
|
|
790
|
+
╔══════════════════════════════════════════════════╗
|
|
791
|
+
║ ATA Coder — Web GUI ║
|
|
792
|
+
╠══════════════════════════════════════════════════╣
|
|
793
|
+
║ URL: {url:<38}║
|
|
794
|
+
║ Model: {config.llm.model:<38}║
|
|
795
|
+
║ Workspace: {config.agent.workspace_dir[:36]:<36}║
|
|
796
|
+
╚══════════════════════════════════════════════════╝
|
|
797
|
+
""")
|
|
798
|
+
|
|
799
|
+
if not no_browser:
|
|
800
|
+
webbrowser.open(url)
|
|
801
|
+
|
|
802
|
+
try:
|
|
803
|
+
srv.serve_forever()
|
|
804
|
+
except KeyboardInterrupt:
|
|
805
|
+
pass
|
|
806
|
+
click.echo("Server stopped.")
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
def main():
|
|
810
|
+
cli(standalone_mode=True)
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
if __name__ == "__main__":
|
|
814
|
+
sys.exit(main())
|