
Skill Integrator
- 283 installs
- 133 repo stars
- Updated February 24, 2026
- jwynia/agent-skills
skill-integrator is a meta Claude Code skill that inventories installed skills and generates trigger-based CLAUDE.md guidance for developers whose agents ignore silently installed capabilities.
About
skill-integrator is a meta utility skill (version 1.0) in jwynia/agent-skills that closes the gap between installed skills and agent awareness. It runs a Deno pipeline—analyze-project.ts, scan-skills.ts, and generate-guidance.ts—to detect project type, inventory .claude/skills, score relevance, and emit a marked guidance section for CLAUDE.md or AGENTS.md. Four diagnostic states (SI0–SI3) cover first-time setup, stale sections after skill changes, single-skill additions, and misfit trigger categories. Developers reach for skill-integrator after installing skills, bootstrapping a new repo, or when agents never invoke available tools despite a full skills directory. Single-skill mode adds one entry; full mode regenerates the entire <!-- Generated by skill-integrator --> block with project-specific trigger phrasing instead of echoing SKILL.md descriptions verbatim.
- Merges multiple skills into one workflow
- Resolves trigger overlap and precedence
- Improves maintainable agent skill stacks
- Standardizes how skills compose in repos
- Speeds up multi-skill Claude Code setups
Skill Integrator by the numbers
- 283 all-time installs (skills.sh)
- +2 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #153 of 782 Skill Development skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/jwynia/agent-skills --skill skill-integratorAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 283 |
|---|---|
| repo stars | ★ 133 |
| Last updated | February 24, 2026 |
| Repository | jwynia/agent-skills ↗ |
How do you wire installed Claude skills into CLAUDE.md?
Combine multiple Claude Code skills into a coherent agent setup with consistent triggers, precedence, and workflow boundaries so capabilities work together without conflict.
Who is it for?
Engineers maintaining Claude Code or Codex projects with growing .claude/skills directories that agents fail to invoke.
Skip if: Repositories without installable agent skills or teams that manage triggers manually in a single static CLAUDE.md.
When should I use this skill?
Skills are installed but agents do not use them, after adding a new skill, or when CLAUDE.md skill guidance is missing or stale.
What you get
Marked CLAUDE.md or AGENTS.md skill-guidance section with trigger categories, relevance scores, and per-skill invocation rules.
- CLAUDE.md skill guidance section
- Skill relevance scores
- Trigger category assignments
By the numbers
- Bundles 3 Deno pipeline scripts for project analysis, skill scanning, and guidance generation
- Defines 4 diagnostic integration states labeled SI0 through SI3
- Published at skill metadata version 1.0
Files
Skill Integrator: Surface Skills Where They're Needed
You bridge the gap between installed skills and agent awareness. Your role is to analyze project context, score installed skills for relevance, and generate actionable trigger-based guidance that gets inserted into CLAUDE.md or AGENTS.md.
Core Principle
Skills only help when agents know to reach for them. A hundred installed skills are useless if the agent doesn't know when each one applies. This skill transforms a silent inventory into active, contextual guidance.
Quick Reference
| Situation | Command |
|---|---|
| First-time setup | /skill-integrator → generates full guidance section |
| After installing a skill | /skill-integrator <skill-name> → adds single skill |
| Guidance feels stale | /skill-integrator → regenerates with current inventory |
| Check what scripts do | --help flag on any script |
Pipeline:
deno run --allow-read scripts/analyze-project.ts --json > /tmp/ctx.json
deno run --allow-read scripts/scan-skills.ts --json > /tmp/skills.json
deno run --allow-read scripts/generate-guidance.ts --project-context /tmp/ctx.json --skills /tmp/skills.jsonThe States
State SI0: No Integration
Symptoms: Project has skills installed in .claude/skills/ but CLAUDE.md/AGENTS.md has no skill guidance section. Agents work without awareness of available tools. Key Questions: How many skills are installed? What project type is this? Does CLAUDE.md even exist? Interventions: Run full analysis pipeline: analyze-project → scan-skills → generate-guidance. Generate comprehensive trigger-based section. Present to user for insertion.
State SI1: Stale Integration
Symptoms: A skill guidance section exists (look for <!-- Generated by skill-integrator comment) but skills have been added or removed since it was last generated. Installed skill count doesn't match documented count. Key Questions: Which skills were added since last generation? Which were removed? Has the project type changed? Interventions: Run pipeline again with current state. Diff against existing section. Present changes for approval. Surgically update the marked section.
State SI2: Single Skill Addition
Symptoms: User just installed one skill and wants it integrated into existing guidance. Or user invokes /skill-integrator <skill-name>. Key Questions: What does this skill do? Which trigger category does it belong in? Where in the existing guidance should it go? Interventions: Run single-skill mode: scan the new skill, score against project context, generate a single guidance line with trigger category placement. Insert into existing section at the right location.
State SI3: Wrong Fit
Symptoms: Guidance references irrelevant skills, wrong trigger categories, or generic descriptions that don't help agents decide when to use a skill. Skills are listed but not effectively surfaced. Key Questions: Are trigger categories appropriate for this project type? Are descriptions actionable or just echoing SKILL.md? Does the threshold need adjusting? Interventions: Re-run with adjusted threshold. Review trigger category assignments. Customize descriptions to project-specific contexts rather than generic skill descriptions.
Diagnostic Process
When invoked:
1. Check existing state - Does CLAUDE.md/AGENTS.md exist? Does it have a <!-- Generated by skill-integrator section? Count installed vs documented skills.
2. Determine state - Match to SI0 (no section), SI1 (stale section), SI2 (single skill addition), or SI3 (wrong fit).
3. Run analyze-project.ts to detect project type, domains, and tech stack:
deno run --allow-read scripts/analyze-project.ts --jsonSave output as project context.
4. Run scan-skills.ts to inventory all installed skills:
deno run --allow-read scripts/scan-skills.ts --jsonOr for single skill mode:
deno run --allow-read scripts/scan-skills.ts --skill <name> --json5. Run generate-guidance.ts to produce the guidance section:
deno run --allow-read scripts/generate-guidance.ts --project-context ctx.json --skills skills.jsonOr for single skill:
deno run --allow-read scripts/generate-guidance.ts --skill <name> --project-context ctx.json --skills skills.json6. Review and refine - The scripts produce structured data; use your judgment to:
- Adjust trigger descriptions to be project-specific
- Verify trigger category assignments make sense
- Ensure descriptions answer "when should I use this?" not "what does this do?"
7. Present to user - Show the generated guidance section. Apply on approval by inserting/replacing the marked section in CLAUDE.md or AGENTS.md.
Two Modes
All-Skills Mode (default)
Invoked with /skill-integrator or when diagnosing SI0/SI1.
- Analyzes all installed skills against project context
- Generates comprehensive trigger-based section
- Groups skills by trigger category (When Planning, When Writing Code, etc.)
- Only includes skills scoring >= threshold (default 0.3)
Single-Skill Mode
Invoked with /skill-integrator <skill-name> or when diagnosing SI2.
- Analyzes one specific skill against project context
- Generates a single guidance line with placement recommendation
- Shows scoring breakdown for transparency
Available Tools
analyze-project.ts
Detects project type from indicator files, file extensions, and installed skills.
deno run --allow-read scripts/analyze-project.ts
deno run --allow-read scripts/analyze-project.ts --path /some/project
deno run --allow-read scripts/analyze-project.ts --jsonOutput: ProjectContext JSON with type, domains, techStack, fileTypes, hasContextNetwork, skillsInstalled.
scan-skills.ts
Parses all installed skill SKILL.md files and extracts metadata.
deno run --allow-read scripts/scan-skills.ts
deno run --allow-read scripts/scan-skills.ts --skill code-review
deno run --allow-read scripts/scan-skills.ts --jsonOutput: Array of SkillInfo objects with name, description, domain, type, mode, keywords, triggerPhrases.
generate-guidance.ts
Scores skills for relevance and generates formatted guidance.
deno run --allow-read scripts/generate-guidance.ts --project-context ctx.json --skills skills.json
deno run --allow-read scripts/generate-guidance.ts --format trigger --threshold 0.3 --project-context ctx.json --skills skills.json
deno run --allow-read scripts/generate-guidance.ts --skill code-review --project-context ctx.json --skills skills.json
deno run --allow-read scripts/generate-guidance.ts --format table --json --project-context ctx.json --skills skills.jsonFormats: trigger (default, grouped by when-to-use), domain (grouped by skill domain), table (relevance score table).
Key Questions
For Initial Integration (SI0)
- How many skills are installed?
- What is the primary project type?
- Does CLAUDE.md already exist, or does it need to be created?
- Are there project-specific trigger contexts beyond the defaults?
For Updates (SI1, SI2)
- Which skills changed since last integration?
- Has the project type or tech stack changed?
- Are existing trigger descriptions still accurate?
For Quality (SI3)
- Are agents actually using the guided skills?
- Are trigger descriptions actionable or just descriptive?
- Is the threshold too low (too many irrelevant skills) or too high (missing useful ones)?
Anti-Patterns
The Skill Dump
Pattern: Listing all installed skills regardless of relevance. Problem: Information overload. Agents can't distinguish useful from irrelevant. Guidance becomes noise. Fix: Use threshold-based scoring. Only include skills with relevance >= 0.3. Quality over quantity. Detection: More than 30 skills in the guidance section, or skills with no domain overlap appearing.
The Feature Echo
Pattern: Duplicating SKILL.md descriptions verbatim in the guidance. Problem: Descriptions answer "what does this do?" instead of "when should I use this?" Agents need trigger context, not feature lists. Fix: Transform descriptions into trigger-based guidance. "Use when [situation]" not "This skill [capability]." Detection: Guidance text matches SKILL.md description word-for-word.
The Static Guide
Pattern: Generating guidance once and never updating after adding/removing skills. Problem: Guidance becomes stale. New skills go unused. Removed skills cause confusion. Fix: Re-run skill-integrator after any skill installation. Use the timestamp comment to track freshness. Detection: Timestamp in <!-- Generated by skill-integrator comment is more than 30 days old while skills were recently changed.
Example Interaction
User: "I just installed a bunch of skills but my agent doesn't seem to know about them."
Your approach: 1. Identify State SI0 (no guidance section in CLAUDE.md) 2. Run analyze-project.ts --json to detect project type 3. Run scan-skills.ts --json to inventory installed skills 4. Run generate-guidance.ts with project context and skills data 5. Review output, adjust descriptions for project context 6. Present trigger-based guidance section to user 7. On approval, insert into CLAUDE.md with timestamped markers
User: "/skill-integrator code-review"
Your approach: 1. Identify State SI2 (single skill addition) 2. Run scan-skills.ts --skill code-review --json 3. Run analyze-project.ts --json for context 4. Run generate-guidance.ts --skill code-review for targeted scoring 5. Present the recommended guidance line and trigger category placement 6. On approval, insert into the existing guidance section
What You Do NOT Do
- You do not rewrite SKILL.md files or modify skills themselves
- You do not install or uninstall skills
- You do not modify CLAUDE.md without user approval
- You do not include skills below the relevance threshold without explicit request
- You do not generate guidance for skills that don't have SKILL.md files
- You diagnose and generate; the user decides what goes into their config
Integration Graph
Inbound (From Other Skills)
| Source Skill | Source State | Leads to State |
|---|---|---|
| skill-builder | After creating a new skill | SI2: Single Skill Addition |
| find-skills | After installing a skill | SI2: Single Skill Addition |
Outbound (To Other Skills)
| This State | Leads to Skill | Target State |
|---|---|---|
| SI3: Wrong Fit | skill-builder | Quality issues found during analysis |
Complementary Skills
| Skill | Relationship |
|---|---|
| context-network | Guidance section lives in CLAUDE.md which context-network also manages |
| agent-bootstrap | New project setup benefits from immediate skill integration |
| skill-builder | After building a skill, integrate it into project guidance |
| find-skills | After discovering and installing skills, surface them via guidance |
Reasoning Requirements
Standard Reasoning
- Running scripts and interpreting output
- Matching skills to trigger categories
- Basic state identification (SI0-SI3)
Extended Reasoning (ultrathink)
Use extended thinking for:
- Customizing trigger descriptions to project-specific contexts - [Why: requires understanding both skill capability and project needs]
- Evaluating threshold adjustments when many skills are borderline - [Why: trade-off analysis between coverage and noise]
- Resolving SI3 wrong-fit situations - [Why: requires reasoning about why current categorizations fail]
Trigger phrases: "customize guidance", "refine triggers", "adjust threshold"
Execution Strategy
Sequential (Default)
- analyze-project.ts must complete before generate-guidance.ts
- scan-skills.ts must complete before generate-guidance.ts
- State identification before running pipeline
Parallelizable
- analyze-project.ts and scan-skills.ts can run concurrently
- Multiple single-skill scans can run concurrently
Subagent Candidates
| Task | Agent Type | When to Spawn |
|---|---|---|
| Project analysis | Bash | Running analyze-project.ts |
| Skill scanning | Bash | Running scan-skills.ts (especially for 100+ skills) |
Context Management
Approximate Token Footprint
- Skill base: ~3k tokens
- With script outputs inline: ~6k tokens
- With full skill inventory (100+ skills): ~15k tokens (write to file, don't inline)
Context Optimization
- Always write script JSON output to temporary files rather than inlining
- Reference skills by name rather than embedding full metadata
- Use
--jsonoutput mode for scripts, only inline the final guidance markdown
When Context Gets Tight
- Prioritize: Current state diagnosis and generated guidance output
- Defer: Full skill inventory details, scoring breakdowns
- Drop: Raw project file type counts, individual skill metadata
Output Persistence
Output Discovery
Before doing any other work:
1. Check if CLAUDE.md exists in the project root 2. Check if it already has a <!-- Generated by skill-integrator section 3. If CLAUDE.md doesn't exist, ask the user where to place the guidance
Primary Output
For this skill, persist:
- Guidance section - The trigger-based markdown inserted into CLAUDE.md/AGENTS.md
- Timestamp marker - HTML comment marking the section for surgical updates
Conversation vs. File
| Goes to File | Stays in Conversation |
|---|---|
| Trigger-based guidance section | Scoring breakdown and reasoning |
| Timestamp markers | State diagnosis discussion |
| CLAUDE.md updates | Threshold adjustment discussion |
File Naming
Pattern: Inserted section within existing CLAUDE.md or AGENTS.md Markers: <!-- Generated by skill-integrator | Last updated: YYYY-MM-DD -->
{
"_meta": {
"description": "Project type detection rules for skill-integrator",
"usage": "Each pattern has indicator files, file extensions, and resulting domains",
"version": "1.0"
},
"software.typescript": {
"indicators": ["package.json", "tsconfig.json"],
"requiredIndicators": 2,
"extensions": [".ts", ".tsx", ".js", ".jsx"],
"domains": ["web", "typescript", "software"],
"techStack": ["node", "typescript"]
},
"software.rust": {
"indicators": ["Cargo.toml"],
"requiredIndicators": 1,
"extensions": [".rs"],
"domains": ["rust", "systems", "software"],
"techStack": ["rust", "cargo"]
},
"software.python": {
"indicators": ["pyproject.toml", "setup.py", "requirements.txt", "Pipfile"],
"requiredIndicators": 1,
"extensions": [".py"],
"domains": ["python", "software"],
"techStack": ["python"]
},
"software.go": {
"indicators": ["go.mod"],
"requiredIndicators": 1,
"extensions": [".go"],
"domains": ["go", "systems", "software"],
"techStack": ["go"]
},
"software.godot": {
"indicators": ["project.godot"],
"requiredIndicators": 1,
"extensions": [".gd", ".tscn", ".tres"],
"domains": ["gamedev", "godot", "software"],
"techStack": ["godot", "gdscript"]
},
"software.electron": {
"indicators": ["package.json", "electron-builder.yml"],
"requiredIndicators": 2,
"extensions": [".ts", ".tsx", ".js", ".jsx"],
"domains": ["desktop", "electron", "web", "software"],
"techStack": ["electron", "node", "typescript"]
},
"software.react": {
"indicators": ["package.json"],
"requiredIndicators": 1,
"packageJsonDeps": ["react", "react-dom"],
"extensions": [".tsx", ".jsx", ".ts", ".js"],
"domains": ["web", "react", "frontend", "software"],
"techStack": ["react", "node", "typescript"]
},
"creative.fiction": {
"indicators": [".fiction"],
"requiredIndicators": 0,
"extensions": [".md", ".txt"],
"domains": ["fiction", "narrative", "creative"],
"techStack": [],
"skillSignals": ["character-arc", "dialogue", "worldbuilding", "story-sense", "scene-sequencing"]
},
"creative.writing": {
"indicators": [],
"requiredIndicators": 0,
"extensions": [".md", ".txt"],
"domains": ["writing", "creative"],
"techStack": [],
"highMdRatio": true
},
"documentation": {
"indicators": ["docs/", "wiki/"],
"requiredIndicators": 0,
"extensions": [".md"],
"domains": ["docs", "writing"],
"techStack": [],
"highMdRatio": true
},
"infrastructure": {
"indicators": [".claude/skills/", "CLAUDE.md", ".context-network.md"],
"requiredIndicators": 2,
"extensions": [".md", ".ts", ".json"],
"domains": ["infrastructure", "skills", "meta"],
"techStack": ["deno", "typescript"]
}
}
#!/usr/bin/env -S deno run --allow-read
/**
* Project Analyzer
*
* Detects project type, domains, and tech stack by scanning
* indicator files, file extensions, and installed skills.
*
* Usage:
* deno run --allow-read analyze-project.ts
* deno run --allow-read analyze-project.ts --path /some/project
* deno run --allow-read analyze-project.ts --json
*/
// === INTERFACES ===
interface ProjectPattern {
indicators: string[];
requiredIndicators: number;
extensions: string[];
domains: string[];
techStack: string[];
packageJsonDeps?: string[];
skillSignals?: string[];
highMdRatio?: boolean;
}
interface ProjectPatterns {
_meta: { description: string; usage: string; version: string };
[key: string]: ProjectPattern | { description: string; usage: string; version: string };
}
interface ProjectContext {
type: string;
domains: string[];
techStack: string[];
fileTypes: Record<string, number>;
hasContextNetwork: boolean;
skillsInstalled: string[];
projectPath: string;
matchedPatterns: string[];
}
// === DATA LOADING ===
async function loadPatterns(): Promise<ProjectPatterns> {
const scriptDir = new URL(".", import.meta.url).pathname;
const dataPath = `${scriptDir}../data/project-patterns.json`;
try {
const text = await Deno.readTextFile(dataPath);
return JSON.parse(text);
} catch (e) {
console.error(`Error loading project-patterns.json: ${e}`);
Deno.exit(1);
}
}
// === UTILITIES ===
async function exists(path: string): Promise<boolean> {
try {
await Deno.stat(path);
return true;
} catch {
return false;
}
}
async function countFilesByExtension(
dir: string,
maxDepth: number = 3,
currentDepth: number = 0
): Promise<Record<string, number>> {
const counts: Record<string, number> = {};
if (currentDepth >= maxDepth) return counts;
try {
for await (const entry of Deno.readDir(dir)) {
// Skip hidden dirs, node_modules, target, etc.
if (entry.name.startsWith(".") || entry.name === "node_modules" ||
entry.name === "target" || entry.name === "dist" ||
entry.name === "__pycache__" || entry.name === "vendor") {
continue;
}
if (entry.isFile) {
const ext = entry.name.includes(".")
? "." + entry.name.split(".").pop()!
: "(none)";
counts[ext] = (counts[ext] || 0) + 1;
} else if (entry.isDirectory) {
const subCounts = await countFilesByExtension(
`${dir}/${entry.name}`,
maxDepth,
currentDepth + 1
);
for (const [ext, count] of Object.entries(subCounts)) {
counts[ext] = (counts[ext] || 0) + count;
}
}
}
} catch {
// Permission or access error, skip
}
return counts;
}
async function getInstalledSkills(projectPath: string): Promise<string[]> {
const skillsDir = `${projectPath}/.claude/skills`;
const skills: string[] = [];
try {
for await (const entry of Deno.readDir(skillsDir)) {
if (entry.isDirectory && !entry.name.startsWith(".")) {
// Verify it has a SKILL.md
if (await exists(`${skillsDir}/${entry.name}/SKILL.md`)) {
skills.push(entry.name);
}
}
}
} catch {
// No skills directory
}
return skills.sort();
}
async function readPackageJsonDeps(projectPath: string): Promise<string[]> {
try {
const text = await Deno.readTextFile(`${projectPath}/package.json`);
const pkg = JSON.parse(text);
const deps = Object.keys(pkg.dependencies || {});
const devDeps = Object.keys(pkg.devDependencies || {});
return [...deps, ...devDeps];
} catch {
return [];
}
}
// === CORE LOGIC ===
async function analyzeProject(projectPath: string): Promise<ProjectContext> {
const patterns = await loadPatterns();
const fileTypes = await countFilesByExtension(projectPath);
const installedSkills = await getInstalledSkills(projectPath);
const hasContextNetwork = await exists(`${projectPath}/.context-network.md`);
const packageDeps = await readPackageJsonDeps(projectPath);
const matchedPatterns: string[] = [];
const allDomains = new Set<string>();
const allTechStack = new Set<string>();
// Score each pattern
for (const [patternName, pattern] of Object.entries(patterns)) {
if (patternName === "_meta") continue;
const p = pattern as ProjectPattern;
// Check indicator files
let indicatorHits = 0;
for (const indicator of p.indicators) {
if (await exists(`${projectPath}/${indicator}`)) {
indicatorHits++;
}
}
// Check package.json deps if pattern specifies them
let depsMatch = true;
if (p.packageJsonDeps && p.packageJsonDeps.length > 0) {
depsMatch = p.packageJsonDeps.some(dep => packageDeps.includes(dep));
if (!depsMatch) continue;
}
// Check skill signals (for creative patterns)
let skillSignalMatch = false;
if (p.skillSignals && p.skillSignals.length > 0) {
const matchCount = p.skillSignals.filter(s => installedSkills.includes(s)).length;
skillSignalMatch = matchCount >= 2;
}
// Check high markdown ratio
let mdRatioMatch = false;
if (p.highMdRatio) {
const totalFiles = Object.values(fileTypes).reduce((a, b) => a + b, 0);
const mdFiles = fileTypes[".md"] || 0;
mdRatioMatch = totalFiles > 0 && mdFiles / totalFiles > 0.5;
}
// Determine if pattern matches
const indicatorsMet = indicatorHits >= p.requiredIndicators;
const hasExtensions = p.extensions.some(ext => (fileTypes[ext] || 0) > 0);
if (
(indicatorsMet && p.requiredIndicators > 0) ||
(skillSignalMatch) ||
(mdRatioMatch && p.highMdRatio) ||
(indicatorsMet && hasExtensions && p.requiredIndicators === 0)
) {
matchedPatterns.push(patternName);
for (const domain of p.domains) allDomains.add(domain);
for (const tech of p.techStack) allTechStack.add(tech);
}
}
// Determine primary type
let type = "unknown";
if (matchedPatterns.length === 0) {
type = "unknown";
} else if (matchedPatterns.length === 1) {
type = matchedPatterns[0];
} else {
// Multiple matches - prefer more specific patterns
const softwarePatterns = matchedPatterns.filter(p => p.startsWith("software."));
const creativePatterns = matchedPatterns.filter(p => p.startsWith("creative."));
if (softwarePatterns.length > 0 && creativePatterns.length === 0) {
// Pick most specific software pattern
type = softwarePatterns.find(p => p !== "software.typescript") || softwarePatterns[0];
} else if (creativePatterns.length > 0 && softwarePatterns.length === 0) {
type = creativePatterns[0];
} else {
type = "mixed";
}
}
return {
type,
domains: [...allDomains].sort(),
techStack: [...allTechStack].sort(),
fileTypes,
hasContextNetwork,
skillsInstalled: installedSkills,
projectPath,
matchedPatterns,
};
}
// === FORMATTING ===
function formatResult(ctx: ProjectContext): string {
const lines: string[] = [];
lines.push(`# Project Analysis: ${ctx.projectPath}\n`);
lines.push(`**Type:** ${ctx.type}`);
lines.push(`**Domains:** ${ctx.domains.join(", ") || "(none detected)"}`);
lines.push(`**Tech Stack:** ${ctx.techStack.join(", ") || "(none detected)"}`);
lines.push(`**Context Network:** ${ctx.hasContextNetwork ? "Yes" : "No"}`);
lines.push(`**Matched Patterns:** ${ctx.matchedPatterns.join(", ") || "(none)"}`);
lines.push("");
lines.push(`## File Types\n`);
const sorted = Object.entries(ctx.fileTypes).sort((a, b) => b[1] - a[1]);
for (const [ext, count] of sorted.slice(0, 15)) {
lines.push(` ${ext}: ${count}`);
}
lines.push("");
lines.push(`## Installed Skills (${ctx.skillsInstalled.length})\n`);
if (ctx.skillsInstalled.length === 0) {
lines.push(" (none)");
} else {
for (const skill of ctx.skillsInstalled) {
lines.push(` - ${skill}`);
}
}
return lines.join("\n");
}
// === MAIN ===
async function main(): Promise<void> {
const args = Deno.args;
if (args.includes("--help") || args.includes("-h")) {
console.log(`Project Analyzer
Detects project type, domains, tech stack, and installed skills.
Usage:
deno run --allow-read analyze-project.ts [options]
Options:
--path P Project path to analyze (default: current directory)
--json Output as JSON
Examples:
deno run --allow-read analyze-project.ts
deno run --allow-read analyze-project.ts --path /my/project --json
`);
Deno.exit(0);
}
const pathIndex = args.indexOf("--path");
const projectPath = pathIndex !== -1 && args[pathIndex + 1]
? args[pathIndex + 1]
: Deno.cwd();
const jsonOutput = args.includes("--json");
const result = await analyzeProject(projectPath);
if (jsonOutput) {
console.log(JSON.stringify(result, null, 2));
} else {
console.log(formatResult(result));
}
}
main();
#!/usr/bin/env -S deno run --allow-read
/**
* Guidance Generator
*
* Takes project context + skill metadata and produces trigger-based
* usage guidance ready to insert into CLAUDE.md or AGENTS.md.
*
* Usage:
* deno run --allow-read generate-guidance.ts --project-context ctx.json --skills skills.json
* cat ctx.json | deno run --allow-read generate-guidance.ts --skills skills.json
* deno run --allow-read generate-guidance.ts --skill code-review --project-context ctx.json
*/
// === INTERFACES ===
interface ProjectContext {
type: string;
domains: string[];
techStack: string[];
fileTypes: Record<string, number>;
hasContextNetwork: boolean;
skillsInstalled: string[];
projectPath: string;
matchedPatterns: string[];
}
interface SkillInfo {
name: string;
description: string;
domain: string;
type: string;
mode: string;
keywords: string[];
triggerPhrases: string[];
path: string;
hasScripts: boolean;
hasData: boolean;
}
interface ScoredSkill {
skill: SkillInfo;
score: number;
reasons: string[];
triggerCategory: string;
}
// === TRIGGER CATEGORIES ===
const TRIGGER_CATEGORIES: Record<string, {
label: string;
keywords: string[];
types: string[];
modes: string[];
domains: string[];
}> = {
planning: {
label: "When Planning & Designing",
keywords: ["requirements", "architecture", "design", "planning", "analysis", "decomposition", "breakdown"],
types: ["diagnostic", "utility"],
modes: ["diagnostic", "assistive"],
domains: ["agile-software", "infrastructure"],
},
writing_code: {
label: "When Writing Code",
keywords: ["typescript", "rust", "python", "godot", "react", "electron", "coding", "implementation", "best-practices", "frontend"],
types: ["diagnostic", "generator"],
modes: ["diagnostic", "generative", "collaborative"],
domains: ["web", "typescript", "rust", "python", "gamedev", "desktop", "frontend"],
},
writing_content: {
label: "When Writing Content",
keywords: ["drafting", "writing", "prose", "dialogue", "narrative", "fiction", "revision", "story", "chapter"],
types: ["diagnostic", "generator"],
modes: ["assistive", "collaborative", "generative"],
domains: ["fiction", "narrative", "creative", "writing"],
},
stuck: {
label: "When Stuck or Debugging",
keywords: ["thinking", "debugging", "stuck", "breakdown", "decomposition", "brainstorming", "analysis"],
types: ["diagnostic", "utility"],
modes: ["diagnostic", "assistive"],
domains: [],
},
reviewing: {
label: "When Reviewing & Improving",
keywords: ["review", "revision", "analysis", "evaluation", "check", "audit", "sensitivity", "fact"],
types: ["diagnostic"],
modes: ["evaluative", "diagnostic"],
domains: [],
},
managing: {
label: "When Managing the Project",
keywords: ["context", "network", "agile", "workflow", "coordination", "bootstrap", "integration"],
types: ["utility"],
modes: ["generative", "diagnostic"],
domains: ["infrastructure", "agile-software"],
},
worldbuilding: {
label: "When Worldbuilding",
keywords: ["world", "setting", "culture", "language", "religion", "governance", "economic", "settlement", "evolution"],
types: ["diagnostic", "generator"],
modes: ["generative", "diagnostic", "collaborative"],
domains: ["fiction", "narrative", "creative"],
},
generating: {
label: "When Generating Documents & Assets",
keywords: ["pdf", "docx", "xlsx", "pptx", "presentation", "document", "spreadsheet", "ebook"],
types: ["generator", "utility"],
modes: ["generative"],
domains: [],
},
};
// === SCORING ===
function scoreSkill(skill: SkillInfo, context: ProjectContext): ScoredSkill {
let score = 0;
const reasons: string[] = [];
// Domain overlap (heaviest weight: 0-0.4)
const skillDomains = new Set([skill.domain]);
for (const kw of skill.keywords) {
if (context.domains.includes(kw)) {
skillDomains.add(kw);
}
}
const domainOverlap = context.domains.filter(d =>
skillDomains.has(d) || skill.keywords.includes(d)
).length;
if (domainOverlap > 0) {
const domainScore = Math.min(domainOverlap * 0.15, 0.4);
score += domainScore;
reasons.push(`domain overlap (${domainOverlap} matches)`);
}
// Direct domain match
if (context.domains.includes(skill.domain)) {
score += 0.2;
reasons.push(`direct domain match: ${skill.domain}`);
}
// Skill type appropriateness (0-0.2)
// Diagnostic and utility skills are always somewhat useful
if (skill.type === "diagnostic" || skill.type === "utility") {
score += 0.1;
reasons.push(`${skill.type} type is broadly useful`);
}
// Generators need domain match to be relevant
if (skill.type === "generator" && domainOverlap === 0) {
score -= 0.15;
reasons.push("generator without domain match");
}
// Tech stack match (0-0.15)
const techOverlap = context.techStack.filter(t =>
skill.keywords.includes(t) || skill.name.includes(t)
).length;
if (techOverlap > 0) {
score += Math.min(techOverlap * 0.1, 0.15);
reasons.push(`tech stack match (${techOverlap})`);
}
// Infrastructure skills get a baseline if project has skills
if (skill.domain === "infrastructure" && context.skillsInstalled.length > 5) {
score += 0.1;
reasons.push("infrastructure skill for skill-heavy project");
}
// Context network relevance
if (context.hasContextNetwork && skill.keywords.includes("context")) {
score += 0.1;
reasons.push("project uses context network");
}
// Determine trigger category
let bestCategory = "managing";
let bestCategoryScore = 0;
for (const [catKey, cat] of Object.entries(TRIGGER_CATEGORIES)) {
let catScore = 0;
// Keyword match
const kwMatches = cat.keywords.filter(kw =>
skill.keywords.includes(kw) || skill.name.includes(kw)
).length;
catScore += kwMatches * 2;
// Type match
if (cat.types.includes(skill.type)) catScore += 1;
// Mode match
if (cat.modes.includes(skill.mode)) catScore += 1;
// Domain match
if (cat.domains.includes(skill.domain)) catScore += 1;
if (catScore > bestCategoryScore) {
bestCategoryScore = catScore;
bestCategory = catKey;
}
}
return {
skill,
score: Math.max(0, Math.min(1, score)),
reasons,
triggerCategory: bestCategory,
};
}
// === FORMATTING ===
function generateTriggerFormat(
scoredSkills: ScoredSkill[],
threshold: number
): string {
const lines: string[] = [];
const today = new Date().toISOString().split("T")[0];
const relevant = scoredSkills.filter(s => s.score >= threshold);
lines.push("## Installed Skills: Usage Guide");
lines.push(`<!-- Generated by skill-integrator | Last updated: ${today} -->\n`);
// Group by trigger category
const grouped: Record<string, ScoredSkill[]> = {};
for (const scored of relevant) {
if (!grouped[scored.triggerCategory]) {
grouped[scored.triggerCategory] = [];
}
grouped[scored.triggerCategory].push(scored);
}
// Output in a logical order
const categoryOrder = [
"planning", "writing_code", "writing_content",
"stuck", "reviewing", "worldbuilding",
"managing", "generating",
];
for (const catKey of categoryOrder) {
const skills = grouped[catKey];
if (!skills || skills.length === 0) continue;
const label = TRIGGER_CATEGORIES[catKey]?.label || catKey;
lines.push(`### ${label}`);
// Sort by score descending within category
skills.sort((a, b) => b.score - a.score);
for (const scored of skills) {
// Create a concise trigger description from the skill's description
let triggerDesc = scored.skill.description;
// Truncate to first sentence or first "Use when" phrase
const useWhenMatch = triggerDesc.match(/Use when[^.]+\./i);
if (useWhenMatch) {
triggerDesc = useWhenMatch[0];
} else {
const firstSentence = triggerDesc.split(". ")[0];
if (firstSentence.length < 120) {
triggerDesc = firstSentence;
} else {
triggerDesc = firstSentence.slice(0, 117) + "...";
}
}
lines.push(`- **${scored.skill.name}**: ${triggerDesc}`);
}
lines.push("");
}
// Footer
const totalInstalled = scoredSkills.length;
lines.push(`[Full inventory: ${totalInstalled} skills installed in .claude/skills/]`);
return lines.join("\n");
}
function generateDomainFormat(
scoredSkills: ScoredSkill[],
threshold: number
): string {
const lines: string[] = [];
const today = new Date().toISOString().split("T")[0];
const relevant = scoredSkills.filter(s => s.score >= threshold);
lines.push("## Installed Skills: By Domain");
lines.push(`<!-- Generated by skill-integrator | Last updated: ${today} -->\n`);
// Group by domain
const grouped: Record<string, ScoredSkill[]> = {};
for (const scored of relevant) {
const domain = scored.skill.domain || "general";
if (!grouped[domain]) grouped[domain] = [];
grouped[domain].push(scored);
}
for (const [domain, skills] of Object.entries(grouped).sort()) {
lines.push(`### ${domain}`);
skills.sort((a, b) => b.score - a.score);
for (const scored of skills) {
lines.push(`- **${scored.skill.name}** (${scored.skill.type}): ${scored.skill.description.split(". ")[0]}`);
}
lines.push("");
}
return lines.join("\n");
}
function generateTableFormat(
scoredSkills: ScoredSkill[],
threshold: number
): string {
const lines: string[] = [];
const today = new Date().toISOString().split("T")[0];
const relevant = scoredSkills.filter(s => s.score >= threshold);
lines.push("## Installed Skills: Relevance Table");
lines.push(`<!-- Generated by skill-integrator | Last updated: ${today} -->\n`);
lines.push("| Skill | Domain | Type | Score | Trigger |");
lines.push("|-------|--------|------|-------|---------|");
relevant.sort((a, b) => b.score - a.score);
for (const scored of relevant) {
const cat = TRIGGER_CATEGORIES[scored.triggerCategory]?.label || scored.triggerCategory;
lines.push(
`| ${scored.skill.name} | ${scored.skill.domain} | ${scored.skill.type} | ${scored.score.toFixed(2)} | ${cat} |`
);
}
return lines.join("\n");
}
function generateSingleSkillGuidance(scored: ScoredSkill): string {
const lines: string[] = [];
const cat = TRIGGER_CATEGORIES[scored.triggerCategory]?.label || scored.triggerCategory;
lines.push(`## Skill Integration: ${scored.skill.name}\n`);
lines.push(`**Relevance Score:** ${scored.score.toFixed(2)}`);
lines.push(`**Trigger Category:** ${cat}`);
lines.push(`**Domain:** ${scored.skill.domain} | **Type:** ${scored.skill.type} | **Mode:** ${scored.skill.mode}`);
lines.push("");
lines.push("### Suggested Guidance Entry\n");
lines.push("```markdown");
let triggerDesc = scored.skill.description;
const useWhenMatch = triggerDesc.match(/Use when[^.]+\./i);
if (useWhenMatch) {
triggerDesc = useWhenMatch[0];
} else {
triggerDesc = triggerDesc.split(". ")[0];
}
lines.push(`- **${scored.skill.name}**: ${triggerDesc}`);
lines.push("```\n");
lines.push("### Scoring Breakdown\n");
for (const reason of scored.reasons) {
lines.push(`- ${reason}`);
}
if (scored.skill.triggerPhrases.length > 0) {
lines.push("\n### Trigger Phrases from Skill\n");
for (const phrase of scored.skill.triggerPhrases.slice(0, 5)) {
lines.push(`- ${phrase}`);
}
}
return lines.join("\n");
}
// === DATA LOADING ===
async function loadJson<T>(path: string): Promise<T> {
try {
const text = await Deno.readTextFile(path);
return JSON.parse(text);
} catch (e) {
console.error(`Error loading ${path}: ${e}`);
Deno.exit(1);
}
}
async function readStdin(): Promise<string> {
const decoder = new TextDecoder();
const chunks: string[] = [];
const reader = Deno.stdin.readable.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(decoder.decode(value, { stream: true }));
}
} finally {
reader.releaseLock();
}
return chunks.join("");
}
// === MAIN ===
async function main(): Promise<void> {
const args = Deno.args;
if (args.includes("--help") || args.includes("-h")) {
console.log(`Guidance Generator
Generates trigger-based skill usage guidance for CLAUDE.md/AGENTS.md.
Usage:
deno run --allow-read generate-guidance.ts --project-context ctx.json --skills skills.json
deno run --allow-read generate-guidance.ts --skill code-review --project-context ctx.json --skills skills.json
Options:
--project-context F Path to project context JSON (from analyze-project.ts)
--skills F Path to skills JSON (from scan-skills.ts)
--skill S Generate guidance for a single skill only
--format F Output format: trigger (default), domain, table
--threshold N Minimum relevance score (default: 0.3)
--json Output scored data as JSON instead of markdown
Examples:
deno run --allow-read generate-guidance.ts --project-context ctx.json --skills skills.json
deno run --allow-read generate-guidance.ts --skill code-review --project-context ctx.json --skills skills.json
deno run --allow-read generate-guidance.ts --format table --threshold 0.5 --project-context ctx.json --skills skills.json
`);
Deno.exit(0);
}
// Parse arguments
const ctxIndex = args.indexOf("--project-context");
const skillsIndex = args.indexOf("--skills");
const skillIndex = args.indexOf("--skill");
const formatIndex = args.indexOf("--format");
const thresholdIndex = args.indexOf("--threshold");
const jsonOutput = args.includes("--json");
const format = formatIndex !== -1 && args[formatIndex + 1]
? args[formatIndex + 1]
: "trigger";
const threshold = thresholdIndex !== -1 && args[thresholdIndex + 1]
? parseFloat(args[thresholdIndex + 1])
: 0.3;
const singleSkill = skillIndex !== -1 && args[skillIndex + 1]
? args[skillIndex + 1]
: null;
// Load project context
let context: ProjectContext;
if (ctxIndex !== -1 && args[ctxIndex + 1]) {
context = await loadJson<ProjectContext>(args[ctxIndex + 1]);
} else {
// Try reading from stdin
try {
const input = await readStdin();
context = JSON.parse(input);
} catch {
console.error("Error: Provide --project-context or pipe JSON to stdin");
Deno.exit(1);
}
}
// Load skills data
let skills: SkillInfo[];
if (skillsIndex !== -1 && args[skillsIndex + 1]) {
skills = await loadJson<SkillInfo[]>(args[skillsIndex + 1]);
} else {
console.error("Error: --skills argument is required");
Deno.exit(1);
}
// Score all skills
const scoredSkills = skills.map(skill => scoreSkill(skill, context));
// Single skill mode
if (singleSkill) {
const scored = scoredSkills.find(s => s.skill.name === singleSkill);
if (!scored) {
console.error(`Skill not found: ${singleSkill}`);
Deno.exit(1);
}
if (jsonOutput) {
console.log(JSON.stringify(scored, null, 2));
} else {
console.log(generateSingleSkillGuidance(scored));
}
return;
}
// Full output
if (jsonOutput) {
console.log(JSON.stringify(scoredSkills.filter(s => s.score >= threshold), null, 2));
return;
}
switch (format) {
case "domain":
console.log(generateDomainFormat(scoredSkills, threshold));
break;
case "table":
console.log(generateTableFormat(scoredSkills, threshold));
break;
case "trigger":
default:
console.log(generateTriggerFormat(scoredSkills, threshold));
break;
}
}
main();
#!/usr/bin/env -S deno run --allow-read
/**
* Skill Scanner
*
* Reads all installed skill SKILL.md files, parses frontmatter,
* and extracts metadata for integration analysis.
*
* Usage:
* deno run --allow-read scan-skills.ts
* deno run --allow-read scan-skills.ts --skill code-review
* deno run --allow-read scan-skills.ts --path /project/.claude/skills
* deno run --allow-read scan-skills.ts --json
*/
// === INTERFACES ===
interface SkillInfo {
name: string;
description: string;
domain: string;
type: string;
mode: string;
keywords: string[];
triggerPhrases: string[];
path: string;
hasScripts: boolean;
hasData: boolean;
}
// === UTILITIES ===
async function exists(path: string): Promise<boolean> {
try {
await Deno.stat(path);
return true;
} catch {
return false;
}
}
function parseFrontmatter(content: string): Record<string, string> {
const match = content.match(/^---\n([\s\S]*?)\n---/);
if (!match) return {};
const result: Record<string, string> = {};
const lines = match[1].split("\n");
let inMetadata = false;
for (const line of lines) {
if (line.startsWith("metadata:")) {
inMetadata = true;
continue;
}
if (inMetadata && line.startsWith(" ")) {
const metaMatch = line.match(/^\s+(\w+):\s*"?([^"]*)"?$/);
if (metaMatch) {
result[`metadata.${metaMatch[1]}`] = metaMatch[2].trim();
}
} else if (!line.startsWith(" ")) {
inMetadata = false;
const keyMatch = line.match(/^(\w+):\s*(.+)$/);
if (keyMatch) {
result[keyMatch[1]] = keyMatch[2].replace(/^"(.*)"$/, "$1").trim();
}
}
}
return result;
}
function extractTriggerPhrases(content: string): string[] {
const triggers: string[] = [];
// Extract from "When to Use" section
const whenToUseMatch = content.match(/## When to Use[\s\S]*?(?=\n## )/i);
if (whenToUseMatch) {
const bullets = whenToUseMatch[0].match(/^-\s+(.+)$/gm);
if (bullets) {
triggers.push(...bullets.map(b => b.replace(/^-\s+/, "").trim()));
}
}
// Extract from description field - split on commas and "when"
const frontmatter = parseFrontmatter(content);
if (frontmatter.description) {
const desc = frontmatter.description;
// Extract "Use when..." and "when..." phrases
const whenPhrases = desc.match(/(?:Use )?when [^,.]+/gi);
if (whenPhrases) {
triggers.push(...whenPhrases.map(p => p.trim()));
}
}
return triggers;
}
function extractKeywords(content: string, frontmatter: Record<string, string>): string[] {
const keywords = new Set<string>();
// From domain
if (frontmatter["metadata.domain"]) {
keywords.add(frontmatter["metadata.domain"]);
}
// From name
const name = frontmatter.name || "";
for (const word of name.split("-")) {
if (word.length > 2) keywords.add(word.toLowerCase());
}
// From type and mode
if (frontmatter["metadata.type"]) keywords.add(frontmatter["metadata.type"]);
if (frontmatter["metadata.mode"]) {
for (const m of frontmatter["metadata.mode"].split("+")) {
keywords.add(m.trim());
}
}
// Extract key terms from first paragraph after frontmatter
const bodyStart = content.indexOf("---", 3);
if (bodyStart !== -1) {
const body = content.slice(bodyStart + 3).trim();
const firstParagraph = body.split("\n\n")[0] || "";
// Pull out significant words (skip common ones)
const skipWords = new Set([
"the", "a", "an", "and", "or", "but", "in", "on", "at", "to",
"for", "of", "with", "by", "from", "is", "are", "was", "were",
"be", "been", "being", "have", "has", "had", "do", "does", "did",
"will", "would", "could", "should", "may", "might", "can", "this",
"that", "these", "those", "it", "its", "you", "your", "when",
"use", "using", "used",
]);
const words = firstParagraph
.replace(/[#*`\[\](){}|]/g, "")
.split(/\s+/)
.map(w => w.toLowerCase().replace(/[.,;:!?]/g, ""))
.filter(w => w.length > 3 && !skipWords.has(w));
for (const w of words.slice(0, 10)) {
keywords.add(w);
}
}
return [...keywords];
}
// === CORE LOGIC ===
async function scanSkill(skillPath: string): Promise<SkillInfo | null> {
const skillMdPath = `${skillPath}/SKILL.md`;
try {
const content = await Deno.readTextFile(skillMdPath);
const frontmatter = parseFrontmatter(content);
const name = frontmatter.name || skillPath.split("/").pop() || "unknown";
return {
name,
description: frontmatter.description || "",
domain: frontmatter["metadata.domain"] || "general",
type: frontmatter["metadata.type"] || "unknown",
mode: frontmatter["metadata.mode"] || "unknown",
keywords: extractKeywords(content, frontmatter),
triggerPhrases: extractTriggerPhrases(content),
path: skillPath,
hasScripts: await exists(`${skillPath}/scripts`),
hasData: await exists(`${skillPath}/data`),
};
} catch {
return null;
}
}
async function scanAllSkills(skillsDir: string): Promise<SkillInfo[]> {
const skills: SkillInfo[] = [];
try {
for await (const entry of Deno.readDir(skillsDir)) {
if (entry.isDirectory && !entry.name.startsWith(".")) {
const skillPath = `${skillsDir}/${entry.name}`;
if (await exists(`${skillPath}/SKILL.md`)) {
const info = await scanSkill(skillPath);
if (info) skills.push(info);
}
}
}
} catch (e) {
console.error(`Error scanning skills directory: ${e}`);
}
return skills.sort((a, b) => a.name.localeCompare(b.name));
}
// === FORMATTING ===
function formatSkillInfo(skill: SkillInfo): string {
const lines: string[] = [];
lines.push(`## ${skill.name}`);
lines.push(`**Description:** ${skill.description || "(none)"}`);
lines.push(`**Domain:** ${skill.domain} | **Type:** ${skill.type} | **Mode:** ${skill.mode}`);
lines.push(`**Keywords:** ${skill.keywords.join(", ")}`);
if (skill.triggerPhrases.length > 0) {
lines.push(`**Triggers:**`);
for (const trigger of skill.triggerPhrases.slice(0, 5)) {
lines.push(` - ${trigger}`);
}
}
lines.push(`**Has Scripts:** ${skill.hasScripts ? "Yes" : "No"} | **Has Data:** ${skill.hasData ? "Yes" : "No"}`);
lines.push("");
return lines.join("\n");
}
function formatResults(skills: SkillInfo[]): string {
const lines: string[] = [];
lines.push(`# Installed Skills: ${skills.length} found\n`);
// Summary by domain
const domainCounts: Record<string, number> = {};
for (const skill of skills) {
domainCounts[skill.domain] = (domainCounts[skill.domain] || 0) + 1;
}
lines.push("## By Domain\n");
for (const [domain, count] of Object.entries(domainCounts).sort((a, b) => b[1] - a[1])) {
lines.push(` ${domain}: ${count}`);
}
lines.push("");
// Summary by type
const typeCounts: Record<string, number> = {};
for (const skill of skills) {
typeCounts[skill.type] = (typeCounts[skill.type] || 0) + 1;
}
lines.push("## By Type\n");
for (const [type, count] of Object.entries(typeCounts).sort((a, b) => b[1] - a[1])) {
lines.push(` ${type}: ${count}`);
}
lines.push("");
// Individual skills
lines.push("## Skills\n");
for (const skill of skills) {
lines.push(formatSkillInfo(skill));
}
return lines.join("\n");
}
// === MAIN ===
async function main(): Promise<void> {
const args = Deno.args;
if (args.includes("--help") || args.includes("-h")) {
console.log(`Skill Scanner
Reads all installed skill SKILL.md files and extracts metadata.
Usage:
deno run --allow-read scan-skills.ts [options]
Options:
--path P Path to skills directory (default: .claude/skills in cwd)
--skill S Scan a single skill by name
--json Output as JSON
Examples:
deno run --allow-read scan-skills.ts
deno run --allow-read scan-skills.ts --skill code-review --json
deno run --allow-read scan-skills.ts --path /project/.claude/skills
`);
Deno.exit(0);
}
const pathIndex = args.indexOf("--path");
const skillsDir = pathIndex !== -1 && args[pathIndex + 1]
? args[pathIndex + 1]
: `${Deno.cwd()}/.claude/skills`;
const skillIndex = args.indexOf("--skill");
const singleSkill = skillIndex !== -1 && args[skillIndex + 1]
? args[skillIndex + 1]
: null;
const jsonOutput = args.includes("--json");
if (singleSkill) {
const skillPath = `${skillsDir}/${singleSkill}`;
const info = await scanSkill(skillPath);
if (!info) {
console.error(`Skill not found: ${singleSkill}`);
Deno.exit(1);
}
if (jsonOutput) {
console.log(JSON.stringify(info, null, 2));
} else {
console.log(formatSkillInfo(info));
}
} else {
const skills = await scanAllSkills(skillsDir);
if (jsonOutput) {
console.log(JSON.stringify(skills, null, 2));
} else {
console.log(formatResults(skills));
}
}
}
main();
Related skills
How it compares
Pick skill-integrator to surface installed skills in CLAUDE.md; pick skill-builder to author new SKILL.md files from scratch.
FAQ
What does skill-integrator generate?
skill-integrator generates a marked CLAUDE.md or AGENTS.md section listing installed skills with project-scored trigger phrases so agents know when to invoke each capability instead of ignoring the skills directory.
How does skill-integrator detect stale guidance?
skill-integrator looks for the <!-- Generated by skill-integrator --> comment and compares installed skill count against documented entries, then re-runs analyze-project.ts and scan-skills.ts to diff and update the section.