diff --git a/prompts/agent.system.tool.skills.md b/prompts/agent.system.tool.skills.md index e9bad8f97..4822daa10 100644 --- a/prompts/agent.system.tool.skills.md +++ b/prompts/agent.system.tool.skills.md @@ -3,14 +3,14 @@ manage and use agent skills for specialized capabilities skills are composable bundles of instructions context and executable code use progressive disclosure: metadata → full content → referenced files -use "method" arg to specify operation: "list" "load" "read_file" "execute_script" "search" +use "method" arg to specify operation: "list" "load" "read_file" "search" ## Overview Skills system provides three-level progressive disclosure: - Level 1: Metadata (name + description) loaded in system prompt at startup - Level 2: Full SKILL.md content loaded when relevant to task -- Level 3+: Referenced files and scripts loaded on-demand +- Level 3+: Referenced files loaded on-demand When to use skills: - Task matches skill description from available skills list @@ -23,7 +23,7 @@ Progressive workflow: 2. Use "search" if looking for specific capability 3. Use "load" to get full skill instructions and context 4. Use "read_file" to load additional reference documents -5. Use "execute_script" to run deterministic operations +5. Use code_execution_tool to run any scripts referenced by the skill ## Operations @@ -116,82 +116,7 @@ Security: - Only files within skill directory accessible - Supports markdown, text, code files -### 4. execute skill script - -Executes bundled scripts from skill with arguments -Scripts receive arguments via standard CLI conventions (sys.argv, process.argv) -Use when: skill provides script for deterministic operation or automation - -~~~json -{ - "thoughts": [ - "Need to convert PDF to images", - "Skill provides convert_pdf_to_images.py script", - "Script expects positional args: input_pdf output_dir" - ], - "headline": "Converting PDF to images", - "tool_name": "skills_tool", - "tool_args": { - "method": "execute_script", - "skill_name": "pdf_editing", - "script_path": "scripts/convert_pdf_to_images.py", - "script_args": { - "input_pdf": "/path/to/document.pdf", - "output_dir": "/tmp/images" - } - } -} -~~~ - -Required args: -- skill_name: name of skill containing script -- script_path: relative path to script file -- script_args: dictionary of arguments passed to script - -Optional args: -- arg_style: how to pass arguments to script (default: "positional") - - "positional": values as positional args → sys.argv = ['script.py', 'value1', 'value2'] - - "named": as --key value pairs → sys.argv = ['script.py', '--key1', 'value1', '--key2', 'value2'] - - "env": only environment variables, no CLI args - -How scripts receive arguments: -- .py (Python): sys.argv[1], sys.argv[2], etc. (standard argparse/CLI compatible) -- .js (Node.js): process.argv[2], process.argv[3], etc. (standard CLI compatible) -- .sh (Shell): $1, $2, etc. as positional parameters - -Environment variables (always available as fallback): -- SKILL_ARG_KEY1=value1, SKILL_ARG_KEY2=value2, etc. -- Scripts can use os.environ.get('SKILL_ARG_INPUT_PDF') if needed - -Script execution: -- Runs in Docker container sandbox -- Has access to installed packages -- Returns stdout/stderr output -- Secure and isolated execution - -Example with argparse script (use arg_style="named"): -~~~json -{ - "thoughts": [ - "Script uses argparse with --input and --output flags", - "Need to use named arg_style" - ], - "headline": "Running argparse-based script", - "tool_name": "skills_tool", - "tool_args": { - "method": "execute_script", - "skill_name": "data_processor", - "script_path": "scripts/process.py", - "script_args": { - "input": "/path/to/data.csv", - "output": "/tmp/result.json" - }, - "arg_style": "named" - } -} -~~~ - -### 5. search skills by query +### 4. search skills by query Searches skills by text matching in name, description, and tags Returns ranked results by relevance score @@ -222,6 +147,48 @@ Scoring: - Tag match: +1 point per tag - Results sorted by descending score +## Running skill scripts + +When a skill includes scripts (listed under its files), use code_execution_tool directly to run them. +The skill's "load" output shows the skill directory path and lists available scripts. +Use read_file to inspect a script before running it if needed. + +Example: running a Python script from a skill +1. Load the skill to get its path and script list +2. Use code_execution_tool with runtime="python" to run the script + +~~~json +{ + "thoughts": [ + "Need to convert PDF to images", + "Skill provides convert_pdf_to_images.py at scripts/convert_pdf_to_images.py", + "Using code_execution_tool to run it directly" + ], + "headline": "Converting PDF to images", + "tool_name": "code_execution_tool", + "tool_args": { + "runtime": "python", + "code": "import subprocess\nsubprocess.run(['python', '/path/to/skill/scripts/convert_pdf_to_images.py', '/path/to/document.pdf', '/tmp/images'], check=True)" + } +} +~~~ + +Example: running a shell script from a skill +~~~json +{ + "thoughts": [ + "Skill provides a shell script for data processing", + "Running it via terminal runtime" + ], + "headline": "Running data processing script", + "tool_name": "code_execution_tool", + "tool_args": { + "runtime": "terminal", + "code": "cd /path/to/skill && bash scripts/process.sh /data/input.csv /tmp/output" + } +} +~~~ + ## Best Practices ### When to use skills vs other tools @@ -229,13 +196,15 @@ Scoring: Use skills when: - Task requires specialized domain knowledge - Need structured procedures or step-by-step guidance -- Deterministic scripts available for automation - Complex multi-step operations with best practices +Use code_execution_tool directly when: +- Running skill scripts (load skill first to get paths) +- Simple file operations +- General computation + Use other tools when: -- Simple file operations (use code_execution_tool) - Web search (use search_engine) -- General computation (use code_execution_tool) - Memory operations (use memory tools) ### Progressive disclosure workflow @@ -252,9 +221,9 @@ Use other tools when: - Use "read_file" for detailed documentation - Load only files relevant to current subtask -4. Execute scripts for automation - - Use "execute_script" for deterministic operations - - Provide appropriate arguments from context +4. Execute scripts via code_execution_tool + - Use skill path from "load" output + - Run scripts directly with code_execution_tool ### Common patterns @@ -263,12 +232,7 @@ Pattern: Using a skill for first time 2. Load full skill content 3. Follow instructions in content 4. Load reference files if mentioned -5. Execute scripts if provided - -Pattern: Quick script execution -1. Know skill name from previous use -2. Execute script directly with args -3. Process output +5. Run scripts via code_execution_tool using paths from load output Pattern: Exploring capabilities 1. Search with query terms @@ -280,140 +244,18 @@ Pattern: Exploring capabilities Common errors: - "Skill not found": Check spelling, use list or search to find correct name - "File not found": Verify file_path matches referenced files from load output -- "Script failed": Check script_args match expected parameters, review skill docs -- "Unsupported script type": Only .py, .js, .sh supported When skill loading fails: - Verify skill exists using list method - Check for typos in skill_name - Ensure skill system is enabled in settings -When script execution fails: -- Review skill documentation for required arguments -- Check script_args dictionary format -- Verify required packages installed in container -- Check script output for specific error messages - -## Examples - -Example 1: Simple script with positional args (default) -Script expects: python script.py /path/to/file.pdf -~~~json -{ - "thoughts": [ - "User has PDF to convert to images", - "Script uses sys.argv[1] for input, sys.argv[2] for output", - "Using default positional arg_style" - ], - "headline": "Converting PDF to images", - "tool_name": "skills_tool", - "tool_args": { - "method": "execute_script", - "skill_name": "pdf_editing", - "script_path": "scripts/convert_pdf_to_images.py", - "script_args": { - "input_pdf": "/workspace/document.pdf", - "output_dir": "/tmp/images" - } - } -} -~~~ -Result: sys.argv = ['script.py', '/workspace/document.pdf', '/tmp/images'] - -Example 2: Argparse script with named args -Script expects: python script.py --url https://... --selector .price -~~~json -{ - "thoughts": [ - "Need to scrape product prices from website", - "Script uses argparse with --url and --selector flags", - "Using arg_style='named' for argparse compatibility" - ], - "headline": "Scraping product prices from webpage", - "tool_name": "skills_tool", - "tool_args": { - "method": "execute_script", - "skill_name": "web_scraping", - "script_path": "scripts/fetch_page.py", - "script_args": { - "url": "https://example.com/products", - "selector": ".price" - }, - "arg_style": "named" - } -} -~~~ -Result: sys.argv = ['script.py', '--url', 'https://...', '--selector', '.price'] - -Example 3: Environment-only script -Script reads from os.environ only -~~~json -{ - "thoughts": [ - "Script reads configuration from environment variables", - "Using arg_style='env' to only set env vars" - ], - "headline": "Running config-based processor", - "tool_name": "skills_tool", - "tool_args": { - "method": "execute_script", - "skill_name": "data_processor", - "script_path": "scripts/process.py", - "script_args": { - "input_file": "/data/input.csv", - "mode": "production" - }, - "arg_style": "env" - } -} -~~~ -Result: SKILL_ARG_INPUT_FILE=/data/input.csv, SKILL_ARG_MODE=production - -Example 4: Data analysis workflow -~~~json -{ - "thoughts": [ - "User needs CSV analysis", - "data_analysis skill has analysis procedures", - "Loading skill for detailed instructions" - ], - "headline": "Loading data analysis skill", - "tool_name": "skills_tool", - "tool_args": { - "method": "load", - "skill_name": "data_analysis" - } -} -~~~ - -Then follow up with script (positional args): -~~~json -{ - "thoughts": [ - "Skill loaded, now analyzing CSV", - "Script takes csv_path as first arg, group_by as second" - ], - "headline": "Analyzing sales data grouped by category", - "tool_name": "skills_tool", - "tool_args": { - "method": "execute_script", - "skill_name": "data_analysis", - "script_path": "scripts/analyze_csv.py", - "script_args": { - "csv_path": "/workspace/sales_data.csv", - "group_by": "category" - } - } -} -~~~ - ## Notes - Skills metadata already loaded in your system prompt - Skills cache after first load for efficiency - Referenced files listed in load response -- Scripts receive arguments via sys.argv (positional by default) + SKILL_ARG_* env vars -- Use arg_style parameter to control argument passing: "positional", "named", or "env" +- Use code_execution_tool to run any scripts provided by skills - All operations return formatted text responses - Skills follow the open SKILL.md standard (cross-platform compatible) - Use skills for structured procedures and contextual expertise diff --git a/python/extensions/message_loop_prompts_after/_55_recall_skills.py b/python/extensions/message_loop_prompts_after/_55_recall_skills.py index 67bf4a62e..be9663aa3 100644 --- a/python/extensions/message_loop_prompts_after/_55_recall_skills.py +++ b/python/extensions/message_loop_prompts_after/_55_recall_skills.py @@ -6,6 +6,7 @@ from agent import LoopData from python.helpers.memory import Memory from python.helpers import files from python.helpers import skills as skills_helper +from python.helpers import projects class RecallSkills(Extension): @@ -29,6 +30,9 @@ class RecallSkills(Extension): if not user_instruction or len(user_instruction) < 8: return + # Get active project for project-scoped skill discovery + project_name = projects.get_context_project_name(self.agent.context) if self.agent.context else None + try: db = await Memory.get(self.agent) docs = await db.search_similarity_threshold( @@ -56,8 +60,8 @@ class RecallSkills(Extension): break if not recalled: - # cheap lexical fallback - matches = skills_helper.search_skills(user_instruction, limit=6) + # cheap lexical fallback (includes project skills when project is active) + matches = skills_helper.search_skills(user_instruction, limit=6, project_name=project_name) for s in matches: recalled.append(str(s.skill_md_path)) @@ -75,7 +79,7 @@ class RecallSkills(Extension): text = abs_path.read_text(encoding="utf-8", errors="replace") fm, body = skills_helper.split_frontmatter(text) - # Infer source if possible (custom/builtin/shared), else "unknown" + # Infer source if possible (custom/builtin/shared/project), else "unknown" source = "unknown" try: rel = abs_path.resolve().relative_to(base_skills_dir) @@ -83,6 +87,14 @@ class RecallSkills(Extension): source = rel.parts[0] except Exception: pass + if source == "unknown" and project_name: + try: + from python.helpers.skills_import import get_project_skills_folder + proj_skills = get_project_skills_folder(project_name) + abs_path.resolve().relative_to(proj_skills.resolve()) + source = "project" + except Exception: + pass name = str(fm.get("name") or abs_path.parent.name).strip() desc = str(fm.get("description") or "").strip() diff --git a/python/helpers/memory.py b/python/helpers/memory.py index 18d794cb5..daa5dae8c 100644 --- a/python/helpers/memory.py +++ b/python/helpers/memory.py @@ -335,15 +335,32 @@ class Memory: filename_pattern="**/SKILL.md", ) + # load project-scoped skills from all projects + try: + from python.helpers.skills_import import get_project_skills_folder + from python.helpers import projects as projects_helper + for proj in projects_helper.get_active_projects_list(): + proj_skills_path = str(get_project_skills_folder(proj["name"])) + if os.path.isdir(proj_skills_path): + index = knowledge_import.load_knowledge( + log_item, + proj_skills_path, + index, + {"area": Memory.Area.SKILLS.value}, + filename_pattern="**/SKILL.md", + ) + except Exception: + pass + # load custom instruments descriptions - index = knowledge_import.load_knowledge( - log_item, - files.get_abs_path("usr/instruments"), - index, - {"area": Memory.Area.INSTRUMENTS.value}, - filename_pattern="**/*.md", - recursive=True, - ) + # index = knowledge_import.load_knowledge( + # log_item, + # files.get_abs_path("usr/instruments"), + # index, + # {"area": Memory.Area.INSTRUMENTS.value}, + # filename_pattern="**/*.md", + # recursive=True, + # ) return index diff --git a/python/helpers/migration.py b/python/helpers/migration.py index e27a419bc..f60a5005e 100644 --- a/python/helpers/migration.py +++ b/python/helpers/migration.py @@ -19,7 +19,7 @@ def migrate_user_data() -> None: _move_dir("tmp/downloads", "usr/downloads") _move_dir("tmp/email", "usr/email") _move_dir("knowledge/custom", "usr/knowledge", overwrite=True) - _move_dir("instruments/custom", "usr/instruments", overwrite=True) + _move_dir("skills/custom", "usr/skills", overwrite=True) # --- Migrate Files ------------------------------------------------------------- # Move specific configuration files to usr/ @@ -37,7 +37,7 @@ def migrate_user_data() -> None: # We use _merge_dir_contents because we want to move the *contents* of default/ # into the parent directory, not move the default directory itself. _merge_dir_contents("knowledge/default", "knowledge") - _merge_dir_contents("instruments/default", "instruments") + _merge_dir_contents("skills/default", "skills") # --- Cleanup ------------------------------------------------------------------- @@ -103,7 +103,7 @@ def _cleanup_obsolete() -> None: """ to_remove = [ "knowledge/default", - "instruments/default", + "skills/default", "memory" ] for path in to_remove: diff --git a/python/helpers/skills.py b/python/helpers/skills.py index 0ea94765d..14abbbab5 100644 --- a/python/helpers/skills.py +++ b/python/helpers/skills.py @@ -14,7 +14,7 @@ except Exception: # pragma: no cover yaml = None # type: ignore -SkillSource = Literal["custom", "builtin", "shared"] +SkillSource = Literal["custom", "builtin", "shared", "project"] @dataclass(slots=True) @@ -42,10 +42,25 @@ def get_skills_base_dir() -> Path: return Path(files.get_abs_path("skills")) -def get_skill_roots(order: Optional[List[SkillSource]] = None) -> List[Tuple[SkillSource, Path]]: +def get_skill_roots( + order: Optional[List[SkillSource]] = None, + project_name: Optional[str] = None, +) -> List[Tuple[SkillSource, Path]]: base = get_skills_base_dir() order = order or ["custom", "builtin", "shared"] - return [(src, base / src) for src in order] + roots: List[Tuple[SkillSource, Path]] = [(src, base / src) for src in order] + + # Include project-scoped skills if a project is active + if project_name: + try: + from python.helpers.skills_import import get_project_skills_folder + project_skills = get_project_skills_folder(project_name) + if project_skills.exists(): + roots.insert(0, ("project", project_skills)) + except Exception: + pass + + return roots def _is_hidden_path(path: Path) -> bool: @@ -254,10 +269,11 @@ def list_skills( include_content: bool = False, dedupe: bool = True, root_order: Optional[List[SkillSource]] = None, + project_name: Optional[str] = None, ) -> List[Skill]: skills: List[Skill] = [] - roots = get_skill_roots(order=root_order) + roots = get_skill_roots(order=root_order, project_name=project_name) for source, root in roots: for skill_md in discover_skill_md_files(root): s = skill_from_markdown(skill_md, source, include_content=include_content) @@ -281,12 +297,13 @@ def find_skill( *, include_content: bool = False, root_order: Optional[List[SkillSource]] = None, + project_name: Optional[str] = None, ) -> Optional[Skill]: target = _normalize_name(skill_name) if not target: return None - roots = get_skill_roots(order=root_order) + roots = get_skill_roots(order=root_order, project_name=project_name) for source, root in roots: for skill_md in discover_skill_md_files(root): s = skill_from_markdown(skill_md, source, include_content=include_content) @@ -297,13 +314,13 @@ def find_skill( return None -def search_skills(query: str, *, limit: int = 25) -> List[Skill]: +def search_skills(query: str, *, limit: int = 25, project_name: Optional[str] = None) -> List[Skill]: q = (query or "").strip().lower() if not q: return [] terms = [t for t in re.split(r"\s+", q) if t] - candidates = list_skills(include_content=False, dedupe=True) + candidates = list_skills(include_content=False, dedupe=True, project_name=project_name) scored: List[Tuple[int, Skill]] = [] for s in candidates: diff --git a/python/tools/skills_tool.py b/python/tools/skills_tool.py index 9fba3be81..48103f5d8 100644 --- a/python/tools/skills_tool.py +++ b/python/tools/skills_tool.py @@ -1,13 +1,11 @@ from __future__ import annotations -import json -import re -import shlex from pathlib import Path -from typing import Any, Dict, List +from typing import List from python.helpers.tool import Tool, Response from python.helpers import files +from python.helpers import projects from python.helpers import skills as skills_helper @@ -20,18 +18,14 @@ class SkillsTool(Tool): - search (query) - load (skill_name) - read_file (skill_name, file_path) - - execute_script (skill_name, script_path, script_args, arg_style) - arg_style options for execute_script: - - "positional" (default): Pass values as positional args (sys.argv[1], sys.argv[2]) - Example: {"input": "file.pdf", "output": "/tmp"} → sys.argv = ['script.py', 'file.pdf', '/tmp'] - - "named": Pass as --key value pairs (for argparse/click scripts) - Example: {"input": "file.pdf", "output": "/tmp"} → sys.argv = ['script.py', '--input', 'file.pdf', '--output', '/tmp'] - - "env": Only use environment variables (SKILL_ARG_INPUT, SKILL_ARG_OUTPUT) - - Environment variables (SKILL_ARG_*) are always set regardless of arg_style. + Script execution is handled by code_execution_tool directly. """ + def _get_project_name(self) -> str | None: + ctx = getattr(self.agent, "context", None) + return projects.get_context_project_name(ctx) if ctx else None + async def execute(self, **kwargs) -> Response: method = ( (kwargs.get("method") or self.args.get("method") or self.method or "") @@ -52,20 +46,11 @@ class SkillsTool(Tool): skill_name = str(kwargs.get("skill_name") or "").strip() file_path = str(kwargs.get("file_path") or "").strip() return Response(message=self._read_file(skill_name, file_path), break_loop=False) - if method == "execute_script": - skill_name = str(kwargs.get("skill_name") or "").strip() - script_path = str(kwargs.get("script_path") or "").strip() - script_args = kwargs.get("script_args") or {} - if not isinstance(script_args, dict): - script_args = {} - # arg_style: "positional" (default), "named" (--key value), or "env" (env vars only) - arg_style = str(kwargs.get("arg_style") or "positional").strip().lower() - return await self._execute_script(skill_name, script_path, script_args, arg_style) return Response( message=( "Error: missing/invalid 'method'. Supported methods: " - "list, search, load, read_file, execute_script." + "list, search, load, read_file." ), break_loop=False, ) @@ -73,7 +58,7 @@ class SkillsTool(Tool): return Response(message=f"Error in skills_tool: {e}", break_loop=False) def _list(self) -> str: - skills = skills_helper.list_skills(include_content=False, dedupe=True) + skills = skills_helper.list_skills(include_content=False, dedupe=True, project_name=self._get_project_name()) if not skills: return "No skills found. Expected SKILL.md files under: skills/{custom,builtin,shared}." @@ -97,7 +82,7 @@ class SkillsTool(Tool): if not query: return "Error: 'query' is required for method=search." - results = skills_helper.search_skills(query, limit=25) + results = skills_helper.search_skills(query, limit=25, project_name=self._get_project_name()) if not results: return f"No skills matched query: {query!r}" @@ -116,7 +101,7 @@ class SkillsTool(Tool): if not skill_name: return "Error: 'skill_name' is required for method=load." - skill = skills_helper.find_skill(skill_name, include_content=True) + skill = skills_helper.find_skill(skill_name, include_content=True, project_name=self._get_project_name()) if not skill: return f"Error: skill not found: {skill_name!r}. Try skills_tool method=list or method=search." @@ -166,7 +151,7 @@ class SkillsTool(Tool): if not file_path: return "Error: 'file_path' is required for method=read_file." - skill = skills_helper.find_skill(skill_name, include_content=False) + skill = skills_helper.find_skill(skill_name, include_content=False, project_name=self._get_project_name()) if not skill: return f"Error: skill not found: {skill_name!r}." @@ -186,134 +171,6 @@ class SkillsTool(Tool): text = content.decode("utf-8", errors="replace") return f"File: {file_path}\n\n{text}" - async def _execute_script( - self, skill_name: str, script_path: str, script_args: Dict[str, Any], - arg_style: str = "positional" - ) -> Response: - if not skill_name: - return Response(message="Error: 'skill_name' is required for method=execute_script.", break_loop=False) - if not script_path: - return Response(message="Error: 'script_path' is required for method=execute_script.", break_loop=False) - - skill = skills_helper.find_skill(skill_name, include_content=False) - if not skill: - return Response(message=f"Error: skill not found: {skill_name!r}.", break_loop=False) - - try: - script_abs = skills_helper.safe_path_within_dir(skill.path, script_path) - except Exception as e: - return Response(message=f"Error: invalid script_path: {e}", break_loop=False) - - if not script_abs.exists() or not script_abs.is_file(): - return Response(message=f"Error: script not found: {script_path!r} (within skill {skill.name})", break_loop=False) - - ext = script_abs.suffix.lower() - runtime: str - code: str - - # Use /a0 paths for remote (SSH) execution inside the container; use local absolute paths otherwise. - if self.agent.config.code_exec_ssh_enabled: - script_runtime_path = files.normalize_a0_path(str(script_abs)) - script_runtime_dir = files.normalize_a0_path(str(script_abs.parent)) - else: - script_runtime_path = str(script_abs) - script_runtime_dir = str(script_abs.parent) - - # Build environment variables (SKILL_ARG_*) - always set as fallback - env_vars: Dict[str, str] = {} - for k, v in (script_args or {}).items(): - env_key = f"SKILL_ARG_{re.sub(r'[^A-Za-z0-9_]', '_', str(k).upper())}" - env_vars[env_key] = str(v) - - # Build CLI args based on arg_style: - # - "positional": ['value1', 'value2'] - for scripts using sys.argv[1], sys.argv[2] - # - "named": ['--key1', 'value1', '--key2', 'value2'] - for argparse/click scripts - # - "env": [] - only use environment variables, no CLI args - cli_args: List[str] = [] - if arg_style == "positional": - cli_args = [str(v) for v in (script_args or {}).values()] - elif arg_style == "named": - for k, v in (script_args or {}).items(): - cli_args.append(f"--{k}") - cli_args.append(str(v)) - # "env" style: cli_args stays empty, only env vars are used - - if ext == ".py": - runtime = "python" - # Set env vars (always available as fallback) - env_lines = [f"os.environ[{json.dumps(k)}] = {json.dumps(v)}" for k, v in env_vars.items()] - env_setup = "\n".join(env_lines) if env_lines else "pass" - # Set sys.argv: ['script.py', ...cli_args] - argv_list = [script_runtime_path] + cli_args - argv_setup = f"sys.argv = {json.dumps(argv_list)}" - code = ( - "import os, sys, runpy\n" - f"os.chdir({json.dumps(script_runtime_dir)})\n" - f"{env_setup}\n" - f"{argv_setup}\n" - f"runpy.run_path({json.dumps(script_runtime_path)}, run_name='__main__')\n" - ) - elif ext == ".js": - runtime = "nodejs" - # Set process.env (always available as fallback) - env_lines = [f"process.env[{json.dumps(k)}] = {json.dumps(v)};" for k, v in env_vars.items()] - env_setup = "\n".join(env_lines) if env_lines else "" - # Node.js argv: ['node', 'script.js', ...cli_args] - argv_list = ["node", script_runtime_path] + cli_args - code = ( - f"process.chdir({json.dumps(script_runtime_dir)});\n" - f"{env_setup}\n" - f"process.argv = {json.dumps(argv_list)};\n" - f"require({json.dumps(script_runtime_path)});\n" - ) - elif ext == ".sh": - runtime = "terminal" - # Environment variables (always available as fallback) - env_parts = [f"{k}={shlex.quote(v)}" for k, v in env_vars.items()] - env_prefix = " ".join(env_parts) - # Pass CLI args to script - cli_args_str = " ".join(shlex.quote(a) for a in cli_args) - cd_cmd = f"cd {shlex.quote(script_runtime_dir)}" - run_cmd = f"bash {shlex.quote(script_runtime_path)}" - if cli_args_str: - run_cmd = f"{run_cmd} {cli_args_str}" - if env_prefix: - code = f"{cd_cmd} && {env_prefix} {run_cmd}" - else: - code = f"{cd_cmd} && {run_cmd}" - else: - return Response( - message=f"Error: unsupported script type {ext!r}. Supported: .py, .js, .sh", - break_loop=False, - ) - - # Delegate actual execution to code_execution_tool (sandboxed) - from python.tools.code_execution_tool import CodeExecution - - cet = CodeExecution( - agent=self.agent, - name="code_execution_tool", - method=None, - args={ - "runtime": runtime, - "code": code, - "session": int(self.args.get("session", 0) or 0), - }, - message=self.message, - loop_data=self.loop_data, - ) - - # Must call before_execution to initialize self.log before execute() - await cet.before_execution(**cet.args) - resp = await cet.execute(**cet.args) - # Wrap result to make it clear it was a skill script - wrapped = ( - f"Executed script: {skill.name}/{script_path}\n" - f"Runtime: {runtime}\n\n" - f"{resp.message}" - ) - return Response(message=wrapped, break_loop=False) - def _list_skill_files(self, skill_dir: Path, *, max_files: int = 80) -> List[str]: if not skill_dir.exists(): return [] diff --git a/webui/components/settings/settings.html b/webui/components/settings/settings.html index d3185ee52..6edaa4102 100644 --- a/webui/components/settings/settings.html +++ b/webui/components/settings/settings.html @@ -46,10 +46,14 @@ :class="{'active': $store.settingsStore.activeTab === 'developer'}" @click="$store.settingsStore.switchTab('developer')" title="Developer">Developer -
Backup & Restore
+
Skills
@@ -70,6 +74,9 @@
+
+ +
diff --git a/webui/components/settings/skills/skills-import-store.js b/webui/components/settings/skills/skills-import-store.js index d5d6c021d..c12956196 100644 --- a/webui/components/settings/skills/skills-import-store.js +++ b/webui/components/settings/skills/skills-import-store.js @@ -41,6 +41,10 @@ const model = { onClose() { this.resetState(); this.skillsFile = null; + this.namespace = ""; + this.dest = "shared"; + this.conflict = "skip"; + this.projectName = ""; }, async loadProjects() { diff --git a/webui/components/settings/skills/skills-settings.html b/webui/components/settings/skills/skills-settings.html new file mode 100644 index 000000000..b295a0fd5 --- /dev/null +++ b/webui/components/settings/skills/skills-settings.html @@ -0,0 +1,28 @@ + + + Skills + + + +
+ +
+ +