Enhance skills management by integrating project-scoped skills and updating skill search functionality. Adjusted skills tool to utilize project context for skill discovery and refined the skills loading process. Updated documentation to reflect changes in skill operations and removed deprecated script execution methods. Uses code_execution_tool for skill scripts instead of execute_scripts from the skills_tool.
TerminallyLazy committed
Jan 31, 2026 at 04:06 UTC
3830c64be05c4ebd2c6314d42cef281171838313
9 files changed
+175
-391
prompts/agent.system.tool.skills.md
+55
-213
@@ -3,14 +3,14 @@
3
manage and use agent skills for specialized capabilities
4
skills are composable bundles of instructions context and executable code
5
use progressive disclosure: metadata → full content → referenced files
6
-use "method" arg to specify operation: "list" "load" "read_file" "execute_script" "search"
6
+use "method" arg to specify operation: "list" "load" "read_file" "search"
7
8
## Overview
9
10
Skills system provides three-level progressive disclosure:
11
- Level 1: Metadata (name + description) loaded in system prompt at startup
12
- Level 2: Full SKILL.md content loaded when relevant to task
13
-- Level 3+: Referenced files and scripts loaded on-demand
13
+- Level 3+: Referenced files loaded on-demand
14
15
When to use skills:
16
- Task matches skill description from available skills list
@@ -23,7 +23,7 @@ Progressive workflow:
23
2. Use "search" if looking for specific capability
24
3. Use "load" to get full skill instructions and context
25
4. Use "read_file" to load additional reference documents
26
-5. Use "execute_script" to run deterministic operations
26
+5. Use code_execution_tool to run any scripts referenced by the skill
27
28
## Operations
29
@@ -116,112 +116,79 @@ Security:
116
- Only files within skill directory accessible
117
- Supports markdown, text, code files
118
119
-### 4. execute skill script
119
+### 4. search skills by query
120
121
-Executes bundled scripts from skill with arguments
122
-Scripts receive arguments via standard CLI conventions (sys.argv, process.argv)
123
-Use when: skill provides script for deterministic operation or automation
121
+Searches skills by text matching in name, description, and tags
122
+Returns ranked results by relevance score
123
+Use when: looking for skills without knowing exact name
124
125
~~~json
126
{
127
"thoughts": [
128
- "Need to convert PDF to images",
129
- "Skill provides convert_pdf_to_images.py script",
130
- "Script expects positional args: input_pdf output_dir"
128
+ "User needs web scraping capability",
129
+ "Not sure of exact skill name",
130
+ "Searching for web-related skills"
131
],
132
- "headline": "Converting PDF to images",
132
+ "headline": "Searching for web scraping skills",
133
"tool_name": "skills_tool",
134
"tool_args": {
135
- "method": "execute_script",
136
- "skill_name": "pdf_editing",
137
- "script_path": "scripts/convert_pdf_to_images.py",
138
- "script_args": {
139
- "input_pdf": "/path/to/document.pdf",
140
- "output_dir": "/tmp/images"
141
- }
135
+ "method": "search",
136
+ "query": "web scraping html parsing"
137
}
138
}
139
~~~
140
141
Required args:
147
-- skill_name: name of skill containing script
148
-- script_path: relative path to script file
149
-- script_args: dictionary of arguments passed to script
150
-
151
-Optional args:
152
-- arg_style: how to pass arguments to script (default: "positional")
153
- - "positional": values as positional args → sys.argv = ['script.py', 'value1', 'value2']
154
- - "named": as --key value pairs → sys.argv = ['script.py', '--key1', 'value1', '--key2', 'value2']
155
- - "env": only environment variables, no CLI args
156
-
157
-How scripts receive arguments:
158
-- .py (Python): sys.argv[1], sys.argv[2], etc. (standard argparse/CLI compatible)
159
-- .js (Node.js): process.argv[2], process.argv[3], etc. (standard CLI compatible)
160
-- .sh (Shell): $1, $2, etc. as positional parameters
161
-
162
-Environment variables (always available as fallback):
163
-- SKILL_ARG_KEY1=value1, SKILL_ARG_KEY2=value2, etc.
164
-- Scripts can use os.environ.get('SKILL_ARG_INPUT_PDF') if needed
165
-
166
-Script execution:
167
-- Runs in Docker container sandbox
168
-- Has access to installed packages
169
-- Returns stdout/stderr output
170
-- Secure and isolated execution
171
-
172
-Example with argparse script (use arg_style="named"):
142
+- query: search text (searches name, description, tags)
143
+
144
+Scoring:
145
+- Name match: +3 points
146
+- Description match: +2 points
147
+- Tag match: +1 point per tag
148
+- Results sorted by descending score
149
+
150
+## Running skill scripts
151
+
152
+When a skill includes scripts (listed under its files), use code_execution_tool directly to run them.
153
+The skill's "load" output shows the skill directory path and lists available scripts.
154
+Use read_file to inspect a script before running it if needed.
155
+
156
+Example: running a Python script from a skill
157
+1. Load the skill to get its path and script list
158
+2. Use code_execution_tool with runtime="python" to run the script
159
+
160
~~~json
161
{
162
"thoughts": [
176
- "Script uses argparse with --input and --output flags",
177
- "Need to use named arg_style"
163
+ "Need to convert PDF to images",
164
+ "Skill provides convert_pdf_to_images.py at scripts/convert_pdf_to_images.py",
165
+ "Using code_execution_tool to run it directly"
166
],
179
- "headline": "Running argparse-based script",
180
- "tool_name": "skills_tool",
167
+ "headline": "Converting PDF to images",
168
+ "tool_name": "code_execution_tool",
169
"tool_args": {
182
- "method": "execute_script",
183
- "skill_name": "data_processor",
184
- "script_path": "scripts/process.py",
185
- "script_args": {
186
- "input": "/path/to/data.csv",
187
- "output": "/tmp/result.json"
188
- },
189
- "arg_style": "named"
170
+ "runtime": "python",
171
+ "code": "import subprocess\nsubprocess.run(['python', '/path/to/skill/scripts/convert_pdf_to_images.py', '/path/to/document.pdf', '/tmp/images'], check=True)"
172
}
173
}
174
~~~
175
194
-### 5. search skills by query
195
-
196
-Searches skills by text matching in name, description, and tags
197
-Returns ranked results by relevance score
198
-Use when: looking for skills without knowing exact name
199
-
176
+Example: running a shell script from a skill
177
~~~json
178
{
179
"thoughts": [
203
- "User needs web scraping capability",
204
- "Not sure of exact skill name",
205
- "Searching for web-related skills"
180
+ "Skill provides a shell script for data processing",
181
+ "Running it via terminal runtime"
182
],
207
- "headline": "Searching for web scraping skills",
208
- "tool_name": "skills_tool",
183
+ "headline": "Running data processing script",
184
+ "tool_name": "code_execution_tool",
185
"tool_args": {
210
- "method": "search",
211
- "query": "web scraping html parsing"
186
+ "runtime": "terminal",
187
+ "code": "cd /path/to/skill && bash scripts/process.sh /data/input.csv /tmp/output"
188
}
189
}
190
~~~
191
216
-Required args:
217
-- query: search text (searches name, description, tags)
218
-
219
-Scoring:
220
-- Name match: +3 points
221
-- Description match: +2 points
222
-- Tag match: +1 point per tag
223
-- Results sorted by descending score
224
-
192
## Best Practices
193
194
### When to use skills vs other tools
@@ -229,13 +196,15 @@ Scoring:
196
Use skills when:
197
- Task requires specialized domain knowledge
198
- Need structured procedures or step-by-step guidance
232
-- Deterministic scripts available for automation
199
- Complex multi-step operations with best practices
200
201
+Use code_execution_tool directly when:
202
+- Running skill scripts (load skill first to get paths)
203
+- Simple file operations
204
+- General computation
205
+
206
Use other tools when:
236
-- Simple file operations (use code_execution_tool)
207
- Web search (use search_engine)
238
-- General computation (use code_execution_tool)
208
- Memory operations (use memory tools)
209
210
### Progressive disclosure workflow
@@ -252,9 +221,9 @@ Use other tools when:
221
- Use "read_file" for detailed documentation
222
- Load only files relevant to current subtask
223
255
-4. Execute scripts for automation
256
- - Use "execute_script" for deterministic operations
257
- - Provide appropriate arguments from context
224
+4. Execute scripts via code_execution_tool
225
+ - Use skill path from "load" output
226
+ - Run scripts directly with code_execution_tool
227
228
### Common patterns
229
@@ -263,12 +232,7 @@ Pattern: Using a skill for first time
232
2. Load full skill content
233
3. Follow instructions in content
234
4. Load reference files if mentioned
266
-5. Execute scripts if provided
267
-
268
-Pattern: Quick script execution
269
-1. Know skill name from previous use
270
-2. Execute script directly with args
271
-3. Process output
235
+5. Run scripts via code_execution_tool using paths from load output
236
237
Pattern: Exploring capabilities
238
1. Search with query terms
@@ -280,140 +244,18 @@ Pattern: Exploring capabilities
244
Common errors:
245
- "Skill not found": Check spelling, use list or search to find correct name
246
- "File not found": Verify file_path matches referenced files from load output
283
-- "Script failed": Check script_args match expected parameters, review skill docs
284
-- "Unsupported script type": Only .py, .js, .sh supported
247
248
When skill loading fails:
249
- Verify skill exists using list method
250
- Check for typos in skill_name
251
- Ensure skill system is enabled in settings
252
291
-When script execution fails:
292
-- Review skill documentation for required arguments
293
-- Check script_args dictionary format
294
-- Verify required packages installed in container
295
-- Check script output for specific error messages
296
-
297
-## Examples
298
-
299
-Example 1: Simple script with positional args (default)
300
-Script expects: python script.py /path/to/file.pdf
301
-~~~json
302
-{
303
- "thoughts": [
304
- "User has PDF to convert to images",
305
- "Script uses sys.argv[1] for input, sys.argv[2] for output",
306
- "Using default positional arg_style"
307
- ],
308
- "headline": "Converting PDF to images",
309
- "tool_name": "skills_tool",
310
- "tool_args": {
311
- "method": "execute_script",
312
- "skill_name": "pdf_editing",
313
- "script_path": "scripts/convert_pdf_to_images.py",
314
- "script_args": {
315
- "input_pdf": "/workspace/document.pdf",
316
- "output_dir": "/tmp/images"
317
- }
318
- }
319
-}
320
-~~~
321
-Result: sys.argv = ['script.py', '/workspace/document.pdf', '/tmp/images']
322
-
323
-Example 2: Argparse script with named args
324
-Script expects: python script.py --url https://... --selector .price
325
-~~~json
326
-{
327
- "thoughts": [
328
- "Need to scrape product prices from website",
329
- "Script uses argparse with --url and --selector flags",
330
- "Using arg_style='named' for argparse compatibility"
331
- ],
332
- "headline": "Scraping product prices from webpage",
333
- "tool_name": "skills_tool",
334
- "tool_args": {
335
- "method": "execute_script",
336
- "skill_name": "web_scraping",
337
- "script_path": "scripts/fetch_page.py",
338
- "script_args": {
339
- "url": "https://example.com/products",
340
- "selector": ".price"
341
- },
342
- "arg_style": "named"
343
- }
344
-}
345
-~~~
346
-Result: sys.argv = ['script.py', '--url', 'https://...', '--selector', '.price']
347
-
348
-Example 3: Environment-only script
349
-Script reads from os.environ only
350
-~~~json
351
-{
352
- "thoughts": [
353
- "Script reads configuration from environment variables",
354
- "Using arg_style='env' to only set env vars"
355
- ],
356
- "headline": "Running config-based processor",
357
- "tool_name": "skills_tool",
358
- "tool_args": {
359
- "method": "execute_script",
360
- "skill_name": "data_processor",
361
- "script_path": "scripts/process.py",
362
- "script_args": {
363
- "input_file": "/data/input.csv",
364
- "mode": "production"
365
- },
366
- "arg_style": "env"
367
- }
368
-}
369
-~~~
370
-Result: SKILL_ARG_INPUT_FILE=/data/input.csv, SKILL_ARG_MODE=production
371
-
372
-Example 4: Data analysis workflow
373
-~~~json
374
-{
375
- "thoughts": [
376
- "User needs CSV analysis",
377
- "data_analysis skill has analysis procedures",
378
- "Loading skill for detailed instructions"
379
- ],
380
- "headline": "Loading data analysis skill",
381
- "tool_name": "skills_tool",
382
- "tool_args": {
383
- "method": "load",
384
- "skill_name": "data_analysis"
385
- }
386
-}
387
-~~~
388
-
389
-Then follow up with script (positional args):
390
-~~~json
391
-{
392
- "thoughts": [
393
- "Skill loaded, now analyzing CSV",
394
- "Script takes csv_path as first arg, group_by as second"
395
- ],
396
- "headline": "Analyzing sales data grouped by category",
397
- "tool_name": "skills_tool",
398
- "tool_args": {
399
- "method": "execute_script",
400
- "skill_name": "data_analysis",
401
- "script_path": "scripts/analyze_csv.py",
402
- "script_args": {
403
- "csv_path": "/workspace/sales_data.csv",
404
- "group_by": "category"
405
- }
406
- }
407
-}
408
-~~~
409
-
253
## Notes
254
255
- Skills metadata already loaded in your system prompt
256
- Skills cache after first load for efficiency
257
- Referenced files listed in load response
415
-- Scripts receive arguments via sys.argv (positional by default) + SKILL_ARG_* env vars
416
-- Use arg_style parameter to control argument passing: "positional", "named", or "env"
258
+- Use code_execution_tool to run any scripts provided by skills
259
- All operations return formatted text responses
260
- Skills follow the open SKILL.md standard (cross-platform compatible)
261
- Use skills for structured procedures and contextual expertise
python/extensions/message_loop_prompts_after/_55_recall_skills.py
+15
-3
@@ -6,6 +6,7 @@ from agent import LoopData
6
from python.helpers.memory import Memory
7
from python.helpers import files
8
from python.helpers import skills as skills_helper
9
+from python.helpers import projects
10
11
12
class RecallSkills(Extension):
@@ -29,6 +30,9 @@ class RecallSkills(Extension):
30
if not user_instruction or len(user_instruction) < 8:
31
return
32
33
+ # Get active project for project-scoped skill discovery
34
+ project_name = projects.get_context_project_name(self.agent.context) if self.agent.context else None
35
+
36
try:
37
db = await Memory.get(self.agent)
38
docs = await db.search_similarity_threshold(
@@ -56,8 +60,8 @@ class RecallSkills(Extension):
60
break
61
62
if not recalled:
59
- # cheap lexical fallback
60
- matches = skills_helper.search_skills(user_instruction, limit=6)
63
+ # cheap lexical fallback (includes project skills when project is active)
64
+ matches = skills_helper.search_skills(user_instruction, limit=6, project_name=project_name)
65
for s in matches:
66
recalled.append(str(s.skill_md_path))
67
@@ -75,7 +79,7 @@ class RecallSkills(Extension):
79
text = abs_path.read_text(encoding="utf-8", errors="replace")
80
fm, body = skills_helper.split_frontmatter(text)
81
78
- # Infer source if possible (custom/builtin/shared), else "unknown"
82
+ # Infer source if possible (custom/builtin/shared/project), else "unknown"
83
source = "unknown"
84
try:
85
rel = abs_path.resolve().relative_to(base_skills_dir)
@@ -83,6 +87,14 @@ class RecallSkills(Extension):
87
source = rel.parts[0]
88
except Exception:
89
pass
90
+ if source == "unknown" and project_name:
91
+ try:
92
+ from python.helpers.skills_import import get_project_skills_folder
93
+ proj_skills = get_project_skills_folder(project_name)
94
+ abs_path.resolve().relative_to(proj_skills.resolve())
95
+ source = "project"
96
+ except Exception:
97
+ pass
98
99
name = str(fm.get("name") or abs_path.parent.name).strip()
100
desc = str(fm.get("description") or "").strip()
python/helpers/memory.py
+25
-8
@@ -335,15 +335,32 @@ class Memory:
335
filename_pattern="**/SKILL.md",
336
)
337
338
+ # load project-scoped skills from all projects
339
+ try:
340
+ from python.helpers.skills_import import get_project_skills_folder
341
+ from python.helpers import projects as projects_helper
342
+ for proj in projects_helper.get_active_projects_list():
343
+ proj_skills_path = str(get_project_skills_folder(proj["name"]))
344
+ if os.path.isdir(proj_skills_path):
345
+ index = knowledge_import.load_knowledge(
346
+ log_item,
347
+ proj_skills_path,
348
+ index,
349
+ {"area": Memory.Area.SKILLS.value},
350
+ filename_pattern="**/SKILL.md",
351
+ )
352
+ except Exception:
353
+ pass
354
+
355
# load custom instruments descriptions
339
- index = knowledge_import.load_knowledge(
340
- log_item,
341
- files.get_abs_path("usr/instruments"),
342
- index,
343
- {"area": Memory.Area.INSTRUMENTS.value},
344
- filename_pattern="**/*.md",
345
- recursive=True,
346
- )
356
+ # index = knowledge_import.load_knowledge(
357
+ # log_item,
358
+ # files.get_abs_path("usr/instruments"),
359
+ # index,
360
+ # {"area": Memory.Area.INSTRUMENTS.value},
361
+ # filename_pattern="**/*.md",
362
+ # recursive=True,
363
+ # )
364
365
return index
366
python/helpers/migration.py
+3
-3
@@ -19,7 +19,7 @@ def migrate_user_data() -> None:
19
_move_dir("tmp/downloads", "usr/downloads")
20
_move_dir("tmp/email", "usr/email")
21
_move_dir("knowledge/custom", "usr/knowledge", overwrite=True)
22
- _move_dir("instruments/custom", "usr/instruments", overwrite=True)
22
+ _move_dir("skills/custom", "usr/skills", overwrite=True)
23
24
# --- Migrate Files -------------------------------------------------------------
25
# Move specific configuration files to usr/
@@ -37,7 +37,7 @@ def migrate_user_data() -> None:
37
# We use _merge_dir_contents because we want to move the *contents* of default/
38
# into the parent directory, not move the default directory itself.
39
_merge_dir_contents("knowledge/default", "knowledge")
40
- _merge_dir_contents("instruments/default", "instruments")
40
+ _merge_dir_contents("skills/default", "skills")
41
42
# --- Cleanup -------------------------------------------------------------------
43
@@ -103,7 +103,7 @@ def _cleanup_obsolete() -> None:
103
"""
104
to_remove = [
105
"knowledge/default",
106
- "instruments/default",
106
+ "skills/default",
107
"memory"
108
]
109
for path in to_remove:
python/helpers/skills.py
+24
-7
@@ -14,7 +14,7 @@ except Exception: # pragma: no cover
14
yaml = None # type: ignore
15
16
17
-SkillSource = Literal["custom", "builtin", "shared"]
17
+SkillSource = Literal["custom", "builtin", "shared", "project"]
18
19
20
@dataclass(slots=True)
@@ -42,10 +42,25 @@ def get_skills_base_dir() -> Path:
42
return Path(files.get_abs_path("skills"))
43
44
45
-def get_skill_roots(order: Optional[List[SkillSource]] = None) -> List[Tuple[SkillSource, Path]]:
45
+def get_skill_roots(
46
+ order: Optional[List[SkillSource]] = None,
47
+ project_name: Optional[str] = None,
48
+) -> List[Tuple[SkillSource, Path]]:
49
base = get_skills_base_dir()
50
order = order or ["custom", "builtin", "shared"]
48
- return [(src, base / src) for src in order]
51
+ roots: List[Tuple[SkillSource, Path]] = [(src, base / src) for src in order]
52
+
53
+ # Include project-scoped skills if a project is active
54
+ if project_name:
55
+ try:
56
+ from python.helpers.skills_import import get_project_skills_folder
57
+ project_skills = get_project_skills_folder(project_name)
58
+ if project_skills.exists():
59
+ roots.insert(0, ("project", project_skills))
60
+ except Exception:
61
+ pass
62
+
63
+ return roots
64
65
66
def _is_hidden_path(path: Path) -> bool:
@@ -254,10 +269,11 @@ def list_skills(
269
include_content: bool = False,
270
dedupe: bool = True,
271
root_order: Optional[List[SkillSource]] = None,
272
+ project_name: Optional[str] = None,
273
) -> List[Skill]:
274
skills: List[Skill] = []
275
260
- roots = get_skill_roots(order=root_order)
276
+ roots = get_skill_roots(order=root_order, project_name=project_name)
277
for source, root in roots:
278
for skill_md in discover_skill_md_files(root):
279
s = skill_from_markdown(skill_md, source, include_content=include_content)
@@ -281,12 +297,13 @@ def find_skill(
297
*,
298
include_content: bool = False,
299
root_order: Optional[List[SkillSource]] = None,
300
+ project_name: Optional[str] = None,
301
) -> Optional[Skill]:
302
target = _normalize_name(skill_name)
303
if not target:
304
return None
305
289
- roots = get_skill_roots(order=root_order)
306
+ roots = get_skill_roots(order=root_order, project_name=project_name)
307
for source, root in roots:
308
for skill_md in discover_skill_md_files(root):
309
s = skill_from_markdown(skill_md, source, include_content=include_content)
@@ -297,13 +314,13 @@ def find_skill(
314
return None
315
316
300
-def search_skills(query: str, *, limit: int = 25) -> List[Skill]:
317
+def search_skills(query: str, *, limit: int = 25, project_name: Optional[str] = None) -> List[Skill]:
318
q = (query or "").strip().lower()
319
if not q:
320
return []
321
322
terms = [t for t in re.split(r"\s+", q) if t]
306
- candidates = list_skills(include_content=False, dedupe=True)
323
+ candidates = list_skills(include_content=False, dedupe=True, project_name=project_name)
324
325
scored: List[Tuple[int, Skill]] = []
326
for s in candidates:
python/tools/skills_tool.py
+12
-155
@@ -1,13 +1,11 @@
1
from __future__ import annotations
2
3
-import json
4
-import re
5
-import shlex
3
from pathlib import Path
7
-from typing import Any, Dict, List
4
+from typing import List
5
6
from python.helpers.tool import Tool, Response
7
from python.helpers import files
8
+from python.helpers import projects
9
from python.helpers import skills as skills_helper
10
11
@@ -20,18 +18,14 @@ class SkillsTool(Tool):
18
- search (query)
19
- load (skill_name)
20
- read_file (skill_name, file_path)
23
- - execute_script (skill_name, script_path, script_args, arg_style)
21
25
- arg_style options for execute_script:
26
- - "positional" (default): Pass values as positional args (sys.argv[1], sys.argv[2])
27
- Example: {"input": "file.pdf", "output": "/tmp"} → sys.argv = ['script.py', 'file.pdf', '/tmp']
28
- - "named": Pass as --key value pairs (for argparse/click scripts)
29
- Example: {"input": "file.pdf", "output": "/tmp"} → sys.argv = ['script.py', '--input', 'file.pdf', '--output', '/tmp']
30
- - "env": Only use environment variables (SKILL_ARG_INPUT, SKILL_ARG_OUTPUT)
31
-
32
- Environment variables (SKILL_ARG_*) are always set regardless of arg_style.
22
+ Script execution is handled by code_execution_tool directly.
23
"""
24
25
+ def _get_project_name(self) -> str | None:
26
+ ctx = getattr(self.agent, "context", None)
27
+ return projects.get_context_project_name(ctx) if ctx else None
28
+
29
async def execute(self, **kwargs) -> Response:
30
method = (
31
(kwargs.get("method") or self.args.get("method") or self.method or "")
@@ -52,20 +46,11 @@ class SkillsTool(Tool):
46
skill_name = str(kwargs.get("skill_name") or "").strip()
47
file_path = str(kwargs.get("file_path") or "").strip()
48
return Response(message=self._read_file(skill_name, file_path), break_loop=False)
55
- if method == "execute_script":
56
- skill_name = str(kwargs.get("skill_name") or "").strip()
57
- script_path = str(kwargs.get("script_path") or "").strip()
58
- script_args = kwargs.get("script_args") or {}
59
- if not isinstance(script_args, dict):
60
- script_args = {}
61
- # arg_style: "positional" (default), "named" (--key value), or "env" (env vars only)
62
- arg_style = str(kwargs.get("arg_style") or "positional").strip().lower()
63
- return await self._execute_script(skill_name, script_path, script_args, arg_style)
49
50
return Response(
51
message=(
52
"Error: missing/invalid 'method'. Supported methods: "
68
- "list, search, load, read_file, execute_script."
53
+ "list, search, load, read_file."
54
),
55
break_loop=False,
56
)
@@ -73,7 +58,7 @@ class SkillsTool(Tool):
58
return Response(message=f"Error in skills_tool: {e}", break_loop=False)
59
60
def _list(self) -> str:
76
- skills = skills_helper.list_skills(include_content=False, dedupe=True)
61
+ skills = skills_helper.list_skills(include_content=False, dedupe=True, project_name=self._get_project_name())
62
if not skills:
63
return "No skills found. Expected SKILL.md files under: skills/{custom,builtin,shared}."
64
@@ -97,7 +82,7 @@ class SkillsTool(Tool):
82
if not query:
83
return "Error: 'query' is required for method=search."
84
100
- results = skills_helper.search_skills(query, limit=25)
85
+ results = skills_helper.search_skills(query, limit=25, project_name=self._get_project_name())
86
if not results:
87
return f"No skills matched query: {query!r}"
88
@@ -116,7 +101,7 @@ class SkillsTool(Tool):
101
if not skill_name:
102
return "Error: 'skill_name' is required for method=load."
103
119
- skill = skills_helper.find_skill(skill_name, include_content=True)
104
+ skill = skills_helper.find_skill(skill_name, include_content=True, project_name=self._get_project_name())
105
if not skill:
106
return f"Error: skill not found: {skill_name!r}. Try skills_tool method=list or method=search."
107
@@ -166,7 +151,7 @@ class SkillsTool(Tool):
151
if not file_path:
152
return "Error: 'file_path' is required for method=read_file."
153
169
- skill = skills_helper.find_skill(skill_name, include_content=False)
154
+ skill = skills_helper.find_skill(skill_name, include_content=False, project_name=self._get_project_name())
155
if not skill:
156
return f"Error: skill not found: {skill_name!r}."
157
@@ -186,134 +171,6 @@ class SkillsTool(Tool):
171
text = content.decode("utf-8", errors="replace")
172
return f"File: {file_path}\n\n{text}"
173
189
- async def _execute_script(
190
- self, skill_name: str, script_path: str, script_args: Dict[str, Any],
191
- arg_style: str = "positional"
192
- ) -> Response:
193
- if not skill_name:
194
- return Response(message="Error: 'skill_name' is required for method=execute_script.", break_loop=False)
195
- if not script_path:
196
- return Response(message="Error: 'script_path' is required for method=execute_script.", break_loop=False)
197
-
198
- skill = skills_helper.find_skill(skill_name, include_content=False)
199
- if not skill:
200
- return Response(message=f"Error: skill not found: {skill_name!r}.", break_loop=False)
201
-
202
- try:
203
- script_abs = skills_helper.safe_path_within_dir(skill.path, script_path)
204
- except Exception as e:
205
- return Response(message=f"Error: invalid script_path: {e}", break_loop=False)
206
-
207
- if not script_abs.exists() or not script_abs.is_file():
208
- return Response(message=f"Error: script not found: {script_path!r} (within skill {skill.name})", break_loop=False)
209
-
210
- ext = script_abs.suffix.lower()
211
- runtime: str
212
- code: str
213
-
214
- # Use /a0 paths for remote (SSH) execution inside the container; use local absolute paths otherwise.
215
- if self.agent.config.code_exec_ssh_enabled:
216
- script_runtime_path = files.normalize_a0_path(str(script_abs))
217
- script_runtime_dir = files.normalize_a0_path(str(script_abs.parent))
218
- else:
219
- script_runtime_path = str(script_abs)
220
- script_runtime_dir = str(script_abs.parent)
221
-
222
- # Build environment variables (SKILL_ARG_*) - always set as fallback
223
- env_vars: Dict[str, str] = {}
224
- for k, v in (script_args or {}).items():
225
- env_key = f"SKILL_ARG_{re.sub(r'[^A-Za-z0-9_]', '_', str(k).upper())}"
226
- env_vars[env_key] = str(v)
227
-
228
- # Build CLI args based on arg_style:
229
- # - "positional": ['value1', 'value2'] - for scripts using sys.argv[1], sys.argv[2]
230
- # - "named": ['--key1', 'value1', '--key2', 'value2'] - for argparse/click scripts
231
- # - "env": [] - only use environment variables, no CLI args
232
- cli_args: List[str] = []
233
- if arg_style == "positional":
234
- cli_args = [str(v) for v in (script_args or {}).values()]
235
- elif arg_style == "named":
236
- for k, v in (script_args or {}).items():
237
- cli_args.append(f"--{k}")
238
- cli_args.append(str(v))
239
- # "env" style: cli_args stays empty, only env vars are used
240
-
241
- if ext == ".py":
242
- runtime = "python"
243
- # Set env vars (always available as fallback)
244
- env_lines = [f"os.environ[{json.dumps(k)}] = {json.dumps(v)}" for k, v in env_vars.items()]
245
- env_setup = "\n".join(env_lines) if env_lines else "pass"
246
- # Set sys.argv: ['script.py', ...cli_args]
247
- argv_list = [script_runtime_path] + cli_args
248
- argv_setup = f"sys.argv = {json.dumps(argv_list)}"
249
- code = (
250
- "import os, sys, runpy\n"
251
- f"os.chdir({json.dumps(script_runtime_dir)})\n"
252
- f"{env_setup}\n"
253
- f"{argv_setup}\n"
254
- f"runpy.run_path({json.dumps(script_runtime_path)}, run_name='__main__')\n"
255
- )
256
- elif ext == ".js":
257
- runtime = "nodejs"
258
- # Set process.env (always available as fallback)
259
- env_lines = [f"process.env[{json.dumps(k)}] = {json.dumps(v)};" for k, v in env_vars.items()]
260
- env_setup = "\n".join(env_lines) if env_lines else ""
261
- # Node.js argv: ['node', 'script.js', ...cli_args]
262
- argv_list = ["node", script_runtime_path] + cli_args
263
- code = (
264
- f"process.chdir({json.dumps(script_runtime_dir)});\n"
265
- f"{env_setup}\n"
266
- f"process.argv = {json.dumps(argv_list)};\n"
267
- f"require({json.dumps(script_runtime_path)});\n"
268
- )
269
- elif ext == ".sh":
270
- runtime = "terminal"
271
- # Environment variables (always available as fallback)
272
- env_parts = [f"{k}={shlex.quote(v)}" for k, v in env_vars.items()]
273
- env_prefix = " ".join(env_parts)
274
- # Pass CLI args to script
275
- cli_args_str = " ".join(shlex.quote(a) for a in cli_args)
276
- cd_cmd = f"cd {shlex.quote(script_runtime_dir)}"
277
- run_cmd = f"bash {shlex.quote(script_runtime_path)}"
278
- if cli_args_str:
279
- run_cmd = f"{run_cmd} {cli_args_str}"
280
- if env_prefix:
281
- code = f"{cd_cmd} && {env_prefix} {run_cmd}"
282
- else:
283
- code = f"{cd_cmd} && {run_cmd}"
284
- else:
285
- return Response(
286
- message=f"Error: unsupported script type {ext!r}. Supported: .py, .js, .sh",
287
- break_loop=False,
288
- )
289
-
290
- # Delegate actual execution to code_execution_tool (sandboxed)
291
- from python.tools.code_execution_tool import CodeExecution
292
-
293
- cet = CodeExecution(
294
- agent=self.agent,
295
- name="code_execution_tool",
296
- method=None,
297
- args={
298
- "runtime": runtime,
299
- "code": code,
300
- "session": int(self.args.get("session", 0) or 0),
301
- },
302
- message=self.message,
303
- loop_data=self.loop_data,
304
- )
305
-
306
- # Must call before_execution to initialize self.log before execute()
307
- await cet.before_execution(**cet.args)
308
- resp = await cet.execute(**cet.args)
309
- # Wrap result to make it clear it was a skill script
310
- wrapped = (
311
- f"Executed script: {skill.name}/{script_path}\n"
312
- f"Runtime: {runtime}\n\n"
313
- f"{resp.message}"
314
- )
315
- return Response(message=wrapped, break_loop=False)
316
-
174
def _list_skill_files(self, skill_dir: Path, *, max_files: int = 80) -> List[str]:
175
if not skill_dir.exists():
176
return []
webui/components/settings/settings.html
+9
-2
@@ -46,10 +46,14 @@
46
:class="{'active': $store.settingsStore.activeTab === 'developer'}"
47
@click="$store.settingsStore.switchTab('developer')"
48
title="Developer">Developer</div>
49
- <div class="settings-tab"
49
+ <div class="settings-tab"
50
:class="{'active': $store.settingsStore.activeTab === 'backup'}"
51
- @click="$store.settingsStore.switchTab('backup')"
51
+ @click="$store.settingsStore.switchTab('backup')"
52
title="Backup & Restore">Backup & Restore</div>
53
+ <div class="settings-tab"
54
+ :class="{'active': $store.settingsStore.activeTab === 'skills'}"
55
+ @click="$store.settingsStore.switchTab('skills')"
56
+ title="Skills">Skills</div>
57
</div>
58
</div>
59
@@ -70,6 +74,9 @@
74
<div x-show="$store.settingsStore.activeTab === 'backup'">
75
<x-component path="settings/backup/backup-settings.html"></x-component>
76
</div>
77
+ <div x-show="$store.settingsStore.activeTab === 'skills'">
78
+ <x-component path="settings/skills/skills-settings.html"></x-component>
79
+ </div>
80
</div>
81
</div>
82
webui/components/settings/skills/skills-import-store.js
+4
@@ -41,6 +41,10 @@ const model = {
41
onClose() {
42
this.resetState();
43
this.skillsFile = null;
44
+ this.namespace = "";
45
+ this.dest = "shared";
46
+ this.conflict = "skip";
47
+ this.projectName = "";
48
},
49
50
async loadProjects() {
webui/components/settings/skills/skills-settings.html
new
+28
@@ -0,0 +1,28 @@
1
+<html>
2
+ <head>
3
+ <title>Skills</title>
4
+ </head>
5
+
6
+ <body>
7
+ <div x-data>
8
+ <template x-if="$store.settingsStore">
9
+ <div>
10
+ <nav>
11
+ <ul>
12
+ <li>
13
+ <a href="#section-skills-import">
14
+ <img src="/public/skills.svg" alt="Skills" />
15
+ <span>Import Skills</span>
16
+ </a>
17
+ </li>
18
+ </ul>
19
+ </nav>
20
+
21
+ <div id="section-skills-import" class="section">
22
+ <x-component path="settings/skills/import.html"></x-component>
23
+ </div>
24
+ </div>
25
+ </template>
26
+ </div>
27
+ </body>
28
+</html>