From 59f629024f334a5529326af1ebe8485ee7b35e7c Mon Sep 17 00:00:00 2001 From: livevil7 Date: Wed, 25 Feb 2026 17:17:02 +0900 Subject: [PATCH] =?UTF-8?q?Add=20creet=20v1.5.0=20=E2=80=94=20Skill=20navi?= =?UTF-8?q?gator=20for=20Claude=20Code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Creet scans all installed plugins (Skills, MCP tools, LSP servers), recommends the best match for your task in 8 languages, and executes it. - /c — Single skill navigator with AskUserQuestion recommendation - /cc — Parallel multi-agent execution across all matching skills - SessionStart hook auto-scans installed plugins each session - UserPromptSubmit hook suggests relevant skills as you type - Cross-platform (Windows, Mac, Linux) Homepage: https://github.com/Creeta-creet/creet --- .claude-plugin/marketplace.json | 21 + plugins/creet/.claude-plugin/plugin.json | 26 + plugins/creet/LICENSE | 21 + plugins/creet/README.md | 248 +++++++++ plugins/creet/creet.config.json | 8 + plugins/creet/hooks/hooks.json | 29 ++ plugins/creet/hooks/session-start.js | 170 +++++++ plugins/creet/lib/keyword-matcher.js | 114 +++++ plugins/creet/lib/memory-store.js | 159 ++++++ plugins/creet/lib/plugin-registry.js | 166 +++++++ plugins/creet/lib/skill-scanner.js | 497 +++++++++++++++++++ plugins/creet/scripts/user-prompt-handler.js | 149 ++++++ plugins/creet/skills/c/SKILL.md | 69 +++ plugins/creet/skills/cc/SKILL.md | 137 +++++ 14 files changed, 1814 insertions(+) create mode 100644 plugins/creet/.claude-plugin/plugin.json create mode 100644 plugins/creet/LICENSE create mode 100644 plugins/creet/README.md create mode 100644 plugins/creet/creet.config.json create mode 100644 plugins/creet/hooks/hooks.json create mode 100644 plugins/creet/hooks/session-start.js create mode 100644 plugins/creet/lib/keyword-matcher.js create mode 100644 plugins/creet/lib/memory-store.js create mode 100644 plugins/creet/lib/plugin-registry.js create mode 100644 plugins/creet/lib/skill-scanner.js create mode 100644 plugins/creet/scripts/user-prompt-handler.js create mode 100644 plugins/creet/skills/c/SKILL.md create mode 100644 plugins/creet/skills/cc/SKILL.md diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 6f29b6b..4117e90 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -10,6 +10,27 @@ "homepage": "https://claudecodeplugins.dev" }, "plugins": [ + { + "name": "creet", + "source": "./plugins/creet", + "description": "Skill navigator for Claude Code. Scans all installed plugins (Skills, MCP tools, LSP servers), recommends the best match for your task in 8 languages, and executes it. Type /c to start.", + "version": "1.5.0", + "author": { + "name": "Creeta", + "url": "https://www.creeta.com" + }, + "category": "productivity", + "homepage": "https://github.com/Creeta-creet/creet", + "keywords": [ + "creet", + "skill-navigator", + "plugin-scanner", + "productivity", + "session-hook", + "auto-recommend", + "multilingual" + ] + }, { "name": "documentation-generator", "source": "./plugins/documentation-generator", diff --git a/plugins/creet/.claude-plugin/plugin.json b/plugins/creet/.claude-plugin/plugin.json new file mode 100644 index 0000000..8c47f22 --- /dev/null +++ b/plugins/creet/.claude-plugin/plugin.json @@ -0,0 +1,26 @@ +{ + "name": "creet", + "version": "1.5.0", + "description": "Skill navigator for Claude Code. Scans all installed plugins (Skills, MCP tools, LSP servers), recommends the best match for your task in 8 languages, and executes it. Type /c to start.", + "author": { + "name": "Creeta", + "email": "creet@creeta.com", + "url": "https://www.creeta.com" + }, + "repository": "https://github.com/Creeta-creet/creet", + "license": "MIT", + "keywords": [ + "claude-code", + "creet", + "skill-navigator", + "plugin-scanner", + "mcp-tools", + "lsp-servers", + "plugin-recommendation", + "meta-skill", + "productivity", + "session-hook", + "auto-recommend", + "multilingual" + ] +} diff --git a/plugins/creet/LICENSE b/plugins/creet/LICENSE new file mode 100644 index 0000000..91c80b2 --- /dev/null +++ b/plugins/creet/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 livevil7 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/creet/README.md b/plugins/creet/README.md new file mode 100644 index 0000000..5dc552c --- /dev/null +++ b/plugins/creet/README.md @@ -0,0 +1,248 @@ +# Creet + +**Never wonder which plugin to use again.** + +Creet is a skill navigator for Claude Code by [Creeta](https://www.creeta.com). It scans your installed plugins, finds the best skill for your task, and runs it — all from a single command. + +Works with **any** combination of plugins. No hardcoded dependencies. + +## The Problem + +You installed 10+ plugins. That's 50+ slash commands, MCP tools, and LSP servers. You can't remember them all, and you don't know which combination works best for your task. + +## The Solution + +``` +You: /c build a dashboard with auth + +Creet — Skill Scan +| # | Name | Type | Plugin | Domain | +|----|---------------|-------|-----------|----------| +| 1 | /auth-setup | Skill | plugin-a | Auth | +| 2 | /ui-builder | Skill | plugin-b | Frontend | +| 3 | context7 | MCP | context7 | Docs | +| 4 | typescript | LSP | ts-tools | LSP | +| .. | ... | ... | ... | ... | + +Total: 30 skills, 3 MCP tools, 2 LSP servers from 10 plugins + +Creet — Recommendation + +> "Build a dashboard with auth" + +Which skill should I run? + /auth-setup (Recommended) — Auth logic for your app + /ui-builder — Dashboard UI components + Other +``` + +Select a skill and Creet runs it immediately. + +## Installation + +### Option 1: Load directly from GitHub (Recommended) + +Clone the repo and load it with `--plugin-dir`: + +```bash +git clone https://github.com/Creeta-creet/creet.git +claude --plugin-dir ./creet +``` + +Then use `/creet:c` inside Claude Code. + +### Option 2: Copy to your commands (Quick setup) + +Copy the skill file to your user-level commands for a shorter `/c` command: + +```bash +mkdir -p ~/.claude/commands +curl -o ~/.claude/commands/c.md https://raw.githubusercontent.com/Creeta-creet/creet/main/skills/c/SKILL.md +``` + +Restart Claude Code, then use `/c` directly. + +### Option 3: Load as a local plugin + +If you already cloned the repo: + +```bash +claude --plugin-dir /path/to/creet +``` + +## Usage + +``` +/c +``` + +### `/c` — Navigate to the best skill + +``` +/c +``` + +| You type | What happens | +| --- | --- | +| `/c build a login page` | Recommends your best auth + frontend skill | +| `/c review my PR` | Recommends your code review skill | +| `/c deploy to production` | Recommends your deployment skill | +| `/c` (no args) | Shows full skill inventory | + +### `/cc` — Run all relevant skills in parallel + +``` +/cc +``` + +Instead of picking one, `/cc` finds **every** relevant skill and runs them all simultaneously as independent agents, then synthesizes the results. + +| You type | What happens | +| --- | --- | +| `/cc build a dashboard with auth` | Runs `/auth`, `/ui-builder`, `/code-review` in parallel — one unified output | +| `/cc review this codebase` | Runs every review-related skill at once | +| `/cc` (no args) | Shows full skill inventory (same as `/c`) | + +**When to use which:** + +| | `/c` | `/cc` | +|---|---|---| +| Goal | Best single skill | All relevant skills | +| Output | One skill's result | Synthesized multi-agent output | +| Speed | Fast | Slower (parallel agents) | +| Use when | You know roughly what you need | You want comprehensive coverage | + +Creet dynamically matches against whatever plugins you have installed. + +## How It Works + +### `/c` — Single skill navigator +1. **Scan** — Detects all installed skills, MCP tools, and LSP servers +2. **Recommend** — Matches your request to the best skill(s) via AskUserQuestion +3. **Execute** — Runs the chosen skill immediately +4. **Discover** — If no match, suggests installable plugins from registry + +### `/cc` — Multi-agent parallel engine +1. **Scan** — Same as `/c` +2. **Multi-Match** — Selects ALL relevant skills (no cap) +3. **Execute** — Launches every matched skill as a parallel Task agent simultaneously +4. **Synthesize** — Collects all outputs and produces a unified result with agreements, conflicts, and next steps + +## Scanner + +Creet's skill scanner automatically detects all plugin types: + +| Type | Detection | What it finds | +| --- | --- | --- | +| **Skill** | `skills/*/SKILL.md` and `commands/*.md` | Slash commands from any plugin | +| **MCP** | `.mcp.json` (direct and wrapper formats) | MCP tool servers | +| **LSP** | `lspServers` in `plugin.json` | Language servers | +| **Hybrid** | Skill + MCP in same plugin | Plugins with both types | + +### Scanner Details + +- Scans `~/.claude/plugins/cache/` for all installed plugins +- Supports YAML frontmatter and markdown table formats for skill metadata +- Extracts trigger keywords (multi-line, 8 languages) +- Auto-detects domain from 24 pattern categories +- Hybrid plugins are marked with `hasMcp` flag + +### Dynamic Keyword Matching + +Creet builds its keyword map **dynamically** from scanner results. Each skill's trigger keywords become the matching dictionary. No hardcoded skill-to-keyword mappings — if a plugin declares triggers, Creet uses them automatically. + +## Features + +- Auto-scans all installed plugins at session start via SessionStart hook +- Detects Skills, MCP tools, and LSP servers from plugin cache +- **Zero hardcoded dependencies** — works with any plugin combination +- Dynamic keyword matching from scanner-extracted triggers +- Uses AskUserQuestion for interactive skill selection +- Compares overlapping skills and explains the difference +- Recommends execution order for multi-skill workflows +- Max 5 recommendations (no overwhelm) +- Responds in your language (EN, KO, JA, ZH, ES, FR, DE, IT) +- Session memory — remembers your most used skills across sessions +- Plugin Discovery — suggests installable plugins when no match found + +## Architecture + +``` +creet/ + .claude-plugin/ + plugin.json # Plugin manifest + marketplace.json # Marketplace registration + skills/c/ + SKILL.md # /c skill — navigate to best skill + skills/cc/ + SKILL.md # /cc skill — run all relevant skills in parallel + hooks/ + hooks.json # Hook registration (SessionStart, UserPromptSubmit) + session-start.js # Session startup: scan + cache + context injection + scripts/ + user-prompt-handler.js # Per-message keyword matching (from cache) + lib/ + skill-scanner.js # Core scanner (Skills, MCP, LSP detection) + keyword-matcher.js # Dynamic keyword matching (no hardcoded maps) + memory-store.js # Session memory persistence + plugin-registry.js # Known plugins registry for discovery + creet.config.json # Configuration +``` + +## Configuration + +`creet.config.json`: + +```json +{ + "version": "1.3.0", + "autoRecommend": true, + "showReport": true, + "minMatchScore": 5, + "memoryPath": null, + "customKeywords": [] +} +``` + +| Option | Default | Description | +| --- | --- | --- | +| `autoRecommend` | `true` | Show skill suggestions in responses | +| `showReport` | `true` | Show Creet tip line when skill matches | +| `minMatchScore` | `5` | Minimum keyword match score for recommendations | +| `memoryPath` | `null` | Custom path for memory file (null = plugin root) | +| `customKeywords` | `[]` | Additional keyword-to-skill mappings | + +## Building Custom Skills with Creet + +Creet is a navigator, but you can build your own skills that take advantage of the same multi-agent patterns. Here's an example: + +**`design-council`** — A skill that summons all installed design agents in parallel, collects their perspectives, and synthesizes a unified design decision. + +```markdown +--- +name: design-council +description: "Summons all installed design agents in parallel and synthesizes the optimal design decision." +user-invocable: true +--- + +## Phase 1 — Scan active design agents +Use Glob to find installed agents under ~/.claude/plugins/cache/. + +## Phase 2 — Parallel deliberation +Launch each agent via Task tool simultaneously. +Each agent analyzes the task from their domain perspective. + +## Phase 3 — Synthesis +Collect all agent outputs and produce a unified recommendation. +``` + +This pattern works for any domain: security councils, code review boards, architecture committees. Creet's scanner will automatically detect and list any such skill you install. + +## Requirements + +- Claude Code v1.0.33+ +- 2+ plugins installed (otherwise you don't need a navigator) + +## License + +MIT diff --git a/plugins/creet/creet.config.json b/plugins/creet/creet.config.json new file mode 100644 index 0000000..3ad82c9 --- /dev/null +++ b/plugins/creet/creet.config.json @@ -0,0 +1,8 @@ +{ + "version": "1.4.0", + "autoRecommend": true, + "showReport": true, + "minMatchScore": 5, + "memoryPath": null, + "customKeywords": [] +} diff --git a/plugins/creet/hooks/hooks.json b/plugins/creet/hooks/hooks.json new file mode 100644 index 0000000..bee1a1f --- /dev/null +++ b/plugins/creet/hooks/hooks.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json.schemastore.org/claude-code-hooks.json", + "description": "Creet v1.5.0 by Creeta - Skill Navigator for Claude Code", + "hooks": { + "SessionStart": [ + { + "once": true, + "hooks": [ + { + "type": "command", + "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/session-start.js", + "timeout": 5000 + } + ] + } + ], + "UserPromptSubmit": [ + { + "hooks": [ + { + "type": "command", + "command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/user-prompt-handler.js", + "timeout": 3000 + } + ] + } + ] + } +} diff --git a/plugins/creet/hooks/session-start.js b/plugins/creet/hooks/session-start.js new file mode 100644 index 0000000..3a3ff78 --- /dev/null +++ b/plugins/creet/hooks/session-start.js @@ -0,0 +1,170 @@ +/** + * Creet - SessionStart Hook + * Scans installed skills, loads memory, and injects context into the session. + */ + +const path = require('path'); +const fs = require('fs'); + +// Resolve plugin root (hooks/ is one level deep) +const PLUGIN_ROOT = path.resolve(__dirname, '..'); + +// Load modules +const { scanInstalledSkills, formatSkillTable } = require(path.join(PLUGIN_ROOT, 'lib', 'skill-scanner')); +const { loadMemory, saveMemory, recordSessionStart, formatMemorySummary } = require(path.join(PLUGIN_ROOT, 'lib', 'memory-store')); +const { formatKeywordTable, saveScanCache } = require(path.join(PLUGIN_ROOT, 'lib', 'keyword-matcher')); +const { KNOWN_PLUGINS } = require(path.join(PLUGIN_ROOT, 'lib', 'plugin-registry')); + +// Load config +let config = {}; +try { + const configPath = path.join(PLUGIN_ROOT, 'creet.config.json'); + if (fs.existsSync(configPath)) { + config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); + } +} catch { + config = {}; +} + +// ── Main ────────────────────────────────────────────────── + +function main() { + try { + // 1. Scan installed skills and cache for UserPromptSubmit hook + const skills = scanInstalledSkills(); + saveScanCache(skills); + const skillTable = formatSkillTable(skills); + + // 2. Load and update memory + const memoryPath = config.memoryPath || null; + const memory = loadMemory(memoryPath); + recordSessionStart(memory); + saveMemory(memory, memoryPath); + const memorySummary = formatMemorySummary(memory); + + // 3. Build keyword table from scan results (dynamic, not hardcoded) + const keywordTable = formatKeywordTable(skills); + + // 4. Build additional context + const additionalContext = buildAdditionalContext({ + skillTable, + memorySummary, + keywordTable, + skillCount: skills.length, + pluginCount: [...new Set(skills.map(s => s.plugin))].length, + config, + }); + + // 5. Output response + const response = { + systemMessage: `Creet v1.5.0 activated - ${skills.length} skills from ${[...new Set(skills.map(s => s.plugin))].length} plugins detected`, + hookSpecificOutput: { + hookEventName: 'SessionStart', + skillCount: skills.length, + sessionNumber: memory.sessionCount, + additionalContext, + }, + }; + + console.log(JSON.stringify(response)); + process.exit(0); + } catch (err) { + // Fail gracefully - don't break the session + const fallback = { + systemMessage: 'Creet v1.5.0 activated (scan skipped)', + hookSpecificOutput: { + hookEventName: 'SessionStart', + error: err.message, + additionalContext: buildFallbackContext(), + }, + }; + console.log(JSON.stringify(fallback)); + process.exit(0); + } +} + +// ── Context Builder ─────────────────────────────────────── + +function buildAdditionalContext({ skillTable, memorySummary, keywordTable, skillCount, pluginCount, config: cfg }) { + const autoRecommend = cfg.autoRecommend !== false; + const showReport = cfg.showReport !== false; + + let ctx = ''; + + // Header + ctx += `# Creet v1.5.0 - Session Startup\n\n`; + + // Skill inventory + ctx += `## Installed Skills (Auto-Scanned)\n\n`; + ctx += skillTable + '\n\n'; + + // Session memory + ctx += `## Session Memory\n\n`; + ctx += memorySummary + '\n\n'; + + // Auto-recommendation rules + if (autoRecommend) { + ctx += `## Auto-Recommendation Rules\n\n`; + ctx += `When the user's message matches keywords below, proactively suggest the matching skill.\n`; + ctx += `Do NOT auto-execute - just mention: "This task matches /skill-name. Want me to run it?"\n\n`; + ctx += keywordTable + '\n\n'; + ctx += `### Recommendation Behavior\n`; + ctx += `- Only suggest if confidence is high (multiple keyword matches)\n`; + ctx += `- If user already specified a skill (e.g., /commit), do NOT re-recommend\n`; + ctx += `- Maximum 2 skill suggestions per message\n`; + ctx += `- Use the user's language for suggestions\n\n`; + } + + // Plugin Discovery Registry + ctx += `## Plugin Discovery Registry\n\n`; + ctx += `When NO installed skill matches the user's request, search this registry and suggest installable plugins.\n\n`; + ctx += `| Plugin | Source | Domain | Description |\n`; + ctx += `|--------|--------|--------|-------------|\n`; + for (const p of KNOWN_PLUGINS) { + ctx += `| ${p.name} | ${p.source} | ${p.domain} | ${p.description} |\n`; + } + ctx += `\n`; + ctx += `### Discovery Rules\n`; + ctx += `- Only show when NO installed skill matches the request\n`; + ctx += `- Match user's request keywords against plugin keywords\n`; + ctx += `- Show max 3 matching plugins with install commands\n`; + ctx += `- Format: "No installed skill matches, but you can install: \`install-command\`"\n`; + ctx += `- Use the user's language for the suggestion\n\n`; + + // Creet usage guide + ctx += `## Quick Commands\n\n`; + ctx += `- \`/c \` - Scan + Recommend + Execute (pick the best skill)\n`; + ctx += `- \`/cc \` - Run ALL relevant skills in parallel + synthesize results\n`; + ctx += `- \`/c\` or \`/cc\` (no args) - Show full skill inventory\n\n`; + + // Report rule + if (showReport) { + ctx += `## Creet Suggestion Line (Recommended for all responses)\n\n`; + ctx += `When a user's request clearly matches an installed skill but they didn't use /c,\n`; + ctx += `add a single-line suggestion at the end of your response:\n\n`; + ctx += '```\n'; + ctx += `── Creet ──────────────────────────────\n`; + ctx += `Tip: /skill-name can help with this task\n`; + ctx += `─────────────────────────────────────────\n`; + ctx += '```\n\n'; + ctx += `Rules:\n`; + ctx += `- Only show when there's a clear skill match\n`; + ctx += `- Don't show if user already used /c or a specific skill\n`; + ctx += `- Don't show for simple questions or chat\n`; + ctx += `- Maximum 1 suggestion per response\n`; + } + + return ctx; +} + +function buildFallbackContext() { + return `# Creet v1.5.0 - Session Startup + +Skill scan was skipped (no plugins cache found or scan error). +Use \`/c \` to manually scan and get recommendations. +`; +} + +// ── Run ─────────────────────────────────────────────────── + +main(); diff --git a/plugins/creet/lib/keyword-matcher.js b/plugins/creet/lib/keyword-matcher.js new file mode 100644 index 0000000..d179aa5 --- /dev/null +++ b/plugins/creet/lib/keyword-matcher.js @@ -0,0 +1,114 @@ +/** + * Creet - Keyword Matcher Module + * Dynamically builds keyword map from scanner results. + * No hardcoded skill names — works with any installed plugin combination. + */ + +const path = require('path'); +const fs = require('fs'); + +const PLUGIN_ROOT = path.resolve(__dirname, '..'); +const CACHE_PATH = path.join(PLUGIN_ROOT, '.creet-cache.json'); + +/** + * Build keyword map dynamically from scanner results. + * Uses each skill's triggers as keywords. + * @param {Array} scanResults - Output from scanInstalledSkills() + * @returns {Array<{skill: string, domain: string, keywords: string[]}>} + */ +function buildKeywordMap(scanResults) { + return scanResults + .filter(s => s.triggers && s.triggers.length > 0) + .map(s => ({ + skill: s.name, + domain: s.domain || 'General', + keywords: s.triggers, + })); +} + +/** + * Save scan results to cache for use by UserPromptSubmit hook. + */ +function saveScanCache(scanResults) { + try { + fs.writeFileSync(CACHE_PATH, JSON.stringify(scanResults), 'utf-8'); + } catch { + // Non-critical — prompt handler will just skip matching + } +} + +/** + * Load cached scan results. + */ +function loadCachedScanResults() { + try { + if (fs.existsSync(CACHE_PATH)) { + return JSON.parse(fs.readFileSync(CACHE_PATH, 'utf-8')); + } + } catch {} + return []; +} + +/** + * Match user message against dynamically built keyword map. + * @param {string} message - User's message + * @param {Array|null} scanResults - Scanner output (null = use cache) + * @param {Array} customMap - Optional custom keyword entries from config + * @returns {Array<{skill: string, domain: string, score: number}>} + */ +function matchKeywords(message, scanResults, customMap) { + const results = scanResults || loadCachedScanResults(); + const keywordMap = buildKeywordMap(results); + + // Merge custom keywords if provided + if (customMap && Array.isArray(customMap)) { + keywordMap.push(...customMap); + } + + const lowerMsg = message.toLowerCase(); + const matches = []; + + for (const entry of keywordMap) { + let score = 0; + for (const keyword of entry.keywords) { + if (lowerMsg.includes(keyword.toLowerCase())) { + score += keyword.length; + } + } + if (score > 0) { + matches.push({ + skill: entry.skill, + domain: entry.domain, + score, + }); + } + } + + return matches.sort((a, b) => b.score - a.score); +} + +/** + * Format keyword table from scan results for session context. + * @param {Array|null} scanResults - Scanner output (null = use cache) + */ +function formatKeywordTable(scanResults) { + const results = scanResults || loadCachedScanResults(); + const keywordMap = buildKeywordMap(results); + + if (keywordMap.length === 0) { + return 'No keyword mappings detected (no skills with triggers found).'; + } + + let table = '| Domain | Skill | Sample Keywords |\n'; + table += '|--------|-------|-----------------|\n'; + + for (const entry of keywordMap) { + const samples = entry.keywords.slice(0, 5).join(', '); + const prefix = entry.domain === 'LSP' ? '' : '/'; + table += `| ${entry.domain} | ${prefix}${entry.skill} | ${samples} |\n`; + } + + return table; +} + +module.exports = { matchKeywords, formatKeywordTable, buildKeywordMap, saveScanCache, loadCachedScanResults }; diff --git a/plugins/creet/lib/memory-store.js b/plugins/creet/lib/memory-store.js new file mode 100644 index 0000000..a0a4a18 --- /dev/null +++ b/plugins/creet/lib/memory-store.js @@ -0,0 +1,159 @@ +/** + * Creet - Memory Store Module + * Persists session data across Claude Code sessions. + */ + +const fs = require('fs'); +const path = require('path'); +const os = require('os'); + +const DEFAULT_MEMORY_FILE = '.creet-memory.json'; + +/** + * Get memory file path. + * Priority: config override → ~/.claude/creet/ (stable across sessions) + */ +function getMemoryPath(configPath) { + if (configPath) { + const dir = path.dirname(configPath); + if (fs.existsSync(dir)) return configPath; + } + + // Stable path under CLAUDE_HOME or ~/.claude/creet/ + const claudeHome = process.env.CLAUDE_HOME || path.join(os.homedir(), '.claude'); + return path.join(claudeHome, 'creet', DEFAULT_MEMORY_FILE); +} + +/** + * Load memory from file. + */ +function loadMemory(configPath) { + const memPath = getMemoryPath(configPath); + + try { + if (fs.existsSync(memPath)) { + return JSON.parse(fs.readFileSync(memPath, 'utf-8')); + } + } catch { + // Corrupted file, start fresh + } + + return createDefaultMemory(); +} + +/** + * Save memory to file. + */ +function saveMemory(memory, configPath) { + const memPath = getMemoryPath(configPath); + + try { + const dir = path.dirname(memPath); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + fs.writeFileSync(memPath, JSON.stringify(memory, null, 2), 'utf-8'); + return true; + } catch { + return false; + } +} + +/** + * Create default memory structure. + */ +function createDefaultMemory() { + return { + version: '1.0.0', + sessionCount: 0, + lastSessionAt: null, + lastUsedSkills: [], + recommendationHistory: [], + skillUsageCount: {}, + }; +} + +/** + * Record a new session start. + */ +function recordSessionStart(memory) { + memory.sessionCount = (memory.sessionCount || 0) + 1; + memory.lastSessionAt = new Date().toISOString(); + return memory; +} + +/** + * Record a skill usage. + */ +function recordSkillUsage(memory, skillName) { + // Update last used + memory.lastUsedSkills = memory.lastUsedSkills || []; + memory.lastUsedSkills = [skillName, ...memory.lastUsedSkills.filter(s => s !== skillName)].slice(0, 10); + + // Update count + memory.skillUsageCount = memory.skillUsageCount || {}; + memory.skillUsageCount[skillName] = (memory.skillUsageCount[skillName] || 0) + 1; + + return memory; +} + +/** + * Record a recommendation. + */ +function recordRecommendation(memory, request, recommendedSkills) { + memory.recommendationHistory = memory.recommendationHistory || []; + memory.recommendationHistory.unshift({ + request: request.substring(0, 100), + skills: recommendedSkills, + at: new Date().toISOString() + }); + // Keep last 20 recommendations + memory.recommendationHistory = memory.recommendationHistory.slice(0, 20); + return memory; +} + +/** + * Get top used skills. + */ +function getTopSkills(memory, limit = 5) { + const counts = memory.skillUsageCount || {}; + return Object.entries(counts) + .sort((a, b) => b[1] - a[1]) + .slice(0, limit) + .map(([name, count]) => ({ name, count })); +} + +/** + * Format memory summary for session context. + */ +function formatMemorySummary(memory) { + const lines = []; + + lines.push(`Session #${memory.sessionCount || 1}`); + + if (memory.lastSessionAt) { + const last = new Date(memory.lastSessionAt); + lines.push(`Last session: ${last.toLocaleDateString()}`); + } + + if (memory.lastUsedSkills && memory.lastUsedSkills.length > 0) { + lines.push(`Recently used: ${memory.lastUsedSkills.slice(0, 5).map(s => '/' + s).join(', ')}`); + } + + const topSkills = getTopSkills(memory, 3); + if (topSkills.length > 0) { + lines.push(`Most used: ${topSkills.map(s => `/${s.name}(${s.count})`).join(', ')}`); + } + + return lines.join('\n'); +} + +module.exports = { + loadMemory, + saveMemory, + recordSessionStart, + recordSkillUsage, + recordRecommendation, + getTopSkills, + formatMemorySummary, +}; diff --git a/plugins/creet/lib/plugin-registry.js b/plugins/creet/lib/plugin-registry.js new file mode 100644 index 0000000..e709410 --- /dev/null +++ b/plugins/creet/lib/plugin-registry.js @@ -0,0 +1,166 @@ +/** + * Creet - Plugin Registry Module + * Curated list of known Claude Code plugins for discovery when no installed skill matches. + */ + +const KNOWN_PLUGINS = [ + // Multi-Agent Orchestration + { + name: 'design-council', + source: 'creet/skills/design-council', + domain: 'Design', + description: '설치된 디자인 스킬 에이전트들을 병렬 소환해 협의 후 최적 설계안 도출', + keywords: ['design council', 'multi-agent design', 'council', 'design debate', '디자인 협의회', + '에이전트 토론', '설계 협의', '다중 에이전트 디자인', '디자인 토론', '협의', + 'frontend design', 'ui council', 'design team'], + installCmd: '(built-in creet skill — already available via /design-council)', + }, + + // Branding & Design + { + name: 'brand-guardian', + source: 'ccplugins/awesome-claude-code-plugins', + domain: 'Branding', + description: 'Brand guidelines, visual consistency, brand asset management', + keywords: ['brand', 'branding', 'guideline', 'visual identity', 'logo', 'color palette', + '브랜딩', '브랜드', '가이드라인', '비주얼', 'ブランド', 'ガイドライン', '品牌', '指南'], + installCmd: 'claude plugin install ccplugins/awesome-claude-code-plugins --skill brand-guardian', + }, + { + name: 'content-creator', + source: 'ccplugins/awesome-claude-code-plugins', + domain: 'Marketing', + description: 'Marketing content, copywriting, messaging', + keywords: ['content', 'copy', 'copywriting', 'marketing', 'messaging', 'landing page copy', + '카피', '마케팅', '콘텐츠', 'コピー', 'マーケティング', '文案', '营销'], + installCmd: 'claude plugin install ccplugins/awesome-claude-code-plugins --skill content-creator', + }, + { + name: 'growth-hacker', + source: 'ccplugins/awesome-claude-code-plugins', + domain: 'Marketing', + description: 'Growth optimization, scaling, market expansion strategies', + keywords: ['growth', 'scale', 'market', 'acquisition', 'retention', 'funnel', + '성장', '마켓', '퍼널', 'グロース', '増殖', '增长', '漏斗'], + installCmd: 'claude plugin install ccplugins/awesome-claude-code-plugins --skill growth-hacker', + }, + { + name: 'product-sales-specialist', + source: 'ccplugins/awesome-claude-code-plugins', + domain: 'Product', + description: 'B2B sales, product design, user research, project management', + keywords: ['product', 'sales', 'b2b', 'user research', 'project management', + '제품', '세일즈', '유저 리서치', '製品', 'セールス', '产品', '销售'], + installCmd: 'claude plugin install ccplugins/awesome-claude-code-plugins --skill product-sales-specialist', + }, + { + name: 'pricing-packaging-specialist', + source: 'ccplugins/awesome-claude-code-plugins', + domain: 'Product', + description: 'B2B pricing strategies, revenue models, packaging optimization', + keywords: ['pricing', 'package', 'revenue', 'monetize', 'subscription', + '가격', '패키지', '수익', '価格', 'パッケージ', '定价', '收入'], + installCmd: 'claude plugin install ccplugins/awesome-claude-code-plugins --skill pricing-packaging-specialist', + }, + { + name: 'app-store-optimizer', + source: 'ccplugins/awesome-claude-code-plugins', + domain: 'Marketing', + description: 'App store listing optimization, metadata, discoverability', + keywords: ['app store', 'aso', 'listing', 'metadata', 'discoverability', + '앱스토어', '최적화', 'ストア', '应用商店', '优化'], + installCmd: 'claude plugin install ccplugins/awesome-claude-code-plugins --skill app-store-optimizer', + }, + + // DevOps & Infrastructure + { + name: 'docker-compose', + source: 'ccplugins/awesome-claude-code-plugins', + domain: 'DevOps', + description: 'Docker Compose configuration and container orchestration', + keywords: ['docker', 'container', 'compose', 'orchestration', + '도커', '컨테이너', 'コンテナ', '容器'], + installCmd: 'claude plugin install ccplugins/awesome-claude-code-plugins --skill docker-compose', + }, + + // Testing & QA + { + name: 'playwright-test', + source: 'ccplugins/awesome-claude-code-plugins', + domain: 'Testing', + description: 'E2E testing with Playwright, browser automation', + keywords: ['test', 'e2e', 'playwright', 'browser test', 'automation', + '테스트', 'E2E', 'テスト', '测试', '自动化'], + installCmd: 'claude plugin install ccplugins/awesome-claude-code-plugins --skill playwright-test', + }, + + // Documentation + { + name: 'superpowers', + source: 'travisvn/claude-code-superpowers', + domain: 'Productivity', + description: 'Brainstorm, write-plan, execute-plan, structured workflows', + keywords: ['brainstorm', 'plan', 'execute', 'workflow', 'ideation', + '브레인스토밍', '기획', '아이디어', 'ブレスト', '企画', '头脑风暴', '策划'], + installCmd: 'claude plugin install travisvn/claude-code-superpowers', + }, + + // Data & Analytics + { + name: 'data-analyst', + source: 'ccplugins/awesome-claude-code-plugins', + domain: 'Data', + description: 'Data analysis, visualization, statistical insights', + keywords: ['data', 'analytics', 'chart', 'visualization', 'statistics', + '데이터', '분석', '차트', 'データ', '分析', '数据', '图表'], + installCmd: 'claude plugin install ccplugins/awesome-claude-code-plugins --skill data-analyst', + }, +]; + +/** + * Search registry for plugins matching a query. + * @param {string} query - User's request + * @returns {Array<{name, source, domain, description, installCmd, score}>} + */ +function searchRegistry(query) { + const lowerQuery = query.toLowerCase(); + const results = []; + + for (const plugin of KNOWN_PLUGINS) { + let score = 0; + for (const keyword of plugin.keywords) { + if (lowerQuery.includes(keyword.toLowerCase())) { + score += keyword.length; + } + } + // Also check domain and description + if (lowerQuery.includes(plugin.domain.toLowerCase())) { + score += 5; + } + if (score > 0) { + results.push({ ...plugin, score }); + } + } + + return results.sort((a, b) => b.score - a.score).slice(0, 3); +} + +/** + * Format search results for display. + */ +function formatRegistryResults(results) { + if (results.length === 0) return null; + + let output = '## Creet — Plugin Discovery\n\n'; + output += 'No installed skill matches, but these plugins might help:\n\n'; + output += '| Plugin | Domain | Description | Install |\n'; + output += '|--------|--------|-------------|--------|\n'; + + for (const r of results) { + output += `| ${r.name} | ${r.domain} | ${r.description} | \`${r.installCmd}\` |\n`; + } + + return output; +} + +module.exports = { searchRegistry, formatRegistryResults, KNOWN_PLUGINS }; diff --git a/plugins/creet/lib/skill-scanner.js b/plugins/creet/lib/skill-scanner.js new file mode 100644 index 0000000..b0129ff --- /dev/null +++ b/plugins/creet/lib/skill-scanner.js @@ -0,0 +1,497 @@ +/** + * Creet - Skill Scanner Module + * Scans installed Claude Code plugins and extracts skill metadata. + * Supports skills/ (SKILL.md), commands/ (.md), MCP tools (.mcp.json), and LSP servers. + */ + +const fs = require('fs'); +const path = require('path'); +const os = require('os'); + +// Resolve plugins cache dir — prefer env vars over hardcoded paths +function resolvePluginsCacheDir() { + // 1. Explicit env override + if (process.env.CLAUDE_PLUGIN_CACHE_DIR) { + return process.env.CLAUDE_PLUGIN_CACHE_DIR; + } + // 2. Derive from CLAUDE_PLUGIN_ROOT (set by Claude Code for the running plugin) + // Structure: /// → go up 3 levels + if (process.env.CLAUDE_PLUGIN_ROOT) { + return path.resolve(process.env.CLAUDE_PLUGIN_ROOT, '..', '..', '..'); + } + // 3. CLAUDE_HOME env override + if (process.env.CLAUDE_HOME) { + return path.join(process.env.CLAUDE_HOME, 'plugins', 'cache'); + } + // 4. Default: ~/.claude/plugins/cache + return path.join(os.homedir(), '.claude', 'plugins', 'cache'); +} + +const PLUGINS_CACHE_DIR = resolvePluginsCacheDir(); + +/** + * Scan all installed plugins and extract skill information. + * @returns {Array<{name: string, plugin: string, description: string, triggers: string[], domain: string, type: string}>} + */ +function scanInstalledSkills() { + const skills = []; + + if (!fs.existsSync(PLUGINS_CACHE_DIR)) { + return skills; + } + + const orgs = safeReadDir(PLUGINS_CACHE_DIR); + + for (const org of orgs) { + const orgPath = path.join(PLUGINS_CACHE_DIR, org); + if (!isDir(orgPath)) continue; + + const plugins = safeReadDir(orgPath); + for (const plugin of plugins) { + const pluginPath = path.join(orgPath, plugin); + if (!isDir(pluginPath)) continue; + + const versions = safeReadDir(pluginPath); + if (versions.length === 0) continue; + + // Use the latest version (last in sorted order) + const latestVersion = versions.sort().pop(); + const versionPath = path.join(pluginPath, latestVersion); + + // Read plugin manifest + const pluginName = readPluginName(versionPath) || plugin; + + // 1. Scan skills/ directory (skills/{skill-name}/SKILL.md) + const skillsDir = path.join(versionPath, 'skills'); + if (fs.existsSync(skillsDir) && isDir(skillsDir)) { + const skillDirs = safeReadDir(skillsDir); + for (const skillDir of skillDirs) { + const skillMdPath = path.join(skillsDir, skillDir, 'SKILL.md'); + if (!fs.existsSync(skillMdPath)) continue; + + const skillData = parseSkillFile(skillMdPath, pluginName); + if (skillData) { + skills.push(skillData); + } + } + } + + // 2. Scan commands/ directory (commands/{name}.md) + const commandsDir = path.join(versionPath, 'commands'); + if (fs.existsSync(commandsDir) && isDir(commandsDir)) { + const commandFiles = safeReadDir(commandsDir).filter(f => f.endsWith('.md')); + for (const cmdFile of commandFiles) { + const cmdPath = path.join(commandsDir, cmdFile); + const cmdName = cmdFile.replace(/\.md$/, ''); + + // Skip if already found via skills/ directory (avoid duplicates) + if (skills.some(s => s.name === cmdName && s.plugin === pluginName)) continue; + + const skillData = parseCommandFile(cmdPath, cmdName, pluginName); + if (skillData) { + skills.push(skillData); + } + } + } + + const pluginHasSkills = skills.some(s => s.plugin === pluginName); + + // 3. Scan for MCP tools (.mcp.json) + const mcpJsonPath = path.join(versionPath, '.mcp.json'); + if (fs.existsSync(mcpJsonPath)) { + const mcpEntries = parseMcpFile(mcpJsonPath, pluginName, versionPath); + if (pluginHasSkills) { + // Hybrid plugin (e.g. sentry): mark existing entries as MCP-enabled + skills.filter(s => s.plugin === pluginName).forEach(s => { s.hasMcp = true; }); + } else { + skills.push(...mcpEntries); + } + } + + // 4. Scan for LSP servers (lspServers in plugin.json) + if (!skills.some(s => s.plugin === pluginName)) { + const lspEntries = parseLspPlugin(versionPath, pluginName); + skills.push(...lspEntries); + } + } + } + + return skills; +} + +/** + * Parse a SKILL.md file and extract metadata. + */ +function parseSkillFile(filePath, pluginName) { + try { + const content = fs.readFileSync(filePath, 'utf-8'); + + // Try YAML frontmatter format first + const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/); + if (fmMatch) { + const data = parseFrontmatter(fmMatch[1], pluginName); + if (data) { + // Also extract triggers from the full content body (after frontmatter) + const body = content.slice(fmMatch[0].length); + if (data.triggers.length === 0) { + data.triggers = extractTriggers(body); + } + return data; + } + } + + // Try table format (| name | description | license |) + const tableRows = content.match(/^\|.*\|$/gm); + if (tableRows && tableRows.length >= 3) { + for (const row of tableRows.slice(2)) { + if (/^\|[\s-|]+\|$/.test(row)) continue; + const cells = row.split('|').map(c => c.trim()).filter(Boolean); + if (cells.length >= 2 && !/^-+$/.test(cells[0])) { + return { + name: cells[0], + plugin: pluginName, + description: cells[1].substring(0, 80), + triggers: extractTriggers(content), + domain: detectDomain(cells[1]), + type: 'skill' + }; + } + } + } + + return null; + } catch { + return null; + } +} + +/** + * Parse a command .md file (commands/{name}.md format). + * These use YAML frontmatter with description, allowed-tools, etc. + */ +function parseCommandFile(filePath, cmdName, pluginName) { + try { + const content = fs.readFileSync(filePath, 'utf-8'); + + const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/); + if (fmMatch) { + const desc = yamlValue(fmMatch[1], 'description') || ''; + const body = content.slice(fmMatch[0].length); + + return { + name: cmdName, + plugin: pluginName, + description: desc.substring(0, 80), + triggers: extractTriggers(desc + '\n' + body), + domain: detectDomain(desc + ' ' + body.substring(0, 200)), + type: 'skill' + }; + } + + // No frontmatter — use first non-empty line as description + const firstLine = content.split('\n').find(l => l.trim() && !l.startsWith('#')); + return { + name: cmdName, + plugin: pluginName, + description: (firstLine || '').substring(0, 80), + triggers: extractTriggers(content), + domain: detectDomain(content.substring(0, 300)), + type: 'skill' + }; + } catch { + return null; + } +} + +/** + * Parse YAML frontmatter (simplified, no external deps). + */ +function parseFrontmatter(yaml, pluginName) { + const name = yamlValue(yaml, 'name'); + const desc = yamlValue(yaml, 'description'); + if (!name) return null; + + // Extract triggers from the full frontmatter text (multi-line description often has Triggers:) + const fullDesc = extractFullDescription(yaml); + + return { + name, + plugin: pluginName, + description: (desc || '').substring(0, 80), + triggers: extractTriggers(fullDesc || desc || ''), + domain: detectDomain(fullDesc || desc || ''), + type: 'skill' + }; +} + +/** + * Extract the full multi-line description block from YAML frontmatter. + * This captures Triggers: lines that yamlValue() truncates. + */ +function extractFullDescription(yaml) { + const normalized = yaml.replace(/\r\n/g, '\n'); + + // Match description block scalar (| or >) + // Empty lines are valid inside YAML block scalars, so we can't use a simple regex. + const blockStart = normalized.match(/^description:\s*[|>]\s*\n/m); + if (blockStart) { + const startIdx = blockStart.index + blockStart[0].length; + const lines = normalized.slice(startIdx).split('\n'); + const blockLines = []; + for (const line of lines) { + // Block ends at a non-indented, non-empty line (next YAML key) + if (line.length > 0 && !line.startsWith(' ') && !line.startsWith('\t')) break; + blockLines.push(line); + } + return blockLines.join('\n'); + } + + // Match single-line description + const lineMatch = normalized.match(/^description:\s*(.+)$/m); + if (lineMatch) { + return lineMatch[1].trim(); + } + + return null; +} + +/** + * Extract a simple YAML value (single line or first line of multi-line). + */ +function yamlValue(yaml, key) { + const normalized = yaml.replace(/\r\n/g, '\n'); + + // Multi-line block scalar (description: | or description: >) + const multiMatch = normalized.match(new RegExp(`^${key}:\\s*[|>]\\s*\\n((?:[ \\t]+[^\\n]*\\n?)*)`, 'm')); + if (multiMatch) { + // Return only the first meaningful line (before Triggers:) + const lines = multiMatch[1].split('\n').map(l => l.trim()).filter(Boolean); + const firstLines = []; + for (const line of lines) { + if (/^Triggers?:/i.test(line)) break; + if (/^argument-hint:/i.test(line)) break; + firstLines.push(line); + } + return firstLines.join(' ') || lines[0] || null; + } + + // Single-line value + const match = normalized.match(new RegExp(`^${key}:\\s*(.+)$`, 'm')); + if (match) { + const val = match[1].trim().replace(/^["']|["']$/g, ''); + if (val === '|' || val === '>') return null; + return val; + } + + return null; +} + +/** + * Extract trigger keywords from text. + */ +function extractTriggers(text) { + // Match "Triggers:" or "Trigger:" followed by comma-separated keywords (possibly multi-line) + const triggerMatch = text.match(/Triggers?:\s*([\s\S]*?)(?:\n\n|\nargument-hint:|\nuser-invocable:|\nallowed-tools:|\n[A-Z#]|$)/i); + if (!triggerMatch) return []; + + return triggerMatch[1] + .split(/[,\n]/) + .map(t => t.trim()) + .filter(t => t.length > 0 && t.length < 40 && !/^[-|]/.test(t)); +} + +/** + * Auto-detect domain from description. + */ +function detectDomain(desc) { + const lower = desc.toLowerCase(); + const domainMap = [ + [/sentry|error track|monitoring|logging|metric|tracing/i, 'Monitoring'], + [/security|vulnerability|owasp|xss|csrf|seo.*security/i, 'Security'], + [/deploy|ci\/cd|infrastructure|kubernetes|docker|production.*environment/i, 'DevOps'], + [/mobile|react native|flutter|expo|ios|android/i, 'Mobile'], + [/desktop|electron|tauri/i, 'Desktop'], + [/pdca|plan.*design|development.*pipeline|phase|workflow|9-phase/i, 'Workflow'], + [/fullstack|baas|bkend.*platform|dynamic.*web/i, 'Fullstack'], + [/auth|login|signup|jwt|oauth|social login|password|rbac/i, 'Auth'], + [/database|db |table|crud|query|data model/i, 'Database'], + [/storage|upload|file.*stor|bucket|presigned|download.*cdn/i, 'Storage'], + [/backend|api(?!\s*key)|server|endpoint|rest\s*api/i, 'Backend'], + [/frontend|ui\b|component|design system|css|layout|mockup|wireframe/i, 'Frontend'], + [/git |commit|branch|pull request|merge|pr review|push/i, 'Git'], + [/code.?review|audit|quality|analyz|convention|coding.*rule/i, 'Quality'], + [/test|qa |zero.?script/i, 'QA'], + [/learn|guide|beginner|tutorial|education|onboarding|quickstart/i, 'Learning'], + [/template|document.*template/i, 'Template'], + [/cookbook|recipe|troubleshoot/i, 'Guide'], + [/brand|visual.*identity|guideline.*brand/i, 'Branding'], + [/enterprise|microservice|monorepo/i, 'Enterprise'], + [/plugin.*help|function.*list|skill.*navigat/i, 'Navigator'], + [/claude\.?md|project.*memory|config/i, 'Config'], + [/schema|terminology|data.*structure|entity/i, 'Schema'], + [/sdk|agent.*develop/i, 'SDK'], + ]; + + for (const [pattern, domain] of domainMap) { + if (pattern.test(lower)) return domain; + } + return 'General'; +} + +/** + * Parse .mcp.json file and create entries for MCP tool servers. + */ +function parseMcpFile(filePath, pluginName, versionPath) { + try { + let mcpConfig = JSON.parse(fs.readFileSync(filePath, 'utf-8')); + const pluginDesc = readPluginDescription(versionPath); + const results = []; + + // Unwrap {"mcpServers": {...}} wrapper format (used by sentry, etc.) + if (mcpConfig.mcpServers && typeof mcpConfig.mcpServers === 'object') { + mcpConfig = mcpConfig.mcpServers; + } + + for (const [serverName, serverConfig] of Object.entries(mcpConfig)) { + const serverType = serverConfig.type === 'http' ? 'HTTP' : 'NPX'; + const desc = pluginDesc || `MCP ${serverType} tool server`; + + results.push({ + name: serverName, + plugin: pluginName, + description: desc.substring(0, 80), + triggers: [], + domain: detectDomain(desc + ' ' + serverName), + type: 'mcp', + mcpType: serverType.toLowerCase() + }); + } + + return results; + } catch { + return []; + } +} + +/** + * Parse LSP server config from plugin.json. + */ +function parseLspPlugin(versionPath, pluginName) { + const pluginJsonPaths = [ + path.join(versionPath, '.claude-plugin', 'plugin.json'), + path.join(versionPath, 'plugin.json'), + ]; + + for (const p of pluginJsonPaths) { + try { + const data = JSON.parse(fs.readFileSync(p, 'utf-8')); + if (!data.lspServers || typeof data.lspServers !== 'object') continue; + + // lspServers is an object: { "serverName": { command, args, extensionToLanguage } } + return Object.entries(data.lspServers).map(([serverName, serverConfig]) => { + const langs = serverConfig.extensionToLanguage + ? [...new Set(Object.values(serverConfig.extensionToLanguage))] + : []; + + return { + name: serverName, + plugin: pluginName, + description: (data.description || `${serverName} language server`).substring(0, 80), + triggers: langs, + domain: 'LSP', + type: 'lsp', + languages: langs + }; + }); + } catch { + continue; + } + } + return []; +} + +/** + * Read plugin description from manifest. + */ +function readPluginDescription(versionPath) { + const paths = [ + path.join(versionPath, '.claude-plugin', 'plugin.json'), + path.join(versionPath, 'plugin.json'), + ]; + + for (const p of paths) { + try { + const data = JSON.parse(fs.readFileSync(p, 'utf-8')); + return data.description || null; + } catch { + continue; + } + } + return null; +} + +/** + * Read plugin name from manifest. + */ +function readPluginName(versionPath) { + const paths = [ + path.join(versionPath, '.claude-plugin', 'plugin.json'), + path.join(versionPath, 'plugin.json'), + ]; + + for (const p of paths) { + try { + const data = JSON.parse(fs.readFileSync(p, 'utf-8')); + return data.name || null; + } catch { + continue; + } + } + return null; +} + +function safeReadDir(dir) { + try { + return fs.readdirSync(dir).filter(f => !f.startsWith('.')); + } catch { + return []; + } +} + +function isDir(p) { + try { + return fs.statSync(p).isDirectory(); + } catch { + return false; + } +} + +/** + * Format skills as a markdown table. + */ +function formatSkillTable(skills) { + if (skills.length === 0) return 'No plugins detected.'; + + const plugins = [...new Set(skills.map(s => s.plugin))]; + const typeLabels = { skill: 'Skill', mcp: 'MCP', lsp: 'LSP' }; + const skillCount = skills.filter(s => s.type === 'skill').length; + const mcpCount = skills.filter(s => s.type === 'mcp').length; + const lspCount = skills.filter(s => s.type === 'lsp').length; + + let table = `| # | Name | Type | Plugin | Domain | Description |\n`; + table += `|---|------|------|--------|--------|-------------|\n`; + + skills.forEach((s, i) => { + const typeLabel = typeLabels[s.type] || 'Skill'; + const prefix = s.type === 'skill' ? '/' : ''; + table += `| ${i + 1} | ${prefix}${s.name} | ${typeLabel} | ${s.plugin} | ${s.domain} | ${s.description.substring(0, 50)} |\n`; + }); + + const parts = [`${skillCount} skills`]; + if (mcpCount > 0) parts.push(`${mcpCount} MCP tools`); + if (lspCount > 0) parts.push(`${lspCount} LSP servers`); + table += `\nTotal: ${parts.join(', ')} from ${plugins.length} plugins`; + return table; +} + +module.exports = { scanInstalledSkills, formatSkillTable, detectDomain }; diff --git a/plugins/creet/scripts/user-prompt-handler.js b/plugins/creet/scripts/user-prompt-handler.js new file mode 100644 index 0000000..a19ae54 --- /dev/null +++ b/plugins/creet/scripts/user-prompt-handler.js @@ -0,0 +1,149 @@ +/** + * Creet - UserPromptSubmit Hook + * Analyzes user input and suggests matching skills. + */ + +const path = require('path'); +const fs = require('fs'); + +const PLUGIN_ROOT = path.resolve(__dirname, '..'); + +// Load config +let config = {}; +try { + const configPath = path.join(PLUGIN_ROOT, 'creet.config.json'); + if (fs.existsSync(configPath)) { + config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); + } +} catch { + config = {}; +} + +// Skip if auto-recommend is disabled +if (config.autoRecommend === false) { + console.log(JSON.stringify({ systemMessage: '' })); + process.exit(0); +} + +const { matchKeywords } = require(path.join(PLUGIN_ROOT, 'lib', 'keyword-matcher')); +const { searchRegistry } = require(path.join(PLUGIN_ROOT, 'lib', 'plugin-registry')); + +function main() { + try { + // Read user message from stdin (Claude Code passes it via environment or stdin) + const input = getInput(); + if (!input || !input.userMessage) { + console.log(JSON.stringify({ systemMessage: '' })); + process.exit(0); + } + + const message = input.userMessage; + + // Override: When user explicitly invokes a skill command, inject priority instruction + if (message.startsWith('/')) { + const skillName = message.split(/\s+/)[0].substring(1); + console.log(JSON.stringify({ + systemMessage: `OVERRIDE — HIGHEST PRIORITY: The user explicitly invoked the /${skillName} skill command. You MUST execute this skill immediately using the Skill tool. Do NOT override this with AskUserQuestion or any other action. Explicit skill commands from the user always take the highest priority over any other hook instructions.` + })); + process.exit(0); + } + + // Skip very short messages + if (message.length < 5) { + console.log(JSON.stringify({ systemMessage: '' })); + process.exit(0); + } + + // Match keywords dynamically against cached scan results + const customMap = config.customKeywords || null; + const matches = matchKeywords(message, null, customMap); + + // Take top match (only suggest if score is high enough) + const MIN_SCORE = config.minMatchScore || 5; + + if (matches.length > 0 && matches[0].score >= MIN_SCORE) { + // Installed skill match found + const suggestions = matches.slice(0, 2).map(m => `/${m.skill}`).join(', '); + const hint = `Creet detected skill match: ${suggestions} may help with this request. Suggest it naturally if appropriate.`; + + const response = { + systemMessage: hint, + hookSpecificOutput: { + hookEventName: 'UserPromptSubmit', + matchType: 'installed', + matchedSkills: matches.slice(0, 2), + userMessage: message.substring(0, 50), + }, + }; + + console.log(JSON.stringify(response)); + process.exit(0); + } + + // No installed skill match — search plugin registry + const registryMatches = searchRegistry(message); + + if (registryMatches.length > 0) { + const pluginNames = registryMatches.map(r => r.name).join(', '); + const hint = `Creet: No installed skill matches, but external plugins may help: ${pluginNames}. Suggest them if the user uses /c.`; + + const response = { + systemMessage: hint, + hookSpecificOutput: { + hookEventName: 'UserPromptSubmit', + matchType: 'registry', + suggestedPlugins: registryMatches.map(r => ({ + name: r.name, + domain: r.domain, + description: r.description, + installCmd: r.installCmd, + })), + userMessage: message.substring(0, 50), + }, + }; + + console.log(JSON.stringify(response)); + process.exit(0); + } + + // No match at all + console.log(JSON.stringify({ systemMessage: '' })); + process.exit(0); + } catch { + console.log(JSON.stringify({ systemMessage: '' })); + process.exit(0); + } +} + +/** + * Get input from environment or stdin. + */ +function getInput() { + // Try environment variable first + if (process.env.CLAUDE_USER_MESSAGE) { + return { userMessage: process.env.CLAUDE_USER_MESSAGE }; + } + + // Try reading from stdin (cross-platform: fd 0) + try { + const stdinData = fs.readFileSync(0, 'utf-8'); + if (stdinData) { + return JSON.parse(stdinData); + } + } catch { + // stdin not available + } + + // Try reading from process.argv + if (process.argv[2]) { + try { + return JSON.parse(process.argv[2]); + } catch { + return { userMessage: process.argv[2] }; + } + } + + return null; +} + +main(); diff --git a/plugins/creet/skills/c/SKILL.md b/plugins/creet/skills/c/SKILL.md new file mode 100644 index 0000000..d222442 --- /dev/null +++ b/plugins/creet/skills/c/SKILL.md @@ -0,0 +1,69 @@ +--- +name: c +description: "Creet v1.5.0 — Scan all installed plugins (Skills, MCP tools, LSP servers), recommend the best match, and execute. For parallel multi-agent execution, use /cc." +argument-hint: "" +user-invocable: true +--- + +| name | description | license | +|------|-------------|---------| +| c | Creet v1.5.0 — Scan all installed plugins (Skills, MCP tools, LSP servers), recommend the best match, and execute. Use /cc to run ALL relevant skills in parallel. | MIT | + +Triggers: creet, navigate, which skill, what skill, find skill, recommend, scan skills, skill list, +어떤 스킬, 스킬 찾기, 추천, 스킬 목록, どのスキル, スキル検索, 推荐, 哪个技能, +qué skill, quel skill, welches Skill, quale skill + +You are **Creet**, a skill navigator for Claude Code. + +## Workflow + +Always follow this 4-phase flow: + +### 1. Scan + +Read ALL available skills from the session context (slash commands listed under "The following skills are available") and display them in a table grouped by plugin: + +| # | Name | Type | Plugin | Domain | What it does | +|---|------|------|--------|--------|--------------| + +- **Type**: Skill (slash commands), MCP (tool servers), LSP (language servers) +- End with: `Total: X skills, Y MCP tools, Z LSP servers from N plugins` +- If a plugin has both Skills and MCP, note it as hybrid + +### 2. Recommend + +If the user provided a request (not just "list skills"): +- Analyze the request and match against installed skills +- Use **AskUserQuestion** (header: "Creet") to let the user choose +- Each option: label = `/skill-name`, description = role + reason +- Add `(Recommended)` to the best match +- Never ask y/n in plain text — always use AskUserQuestion +- Max recommendations: 1 for simple, 2-3 for medium, 5 for complex requests + +### 3. Execute + +When the user selects a skill, immediately invoke it using the Skill tool. +Pass the user's original request as context. No extra explanation needed. + +### 4. Discover + +If NO installed skill matches: +- Search the Creet plugin registry (injected via session context) for matching plugins +- Present results via AskUserQuestion with install info +- If registry has no match either, suggest searching the plugin marketplace + +## Matching Rules + +- Prefer specific over generic (e.g., a dedicated auth skill over a general fullstack skill) +- For multi-domain requests, recommend skills in execution order +- For ambiguous matches, briefly compare the options +- Read context clues: active PDCA phase, beginner signals, framework in package.json + +## Rules + +- ONLY recommend skills actually available in this session — never invent names +- Respond in the user's language +- No emojis unless the user uses them +- If user already specified a skill (e.g., `/commit`), do NOT re-recommend +- `/c` with no args = show full inventory only (skip recommend phase) +- For running ALL relevant skills simultaneously, direct the user to `/cc` diff --git a/plugins/creet/skills/cc/SKILL.md b/plugins/creet/skills/cc/SKILL.md new file mode 100644 index 0000000..80d8c18 --- /dev/null +++ b/plugins/creet/skills/cc/SKILL.md @@ -0,0 +1,137 @@ +--- +name: cc +description: "Creet Multi v1.0 — Find ALL relevant skills and run them in parallel as a multi-agent team. Synthesizes all outputs into one unified result." +argument-hint: "" +user-invocable: true +--- + +| name | description | license | +|------|-------------|---------| +| cc | Creet Multi v1.0 — Parallel multi-skill execution. Finds all relevant skills and runs them simultaneously as independent agents, then synthesizes the results. | MIT | + +Triggers: run all, parallel, multi-skill, all at once, all agents, simultaneously, 동시 실행, 멀티 에이전트, 한꺼번에, 전부 실행, 병렬, 모든 스킬, +同時実行, 並列, マルチエージェント, 并行, 同时执行, 多代理, +ejecutar todo, paralelo, tous les skills, parallèle, alle Skills, parallel, eseguire tutto, parallelo + +You are **Creet Multi**, the parallel execution engine of Creet. + +Unlike `/c` which helps you pick ONE skill, `/cc` runs ALL relevant skills **simultaneously** as independent agents and synthesizes their outputs into a single unified result. + +## Workflow + +### 1. Scan + +Read ALL available skills from the session context (slash commands listed under "The following skills are available") and identify the full inventory: + +| # | Name | Type | Plugin | Domain | What it does | +|---|------|------|--------|--------|--------------| + +### 2. Multi-Match + +Analyze the user's request and select **ALL** skills that are meaningfully relevant: + +- Match semantically against each skill's name, domain, and description +- **No top-N cap** — if 8 skills are relevant, select all 8 +- Threshold: clearly connected to the request (skip tangential matches) +- Group selected skills by domain to verify coverage + +Display the execution plan: + +``` +Creet Multi — Parallel Execution Plan +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +→ /skill-a [Domain] why it's relevant +→ /skill-b [Domain] why it's relevant +→ /skill-c [Domain] why it's relevant + +Total: N skills will run in parallel +``` + +### 3. Confirm + +- **N ≤ 5**: proceed automatically without asking +- **N > 5**: use AskUserQuestion (header: "Creet Multi") to confirm or let user deselect skills before proceeding + +### 4. Find Skill Files + +For each selected skill, locate its SKILL.md file. Try these paths in order: + +1. `~/.claude/commands/{skill-name}.md` — user-level command +2. `~/.claude/plugins/cache/*/*/skills/{skill-name}/SKILL.md` — plugin skill (Glob) +3. `~/.claude/plugins/cache/*/*/*/skills/{skill-name}/SKILL.md` — nested org structure (Glob) + +Use Glob to search when the exact path is uncertain. If a file is not found, use the skill's description from the session context as a minimal prompt instead. + +### 5. Parallel Execution + +Launch **all matched skills simultaneously** in a single message — one Task per skill. + +For each skill, spawn a `general-purpose` Task with this prompt structure: + +``` +You are acting as the [{skill-name}] skill agent. + +## Your Role & Instructions +{full content of SKILL.md — or description if file not found} + +--- + +## Task +{user's original request verbatim} + +## Project Context (if relevant) +{brief summary of current directory/files if the task involves code} + +## Output Rules +- Complete your role fully as defined above +- Be specific and actionable +- Do not reference other agents or this orchestration setup +- Respond in {user's language} +``` + +Launch ALL tasks simultaneously. Do NOT wait for one to finish before starting the next. + +### 6. Synthesize + +After all tasks complete, present results in this format: + +``` +╔══════════════════════════════════════════════════╗ +║ Creet Multi — Results ║ +╚══════════════════════════════════════════════════╝ + +━━━ /skill-a ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +[skill-a full output] + +━━━ /skill-b ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +[skill-b full output] + +... + +╔══════════════════════════════════════════════════╗ +║ Synthesis ║ +╚══════════════════════════════════════════════════╝ + +## Points of Agreement +[What multiple skills consistently recommended] + +## Conflicts & Trade-offs +[Where skills disagreed — and the reasoning behind each position] + +## Recommended Next Steps +[Concrete, prioritized actions combining the best of all outputs] +``` + +## Fallback Behavior + +- If **no skills match** the request: fall back to `/c` workflow (single best recommendation) +- If **a skill errors or times out**: note it briefly and continue with the remaining agents +- If **only 1 skill matches**: run it directly via Skill tool (no need for multi-agent overhead) + +## Rules + +- ONLY use skills listed in the session context — never invent skill names +- `/cc` with no args = show full inventory (same as `/c`) +- Respond in the user's language throughout +- Do not expose internal orchestration details (file paths, Task IDs) to the user +- Keep each skill's section clearly separated — do not merge outputs before the Synthesis block