superqode 0.1.5__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 (288) hide show
  1. superqode/__init__.py +33 -0
  2. superqode/acp/__init__.py +23 -0
  3. superqode/acp/client.py +913 -0
  4. superqode/acp/permission_screen.py +457 -0
  5. superqode/acp/types.py +480 -0
  6. superqode/acp_discovery.py +856 -0
  7. superqode/agent/__init__.py +22 -0
  8. superqode/agent/edit_strategies.py +334 -0
  9. superqode/agent/loop.py +892 -0
  10. superqode/agent/qe_report_templates.py +39 -0
  11. superqode/agent/system_prompts.py +353 -0
  12. superqode/agent_output.py +721 -0
  13. superqode/agent_stream.py +953 -0
  14. superqode/agents/__init__.py +59 -0
  15. superqode/agents/acp_registry.py +305 -0
  16. superqode/agents/client.py +249 -0
  17. superqode/agents/data/augmentcode.com.toml +51 -0
  18. superqode/agents/data/cagent.dev.toml +51 -0
  19. superqode/agents/data/claude.com.toml +60 -0
  20. superqode/agents/data/codeassistant.dev.toml +51 -0
  21. superqode/agents/data/codex.openai.com.toml +57 -0
  22. superqode/agents/data/fastagent.ai.toml +66 -0
  23. superqode/agents/data/geminicli.com.toml +77 -0
  24. superqode/agents/data/goose.block.xyz.toml +54 -0
  25. superqode/agents/data/junie.jetbrains.com.toml +56 -0
  26. superqode/agents/data/kimi.moonshot.cn.toml +57 -0
  27. superqode/agents/data/llmlingagent.dev.toml +51 -0
  28. superqode/agents/data/molt.bot.toml +49 -0
  29. superqode/agents/data/opencode.ai.toml +60 -0
  30. superqode/agents/data/stakpak.dev.toml +51 -0
  31. superqode/agents/data/vtcode.dev.toml +51 -0
  32. superqode/agents/discovery.py +266 -0
  33. superqode/agents/messaging.py +160 -0
  34. superqode/agents/persona.py +166 -0
  35. superqode/agents/registry.py +421 -0
  36. superqode/agents/schema.py +72 -0
  37. superqode/agents/unified.py +367 -0
  38. superqode/app/__init__.py +111 -0
  39. superqode/app/constants.py +314 -0
  40. superqode/app/css.py +366 -0
  41. superqode/app/models.py +118 -0
  42. superqode/app/suggester.py +125 -0
  43. superqode/app/widgets.py +1591 -0
  44. superqode/app_enhanced.py +399 -0
  45. superqode/app_main.py +17187 -0
  46. superqode/approval.py +312 -0
  47. superqode/atomic.py +296 -0
  48. superqode/commands/__init__.py +1 -0
  49. superqode/commands/acp.py +965 -0
  50. superqode/commands/agents.py +180 -0
  51. superqode/commands/auth.py +278 -0
  52. superqode/commands/config.py +374 -0
  53. superqode/commands/init.py +826 -0
  54. superqode/commands/providers.py +819 -0
  55. superqode/commands/qe.py +1145 -0
  56. superqode/commands/roles.py +380 -0
  57. superqode/commands/serve.py +172 -0
  58. superqode/commands/suggestions.py +127 -0
  59. superqode/commands/superqe.py +460 -0
  60. superqode/config/__init__.py +51 -0
  61. superqode/config/loader.py +812 -0
  62. superqode/config/schema.py +498 -0
  63. superqode/core/__init__.py +111 -0
  64. superqode/core/roles.py +281 -0
  65. superqode/danger.py +386 -0
  66. superqode/data/superqode-template.yaml +1522 -0
  67. superqode/design_system.py +1080 -0
  68. superqode/dialogs/__init__.py +6 -0
  69. superqode/dialogs/base.py +39 -0
  70. superqode/dialogs/model.py +130 -0
  71. superqode/dialogs/provider.py +870 -0
  72. superqode/diff_view.py +919 -0
  73. superqode/enterprise.py +21 -0
  74. superqode/evaluation/__init__.py +25 -0
  75. superqode/evaluation/adapters.py +93 -0
  76. superqode/evaluation/behaviors.py +89 -0
  77. superqode/evaluation/engine.py +209 -0
  78. superqode/evaluation/scenarios.py +96 -0
  79. superqode/execution/__init__.py +36 -0
  80. superqode/execution/linter.py +538 -0
  81. superqode/execution/modes.py +347 -0
  82. superqode/execution/resolver.py +283 -0
  83. superqode/execution/runner.py +642 -0
  84. superqode/file_explorer.py +811 -0
  85. superqode/file_viewer.py +471 -0
  86. superqode/flash.py +183 -0
  87. superqode/guidance/__init__.py +58 -0
  88. superqode/guidance/config.py +203 -0
  89. superqode/guidance/prompts.py +71 -0
  90. superqode/harness/__init__.py +54 -0
  91. superqode/harness/accelerator.py +291 -0
  92. superqode/harness/config.py +319 -0
  93. superqode/harness/validator.py +147 -0
  94. superqode/history.py +279 -0
  95. superqode/integrations/superopt_runner.py +124 -0
  96. superqode/logging/__init__.py +49 -0
  97. superqode/logging/adapters.py +219 -0
  98. superqode/logging/formatter.py +923 -0
  99. superqode/logging/integration.py +341 -0
  100. superqode/logging/sinks.py +170 -0
  101. superqode/logging/unified_log.py +417 -0
  102. superqode/lsp/__init__.py +26 -0
  103. superqode/lsp/client.py +544 -0
  104. superqode/main.py +1069 -0
  105. superqode/mcp/__init__.py +89 -0
  106. superqode/mcp/auth_storage.py +380 -0
  107. superqode/mcp/client.py +1236 -0
  108. superqode/mcp/config.py +319 -0
  109. superqode/mcp/integration.py +337 -0
  110. superqode/mcp/oauth.py +436 -0
  111. superqode/mcp/oauth_callback.py +385 -0
  112. superqode/mcp/types.py +290 -0
  113. superqode/memory/__init__.py +31 -0
  114. superqode/memory/feedback.py +342 -0
  115. superqode/memory/store.py +522 -0
  116. superqode/notifications.py +369 -0
  117. superqode/optimization/__init__.py +5 -0
  118. superqode/optimization/config.py +33 -0
  119. superqode/permissions/__init__.py +25 -0
  120. superqode/permissions/rules.py +488 -0
  121. superqode/plan.py +323 -0
  122. superqode/providers/__init__.py +33 -0
  123. superqode/providers/gateway/__init__.py +165 -0
  124. superqode/providers/gateway/base.py +228 -0
  125. superqode/providers/gateway/litellm_gateway.py +1170 -0
  126. superqode/providers/gateway/openresponses_gateway.py +436 -0
  127. superqode/providers/health.py +297 -0
  128. superqode/providers/huggingface/__init__.py +74 -0
  129. superqode/providers/huggingface/downloader.py +472 -0
  130. superqode/providers/huggingface/endpoints.py +442 -0
  131. superqode/providers/huggingface/hub.py +531 -0
  132. superqode/providers/huggingface/inference.py +394 -0
  133. superqode/providers/huggingface/transformers_runner.py +516 -0
  134. superqode/providers/local/__init__.py +100 -0
  135. superqode/providers/local/base.py +438 -0
  136. superqode/providers/local/discovery.py +418 -0
  137. superqode/providers/local/lmstudio.py +256 -0
  138. superqode/providers/local/mlx.py +457 -0
  139. superqode/providers/local/ollama.py +486 -0
  140. superqode/providers/local/sglang.py +268 -0
  141. superqode/providers/local/tgi.py +260 -0
  142. superqode/providers/local/tool_support.py +477 -0
  143. superqode/providers/local/vllm.py +258 -0
  144. superqode/providers/manager.py +1338 -0
  145. superqode/providers/models.py +1016 -0
  146. superqode/providers/models_dev.py +578 -0
  147. superqode/providers/openresponses/__init__.py +87 -0
  148. superqode/providers/openresponses/converters/__init__.py +17 -0
  149. superqode/providers/openresponses/converters/messages.py +343 -0
  150. superqode/providers/openresponses/converters/tools.py +268 -0
  151. superqode/providers/openresponses/schema/__init__.py +56 -0
  152. superqode/providers/openresponses/schema/models.py +585 -0
  153. superqode/providers/openresponses/streaming/__init__.py +5 -0
  154. superqode/providers/openresponses/streaming/parser.py +338 -0
  155. superqode/providers/openresponses/tools/__init__.py +21 -0
  156. superqode/providers/openresponses/tools/apply_patch.py +352 -0
  157. superqode/providers/openresponses/tools/code_interpreter.py +290 -0
  158. superqode/providers/openresponses/tools/file_search.py +333 -0
  159. superqode/providers/openresponses/tools/mcp_adapter.py +252 -0
  160. superqode/providers/registry.py +716 -0
  161. superqode/providers/usage.py +332 -0
  162. superqode/pure_mode.py +384 -0
  163. superqode/qr/__init__.py +23 -0
  164. superqode/qr/dashboard.py +781 -0
  165. superqode/qr/generator.py +1018 -0
  166. superqode/qr/templates.py +135 -0
  167. superqode/safety/__init__.py +41 -0
  168. superqode/safety/sandbox.py +413 -0
  169. superqode/safety/warnings.py +256 -0
  170. superqode/server/__init__.py +33 -0
  171. superqode/server/lsp_server.py +775 -0
  172. superqode/server/web.py +250 -0
  173. superqode/session/__init__.py +25 -0
  174. superqode/session/persistence.py +580 -0
  175. superqode/session/sharing.py +477 -0
  176. superqode/session.py +475 -0
  177. superqode/sidebar.py +2991 -0
  178. superqode/stream_view.py +648 -0
  179. superqode/styles/__init__.py +3 -0
  180. superqode/superqe/__init__.py +184 -0
  181. superqode/superqe/acp_runner.py +1064 -0
  182. superqode/superqe/constitution/__init__.py +62 -0
  183. superqode/superqe/constitution/evaluator.py +308 -0
  184. superqode/superqe/constitution/loader.py +432 -0
  185. superqode/superqe/constitution/schema.py +250 -0
  186. superqode/superqe/events.py +591 -0
  187. superqode/superqe/frameworks/__init__.py +65 -0
  188. superqode/superqe/frameworks/base.py +234 -0
  189. superqode/superqe/frameworks/e2e.py +263 -0
  190. superqode/superqe/frameworks/executor.py +237 -0
  191. superqode/superqe/frameworks/javascript.py +409 -0
  192. superqode/superqe/frameworks/python.py +373 -0
  193. superqode/superqe/frameworks/registry.py +92 -0
  194. superqode/superqe/mcp_tools/__init__.py +47 -0
  195. superqode/superqe/mcp_tools/core_tools.py +418 -0
  196. superqode/superqe/mcp_tools/registry.py +230 -0
  197. superqode/superqe/mcp_tools/testing_tools.py +167 -0
  198. superqode/superqe/noise.py +89 -0
  199. superqode/superqe/orchestrator.py +778 -0
  200. superqode/superqe/roles.py +609 -0
  201. superqode/superqe/session.py +713 -0
  202. superqode/superqe/skills/__init__.py +57 -0
  203. superqode/superqe/skills/base.py +106 -0
  204. superqode/superqe/skills/core_skills.py +899 -0
  205. superqode/superqe/skills/registry.py +90 -0
  206. superqode/superqe/verifier.py +101 -0
  207. superqode/superqe_cli.py +76 -0
  208. superqode/tool_call.py +358 -0
  209. superqode/tools/__init__.py +93 -0
  210. superqode/tools/agent_tools.py +496 -0
  211. superqode/tools/base.py +324 -0
  212. superqode/tools/batch_tool.py +133 -0
  213. superqode/tools/diagnostics.py +311 -0
  214. superqode/tools/edit_tools.py +653 -0
  215. superqode/tools/enhanced_base.py +515 -0
  216. superqode/tools/file_tools.py +269 -0
  217. superqode/tools/file_tracking.py +45 -0
  218. superqode/tools/lsp_tools.py +610 -0
  219. superqode/tools/network_tools.py +350 -0
  220. superqode/tools/permissions.py +400 -0
  221. superqode/tools/question_tool.py +324 -0
  222. superqode/tools/search_tools.py +598 -0
  223. superqode/tools/shell_tools.py +259 -0
  224. superqode/tools/todo_tools.py +121 -0
  225. superqode/tools/validation.py +80 -0
  226. superqode/tools/web_tools.py +639 -0
  227. superqode/tui.py +1152 -0
  228. superqode/tui_integration.py +875 -0
  229. superqode/tui_widgets/__init__.py +27 -0
  230. superqode/tui_widgets/widgets/__init__.py +18 -0
  231. superqode/tui_widgets/widgets/progress.py +185 -0
  232. superqode/tui_widgets/widgets/tool_display.py +188 -0
  233. superqode/undo_manager.py +574 -0
  234. superqode/utils/__init__.py +5 -0
  235. superqode/utils/error_handling.py +323 -0
  236. superqode/utils/fuzzy.py +257 -0
  237. superqode/widgets/__init__.py +477 -0
  238. superqode/widgets/agent_collab.py +390 -0
  239. superqode/widgets/agent_store.py +936 -0
  240. superqode/widgets/agent_switcher.py +395 -0
  241. superqode/widgets/animation_manager.py +284 -0
  242. superqode/widgets/code_context.py +356 -0
  243. superqode/widgets/command_palette.py +412 -0
  244. superqode/widgets/connection_status.py +537 -0
  245. superqode/widgets/conversation_history.py +470 -0
  246. superqode/widgets/diff_indicator.py +155 -0
  247. superqode/widgets/enhanced_status_bar.py +385 -0
  248. superqode/widgets/enhanced_toast.py +476 -0
  249. superqode/widgets/file_browser.py +809 -0
  250. superqode/widgets/file_reference.py +585 -0
  251. superqode/widgets/issue_timeline.py +340 -0
  252. superqode/widgets/leader_key.py +264 -0
  253. superqode/widgets/mode_switcher.py +445 -0
  254. superqode/widgets/model_picker.py +234 -0
  255. superqode/widgets/permission_preview.py +1205 -0
  256. superqode/widgets/prompt.py +358 -0
  257. superqode/widgets/provider_connect.py +725 -0
  258. superqode/widgets/pty_shell.py +587 -0
  259. superqode/widgets/qe_dashboard.py +321 -0
  260. superqode/widgets/resizable_sidebar.py +377 -0
  261. superqode/widgets/response_changes.py +218 -0
  262. superqode/widgets/response_display.py +528 -0
  263. superqode/widgets/rich_tool_display.py +613 -0
  264. superqode/widgets/sidebar_panels.py +1180 -0
  265. superqode/widgets/slash_complete.py +356 -0
  266. superqode/widgets/split_view.py +612 -0
  267. superqode/widgets/status_bar.py +273 -0
  268. superqode/widgets/superqode_display.py +786 -0
  269. superqode/widgets/thinking_display.py +815 -0
  270. superqode/widgets/throbber.py +87 -0
  271. superqode/widgets/toast.py +206 -0
  272. superqode/widgets/unified_output.py +1073 -0
  273. superqode/workspace/__init__.py +75 -0
  274. superqode/workspace/artifacts.py +472 -0
  275. superqode/workspace/coordinator.py +353 -0
  276. superqode/workspace/diff_tracker.py +429 -0
  277. superqode/workspace/git_guard.py +373 -0
  278. superqode/workspace/git_snapshot.py +526 -0
  279. superqode/workspace/manager.py +750 -0
  280. superqode/workspace/snapshot.py +357 -0
  281. superqode/workspace/watcher.py +535 -0
  282. superqode/workspace/worktree.py +440 -0
  283. superqode-0.1.5.dist-info/METADATA +204 -0
  284. superqode-0.1.5.dist-info/RECORD +288 -0
  285. superqode-0.1.5.dist-info/WHEEL +5 -0
  286. superqode-0.1.5.dist-info/entry_points.txt +3 -0
  287. superqode-0.1.5.dist-info/licenses/LICENSE +648 -0
  288. superqode-0.1.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,357 @@
1
+ """
2
+ Snapshot Manager for Ephemeral Workspace.
3
+
4
+ Captures the state of modified files and enables full reversion
5
+ after QE session completes. Uses efficient in-memory tracking
6
+ with disk backup for large files.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import hashlib
12
+ import shutil
13
+ import tempfile
14
+ from dataclasses import dataclass, field
15
+ from datetime import datetime
16
+ from pathlib import Path
17
+ from typing import Dict, List, Optional, Set
18
+ import json
19
+
20
+
21
+ @dataclass
22
+ class FileSnapshot:
23
+ """Snapshot of a single file's state."""
24
+
25
+ path: Path
26
+ original_content: Optional[bytes] # None if file didn't exist
27
+ original_hash: Optional[str]
28
+ existed: bool
29
+ timestamp: datetime = field(default_factory=datetime.now)
30
+
31
+ @property
32
+ def was_created(self) -> bool:
33
+ """True if this file was created (didn't exist before)."""
34
+ return not self.existed
35
+
36
+ def content_changed(self, current_content: bytes) -> bool:
37
+ """Check if content has changed from original."""
38
+ if self.original_content is None:
39
+ return True
40
+ current_hash = hashlib.sha256(current_content).hexdigest()
41
+ return current_hash != self.original_hash
42
+
43
+
44
+ @dataclass
45
+ class DirectorySnapshot:
46
+ """Snapshot of a directory that was created."""
47
+
48
+ path: Path
49
+ existed: bool
50
+ timestamp: datetime = field(default_factory=datetime.now)
51
+
52
+
53
+ class SnapshotManager:
54
+ """
55
+ Manages file snapshots for ephemeral workspace.
56
+
57
+ Tracks all file modifications during a QE session and enables
58
+ complete reversion to original state.
59
+
60
+ Usage:
61
+ snapshot = SnapshotManager(project_root)
62
+ snapshot.start_session()
63
+
64
+ # Track file before modification
65
+ snapshot.capture_file(Path("src/main.py"))
66
+
67
+ # ... agent modifies files ...
68
+
69
+ # Revert everything
70
+ snapshot.revert_all()
71
+ """
72
+
73
+ # Files larger than this are backed up to disk
74
+ LARGE_FILE_THRESHOLD = 10 * 1024 * 1024 # 10MB
75
+
76
+ def __init__(self, project_root: Path):
77
+ self.project_root = project_root.resolve()
78
+ self.session_id: Optional[str] = None
79
+ self.session_start: Optional[datetime] = None
80
+
81
+ # Tracking state
82
+ self._file_snapshots: Dict[Path, FileSnapshot] = {}
83
+ self._dir_snapshots: Dict[Path, DirectorySnapshot] = {}
84
+ self._large_file_backup_dir: Optional[Path] = None
85
+
86
+ # Statistics
87
+ self._files_modified: Set[Path] = set()
88
+ self._files_created: Set[Path] = set()
89
+ self._files_deleted: Set[Path] = set()
90
+ self._dirs_created: Set[Path] = set()
91
+
92
+ def start_session(self, session_id: Optional[str] = None) -> str:
93
+ """Start a new snapshot session."""
94
+ if self.session_id:
95
+ raise RuntimeError("Session already active. Call revert_all() or end_session() first.")
96
+
97
+ self.session_id = session_id or f"qe-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
98
+ self.session_start = datetime.now()
99
+
100
+ # Create temp dir for large file backups
101
+ self._large_file_backup_dir = Path(tempfile.mkdtemp(prefix=f"superqode-{self.session_id}-"))
102
+
103
+ # Reset tracking
104
+ self._file_snapshots.clear()
105
+ self._dir_snapshots.clear()
106
+ self._files_modified.clear()
107
+ self._files_created.clear()
108
+ self._files_deleted.clear()
109
+ self._dirs_created.clear()
110
+
111
+ return self.session_id
112
+
113
+ def capture_file(self, file_path: Path) -> FileSnapshot:
114
+ """
115
+ Capture a file's state before modification.
116
+
117
+ Call this BEFORE any modification to the file.
118
+ """
119
+ if not self.session_id:
120
+ raise RuntimeError("No active session. Call start_session() first.")
121
+
122
+ abs_path = (self.project_root / file_path).resolve()
123
+ rel_path = abs_path.relative_to(self.project_root)
124
+
125
+ # Already captured
126
+ if rel_path in self._file_snapshots:
127
+ return self._file_snapshots[rel_path]
128
+
129
+ # Capture current state
130
+ if abs_path.exists() and abs_path.is_file():
131
+ content = abs_path.read_bytes()
132
+ content_hash = hashlib.sha256(content).hexdigest()
133
+
134
+ # Large files go to disk backup
135
+ if len(content) > self.LARGE_FILE_THRESHOLD:
136
+ backup_path = self._large_file_backup_dir / rel_path
137
+ backup_path.parent.mkdir(parents=True, exist_ok=True)
138
+ shutil.copy2(abs_path, backup_path)
139
+ original_content = None # Don't keep in memory
140
+ else:
141
+ original_content = content
142
+
143
+ snapshot = FileSnapshot(
144
+ path=rel_path,
145
+ original_content=original_content,
146
+ original_hash=content_hash,
147
+ existed=True,
148
+ )
149
+ else:
150
+ # File doesn't exist yet
151
+ snapshot = FileSnapshot(
152
+ path=rel_path,
153
+ original_content=None,
154
+ original_hash=None,
155
+ existed=False,
156
+ )
157
+
158
+ self._file_snapshots[rel_path] = snapshot
159
+ return snapshot
160
+
161
+ def capture_directory(self, dir_path: Path) -> DirectorySnapshot:
162
+ """Capture a directory's existence state before creation."""
163
+ if not self.session_id:
164
+ raise RuntimeError("No active session. Call start_session() first.")
165
+
166
+ abs_path = (self.project_root / dir_path).resolve()
167
+ rel_path = abs_path.relative_to(self.project_root)
168
+
169
+ if rel_path in self._dir_snapshots:
170
+ return self._dir_snapshots[rel_path]
171
+
172
+ snapshot = DirectorySnapshot(
173
+ path=rel_path,
174
+ existed=abs_path.exists(),
175
+ )
176
+ self._dir_snapshots[rel_path] = snapshot
177
+
178
+ if not snapshot.existed:
179
+ self._dirs_created.add(rel_path)
180
+
181
+ return snapshot
182
+
183
+ def record_modification(self, file_path: Path) -> None:
184
+ """Record that a file was modified (after capturing)."""
185
+ rel_path = Path(file_path)
186
+ if rel_path in self._file_snapshots:
187
+ snapshot = self._file_snapshots[rel_path]
188
+ if snapshot.existed:
189
+ self._files_modified.add(rel_path)
190
+ else:
191
+ self._files_created.add(rel_path)
192
+
193
+ def record_deletion(self, file_path: Path) -> None:
194
+ """Record that a file was deleted."""
195
+ rel_path = Path(file_path)
196
+ if rel_path in self._file_snapshots and self._file_snapshots[rel_path].existed:
197
+ self._files_deleted.add(rel_path)
198
+
199
+ def revert_file(self, file_path: Path) -> bool:
200
+ """Revert a single file to its original state."""
201
+ rel_path = Path(file_path)
202
+ if rel_path not in self._file_snapshots:
203
+ return False
204
+
205
+ snapshot = self._file_snapshots[rel_path]
206
+ abs_path = self.project_root / rel_path
207
+
208
+ if not snapshot.existed:
209
+ # File was created during session - delete it
210
+ if abs_path.exists():
211
+ abs_path.unlink()
212
+ return True
213
+
214
+ # Restore original content
215
+ if snapshot.original_content is not None:
216
+ abs_path.parent.mkdir(parents=True, exist_ok=True)
217
+ abs_path.write_bytes(snapshot.original_content)
218
+ else:
219
+ # Large file - restore from backup
220
+ backup_path = self._large_file_backup_dir / rel_path
221
+ if backup_path.exists():
222
+ abs_path.parent.mkdir(parents=True, exist_ok=True)
223
+ shutil.copy2(backup_path, abs_path)
224
+
225
+ return True
226
+
227
+ def revert_all(self) -> Dict[str, List[str]]:
228
+ """
229
+ Revert ALL changes made during the session.
230
+
231
+ Returns a summary of what was reverted.
232
+ """
233
+ if not self.session_id:
234
+ return {"error": "No active session"}
235
+
236
+ reverted = {
237
+ "files_restored": [],
238
+ "files_deleted": [],
239
+ "dirs_deleted": [],
240
+ "errors": [],
241
+ }
242
+
243
+ # Revert files
244
+ for rel_path, snapshot in self._file_snapshots.items():
245
+ abs_path = self.project_root / rel_path
246
+
247
+ try:
248
+ if not snapshot.existed:
249
+ # Delete files that were created
250
+ if abs_path.exists():
251
+ abs_path.unlink()
252
+ reverted["files_deleted"].append(str(rel_path))
253
+ else:
254
+ # Restore original content
255
+ if snapshot.original_content is not None:
256
+ abs_path.parent.mkdir(parents=True, exist_ok=True)
257
+ abs_path.write_bytes(snapshot.original_content)
258
+ else:
259
+ # Large file from backup
260
+ backup_path = self._large_file_backup_dir / rel_path
261
+ if backup_path.exists():
262
+ abs_path.parent.mkdir(parents=True, exist_ok=True)
263
+ shutil.copy2(backup_path, abs_path)
264
+ reverted["files_restored"].append(str(rel_path))
265
+ except Exception as e:
266
+ reverted["errors"].append(f"{rel_path}: {e}")
267
+
268
+ # Remove created directories (in reverse order - deepest first)
269
+ created_dirs = sorted(self._dirs_created, key=lambda p: len(p.parts), reverse=True)
270
+ for rel_path in created_dirs:
271
+ abs_path = self.project_root / rel_path
272
+ try:
273
+ if abs_path.exists() and abs_path.is_dir():
274
+ # Only remove if empty
275
+ if not any(abs_path.iterdir()):
276
+ abs_path.rmdir()
277
+ reverted["dirs_deleted"].append(str(rel_path))
278
+ except Exception as e:
279
+ reverted["errors"].append(f"dir {rel_path}: {e}")
280
+
281
+ return reverted
282
+
283
+ def end_session(self, revert: bool = True) -> Dict[str, any]:
284
+ """
285
+ End the current session.
286
+
287
+ Args:
288
+ revert: If True, revert all changes. If False, keep changes.
289
+ """
290
+ if not self.session_id:
291
+ return {"error": "No active session"}
292
+
293
+ result = {
294
+ "session_id": self.session_id,
295
+ "duration_seconds": (datetime.now() - self.session_start).total_seconds(),
296
+ "files_tracked": len(self._file_snapshots),
297
+ "files_modified": list(str(p) for p in self._files_modified),
298
+ "files_created": list(str(p) for p in self._files_created),
299
+ "files_deleted": list(str(p) for p in self._files_deleted),
300
+ "dirs_created": list(str(p) for p in self._dirs_created),
301
+ }
302
+
303
+ if revert:
304
+ revert_result = self.revert_all()
305
+ result["revert_result"] = revert_result
306
+
307
+ # Cleanup temp backup dir
308
+ if self._large_file_backup_dir and self._large_file_backup_dir.exists():
309
+ shutil.rmtree(self._large_file_backup_dir, ignore_errors=True)
310
+
311
+ # Reset state
312
+ self.session_id = None
313
+ self.session_start = None
314
+ self._file_snapshots.clear()
315
+ self._dir_snapshots.clear()
316
+ self._large_file_backup_dir = None
317
+
318
+ return result
319
+
320
+ def get_changes_summary(self) -> Dict[str, any]:
321
+ """Get a summary of all changes in the current session."""
322
+ return {
323
+ "session_id": self.session_id,
324
+ "files_tracked": len(self._file_snapshots),
325
+ "files_modified": [str(p) for p in self._files_modified],
326
+ "files_created": [str(p) for p in self._files_created],
327
+ "files_deleted": [str(p) for p in self._files_deleted],
328
+ "dirs_created": [str(p) for p in self._dirs_created],
329
+ }
330
+
331
+ def get_modified_content(self, file_path: Path) -> Optional[bytes]:
332
+ """Get the current (modified) content of a tracked file."""
333
+ abs_path = self.project_root / file_path
334
+ if abs_path.exists():
335
+ return abs_path.read_bytes()
336
+ return None
337
+
338
+ def get_original_content(self, file_path: Path) -> Optional[bytes]:
339
+ """Get the original content of a tracked file."""
340
+ rel_path = Path(file_path)
341
+ if rel_path not in self._file_snapshots:
342
+ return None
343
+
344
+ snapshot = self._file_snapshots[rel_path]
345
+
346
+ if not snapshot.existed:
347
+ return None
348
+
349
+ if snapshot.original_content is not None:
350
+ return snapshot.original_content
351
+
352
+ # Large file from backup
353
+ backup_path = self._large_file_backup_dir / rel_path
354
+ if backup_path.exists():
355
+ return backup_path.read_bytes()
356
+
357
+ return None