@spardutti/claude-skills 2.2.0 → 2.3.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/lib/github.mjs +51 -11
- package/lib/install.mjs +8 -1
- package/lib/setup-hook.mjs +85 -0
- package/package.json +1 -1
package/lib/github.mjs
CHANGED
|
@@ -67,17 +67,57 @@ async function fetchListing({ apiUrl, label, entryFilter, buildRawUrl, mapEntry,
|
|
|
67
67
|
return results.filter(Boolean);
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
export function fetchSkills() {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
70
|
+
export async function fetchSkills() {
|
|
71
|
+
const headers = getAuthHeaders();
|
|
72
|
+
const res = await fetch(CONTENTS_API, { headers });
|
|
73
|
+
|
|
74
|
+
if (!res.ok) {
|
|
75
|
+
if (res.status === 403 || res.status === 429) {
|
|
76
|
+
throw new Error("GitHub API rate limit exceeded. Try again later or install gh CLI (https://cli.github.com).");
|
|
77
|
+
}
|
|
78
|
+
throw new Error(`Failed to list skills: ${res.status} ${res.statusText}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const dirs = (await res.json()).filter((e) => e.type === "dir");
|
|
82
|
+
|
|
83
|
+
const skills = await Promise.all(
|
|
84
|
+
dirs.map(async (dir) => {
|
|
85
|
+
try {
|
|
86
|
+
const listRes = await fetch(`${CONTENTS_API}/${dir.name}`, { headers });
|
|
87
|
+
if (!listRes.ok) {
|
|
88
|
+
console.warn(` Warning: Failed to list skill ${dir.name}, skipping`);
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
const files = (await listRes.json()).filter((e) => e.type === "file");
|
|
92
|
+
|
|
93
|
+
const fetched = await Promise.all(
|
|
94
|
+
files.map(async (f) => {
|
|
95
|
+
const r = await fetch(`${RAW_BASE}/${dir.name}/${f.name}`, { headers });
|
|
96
|
+
if (!r.ok) {
|
|
97
|
+
console.warn(` Warning: Failed to fetch ${dir.name}/${f.name}, skipping`);
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
return { name: f.name, content: await r.text(), executable: f.name.endsWith(".sh") };
|
|
101
|
+
})
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const valid = fetched.filter(Boolean);
|
|
105
|
+
const skillMd = valid.find((f) => f.name === "SKILL.md");
|
|
106
|
+
if (!skillMd) {
|
|
107
|
+
console.warn(` Warning: ${dir.name}/SKILL.md missing, skipping`);
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
const peerFiles = valid.filter((f) => f.name !== "SKILL.md");
|
|
111
|
+
const { name, description, category } = parseFrontmatter(skillMd.content, dir.name);
|
|
112
|
+
return { dirName: dir.name, name, description, category, content: skillMd.content, peerFiles };
|
|
113
|
+
} catch {
|
|
114
|
+
console.warn(` Warning: Failed to fetch ${dir.name}, skipping`);
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
return skills.filter(Boolean);
|
|
81
121
|
}
|
|
82
122
|
|
|
83
123
|
export function fetchCommands() {
|
package/lib/install.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
1
|
+
import { mkdir, writeFile, chmod } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
|
|
@@ -24,6 +24,13 @@ export async function installSkills(skills, targetDir = process.cwd()) {
|
|
|
24
24
|
const skillDir = join(baseDir, skill.dirName);
|
|
25
25
|
await mkdir(skillDir, { recursive: true });
|
|
26
26
|
await writeFile(join(skillDir, "SKILL.md"), skill.content);
|
|
27
|
+
|
|
28
|
+
for (const peer of skill.peerFiles ?? []) {
|
|
29
|
+
const peerPath = join(skillDir, peer.name);
|
|
30
|
+
await writeFile(peerPath, peer.content);
|
|
31
|
+
if (peer.executable) await chmod(peerPath, 0o755);
|
|
32
|
+
}
|
|
33
|
+
|
|
27
34
|
console.log(` ${chalk.green("✔")} ${chalk.bold(humanName(skill))} ${chalk.dim(`→ .claude/skills/${skill.dirName}/`)}`);
|
|
28
35
|
}
|
|
29
36
|
}
|
package/lib/setup-hook.mjs
CHANGED
|
@@ -54,8 +54,78 @@ touch "/tmp/claude-skill-gate-$SESSION_ID"
|
|
|
54
54
|
exit 0
|
|
55
55
|
`;
|
|
56
56
|
|
|
57
|
+
// PreToolUse audit runner: runs each installed skill's audit.sh against the
|
|
58
|
+
// write target. Denies the write if any audit exits 2. Defers (exits 0) when
|
|
59
|
+
// the skill-gate marker isn't set yet — the gate handles that case.
|
|
60
|
+
//
|
|
61
|
+
// Honors .claude/skill-audit-ignore (one skill name per line, # comments OK).
|
|
62
|
+
const AUDIT_RUNNER_SCRIPT = `#!/bin/bash
|
|
63
|
+
# PreToolUse skill-audit runner.
|
|
64
|
+
|
|
65
|
+
INPUT=$(cat)
|
|
66
|
+
|
|
67
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
68
|
+
PROJECT_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")"
|
|
69
|
+
|
|
70
|
+
SESSION_ID=$(printf '%s' "$INPUT" | grep -o '"session_id":"[^"]*"' | head -1 | sed 's/"session_id":"//; s/"$//')
|
|
71
|
+
if [ -z "$SESSION_ID" ]; then
|
|
72
|
+
exit 0
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
# Defer until the skill-gate has been satisfied in this session.
|
|
76
|
+
if [ ! -f "/tmp/claude-skill-gate-$SESSION_ID" ]; then
|
|
77
|
+
exit 0
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
FILE_PATH=$(printf '%s' "$INPUT" | grep -o '"file_path":"[^"]*"' | head -1 | sed 's/"file_path":"//; s/"$//')
|
|
81
|
+
if [ -z "$FILE_PATH" ]; then
|
|
82
|
+
exit 0
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
SKILLS_DIR="$PROJECT_DIR/.claude/skills"
|
|
86
|
+
[ ! -d "$SKILLS_DIR" ] && exit 0
|
|
87
|
+
|
|
88
|
+
IGNORE_FILE="$PROJECT_DIR/.claude/skill-audit-ignore"
|
|
89
|
+
is_ignored() {
|
|
90
|
+
[ ! -f "$IGNORE_FILE" ] && return 1
|
|
91
|
+
grep -qE "^[[:space:]]*\${1}[[:space:]]*(#.*)?$" "$IGNORE_FILE"
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# Portable JSON string escape for the deny reason.
|
|
95
|
+
json_escape() {
|
|
96
|
+
local s="$1"
|
|
97
|
+
s="\${s//\\\\/\\\\\\\\}"
|
|
98
|
+
s="\${s//\\"/\\\\\\"}"
|
|
99
|
+
s="\${s//$'\\n'/\\\\n}"
|
|
100
|
+
s="\${s//$'\\r'/\\\\r}"
|
|
101
|
+
s="\${s//$'\\t'/\\\\t}"
|
|
102
|
+
printf '"%s"' "$s"
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
for audit in "$SKILLS_DIR"/*/audit.sh; do
|
|
106
|
+
[ ! -f "$audit" ] && continue
|
|
107
|
+
[ ! -x "$audit" ] && continue
|
|
108
|
+
skill_name=$(basename "$(dirname "$audit")")
|
|
109
|
+
if is_ignored "$skill_name"; then
|
|
110
|
+
continue
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
output=$("$audit" "$FILE_PATH" 2>&1 >/dev/null)
|
|
114
|
+
rc=$?
|
|
115
|
+
|
|
116
|
+
if [ $rc -eq 2 ]; then
|
|
117
|
+
reason=$(json_escape "$output")
|
|
118
|
+
printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":%s}}\\n' "$reason"
|
|
119
|
+
exit 0
|
|
120
|
+
fi
|
|
121
|
+
done
|
|
122
|
+
|
|
123
|
+
exit 0
|
|
124
|
+
`;
|
|
125
|
+
|
|
57
126
|
const GATE_FILENAME = "skill-gate.sh";
|
|
58
127
|
const AUTO_MARK_FILENAME = "skill-gate-automark.sh";
|
|
128
|
+
const AUDIT_RUNNER_FILENAME = "skill-audit-runner.sh";
|
|
59
129
|
const LEGACY_EVAL_FILENAME = "skill-forced-eval-hook.sh";
|
|
60
130
|
|
|
61
131
|
export async function setupHook(targetDir = process.cwd()) {
|
|
@@ -63,6 +133,7 @@ export async function setupHook(targetDir = process.cwd()) {
|
|
|
63
133
|
const hooksDir = join(resolved, ".claude", "hooks");
|
|
64
134
|
const gatePath = join(hooksDir, GATE_FILENAME);
|
|
65
135
|
const autoMarkPath = join(hooksDir, AUTO_MARK_FILENAME);
|
|
136
|
+
const auditRunnerPath = join(hooksDir, AUDIT_RUNNER_FILENAME);
|
|
66
137
|
const settingsPath = join(resolved, ".claude", "settings.json");
|
|
67
138
|
|
|
68
139
|
await mkdir(hooksDir, { recursive: true });
|
|
@@ -70,6 +141,8 @@ export async function setupHook(targetDir = process.cwd()) {
|
|
|
70
141
|
await chmod(gatePath, 0o755);
|
|
71
142
|
await writeFile(autoMarkPath, AUTO_MARK_SCRIPT, { mode: 0o755 });
|
|
72
143
|
await chmod(autoMarkPath, 0o755);
|
|
144
|
+
await writeFile(auditRunnerPath, AUDIT_RUNNER_SCRIPT, { mode: 0o755 });
|
|
145
|
+
await chmod(auditRunnerPath, 0o755);
|
|
73
146
|
|
|
74
147
|
let settings = {};
|
|
75
148
|
try {
|
|
@@ -105,6 +178,17 @@ export async function setupHook(targetDir = process.cwd()) {
|
|
|
105
178
|
settings.hooks.PreToolUse = [gateEntry];
|
|
106
179
|
}
|
|
107
180
|
|
|
181
|
+
// Register PreToolUse audit runner (after the gate).
|
|
182
|
+
const auditCommand = `$CLAUDE_PROJECT_DIR/.claude/hooks/${AUDIT_RUNNER_FILENAME}`;
|
|
183
|
+
const auditEntry = {
|
|
184
|
+
matcher: "Write|Edit|MultiEdit",
|
|
185
|
+
hooks: [{ type: "command", command: auditCommand }],
|
|
186
|
+
};
|
|
187
|
+
const auditAlreadyRegistered = settings.hooks.PreToolUse.some((entry) =>
|
|
188
|
+
entry.hooks?.some((h) => h.command?.endsWith(AUDIT_RUNNER_FILENAME))
|
|
189
|
+
);
|
|
190
|
+
if (!auditAlreadyRegistered) settings.hooks.PreToolUse.push(auditEntry);
|
|
191
|
+
|
|
108
192
|
// Register PostToolUse auto-mark on Skill.
|
|
109
193
|
const autoMarkCommand = `$CLAUDE_PROJECT_DIR/.claude/hooks/${AUTO_MARK_FILENAME}`;
|
|
110
194
|
const autoMarkEntry = {
|
|
@@ -125,5 +209,6 @@ export async function setupHook(targetDir = process.cwd()) {
|
|
|
125
209
|
|
|
126
210
|
console.log(` Hook installed: .claude/hooks/${GATE_FILENAME}`);
|
|
127
211
|
console.log(` Hook installed: .claude/hooks/${AUTO_MARK_FILENAME}`);
|
|
212
|
+
console.log(` Hook installed: .claude/hooks/${AUDIT_RUNNER_FILENAME}`);
|
|
128
213
|
console.log(` Settings updated: .claude/settings.json`);
|
|
129
214
|
}
|