bone-agent 1.3.3 → 2.0.0
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.
- package/bin/bone.js +39 -0
- package/package.json +25 -39
- package/LICENSE +0 -21
- package/README.md +0 -184
- package/bin/npm-wrapper.js +0 -235
- package/bin/rg +0 -0
- package/bin/rg.exe +0 -0
- package/config.yaml.example +0 -141
- package/prompts/main/ask_questions.md +0 -31
- package/prompts/main/batch_independent_calls.md +0 -5
- package/prompts/main/casual_interactions.md +0 -11
- package/prompts/main/code_references.md +0 -8
- package/prompts/main/communication_style.md +0 -12
- package/prompts/main/context_reliability.md +0 -12
- package/prompts/main/conversational_tool_calling.md +0 -15
- package/prompts/main/dream.md +0 -36
- package/prompts/main/editing_pattern.md +0 -13
- package/prompts/main/error_handling.md +0 -6
- package/prompts/main/exploration_pattern.md +0 -21
- package/prompts/main/intro.md +0 -1
- package/prompts/main/obsidian.md +0 -16
- package/prompts/main/obsidian_project.md +0 -79
- package/prompts/main/professional_objectivity.md +0 -3
- package/prompts/main/targeted_searching.md +0 -10
- package/prompts/main/task_lists_pattern.md +0 -8
- package/prompts/main/temp_folder.md +0 -9
- package/prompts/main/think_before_acting.md +0 -10
- package/prompts/main/tone_and_style.md +0 -4
- package/prompts/main/tool_preferences.md +0 -24
- package/prompts/main/trust_subagent_context.md +0 -21
- package/prompts/main/when_to_use_sub_agent.md +0 -7
- package/prompts/micro/ask_questions.md +0 -1
- package/prompts/micro/batch_independent_calls.md +0 -1
- package/prompts/micro/casual_interactions.md +0 -1
- package/prompts/micro/code_references.md +0 -1
- package/prompts/micro/communication_style.md +0 -1
- package/prompts/micro/context_reliability.md +0 -1
- package/prompts/micro/conversational_tool_calling.md +0 -1
- package/prompts/micro/editing_pattern.md +0 -1
- package/prompts/micro/error_handling.md +0 -1
- package/prompts/micro/exploration_pattern.md +0 -1
- package/prompts/micro/intro.md +0 -1
- package/prompts/micro/obsidian.md +0 -4
- package/prompts/micro/obsidian_project.md +0 -5
- package/prompts/micro/professional_objectivity.md +0 -1
- package/prompts/micro/targeted_searching.md +0 -1
- package/prompts/micro/task_lists_pattern.md +0 -1
- package/prompts/micro/temp_folder.md +0 -1
- package/prompts/micro/think_before_acting.md +0 -5
- package/prompts/micro/tone_and_style.md +0 -1
- package/prompts/micro/tool_preferences.md +0 -1
- package/prompts/micro/trust_subagent_context.md +0 -1
- package/prompts/micro/when_to_use_sub_agent.md +0 -1
- package/requirements.txt +0 -9
- package/src/__init__.py +0 -11
- package/src/core/__init__.py +0 -1
- package/src/core/agentic.py +0 -985
- package/src/core/chat_manager.py +0 -1564
- package/src/core/config_manager.py +0 -253
- package/src/core/cron.py +0 -582
- package/src/core/cron_allowlist.py +0 -118
- package/src/core/memory.py +0 -145
- package/src/core/retry.py +0 -71
- package/src/core/sub_agent.py +0 -326
- package/src/core/tool_approval.py +0 -220
- package/src/core/tool_feedback.py +0 -778
- package/src/exceptions.py +0 -79
- package/src/llm/__init__.py +0 -1
- package/src/llm/client.py +0 -171
- package/src/llm/config.py +0 -492
- package/src/llm/prompts.py +0 -489
- package/src/llm/providers.py +0 -436
- package/src/llm/streaming.py +0 -163
- package/src/llm/token_tracker.py +0 -384
- package/src/tools/__init__.py +0 -212
- package/src/tools/constants.py +0 -59
- package/src/tools/create_file.py +0 -136
- package/src/tools/directory.py +0 -389
- package/src/tools/edit.py +0 -545
- package/src/tools/file_reader.py +0 -322
- package/src/tools/helpers/__init__.py +0 -105
- package/src/tools/helpers/base.py +0 -550
- package/src/tools/helpers/converters.py +0 -44
- package/src/tools/helpers/file_helpers.py +0 -189
- package/src/tools/helpers/formatters.py +0 -411
- package/src/tools/helpers/loader.py +0 -231
- package/src/tools/helpers/parallel_executor.py +0 -231
- package/src/tools/helpers/path_resolver.py +0 -232
- package/src/tools/helpers/plugin_manifest.py +0 -156
- package/src/tools/obsidian.py +0 -96
- package/src/tools/review_sub_agent.py +0 -189
- package/src/tools/rg_search.py +0 -460
- package/src/tools/search_plugins.py +0 -109
- package/src/tools/select_option.py +0 -600
- package/src/tools/shell.py +0 -302
- package/src/tools/sub_agent.py +0 -139
- package/src/tools/task_list.py +0 -269
- package/src/tools/web_search.py +0 -61
- package/src/ui/__init__.py +0 -1
- package/src/ui/banner.py +0 -87
- package/src/ui/commands.py +0 -2809
- package/src/ui/displays.py +0 -214
- package/src/ui/loader.py +0 -284
- package/src/ui/main.py +0 -647
- package/src/ui/prompt_utils.py +0 -113
- package/src/ui/setting_selector.py +0 -590
- package/src/ui/setup_wizard.py +0 -294
- package/src/ui/sub_agent_panel.py +0 -234
- package/src/ui/tool_confirmation.py +0 -215
- package/src/utils/__init__.py +0 -1
- package/src/utils/citation_parser.py +0 -199
- package/src/utils/editor.py +0 -158
- package/src/utils/gitignore_filter.py +0 -149
- package/src/utils/logger.py +0 -254
- package/src/utils/paths.py +0 -30
- package/src/utils/result_parsers.py +0 -108
- package/src/utils/safe_commands.py +0 -243
- package/src/utils/settings.py +0 -191
- package/src/utils/user_message_logger.py +0 -120
- package/src/utils/validation.py +0 -191
- package/src/utils/web_search.py +0 -173
package/src/tools/rg_search.py
DELETED
|
@@ -1,460 +0,0 @@
|
|
|
1
|
-
"""Ripgrep search tool."""
|
|
2
|
-
|
|
3
|
-
import logging
|
|
4
|
-
import re
|
|
5
|
-
import stat
|
|
6
|
-
import subprocess
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
from typing import Optional
|
|
9
|
-
|
|
10
|
-
from .helpers.base import tool
|
|
11
|
-
from .helpers.formatters import format_tool_result
|
|
12
|
-
from .shell import _execute_direct_command, _prepare_execution_environment
|
|
13
|
-
from .helpers.converters import coerce_bool, coerce_int
|
|
14
|
-
from utils.settings import tool_settings
|
|
15
|
-
|
|
16
|
-
logger = logging.getLogger(__name__)
|
|
17
|
-
|
|
18
|
-
# Default match limit for vault searches (separate from repo limit)
|
|
19
|
-
_VAULT_MAX_MATCHES = 20
|
|
20
|
-
|
|
21
|
-
# Regex for detecting file-path lines in rg output (shared by _annotate_file_sizes and _search_vault)
|
|
22
|
-
_path_line_re = re.compile(r"^[^\s:|].*[/.]")
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def _format_file_size(size_bytes: int) -> str:
|
|
26
|
-
"""Format file size in human-readable form."""
|
|
27
|
-
if size_bytes < 1024:
|
|
28
|
-
return f"{size_bytes} B"
|
|
29
|
-
elif size_bytes < 1024 * 1024:
|
|
30
|
-
return f"{size_bytes / 1024:.1f} KB"
|
|
31
|
-
else:
|
|
32
|
-
return f"{size_bytes / (1024 * 1024):.1f} MB"
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def _annotate_file_sizes(formatted_output: str, base_path: Path, output_mode: str = "files_with_matches") -> str:
|
|
36
|
-
"""Append human-readable file sizes to each file path line in rg output.
|
|
37
|
-
|
|
38
|
-
Works on files_with_matches and count output modes where each content
|
|
39
|
-
line starts with a file path. Skips metadata, truncation, and section
|
|
40
|
-
header lines. Skipped entirely for content mode (no benefit there).
|
|
41
|
-
"""
|
|
42
|
-
if output_mode == "content":
|
|
43
|
-
return formatted_output
|
|
44
|
-
|
|
45
|
-
lines = formatted_output.split("\n")
|
|
46
|
-
annotated = []
|
|
47
|
-
for line in lines:
|
|
48
|
-
stripped = line.strip()
|
|
49
|
-
if (not stripped
|
|
50
|
-
or stripped.startswith("exit_code=")
|
|
51
|
-
or stripped.startswith("matches=")
|
|
52
|
-
or stripped.startswith("files=")
|
|
53
|
-
or stripped.startswith("... (")
|
|
54
|
-
or stripped.startswith("[repo]")
|
|
55
|
-
or stripped.startswith("[vault]")):
|
|
56
|
-
annotated.append(line)
|
|
57
|
-
continue
|
|
58
|
-
# Only annotate pure file-path lines (files_with_matches: "file",
|
|
59
|
-
# count: "file:N"). Skip content-mode match lines ("file:line:match")
|
|
60
|
-
# which always have 2+ colons or a colon-digit-dash pattern.
|
|
61
|
-
parts = line.split(":")
|
|
62
|
-
is_file_line = (
|
|
63
|
-
_path_line_re.match(line)
|
|
64
|
-
and len(parts) <= 2
|
|
65
|
-
and (len(parts) == 1 or parts[1].strip().isdigit())
|
|
66
|
-
)
|
|
67
|
-
if is_file_line:
|
|
68
|
-
file_part = parts[0].strip()
|
|
69
|
-
full_path = base_path / file_part
|
|
70
|
-
try:
|
|
71
|
-
st = full_path.stat()
|
|
72
|
-
if stat.S_ISREG(st.st_mode):
|
|
73
|
-
size = _format_file_size(st.st_size)
|
|
74
|
-
annotated.append(f"{line} {size:>8}")
|
|
75
|
-
else:
|
|
76
|
-
annotated.append(line)
|
|
77
|
-
except (OSError, ValueError):
|
|
78
|
-
annotated.append(line)
|
|
79
|
-
else:
|
|
80
|
-
annotated.append(line)
|
|
81
|
-
return "\n".join(annotated)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
@tool(
|
|
85
|
-
name="rg",
|
|
86
|
-
description="Search files using ripgrep. Use for ALL code searches (never shell commands). Supports regex, glob/type filtering, and output modes: content, files_with_matches, or count.",
|
|
87
|
-
parameters={
|
|
88
|
-
"type": "object",
|
|
89
|
-
"properties": {
|
|
90
|
-
"pattern": {
|
|
91
|
-
"type": "string",
|
|
92
|
-
"description": "Regular expression pattern to search for"
|
|
93
|
-
},
|
|
94
|
-
"path": {
|
|
95
|
-
"type": "string",
|
|
96
|
-
"description": "File or directory to search (default: current directory)"
|
|
97
|
-
},
|
|
98
|
-
"glob": {
|
|
99
|
-
"type": "string",
|
|
100
|
-
"description": "Glob filter (e.g. \"*.js\", \"**/*.tsx\")"
|
|
101
|
-
},
|
|
102
|
-
"type": {
|
|
103
|
-
"type": "string",
|
|
104
|
-
"description": "File type (e.g. js, py, rust, go, java)"
|
|
105
|
-
},
|
|
106
|
-
"output_mode": {
|
|
107
|
-
"type": "string",
|
|
108
|
-
"enum": ["content", "files_with_matches", "count"],
|
|
109
|
-
"description": "Output mode (default: files_with_matches)"
|
|
110
|
-
},
|
|
111
|
-
"context_lines": {
|
|
112
|
-
"type": "integer",
|
|
113
|
-
"description": "Context lines around matches (requires output_mode: content)"
|
|
114
|
-
},
|
|
115
|
-
"case_insensitive": {
|
|
116
|
-
"type": "boolean",
|
|
117
|
-
"description": "Case insensitive search"
|
|
118
|
-
},
|
|
119
|
-
"multiline": {
|
|
120
|
-
"type": "boolean",
|
|
121
|
-
"description": "Patterns can span lines"
|
|
122
|
-
},
|
|
123
|
-
"max_matches": {
|
|
124
|
-
"type": "integer",
|
|
125
|
-
"description": "Max matches across all files (default: 100, 0 = use line limit)"
|
|
126
|
-
}
|
|
127
|
-
},
|
|
128
|
-
"required": ["pattern"]
|
|
129
|
-
},
|
|
130
|
-
requires_approval=False
|
|
131
|
-
)
|
|
132
|
-
def rg(
|
|
133
|
-
pattern: str,
|
|
134
|
-
repo_root: Path,
|
|
135
|
-
rg_exe_path: str,
|
|
136
|
-
console,
|
|
137
|
-
chat_manager,
|
|
138
|
-
debug_mode: bool = False,
|
|
139
|
-
gitignore_spec = None,
|
|
140
|
-
path: Optional[str] = None,
|
|
141
|
-
glob: Optional[str] = None,
|
|
142
|
-
output_mode: str = "files_with_matches",
|
|
143
|
-
vault_root: Optional[str] = None,
|
|
144
|
-
**kwargs
|
|
145
|
-
) -> str:
|
|
146
|
-
"""Search for patterns using ripgrep.
|
|
147
|
-
|
|
148
|
-
Args:
|
|
149
|
-
pattern: Regular expression pattern to search for
|
|
150
|
-
repo_root: Repository root directory (injected by context)
|
|
151
|
-
rg_exe_path: Path to rg executable (injected by context)
|
|
152
|
-
console: Rich console for output (injected by context)
|
|
153
|
-
chat_manager: ChatManager instance (injected by context)
|
|
154
|
-
debug_mode: Whether debug mode is enabled (injected by context)
|
|
155
|
-
gitignore_spec: PathSpec for .gitignore filtering (injected by context)
|
|
156
|
-
path: File or directory to search in (default: current directory)
|
|
157
|
-
glob: Glob pattern to filter files
|
|
158
|
-
output_mode: Output mode (content/files_with_matches/count)
|
|
159
|
-
vault_root: Obsidian vault root path (injected by context)
|
|
160
|
-
**kwargs: Additional keyword arguments (type, multiline, context_lines, case_insensitive)
|
|
161
|
-
|
|
162
|
-
Returns:
|
|
163
|
-
Search results with exit code. Vault results are included in a
|
|
164
|
-
separate section when vault_root is active.
|
|
165
|
-
"""
|
|
166
|
-
if not isinstance(pattern, str) or not pattern.strip():
|
|
167
|
-
return "exit_code=1\nrg requires a non-empty 'pattern' argument."
|
|
168
|
-
|
|
169
|
-
# Build rg args as a list (bypass string roundtrip to avoid shlex issues with regex metacharacters)
|
|
170
|
-
args = []
|
|
171
|
-
|
|
172
|
-
# Add --line-number for content mode
|
|
173
|
-
if output_mode == "content":
|
|
174
|
-
args.append("--line-number")
|
|
175
|
-
|
|
176
|
-
# Add multiline flag
|
|
177
|
-
multiline = coerce_bool(kwargs.get("multiline"), default=False)
|
|
178
|
-
if multiline:
|
|
179
|
-
args.append("-U")
|
|
180
|
-
args.append("--multiline-dotall")
|
|
181
|
-
|
|
182
|
-
# Add case insensitive flag
|
|
183
|
-
case_insensitive = coerce_bool(kwargs.get("case_insensitive"), default=False)
|
|
184
|
-
if case_insensitive:
|
|
185
|
-
args.append("--ignore-case")
|
|
186
|
-
|
|
187
|
-
# Add context lines flag
|
|
188
|
-
context_lines = coerce_int(kwargs.get("context_lines"))[0] if kwargs.get("context_lines") else None
|
|
189
|
-
if context_lines:
|
|
190
|
-
args.append(f"--context={context_lines}")
|
|
191
|
-
|
|
192
|
-
# Add glob pattern
|
|
193
|
-
if glob:
|
|
194
|
-
args.append(f"--glob={glob}")
|
|
195
|
-
|
|
196
|
-
# Add file type filter
|
|
197
|
-
file_type = kwargs.get("type")
|
|
198
|
-
if file_type:
|
|
199
|
-
args.append(f"--type={file_type}")
|
|
200
|
-
|
|
201
|
-
# Add output mode flags
|
|
202
|
-
if output_mode == "files_with_matches":
|
|
203
|
-
args.append("--files-with-matches")
|
|
204
|
-
elif output_mode == "count":
|
|
205
|
-
args.append("--count")
|
|
206
|
-
|
|
207
|
-
# Pattern and search path — no quoting needed, subprocess list form bypasses shell
|
|
208
|
-
args.append(pattern)
|
|
209
|
-
|
|
210
|
-
search_path = path or "."
|
|
211
|
-
args.append(search_path)
|
|
212
|
-
|
|
213
|
-
# Get max_matches from kwargs (default: 100, set to 0 for no limit)
|
|
214
|
-
raw = coerce_int(kwargs.get("max_matches"))[0] if kwargs.get("max_matches") is not None else None
|
|
215
|
-
max_matches = raw if raw is not None and raw >= 0 else 100
|
|
216
|
-
|
|
217
|
-
# Execute repo search directly (no string→shlex roundtrip)
|
|
218
|
-
try:
|
|
219
|
-
env = _prepare_execution_environment(repo_root, rg_exe_path)
|
|
220
|
-
|
|
221
|
-
result = _execute_direct_command(
|
|
222
|
-
[str(rg_exe_path)] + args,
|
|
223
|
-
repo_root, env, debug_mode, console,
|
|
224
|
-
)
|
|
225
|
-
|
|
226
|
-
command_display = "rg " + " ".join(args)
|
|
227
|
-
repo_result = format_tool_result(
|
|
228
|
-
result, command=command_display, is_rg=True,
|
|
229
|
-
debug_mode=debug_mode, max_matches=max_matches,
|
|
230
|
-
)
|
|
231
|
-
except Exception as e:
|
|
232
|
-
return f"exit_code=1\nrg command failed: {str(e)}"
|
|
233
|
-
|
|
234
|
-
# If no vault configured, return repo results directly
|
|
235
|
-
if not vault_root:
|
|
236
|
-
repo_result = _annotate_file_sizes(repo_result, repo_root, output_mode)
|
|
237
|
-
return repo_result
|
|
238
|
-
|
|
239
|
-
# Run vault search and merge results
|
|
240
|
-
vault_output = _search_vault(
|
|
241
|
-
vault_root, rg_exe_path, output_mode, debug_mode, console,
|
|
242
|
-
pattern=pattern,
|
|
243
|
-
glob=glob,
|
|
244
|
-
file_type=kwargs.get("type"),
|
|
245
|
-
case_insensitive=case_insensitive,
|
|
246
|
-
multiline=multiline,
|
|
247
|
-
context_lines=context_lines,
|
|
248
|
-
max_matches=_VAULT_MAX_MATCHES,
|
|
249
|
-
)
|
|
250
|
-
|
|
251
|
-
if not vault_output:
|
|
252
|
-
repo_result = _annotate_file_sizes(repo_result, repo_root, output_mode)
|
|
253
|
-
return repo_result
|
|
254
|
-
|
|
255
|
-
# Merge results: repo section + vault section with absolute paths
|
|
256
|
-
repo_result = _annotate_file_sizes(repo_result, repo_root, output_mode)
|
|
257
|
-
vault_output = _annotate_file_sizes(vault_output, Path(vault_root), output_mode)
|
|
258
|
-
return _merge_results(repo_result, vault_output, output_mode)
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
def _search_vault(vault_root, rg_exe_path, output_mode, debug_mode, console,
|
|
262
|
-
pattern, glob=None, file_type=None, case_insensitive=False,
|
|
263
|
-
multiline=False, context_lines=None, max_matches=20):
|
|
264
|
-
"""Run rg against the vault and return formatted output string (or None).
|
|
265
|
-
|
|
266
|
-
Builds its own command from explicit parameters to avoid mutating any
|
|
267
|
-
shared state. Uses direct subprocess with cwd=vault_root so that
|
|
268
|
-
any .gitignore in the vault doesn't filter searchable content.
|
|
269
|
-
"""
|
|
270
|
-
vault_path = Path(vault_root).resolve()
|
|
271
|
-
if not vault_path.is_dir():
|
|
272
|
-
return None
|
|
273
|
-
|
|
274
|
-
try:
|
|
275
|
-
# Build exclude globs from obsidian_settings
|
|
276
|
-
from utils.settings import obsidian_settings
|
|
277
|
-
try:
|
|
278
|
-
exclude_folders = obsidian_settings.exclude_folders_list
|
|
279
|
-
except (AttributeError, TypeError):
|
|
280
|
-
exclude_folders = []
|
|
281
|
-
|
|
282
|
-
# Build vault rg command from scratch (no shared state mutation)
|
|
283
|
-
vault_args = ["--no-ignore"]
|
|
284
|
-
|
|
285
|
-
# Add exclude folders first so they can't be overridden
|
|
286
|
-
for folder in exclude_folders:
|
|
287
|
-
vault_args.append(f"--glob=!{folder}")
|
|
288
|
-
|
|
289
|
-
# Add output-mode flags
|
|
290
|
-
if output_mode == "content":
|
|
291
|
-
vault_args.append("--line-number")
|
|
292
|
-
elif output_mode == "files_with_matches":
|
|
293
|
-
vault_args.append("--files-with-matches")
|
|
294
|
-
elif output_mode == "count":
|
|
295
|
-
vault_args.append("--count")
|
|
296
|
-
|
|
297
|
-
# Add search flags
|
|
298
|
-
if multiline:
|
|
299
|
-
vault_args.append("-U")
|
|
300
|
-
vault_args.append("--multiline-dotall")
|
|
301
|
-
if case_insensitive:
|
|
302
|
-
vault_args.append("--ignore-case")
|
|
303
|
-
if context_lines:
|
|
304
|
-
vault_args.append(f"--context={context_lines}")
|
|
305
|
-
if glob:
|
|
306
|
-
vault_args.append(f"--glob={glob}")
|
|
307
|
-
if file_type:
|
|
308
|
-
vault_args.append(f"--type={file_type}")
|
|
309
|
-
|
|
310
|
-
# Pattern and search path (always "." since cwd is vault root)
|
|
311
|
-
# No quoting needed — subprocess list args bypass the shell
|
|
312
|
-
vault_args.append(pattern)
|
|
313
|
-
vault_args.append(".")
|
|
314
|
-
|
|
315
|
-
# Prepare environment with rg on PATH
|
|
316
|
-
env = _prepare_execution_environment(vault_path, rg_exe_path)
|
|
317
|
-
|
|
318
|
-
if debug_mode and console:
|
|
319
|
-
console.print(f"[dim]→ Vault search: rg {' '.join(vault_args)}[/dim]")
|
|
320
|
-
console.print(f"[dim]→ Vault cwd: {vault_path}[/dim]")
|
|
321
|
-
|
|
322
|
-
result = subprocess.run(
|
|
323
|
-
[str(rg_exe_path)] + vault_args,
|
|
324
|
-
capture_output=True,
|
|
325
|
-
text=True,
|
|
326
|
-
encoding="utf-8",
|
|
327
|
-
errors="replace",
|
|
328
|
-
timeout=tool_settings.command_timeout_sec,
|
|
329
|
-
cwd=str(vault_path),
|
|
330
|
-
env=env,
|
|
331
|
-
)
|
|
332
|
-
|
|
333
|
-
if debug_mode and console:
|
|
334
|
-
console.print(f"[dim]→ Vault exit code: {result.returncode}[/dim]")
|
|
335
|
-
|
|
336
|
-
# rg returns 1 for no matches — that's fine, just no vault results
|
|
337
|
-
if result.returncode == 2:
|
|
338
|
-
logger.debug("Vault rg error (exit 2): %s", result.stderr.strip())
|
|
339
|
-
return None
|
|
340
|
-
|
|
341
|
-
output = (result.stdout or "").strip()
|
|
342
|
-
if not output:
|
|
343
|
-
return None
|
|
344
|
-
|
|
345
|
-
# Format vault output with its own match limit
|
|
346
|
-
formatted = format_tool_result(
|
|
347
|
-
result,
|
|
348
|
-
command="rg " + " ".join(vault_args),
|
|
349
|
-
is_rg=True,
|
|
350
|
-
debug_mode=debug_mode,
|
|
351
|
-
max_matches=max_matches,
|
|
352
|
-
)
|
|
353
|
-
|
|
354
|
-
# Prefix vault paths with absolute vault root for clarity.
|
|
355
|
-
# Only rewrite lines that look like they start with an rg file path.
|
|
356
|
-
# rg output: "relative/path:linenum:match" or "relative/path-linenum-context"
|
|
357
|
-
# or "relative/path:count" (count mode). Must contain / or . before any
|
|
358
|
-
# colon to avoid matching content-only lines or binary headers.
|
|
359
|
-
vault_prefix = str(vault_path)
|
|
360
|
-
|
|
361
|
-
lines = formatted.split("\n")
|
|
362
|
-
rewritten = []
|
|
363
|
-
for line in lines:
|
|
364
|
-
# Skip metadata lines (exit_code, matches/files)
|
|
365
|
-
if line.startswith("exit_code=") or line.startswith("matches=") or line.startswith("files="):
|
|
366
|
-
rewritten.append(line)
|
|
367
|
-
continue
|
|
368
|
-
if not line.strip() or line.startswith("... ("):
|
|
369
|
-
rewritten.append(line)
|
|
370
|
-
continue
|
|
371
|
-
# Only rewrite lines that start with a relative path
|
|
372
|
-
m = _path_line_re.match(line)
|
|
373
|
-
if m:
|
|
374
|
-
rewritten.append(f"{vault_prefix}/{line}")
|
|
375
|
-
else:
|
|
376
|
-
rewritten.append(line)
|
|
377
|
-
|
|
378
|
-
return "\n".join(rewritten)
|
|
379
|
-
|
|
380
|
-
except Exception:
|
|
381
|
-
logger.warning("Vault search failed", exc_info=True)
|
|
382
|
-
return None
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
def _merge_results(repo_result, vault_output, output_mode):
|
|
386
|
-
"""Merge repo and vault results into a single response.
|
|
387
|
-
|
|
388
|
-
Both inputs are raw formatted strings from format_tool_result.
|
|
389
|
-
We extract the content sections and combine them under headers.
|
|
390
|
-
Metadata (matches/files counts) is preserved so the display parser
|
|
391
|
-
can extract a summary for the user.
|
|
392
|
-
"""
|
|
393
|
-
def _extract_content(formatted):
|
|
394
|
-
"""Extract content lines (skip metadata header)."""
|
|
395
|
-
lines = formatted.split("\n")
|
|
396
|
-
content_lines = []
|
|
397
|
-
for line in lines:
|
|
398
|
-
stripped = line.strip()
|
|
399
|
-
if not stripped or stripped.startswith("exit_code=") or stripped.startswith("matches=") or stripped.startswith("files="):
|
|
400
|
-
continue
|
|
401
|
-
if stripped.startswith("... ("):
|
|
402
|
-
continue
|
|
403
|
-
content_lines.append(line)
|
|
404
|
-
return "\n".join(content_lines).strip()
|
|
405
|
-
|
|
406
|
-
def _extract_exit_code(formatted):
|
|
407
|
-
for line in formatted.split("\n"):
|
|
408
|
-
if line.startswith("exit_code="):
|
|
409
|
-
return line.split("=", 1)[1]
|
|
410
|
-
return "0"
|
|
411
|
-
|
|
412
|
-
def _extract_count(formatted):
|
|
413
|
-
"""Extract matches=N or files=N count from formatted result."""
|
|
414
|
-
for line in formatted.split("\n"):
|
|
415
|
-
if line.startswith("matches="):
|
|
416
|
-
try:
|
|
417
|
-
return ("matches", int(line.split("=", 1)[1].strip()))
|
|
418
|
-
except (ValueError, IndexError):
|
|
419
|
-
pass
|
|
420
|
-
elif line.startswith("files="):
|
|
421
|
-
try:
|
|
422
|
-
return ("files", int(line.split("=", 1)[1].strip()))
|
|
423
|
-
except (ValueError, IndexError):
|
|
424
|
-
pass
|
|
425
|
-
return None
|
|
426
|
-
|
|
427
|
-
repo_exit_code = _extract_exit_code(repo_result)
|
|
428
|
-
repo_content = _extract_content(repo_result)
|
|
429
|
-
vault_content = _extract_content(vault_output)
|
|
430
|
-
|
|
431
|
-
if not vault_content:
|
|
432
|
-
return repo_result
|
|
433
|
-
|
|
434
|
-
# Build combined metadata line for the display parser
|
|
435
|
-
repo_count = _extract_count(repo_result)
|
|
436
|
-
vault_count = _extract_count(vault_output)
|
|
437
|
-
|
|
438
|
-
metadata_line = ""
|
|
439
|
-
if repo_count and vault_count and repo_count[0] == vault_count[0]:
|
|
440
|
-
# Same count type (both matches or both files) — sum them
|
|
441
|
-
combined = repo_count[1] + vault_count[1]
|
|
442
|
-
metadata_line = f"{repo_count[0]}={combined}"
|
|
443
|
-
elif repo_count:
|
|
444
|
-
metadata_line = f"{repo_count[0]}={repo_count[1]}"
|
|
445
|
-
elif vault_count:
|
|
446
|
-
metadata_line = f"{vault_count[0]}={vault_count[1]}"
|
|
447
|
-
|
|
448
|
-
if not repo_content:
|
|
449
|
-
# Only vault results — return with vault label
|
|
450
|
-
header = f"exit_code=0"
|
|
451
|
-
if metadata_line:
|
|
452
|
-
header += f"\n{metadata_line}"
|
|
453
|
-
return f"{header}\n[vault]\n{vault_content}\n\n"
|
|
454
|
-
|
|
455
|
-
# Both — present under labeled sections, preserve repo exit code
|
|
456
|
-
header = f"exit_code={repo_exit_code}"
|
|
457
|
-
if metadata_line:
|
|
458
|
-
header += f"\n{metadata_line}"
|
|
459
|
-
merged = f"{header}\n[repo]\n{repo_content}\n\n[vault]\n{vault_content}\n\n"
|
|
460
|
-
return merged
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
"""search_plugins core tool for on-demand plugin discovery.
|
|
2
|
-
|
|
3
|
-
This tool lets the LLM agent search for available plugin tools and
|
|
4
|
-
activate them. Plugin schemas are not sent by default to avoid context
|
|
5
|
-
bloat — they are only included after activation.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from typing import List, Optional
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
|
|
11
|
-
from tools.helpers.base import tool, ToolRegistry, TERMINAL_NONE
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@tool(
|
|
15
|
-
name="search_plugins",
|
|
16
|
-
description=(
|
|
17
|
-
"Search for available plugin tools that can help with your task. "
|
|
18
|
-
"Plugin tools are NOT in your available tools by default — use this "
|
|
19
|
-
"to discover and activate them. Returns matching plugin names and "
|
|
20
|
-
"descriptions. Once activated, the plugin's full schema will be "
|
|
21
|
-
"available in your next response."
|
|
22
|
-
),
|
|
23
|
-
parameters={
|
|
24
|
-
"type": "object",
|
|
25
|
-
"properties": {
|
|
26
|
-
"query": {
|
|
27
|
-
"type": "string",
|
|
28
|
-
"description": "Search query describing what you need (e.g., 'send email', 'query database', 'http request')"
|
|
29
|
-
},
|
|
30
|
-
"category": {
|
|
31
|
-
"type": "string",
|
|
32
|
-
"description": "Optional category filter (e.g., 'email', 'database', 'analysis')"
|
|
33
|
-
}
|
|
34
|
-
},
|
|
35
|
-
"required": ["query"]
|
|
36
|
-
},
|
|
37
|
-
requires_approval=False,
|
|
38
|
-
terminal_policy=TERMINAL_NONE,
|
|
39
|
-
tier="core",
|
|
40
|
-
tags=["plugin", "discovery", "meta"],
|
|
41
|
-
category="core"
|
|
42
|
-
)
|
|
43
|
-
def search_plugins(
|
|
44
|
-
query: str,
|
|
45
|
-
category: str = None,
|
|
46
|
-
) -> str:
|
|
47
|
-
"""Search the plugin manifest and activate matching plugins.
|
|
48
|
-
|
|
49
|
-
Args:
|
|
50
|
-
query: Search query describing the needed capability
|
|
51
|
-
category: Optional category filter
|
|
52
|
-
|
|
53
|
-
Returns:
|
|
54
|
-
Formatted result with activated plugin names and descriptions
|
|
55
|
-
"""
|
|
56
|
-
from tools.helpers.plugin_manifest import plugin_manifest
|
|
57
|
-
|
|
58
|
-
# Check if any core tool matches the query — early return
|
|
59
|
-
core_tools = ToolRegistry.get_all(include_plugins=False)
|
|
60
|
-
query_lower = query.lower()
|
|
61
|
-
for ct in core_tools:
|
|
62
|
-
if query_lower == ct.name.lower() or query_lower in ct.name.lower():
|
|
63
|
-
return (
|
|
64
|
-
f"exit_code=0\n"
|
|
65
|
-
f"'{query}' matches a core tool that is already available: **{ct.name}**.\n"
|
|
66
|
-
f"Description: {ct.description}"
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
# Search the plugin manifest
|
|
70
|
-
matches = plugin_manifest.search(query, category=category, max_results=5)
|
|
71
|
-
|
|
72
|
-
if not matches:
|
|
73
|
-
# No matches — suggest available categories
|
|
74
|
-
categories = plugin_manifest.get_categories()
|
|
75
|
-
if categories:
|
|
76
|
-
cat_list = ", ".join(f"'{c}'" for c in categories)
|
|
77
|
-
return (
|
|
78
|
-
f"exit_code=0\n"
|
|
79
|
-
f"No plugins found matching '{query}'.\n"
|
|
80
|
-
f"Available plugin categories: {cat_list}\n"
|
|
81
|
-
f"Total plugins in manifest: {plugin_manifest.plugin_count()}"
|
|
82
|
-
)
|
|
83
|
-
return (
|
|
84
|
-
f"exit_code=0\n"
|
|
85
|
-
f"No plugins found matching '{query}'. "
|
|
86
|
-
f"No plugins are currently registered in the manifest."
|
|
87
|
-
)
|
|
88
|
-
|
|
89
|
-
# Activate matched plugins in the registry
|
|
90
|
-
activated = []
|
|
91
|
-
already_active = []
|
|
92
|
-
for tool_def in matches:
|
|
93
|
-
if ToolRegistry.is_plugin_active(tool_def.name):
|
|
94
|
-
already_active.append(tool_def.name)
|
|
95
|
-
else:
|
|
96
|
-
ToolRegistry.activate_plugin(tool_def)
|
|
97
|
-
activated.append(tool_def.name)
|
|
98
|
-
|
|
99
|
-
# Build result
|
|
100
|
-
lines = [f"exit_code=0\nFound {len(matches)} plugin(s) matching '{query}':\n"]
|
|
101
|
-
|
|
102
|
-
for tool_def in matches:
|
|
103
|
-
status = "activated" if tool_def.name in activated else "already active"
|
|
104
|
-
cat_part = f" [{tool_def.category}]" if tool_def.category else ""
|
|
105
|
-
lines.append(f"- **{tool_def.name}**{cat_part} ({status}): {tool_def.description}")
|
|
106
|
-
if tool_def.tags:
|
|
107
|
-
lines.append(f" Tags: {', '.join(tool_def.tags)}")
|
|
108
|
-
|
|
109
|
-
return "\n".join(lines)
|