bone-agent 1.3.3 → 1.4.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/README.md +17 -0
- package/config.yaml.example +5 -2
- package/package.json +1 -1
- package/prompts/main/communication_style.md +1 -1
- package/prompts/main/dream.md +23 -9
- package/prompts/main/skills.md +3 -0
- package/prompts/micro/communication_style.md +1 -1
- package/prompts/micro/skills.md +1 -0
- package/src/core/agentic.py +138 -38
- package/src/core/chat_manager.py +19 -6
- package/src/core/config_manager.py +8 -1
- package/src/core/cron.py +0 -4
- package/src/core/metadata.py +75 -0
- package/src/core/skills.py +463 -0
- package/src/core/sub_agent.py +93 -43
- package/src/core/tool_feedback.py +87 -76
- package/src/llm/client.py +7 -2
- package/src/llm/codex_provider.py +350 -0
- package/src/llm/config.py +46 -2
- package/src/llm/prompts.py +12 -7
- package/src/llm/providers.py +3 -1
- package/src/llm/token_tracker.py +15 -0
- package/src/tools/__init__.py +24 -85
- package/src/tools/create_file.py +1 -1
- package/src/tools/directory.py +1 -1
- package/src/tools/edit.py +5 -1
- package/src/tools/file_reader.py +1 -1
- package/src/tools/helpers/__init__.py +1 -7
- package/src/tools/helpers/base.py +65 -16
- package/src/tools/helpers/loader.py +2 -88
- package/src/tools/helpers/path_resolver.py +54 -3
- package/src/tools/helpers/plugin_manifest.py +99 -70
- package/src/tools/review_sub_agent.py +2 -1
- package/src/tools/rg_search.py +24 -7
- package/src/tools/search_plugins.py +140 -72
- package/src/tools/shell.py +3 -3
- package/src/ui/commands.py +355 -33
- package/src/ui/displays.py +26 -1
- package/src/ui/main.py +0 -4
- package/src/ui/tool_confirmation.py +16 -5
- package/src/utils/editor.py +88 -39
- package/src/utils/settings.py +6 -2
- package/src/utils/validation.py +10 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Capability manifest for on-demand plugin and skill discovery.
|
|
2
2
|
|
|
3
3
|
Plugin-tier tools are registered here instead of ToolRegistry at import time.
|
|
4
4
|
This keeps plugin schemas out of the LLM context window until explicitly
|
|
@@ -6,19 +6,41 @@ activated via the search_plugins core tool.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import logging
|
|
9
|
-
from
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import Dict, Iterable, List, Optional
|
|
11
|
+
|
|
12
|
+
from core.skills import (
|
|
13
|
+
SearchCandidate,
|
|
14
|
+
iter_skill_summaries,
|
|
15
|
+
search_candidates,
|
|
16
|
+
)
|
|
17
|
+
from utils.settings import tool_settings
|
|
10
18
|
|
|
11
19
|
from .base import ToolDefinition
|
|
12
20
|
|
|
13
21
|
logger = logging.getLogger(__name__)
|
|
14
22
|
|
|
15
23
|
|
|
24
|
+
@dataclass
|
|
25
|
+
class CapabilityMatch:
|
|
26
|
+
kind: str
|
|
27
|
+
name: str
|
|
28
|
+
description: str
|
|
29
|
+
category: str | None = None
|
|
30
|
+
tags: list[str] | None = None
|
|
31
|
+
tool_def: ToolDefinition | None = None
|
|
32
|
+
preview: str | None = None
|
|
33
|
+
activated: bool = False
|
|
34
|
+
already_active: bool = False
|
|
35
|
+
|
|
36
|
+
|
|
16
37
|
class PluginManifest:
|
|
17
|
-
"""Index of plugin-tier tools
|
|
38
|
+
"""Index of plugin-tier tools and stored skills for discovery surfaces.
|
|
18
39
|
|
|
19
|
-
|
|
20
|
-
imported.
|
|
21
|
-
activate
|
|
40
|
+
Plugin tools are registered here when modules with @tool(tier="plugin")
|
|
41
|
+
are imported. Stored skills are discovered from disk on demand. The
|
|
42
|
+
search_plugins core tool queries this manifest to find and activate or
|
|
43
|
+
load capabilities.
|
|
22
44
|
"""
|
|
23
45
|
|
|
24
46
|
def __init__(self):
|
|
@@ -58,70 +80,77 @@ class PluginManifest:
|
|
|
58
80
|
"""
|
|
59
81
|
return list(self._plugins.values())
|
|
60
82
|
|
|
61
|
-
def
|
|
62
|
-
"""
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
83
|
+
def _iter_capabilities(self, category: str = None) -> Iterable[CapabilityMatch]:
|
|
84
|
+
"""Yield available plugin and skill capabilities for discovery surfaces."""
|
|
85
|
+
include_plugins = category in (None, "plugin")
|
|
86
|
+
include_skills = category in (None, "skill")
|
|
87
|
+
disabled_tools = set(tool_settings.disabled_tools or [])
|
|
88
|
+
hidden_skills = set(tool_settings.hidden_skills or [])
|
|
89
|
+
|
|
90
|
+
if include_plugins:
|
|
91
|
+
for tool_def in self._plugins.values():
|
|
92
|
+
if tool_def.name in disabled_tools:
|
|
93
|
+
continue
|
|
94
|
+
if category not in (None, "plugin") and tool_def.category != category:
|
|
95
|
+
continue
|
|
96
|
+
yield CapabilityMatch(
|
|
97
|
+
kind="plugin",
|
|
98
|
+
name=tool_def.name,
|
|
99
|
+
description=tool_def.description,
|
|
100
|
+
category=tool_def.category,
|
|
101
|
+
tags=list(tool_def.tags or []),
|
|
102
|
+
tool_def=tool_def,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if include_skills:
|
|
106
|
+
for summary in iter_skill_summaries():
|
|
107
|
+
if summary.name in hidden_skills:
|
|
108
|
+
continue
|
|
109
|
+
yield CapabilityMatch(
|
|
110
|
+
kind="skill",
|
|
111
|
+
name=summary.name,
|
|
112
|
+
description=summary.description or summary.preview,
|
|
113
|
+
category="skill",
|
|
114
|
+
tags=summary.tags or ["skill"],
|
|
115
|
+
preview=summary.preview,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
def search_capabilities(
|
|
119
|
+
self,
|
|
120
|
+
query: str,
|
|
121
|
+
category: str = None,
|
|
122
|
+
max_results: int = 5,
|
|
123
|
+
) -> List[CapabilityMatch]:
|
|
124
|
+
"""Search plugins and skills through one shared discovery path."""
|
|
125
|
+
combined_candidates = [
|
|
126
|
+
SearchCandidate(
|
|
127
|
+
item=capability,
|
|
128
|
+
text=" ".join(
|
|
129
|
+
part
|
|
130
|
+
for part in [
|
|
131
|
+
capability.name,
|
|
132
|
+
capability.description,
|
|
133
|
+
capability.category or "",
|
|
134
|
+
" ".join(capability.tags or []),
|
|
135
|
+
]
|
|
136
|
+
if part
|
|
137
|
+
),
|
|
138
|
+
compact_text="",
|
|
139
|
+
exact_text=capability.name,
|
|
140
|
+
)
|
|
141
|
+
for capability in self._iter_capabilities(category=category)
|
|
142
|
+
]
|
|
143
|
+
combined_matches = search_candidates(
|
|
144
|
+
query,
|
|
145
|
+
combined_candidates,
|
|
146
|
+
max_results=max_results,
|
|
147
|
+
item_key=lambda capability: f"{capability.kind}:{capability.name}",
|
|
148
|
+
)
|
|
149
|
+
return [match.item for match in combined_matches]
|
|
150
|
+
|
|
151
|
+
def list_all_capabilities(self, category: str = None) -> List[CapabilityMatch]:
|
|
152
|
+
"""Return all available capabilities without fuzzy scoring."""
|
|
153
|
+
return list(self._iter_capabilities(category=category))
|
|
125
154
|
|
|
126
155
|
def get_categories(self) -> List[str]:
|
|
127
156
|
"""Get all unique categories in the manifest.
|
|
@@ -172,7 +172,8 @@ def review_changes(
|
|
|
172
172
|
panel.set_complete({
|
|
173
173
|
'prompt_tokens': usage.get('prompt_tokens', 0),
|
|
174
174
|
'completion_tokens': usage.get('completion_tokens', 0),
|
|
175
|
-
'total_tokens': usage.get('total_tokens', 0)
|
|
175
|
+
'total_tokens': usage.get('total_tokens', 0),
|
|
176
|
+
'context_tokens': usage.get('context_tokens', 0),
|
|
176
177
|
})
|
|
177
178
|
|
|
178
179
|
raw_result = sub_agent_data.get('result', '')
|
package/src/tools/rg_search.py
CHANGED
|
@@ -5,7 +5,7 @@ import re
|
|
|
5
5
|
import stat
|
|
6
6
|
import subprocess
|
|
7
7
|
from pathlib import Path
|
|
8
|
-
from typing import Optional
|
|
8
|
+
from typing import Optional, Sequence
|
|
9
9
|
|
|
10
10
|
from .helpers.base import tool
|
|
11
11
|
from .helpers.formatters import format_tool_result
|
|
@@ -83,7 +83,7 @@ def _annotate_file_sizes(formatted_output: str, base_path: Path, output_mode: st
|
|
|
83
83
|
|
|
84
84
|
@tool(
|
|
85
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.",
|
|
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. Use 'path' for one file/directory or 'paths' for multiple files/directories; do not pass space-separated paths in 'path'.",
|
|
87
87
|
parameters={
|
|
88
88
|
"type": "object",
|
|
89
89
|
"properties": {
|
|
@@ -93,7 +93,12 @@ def _annotate_file_sizes(formatted_output: str, base_path: Path, output_mode: st
|
|
|
93
93
|
},
|
|
94
94
|
"path": {
|
|
95
95
|
"type": "string",
|
|
96
|
-
"description": "
|
|
96
|
+
"description": "Single file or directory to search (default: current directory). Do not pass multiple space-separated paths here; use 'paths' instead."
|
|
97
|
+
},
|
|
98
|
+
"paths": {
|
|
99
|
+
"type": "array",
|
|
100
|
+
"items": {"type": "string"},
|
|
101
|
+
"description": "Multiple files or directories to search. Use this instead of space-separated values in 'path'. If set, 'paths' overrides 'path'."
|
|
97
102
|
},
|
|
98
103
|
"glob": {
|
|
99
104
|
"type": "string",
|
|
@@ -138,6 +143,7 @@ def rg(
|
|
|
138
143
|
debug_mode: bool = False,
|
|
139
144
|
gitignore_spec = None,
|
|
140
145
|
path: Optional[str] = None,
|
|
146
|
+
paths: Optional[Sequence[str]] = None,
|
|
141
147
|
glob: Optional[str] = None,
|
|
142
148
|
output_mode: str = "files_with_matches",
|
|
143
149
|
vault_root: Optional[str] = None,
|
|
@@ -153,7 +159,8 @@ def rg(
|
|
|
153
159
|
chat_manager: ChatManager instance (injected by context)
|
|
154
160
|
debug_mode: Whether debug mode is enabled (injected by context)
|
|
155
161
|
gitignore_spec: PathSpec for .gitignore filtering (injected by context)
|
|
156
|
-
path:
|
|
162
|
+
path: Single file or directory to search in (default: current directory)
|
|
163
|
+
paths: Multiple files or directories to search; overrides path when set
|
|
157
164
|
glob: Glob pattern to filter files
|
|
158
165
|
output_mode: Output mode (content/files_with_matches/count)
|
|
159
166
|
vault_root: Obsidian vault root path (injected by context)
|
|
@@ -204,11 +211,21 @@ def rg(
|
|
|
204
211
|
elif output_mode == "count":
|
|
205
212
|
args.append("--count")
|
|
206
213
|
|
|
207
|
-
# Pattern and search
|
|
214
|
+
# Pattern and search paths — no quoting needed, subprocess list form bypasses shell
|
|
208
215
|
args.append(pattern)
|
|
209
216
|
|
|
210
|
-
|
|
211
|
-
|
|
217
|
+
if paths is not None:
|
|
218
|
+
if not isinstance(paths, Sequence) or isinstance(paths, (str, bytes)):
|
|
219
|
+
return "exit_code=1\nrg 'paths' must be an array of path strings. Use 'path' for one path."
|
|
220
|
+
if not paths:
|
|
221
|
+
return "exit_code=1\nrg 'paths' must be a non-empty array. Omit 'paths' to search the current directory."
|
|
222
|
+
search_paths = [p for p in paths if isinstance(p, str) and p.strip()]
|
|
223
|
+
if len(search_paths) != len(paths):
|
|
224
|
+
return "exit_code=1\nrg 'paths' must contain only non-empty strings."
|
|
225
|
+
else:
|
|
226
|
+
search_paths = [path or "."]
|
|
227
|
+
|
|
228
|
+
args.extend(search_paths)
|
|
212
229
|
|
|
213
230
|
# Get max_matches from kwargs (default: 100, set to 0 for no limit)
|
|
214
231
|
raw = coerce_int(kwargs.get("max_matches"))[0] if kwargs.get("max_matches") is not None else None
|
|
@@ -1,38 +1,41 @@
|
|
|
1
|
-
"""search_plugins core tool for on-demand
|
|
1
|
+
"""search_plugins core tool for on-demand capability discovery.
|
|
2
2
|
|
|
3
3
|
This tool lets the LLM agent search for available plugin tools and
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
stored skills, then explicitly activate plugins or load skills through
|
|
5
|
+
the same entrypoint. Plugin schemas are not sent by default to avoid
|
|
6
|
+
context bloat — they are only included after activation.
|
|
6
7
|
"""
|
|
7
8
|
|
|
8
|
-
from typing import List, Optional
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
|
|
11
9
|
from tools.helpers.base import tool, ToolRegistry, TERMINAL_NONE
|
|
12
10
|
|
|
11
|
+
HEADER_MATCHES = "Capability matches for: "
|
|
12
|
+
HEADER_ALL = "All available capabilities"
|
|
13
|
+
|
|
13
14
|
|
|
14
15
|
@tool(
|
|
15
16
|
name="search_plugins",
|
|
16
17
|
description=(
|
|
17
|
-
"Search for available plugin tools that can help
|
|
18
|
-
"
|
|
19
|
-
"to discover and activate them.
|
|
20
|
-
"
|
|
21
|
-
"available in your
|
|
18
|
+
"Search for available plugin tools and saved skills that can help "
|
|
19
|
+
"with your task. Plugins are NOT in your available tools by default "
|
|
20
|
+
"— use this to discover and activate them. Skills can also be loaded "
|
|
21
|
+
"through this same tool by passing explicit capability names in 'load'. "
|
|
22
|
+
"Once a plugin is activated, its full schema will be available in your "
|
|
23
|
+
"next response."
|
|
22
24
|
),
|
|
23
25
|
parameters={
|
|
24
26
|
"type": "object",
|
|
25
27
|
"properties": {
|
|
26
28
|
"query": {
|
|
27
29
|
"type": "string",
|
|
28
|
-
"description": "Search query describing what you need (e.g., 'send email', 'query database', 'http request')"
|
|
30
|
+
"description": "Search query describing what you need (e.g., 'send email', 'query database', 'http request'). Omit to list all available plugins and skills."
|
|
29
31
|
},
|
|
30
|
-
"
|
|
31
|
-
"type": "
|
|
32
|
-
"
|
|
32
|
+
"load": {
|
|
33
|
+
"type": "array",
|
|
34
|
+
"items": {"type": "string"},
|
|
35
|
+
"description": "Optional list of exact capability names from the current search results to activate or load. Plugins are activated; skills are injected into the current chat."
|
|
33
36
|
}
|
|
34
37
|
},
|
|
35
|
-
"required": [
|
|
38
|
+
"required": []
|
|
36
39
|
},
|
|
37
40
|
requires_approval=False,
|
|
38
41
|
terminal_policy=TERMINAL_NONE,
|
|
@@ -41,69 +44,134 @@ from tools.helpers.base import tool, ToolRegistry, TERMINAL_NONE
|
|
|
41
44
|
category="core"
|
|
42
45
|
)
|
|
43
46
|
def search_plugins(
|
|
44
|
-
query: str,
|
|
45
|
-
|
|
47
|
+
query: str = "",
|
|
48
|
+
load: list[str] | None = None,
|
|
49
|
+
chat_manager=None,
|
|
46
50
|
) -> str:
|
|
47
|
-
"""Search
|
|
48
|
-
|
|
49
|
-
Args:
|
|
50
|
-
query: Search query describing the needed capability
|
|
51
|
-
category: Optional category filter
|
|
51
|
+
"""Search discoverable capabilities and optionally activate/load selected matches.
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
"""
|
|
53
|
+
When using `load`, `query` must be provided so selections come from the current search results."""
|
|
54
|
+
from core.skills import SkillError, activate_skill, validate_skill_name
|
|
56
55
|
from tools.helpers.plugin_manifest import plugin_manifest
|
|
57
56
|
|
|
58
|
-
# Check if any core tool matches the query — early return
|
|
59
57
|
core_tools = ToolRegistry.get_all(include_plugins=False)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
#
|
|
70
|
-
|
|
58
|
+
core_tool_note = None
|
|
59
|
+
query = query.strip()
|
|
60
|
+
|
|
61
|
+
if load and not query:
|
|
62
|
+
return "\n".join([
|
|
63
|
+
"exit_code=1",
|
|
64
|
+
"Loading capabilities requires a query so selections come from the current search results.",
|
|
65
|
+
])
|
|
66
|
+
|
|
67
|
+
# No query → list everything
|
|
68
|
+
if not query:
|
|
69
|
+
matches = plugin_manifest.list_all_capabilities()
|
|
70
|
+
else:
|
|
71
|
+
query_lower = query.lower()
|
|
72
|
+
for ct in core_tools:
|
|
73
|
+
if query_lower == ct.name.lower():
|
|
74
|
+
core_tool_note = (
|
|
75
|
+
f"Core tool already available: {ct.name}\n"
|
|
76
|
+
f" {ct.description}"
|
|
77
|
+
)
|
|
78
|
+
break
|
|
79
|
+
|
|
80
|
+
matches = plugin_manifest.search_capabilities(query, max_results=10)
|
|
71
81
|
|
|
72
82
|
if not matches:
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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)
|
|
83
|
+
lines = ["exit_code=0"]
|
|
84
|
+
if core_tool_note:
|
|
85
|
+
lines.extend([core_tool_note, ""])
|
|
86
|
+
if query:
|
|
87
|
+
lines.append(f"No matches for: {query}")
|
|
95
88
|
else:
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
89
|
+
lines.append("No plugins or skills available.")
|
|
90
|
+
return "\n".join(lines)
|
|
91
|
+
|
|
92
|
+
requested = [name for name in (load or []) if isinstance(name, str) and name.strip()]
|
|
93
|
+
requested_normalized = {name.strip().lower(): name.strip() for name in requested}
|
|
94
|
+
matched_by_name = {match.name.lower(): match for match in matches}
|
|
95
|
+
|
|
96
|
+
plugin_count = 0
|
|
97
|
+
skill_count = 0
|
|
98
|
+
loaded_plugins = []
|
|
99
|
+
loaded_skills = []
|
|
100
|
+
load_errors = []
|
|
101
|
+
|
|
102
|
+
for match in matches:
|
|
103
|
+
if match.kind == "plugin" and match.tool_def:
|
|
104
|
+
plugin_count += 1
|
|
105
|
+
if ToolRegistry.is_plugin_active(match.tool_def.name):
|
|
106
|
+
match.already_active = True
|
|
107
|
+
if match.name.lower() in requested_normalized and not match.already_active:
|
|
108
|
+
if ToolRegistry.activate_plugin(match.tool_def):
|
|
109
|
+
match.activated = True
|
|
110
|
+
loaded_plugins.append(match.name)
|
|
111
|
+
else:
|
|
112
|
+
load_errors.append(f"Plugin '{match.name}' is disabled. Enable it before loading.")
|
|
113
|
+
continue
|
|
114
|
+
skill_count += 1
|
|
115
|
+
if match.name.lower() in requested_normalized:
|
|
116
|
+
if chat_manager is None:
|
|
117
|
+
load_errors.append(f"Skill '{match.name}' cannot be loaded without an active chat.")
|
|
118
|
+
continue
|
|
119
|
+
try:
|
|
120
|
+
skill_name = validate_skill_name(match.name)
|
|
121
|
+
activate_skill(chat_manager, skill_name)
|
|
122
|
+
loaded_skills.append(skill_name)
|
|
123
|
+
except SkillError as exc:
|
|
124
|
+
load_errors.append(str(exc))
|
|
125
|
+
|
|
126
|
+
missing_requested = [
|
|
127
|
+
original_name
|
|
128
|
+
for normalized_name, original_name in requested_normalized.items()
|
|
129
|
+
if normalized_name not in matched_by_name
|
|
130
|
+
]
|
|
131
|
+
for missing in missing_requested:
|
|
132
|
+
load_errors.append(f"Capability '{missing}' was not found in the current search results.")
|
|
133
|
+
|
|
134
|
+
lines = ["exit_code=0"]
|
|
135
|
+
if core_tool_note:
|
|
136
|
+
lines.extend([core_tool_note, ""])
|
|
137
|
+
|
|
138
|
+
if query:
|
|
139
|
+
lines.extend([
|
|
140
|
+
f"{HEADER_MATCHES}{query}",
|
|
141
|
+
f"Results: {len(matches)} total ({plugin_count} plugin, {skill_count} skill)",
|
|
142
|
+
"",
|
|
143
|
+
])
|
|
144
|
+
else:
|
|
145
|
+
lines.extend([
|
|
146
|
+
HEADER_ALL,
|
|
147
|
+
f"Total: {len(matches)} ({plugin_count} plugin, {skill_count} skill)",
|
|
148
|
+
"",
|
|
149
|
+
])
|
|
150
|
+
|
|
151
|
+
for match in matches:
|
|
152
|
+
if match.kind == "plugin":
|
|
153
|
+
status = "disabled" if ToolRegistry.is_disabled(match.name) else "activated" if match.activated else "active" if match.already_active else "available"
|
|
154
|
+
lines.append(f"- {match.name}")
|
|
155
|
+
lines.append(" type: plugin")
|
|
156
|
+
lines.append(f" status: {status}")
|
|
157
|
+
lines.append(f" summary: {match.description}")
|
|
158
|
+
if match.tags:
|
|
159
|
+
lines.append(f" tags: {', '.join(match.tags)}")
|
|
160
|
+
continue
|
|
161
|
+
|
|
162
|
+
lines.append(f"- {match.name}")
|
|
163
|
+
lines.append(" type: skill")
|
|
164
|
+
lines.append(f" summary: {match.description}")
|
|
165
|
+
if match.tags:
|
|
166
|
+
lines.append(f" tags: {', '.join(match.tags)}")
|
|
167
|
+
|
|
168
|
+
if requested:
|
|
169
|
+
lines.append("")
|
|
170
|
+
if loaded_plugins:
|
|
171
|
+
lines.append(f"Activated plugins: {', '.join(loaded_plugins)}")
|
|
172
|
+
if loaded_skills:
|
|
173
|
+
lines.append(f"Loaded skills: {', '.join(loaded_skills)}")
|
|
174
|
+
if load_errors:
|
|
175
|
+
lines.append(f"Load issues: {'; '.join(load_errors)}")
|
|
108
176
|
|
|
109
177
|
return "\n".join(lines)
|
package/src/tools/shell.py
CHANGED
|
@@ -231,13 +231,13 @@ def run_shell_command(command, repo_root, rg_exe_path, console, debug_mode, giti
|
|
|
231
231
|
|
|
232
232
|
@tool(
|
|
233
233
|
name="execute_command",
|
|
234
|
-
description="Execute shell commands for git, system tasks, file ops, network, and package management. Runs from repo root. Use for git, ps, systemctl, rm, mv, cp, mkdir, ping, curl, wget, ssh, pacman, pip, npm, apt. Disallowed: rg, cat, ls, grep, find, head, tail, sed, awk, sort, uniq, wc, echo, touch, get-content, type, get-childitem, dir, new-item, set-content, add-content, tee. Use native tools instead.",
|
|
234
|
+
description="Execute shell commands for git, system tasks, file ops, network, and package management. Runs from repo root. Multi-line shell commands are supported and preserved exactly, including heredocs. Use for git, ps, systemctl, rm, mv, cp, mkdir, ping, curl, wget, ssh, pacman, pip, npm, apt. Disallowed: rg, cat, ls, grep, find, head, tail, sed, awk, sort, uniq, wc, echo, touch, get-content, type, get-childitem, dir, new-item, set-content, add-content, tee. Use native tools instead.",
|
|
235
235
|
parameters={
|
|
236
236
|
"type": "object",
|
|
237
237
|
"properties": {
|
|
238
238
|
"command": {
|
|
239
239
|
"type": "string",
|
|
240
|
-
"description": "
|
|
240
|
+
"description": "Shell command to execute from the repo root. May be a single-line command or a multi-line shell script/heredoc; newlines are preserved exactly."
|
|
241
241
|
},
|
|
242
242
|
"reason": {
|
|
243
243
|
"type": "string",
|
|
@@ -262,7 +262,7 @@ def execute_command(
|
|
|
262
262
|
"""Execute a shell command.
|
|
263
263
|
|
|
264
264
|
Args:
|
|
265
|
-
command: Command string to execute
|
|
265
|
+
command: Command string to execute. May contain newlines/heredocs; preserved exactly for shell execution.
|
|
266
266
|
repo_root: Repository root directory (injected by context)
|
|
267
267
|
rg_exe_path: Path to rg executable (injected by context)
|
|
268
268
|
console: Rich console for output (injected by context)
|