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,269 @@
1
+ """
2
+ File Tools - Minimal, Transparent File Operations.
3
+
4
+ NO fancy algorithms, NO hidden context, NO opinionated formatting.
5
+ Just raw file operations that let the model do its thing.
6
+
7
+ When a QE session is active, writes are routed through the WorkspaceManager
8
+ to ensure the immutable repo guarantee.
9
+ """
10
+
11
+ import os
12
+ from pathlib import Path
13
+ from typing import Any, Dict, Optional
14
+
15
+ from .base import Tool, ToolResult, ToolContext
16
+ from .validation import validate_path_in_working_directory
17
+ from .file_tracking import record_file_read
18
+
19
+
20
+ def _get_workspace():
21
+ """Get the active workspace manager if available."""
22
+ try:
23
+ from superqode.workspace import WorkspaceManager
24
+ from superqode.workspace.manager import get_workspace
25
+
26
+ workspace = get_workspace()
27
+ if workspace and workspace.is_active:
28
+ return workspace
29
+ except ImportError:
30
+ pass
31
+ return None
32
+
33
+
34
+ class ReadFileTool(Tool):
35
+ """Read file contents. Simple, no magic."""
36
+
37
+ @property
38
+ def name(self) -> str:
39
+ return "read_file"
40
+
41
+ @property
42
+ def description(self) -> str:
43
+ # Minimal description - let the model figure out when to use it
44
+ return "Read the contents of a file."
45
+
46
+ @property
47
+ def parameters(self) -> Dict[str, Any]:
48
+ return {
49
+ "type": "object",
50
+ "properties": {
51
+ "path": {"type": "string", "description": "Path to the file to read"},
52
+ "start_line": {
53
+ "type": "integer",
54
+ "description": "Starting line number (1-indexed, optional)",
55
+ },
56
+ "end_line": {
57
+ "type": "integer",
58
+ "description": "Ending line number (inclusive, optional)",
59
+ },
60
+ },
61
+ "required": ["path"],
62
+ }
63
+
64
+ async def execute(self, args: Dict[str, Any], ctx: ToolContext) -> ToolResult:
65
+ path = args.get("path", "")
66
+ start_line = args.get("start_line")
67
+ end_line = args.get("end_line")
68
+
69
+ try:
70
+ # Validate and resolve path - ensures it stays within working directory
71
+ file_path = validate_path_in_working_directory(path, ctx.working_directory)
72
+ if not file_path.exists():
73
+ return ToolResult(success=False, output="", error=f"File not found: {path}")
74
+
75
+ if file_path.is_dir():
76
+ return ToolResult(
77
+ success=False, output="", error=f"Path is a directory, not a file: {path}"
78
+ )
79
+
80
+ content = file_path.read_text()
81
+
82
+ # Record file read time for edit-conflict detection
83
+ try:
84
+ record_file_read(
85
+ getattr(ctx, "session_id", "") or "",
86
+ str(file_path.resolve()),
87
+ file_path.stat().st_mtime,
88
+ )
89
+ except OSError:
90
+ pass
91
+
92
+ # Handle line range if specified
93
+ if start_line is not None or end_line is not None:
94
+ lines = content.split("\n")
95
+ start = (start_line - 1) if start_line else 0
96
+ end = end_line if end_line else len(lines)
97
+ content = "\n".join(lines[start:end])
98
+
99
+ return ToolResult(
100
+ success=True,
101
+ output=content,
102
+ metadata={"path": str(file_path), "size": len(content)},
103
+ )
104
+
105
+ except Exception as e:
106
+ return ToolResult(success=False, output="", error=str(e))
107
+
108
+
109
+ class WriteFileTool(Tool):
110
+ """Write content to a file. Creates directories if needed.
111
+
112
+ When a QE session is active, writes go through the WorkspaceManager
113
+ to ensure changes can be tracked and reverted.
114
+ """
115
+
116
+ @property
117
+ def name(self) -> str:
118
+ return "write_file"
119
+
120
+ @property
121
+ def description(self) -> str:
122
+ return (
123
+ "Write content to a file. Creates the file and parent directories if they don't exist."
124
+ )
125
+
126
+ @property
127
+ def parameters(self) -> Dict[str, Any]:
128
+ return {
129
+ "type": "object",
130
+ "properties": {
131
+ "path": {"type": "string", "description": "Path to the file to write"},
132
+ "content": {"type": "string", "description": "Content to write to the file"},
133
+ },
134
+ "required": ["path", "content"],
135
+ }
136
+
137
+ async def execute(self, args: Dict[str, Any], ctx: ToolContext) -> ToolResult:
138
+ path = args.get("path", "")
139
+ content = args.get("content", "")
140
+
141
+ try:
142
+ # Validate and resolve path - ensures it stays within working directory
143
+ file_path = validate_path_in_working_directory(path, ctx.working_directory)
144
+ # Check if QE session is active - route through workspace
145
+ workspace = _get_workspace()
146
+ if workspace:
147
+ # Get relative path for workspace
148
+ try:
149
+ rel_path = file_path.relative_to(workspace.project_root)
150
+ workspace.write_file(str(rel_path), content)
151
+ return ToolResult(
152
+ success=True,
153
+ output=f"Successfully wrote {len(content)} bytes to {path} (tracked for QE revert)",
154
+ metadata={"path": str(file_path), "size": len(content), "qe_tracked": True},
155
+ )
156
+ except ValueError:
157
+ # Path is outside project root, write directly
158
+ pass
159
+
160
+ # Direct write (no QE session or outside project)
161
+ file_path.parent.mkdir(parents=True, exist_ok=True)
162
+ file_path.write_text(content)
163
+
164
+ return ToolResult(
165
+ success=True,
166
+ output=f"Successfully wrote {len(content)} bytes to {path}",
167
+ metadata={"path": str(file_path), "size": len(content)},
168
+ )
169
+
170
+ except Exception as e:
171
+ return ToolResult(success=False, output="", error=str(e))
172
+
173
+
174
+ class ListDirectoryTool(Tool):
175
+ """List directory contents."""
176
+
177
+ @property
178
+ def name(self) -> str:
179
+ return "list_directory"
180
+
181
+ @property
182
+ def description(self) -> str:
183
+ return "List files and directories in a path."
184
+
185
+ @property
186
+ def parameters(self) -> Dict[str, Any]:
187
+ return {
188
+ "type": "object",
189
+ "properties": {
190
+ "path": {
191
+ "type": "string",
192
+ "description": "Directory path to list (default: current directory)",
193
+ },
194
+ "recursive": {
195
+ "type": "boolean",
196
+ "description": "List recursively (default: false)",
197
+ },
198
+ "max_depth": {
199
+ "type": "integer",
200
+ "description": "Maximum depth for recursive listing (default: 3)",
201
+ },
202
+ },
203
+ "required": [],
204
+ }
205
+
206
+ async def execute(self, args: Dict[str, Any], ctx: ToolContext) -> ToolResult:
207
+ path = args.get("path", ".")
208
+ recursive = args.get("recursive", False)
209
+ max_depth = args.get("max_depth", 3)
210
+
211
+ try:
212
+ # Validate and resolve path - ensures it stays within working directory
213
+ dir_path = validate_path_in_working_directory(path, ctx.working_directory)
214
+ if not dir_path.exists():
215
+ return ToolResult(success=False, output="", error=f"Directory not found: {path}")
216
+
217
+ if not dir_path.is_dir():
218
+ return ToolResult(
219
+ success=False, output="", error=f"Path is not a directory: {path}"
220
+ )
221
+
222
+ entries = []
223
+
224
+ if recursive:
225
+ entries = self._list_recursive(dir_path, dir_path, max_depth, 0)
226
+ else:
227
+ for entry in sorted(dir_path.iterdir()):
228
+ prefix = "[DIR] " if entry.is_dir() else "[FILE]"
229
+ entries.append(f"{prefix} {entry.name}")
230
+
231
+ output = "\n".join(entries) if entries else "(empty directory)"
232
+
233
+ return ToolResult(
234
+ success=True, output=output, metadata={"path": str(dir_path), "count": len(entries)}
235
+ )
236
+
237
+ except Exception as e:
238
+ return ToolResult(success=False, output="", error=str(e))
239
+
240
+ def _list_recursive(self, base: Path, current: Path, max_depth: int, depth: int) -> list:
241
+ """Recursively list directory contents."""
242
+ if depth >= max_depth:
243
+ return []
244
+
245
+ entries = []
246
+ indent = " " * depth
247
+
248
+ try:
249
+ for entry in sorted(current.iterdir()):
250
+ # Skip hidden and common ignore patterns
251
+ if entry.name.startswith(".") or entry.name in (
252
+ "node_modules",
253
+ "__pycache__",
254
+ "venv",
255
+ ".git",
256
+ ):
257
+ continue
258
+
259
+ rel_path = entry.relative_to(base)
260
+
261
+ if entry.is_dir():
262
+ entries.append(f"{indent}[DIR] {rel_path}/")
263
+ entries.extend(self._list_recursive(base, entry, max_depth, depth + 1))
264
+ else:
265
+ entries.append(f"{indent}[FILE] {rel_path}")
266
+ except PermissionError:
267
+ pass
268
+
269
+ return entries
@@ -0,0 +1,45 @@
1
+ """
2
+ File Time Tracking - Prevent edit conflicts.
3
+
4
+ Tracks when files were last read per session. Before editing, we check that
5
+ the file has not been modified externally since the last read. If it has,
6
+ we require the user/agent to re-read and try again.
7
+
8
+ """
9
+
10
+ from pathlib import Path
11
+ from typing import Dict, Optional, Tuple
12
+
13
+ # (session_id, resolved_path_str) -> mtime at last read
14
+ _file_read_times: Dict[Tuple[str, str], float] = {}
15
+
16
+
17
+ def record_file_read(session_id: str, path: str, mtime: float) -> None:
18
+ """Record that a file was read at the given mtime."""
19
+ resolved = str(Path(path).resolve())
20
+ _file_read_times[(session_id, resolved)] = mtime
21
+
22
+
23
+ def get_file_read_mtime(session_id: str, path: str) -> Optional[float]:
24
+ """Get the mtime when this file was last read in this session, or None."""
25
+ resolved = str(Path(path).resolve())
26
+ return _file_read_times.get((session_id, resolved))
27
+
28
+
29
+ def check_file_unchanged(
30
+ session_id: str, path: str, current_mtime: float
31
+ ) -> Tuple[bool, Optional[str]]:
32
+ """
33
+ Check that the file has not been modified since last read.
34
+ Returns (True, None) if ok to edit, or (False, error_message) if not.
35
+ If the file was never read in this session, we allow the edit (no prior read to compare).
36
+ """
37
+ stored = get_file_read_mtime(session_id, path)
38
+ if stored is None:
39
+ return (True, None)
40
+ if current_mtime != stored:
41
+ return (
42
+ False,
43
+ "File was modified externally since last read. Re-read the file and try again.",
44
+ )
45
+ return (True, None)