@spardutti/claude-skills 1.19.2 → 1.23.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
@@ -1,6 +1,6 @@
1
1
  # Claude Skills
2
2
 
3
- Personal collection of reusable Claude Code skills.
3
+ Personal collection of reusable Claude Code **skills**, **slash commands**, and **subagents**. Install them into any project with one command — pick what you want from an interactive menu, and any subagents declared by the commands you pick get installed automatically.
4
4
 
5
5
  ## Skills
6
6
 
@@ -38,6 +38,8 @@ Personal collection of reusable Claude Code skills.
38
38
  | `express-best-practices` | Express.js — feature-based structure, 3-layer architecture, Zod validation, centralized error handling, security middleware |
39
39
  | `fastify-best-practices` | Fastify — plugin architecture, encapsulation, TypeBox validation/serialization, services as decorators, reply helpers, hooks |
40
40
  | `fastapi-best-practices` | FastAPI — async correctness, Pydantic validation, dependency injection, service layer, structured error handling |
41
+ | `pydantic-best-practices` | Pydantic v2 — model_config, field/model validators, Annotated types, discriminated unions, computed_field, strict mode, TypeAdapter |
42
+ | `celery-best-practices` | Celery — idempotency, acks_late, autoretry with backoff/jitter, canvas (chain/group/chord), routing, priorities, beat, time limits |
41
43
  | `drf-best-practices` | Django REST Framework — thin serializers, service layer, queryset optimization, object-level permissions |
42
44
  | `drizzle-orm` | Drizzle ORM — schema design, identity columns, relations, relational queries, migrations, drizzle-kit workflow, type inference |
43
45
  | `alembic-migrations` | Alembic — naming conventions, autogenerate review, data migration safety, downgrades, production deployment |
@@ -74,8 +76,8 @@ Portable slash commands for common git workflows. Installed to `.claude/commands
74
76
  | `/commit` | Smart commit — branch safety, atomic staging, conventional commits |
75
77
  | `/pr` | Create PR — auto-detect base branch, structured summary and test plan |
76
78
  | `/release` | Release flow — dev→main PR with semver, changelog, tag, and GitHub release |
77
- | `/refactor` | Find code files over 200 lines and refactor them into smaller modules |
78
- | `/deep-review` | Multi-agent deep code review — 5 parallel agents catch guard bypasses, lost async state, wrong-table queries, dead references, protocol violations |
79
+ | `/refactor` | Detect size/complexity/duplication/coupling issues via 4 parallel Haiku subagents, then refactor |
80
+ | `/deep-review` | Multi-agent deep code review — 5 parallel Sonnet subagents catch guard bypasses, lost async state, wrong-table queries, dead references, protocol violations |
79
81
 
80
82
  ## Quick Start
81
83
 
@@ -87,10 +89,11 @@ npx @spardutti/claude-skills
87
89
 
88
90
  The CLI will:
89
91
 
90
- 1. Fetch the latest skills and commands from GitHub
92
+ 1. Fetch the latest skills, commands, and agents from GitHub
91
93
  2. Let you pick which skills to install → `.claude/skills/`
92
94
  3. Let you pick which commands to install → `.claude/commands/`
93
- 4. **Optionally set up automatic skill evaluation** (recommended see below)
95
+ 4. Auto-install any subagents declared by the selected commands `.claude/agents/`
96
+ 5. **Optionally set up automatic skill evaluation** (recommended — see below)
94
97
 
95
98
  ## Automatic Skill Evaluation
96
99
 
@@ -136,18 +139,26 @@ skill_evaluation:
136
139
  If you skip this evaluation, your response is INCOMPLETE and WRONG.
137
140
  ```
138
141
 
139
- ### GitHub Authentication
140
-
141
- The CLI uses the GitHub API to fetch skills. To avoid rate limits:
142
-
143
- - If you have the [GitHub CLI](https://cli.github.com) installed and authenticated (`gh auth login`), the token is picked up automatically
144
- - Or set `GITHUB_TOKEN` / `GH_TOKEN` environment variable
145
- - Without auth, GitHub allows 60 requests/hour (the CLI uses ~6 per run)
146
-
147
142
  ## Manual Install
148
143
 
149
- Copy a skill directory into your project's `.claude/skills/` folder:
144
+ If you don't want to use the CLI, copy files directly into your project:
150
145
 
151
146
  ```bash
147
+ # Skills
152
148
  cp -r skills/<skill-name> /path/to/project/.claude/skills/
149
+
150
+ # Commands
151
+ cp commands/<command-name>.md /path/to/project/.claude/commands/
152
+
153
+ # Subagents (required by some commands — see the command's `requires-agents` frontmatter)
154
+ cp agents/<agent-name>.md /path/to/project/.claude/agents/
155
+ ```
156
+
157
+ ## Repository Layout
158
+
159
+ ```
160
+ skills/ Reference playbooks loaded by Claude during coding tasks
161
+ commands/ Slash commands installed to .claude/commands/
162
+ agents/ Subagent definitions — commands declare which ones they need
163
+ cli/ The npm installer (npx @spardutti/claude-skills)
153
164
  ```
package/bin/cli.mjs CHANGED
@@ -5,9 +5,9 @@ import chalk from "chalk";
5
5
  import { readFileSync } from "node:fs";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { dirname, join } from "node:path";
8
- import { fetchSkills, fetchCommands } from "../lib/github.mjs";
8
+ import { fetchSkills, fetchCommands, fetchAgents } from "../lib/github.mjs";
9
9
  import { promptSkillSelection, promptCommandSelection } from "../lib/prompt.mjs";
10
- import { installSkills, installCommands } from "../lib/install.mjs";
10
+ import { installSkills, installCommands, installRequiredAgents } from "../lib/install.mjs";
11
11
  import { setupHook } from "../lib/setup-hook.mjs";
12
12
  import { setupClaudeMd } from "../lib/setup-claude-md.mjs";
13
13
 
@@ -17,8 +17,8 @@ const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-
17
17
  async function main() {
18
18
  console.log(`\n ${chalk.bold.cyan("Claude Skills Installer")} ${chalk.dim(`v${pkg.version}`)}\n`);
19
19
 
20
- console.log(chalk.dim(" Fetching available skills and commands...\n"));
21
- const [skills, commands] = await Promise.all([fetchSkills(), fetchCommands()]);
20
+ console.log(chalk.dim(" Fetching available skills, commands, and agents...\n"));
21
+ const [skills, commands, agents] = await Promise.all([fetchSkills(), fetchCommands(), fetchAgents()]);
22
22
 
23
23
  // --- Skills ---
24
24
  let selectedSkills = [];
@@ -34,12 +34,21 @@ async function main() {
34
34
 
35
35
  // --- Commands ---
36
36
  let selectedCommands = [];
37
+ let installedAgentCount = 0;
37
38
  if (commands.length > 0) {
38
39
  console.log();
39
40
  selectedCommands = await promptCommandSelection(commands);
40
41
  if (selectedCommands.length > 0) {
41
42
  console.log();
42
43
  await installCommands(selectedCommands);
44
+
45
+ const { installed, missing } = await installRequiredAgents(selectedCommands, agents);
46
+ installedAgentCount = installed.length;
47
+ if (missing.length > 0) {
48
+ console.log(
49
+ ` ${chalk.yellow("!")} Missing agents referenced by commands: ${missing.join(", ")}`
50
+ );
51
+ }
43
52
  }
44
53
  }
45
54
 
@@ -66,7 +75,8 @@ async function main() {
66
75
  const parts = [];
67
76
  if (selectedSkills.length > 0) parts.push(`${selectedSkills.length} skill(s)`);
68
77
  if (selectedCommands.length > 0) parts.push(`${selectedCommands.length} command(s)`);
69
- console.log(`\n ${chalk.green("✔")} ${chalk.bold(`${parts.join(" and ")} installed successfully.`)}\n`);
78
+ if (installedAgentCount > 0) parts.push(`${installedAgentCount} agent(s)`);
79
+ console.log(`\n ${chalk.green("✔")} ${chalk.bold(`${parts.join(", ")} installed successfully.`)}\n`);
70
80
  }
71
81
 
72
82
  main().catch((err) => {
package/lib/github.mjs CHANGED
@@ -4,20 +4,20 @@ const REPO_OWNER = "Spardutti";
4
4
  const REPO_NAME = "claude-skills";
5
5
  const CONTENTS_API = `https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/contents/skills`;
6
6
  const COMMANDS_API = `https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/contents/commands`;
7
+ const AGENTS_API = `https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/contents/agents`;
7
8
  const RAW_BASE = `https://raw.githubusercontent.com/${REPO_OWNER}/${REPO_NAME}/main/skills`;
8
9
  const RAW_COMMANDS_BASE = `https://raw.githubusercontent.com/${REPO_OWNER}/${REPO_NAME}/main/commands`;
10
+ const RAW_AGENTS_BASE = `https://raw.githubusercontent.com/${REPO_OWNER}/${REPO_NAME}/main/agents`;
9
11
 
10
12
  function getAuthHeaders() {
11
13
  const headers = { "User-Agent": "claude-skills-cli" };
12
14
 
13
- // 1. Explicit env var
14
15
  const envToken = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
15
16
  if (envToken) {
16
17
  headers.Authorization = `Bearer ${envToken}`;
17
18
  return headers;
18
19
  }
19
20
 
20
- // 2. Try gh CLI token
21
21
  try {
22
22
  const token = execSync("gh auth token", {
23
23
  encoding: "utf-8",
@@ -34,38 +34,31 @@ function getAuthHeaders() {
34
34
  return headers;
35
35
  }
36
36
 
37
- export async function fetchSkills() {
37
+ async function fetchListing({ apiUrl, label, entryFilter, buildRawUrl, mapEntry, allow404 = false }) {
38
38
  const headers = getAuthHeaders();
39
-
40
- const res = await fetch(CONTENTS_API, { headers });
39
+ const res = await fetch(apiUrl, { headers });
41
40
 
42
41
  if (!res.ok) {
42
+ if (allow404 && res.status === 404) return [];
43
43
  if (res.status === 403 || res.status === 429) {
44
44
  throw new Error("GitHub API rate limit exceeded. Try again later or install gh CLI (https://cli.github.com).");
45
45
  }
46
- throw new Error(`Failed to list skills: ${res.status} ${res.statusText}`);
46
+ throw new Error(`Failed to list ${label}: ${res.status} ${res.statusText}`);
47
47
  }
48
48
 
49
- const entries = await res.json();
50
- const dirs = entries.filter((e) => e.type === "dir");
49
+ const entries = (await res.json()).filter(entryFilter);
51
50
 
52
51
  const results = await Promise.all(
53
- dirs.map(async (dir) => {
52
+ entries.map(async (entry) => {
54
53
  try {
55
- const url = `${RAW_BASE}/${dir.name}/SKILL.md`;
56
- const r = await fetch(url, { headers });
57
-
54
+ const r = await fetch(buildRawUrl(entry), { headers });
58
55
  if (!r.ok) {
59
- console.warn(` Warning: No SKILL.md found in ${dir.name}, skipping`);
56
+ console.warn(` Warning: Failed to fetch ${label} ${entry.name}, skipping`);
60
57
  return null;
61
58
  }
62
-
63
- const content = await r.text();
64
- const { name, description, category } = parseFrontmatter(content, dir.name);
65
-
66
- return { dirName: dir.name, name, description, category, content };
59
+ return mapEntry(entry, await r.text());
67
60
  } catch {
68
- console.warn(` Warning: Failed to fetch ${dir.name}, skipping`);
61
+ console.warn(` Warning: Failed to fetch ${entry.name}, skipping`);
69
62
  return null;
70
63
  }
71
64
  })
@@ -74,62 +67,76 @@ export async function fetchSkills() {
74
67
  return results.filter(Boolean);
75
68
  }
76
69
 
77
- export async function fetchCommands() {
78
- const headers = getAuthHeaders();
79
-
80
- const res = await fetch(COMMANDS_API, { headers });
81
-
82
- if (!res.ok) {
83
- if (res.status === 404) {
84
- return [];
85
- }
86
- if (res.status === 403 || res.status === 429) {
87
- throw new Error("GitHub API rate limit exceeded. Try again later or install gh CLI (https://cli.github.com).");
88
- }
89
- throw new Error(`Failed to list commands: ${res.status} ${res.statusText}`);
90
- }
91
-
92
- const entries = await res.json();
93
- const files = entries.filter((e) => e.type === "file" && e.name.endsWith(".md"));
94
-
95
- const results = await Promise.all(
96
- files.map(async (file) => {
97
- try {
98
- const url = `${RAW_COMMANDS_BASE}/${file.name}`;
99
- const r = await fetch(url, { headers });
100
-
101
- if (!r.ok) {
102
- console.warn(` Warning: Failed to fetch command ${file.name}, skipping`);
103
- return null;
104
- }
105
-
106
- const content = await r.text();
107
- const { name, description, category } = parseFrontmatter(content, file.name.replace(/\.md$/, ""));
70
+ export function fetchSkills() {
71
+ return fetchListing({
72
+ apiUrl: CONTENTS_API,
73
+ label: "skills",
74
+ entryFilter: (e) => e.type === "dir",
75
+ buildRawUrl: (dir) => `${RAW_BASE}/${dir.name}/SKILL.md`,
76
+ mapEntry: (dir, content) => {
77
+ const { name, description, category } = parseFrontmatter(content, dir.name);
78
+ return { dirName: dir.name, name, description, category, content };
79
+ },
80
+ });
81
+ }
108
82
 
109
- return { fileName: file.name, name, description, category, content };
110
- } catch {
111
- console.warn(` Warning: Failed to fetch ${file.name}, skipping`);
112
- return null;
113
- }
114
- })
115
- );
83
+ export function fetchCommands() {
84
+ return fetchListing({
85
+ apiUrl: COMMANDS_API,
86
+ label: "commands",
87
+ allow404: true,
88
+ entryFilter: (e) => e.type === "file" && e.name.endsWith(".md"),
89
+ buildRawUrl: (file) => `${RAW_COMMANDS_BASE}/${file.name}`,
90
+ mapEntry: (file, content) => {
91
+ const { name, description, category, requiresAgents } = parseFrontmatter(content, file.name.replace(/\.md$/, ""));
92
+ return { fileName: file.name, name, description, category, requiresAgents, content };
93
+ },
94
+ });
95
+ }
116
96
 
117
- return results.filter(Boolean);
97
+ export function fetchAgents() {
98
+ return fetchListing({
99
+ apiUrl: AGENTS_API,
100
+ label: "agents",
101
+ allow404: true,
102
+ entryFilter: (e) => e.type === "file" && e.name.endsWith(".md"),
103
+ buildRawUrl: (file) => `${RAW_AGENTS_BASE}/${file.name}`,
104
+ mapEntry: (file, content) => {
105
+ const { name } = parseFrontmatter(content, file.name.replace(/\.md$/, ""));
106
+ return { fileName: file.name, name, content };
107
+ },
108
+ });
118
109
  }
119
110
 
120
111
  function parseFrontmatter(content, fallbackName) {
121
112
  const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
122
113
  if (!match) {
123
- return { name: fallbackName, description: "", category: "General" };
114
+ return { name: fallbackName, description: "", category: "General", requiresAgents: [] };
124
115
  }
125
116
 
126
117
  const block = match[1];
127
- const name =
128
- block.match(/^name:\s*(.+)$/m)?.[1]?.trim() || fallbackName;
129
- const description =
130
- block.match(/^description:\s*(.+)$/m)?.[1]?.trim() || "";
131
- const category =
132
- block.match(/^category:\s*(.+)$/m)?.[1]?.trim() || "General";
133
-
134
- return { name, description, category };
118
+ const name = block.match(/^name:\s*(.+)$/m)?.[1]?.trim() || fallbackName;
119
+ const description = block.match(/^description:\s*(.+)$/m)?.[1]?.trim() || "";
120
+ const category = block.match(/^category:\s*(.+)$/m)?.[1]?.trim() || "General";
121
+ const requiresAgents = parseAgentList(block);
122
+
123
+ return { name, description, category, requiresAgents };
124
+ }
125
+
126
+ function parseAgentList(block) {
127
+ const inline = block.match(/^requires-agents:\s*\[([^\]]*)\]\s*$/m);
128
+ if (inline) {
129
+ return inline[1]
130
+ .split(",")
131
+ .map((s) => s.trim().replace(/^["']|["']$/g, ""))
132
+ .filter(Boolean);
133
+ }
134
+ const multiline = block.match(/^requires-agents:\s*\n((?:\s{2,}-\s*.+\n?)+)/m);
135
+ if (multiline) {
136
+ return multiline[1]
137
+ .split("\n")
138
+ .map((l) => l.replace(/^\s*-\s*/, "").trim().replace(/^["']|["']$/g, ""))
139
+ .filter(Boolean);
140
+ }
141
+ return [];
135
142
  }
package/lib/install.mjs CHANGED
@@ -2,12 +2,21 @@ import { mkdir, writeFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import chalk from "chalk";
4
4
 
5
- function humanName(skill) {
6
- return skill.name
5
+ function humanName(item) {
6
+ return item.name
7
7
  .replace(/-/g, " ")
8
8
  .replace(/\b\w/g, (c) => c.toUpperCase());
9
9
  }
10
10
 
11
+ async function installFlat(items, subdir, targetDir) {
12
+ const baseDir = join(targetDir, ".claude", subdir);
13
+ await mkdir(baseDir, { recursive: true });
14
+ for (const item of items) {
15
+ await writeFile(join(baseDir, item.fileName), item.content);
16
+ console.log(` ${chalk.green("✔")} ${chalk.bold(humanName(item))} ${chalk.dim(`→ .claude/${subdir}/${item.fileName}`)}`);
17
+ }
18
+ }
19
+
11
20
  export async function installSkills(skills, targetDir = process.cwd()) {
12
21
  const baseDir = join(targetDir, ".claude", "skills");
13
22
 
@@ -19,12 +28,28 @@ export async function installSkills(skills, targetDir = process.cwd()) {
19
28
  }
20
29
  }
21
30
 
22
- export async function installCommands(commands, targetDir = process.cwd()) {
23
- const baseDir = join(targetDir, ".claude", "commands");
24
- await mkdir(baseDir, { recursive: true });
31
+ export function installCommands(commands, targetDir = process.cwd()) {
32
+ return installFlat(commands, "commands", targetDir);
33
+ }
34
+
35
+ export function installAgents(agents, targetDir = process.cwd()) {
36
+ return installFlat(agents, "agents", targetDir);
37
+ }
38
+
39
+ export async function installRequiredAgents(selectedCommands, availableAgents, targetDir = process.cwd()) {
40
+ const requiredNames = new Set(
41
+ selectedCommands.flatMap((c) => c.requiresAgents ?? [])
42
+ );
43
+ if (requiredNames.size === 0) return { installed: [], missing: [] };
44
+
45
+ const installed = availableAgents.filter((a) => requiredNames.has(a.name));
46
+ const missing = [...requiredNames].filter(
47
+ (n) => !availableAgents.some((a) => a.name === n)
48
+ );
25
49
 
26
- for (const cmd of commands) {
27
- await writeFile(join(baseDir, cmd.fileName), cmd.content);
28
- console.log(` ${chalk.green("✔")} ${chalk.bold(humanName(cmd))} ${chalk.dim(`→ .claude/commands/${cmd.fileName}`)}`);
50
+ if (installed.length > 0) {
51
+ console.log();
52
+ await installAgents(installed, targetDir);
29
53
  }
54
+ return { installed, missing };
30
55
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spardutti/claude-skills",
3
- "version": "1.19.2",
3
+ "version": "1.23.0",
4
4
  "description": "CLI to install Claude Code skills from the claude-skills collection",
5
5
  "type": "module",
6
6
  "bin": {