ata-coder 2.4.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. ata_coder/__init__.py +1 -0
  2. ata_coder/agent.py +874 -0
  3. ata_coder/agent_compact.py +190 -0
  4. ata_coder/agent_controller.py +218 -0
  5. ata_coder/agent_extension.py +69 -0
  6. ata_coder/agent_routing.py +105 -0
  7. ata_coder/agent_subsystems.py +72 -0
  8. ata_coder/agent_tools.py +318 -0
  9. ata_coder/agent_undo.py +63 -0
  10. ata_coder/anthropic_client.py +465 -0
  11. ata_coder/change_tracker.py +368 -0
  12. ata_coder/clawd_integration.py +574 -0
  13. ata_coder/commands/__init__.py +128 -0
  14. ata_coder/commands/_core.py +184 -0
  15. ata_coder/commands/_safety.py +95 -0
  16. ata_coder/commands/_settings.py +241 -0
  17. ata_coder/commands/_workflow.py +451 -0
  18. ata_coder/commands.py +974 -0
  19. ata_coder/config.py +257 -0
  20. ata_coder/core/__init__.py +35 -0
  21. ata_coder/core/events.py +73 -0
  22. ata_coder/core/queue.py +85 -0
  23. ata_coder/core/state.py +17 -0
  24. ata_coder/event_queue.py +5 -0
  25. ata_coder/extension.py +654 -0
  26. ata_coder/extensions/__init__.py +1 -0
  27. ata_coder/extensions/hello_skill.py +47 -0
  28. ata_coder/fool_proof.py +295 -0
  29. ata_coder/git_workflow.py +371 -0
  30. ata_coder/gui.py +511 -0
  31. ata_coder/llm_client.py +543 -0
  32. ata_coder/main.py +814 -0
  33. ata_coder/mcp_client.py +1095 -0
  34. ata_coder/memory.py +539 -0
  35. ata_coder/model_registry.py +134 -0
  36. ata_coder/model_router.py +105 -0
  37. ata_coder/permissions.py +274 -0
  38. ata_coder/privilege.py +464 -0
  39. ata_coder/project.py +273 -0
  40. ata_coder/prompt_template.py +423 -0
  41. ata_coder/prompts/auto-mode.md +7 -0
  42. ata_coder/prompts/coding-rules.md +40 -0
  43. ata_coder/prompts/execution-guardrails.md +14 -0
  44. ata_coder/prompts/memory-system.md +24 -0
  45. ata_coder/prompts/output-style.md +23 -0
  46. ata_coder/prompts/safety.md +17 -0
  47. ata_coder/prompts/slash-commands.md +24 -0
  48. ata_coder/prompts/sub-agents.md +38 -0
  49. ata_coder/prompts/system-reminders.md +17 -0
  50. ata_coder/prompts/system.md +105 -0
  51. ata_coder/prompts/tool-policy.md +46 -0
  52. ata_coder/repl_theme.py +99 -0
  53. ata_coder/repl_tracker.py +89 -0
  54. ata_coder/repl_ui.py +1214 -0
  55. ata_coder/safety_guard.py +434 -0
  56. ata_coder/self_correct.py +346 -0
  57. ata_coder/server.py +882 -0
  58. ata_coder/server_session.py +159 -0
  59. ata_coder/server_shell.py +129 -0
  60. ata_coder/session.py +431 -0
  61. ata_coder/settings.py +439 -0
  62. ata_coder/setup_wizard.py +136 -0
  63. ata_coder/skill_extension.py +92 -0
  64. ata_coder/skills/architect/SKILL.md +42 -0
  65. ata_coder/skills/code-reviewer/SKILL.md +37 -0
  66. ata_coder/skills/codecraft/SKILL.md +452 -0
  67. ata_coder/skills/debugger/SKILL.md +45 -0
  68. ata_coder/skills/doc-writer/SKILL.md +36 -0
  69. ata_coder/skills/general-coder/SKILL.md +76 -0
  70. ata_coder/skills/math-calculator/README.md +40 -0
  71. ata_coder/skills/math-calculator/SKILL.md +59 -0
  72. ata_coder/skills/math-calculator/handler.py +103 -0
  73. ata_coder/skills/math-calculator/prompts/system.md +8 -0
  74. ata_coder/skills/math-calculator/requirements.txt +2 -0
  75. ata_coder/skills/math-calculator/resources/constants.json +8 -0
  76. ata_coder/skills/math-calculator/tests/test_handler.py +53 -0
  77. ata_coder/skills/security-auditor/SKILL.md +40 -0
  78. ata_coder/skills/test-writer/SKILL.md +36 -0
  79. ata_coder/skills/weather-skill/README.md +45 -0
  80. ata_coder/skills/weather-skill/handler.py +76 -0
  81. ata_coder/skills/weather-skill/manifest.json +48 -0
  82. ata_coder/skills/weather-skill/prompts/system_prompt.txt +9 -0
  83. ata_coder/skills/weather-skill/prompts/user_prompt_template.txt +3 -0
  84. ata_coder/skills/weather-skill/requirements.txt +1 -0
  85. ata_coder/skills/weather-skill/resources/city_list.json +17 -0
  86. ata_coder/skills/weather-skill/resources/error_messages.json +7 -0
  87. ata_coder/skills/weather-skill/tests/test_handler.py +28 -0
  88. ata_coder/skills/weather-skill/weather_utils.py +50 -0
  89. ata_coder/skills.py +1014 -0
  90. ata_coder/sub_agent.py +273 -0
  91. ata_coder/sub_agent_manager.py +203 -0
  92. ata_coder/system_prompt_builder.py +146 -0
  93. ata_coder/task_planner.py +391 -0
  94. ata_coder/terminal.py +318 -0
  95. ata_coder/test_runner.py +219 -0
  96. ata_coder/thread_supervisor.py +195 -0
  97. ata_coder/tool_defs.py +335 -0
  98. ata_coder/tools/__init__.py +11 -0
  99. ata_coder/tools/definitions.py +335 -0
  100. ata_coder/tools/executor.py +1036 -0
  101. ata_coder/tools/result.py +26 -0
  102. ata_coder/tools/subagent.py +332 -0
  103. ata_coder/tools/web.py +361 -0
  104. ata_coder/tools.py +1576 -0
  105. ata_coder/types.py +92 -0
  106. ata_coder/utils.py +113 -0
  107. ata_coder/web/css/style.css +180 -0
  108. ata_coder/web/index.html +84 -0
  109. ata_coder/web/js/app.js +489 -0
  110. ata_coder/web/package-lock.json +25 -0
  111. ata_coder/web/package.json +10 -0
  112. ata_coder/web/tsconfig.json +13 -0
  113. ata_coder-2.4.2.dist-info/METADATA +799 -0
  114. ata_coder-2.4.2.dist-info/RECORD +118 -0
  115. ata_coder-2.4.2.dist-info/WHEEL +5 -0
  116. ata_coder-2.4.2.dist-info/entry_points.txt +2 -0
  117. ata_coder-2.4.2.dist-info/licenses/LICENSE +21 -0
  118. ata_coder-2.4.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,105 @@
1
+ """
2
+ Model routing — length shortcuts + AI-driven complexity classification + model selection.
3
+
4
+ Flow:
5
+ 1. User sends a task
6
+ 2. shortcut_classify() tries a cheap length-based heuristic
7
+ 3. If ambiguous, the cheap model classifies: simple or complex?
8
+ 4. Simple → keep cheap model | Complex → switch to powerful model
9
+
10
+ No hardcoded keywords — the cheap model itself judges complexity for
11
+ medium-length tasks.
12
+ """
13
+
14
+ import logging
15
+ from typing import Any
16
+
17
+ from .settings import get_settings, Settings
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ class ModelRouter:
23
+ """Consolidated complexity classification + model name resolution.
24
+
25
+ Pulls configuration from the global Settings singleton so callers
26
+ don't need to thread settings through every call site.
27
+ """
28
+
29
+ def __init__(self, settings: Settings | None = None):
30
+ self._settings = settings or get_settings()
31
+
32
+ # ── Complexity classification ──────────────────────────────────────────
33
+
34
+ def classify_shortcut(self, task: str) -> str | None:
35
+ """
36
+ Quick length-based shortcut — skip AI classify for obvious cases.
37
+ Returns 'simple', 'complex', or None (None = need AI classify).
38
+
39
+ Moved here from Settings.shortcut_classify to keep classification
40
+ logic in one place.
41
+ """
42
+ if not self._settings.get("complexity", "auto_detect", default=True):
43
+ return "normal"
44
+
45
+ task_len = len(task.strip())
46
+ simple_max = self._settings.get("complexity", "simple_max_chars", default=60)
47
+ complex_min = self._settings.get("complexity", "complex_min_chars", default=500)
48
+
49
+ if task_len <= simple_max:
50
+ return "simple"
51
+ if task_len >= complex_min:
52
+ return "complex"
53
+ return None # middle ground → let AI decide
54
+
55
+ # ── Model resolution ───────────────────────────────────────────────────
56
+
57
+ def resolve(self, complexity: str) -> str:
58
+ """Map a complexity label to a model name."""
59
+ mapping = {
60
+ "simple": self._settings.model_haiku,
61
+ "complex": self._settings.model_opus,
62
+ "normal": self._settings.default_model,
63
+ "explicit": self._settings.default_model,
64
+ }
65
+ return mapping.get(complexity, self._settings.default_model)
66
+
67
+ @property
68
+ def subagent_model(self) -> str:
69
+ return self._settings.model_subagent
70
+
71
+ @property
72
+ def complex_model(self) -> str:
73
+ return self._settings.model_opus
74
+
75
+ @property
76
+ def info(self) -> dict[str, Any]:
77
+ return {
78
+ "default": self._settings.default_model,
79
+ "opus": self._settings.model_opus,
80
+ "sonnet": self._settings.model_sonnet,
81
+ "haiku": self._settings.model_haiku,
82
+ "subagent": self._settings.model_subagent,
83
+ }
84
+
85
+
86
+ # ── Module-level convenience functions (keep backward compat) ──────────────
87
+
88
+ def get_subagent_model(settings: Settings | None = None) -> str:
89
+ """Get the model name for classification/subagent tasks (cheaper/faster)."""
90
+ return ModelRouter(settings).subagent_model
91
+
92
+
93
+ def get_complex_model(settings: Settings | None = None) -> str:
94
+ """Get the model name for complex tasks (powerful)."""
95
+ return ModelRouter(settings).complex_model
96
+
97
+
98
+ def resolve_model(complexity: str, settings: Settings | None = None) -> str:
99
+ """Map a complexity label to a model name. Delegates to ModelRouter."""
100
+ return ModelRouter(settings).resolve(complexity)
101
+
102
+
103
+ def get_model_info(settings: Settings | None = None) -> dict[str, Any]:
104
+ """Get all model configuration info for display. Delegates to ModelRouter."""
105
+ return ModelRouter(settings).info
@@ -0,0 +1,274 @@
1
+ """
2
+ Interactive permission system — Claude Code style.
3
+
4
+ Controls whether the agent can execute tools that modify state:
5
+ - Shell commands (run_shell)
6
+ - File writes (write_file)
7
+ - File edits (edit_file)
8
+
9
+ Permission modes (per tool type):
10
+ - ask — prompt the user each time (default)
11
+ - allow — always allow for this session
12
+ - deny — always deny for this session
13
+ - once — allow once, then revert to ask
14
+
15
+ Permissions can be configured:
16
+ - Globally via settings (permissions.json)
17
+ - Per session via interactive prompts
18
+ - Per project via .ata_coder/permissions.json
19
+
20
+ The permission prompt shows:
21
+ - The tool being called
22
+ - The arguments (truncated for readability)
23
+ - Options: [y]es, [n]o, [a]llow all, [d]eny all
24
+ """
25
+
26
+ import json
27
+ import logging
28
+ import time
29
+ from dataclasses import dataclass
30
+ from enum import Enum
31
+ from pathlib import Path
32
+ from typing import Any, Callable
33
+
34
+ logger = logging.getLogger(__name__)
35
+
36
+
37
+ def _permissions_path() -> Path:
38
+ """Get the permissions file path from settings or fallback."""
39
+ try:
40
+ from .settings import get_settings
41
+ return get_settings().data_dir / "permissions.json"
42
+ except Exception:
43
+ return Path.home() / ".ata_coder" / "permissions.json"
44
+
45
+
46
+ # ── Permission mode ──────────────────────────────────────────────────────────
47
+
48
+ class PermissionMode(Enum):
49
+ ASK = "ask"
50
+ ALLOW = "allow"
51
+ DENY = "deny"
52
+
53
+ def to_label(self) -> str:
54
+ """Human-readable label for this permission mode."""
55
+ if self == PermissionMode.ALLOW:
56
+ return "✅ ALLOW"
57
+ elif self == PermissionMode.DENY:
58
+ return "🚫 DENY"
59
+ return "❓ ASK"
60
+
61
+
62
+ # ── Tool categories ──────────────────────────────────────────────────────────
63
+
64
+ # Tools grouped by risk level
65
+ READ_TOOLS = {"read_file", "grep", "glob", "list_dir"}
66
+ WRITE_TOOLS = {"write_file", "edit_file"}
67
+ SHELL_TOOLS = {"run_shell"}
68
+
69
+
70
+ def tool_category(tool_name: str) -> str:
71
+ """Get the category of a tool."""
72
+ if tool_name in READ_TOOLS:
73
+ return "read"
74
+ elif tool_name in WRITE_TOOLS:
75
+ return "write"
76
+ elif tool_name in SHELL_TOOLS:
77
+ return "shell"
78
+ elif tool_name.startswith("mcp__"):
79
+ return "mcp"
80
+ return "other"
81
+
82
+
83
+ # ── Permission rules ─────────────────────────────────────────────────────────
84
+
85
+ @dataclass
86
+ class PermissionRule:
87
+ """A single permission rule."""
88
+ tool_name: str # exact tool name or "*" for wildcard
89
+ mode: PermissionMode
90
+ category: str = "" # tool category for display
91
+
92
+
93
+ class PermissionStore:
94
+ """
95
+ Manages permission rules and interactive prompting.
96
+
97
+ Rules are checked in order of specificity:
98
+ 1. Exact tool name match
99
+ 2. Category match (e.g., "shell")
100
+ 3. Wildcard "*" match
101
+ 4. Default (ask)
102
+ """
103
+
104
+ def __init__(self, project_dir: str | Path | None = None):
105
+ self._rules: dict[str, PermissionMode] = {}
106
+ self._category_rules: dict[str, PermissionMode] = {}
107
+ self._once_allowed: set[str] = set() # tool calls allowed for one shot
108
+ self._prompt_fn: Callable | None = None # interactive prompt callback
109
+ self._recent_allows: dict[str, float] = {} # category → expiry timestamp
110
+ self._recent_allow_ttl: float = 60.0 # cache recent allows for 60s
111
+
112
+ # Load project-level permissions
113
+ self._project_dir = Path(project_dir) if project_dir else None
114
+ self._load_project_permissions() # always load from ~/.ata_coder/
115
+
116
+ # ── Configuration ─────────────────────────────────────────────────────
117
+
118
+ def set_prompt_callback(self, fn: Callable) -> None:
119
+ """Set the function to call for interactive prompts.
120
+ fn(tool_name, arguments, category) -> bool (allowed?)
121
+ """
122
+ self._prompt_fn = fn
123
+
124
+ def set_rule(self, tool_name: str, mode: PermissionMode) -> None:
125
+ """Set a permission rule for an exact tool name."""
126
+ if mode == PermissionMode.ASK:
127
+ self._rules.pop(tool_name, None)
128
+ else:
129
+ self._rules[tool_name] = mode
130
+
131
+ def set_category_rule(self, category: str, mode: PermissionMode) -> None:
132
+ """Set a permission rule for a tool category."""
133
+ if mode == PermissionMode.ASK:
134
+ self._category_rules.pop(category, None)
135
+ else:
136
+ self._category_rules[category] = mode
137
+
138
+ def get_category_mode(self, category: str) -> PermissionMode | None:
139
+ """Return the permission mode for a category, or None if not configured."""
140
+ return self._category_rules.get(category)
141
+
142
+ def allow_once(self, tool_name: str) -> None:
143
+ """Allow a specific tool call once."""
144
+ self._once_allowed.add(tool_name)
145
+
146
+ # ── Permission check ──────────────────────────────────────────────────
147
+
148
+ def check(self, tool_name: str, arguments: dict[str, Any] | None = None) -> bool:
149
+ """
150
+ Check if a tool call is allowed.
151
+ Returns True if allowed, False if denied.
152
+
153
+ For ASK mode, invokes the interactive prompt callback.
154
+ """
155
+ category = tool_category(tool_name)
156
+
157
+ # 1. One-shot allow
158
+ if tool_name in self._once_allowed:
159
+ self._once_allowed.discard(tool_name)
160
+ return True
161
+
162
+ # 2. Exact tool name rule
163
+ if tool_name in self._rules:
164
+ mode = self._rules[tool_name]
165
+ if mode == PermissionMode.ALLOW:
166
+ return True
167
+ elif mode == PermissionMode.DENY:
168
+ logger.info("Denied by rule: %s", tool_name)
169
+ return False
170
+
171
+ # 3. Category rule
172
+ if category in self._category_rules:
173
+ mode = self._category_rules[category]
174
+ if mode == PermissionMode.ALLOW:
175
+ return True
176
+ elif mode == PermissionMode.DENY:
177
+ logger.info("Denied by category rule: %s", category)
178
+ return False
179
+
180
+ # 4. Wildcard rule
181
+ if "*" in self._rules:
182
+ mode = self._rules["*"]
183
+ if mode == PermissionMode.ALLOW:
184
+ return True
185
+ elif mode == PermissionMode.DENY:
186
+ logger.info("Denied by wildcard rule")
187
+ return False
188
+
189
+ # 5. Recent-allow cache (prevents permission storm)
190
+ if category in self._recent_allows:
191
+ if time.time() < self._recent_allows[category]:
192
+ return True
193
+ del self._recent_allows[category] # expired
194
+
195
+ # 6. Read tools always allowed (safe by default)
196
+ if category == "read":
197
+ return True
198
+
199
+ # 7. Interactive prompt for write/shell/mcp
200
+ if self._prompt_fn:
201
+ allowed = self._prompt_fn(tool_name, arguments or {}, category)
202
+ if allowed:
203
+ self._recent_allows[category] = time.time() + self._recent_allow_ttl
204
+ return allowed
205
+
206
+ # No prompt callback — deny by default for safety
207
+ logger.warning("No prompt callback, denying: %s", tool_name)
208
+ return False
209
+
210
+ # ── Persistence ──────────────────────────────────────────────────────
211
+
212
+ def _load_project_permissions(self) -> None:
213
+ """Load permissions from ~/.ata_coder/permissions.json."""
214
+ perms_file = _permissions_path()
215
+ if not perms_file.exists():
216
+ return
217
+ try:
218
+ with open(perms_file, "r", encoding="utf-8") as f:
219
+ data = json.load(f)
220
+ for tool_name, mode_str in data.get("rules", {}).items():
221
+ try:
222
+ self._rules[tool_name] = PermissionMode(mode_str)
223
+ except ValueError:
224
+ pass
225
+ for category, mode_str in data.get("categories", {}).items():
226
+ try:
227
+ self._category_rules[category] = PermissionMode(mode_str)
228
+ except ValueError:
229
+ pass
230
+ logger.debug("Loaded %d permission rules from project", len(data.get("rules", {})))
231
+ except Exception as e:
232
+ logger.warning("Failed to load project permissions: %s", e)
233
+
234
+ def save_project_permissions(self) -> None:
235
+ """Save permissions to ~/.ata_coder/permissions.json."""
236
+ perms_file = _permissions_path()
237
+ perms_file.parent.mkdir(parents=True, exist_ok=True)
238
+ try:
239
+ data = {
240
+ "rules": {k: v.value for k, v in self._rules.items()},
241
+ "categories": {k: v.value for k, v in self._category_rules.items()},
242
+ }
243
+ with open(perms_file, "w", encoding="utf-8") as f:
244
+ json.dump(data, f, indent=2)
245
+ except Exception as e:
246
+ logger.warning("Failed to save project permissions: %s", e)
247
+
248
+ # ── Status ──────────────────────────────────────────────────────────
249
+
250
+ def describe(self) -> str:
251
+ """Human-readable description of current permission state."""
252
+ lines = ["Permission Rules:"]
253
+ lines.append(" Reads: always allowed")
254
+ for category in ["shell", "write", "mcp"]:
255
+ if category in self._category_rules:
256
+ lines.append(f" {category}: {self._category_rules[category].value}")
257
+ else:
258
+ lines.append(f" {category}: ask")
259
+ for tool_name, mode in sorted(self._rules.items()):
260
+ if tool_name != "*":
261
+ lines.append(f" {tool_name}: {mode.value}")
262
+ return "\n".join(lines)
263
+
264
+
265
+ # ── Global ───────────────────────────────────────────────────────────────────
266
+
267
+ _permission_store: PermissionStore | None = None
268
+
269
+
270
+ def get_permissions(project_dir: str | None = None) -> PermissionStore:
271
+ global _permission_store
272
+ if _permission_store is None:
273
+ _permission_store = PermissionStore(project_dir)
274
+ return _permission_store