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/settings.py
ADDED
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Central settings management for ATA Coder.
|
|
3
|
+
|
|
4
|
+
Stores all persistent configuration in ~/.ata_coder/settings.json.
|
|
5
|
+
The ``env`` section is the canonical source for provider configuration
|
|
6
|
+
(base URL, API key, model mapping, tokens, effort level) — it mirrors
|
|
7
|
+
the Claude Code settings format so a single file works for both tools.
|
|
8
|
+
|
|
9
|
+
Legacy ``api`` / ``model`` / ``vision`` top-level keys are still
|
|
10
|
+
respected as fallbacks, but new config should go into ``env``.
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
from .settings import get_settings
|
|
14
|
+
s = get_settings()
|
|
15
|
+
model = s.model_for(task) # auto-route based on complexity
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import logging
|
|
20
|
+
import os
|
|
21
|
+
import shutil
|
|
22
|
+
import sysconfig
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
# ── Default settings ──────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
DEFAULT_SETTINGS: dict[str, Any] = {
|
|
32
|
+
"env": {
|
|
33
|
+
# Provider-agnostic configuration (Claude Code compatible).
|
|
34
|
+
# These are the canonical keys — everything else falls back here.
|
|
35
|
+
"ATA_CODER_BASE_URL": "https://api.deepseek.com",
|
|
36
|
+
"ATA_CODER_API_KEY": "",
|
|
37
|
+
"ATA_CODER_DEFAULT_MODEL": "deepseek-v4-pro",
|
|
38
|
+
"ATA_CODER_DEFAULT_OPUS_MODEL": "deepseek-v4-pro",
|
|
39
|
+
"ATA_CODER_DEFAULT_SONNET_MODEL": "deepseek-v4-pro",
|
|
40
|
+
"ATA_CODER_DEFAULT_HAIKU_MODEL": "deepseek-v4-flash",
|
|
41
|
+
"ATA_CODER_SUBAGENT_MODEL": "deepseek-v4-flash",
|
|
42
|
+
"ATA_CODER_MAX_OUTPUT_TOKENS": "16384",
|
|
43
|
+
"ATA_CODER_EFFORT_LEVEL": "",
|
|
44
|
+
# Vision config (empty = inherit from main model/API)
|
|
45
|
+
"ATA_CODER_VISION_MODEL": "",
|
|
46
|
+
"ATA_CODER_VISION_API_BASE": "",
|
|
47
|
+
"ATA_CODER_VISION_API_KEY": "",
|
|
48
|
+
},
|
|
49
|
+
"vision": {
|
|
50
|
+
# Override vision-specific provider/model (empty = inherit from env).
|
|
51
|
+
"model": "",
|
|
52
|
+
"api_base": "",
|
|
53
|
+
"api_key": "",
|
|
54
|
+
},
|
|
55
|
+
"complexity": {
|
|
56
|
+
"auto_detect": True,
|
|
57
|
+
"simple_max_chars": 60,
|
|
58
|
+
"complex_min_chars": 500,
|
|
59
|
+
},
|
|
60
|
+
"paths": {
|
|
61
|
+
"data": "~/.ata_coder",
|
|
62
|
+
"skills": "~/.ata_coder/skills",
|
|
63
|
+
"sessions": "~/.ata_coder/sessions",
|
|
64
|
+
"memory": "~/.ata_coder/memory",
|
|
65
|
+
"changes": "~/.ata_coder/changes",
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ── Settings class ────────────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class Settings:
|
|
74
|
+
"""Singleton settings manager backed by ~/.ata_coder/settings.json."""
|
|
75
|
+
|
|
76
|
+
_data: dict[str, Any] = field(default_factory=dict)
|
|
77
|
+
_file: Path | None = None
|
|
78
|
+
|
|
79
|
+
# ── Init / Load / Save ──────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
def load(self, file_path: str | Path | None = None) -> "Settings":
|
|
82
|
+
"""Load settings from disk, creating defaults if needed."""
|
|
83
|
+
if file_path:
|
|
84
|
+
self._file = Path(file_path)
|
|
85
|
+
else:
|
|
86
|
+
self._file = Path.home() / ".ata_coder" / "settings.json"
|
|
87
|
+
|
|
88
|
+
self._file.parent.mkdir(parents=True, exist_ok=True)
|
|
89
|
+
|
|
90
|
+
if self._file.exists():
|
|
91
|
+
try:
|
|
92
|
+
with open(self._file, "r", encoding="utf-8") as f:
|
|
93
|
+
loaded = json.load(f)
|
|
94
|
+
self._data = self._deep_merge(DEFAULT_SETTINGS, loaded)
|
|
95
|
+
logger.debug("Loaded settings from %s", self._file)
|
|
96
|
+
except Exception as e:
|
|
97
|
+
logger.warning("Failed to load %s, using defaults: %s", self._file, e)
|
|
98
|
+
self._data = dict(DEFAULT_SETTINGS)
|
|
99
|
+
self.save()
|
|
100
|
+
else:
|
|
101
|
+
self._data = dict(DEFAULT_SETTINGS)
|
|
102
|
+
self.save()
|
|
103
|
+
logger.info("Created default settings at %s", self._file)
|
|
104
|
+
|
|
105
|
+
return self
|
|
106
|
+
|
|
107
|
+
def save(self) -> None:
|
|
108
|
+
"""Persist current settings to disk.
|
|
109
|
+
|
|
110
|
+
Only writes keys that are in DEFAULT_SETTINGS — extra fields that
|
|
111
|
+
were merged in from disk (e.g. Claude Code hooks, plugins) are
|
|
112
|
+
carried forward because ``_deep_merge`` preserves them in ``_data``,
|
|
113
|
+
but ``save()`` writes the full ``_data`` dict. If you want to
|
|
114
|
+
strip unknown keys, delete them via ``set(key, None)`` first.
|
|
115
|
+
"""
|
|
116
|
+
if not self._file:
|
|
117
|
+
return
|
|
118
|
+
try:
|
|
119
|
+
with open(self._file, "w", encoding="utf-8") as f:
|
|
120
|
+
json.dump(self._data, f, indent=2, ensure_ascii=False)
|
|
121
|
+
# Restrict permissions: owner-only read/write (protects API keys)
|
|
122
|
+
os.chmod(self._file, 0o600)
|
|
123
|
+
except Exception as e:
|
|
124
|
+
logger.error("Failed to save settings: %s", e)
|
|
125
|
+
|
|
126
|
+
def reload(self) -> None:
|
|
127
|
+
"""Reload settings from disk."""
|
|
128
|
+
self.load(self._file)
|
|
129
|
+
|
|
130
|
+
# ── Access ──────────────────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
def get(self, *keys: str, default: Any = None) -> Any:
|
|
133
|
+
"""Get a nested key, e.g. settings.get('model', 'default')."""
|
|
134
|
+
node = self._data
|
|
135
|
+
for k in keys:
|
|
136
|
+
if isinstance(node, dict):
|
|
137
|
+
node = node.get(k)
|
|
138
|
+
else:
|
|
139
|
+
return default
|
|
140
|
+
if node is None:
|
|
141
|
+
return default
|
|
142
|
+
return node
|
|
143
|
+
|
|
144
|
+
def set(self, *keys: str, value: Any, save: bool = True) -> None:
|
|
145
|
+
"""Set a nested key, e.g. settings.set('model', 'default', 'gpt-4o')."""
|
|
146
|
+
node = self._data
|
|
147
|
+
for k in keys[:-1]:
|
|
148
|
+
if k not in node:
|
|
149
|
+
node[k] = {}
|
|
150
|
+
node = node[k]
|
|
151
|
+
node[keys[-1]] = value
|
|
152
|
+
if save:
|
|
153
|
+
self.save()
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def data(self) -> dict:
|
|
157
|
+
return self._data
|
|
158
|
+
|
|
159
|
+
# ── Path helpers ────────────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
def resolve_path(self, key: str) -> Path:
|
|
162
|
+
"""Resolve a path from settings (expanding ~)."""
|
|
163
|
+
raw = self.get("paths", key, default=f"~/.ata_coder/{key}")
|
|
164
|
+
return Path(raw).expanduser().resolve()
|
|
165
|
+
|
|
166
|
+
# ── env section (canonical) ─────────────────────────────────────────────
|
|
167
|
+
# Every provider-adjacent property reads ``env`` FIRST, then falls back
|
|
168
|
+
# to the legacy ``api`` / ``model`` / ``vision`` sections, then to a
|
|
169
|
+
# hardcoded default. This way a single ``env`` block is sufficient, but
|
|
170
|
+
# old settings files without ``env`` still work.
|
|
171
|
+
|
|
172
|
+
def _env_val(self, key: str, default: str = "") -> str:
|
|
173
|
+
"""Read a value from the ``env`` section."""
|
|
174
|
+
return self.get("env", key, default=default)
|
|
175
|
+
|
|
176
|
+
# ── API ─────────────────────────────────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def api_base_url(self) -> str:
|
|
180
|
+
return (
|
|
181
|
+
self._env_val("ATA_CODER_BASE_URL")
|
|
182
|
+
or self.get("api", "base_url", default="") # legacy
|
|
183
|
+
or "https://api.deepseek.com"
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
@property
|
|
187
|
+
def api_key(self) -> str:
|
|
188
|
+
return (
|
|
189
|
+
self._env_val("ATA_CODER_API_KEY")
|
|
190
|
+
or self.get("api", "api_key", default="") # legacy
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
# ── Model routing ───────────────────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
@property
|
|
196
|
+
def default_model(self) -> str:
|
|
197
|
+
return (
|
|
198
|
+
self._env_val("ATA_CODER_DEFAULT_MODEL")
|
|
199
|
+
or self.get("model", "default", default="") # legacy
|
|
200
|
+
or "deepseek-v4-pro"
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
@property
|
|
204
|
+
def model_opus(self) -> str:
|
|
205
|
+
return (
|
|
206
|
+
self._env_val("ATA_CODER_DEFAULT_OPUS_MODEL")
|
|
207
|
+
or self.get("model", "mapping", "opus", default="") # legacy
|
|
208
|
+
or self.default_model
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
@property
|
|
212
|
+
def model_sonnet(self) -> str:
|
|
213
|
+
return (
|
|
214
|
+
self._env_val("ATA_CODER_DEFAULT_SONNET_MODEL")
|
|
215
|
+
or self.get("model", "mapping", "sonnet", default="")
|
|
216
|
+
or self.default_model
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
@property
|
|
220
|
+
def model_haiku(self) -> str:
|
|
221
|
+
return (
|
|
222
|
+
self._env_val("ATA_CODER_DEFAULT_HAIKU_MODEL")
|
|
223
|
+
or self.get("model", "mapping", "haiku", default="")
|
|
224
|
+
or self.default_model
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
@property
|
|
228
|
+
def model_subagent(self) -> str:
|
|
229
|
+
return (
|
|
230
|
+
self._env_val("ATA_CODER_SUBAGENT_MODEL")
|
|
231
|
+
or self.get("model", "mapping", "subagent", default="")
|
|
232
|
+
or self.default_model
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
@property
|
|
236
|
+
def max_output_tokens(self) -> int:
|
|
237
|
+
try:
|
|
238
|
+
return int(self._env_val("ATA_CODER_MAX_OUTPUT_TOKENS", "16384"))
|
|
239
|
+
except (ValueError, TypeError):
|
|
240
|
+
return 16384
|
|
241
|
+
|
|
242
|
+
@property
|
|
243
|
+
def effort_level(self) -> str:
|
|
244
|
+
return self._env_val("ATA_CODER_EFFORT_LEVEL")
|
|
245
|
+
|
|
246
|
+
@property
|
|
247
|
+
def use_anthropic(self) -> bool:
|
|
248
|
+
"""Whether to use Anthropic Messages API format (instead of OpenAI)."""
|
|
249
|
+
return self._env_val("ATA_CODER_USE_ANTHROPIC") == "1"
|
|
250
|
+
|
|
251
|
+
# ── Vision (still uses dedicated section — optional override) ───────────
|
|
252
|
+
|
|
253
|
+
@property
|
|
254
|
+
def vision_model(self) -> str:
|
|
255
|
+
"""Vision model override (empty = use main model from ATA_CODER_DEFAULT_MODEL)."""
|
|
256
|
+
return self._env_val("ATA_CODER_VISION_MODEL") or self.get("vision", "model", default="")
|
|
257
|
+
|
|
258
|
+
@property
|
|
259
|
+
def vision_api_base(self) -> str:
|
|
260
|
+
"""Vision API base override (empty = use main ATA_CODER_BASE_URL)."""
|
|
261
|
+
return self._env_val("ATA_CODER_VISION_API_BASE") or self.get("vision", "api_base", default="")
|
|
262
|
+
|
|
263
|
+
@property
|
|
264
|
+
def vision_api_key(self) -> str:
|
|
265
|
+
"""Vision API key override (empty = use main ATA_CODER_API_KEY)."""
|
|
266
|
+
return self._env_val("ATA_CODER_VISION_API_KEY") or self.get("vision", "api_key", default="")
|
|
267
|
+
|
|
268
|
+
# ── Directories ─────────────────────────────────────────────────────────
|
|
269
|
+
|
|
270
|
+
@property
|
|
271
|
+
def data_dir(self) -> Path:
|
|
272
|
+
return self.resolve_path("data")
|
|
273
|
+
|
|
274
|
+
@property
|
|
275
|
+
def skills_dir(self) -> Path:
|
|
276
|
+
return self.resolve_path("skills")
|
|
277
|
+
|
|
278
|
+
@property
|
|
279
|
+
def sessions_dir(self) -> Path:
|
|
280
|
+
return self.resolve_path("sessions")
|
|
281
|
+
|
|
282
|
+
@property
|
|
283
|
+
def memory_dir(self) -> Path:
|
|
284
|
+
return self.resolve_path("memory")
|
|
285
|
+
|
|
286
|
+
@property
|
|
287
|
+
def changes_dir(self) -> Path:
|
|
288
|
+
return self.resolve_path("changes")
|
|
289
|
+
|
|
290
|
+
def ensure_dirs(self) -> None:
|
|
291
|
+
"""Create all configured directories."""
|
|
292
|
+
for key in ("data", "skills", "sessions", "memory", "changes"):
|
|
293
|
+
d = self.resolve_path(key)
|
|
294
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
295
|
+
|
|
296
|
+
# ── Complexity ──────────────────────────────────────────────────────────
|
|
297
|
+
|
|
298
|
+
def shortcut_classify(self, task: str) -> str | None:
|
|
299
|
+
"""
|
|
300
|
+
Quick length-based shortcut — skip AI classify for obvious cases.
|
|
301
|
+
Returns 'simple', 'complex', or None (None = need AI classify).
|
|
302
|
+
"""
|
|
303
|
+
if not self.get("complexity", "auto_detect", default=True):
|
|
304
|
+
return "normal"
|
|
305
|
+
|
|
306
|
+
task_len = len(task.strip())
|
|
307
|
+
simple_max = self.get("complexity", "simple_max_chars", default=60)
|
|
308
|
+
complex_min = self.get("complexity", "complex_min_chars", default=500)
|
|
309
|
+
|
|
310
|
+
if task_len <= simple_max:
|
|
311
|
+
return "simple"
|
|
312
|
+
if task_len >= complex_min:
|
|
313
|
+
return "complex"
|
|
314
|
+
return None # middle ground → let AI decide
|
|
315
|
+
|
|
316
|
+
# ── Internal ────────────────────────────────────────────────────────────
|
|
317
|
+
|
|
318
|
+
@staticmethod
|
|
319
|
+
def _deep_merge(base: dict, override: dict) -> dict:
|
|
320
|
+
"""Recursively merge override into base.
|
|
321
|
+
|
|
322
|
+
Dict values are recursively merged; lists and scalars are replaced
|
|
323
|
+
wholesale (no append). This is intentional — lists like
|
|
324
|
+
``allowed_commands`` represent the user's explicit choice, not a
|
|
325
|
+
cumulative set.
|
|
326
|
+
"""
|
|
327
|
+
result = dict(base)
|
|
328
|
+
for k, v in override.items():
|
|
329
|
+
if k in result and isinstance(result[k], dict) and isinstance(v, dict):
|
|
330
|
+
result[k] = Settings._deep_merge(result[k], v)
|
|
331
|
+
elif k in result and isinstance(result[k], list) and isinstance(v, list):
|
|
332
|
+
# List override: user's values replace defaults (explicit choice)
|
|
333
|
+
result[k] = v
|
|
334
|
+
else:
|
|
335
|
+
result[k] = v
|
|
336
|
+
return result
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
# ── Global singleton ──────────────────────────────────────────────────────────
|
|
340
|
+
|
|
341
|
+
_settings: Settings | None = None
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def get_settings(file_path: str | Path | None = None) -> Settings:
|
|
345
|
+
"""Get or create the global Settings singleton."""
|
|
346
|
+
global _settings
|
|
347
|
+
if _settings is None:
|
|
348
|
+
_settings = Settings().load(file_path)
|
|
349
|
+
return _settings
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def init_settings(file_path: str | Path | None = None) -> Settings:
|
|
353
|
+
"""Initialize settings, seeding skills from project source if needed."""
|
|
354
|
+
settings = get_settings(file_path)
|
|
355
|
+
|
|
356
|
+
# Ensure all directories exist
|
|
357
|
+
settings.ensure_dirs()
|
|
358
|
+
|
|
359
|
+
# Seed default skills from project source → ~/.ata_coder/skills/
|
|
360
|
+
_seed_skills(settings)
|
|
361
|
+
|
|
362
|
+
# Seed default memories from project source → ~/.ata_coder/memory/
|
|
363
|
+
_seed_memories(settings)
|
|
364
|
+
|
|
365
|
+
return settings
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _find_source_dir(name: str) -> Path | None:
|
|
369
|
+
"""Find a data directory (skills, memory, prompts) in various locations."""
|
|
370
|
+
# 1. Development: next to this file (project root)
|
|
371
|
+
candidate = Path(__file__).parent / name
|
|
372
|
+
if candidate.is_dir():
|
|
373
|
+
return candidate
|
|
374
|
+
# 2. pip install: site-packages data_files
|
|
375
|
+
data_path = Path(sysconfig.get_path("data"))
|
|
376
|
+
candidate = data_path / name
|
|
377
|
+
if candidate.is_dir():
|
|
378
|
+
return candidate
|
|
379
|
+
# 3. pip install (user): user site data
|
|
380
|
+
user_data = Path(sysconfig.get_path("data", scheme="nt_user" if os.name == "nt" else "posix_user"))
|
|
381
|
+
candidate = user_data / name
|
|
382
|
+
if candidate.is_dir():
|
|
383
|
+
return candidate
|
|
384
|
+
return None
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _seed_skills(settings: Settings) -> None:
|
|
388
|
+
"""Copy skill folders from project source to ~/.ata_coder/skills/ if empty."""
|
|
389
|
+
target = settings.skills_dir
|
|
390
|
+
|
|
391
|
+
# Check if skills already exist (folder-based or flat legacy)
|
|
392
|
+
has_skills = any(
|
|
393
|
+
(d / "SKILL.md").exists() or (d / "manifest.json").exists()
|
|
394
|
+
for d in target.iterdir() if d.is_dir()
|
|
395
|
+
)
|
|
396
|
+
if has_skills:
|
|
397
|
+
return
|
|
398
|
+
|
|
399
|
+
source = _find_source_dir("skills")
|
|
400
|
+
if not source:
|
|
401
|
+
logger.debug("No skills source dir found, skipping seed")
|
|
402
|
+
return
|
|
403
|
+
|
|
404
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
405
|
+
for d in source.iterdir():
|
|
406
|
+
if d.is_dir() and not d.name.startswith("."):
|
|
407
|
+
dest = target / d.name
|
|
408
|
+
if not dest.exists():
|
|
409
|
+
shutil.copytree(d, dest)
|
|
410
|
+
logger.info("Seeded skill folder: %s", d.name)
|
|
411
|
+
for fp in source.glob("*.md"):
|
|
412
|
+
dest = target / fp.name
|
|
413
|
+
if not dest.exists():
|
|
414
|
+
shutil.copy2(fp, dest)
|
|
415
|
+
logger.info("Seeded skill: %s", fp.name)
|
|
416
|
+
for fp in source.glob("*.json"):
|
|
417
|
+
dest = target / fp.name
|
|
418
|
+
if not dest.exists():
|
|
419
|
+
shutil.copy2(fp, dest)
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _seed_memories(settings: Settings) -> None:
|
|
423
|
+
"""Copy default memory files from project source to ~/.ata_coder/memory/ if empty."""
|
|
424
|
+
target = settings.memory_dir
|
|
425
|
+
if list(target.glob("*.md")):
|
|
426
|
+
return
|
|
427
|
+
|
|
428
|
+
source = _find_source_dir("memory")
|
|
429
|
+
if not source:
|
|
430
|
+
logger.debug("No memory source dir found, skipping seed")
|
|
431
|
+
return
|
|
432
|
+
|
|
433
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
434
|
+
for fp in source.glob("*"):
|
|
435
|
+
if fp.name == "__pycache__":
|
|
436
|
+
continue
|
|
437
|
+
if fp.is_file():
|
|
438
|
+
shutil.copy2(fp, target / fp.name)
|
|
439
|
+
logger.info("Seeded memory: %s → %s", fp.name, target / fp.name)
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""First-run interactive configuration wizard — extracted from main.py."""
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def is_configured() -> bool:
|
|
8
|
+
"""Check if settings.json exists and has an API key."""
|
|
9
|
+
settings_file = Path.home() / ".ata_coder" / "settings.json"
|
|
10
|
+
if not settings_file.exists():
|
|
11
|
+
return False
|
|
12
|
+
try:
|
|
13
|
+
import json as _json
|
|
14
|
+
data = _json.loads(settings_file.read_text(encoding="utf-8"))
|
|
15
|
+
legacy_key = data.get("api", {}).get("api_key", "").strip()
|
|
16
|
+
env_key = data.get("env", {}).get("ATA_CODER_API_KEY", "").strip()
|
|
17
|
+
return bool(legacy_key or env_key)
|
|
18
|
+
except Exception:
|
|
19
|
+
return False
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def run_setup_wizard() -> None:
|
|
23
|
+
"""Interactive API configuration wizard."""
|
|
24
|
+
settings_dir = Path.home() / ".ata_coder"
|
|
25
|
+
settings_file = settings_dir / "settings.json"
|
|
26
|
+
|
|
27
|
+
print(" 检测到首次运行,开始初始化配置...")
|
|
28
|
+
print()
|
|
29
|
+
|
|
30
|
+
# ── API Base URL ────────────────────────────────────
|
|
31
|
+
default_url = "https://api.deepseek.com"
|
|
32
|
+
print(f" API Base URL [默认: {default_url}]:")
|
|
33
|
+
try:
|
|
34
|
+
base_url = input(" > ").strip()
|
|
35
|
+
except (KeyboardInterrupt, EOFError):
|
|
36
|
+
print("\n 配置取消。")
|
|
37
|
+
sys.exit(0)
|
|
38
|
+
if not base_url:
|
|
39
|
+
base_url = default_url
|
|
40
|
+
|
|
41
|
+
# ── API Key ─────────────────────────────────────────
|
|
42
|
+
print()
|
|
43
|
+
print(" API Key (输入会隐藏):")
|
|
44
|
+
try:
|
|
45
|
+
if os.name == "nt":
|
|
46
|
+
import msvcrt
|
|
47
|
+
api_key_parts: list[str] = []
|
|
48
|
+
while True:
|
|
49
|
+
ch = msvcrt.getch()
|
|
50
|
+
if ch in (b"\r", b"\n"):
|
|
51
|
+
break
|
|
52
|
+
if ch == b"\x08":
|
|
53
|
+
if api_key_parts:
|
|
54
|
+
api_key_parts.pop()
|
|
55
|
+
elif ch == b"\x03":
|
|
56
|
+
print("\n 配置取消。")
|
|
57
|
+
sys.exit(0)
|
|
58
|
+
else:
|
|
59
|
+
api_key_parts.append(ch.decode("utf-8", errors="replace"))
|
|
60
|
+
api_key = "".join(api_key_parts)
|
|
61
|
+
else:
|
|
62
|
+
import tty
|
|
63
|
+
import termios
|
|
64
|
+
fd = sys.stdin.fileno()
|
|
65
|
+
old = termios.tcgetattr(fd)
|
|
66
|
+
try:
|
|
67
|
+
tty.setraw(fd)
|
|
68
|
+
api_key = ""
|
|
69
|
+
while True:
|
|
70
|
+
ch = sys.stdin.read(1)
|
|
71
|
+
if ch in ("\r", "\n"):
|
|
72
|
+
break
|
|
73
|
+
if ch == "\x03":
|
|
74
|
+
print("\n 配置取消。")
|
|
75
|
+
sys.exit(0)
|
|
76
|
+
api_key += ch
|
|
77
|
+
finally:
|
|
78
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old)
|
|
79
|
+
except (KeyboardInterrupt, EOFError):
|
|
80
|
+
print("\n 配置取消。")
|
|
81
|
+
sys.exit(0)
|
|
82
|
+
print()
|
|
83
|
+
|
|
84
|
+
if not api_key.strip():
|
|
85
|
+
print(" ⚠ 未输入 API Key。可稍后编辑 ~/.ata_coder/settings.json")
|
|
86
|
+
print()
|
|
87
|
+
|
|
88
|
+
# ── Write settings ──────────────────────────────────
|
|
89
|
+
settings_dir.mkdir(parents=True, exist_ok=True)
|
|
90
|
+
from .settings import Settings
|
|
91
|
+
s = Settings().load(settings_file)
|
|
92
|
+
s.set("env", "ATA_CODER_BASE_URL", base_url, save=False)
|
|
93
|
+
s.set("env", "ATA_CODER_API_KEY", api_key.strip(), save=False)
|
|
94
|
+
s.save()
|
|
95
|
+
print(f" ✓ 配置已保存到 {settings_file}")
|
|
96
|
+
print()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
__version__ = "2.4.2"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def print_banner() -> None:
|
|
103
|
+
"""Startup banner — project identity in 0.1 seconds."""
|
|
104
|
+
try:
|
|
105
|
+
test_dir = Path(__file__).parent / "tests"
|
|
106
|
+
test_files = list(test_dir.glob("test_*.py"))
|
|
107
|
+
test_count = sum(
|
|
108
|
+
len([l for l in f.read_text(encoding="utf-8").splitlines()
|
|
109
|
+
if l.strip().startswith("def test_")])
|
|
110
|
+
for f in test_files
|
|
111
|
+
)
|
|
112
|
+
except Exception:
|
|
113
|
+
test_count = "?"
|
|
114
|
+
|
|
115
|
+
print(fr"""
|
|
116
|
+
┌─ ATA Coder v{__version__} ────────────┐
|
|
117
|
+
│ 🤖 Self-Bootstrapped AI Assistant │
|
|
118
|
+
│ 📦 {test_count} tests passed {' ' if len(str(test_count)) < 3 else ''} │
|
|
119
|
+
│ 🔒 Security-hardened by self-audit │
|
|
120
|
+
└─────────────────────────────────────┘
|
|
121
|
+
""")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def ensure_first_run(force: bool = False) -> None:
|
|
125
|
+
"""Check config; if not configured (or force=True), run setup wizard.
|
|
126
|
+
|
|
127
|
+
Scenarios:
|
|
128
|
+
A) No config → banner + setup wizard
|
|
129
|
+
B) ata init → force setup wizard (overwrite)
|
|
130
|
+
C) Has config → silent pass, straight to REPL
|
|
131
|
+
"""
|
|
132
|
+
if not force and is_configured():
|
|
133
|
+
return
|
|
134
|
+
if not force:
|
|
135
|
+
print_banner()
|
|
136
|
+
run_setup_wizard()
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
SkillExtension — adapter that wraps a Skill as an Extension.
|
|
4
|
+
|
|
5
|
+
Lets the ExtensionManager manage Skills and non-Skill extensions
|
|
6
|
+
uniformly, enabling multi-skill + extension coexistence.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
from .extension import Extension, ExtensionMeta
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from .skills import Skill
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
__all__ = ["SkillExtension"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SkillExtension(Extension):
|
|
23
|
+
"""
|
|
24
|
+
Adapter: wraps a Skill (from skills.py) as an Extension.
|
|
25
|
+
|
|
26
|
+
Skill.system_prompt → get_prompt()
|
|
27
|
+
Skill.tools (list[str]) → get_tools() (list of tool names)
|
|
28
|
+
Skill.triggers are NOT mapped (SkillManager handles detection separately).
|
|
29
|
+
|
|
30
|
+
Usage:
|
|
31
|
+
from .skills import Skill
|
|
32
|
+
from .skill_extension import SkillExtension
|
|
33
|
+
|
|
34
|
+
skill = Skill(name="debugger", ...)
|
|
35
|
+
ext = SkillExtension(skill)
|
|
36
|
+
manager.register(ext)
|
|
37
|
+
manager.activate("skill:debugger")
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, skill: "Skill"):
|
|
41
|
+
"""
|
|
42
|
+
Args:
|
|
43
|
+
skill: skills.Skill instance
|
|
44
|
+
"""
|
|
45
|
+
self._skill = skill
|
|
46
|
+
self.meta = ExtensionMeta(
|
|
47
|
+
name=f"skill:{skill.name}",
|
|
48
|
+
version="1.0.0",
|
|
49
|
+
description=skill.description or f"Skill: {skill.name}",
|
|
50
|
+
tags=["skill", skill.name],
|
|
51
|
+
priority=80, # Skills have higher priority than general extensions (default 100)
|
|
52
|
+
)
|
|
53
|
+
# Skill-to-skill dependencies (Python packages go in requirements.txt instead)
|
|
54
|
+
if hasattr(skill, "dependencies") and skill.dependencies:
|
|
55
|
+
self.meta.dependencies = list(skill.dependencies)
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def skill_name(self) -> str:
|
|
59
|
+
"""The raw skill name (without the 'skill:' prefix)."""
|
|
60
|
+
return self._skill.name
|
|
61
|
+
|
|
62
|
+
# ── Extension interface ─────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
def get_prompt(self) -> str:
|
|
65
|
+
"""返回 skill 的 system prompt(含 @include 解析)。"""
|
|
66
|
+
return self._skill.resolve_includes(self._skill.system_prompt or "")
|
|
67
|
+
|
|
68
|
+
def get_tools(self) -> list[str]:
|
|
69
|
+
"""
|
|
70
|
+
返回 skill 限制的工具名称列表。
|
|
71
|
+
空列表 = 允许所有工具。
|
|
72
|
+
"""
|
|
73
|
+
if hasattr(self._skill, "tools") and self._skill.tools:
|
|
74
|
+
return list(self._skill.tools)
|
|
75
|
+
return []
|
|
76
|
+
|
|
77
|
+
def on_activate(self) -> None:
|
|
78
|
+
"""Skill 被激活时的回调。"""
|
|
79
|
+
logger.info("Skill activated via extension: %s", self._skill.name)
|
|
80
|
+
|
|
81
|
+
def on_deactivate(self) -> None:
|
|
82
|
+
"""Skill 被停用时的回调。"""
|
|
83
|
+
logger.info("Skill deactivated via extension: %s", self._skill.name)
|
|
84
|
+
|
|
85
|
+
def validate(self) -> tuple[bool, str]:
|
|
86
|
+
"""验证 skill 是否可用。"""
|
|
87
|
+
if not self._skill.system_prompt:
|
|
88
|
+
return False, "Skill has no system_prompt"
|
|
89
|
+
return True, "OK"
|
|
90
|
+
|
|
91
|
+
def __repr__(self) -> str:
|
|
92
|
+
return f"SkillExtension(name={self._skill.name!r})"
|