diff --git a/skillagent/agent.py b/skillagent/agent.py index 6631106..636440e 100644 --- a/skillagent/agent.py +++ b/skillagent/agent.py @@ -6,7 +6,17 @@ from pydantic import BaseModel, Field, ValidationError from typing import Literal from appPublic.worker import awaitify from appPublic.streamhttpclient import StreamHttpClient, liner +from appPublic.dictObject import DictObject +from appPublic.log import debug, exception, info +from ahserver.serverenv import ServerEnv from .skillkit_wrapper import SkillkitWrapper +from .params import ParameterResolver, ResolvedParams, MissingParams +from .tmpls import ( + three_candidates_tmpl, + resume_tmpl, + ask_user_reply_tmpl, + choose_candidates_tmpl +) # --------------------------- # Skill Decision / PlanState @@ -14,6 +24,7 @@ from .skillkit_wrapper import SkillkitWrapper @dataclass class SkillDecision: skill: str + script: str params: dict reason: Optional[str] = None @@ -29,8 +40,8 @@ class PlanState: # 自定义异常 # --------------------------- class MissingParams(Exception): - def __init__(self, skill: str, fields: List[str]): - self.skill = skill + def __init__(self, decision: SkillDecision, fields: List[str]): + self.decision = decision self.fields = fields # --------------------------- @@ -69,8 +80,7 @@ class DummyLLM(LLM): continue if d.get('content'): doc = f'{doc}{d["content"]}' - else: - print(f'{d} error') + debug(f'{doc=}') return doc # --------------------------- @@ -86,10 +96,20 @@ class Agent: def load_skills(self): if self.loaded: return - self.skills = self.skillkit.list_skills() - for s in self.skills: - self.skillkit.load_skill(s.name) - + skills = self.skillkit.list_skills() + self.skills = [] + for s in skills: + sk = self.skillkit.load_skill(s.name) + self.skills.append(sk) + + async def render_txt(self, tmpl_name, lang, data): + env = ServerEnv() + engine = env.tmpl_engine + tmpl = tmpl_name.get(lang) + if not tmpl: + return None + return await engine.renders(tmpl, data) + # --------------------------- # plan: 多 skill 候选 + 参数抽取 # --------------------------- @@ -100,7 +120,7 @@ class Agent: try: validated_params = self._validate_params(decision) except MissingParams as e: - question = await self._ask_user_for_params(user_text, decision.skill, e.fields) + question = await self._ask_user_for_params(user_text, decision.skill, decision.script, e.fields) state = PlanState( user_intent=user_text, skill=decision.skill, @@ -124,6 +144,7 @@ class Agent: def get_scripts(self, skillname): return self.skillkit.get_skill_scripts(skillname) + # --------------------------- # resume: 补 missing 参数 # --------------------------- @@ -132,31 +153,13 @@ class Agent: schema_fields = self.skillkit.get_script_params(state.skill, state.script) if schema_fields is None: schema_fields = [] - prompt = f""" -You are an agent helping a user fill parameters for a skill. - -Skill name: {state.skill} -Script name: {state.script} -Skill required parameters: {schema_fields} - -User original intent: -\"\"\"{state.user_intent}\"\"\" - -Known parameters: -{state.params} - -Missing parameters: -{state.missing} - -User reply: -\"\"\"{user_reply}\"\"\" - -Task: -- Extract values ONLY for missing parameters. -- Do NOT modify existing known parameters. -- All output must match the skill parameter schema. -- Output JSON only with the missing parameters. -""" + data = { + 'state': state, + 'kit': self.skillkit, + 'json': json, + 'user_reply': user_reply + } + prompt = await self.render_txt(resume_tmpl, 'zh', data) raw = await self.llm.complete(prompt) new_params = json.loads(raw) @@ -164,7 +167,7 @@ Task: # 校验 schema try: - validated = self._validate_params(SkillDecision(skill=state.skill, params=state.params)) + validated = self._validate_params(SkillDecision(skill=state.skill, script=state.script, params=state.params)) except MissingParams as e: state.missing = e.fields question = await self._ask_user_for_params(state.user_intent, state.skill, e.fields) @@ -187,75 +190,55 @@ Task: return "Scripts: " + '::'.join(d) async def _candidate_skills(self, user_text: str): - skill_list = "\n".join(f"- skillname:{s.name}({s.description}): {self.scripts_info(s.name)}" for s in self.skills) - prompt = f""" -User request: -\"\"\"{user_text}\"\"\" - -Available skills: -{skill_list} - -Task: -Select up to 3 most relevant skill's scripts. - -Output JSON list only. -""" + data = { + 'kit': self.skillkit, + 'prompt': user_text, + 'json': json, + 'skills': self.skills + } + prompt = await self.render_txt(three_candidates_tmpl, 'zh', data) + debug(f'{prompt=}') raw = await self.llm.complete(prompt) + debug(f'{raw=}') return json.loads(raw) - async def _plan_with_candidates(self, user_text: str, candidates: list[str]): - specs = [s for s in self.skills if s.name in candidates] - spec_desc = "\n".join( - f"- {s.name}: inputs={list(s.schema.model_fields.keys())}" for s in specs if s.schema - ) - prompt = f""" -User request: -\"\"\"{user_text}\"\"\" - -Candidate skills: -{spec_desc} - -Task: -1. Choose the best skill. -2. Extract parameters strictly matching schema. - -Rules: -- If a required parameter is missing, set it to null. -- Output JSON only. - -Output: -{{ - "skill": "...", - "script": "...", - "params": {{ ... }}, - "reason": "..." -}} -""" + async def _plan_with_candidates(self, user_text: str, candidates: list): + data = { + 'prompt': user_text, + 'candidates': candidates, + 'json': json, + 'kit': self.skillkit + } + prompt = await self.render_txt(choose_candidates_tmpl, 'zh', data) raw = await self.llm.complete(prompt) return SkillDecision(**json.loads(raw)) def _validate_params(self, decision: SkillDecision): - spec = next(s for s in self.skills if s.name == decision.skill) - if not spec.schema: + spec = next(s for s in self.skills if s.metadata.name == decision.skill) + schema = self.skillkit.get_script_params(decision.skill, decision.script) + if not schema: return decision.params try: - return spec.schema(**decision.params).dict() + resolver = ParameterResolver(schema) + result = resolver.resolve(decision.params) + if isinstance(result, ResolvedParams): + return result.params + raise MissingParams(decision, result.missing) except ValidationError as e: missing = [err["loc"][0] for err in e.errors() if err["type"] == "missing"] if missing: - raise MissingParams(decision.skill, missing) + raise MissingParams(decision, missing) raise - async def _ask_user_for_params(self, user_text: str, skill: str, fields: List[str]): - prompt = f""" -User request: -\"\"\"{user_text}\"\"\" - -The script "{script} in skill "{skill}" requires the following missing parameters: -{fields} - -Ask the user a concise clarification question. -""" + async def _ask_user_for_params(self, user_text: str, skill: str, script: str, fields: List[str]): + data = { + 'user_text': user_text, + 'skill': skill, + 'script': script, + 'json': json, + 'fields': fields + } + prompt = await self.render_txt(ask_user_reply_tmpl, 'zh', data) return await self.llm.complete(prompt) # --------------------------- diff --git a/skillagent/params.py b/skillagent/params.py new file mode 100644 index 0000000..200f128 --- /dev/null +++ b/skillagent/params.py @@ -0,0 +1,90 @@ +from dataclasses import dataclass +from typing import Any, Dict, List, Optional + +@dataclass(frozen=True) +class MissingParam: + name: str + type: Optional[str] + description: Optional[str] + +@dataclass(frozen=True) +class ResolvedParams: + params: Dict[str, Any] + +@dataclass(frozen=True) +class MissingParams: + missing: List[MissingParam] + +class ParameterResolver: + TYPE_MAP = { + "string": str, + "integer": int, + "number": (int, float), + "boolean": bool, + "array": list, + "object": dict, + } + + def __init__(self, schema: Dict[str, dict]): + self.schema = schema + + def resolve(self, params: Dict[str, Any]): + validated = {} + missing = [] + + for name, spec in self.schema.items(): + required = spec.get("required", False) + default = spec.get("default") + expected_type = spec.get("type") + enum = spec.get("enum") + description = spec.get("description") + + if name in params: + value = params[name] + else: + if required: + missing.append( + MissingParam( + name=name, + type=expected_type, + description=description, + ) + ) + continue + value = default + + if value is None: + validated[name] = value + continue + + # 类型校验 + if expected_type: + py_type = self.TYPE_MAP.get(expected_type) + if py_type and not isinstance(value, py_type): + missing.append( + MissingParam( + name=name, + type=expected_type, + description=f"Invalid type, expected {expected_type}", + ) + ) + continue + + # enum 校验 + if enum and value not in enum: + missing.append( + MissingParam( + name=name, + type=expected_type, + description=f"Invalid value, must be one of {enum}", + ) + ) + continue + + validated[name] = value + + if missing: + return MissingParams(missing=missing) + + return ResolvedParams(params=validated) + diff --git a/skillagent/skillkit_wrapper.py b/skillagent/skillkit_wrapper.py index 6f9536e..25558f4 100644 --- a/skillagent/skillkit_wrapper.py +++ b/skillagent/skillkit_wrapper.py @@ -4,6 +4,7 @@ import yaml from pathlib import Path from typing import Dict, Any from appPublic.dictObject import DictObject +from appPublic.log import debug, exception def find_missing_params( input_schema: Dict[str, Any], @@ -39,7 +40,7 @@ def load_schemas(path) -> Dict[str, Any]: with path.open("r", encoding="utf-8") as f: data = yaml.safe_load(f) - return DictObject(data) + return DictObject(**data) class SkillkitWrapper: def __init__(self, user_skillsroot, sys_skillsroot=None): @@ -54,22 +55,24 @@ class SkillkitWrapper: def load_skill(self, skill_name): skill = self.client.load_skill(skill_name) - print(skill, dir(skill)) schemaspath = skill.base_directory / 'schemas.yaml' if schemaspath.exists(): if not self.schemas.get(skill_name): data = load_schemas(schemaspath) self.schemas[skill_name] = data - print(f'{data=}, {str(schemaspath)}') return skill - def get_script_params(self, skill_name, script_name): + def get_script_schema(self, skill_name, script_name): skill = self.load_skill(skill_name) - d = self.schemas.get('skill_name') + d = self.schemas.get(skill_name) if not d: - return [] - m = d.scripts.get(script_name) + # debug(f'{skill_name=}, {self.schemas=} ,has not schemas.yaml') + return None + return d.scripts.get(script_name) + + def get_script_params(self, skill_name, script_name): + m = self.get_script_schema(skill_name, script_name) if not m: return [] return m.inputs @@ -85,6 +88,5 @@ class SkillkitWrapper: arguments=args ) def invoke_skill(self, skill_name: str, script_name: str, params: dict): - print(f"Invoking skill={skill_nmae}, script={script_nmae}, params={params}") return self.client.invoke_skill(skill_nmae, params) diff --git a/skillagent/tmpls.py b/skillagent/tmpls.py new file mode 100644 index 0000000..f052bcf --- /dev/null +++ b/skillagent/tmpls.py @@ -0,0 +1,184 @@ +choose_candidates_tmpl = { + "en":""" +User request: +\"\"\"{{prompt}}\"\"\" + +Candidate skills: +{{spec_desc}} + +Task: +1. Choose the best skill with scripts. +2. Extract parameters strictly matching schema. + +Rules: +- If a required parameter is missing, set it to null. +- Output JSON only. + +Output: +{ + "skill": "...", + "script": "...", + "params": { ... }, + "reason": "..." +} +""", + "zh":""" +用户需求: +\"\"\"{{promot}}\"\"\" + +候选技能脚本: +技能|技能描述|脚本|脚本描述|已知参数|缺失参数 +{% for c in candidates %} +{{c.skill}}|{{c.skill_desc}}|{{c.script}}|{{c.script_desc}}|{{json.dumps(c.params, ensure_ascii=False)}}|{{json.dumps(c.missing_params, ensure_ascii=False)}} +{% endfor %} + +任务: +1. 选择最符合用户需求的技能脚本 + +规则 +- 如果一个需要的参数缺失,设为null +- 只输出JSON + +输出: +{ + "skill": "...", + "script": "...", + "params": {}, + "reason": "..." +} +""" +} + +resume_tmpl = { + "en":""" +You are an agent helping a user fill parameters for a skill. + +Skill name: {{state.skill}} +Script name: {{state.script}} +{% set schema = kit.get_script_params(state.skill, state.script) %} +{% if schema %} +Skill required parameters: {{json.dumps(schema.inputs, ensure_ascii=False)}} +{% else %} +Skill required parameters:[] +{% endif %} + +User original intent: +\"\"\"{{state.user_intent}}\"\"\" + +Known parameters: +{{json.dumps(state.params, ensure_ascii=False)}} + +Missing parameters: +{{json.dumps(state.missing, ensure_ascii=False)}} + +User reply: +\"\"\"{{user_reply}}\"\"\" + +Task: +- Extract values ONLY for missing parameters. +- Do NOT modify existing known parameters. +- All output must match the skill parameter schema. +- Output JSON only with the missing parameters. +""", + "zh":""" +你是个帮助技能脚本填参的助手 +技能名:{{state.skill}} +脚本名:{{state.script}} +{% set schema = kit.get_script_params(state.skill, state.script) %} +{% if schema %} +脚本需要的参数:{{json.dumps(schema.inputs, ensure_ascii=False)}} +{% else %} +脚本不需要的参数 +{% endif %} + +用户原始需求: +\"\"\"{{state.user_intent}}\"\"\" + +已有的参数: +{{json.dumps(state.params, ensure_ascii=False)}} + +缺失参数: +{{json.dumps(state.missing, ensure_ascii=False)}} + +用户回复: +\"\"\"{{user_reply}}\"\"\" + +任务: +- 仅抽取缺失参数的值 +- 不要修改已知参数的值 +- 所有输出必须符合技能脚本的参数要求 +- 值输出缺失参数JSON +""" +} + +three_candidates_tmpl = { + "en":""" +User request: +\"\"\"{{json.dumps(prompt, ensure_ascii=False)}}\"\"\" + +Available skills: +{{skill_list}} + +Task: +Select up to 3 most relevant skill's scripts. + +Output JSON list only. +""", + "zh":""" +用户需求: +\"\"\"{{json.dumps(prompt, ensure_ascii=False)}}\"\"\" + +可用的技能脚本: +{% for s in skills %} +名字:{{s.metadata.name}} +描述:{{s.metadata.description}} +{% if s.scripts %} + 脚本: +{% for sc in s.scripts %} + 名字:{{sc.name}} +{% set schema = kit.get_script_schema(s.metadata.name, sc.name) %} +{% if schema %} + 描述:{{schema.description or sc.description}} + 参数:{{json.dumps(schema.params, ensure_ascii=False)}} +{% else %} + 描述:{{sc.description}} +{% endif %} +{% endfor %} +{% endif %} +{% endfor %} +任务: +- 选择3个相关的技能脚本 +- 按照脚本参数要求,为脚本从用户需求中识别出参数 + +输出 +仅输出json,每个元素为 +{ + "skill":技能名字, + "skill_desc":技能描述(很重要不能缺失) + "script":脚本名字, + “script_desc”:脚本描述(很重要不能缺失) + “params”:参数字典,key为参数名,value为参数值 + “missing_params”:如果有缺失参数,将缺失参数的参数名放在这数组里 +} +""" +} + +ask_user_reply_tmpl = { + "en":""" +User request: +\"\"\"{{user_text}}\"\"\" + +The script "{{script}} in skill "{{skill}}" requires the following missing parameters: +{{json.dumps(fields, ensure_ascii=False)}} + +Ask the user a concise clarification question. +""", + "zh":""" +用户需求 +\"\"\"{{user_text}}\"\"\" + +这个技能“{{skill}}“下的脚本”{{script}}“需要这些参数: +{{json.dumps(fields, ensure_ascii=False)}} +请用户继续完善问题 +""" +} diff --git a/test/skills/calculator/SKILL.md b/test/skills/calculator/SKILL.md new file mode 100644 index 0000000..59c31d1 --- /dev/null +++ b/test/skills/calculator/SKILL.md @@ -0,0 +1,52 @@ +--- +name: calculator +description: "Perform basic arithmetic operations: addition, subtraction, multiplication, and division" +--- + +# calculator + +Perform basic arithmetic operations: addition, subtraction, multiplication, and division. + +## Input + +The input is a JSON object provided via standard input: + +```json +{ + "a": 10, + "op": "+", + "b": 5 +} +``` + +### Parameters + +- `a` (number, required): First operand +- `op` (string, required): Operator, one of `+`, `-`, `*`, `/` +- `b` (number, required): Second operand + +## Output + +Returns a JSON object to standard output: + +```json +{ + "result": 15 +} +``` + +## Errors + +On failure, returns a JSON object: + +```json +{ + "error": "division by zero" +} +``` + +## Notes + +- This skill is non-interactive. +- Output is always valid JSON. +- No output is written to stderr. diff --git a/test/skills/calculator/run.sh b/test/skills/calculator/run.sh new file mode 100755 index 0000000..c3ec99b --- /dev/null +++ b/test/skills/calculator/run.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +read -r INPUT + +echo "$INPUT" +error() { + echo "{\"error\":\"$1\"}" + exit 0 +} + +A=$(echo "$INPUT" | jq -r '.a // empty') +OP=$(echo "$INPUT" | jq -r '.op // empty') +B=$(echo "$INPUT" | jq -r '.b // empty') + +[[ -z "$A" || -z "$OP" || -z "$B" ]] && error "missing parameter" + +is_number='^-?[0-9]+([.][0-9]+)?$' +[[ ! "$A" =~ $is_number ]] && error "a is not a number" +[[ ! "$B" =~ $is_number ]] && error "b is not a number" + +case "$OP" in + "+") RESULT=$(echo "$A + $B" | bc) ;; + "-") RESULT=$(echo "$A - $B" | bc) ;; + "*") RESULT=$(echo "$A * $B" | bc) ;; + "/") + [[ "$(echo "$B == 0" | bc)" -eq 1 ]] && error "division by zero" + RESULT=$(echo "scale=10; $A / $B" | bc) + ;; + *) error "unsupported operator" ;; +esac + +echo "{\"result\": $RESULT}" diff --git a/test/skills/calculator/schemas.yaml b/test/skills/calculator/schemas.yaml new file mode 100644 index 0000000..114b30a --- /dev/null +++ b/test/skills/calculator/schemas.yaml @@ -0,0 +1,18 @@ +scripts: + run: + description: 四则运算 + inputs: + a: + type: int or float + required: true + description: 计算左值 + op: + type: str + required: true + description: 计算方法 + enum: [+ - * /] + b: + type: int or float + required: true + description: 计算左值 + diff --git a/test/skills/code-reviewer/SKILL.md b/test/skills/code-reviewer/SKILL.md new file mode 100644 index 0000000..6d02c30 --- /dev/null +++ b/test/skills/code-reviewer/SKILL.md @@ -0,0 +1,37 @@ +--- +name: code-reviewer +description: Review code for best practices, potential bugs, security vulnerabilities, and performance issues +allowed-tools: Read, Grep +--- + +# Code Reviewer Skill + +You are an experienced code reviewer. Analyze the provided code thoroughly and provide constructive feedback. + +## Review Areas + +1. **Best Practices**: Check adherence to language-specific conventions +2. **Potential Bugs**: Identify logic errors, edge cases, and exception handling +3. **Security**: Look for common vulnerabilities (SQL injection, XSS, etc.) +4. **Performance**: Spot inefficient algorithms or resource usage +5. **Readability**: Assess code clarity, naming, and documentation + +## Instructions + +$ARGUMENTS + +## Output Format + +Provide your review in the following structure: + +### Summary +Brief overview of the code quality + +### Issues Found +List specific issues with severity (Critical/High/Medium/Low) and location + +### Recommendations +Actionable suggestions for improvement + +### Positive Aspects +Highlight what was done well diff --git a/test/skills/code-reviewer/run.sh b/test/skills/code-reviewer/run.sh new file mode 100755 index 0000000..cfbee7f --- /dev/null +++ b/test/skills/code-reviewer/run.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +set -euo pipefail + +read -r INPUT + +error() { + echo "{\"error\":\"$1\"}" + exit 0 +} + +A=$(echo "$INPUT" | jq -r '.a // empty') +OP=$(echo "$INPUT" | jq -r '.op // empty') +B=$(echo "$INPUT" | jq -r '.b // empty') + +[[ -z "$A" || -z "$OP" || -z "$B" ]] && error "missing parameter" + +is_number='^-?[0-9]+([.][0-9]+)?$' +[[ ! "$A" =~ $is_number ]] && error "a is not a number" +[[ ! "$B" =~ $is_number ]] && error "b is not a number" + +case "$OP" in + "+") RESULT=$(echo "$A + $B" | bc) ;; + "-") RESULT=$(echo "$A - $B" | bc) ;; + "*") RESULT=$(echo "$A * $B" | bc) ;; + "/") + [[ "$(echo "$B == 0" | bc)" -eq 1 ]] && error "division by zero" + RESULT=$(echo "scale=10; $A / $B" | bc) + ;; + *) error "unsupported operator" ;; +esac + +echo "{\"result\": $RESULT}" diff --git a/test/skills/example-plugin/.claude-plugin/plugin.json b/test/skills/example-plugin/.claude-plugin/plugin.json new file mode 100644 index 0000000..169e2ef --- /dev/null +++ b/test/skills/example-plugin/.claude-plugin/plugin.json @@ -0,0 +1,18 @@ +{ + "manifest_version": "0.1", + "name": "example-plugin", + "version": "1.0.0", + "description": "Example plugin demonstrating plugin manifest structure with multiple skill directories", + "author": { + "name": "Skillkit Team", + "email": "team@skillkit.example.com", + "url": "https://github.com/skillkit/example-plugin" + }, + "skills": ["skills/"], + "display_name": "Example Plugin", + "homepage": "https://github.com/skillkit/example-plugin", + "repository": { + "type": "git", + "url": "https://github.com/skillkit/example-plugin" + } +} diff --git a/test/skills/example-plugin/skills/csv-parser/SKILL.md b/test/skills/example-plugin/skills/csv-parser/SKILL.md new file mode 100644 index 0000000..0912675 --- /dev/null +++ b/test/skills/example-plugin/skills/csv-parser/SKILL.md @@ -0,0 +1,38 @@ +--- +name: csv-parser +description: Parse and analyze CSV files with data validation +--- + +# CSV Parser Skill + +You are a CSV file analysis assistant from the example-plugin. + +## Capabilities + +- Parse CSV files with various delimiters +- Validate data types and constraints +- Generate summary statistics +- Detect encoding issues +- Handle malformed data gracefully + +## Usage + +To analyze a CSV file, provide the file path as an argument: + +``` +Arguments: $ARGUMENTS +``` + +## Output Format + +The analysis will include: +- Row count and column count +- Column names and inferred data types +- Missing value report +- Basic statistics for numeric columns +- Encoding and delimiter detection results + +## Example + +Input: data.csv +Output: Analysis report with statistics and validation results diff --git a/test/skills/example-plugin/skills/json-parser/SKILL.md b/test/skills/example-plugin/skills/json-parser/SKILL.md new file mode 100644 index 0000000..3835acc --- /dev/null +++ b/test/skills/example-plugin/skills/json-parser/SKILL.md @@ -0,0 +1,37 @@ +--- +name: json-parser +description: Parse and validate JSON data with schema support +--- + +# JSON Parser Skill + +You are a JSON data validation assistant from the example-plugin. + +## Capabilities + +- Parse and pretty-print JSON data +- Validate against JSON Schema +- Detect malformed JSON +- Extract specific fields using JSONPath +- Convert to other formats (CSV, YAML) + +## Usage + +To parse JSON data, provide the file path or raw JSON as an argument: + +``` +Arguments: $ARGUMENTS +``` + +## Output Format + +The parser will provide: +- Validation status (valid/invalid) +- Structure overview (depth, object count) +- Schema compliance report (if schema provided) +- Extracted values (if JSONPath provided) + +## Example + +Input: config.json +Output: Validated JSON structure with compliance report diff --git a/test/skills/file-reference-skill/SKILL.md b/test/skills/file-reference-skill/SKILL.md new file mode 100644 index 0000000..fb554d5 --- /dev/null +++ b/test/skills/file-reference-skill/SKILL.md @@ -0,0 +1,68 @@ +--- +name: file-reference-skill +description: Example skill demonstrating secure file reference resolution with supporting files +allowed-tools: [] +--- + +# File Reference Skill + +This skill demonstrates how to use supporting files (scripts, templates, documentation) within a skill directory. + +## Overview + +This skill uses helper scripts and templates for data processing. All supporting files are accessible via relative paths from the skill's base directory. + +## Available Supporting Files + +### Scripts +- `scripts/data_processor.py` - Main data processing script +- `scripts/validator.py` - Input validation utilities +- `scripts/helper.sh` - Shell helper script + +### Templates +- `templates/config.yaml` - Configuration template +- `templates/report.md` - Report generation template + +### Documentation +- `docs/usage.md` - Detailed usage instructions +- `docs/examples.md` - Example use cases + +## Usage + +When this skill is invoked with arguments, it can access supporting files using the FilePathResolver: + +```python +from pathlib import Path +from skillkit.core.path_resolver import FilePathResolver + +# Get the skill's base directory (injected by BaseDirectoryProcessor) +base_dir = Path("") + +# Resolve supporting files securely +processor_script = FilePathResolver.resolve_path(base_dir, "scripts/data_processor.py") +config_template = FilePathResolver.resolve_path(base_dir, "templates/config.yaml") +usage_docs = FilePathResolver.resolve_path(base_dir, "docs/usage.md") + +# Read file contents +with open(processor_script) as f: + script_code = f.read() +``` + +## Processing Arguments + +The skill expects data file paths as arguments: + +**Example invocation**: `file-reference-skill data/input.csv data/output.csv` + +Processing steps: +1. Validate input using `scripts/validator.py` +2. Process data using `scripts/data_processor.py` +3. Generate report using `templates/report.md` +4. Output results to specified location + +## Security Notes + +- All file paths are validated to prevent directory traversal attacks +- Symlinks are resolved and verified to stay within skill directory +- Absolute paths and path traversal patterns (../) are blocked +- Any security violation raises PathSecurityError with detailed logging diff --git a/test/skills/file-reference-skill/docs/examples.md b/test/skills/file-reference-skill/docs/examples.md new file mode 100644 index 0000000..836332b --- /dev/null +++ b/test/skills/file-reference-skill/docs/examples.md @@ -0,0 +1,262 @@ +# File Reference Skill - Examples + +## Example 1: Simple Data Processing + +Process a CSV file using the skill's data processor: + +```python +from skillkit import SkillManager +from skillkit.core.path_resolver import FilePathResolver +from pathlib import Path + +# Initialize skill manager +manager = SkillManager("./examples/skills") +manager.discover() + +# Invoke skill +result = manager.invoke_skill( + "file-reference-skill", + "data/input.csv data/output.csv" +) + +print(result) +``` + +## Example 2: Accessing Supporting Scripts + +Read and execute supporting scripts: + +```python +from pathlib import Path +from skillkit.core.path_resolver import FilePathResolver + +# Get skill's base directory +skill = manager.get_skill("file-reference-skill") +base_dir = skill.base_directory + +# Resolve script path securely +processor_path = FilePathResolver.resolve_path( + base_dir, + "scripts/data_processor.py" +) + +# Read script content +with open(processor_path) as f: + script_code = f.read() + +print(f"Script location: {processor_path}") +print(f"Script length: {len(script_code)} bytes") +``` + +## Example 3: Loading Configuration Template + +Load and parse configuration template: + +```python +import yaml +from skillkit.core.path_resolver import FilePathResolver + +# Resolve config template path +config_path = FilePathResolver.resolve_path( + base_dir, + "templates/config.yaml" +) + +# Load configuration +with open(config_path) as f: + config = yaml.safe_load(f) + +print("Configuration:", config) +``` + +## Example 4: Handling Security Violations + +Demonstrate path traversal prevention: + +```python +from skillkit.core.path_resolver import FilePathResolver +from skillkit.core.exceptions import PathSecurityError + +try: + # Attempt path traversal (will be blocked) + malicious_path = FilePathResolver.resolve_path( + base_dir, + "../../../etc/passwd" + ) +except PathSecurityError as e: + print(f"Security violation blocked: {e}") + # Expected output: + # Security violation blocked: Path traversal attempt detected: + # '../../../etc/passwd' resolves outside skill directory +``` + +## Example 5: Validating Input Files + +Use validator script to check input files: + +```python +import subprocess +from skillkit.core.path_resolver import FilePathResolver + +# Resolve validator script +validator_path = FilePathResolver.resolve_path( + base_dir, + "scripts/validator.py" +) + +# Import and use validator +import sys +sys.path.insert(0, str(validator_path.parent)) +from validator import validate_csv_format + +# Validate input file +is_valid = validate_csv_format("data/input.csv") +print(f"File is valid: {is_valid}") +``` + +## Example 6: Generating Reports + +Generate report using template: + +```python +from string import Template +from datetime import datetime +from skillkit.core.path_resolver import FilePathResolver + +# Resolve report template +template_path = FilePathResolver.resolve_path( + base_dir, + "templates/report.md" +) + +# Load template +with open(template_path) as f: + template_content = f.read() + +# Fill template with data +template = Template(template_content) +report = template.safe_substitute({ + 'timestamp': datetime.now().isoformat(), + 'input_file': 'data/input.csv', + 'input_size': '1234', + 'format': 'CSV', + 'encoding': 'UTF-8', + 'start_time': '10:00:00', + 'end_time': '10:00:05', + 'duration': '5', + 'status': 'SUCCESS', + 'output_file': 'data/output.csv', + 'output_size': '1234', + 'record_count': '100', + 'error_count': '0', + 'validation_results': 'All checks passed', + 'processing_log': 'Processing completed successfully' +}) + +print(report) +``` + +## Example 7: Shell Script Integration + +Execute shell helper script: + +```python +import subprocess +from skillkit.core.path_resolver import FilePathResolver + +# Resolve shell script +helper_path = FilePathResolver.resolve_path( + base_dir, + "scripts/helper.sh" +) + +# Execute script +result = subprocess.run( + ['bash', str(helper_path), 'check'], + capture_output=True, + text=True +) + +print(result.stdout) +``` + +## Example 8: Multiple File Access + +Access multiple supporting files in one operation: + +```python +from skillkit.core.path_resolver import FilePathResolver + +# List of files to access +file_paths = [ + "scripts/data_processor.py", + "scripts/validator.py", + "templates/config.yaml", + "docs/usage.md" +] + +# Resolve all paths securely +resolved_paths = {} +for rel_path in file_paths: + try: + abs_path = FilePathResolver.resolve_path(base_dir, rel_path) + resolved_paths[rel_path] = abs_path + print(f"✓ {rel_path} -> {abs_path}") + except PathSecurityError as e: + print(f"✗ {rel_path} -> BLOCKED ({e})") + +print(f"\nSuccessfully resolved {len(resolved_paths)} paths") +``` + +## Example 9: Error Handling Best Practices + +Robust error handling when accessing supporting files: + +```python +from pathlib import Path +from skillkit.core.path_resolver import FilePathResolver +from skillkit.core.exceptions import PathSecurityError + +def safe_load_supporting_file(base_dir: Path, rel_path: str) -> str: + """Safely load supporting file with comprehensive error handling.""" + try: + # Resolve path securely + abs_path = FilePathResolver.resolve_path(base_dir, rel_path) + + # Read file content + with open(abs_path, 'r', encoding='utf-8') as f: + return f.read() + + except PathSecurityError as e: + print(f"Security violation: {e}") + raise + except FileNotFoundError: + print(f"File not found: {rel_path}") + raise + except PermissionError: + print(f"Permission denied: {rel_path}") + raise + except UnicodeDecodeError: + print(f"Invalid UTF-8 encoding: {rel_path}") + raise + except Exception as e: + print(f"Unexpected error loading {rel_path}: {e}") + raise + +# Usage +try: + content = safe_load_supporting_file(base_dir, "scripts/helper.py") + print(f"Loaded {len(content)} bytes") +except Exception as e: + print(f"Failed to load file: {e}") +``` + +## Summary + +These examples demonstrate: +- Secure file path resolution using FilePathResolver +- Accessing scripts, templates, and documentation +- Handling security violations gracefully +- Integration with Python and shell scripts +- Best practices for error handling +- Template-based report generation diff --git a/test/skills/file-reference-skill/docs/usage.md b/test/skills/file-reference-skill/docs/usage.md new file mode 100644 index 0000000..d7f8d31 --- /dev/null +++ b/test/skills/file-reference-skill/docs/usage.md @@ -0,0 +1,141 @@ +# File Reference Skill - Usage Guide + +## Overview + +The file-reference-skill demonstrates how to structure a skill with supporting files (scripts, templates, documentation) and access them securely using the FilePathResolver. + +## Directory Structure + +``` +file-reference-skill/ +├── SKILL.md # Main skill definition +├── scripts/ # Processing scripts +│ ├── data_processor.py # Main data processor +│ ├── validator.py # Input validation +│ └── helper.sh # Shell utilities +├── templates/ # Configuration and output templates +│ ├── config.yaml # Configuration template +│ └── report.md # Report generation template +└── docs/ # Documentation + ├── usage.md # This file + └── examples.md # Example use cases +``` + +## Using Supporting Files + +### From Python + +```python +from pathlib import Path +from skillkit.core.path_resolver import FilePathResolver + +# Base directory is provided in the skill context +base_dir = Path("/path/to/skills/file-reference-skill") + +# Resolve paths securely +processor_path = FilePathResolver.resolve_path( + base_dir, + "scripts/data_processor.py" +) + +# Read file content +with open(processor_path) as f: + script_code = f.read() +``` + +### From Shell + +```bash +# Get base directory from skill context +BASE_DIR="/path/to/skills/file-reference-skill" + +# Use helper script +bash "$BASE_DIR/scripts/helper.sh" check + +# Run data processor +python3 "$BASE_DIR/scripts/data_processor.py" input.csv output.csv +``` + +## Security Features + +The FilePathResolver ensures: + +1. **Path Traversal Prevention**: Blocks attempts to access files outside skill directory +2. **Symlink Validation**: Resolves symlinks and verifies targets stay within base directory +3. **Absolute Path Rejection**: Prevents absolute path injection +4. **Detailed Logging**: All security violations logged at ERROR level + +### Valid Paths + +```python +# Allowed - relative path within skill directory +FilePathResolver.resolve_path(base_dir, "scripts/helper.py") +FilePathResolver.resolve_path(base_dir, "templates/config.yaml") +FilePathResolver.resolve_path(base_dir, "docs/usage.md") +``` + +### Invalid Paths (Blocked) + +```python +# Blocked - directory traversal +FilePathResolver.resolve_path(base_dir, "../../etc/passwd") + +# Blocked - absolute path +FilePathResolver.resolve_path(base_dir, "/etc/passwd") + +# Blocked - symlink escape +# (if symlink target is outside base_dir) +FilePathResolver.resolve_path(base_dir, "malicious_link") +``` + +## Example Workflow + +1. **Skill Invocation** + ```python + manager = SkillManager() + manager.discover() + + result = manager.invoke_skill( + "file-reference-skill", + "input_data.csv output_data.csv" + ) + ``` + +2. **Skill Processing** + - Skill receives base directory in context + - Script paths resolved using FilePathResolver + - Scripts executed with validated paths + - Results returned to caller + +3. **File Access** + - All file operations use resolved paths + - Security violations raise PathSecurityError + - Detailed error messages help debugging + +## Best Practices + +1. **Always use FilePathResolver** for accessing supporting files +2. **Use relative paths** from skill base directory +3. **Document file dependencies** in SKILL.md +4. **Test with various path patterns** including edge cases +5. **Handle PathSecurityError** appropriately in your code + +## Troubleshooting + +### PathSecurityError + +**Problem**: Attempting to access files outside skill directory + +**Solution**: Use relative paths within skill directory only + +### FileNotFoundError + +**Problem**: Resolved path doesn't exist + +**Solution**: Verify file exists in skill directory structure + +### PermissionError + +**Problem**: Cannot read resolved file + +**Solution**: Check file permissions and ownership diff --git a/test/skills/file-reference-skill/scripts/data_processor.py b/test/skills/file-reference-skill/scripts/data_processor.py new file mode 100644 index 0000000..a934819 --- /dev/null +++ b/test/skills/file-reference-skill/scripts/data_processor.py @@ -0,0 +1,51 @@ +"""Data processing script for file-reference-skill. + +This script demonstrates how supporting files can be used within a skill. +""" + +import sys +from pathlib import Path + + +def process_data(input_file: str, output_file: str) -> None: + """Process data from input file and write to output file. + + Args: + input_file: Path to input data file + output_file: Path to output data file + """ + print(f"Processing data from {input_file}") + print(f"Output will be written to {output_file}") + + # Read input file + try: + with open(input_file, 'r') as f: + data = f.read() + print(f"Read {len(data)} bytes from input file") + except FileNotFoundError: + print(f"Error: Input file not found: {input_file}") + sys.exit(1) + + # Process data (example: uppercase transformation) + processed_data = data.upper() + + # Write output file + with open(output_file, 'w') as f: + f.write(processed_data) + print(f"Wrote {len(processed_data)} bytes to output file") + + +def main() -> None: + """Main entry point.""" + if len(sys.argv) != 3: + print("Usage: data_processor.py ") + sys.exit(1) + + input_file = sys.argv[1] + output_file = sys.argv[2] + + process_data(input_file, output_file) + + +if __name__ == "__main__": + main() diff --git a/test/skills/file-reference-skill/scripts/env_demo.py b/test/skills/file-reference-skill/scripts/env_demo.py new file mode 100755 index 0000000..d87f819 --- /dev/null +++ b/test/skills/file-reference-skill/scripts/env_demo.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +"""Environment Variable Demonstration Script + +This script demonstrates how skillkit automatically injects environment +variables into script execution context. + +Injected Variables: + - SKILL_NAME: Name of the skill + - SKILL_BASE_DIR: Absolute path to skill directory + - SKILL_VERSION: Version from skill metadata + - SKILLKIT_VERSION: Current skillkit version + +These variables can be used for: + - Locating files relative to skill directory + - Including skill context in logs + - Version-specific behavior + - Debugging and troubleshooting + +Usage: + This script is designed to be executed by skillkit's script executor. + It reads JSON arguments from stdin and writes results to stdout. +""" + +import json +import os +import sys +from pathlib import Path + + +def main(): + """Demonstrate environment variable access.""" + # Read arguments from stdin (standard skillkit pattern) + try: + args = json.load(sys.stdin) + except json.JSONDecodeError: + args = {} + + # Access injected environment variables + skill_name = os.environ.get('SKILL_NAME', 'unknown') + skill_base = os.environ.get('SKILL_BASE_DIR', 'unknown') + skill_version = os.environ.get('SKILL_VERSION', '0.0.0') + skillkit_version = os.environ.get('SKILLKIT_VERSION', 'unknown') + + # Prepare output + output = { + "message": "Environment variables successfully accessed!", + "context": { + "skill_name": skill_name, + "skill_base_dir": skill_base, + "skill_version": skill_version, + "skillkit_version": skillkit_version + }, + "arguments_received": args, + "examples": { + "relative_file_path": "Use SKILL_BASE_DIR to locate files", + "logging": f"[{skill_name} v{skill_version}] Log message here", + "file_resolution": str(Path(skill_base) / "data" / "config.json") + } + } + + # Print formatted output + print("=" * 60) + print(f"Skill: {skill_name} v{skill_version}") + print(f"Directory: {skill_base}") + print(f"Powered by: skillkit v{skillkit_version}") + print("=" * 60) + print() + print("Environment Variables:") + print(f" SKILL_NAME = {skill_name}") + print(f" SKILL_BASE_DIR = {skill_base}") + print(f" SKILL_VERSION = {skill_version}") + print(f" SKILLKIT_VERSION = {skillkit_version}") + print() + print("Arguments Received:") + print(f" {json.dumps(args, indent=2)}") + print() + print("Example Use Cases:") + print(f" 1. Locate skill files:") + print(f" config_path = Path(os.environ['SKILL_BASE_DIR']) / 'config.json'") + print(f" → {Path(skill_base) / 'config.json'}") + print() + print(f" 2. Contextual logging:") + print(f" logger.info(f'[{{os.environ[\"SKILL_NAME\"]}}] Processing...')") + print(f" → [{skill_name}] Processing...") + print() + print(f" 3. Version-specific behavior:") + print(f" if os.environ['SKILL_VERSION'] >= '2.0.0':") + print(f" use_new_api()") + print() + print("=" * 60) + + # Also output as JSON for programmatic use + print() + print("JSON Output:") + print(json.dumps(output, indent=2)) + + # Exit successfully + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/test/skills/file-reference-skill/scripts/helper.sh b/test/skills/file-reference-skill/scripts/helper.sh new file mode 100755 index 0000000..3e786df --- /dev/null +++ b/test/skills/file-reference-skill/scripts/helper.sh @@ -0,0 +1,39 @@ +#!/bin/bash +# Helper script for file-reference-skill + +echo "File Reference Skill Helper Script" +echo "===================================" +echo "" +echo "This script demonstrates shell scripting support in skills." +echo "" +echo "Usage: ./helper.sh [args...]" +echo "" + +case "${1:-help}" in + check) + echo "Checking environment..." + echo "Python version: $(python3 --version)" + echo "Current directory: $(pwd)" + echo "Script directory: $(dirname "$0")" + ;; + validate) + if [ -z "$2" ]; then + echo "Error: No file specified" + exit 1 + fi + echo "Validating file: $2" + if [ -f "$2" ]; then + echo "File exists: $2" + echo "File size: $(wc -c < "$2") bytes" + else + echo "File not found: $2" + exit 1 + fi + ;; + help|*) + echo "Available commands:" + echo " check - Check environment" + echo " validate - Validate file exists" + echo " help - Show this help message" + ;; +esac diff --git a/test/skills/file-reference-skill/scripts/validator.py b/test/skills/file-reference-skill/scripts/validator.py new file mode 100644 index 0000000..ca5cddc --- /dev/null +++ b/test/skills/file-reference-skill/scripts/validator.py @@ -0,0 +1,60 @@ +"""Input validation utilities for file-reference-skill.""" + +from pathlib import Path + + +def validate_file_path(file_path: str) -> bool: + """Validate that a file path exists and is readable. + + Args: + file_path: Path to validate + + Returns: + True if valid, False otherwise + """ + path = Path(file_path) + + if not path.exists(): + print(f"Error: File does not exist: {file_path}") + return False + + if not path.is_file(): + print(f"Error: Path is not a file: {file_path}") + return False + + try: + with open(path, 'r') as f: + f.read(1) + return True + except PermissionError: + print(f"Error: Permission denied reading file: {file_path}") + return False + except Exception as e: + print(f"Error: Cannot read file: {file_path} ({e})") + return False + + +def validate_csv_format(file_path: str) -> bool: + """Validate that a file is in CSV format. + + Args: + file_path: Path to CSV file + + Returns: + True if valid CSV, False otherwise + """ + if not validate_file_path(file_path): + return False + + # Check file extension + if not file_path.endswith('.csv'): + print(f"Warning: File does not have .csv extension: {file_path}") + + # Check for CSV content (basic validation) + with open(file_path, 'r') as f: + first_line = f.readline() + if ',' not in first_line: + print(f"Warning: File may not be valid CSV (no commas found): {file_path}") + return False + + return True diff --git a/test/skills/file-reference-skill/templates/config.yaml b/test/skills/file-reference-skill/templates/config.yaml new file mode 100644 index 0000000..5306841 --- /dev/null +++ b/test/skills/file-reference-skill/templates/config.yaml @@ -0,0 +1,28 @@ +# Configuration template for file-reference-skill + +# Data processing settings +processing: + input_format: csv + output_format: csv + encoding: utf-8 + delimiter: "," + skip_header: false + +# Validation settings +validation: + check_encoding: true + check_format: true + max_file_size_mb: 100 + required_columns: [] + +# Output settings +output: + include_timestamp: true + compress: false + create_backup: true + +# Logging settings +logging: + level: INFO + format: "%(asctime)s - %(levelname)s - %(message)s" + file: "processing.log" diff --git a/test/skills/file-reference-skill/templates/report.md b/test/skills/file-reference-skill/templates/report.md new file mode 100644 index 0000000..1538bfb --- /dev/null +++ b/test/skills/file-reference-skill/templates/report.md @@ -0,0 +1,39 @@ +# Data Processing Report + +**Generated**: {timestamp} +**Skill**: file-reference-skill + +## Input Summary + +- **Input File**: {input_file} +- **File Size**: {input_size} bytes +- **Format**: {format} +- **Encoding**: {encoding} + +## Processing Summary + +- **Start Time**: {start_time} +- **End Time**: {end_time} +- **Duration**: {duration} seconds +- **Status**: {status} + +## Output Summary + +- **Output File**: {output_file} +- **Output Size**: {output_size} bytes +- **Records Processed**: {record_count} +- **Errors**: {error_count} + +## Validation Results + +{validation_results} + +## Processing Log + +``` +{processing_log} +``` + +--- + +*This report was generated by the file-reference-skill example skill.* diff --git a/test/skills/git-helper/SKILL.md b/test/skills/git-helper/SKILL.md new file mode 100644 index 0000000..65dc96b --- /dev/null +++ b/test/skills/git-helper/SKILL.md @@ -0,0 +1,37 @@ +--- +name: git-helper +description: Generate git commit messages and help with git workflows +allowed-tools: Bash +--- + +# Git Helper Skill + +You are a git workflow assistant. Help users with commit messages, branch naming, and git best practices. + +## Commit Message Format + +Follow conventional commits specification: +- **feat**: New feature +- **fix**: Bug fix +- **docs**: Documentation changes +- **style**: Formatting, missing semicolons, etc. +- **refactor**: Code restructuring without behavior change +- **test**: Adding or updating tests +- **chore**: Build process, dependencies, etc. + +Format: +``` +(): + + + +