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.
- superqode/__init__.py +33 -0
- superqode/acp/__init__.py +23 -0
- superqode/acp/client.py +913 -0
- superqode/acp/permission_screen.py +457 -0
- superqode/acp/types.py +480 -0
- superqode/acp_discovery.py +856 -0
- superqode/agent/__init__.py +22 -0
- superqode/agent/edit_strategies.py +334 -0
- superqode/agent/loop.py +892 -0
- superqode/agent/qe_report_templates.py +39 -0
- superqode/agent/system_prompts.py +353 -0
- superqode/agent_output.py +721 -0
- superqode/agent_stream.py +953 -0
- superqode/agents/__init__.py +59 -0
- superqode/agents/acp_registry.py +305 -0
- superqode/agents/client.py +249 -0
- superqode/agents/data/augmentcode.com.toml +51 -0
- superqode/agents/data/cagent.dev.toml +51 -0
- superqode/agents/data/claude.com.toml +60 -0
- superqode/agents/data/codeassistant.dev.toml +51 -0
- superqode/agents/data/codex.openai.com.toml +57 -0
- superqode/agents/data/fastagent.ai.toml +66 -0
- superqode/agents/data/geminicli.com.toml +77 -0
- superqode/agents/data/goose.block.xyz.toml +54 -0
- superqode/agents/data/junie.jetbrains.com.toml +56 -0
- superqode/agents/data/kimi.moonshot.cn.toml +57 -0
- superqode/agents/data/llmlingagent.dev.toml +51 -0
- superqode/agents/data/molt.bot.toml +49 -0
- superqode/agents/data/opencode.ai.toml +60 -0
- superqode/agents/data/stakpak.dev.toml +51 -0
- superqode/agents/data/vtcode.dev.toml +51 -0
- superqode/agents/discovery.py +266 -0
- superqode/agents/messaging.py +160 -0
- superqode/agents/persona.py +166 -0
- superqode/agents/registry.py +421 -0
- superqode/agents/schema.py +72 -0
- superqode/agents/unified.py +367 -0
- superqode/app/__init__.py +111 -0
- superqode/app/constants.py +314 -0
- superqode/app/css.py +366 -0
- superqode/app/models.py +118 -0
- superqode/app/suggester.py +125 -0
- superqode/app/widgets.py +1591 -0
- superqode/app_enhanced.py +399 -0
- superqode/app_main.py +17187 -0
- superqode/approval.py +312 -0
- superqode/atomic.py +296 -0
- superqode/commands/__init__.py +1 -0
- superqode/commands/acp.py +965 -0
- superqode/commands/agents.py +180 -0
- superqode/commands/auth.py +278 -0
- superqode/commands/config.py +374 -0
- superqode/commands/init.py +826 -0
- superqode/commands/providers.py +819 -0
- superqode/commands/qe.py +1145 -0
- superqode/commands/roles.py +380 -0
- superqode/commands/serve.py +172 -0
- superqode/commands/suggestions.py +127 -0
- superqode/commands/superqe.py +460 -0
- superqode/config/__init__.py +51 -0
- superqode/config/loader.py +812 -0
- superqode/config/schema.py +498 -0
- superqode/core/__init__.py +111 -0
- superqode/core/roles.py +281 -0
- superqode/danger.py +386 -0
- superqode/data/superqode-template.yaml +1522 -0
- superqode/design_system.py +1080 -0
- superqode/dialogs/__init__.py +6 -0
- superqode/dialogs/base.py +39 -0
- superqode/dialogs/model.py +130 -0
- superqode/dialogs/provider.py +870 -0
- superqode/diff_view.py +919 -0
- superqode/enterprise.py +21 -0
- superqode/evaluation/__init__.py +25 -0
- superqode/evaluation/adapters.py +93 -0
- superqode/evaluation/behaviors.py +89 -0
- superqode/evaluation/engine.py +209 -0
- superqode/evaluation/scenarios.py +96 -0
- superqode/execution/__init__.py +36 -0
- superqode/execution/linter.py +538 -0
- superqode/execution/modes.py +347 -0
- superqode/execution/resolver.py +283 -0
- superqode/execution/runner.py +642 -0
- superqode/file_explorer.py +811 -0
- superqode/file_viewer.py +471 -0
- superqode/flash.py +183 -0
- superqode/guidance/__init__.py +58 -0
- superqode/guidance/config.py +203 -0
- superqode/guidance/prompts.py +71 -0
- superqode/harness/__init__.py +54 -0
- superqode/harness/accelerator.py +291 -0
- superqode/harness/config.py +319 -0
- superqode/harness/validator.py +147 -0
- superqode/history.py +279 -0
- superqode/integrations/superopt_runner.py +124 -0
- superqode/logging/__init__.py +49 -0
- superqode/logging/adapters.py +219 -0
- superqode/logging/formatter.py +923 -0
- superqode/logging/integration.py +341 -0
- superqode/logging/sinks.py +170 -0
- superqode/logging/unified_log.py +417 -0
- superqode/lsp/__init__.py +26 -0
- superqode/lsp/client.py +544 -0
- superqode/main.py +1069 -0
- superqode/mcp/__init__.py +89 -0
- superqode/mcp/auth_storage.py +380 -0
- superqode/mcp/client.py +1236 -0
- superqode/mcp/config.py +319 -0
- superqode/mcp/integration.py +337 -0
- superqode/mcp/oauth.py +436 -0
- superqode/mcp/oauth_callback.py +385 -0
- superqode/mcp/types.py +290 -0
- superqode/memory/__init__.py +31 -0
- superqode/memory/feedback.py +342 -0
- superqode/memory/store.py +522 -0
- superqode/notifications.py +369 -0
- superqode/optimization/__init__.py +5 -0
- superqode/optimization/config.py +33 -0
- superqode/permissions/__init__.py +25 -0
- superqode/permissions/rules.py +488 -0
- superqode/plan.py +323 -0
- superqode/providers/__init__.py +33 -0
- superqode/providers/gateway/__init__.py +165 -0
- superqode/providers/gateway/base.py +228 -0
- superqode/providers/gateway/litellm_gateway.py +1170 -0
- superqode/providers/gateway/openresponses_gateway.py +436 -0
- superqode/providers/health.py +297 -0
- superqode/providers/huggingface/__init__.py +74 -0
- superqode/providers/huggingface/downloader.py +472 -0
- superqode/providers/huggingface/endpoints.py +442 -0
- superqode/providers/huggingface/hub.py +531 -0
- superqode/providers/huggingface/inference.py +394 -0
- superqode/providers/huggingface/transformers_runner.py +516 -0
- superqode/providers/local/__init__.py +100 -0
- superqode/providers/local/base.py +438 -0
- superqode/providers/local/discovery.py +418 -0
- superqode/providers/local/lmstudio.py +256 -0
- superqode/providers/local/mlx.py +457 -0
- superqode/providers/local/ollama.py +486 -0
- superqode/providers/local/sglang.py +268 -0
- superqode/providers/local/tgi.py +260 -0
- superqode/providers/local/tool_support.py +477 -0
- superqode/providers/local/vllm.py +258 -0
- superqode/providers/manager.py +1338 -0
- superqode/providers/models.py +1016 -0
- superqode/providers/models_dev.py +578 -0
- superqode/providers/openresponses/__init__.py +87 -0
- superqode/providers/openresponses/converters/__init__.py +17 -0
- superqode/providers/openresponses/converters/messages.py +343 -0
- superqode/providers/openresponses/converters/tools.py +268 -0
- superqode/providers/openresponses/schema/__init__.py +56 -0
- superqode/providers/openresponses/schema/models.py +585 -0
- superqode/providers/openresponses/streaming/__init__.py +5 -0
- superqode/providers/openresponses/streaming/parser.py +338 -0
- superqode/providers/openresponses/tools/__init__.py +21 -0
- superqode/providers/openresponses/tools/apply_patch.py +352 -0
- superqode/providers/openresponses/tools/code_interpreter.py +290 -0
- superqode/providers/openresponses/tools/file_search.py +333 -0
- superqode/providers/openresponses/tools/mcp_adapter.py +252 -0
- superqode/providers/registry.py +716 -0
- superqode/providers/usage.py +332 -0
- superqode/pure_mode.py +384 -0
- superqode/qr/__init__.py +23 -0
- superqode/qr/dashboard.py +781 -0
- superqode/qr/generator.py +1018 -0
- superqode/qr/templates.py +135 -0
- superqode/safety/__init__.py +41 -0
- superqode/safety/sandbox.py +413 -0
- superqode/safety/warnings.py +256 -0
- superqode/server/__init__.py +33 -0
- superqode/server/lsp_server.py +775 -0
- superqode/server/web.py +250 -0
- superqode/session/__init__.py +25 -0
- superqode/session/persistence.py +580 -0
- superqode/session/sharing.py +477 -0
- superqode/session.py +475 -0
- superqode/sidebar.py +2991 -0
- superqode/stream_view.py +648 -0
- superqode/styles/__init__.py +3 -0
- superqode/superqe/__init__.py +184 -0
- superqode/superqe/acp_runner.py +1064 -0
- superqode/superqe/constitution/__init__.py +62 -0
- superqode/superqe/constitution/evaluator.py +308 -0
- superqode/superqe/constitution/loader.py +432 -0
- superqode/superqe/constitution/schema.py +250 -0
- superqode/superqe/events.py +591 -0
- superqode/superqe/frameworks/__init__.py +65 -0
- superqode/superqe/frameworks/base.py +234 -0
- superqode/superqe/frameworks/e2e.py +263 -0
- superqode/superqe/frameworks/executor.py +237 -0
- superqode/superqe/frameworks/javascript.py +409 -0
- superqode/superqe/frameworks/python.py +373 -0
- superqode/superqe/frameworks/registry.py +92 -0
- superqode/superqe/mcp_tools/__init__.py +47 -0
- superqode/superqe/mcp_tools/core_tools.py +418 -0
- superqode/superqe/mcp_tools/registry.py +230 -0
- superqode/superqe/mcp_tools/testing_tools.py +167 -0
- superqode/superqe/noise.py +89 -0
- superqode/superqe/orchestrator.py +778 -0
- superqode/superqe/roles.py +609 -0
- superqode/superqe/session.py +713 -0
- superqode/superqe/skills/__init__.py +57 -0
- superqode/superqe/skills/base.py +106 -0
- superqode/superqe/skills/core_skills.py +899 -0
- superqode/superqe/skills/registry.py +90 -0
- superqode/superqe/verifier.py +101 -0
- superqode/superqe_cli.py +76 -0
- superqode/tool_call.py +358 -0
- superqode/tools/__init__.py +93 -0
- superqode/tools/agent_tools.py +496 -0
- superqode/tools/base.py +324 -0
- superqode/tools/batch_tool.py +133 -0
- superqode/tools/diagnostics.py +311 -0
- superqode/tools/edit_tools.py +653 -0
- superqode/tools/enhanced_base.py +515 -0
- superqode/tools/file_tools.py +269 -0
- superqode/tools/file_tracking.py +45 -0
- superqode/tools/lsp_tools.py +610 -0
- superqode/tools/network_tools.py +350 -0
- superqode/tools/permissions.py +400 -0
- superqode/tools/question_tool.py +324 -0
- superqode/tools/search_tools.py +598 -0
- superqode/tools/shell_tools.py +259 -0
- superqode/tools/todo_tools.py +121 -0
- superqode/tools/validation.py +80 -0
- superqode/tools/web_tools.py +639 -0
- superqode/tui.py +1152 -0
- superqode/tui_integration.py +875 -0
- superqode/tui_widgets/__init__.py +27 -0
- superqode/tui_widgets/widgets/__init__.py +18 -0
- superqode/tui_widgets/widgets/progress.py +185 -0
- superqode/tui_widgets/widgets/tool_display.py +188 -0
- superqode/undo_manager.py +574 -0
- superqode/utils/__init__.py +5 -0
- superqode/utils/error_handling.py +323 -0
- superqode/utils/fuzzy.py +257 -0
- superqode/widgets/__init__.py +477 -0
- superqode/widgets/agent_collab.py +390 -0
- superqode/widgets/agent_store.py +936 -0
- superqode/widgets/agent_switcher.py +395 -0
- superqode/widgets/animation_manager.py +284 -0
- superqode/widgets/code_context.py +356 -0
- superqode/widgets/command_palette.py +412 -0
- superqode/widgets/connection_status.py +537 -0
- superqode/widgets/conversation_history.py +470 -0
- superqode/widgets/diff_indicator.py +155 -0
- superqode/widgets/enhanced_status_bar.py +385 -0
- superqode/widgets/enhanced_toast.py +476 -0
- superqode/widgets/file_browser.py +809 -0
- superqode/widgets/file_reference.py +585 -0
- superqode/widgets/issue_timeline.py +340 -0
- superqode/widgets/leader_key.py +264 -0
- superqode/widgets/mode_switcher.py +445 -0
- superqode/widgets/model_picker.py +234 -0
- superqode/widgets/permission_preview.py +1205 -0
- superqode/widgets/prompt.py +358 -0
- superqode/widgets/provider_connect.py +725 -0
- superqode/widgets/pty_shell.py +587 -0
- superqode/widgets/qe_dashboard.py +321 -0
- superqode/widgets/resizable_sidebar.py +377 -0
- superqode/widgets/response_changes.py +218 -0
- superqode/widgets/response_display.py +528 -0
- superqode/widgets/rich_tool_display.py +613 -0
- superqode/widgets/sidebar_panels.py +1180 -0
- superqode/widgets/slash_complete.py +356 -0
- superqode/widgets/split_view.py +612 -0
- superqode/widgets/status_bar.py +273 -0
- superqode/widgets/superqode_display.py +786 -0
- superqode/widgets/thinking_display.py +815 -0
- superqode/widgets/throbber.py +87 -0
- superqode/widgets/toast.py +206 -0
- superqode/widgets/unified_output.py +1073 -0
- superqode/workspace/__init__.py +75 -0
- superqode/workspace/artifacts.py +472 -0
- superqode/workspace/coordinator.py +353 -0
- superqode/workspace/diff_tracker.py +429 -0
- superqode/workspace/git_guard.py +373 -0
- superqode/workspace/git_snapshot.py +526 -0
- superqode/workspace/manager.py +750 -0
- superqode/workspace/snapshot.py +357 -0
- superqode/workspace/watcher.py +535 -0
- superqode/workspace/worktree.py +440 -0
- superqode-0.1.5.dist-info/METADATA +204 -0
- superqode-0.1.5.dist-info/RECORD +288 -0
- superqode-0.1.5.dist-info/WHEEL +5 -0
- superqode-0.1.5.dist-info/entry_points.txt +3 -0
- superqode-0.1.5.dist-info/licenses/LICENSE +648 -0
- 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)
|