@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 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
- This will:
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 res = await fetch(CONTENTS_API, {
8
- headers: { "User-Agent": "claude-skills-cli" },
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spardutti/claude-skills",
3
- "version": "1.0.3",
3
+ "version": "1.1.0",
4
4
  "description": "CLI to install Claude Code skills from the claude-skills collection",
5
5
  "type": "module",
6
6
  "bin": {