ripperdoc 0.2.6__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.
- ripperdoc/__init__.py +3 -0
- ripperdoc/__main__.py +20 -0
- ripperdoc/cli/__init__.py +1 -0
- ripperdoc/cli/cli.py +405 -0
- ripperdoc/cli/commands/__init__.py +82 -0
- ripperdoc/cli/commands/agents_cmd.py +263 -0
- ripperdoc/cli/commands/base.py +19 -0
- ripperdoc/cli/commands/clear_cmd.py +18 -0
- ripperdoc/cli/commands/compact_cmd.py +23 -0
- ripperdoc/cli/commands/config_cmd.py +31 -0
- ripperdoc/cli/commands/context_cmd.py +144 -0
- ripperdoc/cli/commands/cost_cmd.py +82 -0
- ripperdoc/cli/commands/doctor_cmd.py +221 -0
- ripperdoc/cli/commands/exit_cmd.py +19 -0
- ripperdoc/cli/commands/help_cmd.py +20 -0
- ripperdoc/cli/commands/mcp_cmd.py +70 -0
- ripperdoc/cli/commands/memory_cmd.py +202 -0
- ripperdoc/cli/commands/models_cmd.py +413 -0
- ripperdoc/cli/commands/permissions_cmd.py +302 -0
- ripperdoc/cli/commands/resume_cmd.py +98 -0
- ripperdoc/cli/commands/status_cmd.py +167 -0
- ripperdoc/cli/commands/tasks_cmd.py +278 -0
- ripperdoc/cli/commands/todos_cmd.py +69 -0
- ripperdoc/cli/commands/tools_cmd.py +19 -0
- ripperdoc/cli/ui/__init__.py +1 -0
- ripperdoc/cli/ui/context_display.py +298 -0
- ripperdoc/cli/ui/helpers.py +22 -0
- ripperdoc/cli/ui/rich_ui.py +1557 -0
- ripperdoc/cli/ui/spinner.py +49 -0
- ripperdoc/cli/ui/thinking_spinner.py +128 -0
- ripperdoc/cli/ui/tool_renderers.py +298 -0
- ripperdoc/core/__init__.py +1 -0
- ripperdoc/core/agents.py +486 -0
- ripperdoc/core/commands.py +33 -0
- ripperdoc/core/config.py +559 -0
- ripperdoc/core/default_tools.py +88 -0
- ripperdoc/core/permissions.py +252 -0
- ripperdoc/core/providers/__init__.py +47 -0
- ripperdoc/core/providers/anthropic.py +250 -0
- ripperdoc/core/providers/base.py +265 -0
- ripperdoc/core/providers/gemini.py +615 -0
- ripperdoc/core/providers/openai.py +487 -0
- ripperdoc/core/query.py +1058 -0
- ripperdoc/core/query_utils.py +622 -0
- ripperdoc/core/skills.py +295 -0
- ripperdoc/core/system_prompt.py +431 -0
- ripperdoc/core/tool.py +240 -0
- ripperdoc/sdk/__init__.py +9 -0
- ripperdoc/sdk/client.py +333 -0
- ripperdoc/tools/__init__.py +1 -0
- ripperdoc/tools/ask_user_question_tool.py +431 -0
- ripperdoc/tools/background_shell.py +389 -0
- ripperdoc/tools/bash_output_tool.py +98 -0
- ripperdoc/tools/bash_tool.py +1016 -0
- ripperdoc/tools/dynamic_mcp_tool.py +428 -0
- ripperdoc/tools/enter_plan_mode_tool.py +226 -0
- ripperdoc/tools/exit_plan_mode_tool.py +153 -0
- ripperdoc/tools/file_edit_tool.py +346 -0
- ripperdoc/tools/file_read_tool.py +203 -0
- ripperdoc/tools/file_write_tool.py +205 -0
- ripperdoc/tools/glob_tool.py +179 -0
- ripperdoc/tools/grep_tool.py +370 -0
- ripperdoc/tools/kill_bash_tool.py +136 -0
- ripperdoc/tools/ls_tool.py +471 -0
- ripperdoc/tools/mcp_tools.py +591 -0
- ripperdoc/tools/multi_edit_tool.py +456 -0
- ripperdoc/tools/notebook_edit_tool.py +386 -0
- ripperdoc/tools/skill_tool.py +205 -0
- ripperdoc/tools/task_tool.py +379 -0
- ripperdoc/tools/todo_tool.py +494 -0
- ripperdoc/tools/tool_search_tool.py +380 -0
- ripperdoc/utils/__init__.py +1 -0
- ripperdoc/utils/bash_constants.py +51 -0
- ripperdoc/utils/bash_output_utils.py +43 -0
- ripperdoc/utils/coerce.py +34 -0
- ripperdoc/utils/context_length_errors.py +252 -0
- ripperdoc/utils/exit_code_handlers.py +241 -0
- ripperdoc/utils/file_watch.py +135 -0
- ripperdoc/utils/git_utils.py +274 -0
- ripperdoc/utils/json_utils.py +27 -0
- ripperdoc/utils/log.py +176 -0
- ripperdoc/utils/mcp.py +560 -0
- ripperdoc/utils/memory.py +253 -0
- ripperdoc/utils/message_compaction.py +676 -0
- ripperdoc/utils/messages.py +519 -0
- ripperdoc/utils/output_utils.py +258 -0
- ripperdoc/utils/path_ignore.py +677 -0
- ripperdoc/utils/path_utils.py +46 -0
- ripperdoc/utils/permissions/__init__.py +27 -0
- ripperdoc/utils/permissions/path_validation_utils.py +174 -0
- ripperdoc/utils/permissions/shell_command_validation.py +552 -0
- ripperdoc/utils/permissions/tool_permission_utils.py +279 -0
- ripperdoc/utils/prompt.py +17 -0
- ripperdoc/utils/safe_get_cwd.py +31 -0
- ripperdoc/utils/sandbox_utils.py +38 -0
- ripperdoc/utils/session_history.py +260 -0
- ripperdoc/utils/session_usage.py +117 -0
- ripperdoc/utils/shell_token_utils.py +95 -0
- ripperdoc/utils/shell_utils.py +159 -0
- ripperdoc/utils/todo.py +203 -0
- ripperdoc/utils/token_estimation.py +34 -0
- ripperdoc-0.2.6.dist-info/METADATA +193 -0
- ripperdoc-0.2.6.dist-info/RECORD +107 -0
- ripperdoc-0.2.6.dist-info/WHEEL +5 -0
- ripperdoc-0.2.6.dist-info/entry_points.txt +3 -0
- ripperdoc-0.2.6.dist-info/licenses/LICENSE +53 -0
- ripperdoc-0.2.6.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
"""Tool search helper for deferred tool loading."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
import re
|
|
7
|
+
from collections import Counter, defaultdict
|
|
8
|
+
from difflib import SequenceMatcher
|
|
9
|
+
from typing import Any, AsyncGenerator, Dict, List, Optional
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
12
|
+
|
|
13
|
+
from ripperdoc.core.tool import (
|
|
14
|
+
Tool,
|
|
15
|
+
ToolOutput,
|
|
16
|
+
ToolResult,
|
|
17
|
+
ToolUseContext,
|
|
18
|
+
ToolUseExample,
|
|
19
|
+
ValidationResult,
|
|
20
|
+
build_tool_description,
|
|
21
|
+
)
|
|
22
|
+
from ripperdoc.utils.log import get_logger
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
logger = get_logger()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ToolSearchInput(BaseModel):
|
|
29
|
+
"""Input for tool search and activation."""
|
|
30
|
+
|
|
31
|
+
query: Optional[str] = Field(
|
|
32
|
+
default=None,
|
|
33
|
+
description="Search phrase describing the capability or tool name you need.",
|
|
34
|
+
)
|
|
35
|
+
names: Optional[List[str]] = Field(
|
|
36
|
+
default=None,
|
|
37
|
+
description="Explicit tool names to activate. Use after seeing search results.",
|
|
38
|
+
)
|
|
39
|
+
max_results: int = Field(
|
|
40
|
+
default=5,
|
|
41
|
+
ge=1,
|
|
42
|
+
le=25,
|
|
43
|
+
description="Maximum number of matching tools to return.",
|
|
44
|
+
)
|
|
45
|
+
include_active: bool = Field(
|
|
46
|
+
default=False,
|
|
47
|
+
description="Include already-active tools in the search results.",
|
|
48
|
+
)
|
|
49
|
+
auto_activate: bool = Field(
|
|
50
|
+
default=True,
|
|
51
|
+
description="If true, activate the returned matches so they can be called immediately.",
|
|
52
|
+
)
|
|
53
|
+
include_examples: bool = Field(
|
|
54
|
+
default=False,
|
|
55
|
+
description="Include input examples in the returned tool descriptions.",
|
|
56
|
+
)
|
|
57
|
+
model_config = ConfigDict(extra="ignore")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ToolSearchMatch(BaseModel):
|
|
61
|
+
"""Metadata about a matching tool."""
|
|
62
|
+
|
|
63
|
+
name: str
|
|
64
|
+
user_facing_name: Optional[str] = None
|
|
65
|
+
description: Optional[str] = None
|
|
66
|
+
active: bool = False
|
|
67
|
+
deferred: bool = False
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class ToolSearchOutput(BaseModel):
|
|
71
|
+
"""Search results and activation summary."""
|
|
72
|
+
|
|
73
|
+
matches: List[ToolSearchMatch] = Field(default_factory=list)
|
|
74
|
+
activated: List[str] = Field(default_factory=list)
|
|
75
|
+
missing: List[str] = Field(default_factory=list)
|
|
76
|
+
deferred_remaining: int = 0
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class ToolSearchTool(Tool[ToolSearchInput, ToolSearchOutput]):
|
|
80
|
+
"""Search across available tools and activate deferred ones on demand."""
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def name(self) -> str:
|
|
84
|
+
return "ToolSearch"
|
|
85
|
+
|
|
86
|
+
async def description(self) -> str:
|
|
87
|
+
return (
|
|
88
|
+
"Search available tools by name or description, returning a small set of candidates. "
|
|
89
|
+
"Use this when you suspect a capability exists but is not currently active. "
|
|
90
|
+
"Matching deferred tools are automatically activated so you can call them next."
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def input_schema(self) -> type[ToolSearchInput]:
|
|
95
|
+
return ToolSearchInput
|
|
96
|
+
|
|
97
|
+
def input_examples(self) -> List[ToolUseExample]:
|
|
98
|
+
return [
|
|
99
|
+
ToolUseExample(
|
|
100
|
+
description="Search for notebook-related tools and activate top results",
|
|
101
|
+
example={"query": "notebook", "max_results": 3},
|
|
102
|
+
),
|
|
103
|
+
ToolUseExample(
|
|
104
|
+
description="Activate a known tool by name",
|
|
105
|
+
example={"names": ["mcp__search__query"], "auto_activate": True},
|
|
106
|
+
),
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
async def prompt(self, safe_mode: bool = False) -> str: # noqa: ARG002
|
|
110
|
+
return (
|
|
111
|
+
"Search for a tool by providing a short description (e.g., 'query database', 'render notebook'). "
|
|
112
|
+
"Use names to activate tools you've already discovered. "
|
|
113
|
+
"Keep queries concise to retrieve the 3-5 most relevant tools."
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def is_read_only(self) -> bool:
|
|
117
|
+
return True
|
|
118
|
+
|
|
119
|
+
def is_concurrency_safe(self) -> bool:
|
|
120
|
+
return True
|
|
121
|
+
|
|
122
|
+
def needs_permissions(self, input_data: Optional[ToolSearchInput] = None) -> bool: # noqa: ARG002
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
async def validate_input(
|
|
126
|
+
self,
|
|
127
|
+
input_data: ToolSearchInput,
|
|
128
|
+
context: Optional[ToolUseContext] = None, # noqa: ARG002
|
|
129
|
+
) -> ValidationResult:
|
|
130
|
+
if not (input_data.query or input_data.names):
|
|
131
|
+
return ValidationResult(
|
|
132
|
+
result=False,
|
|
133
|
+
message="Provide a search query or explicit tool names to load.",
|
|
134
|
+
)
|
|
135
|
+
return ValidationResult(result=True)
|
|
136
|
+
|
|
137
|
+
def render_result_for_assistant(self, output: ToolSearchOutput) -> str:
|
|
138
|
+
lines = []
|
|
139
|
+
if output.activated:
|
|
140
|
+
lines.append(f"Activated: {', '.join(sorted(output.activated))}")
|
|
141
|
+
if output.matches:
|
|
142
|
+
lines.append("Matches:")
|
|
143
|
+
for match in output.matches:
|
|
144
|
+
status = []
|
|
145
|
+
if match.active:
|
|
146
|
+
status.append("active")
|
|
147
|
+
if match.deferred:
|
|
148
|
+
status.append("deferred")
|
|
149
|
+
status_text = f" ({', '.join(status)})" if status else ""
|
|
150
|
+
lines.append(f"- {match.name}{status_text}: {match.description or ''}".strip())
|
|
151
|
+
if output.missing:
|
|
152
|
+
lines.append(f"Unknown tool names: {', '.join(sorted(output.missing))}")
|
|
153
|
+
if output.deferred_remaining:
|
|
154
|
+
lines.append(f"Deferred tools remaining: {output.deferred_remaining}")
|
|
155
|
+
return "\n".join(lines) if lines else "No matching tools found."
|
|
156
|
+
|
|
157
|
+
def render_tool_use_message(self, input_data: ToolSearchInput, verbose: bool = False) -> str:
|
|
158
|
+
detail = f'"{input_data.query}"' if input_data.query else ", ".join(input_data.names or [])
|
|
159
|
+
return f"Search tools for {detail}"
|
|
160
|
+
|
|
161
|
+
async def _search(
|
|
162
|
+
self,
|
|
163
|
+
query: str,
|
|
164
|
+
registry: Any,
|
|
165
|
+
*,
|
|
166
|
+
include_active: bool,
|
|
167
|
+
include_examples: bool,
|
|
168
|
+
limit: int,
|
|
169
|
+
) -> List[Dict[str, Any]]:
|
|
170
|
+
"""Regex + BM25-style search over tool metadata."""
|
|
171
|
+
normalized = (query or "").strip().lower()
|
|
172
|
+
if not normalized:
|
|
173
|
+
return []
|
|
174
|
+
|
|
175
|
+
regex: Optional[re.Pattern[str]] = None
|
|
176
|
+
if normalized.startswith("/") and normalized.endswith("/") and len(normalized) > 2:
|
|
177
|
+
try:
|
|
178
|
+
regex = re.compile(normalized[1:-1], re.IGNORECASE)
|
|
179
|
+
except re.error:
|
|
180
|
+
regex = None
|
|
181
|
+
logger.exception("[tool_search] Invalid regex search query", extra={"query": query})
|
|
182
|
+
|
|
183
|
+
def _tokenize(text: str) -> List[str]:
|
|
184
|
+
return re.findall(r"[a-z0-9]+", text.lower())
|
|
185
|
+
|
|
186
|
+
corpus: List[tuple[str, Any, List[str], int, str]] = []
|
|
187
|
+
for name, tool in registry.iter_named_tools():
|
|
188
|
+
try:
|
|
189
|
+
description = await build_tool_description(
|
|
190
|
+
tool, include_examples=include_examples, max_examples=2
|
|
191
|
+
)
|
|
192
|
+
except (OSError, RuntimeError, ValueError, TypeError, AttributeError, KeyError) as exc:
|
|
193
|
+
description = ""
|
|
194
|
+
logger.warning(
|
|
195
|
+
"[tool_search] Failed to build tool description: %s: %s",
|
|
196
|
+
type(exc).__name__, exc,
|
|
197
|
+
extra={"tool_name": getattr(tool, "name", None)},
|
|
198
|
+
)
|
|
199
|
+
doc_text = " ".join([name, tool.user_facing_name(), description])
|
|
200
|
+
tokens = _tokenize(doc_text)
|
|
201
|
+
corpus.append((name, tool, tokens, len(tokens), description))
|
|
202
|
+
|
|
203
|
+
if not corpus:
|
|
204
|
+
return []
|
|
205
|
+
|
|
206
|
+
avg_len = sum(doc_len for _, _, _, doc_len, _ in corpus) / len(corpus)
|
|
207
|
+
query_terms = _tokenize(normalized)
|
|
208
|
+
df: Dict[str, int] = defaultdict(int)
|
|
209
|
+
for _, _, tokens, _, _ in corpus:
|
|
210
|
+
seen_terms = set(tokens)
|
|
211
|
+
for term in query_terms:
|
|
212
|
+
if term in seen_terms:
|
|
213
|
+
df[term] += 1
|
|
214
|
+
|
|
215
|
+
k1 = 1.5
|
|
216
|
+
b = 0.75
|
|
217
|
+
|
|
218
|
+
def _bm25_score(tokens: List[str], doc_len: int) -> float:
|
|
219
|
+
score = 0.0
|
|
220
|
+
counts = Counter(tokens)
|
|
221
|
+
for term in query_terms:
|
|
222
|
+
if term not in counts:
|
|
223
|
+
continue
|
|
224
|
+
tf = counts[term]
|
|
225
|
+
df_term = df.get(term, 0) or 1
|
|
226
|
+
idf = math.log((len(corpus) - df_term + 0.5) / (df_term + 0.5) + 1)
|
|
227
|
+
numerator = tf * (k1 + 1)
|
|
228
|
+
denom = tf + k1 * (1 - b + b * (doc_len / (avg_len or 1)))
|
|
229
|
+
score += idf * (numerator / denom)
|
|
230
|
+
return score
|
|
231
|
+
|
|
232
|
+
results: List[Dict[str, Any]] = []
|
|
233
|
+
for name, tool, tokens, doc_len, description in corpus:
|
|
234
|
+
if not include_active and registry.is_active(name):
|
|
235
|
+
continue
|
|
236
|
+
|
|
237
|
+
combined_text = " ".join([name, tool.user_facing_name(), description]).lower()
|
|
238
|
+
score = _bm25_score(tokens, doc_len)
|
|
239
|
+
if regex and regex.search(combined_text):
|
|
240
|
+
score += 5.0
|
|
241
|
+
if normalized in combined_text:
|
|
242
|
+
score += 3.0
|
|
243
|
+
score += SequenceMatcher(None, normalized, name.lower()).ratio() * 2
|
|
244
|
+
score += SequenceMatcher(None, normalized, tool.user_facing_name().lower()).ratio()
|
|
245
|
+
|
|
246
|
+
results.append(
|
|
247
|
+
{
|
|
248
|
+
"name": name,
|
|
249
|
+
"user_facing_name": tool.user_facing_name(),
|
|
250
|
+
"active": registry.is_active(name),
|
|
251
|
+
"deferred": name in getattr(registry, "deferred_names", set()),
|
|
252
|
+
"description": description,
|
|
253
|
+
"input_schema": tool.input_schema.model_json_schema(),
|
|
254
|
+
"score": score,
|
|
255
|
+
}
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
return sorted(results, key=lambda item: item.get("score", 0), reverse=True)[:limit]
|
|
259
|
+
|
|
260
|
+
async def _describe_by_name(
|
|
261
|
+
self,
|
|
262
|
+
registry: Any,
|
|
263
|
+
names: List[str],
|
|
264
|
+
include_examples: bool,
|
|
265
|
+
limit: int,
|
|
266
|
+
) -> List[Dict[str, Any]]:
|
|
267
|
+
seen = set()
|
|
268
|
+
results: List[Dict[str, Any]] = []
|
|
269
|
+
for name in names:
|
|
270
|
+
if not name or name in seen:
|
|
271
|
+
continue
|
|
272
|
+
seen.add(name)
|
|
273
|
+
tool = registry.get(name) if hasattr(registry, "get") else None
|
|
274
|
+
if not tool:
|
|
275
|
+
continue
|
|
276
|
+
description = await build_tool_description(
|
|
277
|
+
tool, include_examples=include_examples, max_examples=2
|
|
278
|
+
)
|
|
279
|
+
results.append(
|
|
280
|
+
{
|
|
281
|
+
"name": name,
|
|
282
|
+
"user_facing_name": tool.user_facing_name(),
|
|
283
|
+
"description": description,
|
|
284
|
+
"active": (
|
|
285
|
+
getattr(registry, "is_active", lambda *_: False)(name)
|
|
286
|
+
if hasattr(registry, "is_active")
|
|
287
|
+
else False
|
|
288
|
+
),
|
|
289
|
+
"deferred": name in getattr(registry, "deferred_names", set()),
|
|
290
|
+
"score": 0.0,
|
|
291
|
+
}
|
|
292
|
+
)
|
|
293
|
+
if len(results) >= limit:
|
|
294
|
+
break
|
|
295
|
+
return results
|
|
296
|
+
|
|
297
|
+
async def call(
|
|
298
|
+
self,
|
|
299
|
+
input_data: ToolSearchInput,
|
|
300
|
+
context: ToolUseContext,
|
|
301
|
+
) -> AsyncGenerator[ToolOutput, None]:
|
|
302
|
+
registry = getattr(context, "tool_registry", None)
|
|
303
|
+
if not registry:
|
|
304
|
+
yield ToolResult(
|
|
305
|
+
data=ToolSearchOutput(),
|
|
306
|
+
result_for_assistant="Tool registry unavailable; cannot search tools.",
|
|
307
|
+
)
|
|
308
|
+
return
|
|
309
|
+
|
|
310
|
+
matches: List[Dict[str, Any]] = []
|
|
311
|
+
if input_data.query:
|
|
312
|
+
matches = await self._search(
|
|
313
|
+
input_data.query,
|
|
314
|
+
registry,
|
|
315
|
+
include_active=input_data.include_active,
|
|
316
|
+
include_examples=input_data.include_examples,
|
|
317
|
+
limit=input_data.max_results,
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
if input_data.names:
|
|
321
|
+
named_matches = await self._describe_by_name(
|
|
322
|
+
registry,
|
|
323
|
+
input_data.names,
|
|
324
|
+
input_data.include_examples,
|
|
325
|
+
input_data.max_results,
|
|
326
|
+
)
|
|
327
|
+
# Merge in explicit names that weren't returned by the search query.
|
|
328
|
+
known = {m["name"] for m in matches}
|
|
329
|
+
matches.extend([m for m in named_matches if m["name"] not in known])
|
|
330
|
+
|
|
331
|
+
if matches:
|
|
332
|
+
matches = sorted(matches, key=lambda item: item.get("score", 0), reverse=True)
|
|
333
|
+
if input_data.max_results:
|
|
334
|
+
matches = matches[: input_data.max_results]
|
|
335
|
+
|
|
336
|
+
max_description_chars = 600
|
|
337
|
+
for match in matches:
|
|
338
|
+
desc = match.get("description")
|
|
339
|
+
if (
|
|
340
|
+
max_description_chars
|
|
341
|
+
and isinstance(desc, str)
|
|
342
|
+
and len(desc) > max_description_chars
|
|
343
|
+
):
|
|
344
|
+
match["description"] = desc[:max_description_chars] + "..."
|
|
345
|
+
|
|
346
|
+
# Activate tools as requested.
|
|
347
|
+
activation_targets: List[str] = []
|
|
348
|
+
if input_data.names:
|
|
349
|
+
activation_targets.extend(input_data.names)
|
|
350
|
+
elif input_data.auto_activate:
|
|
351
|
+
activation_targets.extend([match["name"] for match in matches])
|
|
352
|
+
|
|
353
|
+
activated: List[str] = []
|
|
354
|
+
missing: List[str] = []
|
|
355
|
+
if activation_targets:
|
|
356
|
+
activated, missing = registry.activate_tools(activation_targets)
|
|
357
|
+
|
|
358
|
+
normalized_matches: List[ToolSearchMatch] = []
|
|
359
|
+
for match in matches[: input_data.max_results]:
|
|
360
|
+
normalized_matches.append(
|
|
361
|
+
ToolSearchMatch(
|
|
362
|
+
name=match.get("name", ""),
|
|
363
|
+
user_facing_name=match.get("user_facing_name"),
|
|
364
|
+
description=match.get("description"),
|
|
365
|
+
active=bool(match.get("active")),
|
|
366
|
+
deferred=bool(match.get("deferred")),
|
|
367
|
+
)
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
output = ToolSearchOutput(
|
|
371
|
+
matches=normalized_matches,
|
|
372
|
+
activated=activated,
|
|
373
|
+
missing=missing,
|
|
374
|
+
deferred_remaining=len(getattr(registry, "deferred_names", [])),
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
yield ToolResult(
|
|
378
|
+
data=output,
|
|
379
|
+
result_for_assistant=self.render_result_for_assistant(output),
|
|
380
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Utility modules for Ripperdoc."""
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Bash-related constants and helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
# Baseline defaults (kept in sync with the reference implementation)
|
|
9
|
+
_BASH_DEFAULT_TIMEOUT_MS = 120_000
|
|
10
|
+
_BASH_MAX_TIMEOUT_MS = 600_000
|
|
11
|
+
_BASH_MAX_OUTPUT_LENGTH = 30_000
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _parse_positive_int(value: Optional[str]) -> Optional[int]:
|
|
15
|
+
"""Best-effort conversion of an environment variable to a positive int."""
|
|
16
|
+
if value is None:
|
|
17
|
+
return None
|
|
18
|
+
try:
|
|
19
|
+
parsed = int(value)
|
|
20
|
+
except (TypeError, ValueError):
|
|
21
|
+
return None
|
|
22
|
+
return parsed if parsed > 0 else None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_bash_max_output_length() -> int:
|
|
26
|
+
"""Return the maximum output length, honoring an env override when valid."""
|
|
27
|
+
override = _parse_positive_int(os.getenv("BASH_MAX_OUTPUT_LENGTH"))
|
|
28
|
+
return override or _BASH_MAX_OUTPUT_LENGTH
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_bash_default_timeout_ms() -> int:
|
|
32
|
+
"""Return the default timeout, honoring an env override when valid."""
|
|
33
|
+
override = _parse_positive_int(os.getenv("BASH_DEFAULT_TIMEOUT_MS"))
|
|
34
|
+
return override or _BASH_DEFAULT_TIMEOUT_MS
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_bash_max_timeout_ms() -> int:
|
|
38
|
+
"""Return the maximum timeout, never lower than the default timeout."""
|
|
39
|
+
override = _parse_positive_int(os.getenv("BASH_MAX_TIMEOUT_MS"))
|
|
40
|
+
baseline = _parse_positive_int(os.getenv("BASH_DEFAULT_TIMEOUT_MS"))
|
|
41
|
+
default_timeout = baseline or _BASH_DEFAULT_TIMEOUT_MS
|
|
42
|
+
if override:
|
|
43
|
+
return max(override, default_timeout)
|
|
44
|
+
return max(_BASH_MAX_TIMEOUT_MS, default_timeout)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
__all__ = [
|
|
48
|
+
"get_bash_max_output_length",
|
|
49
|
+
"get_bash_default_timeout_ms",
|
|
50
|
+
"get_bash_max_timeout_ms",
|
|
51
|
+
]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Output helpers for BashTool."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
from ripperdoc.utils.output_utils import trim_blank_lines, truncate_output
|
|
8
|
+
from ripperdoc.utils.safe_get_cwd import get_original_cwd, safe_get_cwd
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def append_cwd_reset_message(message: str) -> str:
|
|
12
|
+
"""Append a notice when the working directory gets reset."""
|
|
13
|
+
cleaned = message.rstrip()
|
|
14
|
+
suffix = f"Shell cwd was reset to {get_original_cwd()}"
|
|
15
|
+
if cleaned:
|
|
16
|
+
return f"{cleaned}\n{suffix}"
|
|
17
|
+
return suffix
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def reset_cwd_if_needed(allowed_directories: set[str] | None = None) -> bool:
|
|
21
|
+
"""Placeholder that mirrors the reference contract.
|
|
22
|
+
|
|
23
|
+
In this environment we simply report whether the current cwd is outside the
|
|
24
|
+
provided allowed set and reset to the original cwd if so.
|
|
25
|
+
"""
|
|
26
|
+
allowed_directories = allowed_directories or set()
|
|
27
|
+
current = safe_get_cwd()
|
|
28
|
+
if not allowed_directories:
|
|
29
|
+
return False
|
|
30
|
+
if current in allowed_directories:
|
|
31
|
+
return False
|
|
32
|
+
os.chdir(get_original_cwd())
|
|
33
|
+
return True
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"append_cwd_reset_message",
|
|
38
|
+
"reset_cwd_if_needed",
|
|
39
|
+
"trim_blank_lines",
|
|
40
|
+
"truncate_output",
|
|
41
|
+
"safe_get_cwd",
|
|
42
|
+
"get_original_cwd",
|
|
43
|
+
]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Lightweight parsing helpers for permissive type coercion."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def parse_boolish(value: object, default: bool = False) -> bool:
|
|
9
|
+
"""Parse a truthy/falsey value from common representations."""
|
|
10
|
+
if value is None:
|
|
11
|
+
return default
|
|
12
|
+
if isinstance(value, bool):
|
|
13
|
+
return value
|
|
14
|
+
if isinstance(value, (int, float)):
|
|
15
|
+
return bool(value)
|
|
16
|
+
if isinstance(value, str):
|
|
17
|
+
normalized = value.strip().lower()
|
|
18
|
+
if normalized in {"1", "true", "yes", "on"}:
|
|
19
|
+
return True
|
|
20
|
+
if normalized in {"0", "false", "no", "off"}:
|
|
21
|
+
return False
|
|
22
|
+
return default
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def parse_optional_int(value: object) -> Optional[int]:
|
|
26
|
+
"""Best-effort int parsing; returns None on failure."""
|
|
27
|
+
try:
|
|
28
|
+
if value is None:
|
|
29
|
+
return None
|
|
30
|
+
if isinstance(value, bool):
|
|
31
|
+
return int(value)
|
|
32
|
+
return int(str(value).strip())
|
|
33
|
+
except (ValueError, TypeError):
|
|
34
|
+
return None
|