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,368 @@
1
+ """
2
+ Change Tracker — undo/redo for all file modifications.
3
+
4
+ Every write_file and edit_file operation is tracked with:
5
+ - Before/after content snapshots
6
+ - Timestamp and tool call context
7
+ - File path and operation type
8
+
9
+ Supports:
10
+ - /undo <n> — Revert the last N changes
11
+ - /undo all — Revert all changes in this session
12
+ - /changes — List all changes with diffs
13
+ - /restore <n> — Re-apply a reverted change
14
+ - Auto-backup — Files are backed up before modification
15
+
16
+ Backups stored in: .ata_coder/changes/<session-id>/
17
+ """
18
+
19
+ import difflib
20
+ import logging
21
+ import shutil
22
+ import time
23
+ from dataclasses import dataclass
24
+ from enum import Enum
25
+ from pathlib import Path
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ def _default_changes_dir() -> Path:
31
+ """Get the default changes backup directory from settings or fallback."""
32
+ try:
33
+ from .settings import get_settings
34
+ return get_settings().changes_dir
35
+ except Exception:
36
+ return Path.home() / ".ata_coder" / "changes"
37
+
38
+
39
+ # ═══════════════════════════════════════════════════════════════════════════════
40
+ # Data model
41
+ # ═══════════════════════════════════════════════════════════════════════════════
42
+
43
+ class ChangeType(Enum):
44
+ WRITE = "write" # New file created
45
+ EDIT = "edit" # Existing file modified
46
+ DELETE = "delete" # File deleted (via shell)
47
+
48
+
49
+ @dataclass
50
+ class FileChange:
51
+ """A single tracked file change."""
52
+ id: int
53
+ file_path: str
54
+ change_type: ChangeType
55
+ old_content: str | None # None for new files (WRITE)
56
+ new_content: str | None # None for deleted files
57
+ timestamp: str = ""
58
+ reverted: bool = False
59
+
60
+ @property
61
+ def diff(self) -> str:
62
+ """Generate a unified diff."""
63
+ old = (self.old_content or "").splitlines(keepends=True)
64
+ new = (self.new_content or "").splitlines(keepends=True)
65
+ if not old and new:
66
+ # New file — show all additions
67
+ return "".join(f"+{line}" for line in new)
68
+ if old and not new:
69
+ # Deleted file — show all deletions
70
+ return "".join(f"-{line}" for line in old)
71
+ diff = difflib.unified_diff(
72
+ old, new,
73
+ fromfile=f"a/{self.file_path}" if self.old_content else "/dev/null",
74
+ tofile=f"b/{self.file_path}" if self.new_content else "/dev/null",
75
+ )
76
+ return "".join(diff)
77
+
78
+ @property
79
+ def summary(self) -> str:
80
+ """One-line summary."""
81
+ status = "[REVERTED]" if self.reverted else ""
82
+ if self.change_type == ChangeType.WRITE:
83
+ return f"#{self.id} CREATE {self.file_path} {status}"
84
+ elif self.change_type == ChangeType.EDIT:
85
+ old_lines = (self.old_content or "").count("\n") + 1 if self.old_content else 0
86
+ new_lines = (self.new_content or "").count("\n") + 1 if self.new_content else 0
87
+ return f"#{self.id} EDIT {self.file_path} ({old_lines}→{new_lines} lines) {status}"
88
+ else:
89
+ return f"#{self.id} DELETE {self.file_path} {status}"
90
+
91
+
92
+ # ═══════════════════════════════════════════════════════════════════════════════
93
+ # Change Tracker
94
+ # ═══════════════════════════════════════════════════════════════════════════════
95
+
96
+ class ChangeTracker:
97
+ """
98
+ Tracks all file modifications in a session for undo/redo.
99
+
100
+ Also maintains auto-backups of files before modification.
101
+ """
102
+
103
+ def __init__(self, session_id: str = "", backup_dir: str | Path | None = None):
104
+ self.session_id = session_id or time.strftime("%Y%m%d-%H%M%S")
105
+ self.changes: list[FileChange] = []
106
+ self._next_id = 1
107
+ self._backup_dir = Path(backup_dir) if backup_dir else _default_changes_dir() / self.session_id
108
+ self._backup_dir.mkdir(parents=True, exist_ok=True)
109
+ self._backups: dict[str, str] = {}
110
+ self._dry_run = False
111
+ self._last_active: int = -1
112
+
113
+ # ── Dry run toggle ───────────────────────────────────────────────────
114
+
115
+ @property
116
+ def dry_run(self) -> bool:
117
+ return self._dry_run
118
+
119
+ @dry_run.setter
120
+ def dry_run(self, enabled: bool):
121
+ self._dry_run = enabled
122
+ if enabled:
123
+ logger.info("DRY RUN MODE enabled — no actual changes will be made")
124
+
125
+ def reset(self) -> None:
126
+ """Reset tracker state for a new agent run."""
127
+ self.changes.clear()
128
+ self._next_id = 1
129
+ self._last_active = -1
130
+
131
+ # ── Capture changes ──────────────────────────────────────────────────
132
+
133
+ def capture_write(self, file_path: str, content: str) -> FileChange | None:
134
+ """Track a file creation/write. The actual write is done by ToolExecutor."""
135
+ path = Path(file_path)
136
+ exists_before = path.exists()
137
+
138
+ # Backup existing file (for undo)
139
+ old_content = None
140
+ if exists_before:
141
+ try:
142
+ with open(path, "r", encoding="utf-8", errors="replace") as f:
143
+ old_content = f.read()
144
+ self._backup(file_path)
145
+ except Exception:
146
+ pass
147
+
148
+ change = FileChange(
149
+ id=self._next_id,
150
+ file_path=file_path,
151
+ change_type=ChangeType.WRITE if not exists_before else ChangeType.EDIT,
152
+ old_content=old_content,
153
+ new_content=content,
154
+ timestamp=time.strftime("%Y-%m-%dT%H:%M:%SZ"),
155
+ )
156
+
157
+ if self._dry_run:
158
+ # Dry-run: only save to backup dir, do NOT write to actual file
159
+ dry_path = self._backup_dir / f"dry_{self._next_id}_{Path(file_path).name}"
160
+ dry_path.parent.mkdir(parents=True, exist_ok=True)
161
+ with open(dry_path, "w", encoding="utf-8", errors="replace") as f:
162
+ f.write(content)
163
+ change.file_path = str(dry_path)
164
+ logger.info("[DRY-RUN] Would write: %s → %s", file_path, dry_path)
165
+ # Normal mode: actual file write is done by ToolExecutor, we just track + backup
166
+
167
+ self.changes.append(change)
168
+ self._next_id += 1
169
+ return change
170
+
171
+ def capture_edit(self, file_path: str, old_content: str, new_content: str) -> FileChange | None:
172
+ """Track a file edit. The actual edit is done by ToolExecutor."""
173
+ if old_content == new_content:
174
+ return None
175
+
176
+ path = Path(file_path)
177
+ # Backup before edit (for undo)
178
+ self._backup(file_path)
179
+
180
+ change = FileChange(
181
+ id=self._next_id,
182
+ file_path=file_path,
183
+ change_type=ChangeType.EDIT,
184
+ old_content=old_content,
185
+ new_content=new_content,
186
+ timestamp=time.strftime("%Y-%m-%dT%H:%M:%SZ"),
187
+ )
188
+
189
+ if self._dry_run:
190
+ # Dry-run: only save to backup dir, do NOT write to actual file
191
+ dry_path = self._backup_dir / f"dry_{self._next_id}_{Path(file_path).name}"
192
+ dry_path.parent.mkdir(parents=True, exist_ok=True)
193
+ with open(dry_path, "w", encoding="utf-8", errors="replace") as f:
194
+ f.write(new_content)
195
+ change.file_path = str(dry_path)
196
+ logger.info("[DRY-RUN] Would edit: %s → %s", file_path, dry_path)
197
+ # Normal mode: actual file edit is done by ToolExecutor, we just track + backup
198
+
199
+ self.changes.append(change)
200
+ self._next_id += 1
201
+ return change
202
+
203
+ # ── Backup ───────────────────────────────────────────────────────────
204
+
205
+ def _backup(self, file_path: str) -> str:
206
+ """Create a timestamped backup of a file."""
207
+ path = Path(file_path)
208
+ if not path.exists():
209
+ return ""
210
+
211
+ # Use nanosecond precision to avoid backup collisions when two
212
+ # operations touch the same file within the same second.
213
+ backup_name = f"{path.name}.{time.time_ns()}.bak"
214
+ backup_path = self._backup_dir / backup_name
215
+ shutil.copy2(str(path), str(backup_path))
216
+ self._backups[file_path] = str(backup_path)
217
+ logger.debug("Backed up: %s → %s", file_path, backup_path.name)
218
+ return str(backup_path)
219
+
220
+ # ── Undo ─────────────────────────────────────────────────────────────
221
+
222
+ def undo(self, count: int = 1) -> list[FileChange]:
223
+ """Undo the last N changes. O(n) using _last_active index."""
224
+ if self._dry_run:
225
+ return []
226
+
227
+ reverted = []
228
+ for _ in range(count):
229
+ if self._last_active < 0:
230
+ self._last_active = len(self.changes) - 1
231
+ while self._last_active >= 0 and self.changes[self._last_active].reverted:
232
+ self._last_active -= 1
233
+ if self._last_active < 0:
234
+ break
235
+
236
+ c = self.changes[self._last_active]
237
+ self._apply_revert(c)
238
+ c.reverted = True
239
+ reverted.append(c)
240
+ self._last_active -= 1
241
+ logger.info("Undid change #%d: %s", c.id, c.file_path)
242
+
243
+ return reverted
244
+
245
+ def _apply_revert(self, c: FileChange) -> None:
246
+ """Apply revert for a single change."""
247
+ path = Path(c.file_path)
248
+ if c.change_type == ChangeType.WRITE:
249
+ if c.old_content is None:
250
+ if path.exists():
251
+ path.unlink()
252
+ elif path.exists():
253
+ path.write_text(c.old_content, encoding="utf-8", errors="replace")
254
+ elif c.change_type == ChangeType.EDIT:
255
+ if path.exists() and c.old_content is not None:
256
+ path.write_text(c.old_content, encoding="utf-8", errors="replace")
257
+ elif c.change_type == ChangeType.DELETE:
258
+ if c.new_content is not None:
259
+ path.parent.mkdir(parents=True, exist_ok=True)
260
+ path.write_text(c.new_content, encoding="utf-8", errors="replace")
261
+
262
+ def undo_all(self) -> list[FileChange]:
263
+ """Undo all changes in this session."""
264
+ active = sum(1 for c in self.changes if not c.reverted)
265
+ return self.undo(active)
266
+
267
+ def restore(self, change_id: int) -> FileChange | None:
268
+ """Re-apply a previously reverted change."""
269
+ for c in self.changes:
270
+ if c.id == change_id and c.reverted:
271
+ path = Path(c.file_path)
272
+ if c.new_content is not None:
273
+ path.parent.mkdir(parents=True, exist_ok=True)
274
+ with open(path, "w", encoding="utf-8", errors="replace") as f:
275
+ f.write(c.new_content)
276
+ c.reverted = False
277
+ logger.info("Restored change #%d: %s", c.id, c.file_path)
278
+ return c
279
+ return None
280
+
281
+ # ── List & summary ───────────────────────────────────────────────────
282
+
283
+ def list_changes(self, include_reverted: bool = False) -> list[FileChange]:
284
+ """List all changes."""
285
+ if include_reverted:
286
+ return list(self.changes)
287
+ return [c for c in self.changes if not c.reverted]
288
+
289
+ def summary(self) -> str:
290
+ """Multi-line summary of all changes."""
291
+ active = self.list_changes()
292
+ reverted = [c for c in self.changes if c.reverted]
293
+
294
+ lines = []
295
+ lines.append(f"Session: {self.session_id}")
296
+ lines.append(f"Changes: {len(active)} active, {len(reverted)} reverted")
297
+ lines.append(f"Dry-run: {'ON' if self._dry_run else 'OFF'}")
298
+ lines.append("")
299
+
300
+ if active:
301
+ for c in active:
302
+ lines.append(f" {c.summary}")
303
+ else:
304
+ lines.append(" (no active changes)")
305
+
306
+ if reverted:
307
+ lines.append("\n Reverted:")
308
+ for c in reverted:
309
+ lines.append(f" {c.summary}")
310
+
311
+ return "\n".join(lines)
312
+
313
+ def diff_summary(self, last_n: int = 5) -> str:
314
+ """Show diffs for the last N changes."""
315
+ recent = [c for c in self.changes if not c.reverted][-last_n:]
316
+ if not recent:
317
+ return "(no changes to show)"
318
+
319
+ parts = []
320
+ for c in recent:
321
+ parts.append(f"--- {c.summary} ---")
322
+ parts.append(c.diff[:2000])
323
+ parts.append("")
324
+ return "\n".join(parts)
325
+
326
+ def count_active(self) -> int:
327
+ return sum(1 for c in self.changes if not c.reverted)
328
+
329
+ def count_all(self) -> int:
330
+ return len(self.changes)
331
+
332
+
333
+ # ═══════════════════════════════════════════════════════════════════════════════
334
+ # Session change tracker (global, manages multiple sessions)
335
+ # ═══════════════════════════════════════════════════════════════════════════════
336
+
337
+ class SessionChangeManager:
338
+ """Manages change trackers across multiple sessions."""
339
+
340
+ def __init__(self, base_dir: str | Path | None = None):
341
+ self._base = Path(base_dir) if base_dir else _default_changes_dir()
342
+ self._base.mkdir(parents=True, exist_ok=True)
343
+ self._trackers: dict[str, ChangeTracker] = {}
344
+
345
+ def get(self, session_id: str) -> ChangeTracker:
346
+ if session_id not in self._trackers:
347
+ backup_dir = self._base / session_id
348
+ self._trackers[session_id] = ChangeTracker(session_id, backup_dir)
349
+ return self._trackers[session_id]
350
+
351
+ def list_sessions(self) -> list[str]:
352
+ """List sessions that have changes."""
353
+ sessions = []
354
+ if self._base.exists():
355
+ for d in self._base.iterdir():
356
+ if d.is_dir():
357
+ sessions.append(d.name)
358
+ return sorted(sessions, reverse=True)
359
+
360
+ def cleanup_session(self, session_id: str) -> bool:
361
+ """Remove change tracker and its backups."""
362
+ if session_id in self._trackers:
363
+ del self._trackers[session_id]
364
+ backup_dir = self._base / session_id
365
+ if backup_dir.exists():
366
+ shutil.rmtree(str(backup_dir))
367
+ return True
368
+ return False