ata-coder 2.4.2__py3-none-any.whl

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