crabcode 0.1.6__py3-none-win_amd64.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.
- crabcode-0.1.6.dist-info/METADATA +63 -0
- crabcode-0.1.6.dist-info/RECORD +205 -0
- crabcode-0.1.6.dist-info/WHEEL +4 -0
- crabcode-0.1.6.dist-info/entry_points.txt +2 -0
- crabcode_cli/__init__.py +7 -0
- crabcode_cli/__main__.py +4 -0
- crabcode_cli/_native/crabcode-computer-use-host.exe +0 -0
- crabcode_cli/_native/runtime.json +6 -0
- crabcode_cli/app.py +776 -0
- crabcode_cli/banner.py +39 -0
- crabcode_cli/computer_use.py +286 -0
- crabcode_cli/model_manager.py +649 -0
- crabcode_cli/model_picker.py +168 -0
- crabcode_cli/pipe.py +148 -0
- crabcode_cli/render/__init__.py +0 -0
- crabcode_cli/render/messages.py +19 -0
- crabcode_cli/repl.py +4841 -0
- crabcode_cli/session_picker.py +239 -0
- crabcode_core/__init__.py +7 -0
- crabcode_core/_version.py +3 -0
- crabcode_core/_windows_process.py +68 -0
- crabcode_core/agent_manager.py +1731 -0
- crabcode_core/api/__init__.py +20 -0
- crabcode_core/api/anthropic_adapter.py +648 -0
- crabcode_core/api/azure_adapter.py +69 -0
- crabcode_core/api/base.py +233 -0
- crabcode_core/api/codex_adapter.py +1327 -0
- crabcode_core/api/error_logging.py +132 -0
- crabcode_core/api/gemini_adapter.py +26 -0
- crabcode_core/api/model_info.py +257 -0
- crabcode_core/api/network.py +62 -0
- crabcode_core/api/ollama_adapter.py +27 -0
- crabcode_core/api/openai_adapter.py +323 -0
- crabcode_core/api/registry.py +58 -0
- crabcode_core/command_line.py +18 -0
- crabcode_core/compact/__init__.py +17 -0
- crabcode_core/compact/compact.py +514 -0
- crabcode_core/compact/context_tokens.py +165 -0
- crabcode_core/computer_use_mode.py +38 -0
- crabcode_core/computer_use_observation.py +144 -0
- crabcode_core/config/__init__.py +3 -0
- crabcode_core/config/claudemd.py +61 -0
- crabcode_core/config/manager.py +237 -0
- crabcode_core/config/model_catalog.py +256 -0
- crabcode_core/events.py +4896 -0
- crabcode_core/file_lock.py +81 -0
- crabcode_core/filesystem.py +22 -0
- crabcode_core/goal.py +149 -0
- crabcode_core/hooks/__init__.py +5 -0
- crabcode_core/hooks/manager.py +290 -0
- crabcode_core/image_io.py +111 -0
- crabcode_core/io_worker.py +211 -0
- crabcode_core/logging_utils.py +168 -0
- crabcode_core/lsp/__init__.py +6 -0
- crabcode_core/lsp/client.py +786 -0
- crabcode_core/lsp/diagnostics.py +150 -0
- crabcode_core/lsp/manager.py +583 -0
- crabcode_core/mcp/__init__.py +4 -0
- crabcode_core/mcp/client.py +169 -0
- crabcode_core/mcp/config.py +57 -0
- crabcode_core/path_validation.py +34 -0
- crabcode_core/paths.py +24 -0
- crabcode_core/peer/__init__.py +10 -0
- crabcode_core/peer/runtime.py +518 -0
- crabcode_core/permissions/__init__.py +7 -0
- crabcode_core/permissions/ai_reviewer.py +184 -0
- crabcode_core/permissions/manager.py +187 -0
- crabcode_core/plan/__init__.py +5 -0
- crabcode_core/plan/executor.py +270 -0
- crabcode_core/plan/types.py +169 -0
- crabcode_core/prompts/__init__.py +0 -0
- crabcode_core/prompts/blocks.py +10 -0
- crabcode_core/prompts/context.py +229 -0
- crabcode_core/prompts/library.py +92 -0
- crabcode_core/prompts/profile.py +79 -0
- crabcode_core/prompts/system.py +356 -0
- crabcode_core/prompts/templates.py +104 -0
- crabcode_core/query/__init__.py +0 -0
- crabcode_core/query/computer_use_history.py +83 -0
- crabcode_core/query/loop.py +2446 -0
- crabcode_core/query/retry.py +116 -0
- crabcode_core/schedule/__init__.py +13 -0
- crabcode_core/schedule/manager.py +631 -0
- crabcode_core/schedule/models.py +204 -0
- crabcode_core/schedule/store.py +558 -0
- crabcode_core/schedule/timing.py +287 -0
- crabcode_core/session/__init__.py +12 -0
- crabcode_core/session/export.py +141 -0
- crabcode_core/session/meta_db.py +684 -0
- crabcode_core/session/storage.py +2032 -0
- crabcode_core/session/title_gen.py +52 -0
- crabcode_core/skills/__init__.py +6 -0
- crabcode_core/skills/context.py +23 -0
- crabcode_core/skills/loader.py +214 -0
- crabcode_core/skills/matcher.py +203 -0
- crabcode_core/snapshot/__init__.py +21 -0
- crabcode_core/snapshot/snapshot.py +436 -0
- crabcode_core/snapshot/tracker.py +359 -0
- crabcode_core/subprocess_utils.py +260 -0
- crabcode_core/team/__init__.py +27 -0
- crabcode_core/team/bridge.py +66 -0
- crabcode_core/team/inbox.py +237 -0
- crabcode_core/team/manager.py +1660 -0
- crabcode_core/team/message_bus.py +821 -0
- crabcode_core/team/models.py +191 -0
- crabcode_core/team/recovery.py +192 -0
- crabcode_core/text_io.py +52 -0
- crabcode_core/tools/__init__.py +112 -0
- crabcode_core/tools/_input_helpers.py +39 -0
- crabcode_core/tools/_input_schema.py +130 -0
- crabcode_core/tools/agent.py +413 -0
- crabcode_core/tools/apply_patch.py +587 -0
- crabcode_core/tools/ask_user.py +122 -0
- crabcode_core/tools/bash.py +154 -0
- crabcode_core/tools/browser.py +678 -0
- crabcode_core/tools/checklist.py +241 -0
- crabcode_core/tools/checkpoint.py +78 -0
- crabcode_core/tools/computer_use.py +599 -0
- crabcode_core/tools/diff_utils.py +123 -0
- crabcode_core/tools/file_edit.py +240 -0
- crabcode_core/tools/file_read.py +187 -0
- crabcode_core/tools/file_write.py +154 -0
- crabcode_core/tools/glob.py +123 -0
- crabcode_core/tools/goal.py +134 -0
- crabcode_core/tools/grep.py +247 -0
- crabcode_core/tools/image.py +173 -0
- crabcode_core/tools/image_generate.py +115 -0
- crabcode_core/tools/lint.py +388 -0
- crabcode_core/tools/loading.py +187 -0
- crabcode_core/tools/memory.py +456 -0
- crabcode_core/tools/monitor.py +677 -0
- crabcode_core/tools/peer.py +103 -0
- crabcode_core/tools/revert.py +90 -0
- crabcode_core/tools/schedule.py +683 -0
- crabcode_core/tools/skill.py +108 -0
- crabcode_core/tools/switch_mode.py +213 -0
- crabcode_core/tools/team.py +329 -0
- crabcode_core/tools/tool_search.py +64 -0
- crabcode_core/tools/web_search.py +444 -0
- crabcode_core/turn_details.py +64 -0
- crabcode_core/types/__init__.py +95 -0
- crabcode_core/types/config.py +434 -0
- crabcode_core/types/event.py +334 -0
- crabcode_core/types/message.py +263 -0
- crabcode_core/types/tool.py +208 -0
- crabcode_core/usage.py +289 -0
- crabcode_core/utf8_sanitize.py +29 -0
- crabcode_debugger/__init__.py +5 -0
- crabcode_debugger/adapters.py +332 -0
- crabcode_debugger/dap.py +304 -0
- crabcode_debugger/memory.py +1806 -0
- crabcode_debugger/process.py +707 -0
- crabcode_debugger/runtime.py +58 -0
- crabcode_debugger/sessions.py +387 -0
- crabcode_debugger/tool.py +731 -0
- crabcode_gateway/__init__.py +18 -0
- crabcode_gateway/acp/__init__.py +13 -0
- crabcode_gateway/acp/agent.py +1581 -0
- crabcode_gateway/acp/session.py +114 -0
- crabcode_gateway/acp/transport.py +39 -0
- crabcode_gateway/acp/types.py +90 -0
- crabcode_gateway/adapter.py +30 -0
- crabcode_gateway/auth.py +139 -0
- crabcode_gateway/computer_use.py +364 -0
- crabcode_gateway/document_engine.py +637 -0
- crabcode_gateway/document_worker.py +301 -0
- crabcode_gateway/event_bus.py +655 -0
- crabcode_gateway/grpc/__init__.py +0 -0
- crabcode_gateway/grpc/crabcode_pb2.py +70 -0
- crabcode_gateway/grpc/crabcode_pb2_grpc.py +474 -0
- crabcode_gateway/grpc/proto/crabcode.proto +160 -0
- crabcode_gateway/grpc/server.py +765 -0
- crabcode_gateway/middleware.py +344 -0
- crabcode_gateway/protocol.py +17 -0
- crabcode_gateway/routes/__init__.py +0 -0
- crabcode_gateway/routes/agent.py +392 -0
- crabcode_gateway/routes/auth.py +150 -0
- crabcode_gateway/routes/computer_use.py +101 -0
- crabcode_gateway/routes/config.py +1698 -0
- crabcode_gateway/routes/document.py +1810 -0
- crabcode_gateway/routes/event.py +3801 -0
- crabcode_gateway/routes/git_review.py +232 -0
- crabcode_gateway/routes/health.py +14 -0
- crabcode_gateway/routes/peer.py +78 -0
- crabcode_gateway/routes/permission.py +121 -0
- crabcode_gateway/routes/schedule.py +300 -0
- crabcode_gateway/routes/session.py +1496 -0
- crabcode_gateway/routes/snapshot.py +211 -0
- crabcode_gateway/routes/tasks.py +318 -0
- crabcode_gateway/routes/team.py +392 -0
- crabcode_gateway/routes/usage.py +49 -0
- crabcode_gateway/routes/workspace.py +299 -0
- crabcode_gateway/safe_file.py +63 -0
- crabcode_gateway/schemas.py +1820 -0
- crabcode_gateway/server.py +514 -0
- crabcode_gateway/session_registry.py +36 -0
- crabcode_gateway/task_registry.py +708 -0
- crabcode_gateway/turn_details.py +60 -0
- crabcode_search/__init__.py +11 -0
- crabcode_search/background.py +368 -0
- crabcode_search/chunker.py +517 -0
- crabcode_search/embedder.py +497 -0
- crabcode_search/indexer.py +215 -0
- crabcode_search/store.py +222 -0
- crabcode_search/tool.py +268 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""FileWriteTool — create or overwrite files."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from crabcode_core.logging_utils import get_logger
|
|
9
|
+
from crabcode_core.text_io import normalize_newlines, read_utf8_text, write_utf8_text
|
|
10
|
+
from crabcode_core.tools.diff_utils import compute_diff, format_edit_summary
|
|
11
|
+
from crabcode_core.types.tool import Tool, ToolContext, ToolResult
|
|
12
|
+
from crabcode_core.io_worker import run_file_tool
|
|
13
|
+
|
|
14
|
+
logger = get_logger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def _get_lsp_diagnostics(
|
|
18
|
+
file_path: str | Path,
|
|
19
|
+
context: ToolContext,
|
|
20
|
+
) -> str:
|
|
21
|
+
"""Collect LSP diagnostics for the written file, return formatted string."""
|
|
22
|
+
lsp = context.lsp_manager
|
|
23
|
+
if lsp is None:
|
|
24
|
+
return ""
|
|
25
|
+
from crabcode_core.lsp.diagnostics import collect_and_format_diagnostics
|
|
26
|
+
return await collect_and_format_diagnostics(lsp, str(file_path))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class FileWriteTool(Tool):
|
|
30
|
+
name = "Write"
|
|
31
|
+
description = "Write content to a file, creating it if necessary."
|
|
32
|
+
is_read_only = False
|
|
33
|
+
is_concurrency_safe = False
|
|
34
|
+
input_schema = {
|
|
35
|
+
"type": "object",
|
|
36
|
+
"properties": {
|
|
37
|
+
"file_path": {
|
|
38
|
+
"type": "string",
|
|
39
|
+
"description": "The path of the file to write.",
|
|
40
|
+
},
|
|
41
|
+
"content": {
|
|
42
|
+
"type": "string",
|
|
43
|
+
"description": "The content to write to the file.",
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
"required": ["file_path", "content"],
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async def get_prompt(self, **kwargs: Any) -> str:
|
|
50
|
+
return (
|
|
51
|
+
"Write content to a file. Creates the file and parent directories "
|
|
52
|
+
"if they don't exist. WARNING: This will overwrite the existing "
|
|
53
|
+
"file if there is one.\n\n"
|
|
54
|
+
"ALWAYS prefer editing existing files with the Edit tool instead "
|
|
55
|
+
"of rewriting them. Only use Write for:\n"
|
|
56
|
+
"- Creating new files that don't exist yet\n"
|
|
57
|
+
"- Cases where the entire file content needs to be replaced\n\n"
|
|
58
|
+
"NEVER create files unless absolutely necessary for achieving "
|
|
59
|
+
"your goal. This prevents file bloat and builds on existing "
|
|
60
|
+
"work more effectively.\n"
|
|
61
|
+
"NEVER proactively create documentation or README files unless "
|
|
62
|
+
"the user explicitly requests them."
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
async def call(
|
|
66
|
+
self, tool_input: dict[str, Any], context: ToolContext,
|
|
67
|
+
) -> ToolResult:
|
|
68
|
+
result = await run_file_tool(self.name, tool_input, context)
|
|
69
|
+
if not result.is_error and result.data:
|
|
70
|
+
result.result_for_model += await _get_lsp_diagnostics(result.data["file_path"], context)
|
|
71
|
+
return result
|
|
72
|
+
|
|
73
|
+
async def _call_local(
|
|
74
|
+
self,
|
|
75
|
+
tool_input: dict[str, Any],
|
|
76
|
+
context: ToolContext,
|
|
77
|
+
) -> ToolResult:
|
|
78
|
+
file_path = tool_input.get("file_path", "")
|
|
79
|
+
content = tool_input.get("content", "")
|
|
80
|
+
|
|
81
|
+
if not file_path:
|
|
82
|
+
return ToolResult(
|
|
83
|
+
result_for_model="Error: file_path is required",
|
|
84
|
+
is_error=True,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
path = Path(file_path)
|
|
88
|
+
if not path.is_absolute():
|
|
89
|
+
path = Path(context.cwd) / path
|
|
90
|
+
|
|
91
|
+
is_new = not path.exists()
|
|
92
|
+
old_content = ""
|
|
93
|
+
newline: str | None = None
|
|
94
|
+
has_bom = False
|
|
95
|
+
if not is_new:
|
|
96
|
+
try:
|
|
97
|
+
source = read_utf8_text(path)
|
|
98
|
+
old_content = source.text
|
|
99
|
+
newline = source.newline
|
|
100
|
+
has_bom = source.has_bom
|
|
101
|
+
except (OSError, UnicodeError) as exc:
|
|
102
|
+
return ToolResult(
|
|
103
|
+
result_for_model=f"Error reading existing file; refusing to overwrite without a valid backup: {exc}",
|
|
104
|
+
is_error=True,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
if newline is not None:
|
|
108
|
+
content = normalize_newlines(content, newline)
|
|
109
|
+
|
|
110
|
+
# Track snapshot before writing
|
|
111
|
+
if context.session_id:
|
|
112
|
+
try:
|
|
113
|
+
from crabcode_core.snapshot.tracker import track_snapshot_for_file
|
|
114
|
+
track_snapshot_for_file(
|
|
115
|
+
cwd=context.cwd,
|
|
116
|
+
session_id=context.session_id,
|
|
117
|
+
file_path=str(path),
|
|
118
|
+
old_content=old_content if not is_new else None,
|
|
119
|
+
action="create" if is_new else "modify",
|
|
120
|
+
)
|
|
121
|
+
except Exception:
|
|
122
|
+
logger.debug("Failed to track snapshot for write", exc_info=True)
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
126
|
+
write_utf8_text(path, content, has_bom=has_bom)
|
|
127
|
+
except Exception as e:
|
|
128
|
+
return ToolResult(
|
|
129
|
+
result_for_model=f"Error writing file: {e}",
|
|
130
|
+
is_error=True,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
if is_new:
|
|
134
|
+
line_count = content.count("\n") + (1 if content and not content.endswith("\n") else 0)
|
|
135
|
+
lsp_msg = await _get_lsp_diagnostics(path, context)
|
|
136
|
+
return ToolResult(
|
|
137
|
+
data={"file_path": str(path), "created": True, "lines": line_count},
|
|
138
|
+
result_for_model=f"Created {path} ({line_count} lines).{lsp_msg}",
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
diff_info = compute_diff(old_content, content, str(path))
|
|
142
|
+
model_msg, display_msg = format_edit_summary(str(path), diff_info)
|
|
143
|
+
lsp_msg = await _get_lsp_diagnostics(path, context)
|
|
144
|
+
|
|
145
|
+
return ToolResult(
|
|
146
|
+
data={
|
|
147
|
+
"file_path": str(path),
|
|
148
|
+
"created": False,
|
|
149
|
+
"line_range": diff_info["line_range"],
|
|
150
|
+
"stats": diff_info["stats"],
|
|
151
|
+
},
|
|
152
|
+
result_for_model=model_msg + lsp_msg,
|
|
153
|
+
result_for_display=display_msg,
|
|
154
|
+
)
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""GlobTool — find files by pattern."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from crabcode_core.tools._input_helpers import first_non_empty_str
|
|
9
|
+
from crabcode_core.types.tool import Tool, ToolContext, ToolResult
|
|
10
|
+
from crabcode_core.io_worker import run_file_tool
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class GlobTool(Tool):
|
|
14
|
+
name = "Glob"
|
|
15
|
+
description = "Find files matching a glob pattern."
|
|
16
|
+
is_read_only = True
|
|
17
|
+
is_concurrency_safe = True
|
|
18
|
+
input_schema = {
|
|
19
|
+
"type": "object",
|
|
20
|
+
"properties": {
|
|
21
|
+
"pattern": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"minLength": 1,
|
|
24
|
+
"description": "Glob pattern to match (e.g., '**/*.py', 'src/**/*.ts').",
|
|
25
|
+
},
|
|
26
|
+
"path": {
|
|
27
|
+
"type": "string",
|
|
28
|
+
"description": "Directory to search in (default: cwd).",
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
"required": ["pattern"],
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async def get_prompt(self, **kwargs: Any) -> str:
|
|
35
|
+
return (
|
|
36
|
+
"Find files matching a glob pattern. Returns matching file paths "
|
|
37
|
+
"sorted by modification time (most recent first). Works fast "
|
|
38
|
+
"with codebases of any size.\n\n"
|
|
39
|
+
"Patterns not starting with '**/' are automatically prepended "
|
|
40
|
+
"with it for recursive searching. Use this instead of find or "
|
|
41
|
+
"ls via Bash.\n\n"
|
|
42
|
+
"Examples:\n"
|
|
43
|
+
'- "*.py" (becomes "**/*.py") — find all Python files\n'
|
|
44
|
+
'- "src/**/*.ts" — find all TypeScript files under src/\n'
|
|
45
|
+
'- "**/test_*.py" — find all test files'
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
async def call(
|
|
49
|
+
self, tool_input: dict[str, Any], context: ToolContext,
|
|
50
|
+
) -> ToolResult:
|
|
51
|
+
result = await run_file_tool(self.name, tool_input, context)
|
|
52
|
+
return result
|
|
53
|
+
|
|
54
|
+
async def _call_local(
|
|
55
|
+
self,
|
|
56
|
+
tool_input: dict[str, Any],
|
|
57
|
+
context: ToolContext,
|
|
58
|
+
) -> ToolResult:
|
|
59
|
+
pattern = first_non_empty_str(
|
|
60
|
+
tool_input,
|
|
61
|
+
("pattern", "glob_pattern", "file_pattern", "glob", "match", "include"),
|
|
62
|
+
)
|
|
63
|
+
search_path = tool_input.get("path", context.cwd)
|
|
64
|
+
|
|
65
|
+
if not pattern:
|
|
66
|
+
return ToolResult(
|
|
67
|
+
result_for_model=(
|
|
68
|
+
"Error: pattern is required. Pass a non-empty glob string "
|
|
69
|
+
"(e.g. \"**/*.py\"); aliases: glob_pattern, file_pattern, glob, "
|
|
70
|
+
"match, include."
|
|
71
|
+
),
|
|
72
|
+
is_error=True,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
if not pattern.startswith("**/") and not pattern.startswith("/"):
|
|
76
|
+
pattern = f"**/{pattern}"
|
|
77
|
+
|
|
78
|
+
root = Path(search_path)
|
|
79
|
+
if not root.is_absolute():
|
|
80
|
+
root = Path(context.cwd) / root
|
|
81
|
+
|
|
82
|
+
if not root.exists():
|
|
83
|
+
return ToolResult(
|
|
84
|
+
result_for_model=f"Error: directory not found: {root}",
|
|
85
|
+
is_error=True,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
matches: list[tuple[float, str]] = []
|
|
90
|
+
for path in root.glob(pattern):
|
|
91
|
+
if path.is_file():
|
|
92
|
+
try:
|
|
93
|
+
mtime = path.stat().st_mtime
|
|
94
|
+
except OSError:
|
|
95
|
+
mtime = 0
|
|
96
|
+
matches.append((mtime, str(path)))
|
|
97
|
+
|
|
98
|
+
if len(matches) >= 1000:
|
|
99
|
+
break
|
|
100
|
+
|
|
101
|
+
matches.sort(key=lambda x: x[0], reverse=True)
|
|
102
|
+
file_paths = [p for _, p in matches]
|
|
103
|
+
|
|
104
|
+
except Exception as e:
|
|
105
|
+
return ToolResult(
|
|
106
|
+
result_for_model=f"Error during glob: {e}",
|
|
107
|
+
is_error=True,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
if not file_paths:
|
|
111
|
+
return ToolResult(
|
|
112
|
+
result_for_model="No files matched the pattern.",
|
|
113
|
+
data={"count": 0},
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
result_text = "\n".join(file_paths)
|
|
117
|
+
count = len(file_paths)
|
|
118
|
+
suffix = " (showing first 1000)" if count >= 1000 else ""
|
|
119
|
+
|
|
120
|
+
return ToolResult(
|
|
121
|
+
data={"count": count, "files": file_paths},
|
|
122
|
+
result_for_model=f"Found {count} files{suffix}:\n{result_text}",
|
|
123
|
+
)
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Model-facing tools for managing the current session goal."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from crabcode_core.types.tool import Tool, ToolContext, ToolResult
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _session(context: ToolContext) -> Any | None:
|
|
11
|
+
return context.session
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _goal_result(goal: Any, message: str) -> ToolResult:
|
|
15
|
+
data = goal.to_dict()
|
|
16
|
+
return ToolResult(data=data, result_for_model=f"{message}\n{data}")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CreateGoalTool(Tool):
|
|
20
|
+
name = "create_goal"
|
|
21
|
+
description = (
|
|
22
|
+
"Create a persistent goal only when the user explicitly requested goal-backed "
|
|
23
|
+
"work. The objective must include a concrete outcome and verification criteria."
|
|
24
|
+
)
|
|
25
|
+
is_read_only = False
|
|
26
|
+
is_concurrency_safe = False
|
|
27
|
+
input_schema = {
|
|
28
|
+
"type": "object",
|
|
29
|
+
"properties": {
|
|
30
|
+
"objective": {
|
|
31
|
+
"type": "string",
|
|
32
|
+
"description": "Concrete objective, scope, and completion evidence.",
|
|
33
|
+
},
|
|
34
|
+
"token_budget": {
|
|
35
|
+
"type": "integer",
|
|
36
|
+
"minimum": 1,
|
|
37
|
+
"description": (
|
|
38
|
+
"Optional token budget. Set it only when the user explicitly "
|
|
39
|
+
"requested a budget."
|
|
40
|
+
),
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
"required": ["objective"],
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async def validate_input(self, tool_input: dict[str, Any]) -> str | None:
|
|
47
|
+
objective = tool_input.get("objective")
|
|
48
|
+
if not isinstance(objective, str) or not objective.strip():
|
|
49
|
+
return "objective is required"
|
|
50
|
+
budget = tool_input.get("token_budget")
|
|
51
|
+
if budget is not None and (not isinstance(budget, int) or budget <= 0):
|
|
52
|
+
return "token_budget must be a positive integer"
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
async def call(
|
|
56
|
+
self, tool_input: dict[str, Any], context: ToolContext
|
|
57
|
+
) -> ToolResult:
|
|
58
|
+
session = _session(context)
|
|
59
|
+
if session is None:
|
|
60
|
+
return ToolResult(
|
|
61
|
+
result_for_model="Goal management is unavailable outside the main session.",
|
|
62
|
+
is_error=True,
|
|
63
|
+
)
|
|
64
|
+
try:
|
|
65
|
+
goal = session.create_goal(
|
|
66
|
+
tool_input["objective"],
|
|
67
|
+
token_budget=tool_input.get("token_budget"),
|
|
68
|
+
)
|
|
69
|
+
except (RuntimeError, ValueError) as exc:
|
|
70
|
+
return ToolResult(result_for_model=str(exc), is_error=True)
|
|
71
|
+
return _goal_result(goal, "Goal created.")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class GetGoalTool(Tool):
|
|
75
|
+
name = "get_goal"
|
|
76
|
+
description = "Get the current persistent goal and its lifecycle state."
|
|
77
|
+
is_read_only = True
|
|
78
|
+
is_concurrency_safe = True
|
|
79
|
+
input_schema = {"type": "object", "properties": {}}
|
|
80
|
+
|
|
81
|
+
async def call(
|
|
82
|
+
self, tool_input: dict[str, Any], context: ToolContext
|
|
83
|
+
) -> ToolResult:
|
|
84
|
+
session = _session(context)
|
|
85
|
+
if session is None:
|
|
86
|
+
return ToolResult(
|
|
87
|
+
result_for_model="Goal management is unavailable outside the main session.",
|
|
88
|
+
is_error=True,
|
|
89
|
+
)
|
|
90
|
+
goal = session.get_goal()
|
|
91
|
+
if goal is None:
|
|
92
|
+
return ToolResult(data=None, result_for_model="No goal is set.")
|
|
93
|
+
return _goal_result(goal, "Current goal.")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class UpdateGoalTool(Tool):
|
|
97
|
+
name = "update_goal"
|
|
98
|
+
description = (
|
|
99
|
+
"Mark the current goal complete after verification, or blocked when it cannot "
|
|
100
|
+
"make further progress without user input or an external state change."
|
|
101
|
+
)
|
|
102
|
+
is_read_only = False
|
|
103
|
+
is_concurrency_safe = False
|
|
104
|
+
input_schema = {
|
|
105
|
+
"type": "object",
|
|
106
|
+
"properties": {
|
|
107
|
+
"status": {
|
|
108
|
+
"type": "string",
|
|
109
|
+
"enum": ["complete", "blocked"],
|
|
110
|
+
"description": "The terminal state to apply.",
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
"required": ["status"],
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async def validate_input(self, tool_input: dict[str, Any]) -> str | None:
|
|
117
|
+
if tool_input.get("status") not in {"complete", "blocked"}:
|
|
118
|
+
return "status must be 'complete' or 'blocked'"
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
async def call(
|
|
122
|
+
self, tool_input: dict[str, Any], context: ToolContext
|
|
123
|
+
) -> ToolResult:
|
|
124
|
+
session = _session(context)
|
|
125
|
+
if session is None:
|
|
126
|
+
return ToolResult(
|
|
127
|
+
result_for_model="Goal management is unavailable outside the main session.",
|
|
128
|
+
is_error=True,
|
|
129
|
+
)
|
|
130
|
+
try:
|
|
131
|
+
goal = session.update_goal(tool_input["status"])
|
|
132
|
+
except (RuntimeError, ValueError) as exc:
|
|
133
|
+
return ToolResult(result_for_model=str(exc), is_error=True)
|
|
134
|
+
return _goal_result(goal, f"Goal marked {goal.status}.")
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""GrepTool — search file contents with portable fallbacks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import shutil
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from crabcode_core.subprocess_utils import (
|
|
13
|
+
managed_process_command,
|
|
14
|
+
resolve_executable_command,
|
|
15
|
+
)
|
|
16
|
+
from crabcode_core.tools._input_helpers import first_non_empty_str
|
|
17
|
+
from crabcode_core.types.tool import Tool, ToolContext, ToolResult
|
|
18
|
+
from crabcode_core.io_worker import run_file_tool
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
_SKIP_DIRECTORIES = {
|
|
22
|
+
".git",
|
|
23
|
+
".hg",
|
|
24
|
+
".svn",
|
|
25
|
+
".venv",
|
|
26
|
+
"__pycache__",
|
|
27
|
+
"dist",
|
|
28
|
+
"node_modules",
|
|
29
|
+
"target",
|
|
30
|
+
"venv",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _python_search(
|
|
35
|
+
pattern: str,
|
|
36
|
+
search_path: str,
|
|
37
|
+
*,
|
|
38
|
+
cwd: str,
|
|
39
|
+
glob_pattern: str | None,
|
|
40
|
+
case_insensitive: bool,
|
|
41
|
+
) -> str:
|
|
42
|
+
"""Pure-Python fallback for stock Windows installations."""
|
|
43
|
+
flags = re.IGNORECASE if case_insensitive else 0
|
|
44
|
+
expression = re.compile(pattern, flags)
|
|
45
|
+
target = Path(search_path)
|
|
46
|
+
if not target.is_absolute():
|
|
47
|
+
target = Path(cwd) / target
|
|
48
|
+
target = target.resolve()
|
|
49
|
+
if not target.exists():
|
|
50
|
+
raise FileNotFoundError(f"search path does not exist: {search_path}")
|
|
51
|
+
|
|
52
|
+
if target.is_file():
|
|
53
|
+
candidates = [target]
|
|
54
|
+
else:
|
|
55
|
+
candidates = []
|
|
56
|
+
for root, directories, files in os.walk(target):
|
|
57
|
+
directories[:] = [
|
|
58
|
+
name for name in directories if name not in _SKIP_DIRECTORIES
|
|
59
|
+
]
|
|
60
|
+
root_path = Path(root)
|
|
61
|
+
for name in files:
|
|
62
|
+
candidate = root_path / name
|
|
63
|
+
if glob_pattern and not candidate.match(glob_pattern):
|
|
64
|
+
continue
|
|
65
|
+
candidates.append(candidate)
|
|
66
|
+
|
|
67
|
+
matches: list[str] = []
|
|
68
|
+
for candidate in candidates:
|
|
69
|
+
try:
|
|
70
|
+
with open(candidate, "rb") as probe:
|
|
71
|
+
if b"\0" in probe.read(8192):
|
|
72
|
+
continue
|
|
73
|
+
text = candidate.read_text(encoding="utf-8", errors="replace")
|
|
74
|
+
except OSError:
|
|
75
|
+
continue
|
|
76
|
+
try:
|
|
77
|
+
display = str(candidate.relative_to(Path(cwd).resolve()))
|
|
78
|
+
except ValueError:
|
|
79
|
+
display = str(candidate)
|
|
80
|
+
for line_number, line in enumerate(text.splitlines(), start=1):
|
|
81
|
+
if expression.search(line):
|
|
82
|
+
matches.append(f"{display}:{line_number}:{line}")
|
|
83
|
+
if len(matches) >= 200:
|
|
84
|
+
return "\n".join(matches)
|
|
85
|
+
return "\n".join(matches)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class GrepTool(Tool):
|
|
89
|
+
name = "Grep"
|
|
90
|
+
description = "Search file contents using regex patterns."
|
|
91
|
+
is_read_only = True
|
|
92
|
+
is_concurrency_safe = True
|
|
93
|
+
input_schema = {
|
|
94
|
+
"type": "object",
|
|
95
|
+
"properties": {
|
|
96
|
+
"pattern": {
|
|
97
|
+
"type": "string",
|
|
98
|
+
"minLength": 1,
|
|
99
|
+
"description": "The regex pattern to search for.",
|
|
100
|
+
},
|
|
101
|
+
"path": {
|
|
102
|
+
"type": "string",
|
|
103
|
+
"description": (
|
|
104
|
+
"Directory or file to search in (default: cwd). "
|
|
105
|
+
"Directories are searched recursively. "
|
|
106
|
+
"Use the glob parameter to filter file types when searching a directory."
|
|
107
|
+
),
|
|
108
|
+
},
|
|
109
|
+
"glob": {
|
|
110
|
+
"type": "string",
|
|
111
|
+
"description": "File glob pattern to filter (e.g., '*.py').",
|
|
112
|
+
},
|
|
113
|
+
"case_insensitive": {
|
|
114
|
+
"type": "boolean",
|
|
115
|
+
"description": "Case insensitive search.",
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
"required": ["pattern"],
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async def get_prompt(self, **kwargs: Any) -> str:
|
|
122
|
+
return (
|
|
123
|
+
"Search file contents for a regex pattern using ripgrep, system grep, or a built-in fallback. "
|
|
124
|
+
"Returns matching lines with file paths and line numbers. "
|
|
125
|
+
"Use this instead of running grep or rg via Bash.\n\n"
|
|
126
|
+
"Supports full regex syntax (e.g., 'log.*Error', "
|
|
127
|
+
"'function\\s+\\w+'). Filter files with the glob parameter "
|
|
128
|
+
"(e.g., '*.py', '*.tsx').\n\n"
|
|
129
|
+
"Results are capped at 200 matches for responsiveness. "
|
|
130
|
+
"If you need more specific results, narrow your search "
|
|
131
|
+
"with a more specific pattern or glob filter."
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
async def call(
|
|
135
|
+
self, tool_input: dict[str, Any], context: ToolContext,
|
|
136
|
+
) -> ToolResult:
|
|
137
|
+
result = await run_file_tool(self.name, tool_input, context)
|
|
138
|
+
return result
|
|
139
|
+
|
|
140
|
+
async def _call_local(
|
|
141
|
+
self,
|
|
142
|
+
tool_input: dict[str, Any],
|
|
143
|
+
context: ToolContext,
|
|
144
|
+
) -> ToolResult:
|
|
145
|
+
pattern = first_non_empty_str(
|
|
146
|
+
tool_input,
|
|
147
|
+
("pattern", "regex", "regexp", "search", "query"),
|
|
148
|
+
)
|
|
149
|
+
search_path = tool_input.get("path", context.cwd)
|
|
150
|
+
glob_pattern = tool_input.get("glob")
|
|
151
|
+
case_insensitive = tool_input.get("case_insensitive", False)
|
|
152
|
+
|
|
153
|
+
if not pattern:
|
|
154
|
+
return ToolResult(
|
|
155
|
+
result_for_model=(
|
|
156
|
+
"Error: pattern is required. Pass a regex string; aliases: "
|
|
157
|
+
"regex, regexp, search, query."
|
|
158
|
+
),
|
|
159
|
+
is_error=True,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
rg = shutil.which("rg")
|
|
163
|
+
use_ripgrep = bool(rg)
|
|
164
|
+
grep_bin = shutil.which("grep")
|
|
165
|
+
|
|
166
|
+
if not use_ripgrep and not grep_bin:
|
|
167
|
+
try:
|
|
168
|
+
stdout = _python_search(
|
|
169
|
+
pattern, str(search_path), cwd=context.cwd,
|
|
170
|
+
glob_pattern=glob_pattern,
|
|
171
|
+
case_insensitive=bool(case_insensitive),
|
|
172
|
+
)
|
|
173
|
+
except (FileNotFoundError, re.error) as exc:
|
|
174
|
+
return ToolResult(result_for_model=f"Grep error: {exc}", is_error=True)
|
|
175
|
+
lines = stdout.splitlines() if stdout else []
|
|
176
|
+
if len(stdout) > 50_000:
|
|
177
|
+
stdout = stdout[:50_000] + "\n... (truncated)"
|
|
178
|
+
return ToolResult(
|
|
179
|
+
data={"match_count": len(lines)},
|
|
180
|
+
result_for_model=stdout or "No matches found.",
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
if use_ripgrep:
|
|
184
|
+
args = [rg, "--line-number", "--no-heading", "--color=never"]
|
|
185
|
+
if case_insensitive:
|
|
186
|
+
args.append("-i")
|
|
187
|
+
if glob_pattern:
|
|
188
|
+
args.extend(["--glob", glob_pattern])
|
|
189
|
+
args.extend(["--max-count", "200"])
|
|
190
|
+
args.append(pattern)
|
|
191
|
+
args.append("--")
|
|
192
|
+
args.append(search_path)
|
|
193
|
+
else:
|
|
194
|
+
# Fallback to system grep
|
|
195
|
+
assert grep_bin is not None
|
|
196
|
+
args = [grep_bin, "-rn", "--color=never", "-E"]
|
|
197
|
+
if case_insensitive:
|
|
198
|
+
args.append("-i")
|
|
199
|
+
if glob_pattern:
|
|
200
|
+
args.extend(["--include", glob_pattern])
|
|
201
|
+
args.extend(["-m", "200"])
|
|
202
|
+
args.append(pattern)
|
|
203
|
+
args.append(search_path)
|
|
204
|
+
|
|
205
|
+
proc: asyncio.subprocess.Process | None = None
|
|
206
|
+
try:
|
|
207
|
+
args = resolve_executable_command(args, cwd=context.cwd)
|
|
208
|
+
proc = await asyncio.create_subprocess_exec(
|
|
209
|
+
*managed_process_command(args),
|
|
210
|
+
stdout=asyncio.subprocess.PIPE,
|
|
211
|
+
stderr=asyncio.subprocess.PIPE,
|
|
212
|
+
cwd=context.cwd,
|
|
213
|
+
)
|
|
214
|
+
stdout_bytes, stderr_bytes = await proc.communicate()
|
|
215
|
+
except FileNotFoundError:
|
|
216
|
+
return ToolResult(
|
|
217
|
+
result_for_model="Error: neither ripgrep (rg) nor grep found on PATH.",
|
|
218
|
+
is_error=True,
|
|
219
|
+
)
|
|
220
|
+
except ValueError as exc:
|
|
221
|
+
return ToolResult(result_for_model=f"Search error: {exc}", is_error=True)
|
|
222
|
+
|
|
223
|
+
stdout = stdout_bytes.decode("utf-8", errors="replace")
|
|
224
|
+
stderr = stderr_bytes.decode("utf-8", errors="replace")
|
|
225
|
+
|
|
226
|
+
if proc.returncode == 1:
|
|
227
|
+
return ToolResult(
|
|
228
|
+
result_for_model="No matches found.",
|
|
229
|
+
data={"matches": 0},
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
if proc.returncode and proc.returncode > 1:
|
|
233
|
+
return ToolResult(
|
|
234
|
+
result_for_model=f"Grep error: {stderr or 'unknown error'}",
|
|
235
|
+
is_error=True,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
lines = stdout.strip().split("\n") if stdout.strip() else []
|
|
239
|
+
|
|
240
|
+
max_output = 50_000
|
|
241
|
+
if len(stdout) > max_output:
|
|
242
|
+
stdout = stdout[:max_output] + "\n... (truncated)"
|
|
243
|
+
|
|
244
|
+
return ToolResult(
|
|
245
|
+
data={"match_count": len(lines)},
|
|
246
|
+
result_for_model=stdout or "No matches found.",
|
|
247
|
+
)
|