clink-cli 0.2.1__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 (141) hide show
  1. clink/__init__.py +19 -0
  2. clink/agents/__init__.py +30 -0
  3. clink/agents/base.py +70 -0
  4. clink/agents/claude.py +7 -0
  5. clink/agents/codex.py +7 -0
  6. clink/agents/gemini.py +7 -0
  7. clink/allowed_roots.py +356 -0
  8. clink/backend.py +765 -0
  9. clink/cli.py +594 -0
  10. clink/constants.py +78 -0
  11. clink/contracts.py +167 -0
  12. clink/files.py +70 -0
  13. clink/generation.py +274 -0
  14. clink/generation_capabilities.py +207 -0
  15. clink/generation_contracts.py +214 -0
  16. clink/generation_policy.py +174 -0
  17. clink/models.py +243 -0
  18. clink/native.py +409 -0
  19. clink/native_adapters.py +244 -0
  20. clink/native_contracts.py +68 -0
  21. clink/native_generation.py +421 -0
  22. clink/native_generation_claude_policy.py +59 -0
  23. clink/native_generation_contracts.py +264 -0
  24. clink/native_generation_families.py +218 -0
  25. clink/native_generation_family_policy.py +14 -0
  26. clink/native_generation_files.py +153 -0
  27. clink/native_generation_policy.py +82 -0
  28. clink/native_policy.py +86 -0
  29. clink/parsers/__init__.py +34 -0
  30. clink/parsers/agy.py +53 -0
  31. clink/parsers/agy_native.py +161 -0
  32. clink/parsers/agy_native_generation.py +189 -0
  33. clink/parsers/base.py +209 -0
  34. clink/parsers/claude.py +104 -0
  35. clink/parsers/claude_native.py +113 -0
  36. clink/parsers/claude_native_generation.py +346 -0
  37. clink/parsers/codex.py +172 -0
  38. clink/parsers/codex_generation.py +212 -0
  39. clink/parsers/codex_native.py +236 -0
  40. clink/parsers/codex_native_generation.py +367 -0
  41. clink/parsers/gemini.py +110 -0
  42. clink/parsers/grok.py +63 -0
  43. clink/parsers/grok_native.py +91 -0
  44. clink/parsers/grok_native_generation.py +157 -0
  45. clink/policy.py +180 -0
  46. clink/registry.py +544 -0
  47. clink/resources/CODEX-LICENSE +201 -0
  48. clink/resources/CODEX-NOTICE +6 -0
  49. clink/resources/PROVENANCE.md +16 -0
  50. clink/resources/codex-0.166.0-models-direct.json +1284 -0
  51. clink/resources/codex-0.166.0-models-generation.json +1269 -0
  52. clink/resources/native-agy-1.4.12-admission.json +746 -0
  53. clink/resources/native-claude-2.1.304-admission.json +99 -0
  54. clink/resources/native-generation-claude-2.1.304-admission.json +277 -0
  55. clink/resources/native-generation-codex-0.166.0-admission.json +309 -0
  56. clink/resources/native-generation-grok-1.0.70-admission.json +329 -0
  57. clink/resources/native-grok-1.0.70-admission.json +644 -0
  58. clink/runner.py +303 -0
  59. clink/setup.py +399 -0
  60. clink/tasks.py +695 -0
  61. clink_cli-0.2.1.dist-info/METADATA +225 -0
  62. clink_cli-0.2.1.dist-info/RECORD +141 -0
  63. clink_cli-0.2.1.dist-info/WHEEL +5 -0
  64. clink_cli-0.2.1.dist-info/entry_points.txt +3 -0
  65. clink_cli-0.2.1.dist-info/licenses/LICENSE +197 -0
  66. clink_cli-0.2.1.dist-info/licenses/clink/resources/CODEX-LICENSE +201 -0
  67. clink_cli-0.2.1.dist-info/licenses/clink/resources/CODEX-NOTICE +6 -0
  68. clink_cli-0.2.1.dist-info/top_level.txt +7 -0
  69. conf/__init__.py +1 -0
  70. conf/cli_clients/agy.json +40 -0
  71. conf/cli_clients/claude.json +39 -0
  72. conf/cli_clients/codex.json +40 -0
  73. conf/cli_clients/gemini.json +23 -0
  74. conf/cli_clients/grok.json +40 -0
  75. config.py +40 -0
  76. server.py +896 -0
  77. systemprompts/__init__.py +35 -0
  78. systemprompts/analyze_prompt.py +87 -0
  79. systemprompts/chat_prompt.py +57 -0
  80. systemprompts/clink/codex_codereviewer.txt +8 -0
  81. systemprompts/clink/default.txt +8 -0
  82. systemprompts/clink/default_codereviewer.txt +8 -0
  83. systemprompts/clink/default_planner.txt +7 -0
  84. systemprompts/clink/native_codereviewer.txt +1 -0
  85. systemprompts/clink/native_default.txt +1 -0
  86. systemprompts/clink/native_planner.txt +1 -0
  87. systemprompts/codereview_prompt.py +91 -0
  88. systemprompts/consensus_prompt.py +120 -0
  89. systemprompts/debug_prompt.py +150 -0
  90. systemprompts/docgen_prompt.py +252 -0
  91. systemprompts/generate_code_prompt.py +181 -0
  92. systemprompts/planner_prompt.py +127 -0
  93. systemprompts/precommit_prompt.py +179 -0
  94. systemprompts/refactor_prompt.py +323 -0
  95. systemprompts/secaudit_prompt.py +420 -0
  96. systemprompts/testgen_prompt.py +133 -0
  97. systemprompts/thinkdeep_prompt.py +55 -0
  98. systemprompts/tracer_prompt.py +145 -0
  99. tools/__init__.py +43 -0
  100. tools/analyze.py +583 -0
  101. tools/apilookup.py +97 -0
  102. tools/challenge.py +198 -0
  103. tools/chat.py +187 -0
  104. tools/clink.py +130 -0
  105. tools/codereview.py +724 -0
  106. tools/consensus.py +790 -0
  107. tools/debug.py +573 -0
  108. tools/docgen.py +577 -0
  109. tools/listmodels.py +48 -0
  110. tools/models.py +373 -0
  111. tools/planner.py +533 -0
  112. tools/precommit.py +776 -0
  113. tools/refactor.py +645 -0
  114. tools/secaudit.py +685 -0
  115. tools/shared/__init__.py +19 -0
  116. tools/shared/base_models.py +167 -0
  117. tools/shared/base_tool.py +659 -0
  118. tools/shared/exceptions.py +20 -0
  119. tools/shared/schema_builders.py +159 -0
  120. tools/simple/__init__.py +18 -0
  121. tools/simple/base.py +539 -0
  122. tools/testgen.py +513 -0
  123. tools/thinkdeep.py +586 -0
  124. tools/tracer.py +763 -0
  125. tools/version.py +53 -0
  126. tools/workflow/__init__.py +22 -0
  127. tools/workflow/base.py +448 -0
  128. tools/workflow/schema_builders.py +166 -0
  129. tools/workflow/workflow_mixin.py +1195 -0
  130. utils/__init__.py +21 -0
  131. utils/call_state.py +349 -0
  132. utils/client_info.py +293 -0
  133. utils/consensus_manifest.py +131 -0
  134. utils/conversation_memory.py +582 -0
  135. utils/env.py +111 -0
  136. utils/file_types.py +271 -0
  137. utils/file_utils.py +803 -0
  138. utils/image_utils.py +94 -0
  139. utils/security_config.py +163 -0
  140. utils/storage_backend.py +177 -0
  141. utils/token_utils.py +54 -0
tools/simple/base.py ADDED
@@ -0,0 +1,539 @@
1
+ """
2
+ Base class for simple MCP tools.
3
+
4
+ Simple tools follow a straightforward pattern:
5
+ 1. Receive request
6
+ 2. Prepare prompt (with absolute file paths, context, etc.)
7
+ 3. Call AI model
8
+ 4. Format and return response
9
+
10
+ They use the shared SchemaBuilder for consistent schema generation
11
+ and inherit all the conversation, file processing, and model handling
12
+ capabilities from BaseTool.
13
+ """
14
+
15
+ import logging
16
+ from abc import abstractmethod
17
+ from typing import Any, Optional
18
+
19
+ from tools.shared.base_models import ToolRequest
20
+ from tools.shared.base_tool import BaseTool
21
+ from tools.shared.exceptions import ToolExecutionError
22
+ from tools.shared.schema_builders import SchemaBuilder
23
+
24
+
25
+ class SimpleTool(BaseTool):
26
+ """
27
+ Base class for simple (non-workflow) tools.
28
+
29
+ Simple tools are request/response tools that don't require multi-step workflows.
30
+ They benefit from:
31
+ - Automatic schema generation using SchemaBuilder
32
+ - Inherited conversation handling and file processing
33
+ - Standardized model integration
34
+ - Consistent error handling and response formatting
35
+
36
+ To create a simple tool:
37
+ 1. Inherit from SimpleTool
38
+ 2. Implement get_tool_fields() to define tool-specific fields
39
+ 3. Implement prepare_prompt() for prompt preparation
40
+ 4. Optionally override format_response() for custom formatting
41
+ 5. Optionally override get_required_fields() for custom requirements
42
+
43
+ Example:
44
+ class ChatTool(SimpleTool):
45
+ def get_name(self) -> str:
46
+ return "chat"
47
+
48
+ def get_tool_fields(self) -> Dict[str, Dict[str, Any]]:
49
+ return {
50
+ "prompt": {
51
+ "type": "string",
52
+ "description": "Your question or idea...",
53
+ },
54
+ "absolute_file_paths": SimpleTool.FILES_FIELD,
55
+ }
56
+
57
+ def get_required_fields(self) -> List[str]:
58
+ return ["prompt"]
59
+ """
60
+
61
+ # Common field definitions that simple tools can reuse
62
+ FILES_FIELD = SchemaBuilder.SIMPLE_FIELD_SCHEMAS["absolute_file_paths"]
63
+ IMAGES_FIELD = SchemaBuilder.COMMON_FIELD_SCHEMAS["images"]
64
+
65
+ @abstractmethod
66
+ def get_tool_fields(self) -> dict[str, dict[str, Any]]:
67
+ """
68
+ Return tool-specific field definitions.
69
+
70
+ This method should return a dictionary mapping field names to their
71
+ JSON schema definitions. Common fields (model, temperature, etc.)
72
+ are added automatically by the base class.
73
+
74
+ Returns:
75
+ Dict mapping field names to JSON schema objects
76
+
77
+ Example:
78
+ return {
79
+ "prompt": {
80
+ "type": "string",
81
+ "description": "The user's question or request",
82
+ },
83
+ "absolute_file_paths": SimpleTool.FILES_FIELD, # Reuse common field
84
+ "max_tokens": {
85
+ "type": "integer",
86
+ "minimum": 1,
87
+ "description": "Maximum tokens for response",
88
+ }
89
+ }
90
+ """
91
+ pass
92
+
93
+ def get_required_fields(self) -> list[str]:
94
+ """
95
+ Return list of required field names.
96
+
97
+ Override this to specify which fields are required for your tool.
98
+ The model field is automatically added if in auto mode.
99
+
100
+ Returns:
101
+ List of required field names
102
+ """
103
+ return []
104
+
105
+ def get_annotations(self) -> Optional[dict[str, Any]]:
106
+ """
107
+ Return tool annotations. Simple tools are read-only by default.
108
+
109
+ All simple tools perform operations without modifying the environment.
110
+ They may call external AI models for analysis or conversation, but they
111
+ don't write files or make system changes.
112
+
113
+ Override this method if your simple tool needs different annotations.
114
+
115
+ Returns:
116
+ Dictionary with readOnlyHint set to True
117
+ """
118
+ return {"readOnlyHint": True}
119
+
120
+ def format_response(self, response: str, request, model_info: Optional[dict] = None) -> str:
121
+ """
122
+ Format the AI response before returning to the client.
123
+
124
+ This is a hook method that subclasses can override to customize
125
+ response formatting. The default implementation returns the response as-is.
126
+
127
+ Args:
128
+ response: The raw response from the AI model
129
+ request: The validated request object
130
+ model_info: Optional model information dictionary
131
+
132
+ Returns:
133
+ Formatted response string
134
+ """
135
+ return response
136
+
137
+ def get_input_schema(self) -> dict[str, Any]:
138
+ """
139
+ Generate the complete input schema using SchemaBuilder.
140
+
141
+ This method automatically combines:
142
+ - Tool-specific fields from get_tool_fields()
143
+ - Common fields (temperature, thinking_mode, etc.)
144
+ - Model field with proper auto-mode handling
145
+ - Required fields from get_required_fields()
146
+
147
+ Tools can override this method for custom schema generation while
148
+ still benefiting from SimpleTool's convenience methods.
149
+
150
+ Returns:
151
+ Complete JSON schema for the tool
152
+ """
153
+ required_fields = list(self.get_required_fields())
154
+ return SchemaBuilder.build_schema(
155
+ tool_specific_fields=self.get_tool_fields(),
156
+ required_fields=required_fields,
157
+ model_field_schema=self.get_model_field_schema(),
158
+ auto_mode=self.is_effective_auto_mode(),
159
+ )
160
+
161
+ def get_request_model(self):
162
+ """
163
+ Return the request model class.
164
+
165
+ Simple tools use the base ToolRequest by default.
166
+ Override this if your tool needs a custom request model.
167
+ """
168
+ return ToolRequest
169
+
170
+ # Hook methods for safe attribute access without hasattr/getattr
171
+
172
+ def get_request_model_name(self, request) -> Optional[str]:
173
+ """Get model name from request. Override for custom model name handling."""
174
+ try:
175
+ return request.model
176
+ except AttributeError:
177
+ return None
178
+
179
+ def get_request_images(self, request) -> list:
180
+ """Get images from request. Override for custom image handling."""
181
+ try:
182
+ return request.images if request.images is not None else []
183
+ except AttributeError:
184
+ return []
185
+
186
+ def get_request_continuation_id(self, request) -> Optional[str]:
187
+ """Get continuation_id from request. Override for custom continuation handling."""
188
+ try:
189
+ return request.continuation_id
190
+ except AttributeError:
191
+ return None
192
+
193
+ def get_request_prompt(self, request) -> str:
194
+ """Get prompt from request. Override for custom prompt handling."""
195
+ try:
196
+ return request.prompt
197
+ except AttributeError:
198
+ return ""
199
+
200
+ def get_request_temperature(self, request) -> Optional[float]:
201
+ """Get temperature from request. Override for custom temperature handling."""
202
+ try:
203
+ return request.temperature
204
+ except AttributeError:
205
+ return None
206
+
207
+ def get_request_thinking_mode(self, request) -> Optional[str]:
208
+ """Get thinking_mode from request. Override for custom thinking mode handling."""
209
+ try:
210
+ return request.thinking_mode
211
+ except AttributeError:
212
+ return None
213
+
214
+ def get_request_files(self, request) -> list:
215
+ """Get absolute file paths from request. Override for custom file handling."""
216
+ try:
217
+ files = request.absolute_file_paths
218
+ except AttributeError:
219
+ files = None
220
+ if files is None:
221
+ return []
222
+ return files
223
+
224
+ def get_request_as_dict(self, request) -> dict:
225
+ """Convert request to dictionary. Override for custom serialization."""
226
+ try:
227
+ # Try Pydantic v2 method first
228
+ return request.model_dump()
229
+ except AttributeError:
230
+ try:
231
+ # Fall back to Pydantic v1 method
232
+ return request.dict()
233
+ except AttributeError:
234
+ # Last resort - convert to dict manually
235
+ return {"prompt": self.get_request_prompt(request)}
236
+
237
+ def set_request_files(self, request, files: list) -> None:
238
+ """Set absolute file paths on request. Override for custom file setting."""
239
+ try:
240
+ request.absolute_file_paths = files
241
+ except AttributeError:
242
+ pass
243
+
244
+ def get_actually_processed_files(self) -> list:
245
+ """Get actually processed files. Override for custom file tracking."""
246
+ try:
247
+ return self._actually_processed_files
248
+ except AttributeError:
249
+ return []
250
+
251
+ async def execute(self, arguments: dict[str, Any]) -> list:
252
+ """Validate and delegate once; the server owns replay and atomic recording."""
253
+ from mcp.types import TextContent
254
+ from pydantic import ValidationError
255
+
256
+ from clink.contracts import BackendError
257
+ from tools.models import ToolOutput
258
+ from utils.call_state import StateError
259
+
260
+ self._current_arguments = arguments
261
+ try:
262
+ request = self.get_request_model()(**arguments)
263
+ path_error = self._validate_file_paths(request)
264
+ if path_error:
265
+ raise BackendError(
266
+ "path_not_allowed", path_error, "Supply existing absolute paths inside allowed roots."
267
+ )
268
+ files = self._resolve_cli_target(
269
+ request, files=self.get_request_files(request), images=self.get_request_images(request)
270
+ )
271
+ self.set_request_files(request, list(files))
272
+ prompt = await self.prepare_prompt(request)
273
+ result = await self._run_cli(
274
+ request,
275
+ prompt,
276
+ self.get_system_prompt() + "\n\n" + self.get_language_instruction(),
277
+ files=self.get_request_files(request),
278
+ images=self.get_request_images(request),
279
+ )
280
+ output = self._parse_response(result.content, request, {"metadata": result.to_metadata()})
281
+ return [TextContent(type="text", text=output.model_dump_json())]
282
+ except (ToolExecutionError, StateError):
283
+ raise
284
+ except BackendError as exc:
285
+ output = ToolOutput(status="error", content=str(exc), metadata=exc.to_dict())
286
+ raise ToolExecutionError(output.model_dump_json()) from exc
287
+ except (ValidationError, ValueError):
288
+ output = ToolOutput(
289
+ status="error",
290
+ content="Request validation failed; check the declared fields.",
291
+ metadata={"code": "invalid_request"},
292
+ )
293
+ raise ToolExecutionError(output.model_dump_json()) from None
294
+ except Exception as exc:
295
+ logging.getLogger(__name__).error(
296
+ "tool_execution_failed tool=%s error_type=%s", self.get_name(), type(exc).__name__
297
+ )
298
+ output = ToolOutput(
299
+ status="error",
300
+ content="The tool could not complete this request.",
301
+ metadata={"code": "tool_error", "error_type": type(exc).__name__},
302
+ )
303
+ raise ToolExecutionError(output.model_dump_json()) from None
304
+
305
+ def _parse_response(self, raw_text: str, request, model_info: Optional[dict] = None):
306
+ """Keep the public envelope while sharing transaction-owned continuation IDs."""
307
+ from tools.models import ContinuationOffer, ToolOutput
308
+ from utils.call_state import get_current_call
309
+ from utils.conversation_memory import MAX_CONVERSATION_TURNS
310
+
311
+ metadata = dict((model_info or {}).get("metadata", {}))
312
+ metadata["tool_name"] = self.get_name()
313
+ offer = None
314
+ call = get_current_call()
315
+ if call is not None:
316
+ remaining = max(0, MAX_CONVERSATION_TURNS - call.context.successful_turns - 2)
317
+ if remaining:
318
+ offer = ContinuationOffer(
319
+ continuation_id=call.continuation_id,
320
+ remaining_turns=remaining,
321
+ note="Reuse this continuation_id to preserve conversation context.",
322
+ )
323
+ return ToolOutput(
324
+ status="continuation_available" if offer else "success",
325
+ content=self.format_response(raw_text, request, model_info),
326
+ continuation_offer=offer,
327
+ metadata=metadata,
328
+ )
329
+
330
+ # Convenience methods for common tool patterns
331
+
332
+ def build_standard_prompt(
333
+ self, system_prompt: str, user_content: str, request, file_context_title: str = "CONTEXT FILES"
334
+ ) -> str:
335
+ """
336
+ Build a standard prompt with system prompt, user content, and optional files.
337
+
338
+ This is a convenience method that handles the common pattern of:
339
+ 1. Adding file content if present
340
+ 2. Checking token limits
341
+ 3. Adding web search instructions
342
+ 4. Combining everything into a well-formatted prompt
343
+
344
+ Args:
345
+ system_prompt: The system prompt for the tool
346
+ user_content: The main user request/content
347
+ request: The validated request object
348
+ file_context_title: Title for the file context section
349
+
350
+ Returns:
351
+ Complete formatted prompt ready for the AI model
352
+ """
353
+ # Check size limits against raw user input before enriching with internal context
354
+ content_to_validate = self.get_prompt_content_for_size_validation(user_content)
355
+ self._validate_token_limit(content_to_validate, "Content")
356
+
357
+ # Only the common backend reads/embeds declared files, once under its byte cap.
358
+ files = self.get_request_files(request)
359
+ if files:
360
+ references = "\n".join(f"- {path}" for path in files)
361
+ user_content = f"{user_content}\n\n=== {file_context_title} REFERENCES ===\n{references}"
362
+
363
+ # Add standardized web search guidance
364
+ websearch_instruction = self.get_websearch_instruction(self.get_websearch_guidance())
365
+
366
+ # Combine system prompt with user content
367
+ full_prompt = f"""{system_prompt}{websearch_instruction}
368
+
369
+ === USER REQUEST ===
370
+ {user_content}
371
+ === END REQUEST ===
372
+
373
+ Please provide a thoughtful, comprehensive response:"""
374
+
375
+ return full_prompt
376
+
377
+ def get_prompt_content_for_size_validation(self, user_content: str) -> str:
378
+ """
379
+ Override to use original user prompt for size validation when conversation history is embedded.
380
+
381
+ When server.py embeds conversation history into the prompt field, it also stores
382
+ the original user prompt in _original_user_prompt. We use that for size validation
383
+ to avoid incorrectly triggering size limits due to conversation history.
384
+
385
+ Args:
386
+ user_content: The user content (may include conversation history)
387
+
388
+ Returns:
389
+ The original user prompt if available, otherwise the full user content
390
+ """
391
+ # Check if we have the current arguments from execute() method
392
+ current_args = getattr(self, "_current_arguments", None)
393
+ if current_args:
394
+ # If server.py embedded conversation history, it stores original prompt separately
395
+ original_user_prompt = current_args.get("_original_user_prompt")
396
+ if original_user_prompt is not None:
397
+ # Use original user prompt for size validation (excludes conversation history)
398
+ return original_user_prompt
399
+
400
+ # Fallback to default behavior (validate full user content)
401
+ return user_content
402
+
403
+ def get_websearch_guidance(self) -> Optional[str]:
404
+ """
405
+ Return tool-specific web search guidance.
406
+
407
+ Override this to provide tool-specific guidance for when web searches
408
+ would be helpful. Return None to use the default guidance.
409
+
410
+ Returns:
411
+ Tool-specific web search guidance or None for default
412
+ """
413
+ return None
414
+
415
+ def handle_prompt_file_with_fallback(self, request) -> str:
416
+ """Use only an explicitly attached prompt.txt as this call's primary input."""
417
+ import os
418
+
419
+ from clink.contracts import BackendError
420
+
421
+ files = self.get_request_files(request)
422
+ original = self._current_arguments.get("_original_arguments")
423
+ if original is None:
424
+ explicit_files = files
425
+ else:
426
+ originals = {os.path.realpath(os.path.expanduser(path)) for path in original.get("absolute_file_paths", [])}
427
+ explicit_files = [path for path in files if os.path.realpath(path) in originals]
428
+ prompt_content, updated_files = self.handle_prompt_file(explicit_files)
429
+ if prompt_content is not None:
430
+ consumed = set(explicit_files) - set(updated_files or [])
431
+ self.set_request_files(request, [path for path in files if path not in consumed])
432
+ user_content = prompt_content if prompt_content is not None else self.get_request_prompt(request)
433
+ self._effective_user_prompt = user_content
434
+ size_check = self.check_prompt_size(user_content)
435
+ if size_check:
436
+ raise BackendError("input_limit", size_check["content"], size_check["metadata"]["remedy"])
437
+ return user_content
438
+
439
+ def get_chat_style_websearch_guidance(self) -> str:
440
+ """
441
+ Get Chat tool-style web search guidance.
442
+
443
+ Returns web search guidance that matches the original Chat tool pattern.
444
+ This is useful for tools that want to maintain the same search behavior.
445
+
446
+ Returns:
447
+ Web search guidance text
448
+ """
449
+ return """When discussing topics, consider if searches for these would help:
450
+ - Documentation for any technologies or concepts mentioned
451
+ - Current best practices and patterns
452
+ - Recent developments or updates
453
+ - Community discussions and solutions"""
454
+
455
+ def supports_custom_request_model(self) -> bool:
456
+ """
457
+ Indicate whether this tool supports custom request models.
458
+
459
+ Simple tools support custom request models by default. Tools that override
460
+ get_request_model() to return something other than ToolRequest should
461
+ return True here.
462
+
463
+ Returns:
464
+ True if the tool uses a custom request model
465
+ """
466
+ return self.get_request_model() != ToolRequest
467
+
468
+ def _validate_file_paths(self, request) -> Optional[str]:
469
+ """
470
+ Validate that all file paths in the request are absolute paths.
471
+
472
+ This is a security measure to prevent path traversal attacks and ensure
473
+ proper access control. All file paths must be absolute (starting with '/').
474
+
475
+ Args:
476
+ request: The validated request object
477
+
478
+ Returns:
479
+ Optional[str]: Error message if validation fails, None if all paths are valid
480
+ """
481
+ import os
482
+
483
+ # Check if request has absolute file paths attribute (legacy tools may still provide 'files')
484
+ files = self.get_request_files(request)
485
+ if files:
486
+ for file_path in files:
487
+ if not os.path.isabs(file_path):
488
+ return (
489
+ f"Error: All file paths must be FULL absolute paths to real files / folders - DO NOT SHORTEN. "
490
+ f"Received relative path: {file_path}\n"
491
+ f"Please provide the full absolute path starting with '/' (must be FULL absolute paths to real files / folders - DO NOT SHORTEN)"
492
+ )
493
+
494
+ return None
495
+
496
+ def prepare_chat_style_prompt(self, request, system_prompt: str = None) -> str:
497
+ """
498
+ Prepare a prompt using Chat tool-style patterns.
499
+
500
+ This convenience method replicates the Chat tool's prompt preparation logic:
501
+ 1. Handle prompt.txt file if present
502
+ 2. Add file context with specific formatting
503
+ 3. Add web search guidance
504
+ 4. Format with system prompt
505
+
506
+ Args:
507
+ request: The validated request object
508
+ system_prompt: System prompt to use (uses get_system_prompt() if None)
509
+
510
+ Returns:
511
+ Complete formatted prompt
512
+ """
513
+ # Use provided system prompt or get from tool
514
+ if system_prompt is None:
515
+ system_prompt = self.get_system_prompt()
516
+
517
+ # Get user content (handles prompt.txt files)
518
+ user_content = self.handle_prompt_file_with_fallback(request)
519
+
520
+ # Build standard prompt with Chat-style web search guidance
521
+ websearch_guidance = self.get_chat_style_websearch_guidance()
522
+
523
+ # Override the websearch guidance temporarily
524
+ original_guidance = self.get_websearch_guidance
525
+ self.get_websearch_guidance = lambda: websearch_guidance
526
+
527
+ try:
528
+ full_prompt = self.build_standard_prompt(system_prompt, user_content, request, "CONTEXT FILES")
529
+ finally:
530
+ # Restore original guidance method
531
+ self.get_websearch_guidance = original_guidance
532
+
533
+ if system_prompt:
534
+ marker = "\n\n=== USER REQUEST ===\n"
535
+ if marker in full_prompt:
536
+ _, user_section = full_prompt.split(marker, 1)
537
+ return f"=== USER REQUEST ===\n{user_section}"
538
+
539
+ return full_prompt