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.
Files changed (205) hide show
  1. crabcode-0.1.6.dist-info/METADATA +63 -0
  2. crabcode-0.1.6.dist-info/RECORD +205 -0
  3. crabcode-0.1.6.dist-info/WHEEL +4 -0
  4. crabcode-0.1.6.dist-info/entry_points.txt +2 -0
  5. crabcode_cli/__init__.py +7 -0
  6. crabcode_cli/__main__.py +4 -0
  7. crabcode_cli/_native/crabcode-computer-use-host.exe +0 -0
  8. crabcode_cli/_native/runtime.json +6 -0
  9. crabcode_cli/app.py +776 -0
  10. crabcode_cli/banner.py +39 -0
  11. crabcode_cli/computer_use.py +286 -0
  12. crabcode_cli/model_manager.py +649 -0
  13. crabcode_cli/model_picker.py +168 -0
  14. crabcode_cli/pipe.py +148 -0
  15. crabcode_cli/render/__init__.py +0 -0
  16. crabcode_cli/render/messages.py +19 -0
  17. crabcode_cli/repl.py +4841 -0
  18. crabcode_cli/session_picker.py +239 -0
  19. crabcode_core/__init__.py +7 -0
  20. crabcode_core/_version.py +3 -0
  21. crabcode_core/_windows_process.py +68 -0
  22. crabcode_core/agent_manager.py +1731 -0
  23. crabcode_core/api/__init__.py +20 -0
  24. crabcode_core/api/anthropic_adapter.py +648 -0
  25. crabcode_core/api/azure_adapter.py +69 -0
  26. crabcode_core/api/base.py +233 -0
  27. crabcode_core/api/codex_adapter.py +1327 -0
  28. crabcode_core/api/error_logging.py +132 -0
  29. crabcode_core/api/gemini_adapter.py +26 -0
  30. crabcode_core/api/model_info.py +257 -0
  31. crabcode_core/api/network.py +62 -0
  32. crabcode_core/api/ollama_adapter.py +27 -0
  33. crabcode_core/api/openai_adapter.py +323 -0
  34. crabcode_core/api/registry.py +58 -0
  35. crabcode_core/command_line.py +18 -0
  36. crabcode_core/compact/__init__.py +17 -0
  37. crabcode_core/compact/compact.py +514 -0
  38. crabcode_core/compact/context_tokens.py +165 -0
  39. crabcode_core/computer_use_mode.py +38 -0
  40. crabcode_core/computer_use_observation.py +144 -0
  41. crabcode_core/config/__init__.py +3 -0
  42. crabcode_core/config/claudemd.py +61 -0
  43. crabcode_core/config/manager.py +237 -0
  44. crabcode_core/config/model_catalog.py +256 -0
  45. crabcode_core/events.py +4896 -0
  46. crabcode_core/file_lock.py +81 -0
  47. crabcode_core/filesystem.py +22 -0
  48. crabcode_core/goal.py +149 -0
  49. crabcode_core/hooks/__init__.py +5 -0
  50. crabcode_core/hooks/manager.py +290 -0
  51. crabcode_core/image_io.py +111 -0
  52. crabcode_core/io_worker.py +211 -0
  53. crabcode_core/logging_utils.py +168 -0
  54. crabcode_core/lsp/__init__.py +6 -0
  55. crabcode_core/lsp/client.py +786 -0
  56. crabcode_core/lsp/diagnostics.py +150 -0
  57. crabcode_core/lsp/manager.py +583 -0
  58. crabcode_core/mcp/__init__.py +4 -0
  59. crabcode_core/mcp/client.py +169 -0
  60. crabcode_core/mcp/config.py +57 -0
  61. crabcode_core/path_validation.py +34 -0
  62. crabcode_core/paths.py +24 -0
  63. crabcode_core/peer/__init__.py +10 -0
  64. crabcode_core/peer/runtime.py +518 -0
  65. crabcode_core/permissions/__init__.py +7 -0
  66. crabcode_core/permissions/ai_reviewer.py +184 -0
  67. crabcode_core/permissions/manager.py +187 -0
  68. crabcode_core/plan/__init__.py +5 -0
  69. crabcode_core/plan/executor.py +270 -0
  70. crabcode_core/plan/types.py +169 -0
  71. crabcode_core/prompts/__init__.py +0 -0
  72. crabcode_core/prompts/blocks.py +10 -0
  73. crabcode_core/prompts/context.py +229 -0
  74. crabcode_core/prompts/library.py +92 -0
  75. crabcode_core/prompts/profile.py +79 -0
  76. crabcode_core/prompts/system.py +356 -0
  77. crabcode_core/prompts/templates.py +104 -0
  78. crabcode_core/query/__init__.py +0 -0
  79. crabcode_core/query/computer_use_history.py +83 -0
  80. crabcode_core/query/loop.py +2446 -0
  81. crabcode_core/query/retry.py +116 -0
  82. crabcode_core/schedule/__init__.py +13 -0
  83. crabcode_core/schedule/manager.py +631 -0
  84. crabcode_core/schedule/models.py +204 -0
  85. crabcode_core/schedule/store.py +558 -0
  86. crabcode_core/schedule/timing.py +287 -0
  87. crabcode_core/session/__init__.py +12 -0
  88. crabcode_core/session/export.py +141 -0
  89. crabcode_core/session/meta_db.py +684 -0
  90. crabcode_core/session/storage.py +2032 -0
  91. crabcode_core/session/title_gen.py +52 -0
  92. crabcode_core/skills/__init__.py +6 -0
  93. crabcode_core/skills/context.py +23 -0
  94. crabcode_core/skills/loader.py +214 -0
  95. crabcode_core/skills/matcher.py +203 -0
  96. crabcode_core/snapshot/__init__.py +21 -0
  97. crabcode_core/snapshot/snapshot.py +436 -0
  98. crabcode_core/snapshot/tracker.py +359 -0
  99. crabcode_core/subprocess_utils.py +260 -0
  100. crabcode_core/team/__init__.py +27 -0
  101. crabcode_core/team/bridge.py +66 -0
  102. crabcode_core/team/inbox.py +237 -0
  103. crabcode_core/team/manager.py +1660 -0
  104. crabcode_core/team/message_bus.py +821 -0
  105. crabcode_core/team/models.py +191 -0
  106. crabcode_core/team/recovery.py +192 -0
  107. crabcode_core/text_io.py +52 -0
  108. crabcode_core/tools/__init__.py +112 -0
  109. crabcode_core/tools/_input_helpers.py +39 -0
  110. crabcode_core/tools/_input_schema.py +130 -0
  111. crabcode_core/tools/agent.py +413 -0
  112. crabcode_core/tools/apply_patch.py +587 -0
  113. crabcode_core/tools/ask_user.py +122 -0
  114. crabcode_core/tools/bash.py +154 -0
  115. crabcode_core/tools/browser.py +678 -0
  116. crabcode_core/tools/checklist.py +241 -0
  117. crabcode_core/tools/checkpoint.py +78 -0
  118. crabcode_core/tools/computer_use.py +599 -0
  119. crabcode_core/tools/diff_utils.py +123 -0
  120. crabcode_core/tools/file_edit.py +240 -0
  121. crabcode_core/tools/file_read.py +187 -0
  122. crabcode_core/tools/file_write.py +154 -0
  123. crabcode_core/tools/glob.py +123 -0
  124. crabcode_core/tools/goal.py +134 -0
  125. crabcode_core/tools/grep.py +247 -0
  126. crabcode_core/tools/image.py +173 -0
  127. crabcode_core/tools/image_generate.py +115 -0
  128. crabcode_core/tools/lint.py +388 -0
  129. crabcode_core/tools/loading.py +187 -0
  130. crabcode_core/tools/memory.py +456 -0
  131. crabcode_core/tools/monitor.py +677 -0
  132. crabcode_core/tools/peer.py +103 -0
  133. crabcode_core/tools/revert.py +90 -0
  134. crabcode_core/tools/schedule.py +683 -0
  135. crabcode_core/tools/skill.py +108 -0
  136. crabcode_core/tools/switch_mode.py +213 -0
  137. crabcode_core/tools/team.py +329 -0
  138. crabcode_core/tools/tool_search.py +64 -0
  139. crabcode_core/tools/web_search.py +444 -0
  140. crabcode_core/turn_details.py +64 -0
  141. crabcode_core/types/__init__.py +95 -0
  142. crabcode_core/types/config.py +434 -0
  143. crabcode_core/types/event.py +334 -0
  144. crabcode_core/types/message.py +263 -0
  145. crabcode_core/types/tool.py +208 -0
  146. crabcode_core/usage.py +289 -0
  147. crabcode_core/utf8_sanitize.py +29 -0
  148. crabcode_debugger/__init__.py +5 -0
  149. crabcode_debugger/adapters.py +332 -0
  150. crabcode_debugger/dap.py +304 -0
  151. crabcode_debugger/memory.py +1806 -0
  152. crabcode_debugger/process.py +707 -0
  153. crabcode_debugger/runtime.py +58 -0
  154. crabcode_debugger/sessions.py +387 -0
  155. crabcode_debugger/tool.py +731 -0
  156. crabcode_gateway/__init__.py +18 -0
  157. crabcode_gateway/acp/__init__.py +13 -0
  158. crabcode_gateway/acp/agent.py +1581 -0
  159. crabcode_gateway/acp/session.py +114 -0
  160. crabcode_gateway/acp/transport.py +39 -0
  161. crabcode_gateway/acp/types.py +90 -0
  162. crabcode_gateway/adapter.py +30 -0
  163. crabcode_gateway/auth.py +139 -0
  164. crabcode_gateway/computer_use.py +364 -0
  165. crabcode_gateway/document_engine.py +637 -0
  166. crabcode_gateway/document_worker.py +301 -0
  167. crabcode_gateway/event_bus.py +655 -0
  168. crabcode_gateway/grpc/__init__.py +0 -0
  169. crabcode_gateway/grpc/crabcode_pb2.py +70 -0
  170. crabcode_gateway/grpc/crabcode_pb2_grpc.py +474 -0
  171. crabcode_gateway/grpc/proto/crabcode.proto +160 -0
  172. crabcode_gateway/grpc/server.py +765 -0
  173. crabcode_gateway/middleware.py +344 -0
  174. crabcode_gateway/protocol.py +17 -0
  175. crabcode_gateway/routes/__init__.py +0 -0
  176. crabcode_gateway/routes/agent.py +392 -0
  177. crabcode_gateway/routes/auth.py +150 -0
  178. crabcode_gateway/routes/computer_use.py +101 -0
  179. crabcode_gateway/routes/config.py +1698 -0
  180. crabcode_gateway/routes/document.py +1810 -0
  181. crabcode_gateway/routes/event.py +3801 -0
  182. crabcode_gateway/routes/git_review.py +232 -0
  183. crabcode_gateway/routes/health.py +14 -0
  184. crabcode_gateway/routes/peer.py +78 -0
  185. crabcode_gateway/routes/permission.py +121 -0
  186. crabcode_gateway/routes/schedule.py +300 -0
  187. crabcode_gateway/routes/session.py +1496 -0
  188. crabcode_gateway/routes/snapshot.py +211 -0
  189. crabcode_gateway/routes/tasks.py +318 -0
  190. crabcode_gateway/routes/team.py +392 -0
  191. crabcode_gateway/routes/usage.py +49 -0
  192. crabcode_gateway/routes/workspace.py +299 -0
  193. crabcode_gateway/safe_file.py +63 -0
  194. crabcode_gateway/schemas.py +1820 -0
  195. crabcode_gateway/server.py +514 -0
  196. crabcode_gateway/session_registry.py +36 -0
  197. crabcode_gateway/task_registry.py +708 -0
  198. crabcode_gateway/turn_details.py +60 -0
  199. crabcode_search/__init__.py +11 -0
  200. crabcode_search/background.py +368 -0
  201. crabcode_search/chunker.py +517 -0
  202. crabcode_search/embedder.py +497 -0
  203. crabcode_search/indexer.py +215 -0
  204. crabcode_search/store.py +222 -0
  205. 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
+ )