diff --git a/main.py b/main.py index 51a1566..19f6ff0 100644 --- a/main.py +++ b/main.py @@ -248,6 +248,20 @@ def ai_handler(msg: IncomingMessage) -> str: if cmd.startswith("skill"): return _handle_skill_command(msg.text.strip().lstrip("/")) + # Detect natural language skill creation requests and handle locally + # instead of routing to the AI (which may fail or be slow). + _SKILL_CREATE_RE = re.compile( + r"(?:create|make|add|build|write)\s+(?:a\s+)?(?:new\s+)?skill\s+(?:for|to|that|about|called|named)\s+(.+)", + re.IGNORECASE, + ) + skill_match = _SKILL_CREATE_RE.match(text_lower) + if skill_match: + description = skill_match.group(1).strip().rstrip(".") + # Derive a slug name from the description + name = re.sub(r"[^a-z0-9]+", "-", description.lower()).strip("-")[:40] + if name: + return _create_skill_from_description(name, description) + # Build context from memory + skills context = _build_context(msg) @@ -1117,6 +1131,66 @@ def _handle_skill_command(text: str) -> str: ) +def _create_skill_from_description(name: str, description: str) -> str: + """ + Create a skill from a natural language description. + + Generates a SKILL.md with sensible triggers derived from the description, + reloads the skills manager, and confirms to the user. + """ + global _skills + + cfg = load_config() + workspace = os.path.expanduser(cfg.memory.workspace) + skill_dir = os.path.join(workspace, "skills", name) + skill_path = os.path.join(skill_dir, "SKILL.md") + + if os.path.exists(skill_path): + return f"⚠️ Skill `{name}` already exists. Use `skill show {name}` to view it." + + # Derive trigger words from the description + stop_words = { + "a", "an", "the", "for", "to", "at", "in", "on", "of", "and", + "or", "is", "it", "my", "me", "do", "get", "how", "what", "when", + "where", "that", "this", "with", "about", "checking", "check", + "current", "currently", "getting", "looking", "find", "finding", + } + words = re.findall(r"[a-z]+", description.lower()) + triggers = [w for w in words if w not in stop_words and len(w) > 2] + # Deduplicate while preserving order + seen = set() + triggers = [t for t in triggers if not (t in seen or seen.add(t))] + if not triggers: + triggers = [name] + + os.makedirs(skill_dir, exist_ok=True) + template = ( + f"---\n" + f"name: {name}\n" + f"description: {description.capitalize()}\n" + f"triggers: [{', '.join(triggers)}]\n" + f"---\n\n" + f"# {name.replace('-', ' ').title()} Skill\n\n" + f"When the user asks about {description}:\n" + f"1. Use web search or available tools to find the information\n" + f"2. Present the results clearly and concisely\n" + f"3. Offer follow-up suggestions if relevant\n" + ) + with open(skill_path, "w", encoding="utf-8") as f: + f.write(template) + + # Reload so `skill list` picks it up immediately + if _skills: + _skills.reload() + + return ( + f"✅ Skill `{name}` created!\n" + f"Triggers: {', '.join(triggers)}\n\n" + f"Edit it: `skill show {name}`\n" + f"Or tell me what to change and I'll update it." + ) + + def _on_scheduled_job(job: ScheduledJob) -> None: """ Called by the scheduler when a job fires.