@spardutti/claude-skills 1.0.3 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -1
- package/bin/cli.mjs +16 -0
- package/lib/github.mjs +35 -8
- package/lib/setup-claude-md.mjs +42 -0
- package/lib/setup-hook.mjs +91 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,11 +8,21 @@ Interactive CLI to install reusable [Claude Code](https://docs.anthropic.com/en/
|
|
|
8
8
|
npx @spardutti/claude-skills
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Run this from your project's root directory. The CLI will:
|
|
12
12
|
|
|
13
13
|
1. Fetch the latest skills from [GitHub](https://github.com/Spardutti/claude-skills)
|
|
14
14
|
2. Let you interactively select which skills to install
|
|
15
15
|
3. Copy them into your project's `.claude/skills/` directory
|
|
16
|
+
4. Ask to set up **automatic skill evaluation** (hook + CLAUDE.md rule)
|
|
17
|
+
|
|
18
|
+
## Automatic Skill Evaluation
|
|
19
|
+
|
|
20
|
+
Skills alone don't guarantee Claude will use them. The CLI can optionally set up enforcement:
|
|
21
|
+
|
|
22
|
+
- **Hook** (`.claude/hooks/skill-forced-eval-hook.sh`) — Runs on every prompt, injects a mandatory skill evaluation sequence into Claude's context
|
|
23
|
+
- **CLAUDE.md rule** (`skill_evaluation` block) — Instructs Claude to list every skill as ACTIVATE/SKIP before writing any code
|
|
24
|
+
|
|
25
|
+
Together, these force Claude to explicitly evaluate and activate relevant skills instead of silently ignoring them.
|
|
16
26
|
|
|
17
27
|
## Available Skills
|
|
18
28
|
|
|
@@ -23,6 +33,13 @@ This will:
|
|
|
23
33
|
| `react-query` | TanStack React Query with query-key-factory patterns |
|
|
24
34
|
| `tailwind-tokens` | Enforce Tailwind CSS design tokens — no arbitrary values when a token exists |
|
|
25
35
|
|
|
36
|
+
## GitHub Authentication
|
|
37
|
+
|
|
38
|
+
The CLI fetches skills via the GitHub API. Unauthenticated requests are limited to 60/hour. To avoid rate limits:
|
|
39
|
+
|
|
40
|
+
- Install the [GitHub CLI](https://cli.github.com) and run `gh auth login` — the token is detected automatically
|
|
41
|
+
- Or set `GITHUB_TOKEN` / `GH_TOKEN` as an environment variable
|
|
42
|
+
|
|
26
43
|
## What are Claude Code Skills?
|
|
27
44
|
|
|
28
45
|
Skills are markdown files placed in `.claude/skills/` that give Claude Code domain-specific knowledge and guidelines. They help Claude follow your team's patterns and best practices automatically.
|
package/bin/cli.mjs
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import { confirm } from "@inquirer/prompts";
|
|
3
4
|
import { fetchSkills } from "../lib/github.mjs";
|
|
4
5
|
import { promptSkillSelection } from "../lib/prompt.mjs";
|
|
5
6
|
import { installSkills } from "../lib/install.mjs";
|
|
7
|
+
import { setupHook } from "../lib/setup-hook.mjs";
|
|
8
|
+
import { setupClaudeMd } from "../lib/setup-claude-md.mjs";
|
|
6
9
|
|
|
7
10
|
async function main() {
|
|
8
11
|
console.log("\n Claude Skills Installer\n");
|
|
@@ -24,6 +27,19 @@ async function main() {
|
|
|
24
27
|
|
|
25
28
|
console.log();
|
|
26
29
|
await installSkills(selected);
|
|
30
|
+
|
|
31
|
+
console.log();
|
|
32
|
+
const shouldSetup = await confirm({
|
|
33
|
+
message: "Set up skill evaluation hook + CLAUDE.md rule? (Recommended)",
|
|
34
|
+
default: true,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
if (shouldSetup) {
|
|
38
|
+
console.log();
|
|
39
|
+
await setupHook();
|
|
40
|
+
await setupClaudeMd();
|
|
41
|
+
}
|
|
42
|
+
|
|
27
43
|
console.log(`\n Done! ${selected.length} skill(s) installed.\n`);
|
|
28
44
|
}
|
|
29
45
|
|
package/lib/github.mjs
CHANGED
|
@@ -1,16 +1,45 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
|
|
1
3
|
const REPO_OWNER = "Spardutti";
|
|
2
4
|
const REPO_NAME = "claude-skills";
|
|
3
5
|
const CONTENTS_API = `https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/contents/skills`;
|
|
4
6
|
const RAW_BASE = `https://raw.githubusercontent.com/${REPO_OWNER}/${REPO_NAME}/main/skills`;
|
|
5
7
|
|
|
8
|
+
function getAuthHeaders() {
|
|
9
|
+
const headers = { "User-Agent": "claude-skills-cli" };
|
|
10
|
+
|
|
11
|
+
// 1. Explicit env var
|
|
12
|
+
const envToken = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
|
|
13
|
+
if (envToken) {
|
|
14
|
+
headers.Authorization = `Bearer ${envToken}`;
|
|
15
|
+
return headers;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// 2. Try gh CLI token
|
|
19
|
+
try {
|
|
20
|
+
const token = execSync("gh auth token", {
|
|
21
|
+
encoding: "utf-8",
|
|
22
|
+
timeout: 5000,
|
|
23
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
24
|
+
}).trim();
|
|
25
|
+
if (token) {
|
|
26
|
+
headers.Authorization = `Bearer ${token}`;
|
|
27
|
+
}
|
|
28
|
+
} catch {
|
|
29
|
+
// gh not installed or not authenticated — continue unauthenticated
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return headers;
|
|
33
|
+
}
|
|
34
|
+
|
|
6
35
|
export async function fetchSkills() {
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
});
|
|
36
|
+
const headers = getAuthHeaders();
|
|
37
|
+
|
|
38
|
+
const res = await fetch(CONTENTS_API, { headers });
|
|
10
39
|
|
|
11
40
|
if (!res.ok) {
|
|
12
|
-
if (res.status === 403) {
|
|
13
|
-
throw new Error("GitHub API rate limit exceeded. Try again later.");
|
|
41
|
+
if (res.status === 403 || res.status === 429) {
|
|
42
|
+
throw new Error("GitHub API rate limit exceeded. Try again later or install gh CLI (https://cli.github.com).");
|
|
14
43
|
}
|
|
15
44
|
throw new Error(`Failed to list skills: ${res.status} ${res.statusText}`);
|
|
16
45
|
}
|
|
@@ -22,9 +51,7 @@ export async function fetchSkills() {
|
|
|
22
51
|
dirs.map(async (dir) => {
|
|
23
52
|
try {
|
|
24
53
|
const url = `${RAW_BASE}/${dir.name}/SKILL.md`;
|
|
25
|
-
const r = await fetch(url, {
|
|
26
|
-
headers: { "User-Agent": "claude-skills-cli" },
|
|
27
|
-
});
|
|
54
|
+
const r = await fetch(url, { headers });
|
|
28
55
|
|
|
29
56
|
if (!r.ok) {
|
|
30
57
|
console.warn(` Warning: No SKILL.md found in ${dir.name}, skipping`);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
const SKILL_EVAL_BLOCK = `
|
|
5
|
+
skill_evaluation:
|
|
6
|
+
mandatory: true
|
|
7
|
+
rule: |
|
|
8
|
+
BEFORE writing ANY code, you MUST:
|
|
9
|
+
1. List EVERY skill from the system-reminder's available skills section
|
|
10
|
+
2. For each skill, write: [skill-name] → ACTIVATE / SKIP — [one-line reason]
|
|
11
|
+
3. Call Skill(name) for every skill marked ACTIVATE
|
|
12
|
+
4. Only THEN proceed to implementation
|
|
13
|
+
If you skip this evaluation, your response is INCOMPLETE and WRONG.`;
|
|
14
|
+
|
|
15
|
+
const MARKER = "skill_evaluation:";
|
|
16
|
+
|
|
17
|
+
export async function setupClaudeMd(targetDir = process.cwd()) {
|
|
18
|
+
const resolved = resolve(targetDir);
|
|
19
|
+
const claudeMdPath = join(resolved, "CLAUDE.md");
|
|
20
|
+
|
|
21
|
+
let existing = "";
|
|
22
|
+
try {
|
|
23
|
+
existing = await readFile(claudeMdPath, "utf-8");
|
|
24
|
+
} catch {
|
|
25
|
+
// File doesn't exist — will create
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Don't add duplicate block
|
|
29
|
+
if (existing.includes(MARKER)) {
|
|
30
|
+
console.log(" CLAUDE.md already has skill_evaluation block — skipped.");
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Append the block (after trailing ``` if the file uses a yaml code fence)
|
|
35
|
+
const trimmed = existing.trimEnd();
|
|
36
|
+
const content = trimmed.length > 0
|
|
37
|
+
? trimmed + "\n" + SKILL_EVAL_BLOCK + "\n"
|
|
38
|
+
: SKILL_EVAL_BLOCK.trimStart() + "\n";
|
|
39
|
+
|
|
40
|
+
await writeFile(claudeMdPath, content, { mode: 0o644 });
|
|
41
|
+
console.log(" CLAUDE.md updated with skill_evaluation block.");
|
|
42
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { mkdir, writeFile, readFile, chmod } from "node:fs/promises";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
const HOOK_SCRIPT = `#!/bin/bash
|
|
5
|
+
# UserPromptSubmit hook that forces explicit skill evaluation
|
|
6
|
+
|
|
7
|
+
cat > /dev/null
|
|
8
|
+
|
|
9
|
+
DIR="\${CLAUDE_PROJECT_DIR:-.}"
|
|
10
|
+
|
|
11
|
+
# Build skill list from project skills
|
|
12
|
+
SKILL_LIST=""
|
|
13
|
+
while IFS= read -r skillfile; do
|
|
14
|
+
name=$(grep -m1 '^name:' "$skillfile" 2>/dev/null | sed 's/^name: *//' | sed 's/^"//' | sed 's/"$//')
|
|
15
|
+
desc=$(grep -m1 '^description:' "$skillfile" 2>/dev/null | sed 's/^description: *//' | sed 's/^"//' | sed 's/"$//')
|
|
16
|
+
if [ -n "$name" ] && [ -n "$desc" ]; then
|
|
17
|
+
SKILL_LIST="\${SKILL_LIST} - \${name}: \${desc}\\\\n"
|
|
18
|
+
fi
|
|
19
|
+
done < <(find "$DIR" -path '*/.claude/skills/*/SKILL.md' 2>/dev/null | sort -u)
|
|
20
|
+
|
|
21
|
+
INSTRUCTION="INSTRUCTION: MANDATORY SKILL ACTIVATION SEQUENCE\\\\n\\\\n"
|
|
22
|
+
INSTRUCTION+="<available_skills>\\\\n"
|
|
23
|
+
INSTRUCTION+="System skills (from system-reminder):\\\\n - Check system-reminder for built-in skills\\\\n"
|
|
24
|
+
|
|
25
|
+
if [ -n "$SKILL_LIST" ]; then
|
|
26
|
+
INSTRUCTION+="Project skills:\\\\n\${SKILL_LIST}"
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
INSTRUCTION+="</available_skills>\\\\n\\\\n"
|
|
30
|
+
INSTRUCTION+="Step 1 - EVALUATE (do this in your response):\\\\n"
|
|
31
|
+
INSTRUCTION+="For each skill in <available_skills>, state: [skill-name] - YES/NO - [reason]\\\\n\\\\n"
|
|
32
|
+
INSTRUCTION+="Step 2 - ACTIVATE (do this immediately after Step 1):\\\\n"
|
|
33
|
+
INSTRUCTION+="IF any skills are YES -> Use Skill(skill-name) tool for EACH relevant skill NOW\\\\n"
|
|
34
|
+
INSTRUCTION+="IF no skills are YES -> State 'No skills needed' and proceed\\\\n\\\\n"
|
|
35
|
+
INSTRUCTION+="Step 3 - IMPLEMENT:\\\\n"
|
|
36
|
+
INSTRUCTION+="Only after Step 2 is complete, proceed with implementation.\\\\n\\\\n"
|
|
37
|
+
INSTRUCTION+="CRITICAL: You MUST call Skill() tool in Step 2. Do NOT skip to implementation."
|
|
38
|
+
|
|
39
|
+
printf '{"additionalContext": "%s"}\\n' "$INSTRUCTION"
|
|
40
|
+
exit 0
|
|
41
|
+
`;
|
|
42
|
+
|
|
43
|
+
const HOOK_FILENAME = "skill-forced-eval-hook.sh";
|
|
44
|
+
|
|
45
|
+
export async function setupHook(targetDir = process.cwd()) {
|
|
46
|
+
const resolved = resolve(targetDir);
|
|
47
|
+
const hooksDir = join(resolved, ".claude", "hooks");
|
|
48
|
+
const hookPath = join(hooksDir, HOOK_FILENAME);
|
|
49
|
+
const settingsPath = join(resolved, ".claude", "settings.json");
|
|
50
|
+
|
|
51
|
+
// Write the hook script
|
|
52
|
+
await mkdir(hooksDir, { recursive: true });
|
|
53
|
+
await writeFile(hookPath, HOOK_SCRIPT, { mode: 0o755 });
|
|
54
|
+
await chmod(hookPath, 0o755);
|
|
55
|
+
|
|
56
|
+
// Merge into existing settings.json (don't clobber other config)
|
|
57
|
+
let settings = {};
|
|
58
|
+
try {
|
|
59
|
+
const raw = await readFile(settingsPath, "utf-8");
|
|
60
|
+
settings = JSON.parse(raw);
|
|
61
|
+
} catch {
|
|
62
|
+
// File doesn't exist or is invalid — start fresh
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!settings.hooks) {
|
|
66
|
+
settings.hooks = {};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const hookEntry = {
|
|
70
|
+
hooks: [{ type: "command", command: hookPath }],
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// Check if UserPromptSubmit already has this hook to avoid duplicates
|
|
74
|
+
if (Array.isArray(settings.hooks.UserPromptSubmit)) {
|
|
75
|
+
const alreadyInstalled = settings.hooks.UserPromptSubmit.some((entry) =>
|
|
76
|
+
entry.hooks?.some((h) => h.command?.endsWith(HOOK_FILENAME))
|
|
77
|
+
);
|
|
78
|
+
if (!alreadyInstalled) {
|
|
79
|
+
settings.hooks.UserPromptSubmit.push(hookEntry);
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
settings.hooks.UserPromptSubmit = [hookEntry];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
await writeFile(settingsPath, JSON.stringify(settings, null, 2) + "\n", {
|
|
86
|
+
mode: 0o644,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
console.log(` Hook installed: .claude/hooks/${HOOK_FILENAME}`);
|
|
90
|
+
console.log(` Settings updated: .claude/settings.json`);
|
|
91
|
+
}
|