cyber-skills 0.0.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/bin/cyber-skills.mts +102 -0
- package/hooks/definitions/commit-discipline.mts +15 -0
- package/hooks/definitions/init.mts +22 -0
- package/hooks/inject-commit-discipline.mts +66 -0
- package/hooks/lib/commit-discipline-content.mts +53 -0
- package/hooks/lib/hook-command.mts +31 -0
- package/hooks/lib/package-root.mts +7 -0
- package/hooks/register-agent-hooks.mts +290 -0
- package/hooks/runtime/commit-discipline.mts +39 -0
- package/package.json +57 -0
- package/readme.md +76 -0
- package/skills/audit-skill/SKILL.md +271 -0
- package/skills/audit-skill/scripts/validate-skills.mts +495 -0
- package/skills/commit/SKILL.md +34 -0
- package/skills/configure-awesome-sources/SKILL.md +73 -0
- package/skills/configure-awesome-sources/scripts/configure-awesome-sources.mts +252 -0
- package/skills/create-skill/SKILL.md +126 -0
- package/skills/find-awesome-skill/SKILL.md +55 -0
- package/skills/find-awesome-skill/scripts/awesome-lib.mts +476 -0
- package/skills/find-awesome-skill/scripts/find-awesome-skill.mts +39 -0
- package/skills/init/SKILL.md +83 -0
- package/skills/init-commit-discipline/SKILL.md +75 -0
- package/skills/init-commit-discipline/scripts/resolve-commit-skill.mts +76 -0
- package/skills/patch-skill/SKILL.md +229 -0
- package/skills/skillify/SKILL.md +110 -0
- package/skills/update-awesome-list/SKILL.md +65 -0
- package/skills/update-awesome-list/scripts/inspect-skills-repo.mts +112 -0
- package/skills/update-awesome-list/scripts/render-awesome-list.mts +91 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Detect installed commit helper skills and recommend defaults.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync } from 'node:fs'
|
|
7
|
+
import { homedir } from 'node:os'
|
|
8
|
+
import { join } from 'node:path'
|
|
9
|
+
|
|
10
|
+
export const RECOMMENDED_COMMIT_SKILL = 'commit-work'
|
|
11
|
+
export const RECOMMENDED_INSTALL = 'npx skills add softaworks/agent-toolkit --skill commit-work -g'
|
|
12
|
+
export const BUNDLED_COMMIT_SKILL = 'commit'
|
|
13
|
+
|
|
14
|
+
const KNOWN_COMMIT_SKILL_NAMES = new Set(['commit-work', 'commit', 'git-commit', 'commit-workflow'])
|
|
15
|
+
|
|
16
|
+
export interface DetectedCommitSkill {
|
|
17
|
+
name: string
|
|
18
|
+
path: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function skillDirs(root: string): string[] {
|
|
22
|
+
const home = homedir()
|
|
23
|
+
return [
|
|
24
|
+
join(root, '.agents', 'skills'),
|
|
25
|
+
join(home, '.agents', 'skills'),
|
|
26
|
+
join(root, '.claude', 'skills'),
|
|
27
|
+
join(home, '.claude', 'skills'),
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function detectCommitSkills(root = process.cwd()): DetectedCommitSkill[] {
|
|
32
|
+
const found = new Map<string, DetectedCommitSkill>()
|
|
33
|
+
|
|
34
|
+
for (const base of skillDirs(root)) {
|
|
35
|
+
if (!existsSync(base)) continue
|
|
36
|
+
for (const name of KNOWN_COMMIT_SKILL_NAMES) {
|
|
37
|
+
const skillPath = join(base, name, 'SKILL.md')
|
|
38
|
+
if (existsSync(skillPath) && !found.has(name)) {
|
|
39
|
+
found.set(name, { name, path: skillPath })
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return [...found.values()]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function resolveCommitSkillName(detected: DetectedCommitSkill[], preferred?: string): string | null {
|
|
48
|
+
if (preferred) return preferred
|
|
49
|
+
const commitWork = detected.find((s) => s.name === RECOMMENDED_COMMIT_SKILL)
|
|
50
|
+
if (commitWork) return commitWork.name
|
|
51
|
+
if (detected.length === 1) return detected[0]!.name
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (process.argv[1] === import.meta.filename) {
|
|
56
|
+
const args = process.argv.slice(2)
|
|
57
|
+
const rootIdx = args.indexOf('--root')
|
|
58
|
+
const root = rootIdx !== -1 ? args[rootIdx + 1]! : process.cwd()
|
|
59
|
+
|
|
60
|
+
if (args.includes('--recommend')) {
|
|
61
|
+
process.stdout.write(`${RECOMMENDED_INSTALL}\n`)
|
|
62
|
+
process.exit(0)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const detected = detectCommitSkills(root)
|
|
66
|
+
const payload = {
|
|
67
|
+
detected,
|
|
68
|
+
recommended: RECOMMENDED_COMMIT_SKILL,
|
|
69
|
+
recommendedInstall: RECOMMENDED_INSTALL,
|
|
70
|
+
bundledFallback: BUNDLED_COMMIT_SKILL,
|
|
71
|
+
resolved: resolveCommitSkillName(detected),
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
process.stdout.write(`${JSON.stringify(payload)}\n`)
|
|
75
|
+
process.exit(detected.length > 0 ? 0 : 1)
|
|
76
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: patch-skill
|
|
3
|
+
description: Use this skill when contributing local improvements to an installed skill back to its source repo via PR.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Patch Skill
|
|
7
|
+
|
|
8
|
+
When you have improved a skill you installed from another repo, this skill guides you through contributing that improvement back to the source via a pull request.
|
|
9
|
+
|
|
10
|
+
## When to use
|
|
11
|
+
|
|
12
|
+
- You modified a skill (global `~/.agents/skills/<name>/` or repo-internal `.agents/skills/<name>/`) and want to send the improvement upstream
|
|
13
|
+
- The skill was installed from a public repo tracked in `skills-lock.json`
|
|
14
|
+
|
|
15
|
+
Do NOT use for repo-native skills under this repo's `skills/<name>/` — if you're the author here, this repo IS the source.
|
|
16
|
+
|
|
17
|
+
## Source repository scope (required)
|
|
18
|
+
|
|
19
|
+
Only update files under the source repo's canonical skills tree:
|
|
20
|
+
|
|
21
|
+
```text
|
|
22
|
+
<repo-root>/skills/<skill-name>/
|
|
23
|
+
SKILL.md
|
|
24
|
+
scripts/ ← include when changed
|
|
25
|
+
… ← any other files in that skill folder
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Rules:
|
|
29
|
+
|
|
30
|
+
- **Always** map upstream paths to `skills/<skill-name>/…`, even when `skills-lock.json` `skillPath` points at `.agents/skills/…` or another layout in the consumer repo.
|
|
31
|
+
- **Never** update `.agents/skills/`, duplicate trees, or paths outside `skills/<skill-name>/` in the source repository.
|
|
32
|
+
- Include every changed file under that skill folder (e.g. `scripts/*.mts`), not only `SKILL.md`.
|
|
33
|
+
- Never include `SKILL.local.md` — local augmentations stay local.
|
|
34
|
+
|
|
35
|
+
Derive paths:
|
|
36
|
+
|
|
37
|
+
| Input | Upstream path |
|
|
38
|
+
| --- | --- |
|
|
39
|
+
| Lock key / folder name `audit-skill` | `skills/audit-skill/` |
|
|
40
|
+
| `skillPath`: `.agents/skills/create-skill/SKILL.md` | `skills/create-skill/SKILL.md` |
|
|
41
|
+
| `skillPath`: `skills/fix-security-pr/SKILL.md` | `skills/fix-security-pr/SKILL.md` |
|
|
42
|
+
|
|
43
|
+
## Steps
|
|
44
|
+
|
|
45
|
+
### 1. Identify the skill(s) to patch
|
|
46
|
+
|
|
47
|
+
From context or ask the user. Look up origin in `skills-lock.json` at the consumer repo root:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
jq '.skills["<skill-name>"]' skills-lock.json
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Extract: `source` (`owner/repo`), `skillPath` (for naming only — upstream target is always `skills/<skill-name>/`).
|
|
54
|
+
|
|
55
|
+
If there is no lock entry, ask for `owner/repo` and the skill folder name.
|
|
56
|
+
|
|
57
|
+
For multiple related skills (user confirms one PR), repeat file discovery for each `skills/<name>/` directory.
|
|
58
|
+
|
|
59
|
+
### 2. Find the local skill directory
|
|
60
|
+
|
|
61
|
+
| Kind | Local directory |
|
|
62
|
+
| --- | --- |
|
|
63
|
+
| Global | `~/.agents/skills/<name>/` |
|
|
64
|
+
| Repo internal | `.agents/skills/<name>/` |
|
|
65
|
+
|
|
66
|
+
Collect files to contribute (exclude `SKILL.local.md`):
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
find "<local-dir>" -type f ! -name 'SKILL.local.md' | sort
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 3. Diff against source
|
|
73
|
+
|
|
74
|
+
For each local file, map to `skills/<skill-name>/<relative-path>` and compare to upstream on the default branch.
|
|
75
|
+
|
|
76
|
+
List upstream skill folder (optional sanity check):
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
gh api "repos/<owner>/<repo>/contents/skills/<skill-name>?ref=<default-branch>" \
|
|
80
|
+
--jq '.[] | .path'
|
|
81
|
+
# If scripts/ exists, list nested files too:
|
|
82
|
+
gh api "repos/<owner>/<repo>/contents/skills/<skill-name>/scripts?ref=<default-branch>" \
|
|
83
|
+
--jq '.[]? | .path' 2>/dev/null
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Per-file diff:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
UPSTREAM="skills/<skill-name>/SKILL.md"
|
|
90
|
+
gh api "repos/<owner>/<repo>/contents/${UPSTREAM}" --jq '.content' | base64 -d > /tmp/upstream.md
|
|
91
|
+
diff /tmp/upstream.md "<local-dir>/SKILL.md"
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
- **No diffs** across all mapped files → nothing to contribute; stop
|
|
95
|
+
- **Any diff** → show unified diffs and get user confirmation before proceeding
|
|
96
|
+
|
|
97
|
+
### 4. Check write access
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
gh api repos/<owner>/<repo> --jq '{push: .permissions.push, default: .default_branch}'
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
- `push: true` → branch on `owner/repo`
|
|
104
|
+
- `push: false` → `gh repo fork <owner>/<repo> --clone=false`, then use `<your-username>/<repo>`
|
|
105
|
+
|
|
106
|
+
### 5. Create branch and push (single commit — Git Data API)
|
|
107
|
+
|
|
108
|
+
Use the **Git Data API** so all files land in **one commit**. Do not use `PUT /contents/{path}` per file (that creates one commit per file).
|
|
109
|
+
|
|
110
|
+
Set variables:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
OWNER=<owner>
|
|
114
|
+
REPO=<repo>
|
|
115
|
+
REPO_FULL="${OWNER}/${REPO}"
|
|
116
|
+
DEFAULT_BRANCH=main # from step 4
|
|
117
|
+
BRANCH="patch/<skill-name>-<short-slug>" # or shared slug for multi-skill PR
|
|
118
|
+
MSG="fix(<skill-name>): <description>"
|
|
119
|
+
SKILL_NAME=<skill-name>
|
|
120
|
+
LOCAL_DIR="<absolute-local-skill-dir>"
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
#### 5a. Create branch ref from default branch tip
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
BASE_SHA=$(gh api "repos/${REPO_FULL}/git/refs/heads/${DEFAULT_BRANCH}" --jq '.object.sha')
|
|
127
|
+
BASE_TREE=$(gh api "repos/${REPO_FULL}/git/commits/${BASE_SHA}" --jq '.tree.sha')
|
|
128
|
+
|
|
129
|
+
gh api "repos/${REPO_FULL}/git/refs" \
|
|
130
|
+
--method POST \
|
|
131
|
+
--field ref="refs/heads/${BRANCH}" \
|
|
132
|
+
--field sha="${BASE_SHA}"
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
If the branch already exists, skip creation and set `BASE_SHA` to the branch tip instead (for amending, prefer a fresh branch).
|
|
136
|
+
|
|
137
|
+
#### 5b. Create blobs and tree entries for each local file
|
|
138
|
+
|
|
139
|
+
Build a JSON array of tree items. Each local file maps to `skills/${SKILL_NAME}/<relative-path>`.
|
|
140
|
+
|
|
141
|
+
**Use temp files for `--input`**, not inline `$(jq …)`. Embedding file content in the shell argument hits `ARG_MAX` and fails with errors like `file name too long` on typical `SKILL.md` sizes.
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
TREE_ITEMS='[]'
|
|
145
|
+
while IFS= read -r -d '' file; do
|
|
146
|
+
rel="${file#${LOCAL_DIR}/}"
|
|
147
|
+
upstream_path="skills/${SKILL_NAME}/${rel}"
|
|
148
|
+
|
|
149
|
+
jq -n --rawfile content "$file" '{content: $content, encoding: "utf-8"}' > /tmp/patch-blob-input.json
|
|
150
|
+
blob_sha=$(gh api "repos/${REPO_FULL}/git/blobs" \
|
|
151
|
+
--method POST \
|
|
152
|
+
--input /tmp/patch-blob-input.json \
|
|
153
|
+
--jq '.sha')
|
|
154
|
+
|
|
155
|
+
TREE_ITEMS=$(jq -n \
|
|
156
|
+
--argjson items "$TREE_ITEMS" \
|
|
157
|
+
--arg path "$upstream_path" \
|
|
158
|
+
--arg sha "$blob_sha" \
|
|
159
|
+
'$items + [{path: $path, mode: "100644", type: "blob", sha: $sha}]')
|
|
160
|
+
done < <(find "$LOCAL_DIR" -type f ! -name 'SKILL.local.md' -print0)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Repeat the loop for additional skills in the same PR (append to `TREE_ITEMS` with each skill's `SKILL_NAME` and `LOCAL_DIR`).
|
|
164
|
+
|
|
165
|
+
#### 5c. Create tree, commit, update branch ref
|
|
166
|
+
|
|
167
|
+
Write tree and commit payloads to temp files as well (large multi-skill `TREE_ITEMS` can also exceed shell limits):
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
jq -n \
|
|
171
|
+
--arg base "$BASE_TREE" \
|
|
172
|
+
--argjson tree "$TREE_ITEMS" \
|
|
173
|
+
'{base_tree: $base, tree: $tree}' > /tmp/patch-tree-input.json
|
|
174
|
+
NEW_TREE=$(gh api "repos/${REPO_FULL}/git/trees" \
|
|
175
|
+
--method POST \
|
|
176
|
+
--input /tmp/patch-tree-input.json \
|
|
177
|
+
--jq '.sha')
|
|
178
|
+
|
|
179
|
+
jq -n \
|
|
180
|
+
--arg msg "$MSG" \
|
|
181
|
+
--arg tree "$NEW_TREE" \
|
|
182
|
+
--arg parent "$BASE_SHA" \
|
|
183
|
+
'{message: $msg, tree: $tree, parents: [$parent]}' > /tmp/patch-commit-input.json
|
|
184
|
+
COMMIT_SHA=$(gh api "repos/${REPO_FULL}/git/commits" \
|
|
185
|
+
--method POST \
|
|
186
|
+
--input /tmp/patch-commit-input.json \
|
|
187
|
+
--jq '.sha')
|
|
188
|
+
|
|
189
|
+
gh api "repos/${REPO_FULL}/git/refs/heads/${BRANCH}" \
|
|
190
|
+
--method PATCH \
|
|
191
|
+
--field sha="${COMMIT_SHA}"
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Result: exactly **one commit** on the patch branch containing all updated paths under `skills/<skill-name>/`.
|
|
195
|
+
|
|
196
|
+
### 6. Open PR
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
gh pr create \
|
|
200
|
+
--repo "${REPO_FULL}" \
|
|
201
|
+
--base "${DEFAULT_BRANCH}" \
|
|
202
|
+
--head "${BRANCH}" \
|
|
203
|
+
--title "fix(<skill-name>): <description>" \
|
|
204
|
+
--body "$(cat <<'EOF'
|
|
205
|
+
## Summary
|
|
206
|
+
|
|
207
|
+
<what changed and why — only under `skills/<skill-name>/`>
|
|
208
|
+
|
|
209
|
+
## Test plan
|
|
210
|
+
|
|
211
|
+
- [ ] Run audit-skill / `validate-skills.mts` against the updated skill(s)
|
|
212
|
+
- [ ] Confirm no files outside `skills/` were modified
|
|
213
|
+
|
|
214
|
+
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
|
215
|
+
EOF
|
|
216
|
+
)"
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### 7. Report
|
|
220
|
+
|
|
221
|
+
Output the PR URL. After merge, run `npx skills update` in the consumer repo to refresh `skills-lock.json` hashes.
|
|
222
|
+
|
|
223
|
+
## What NOT to do
|
|
224
|
+
|
|
225
|
+
- Do not include `SKILL.local.md` content in the PR
|
|
226
|
+
- Do not push without showing diffs and getting user confirmation
|
|
227
|
+
- Do not create a PR if every mapped file is identical to upstream
|
|
228
|
+
- Do not update `.agents/skills/` or any path outside `skills/<skill-name>/` in the source repo
|
|
229
|
+
- Do not use Contents API `PUT` per file (multi-commit noise); use Git Data API (section 5)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: skillify
|
|
3
|
+
description: Use this skill when generalizing a workflow from the current session into a reusable SKILL.md.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Skillify
|
|
7
|
+
|
|
8
|
+
Extracts a repeatable workflow from what was done in the current session and creates a reusable agent skill from it. Different from `create-skill`, which scaffolds from a blank template — this skill analyzes what actually happened and generalizes it.
|
|
9
|
+
|
|
10
|
+
## When to use
|
|
11
|
+
|
|
12
|
+
- The user says "skillify this", "make this reusable", "turn what we just did into a skill", or similar
|
|
13
|
+
- A multi-step workflow was completed manually and is worth encoding for future use
|
|
14
|
+
- You want to capture decisions made in a session so an agent can repeat them without re-deriving
|
|
15
|
+
|
|
16
|
+
## Steps
|
|
17
|
+
|
|
18
|
+
### 1. Identify the workflow to generalize
|
|
19
|
+
|
|
20
|
+
From the session history, extract:
|
|
21
|
+
- **Trigger:** What situation prompted this work? What would cause someone to want to do this again?
|
|
22
|
+
- **Decisions:** What choices were made and why? (These are the core of the skill)
|
|
23
|
+
- **Steps:** What was done, in what order?
|
|
24
|
+
- **Inputs:** What did the workflow need to know upfront?
|
|
25
|
+
- **Outputs:** What did it produce or change?
|
|
26
|
+
|
|
27
|
+
Separate decisions from documentation. The skill should encode what to decide and how — not reference material the model already knows.
|
|
28
|
+
|
|
29
|
+
### 2. Determine skill kind
|
|
30
|
+
|
|
31
|
+
| Signal | Kind |
|
|
32
|
+
|---|---|
|
|
33
|
+
| Workflow is personal, not tied to a specific codebase | Global (`~/.agents/skills/<name>/`) |
|
|
34
|
+
| Workflow only makes sense for contributors to this repo | Repo internal (`.agents/skills/<name>/`) |
|
|
35
|
+
| Workflow is meant to be installed by users of this package | Repo public (`skills/<name>/`) |
|
|
36
|
+
|
|
37
|
+
Ask the user if the context is ambiguous.
|
|
38
|
+
|
|
39
|
+
### 3. Draft the skill name and description
|
|
40
|
+
|
|
41
|
+
- **Name:** A short verb-noun or noun phrase that identifies the workflow (e.g., `patch-skill`, `deploy-preview`, `sync-tokens`)
|
|
42
|
+
- **Description:** ≤120 characters; must contain "Use this skill when"; specific enough to discriminate from other skills
|
|
43
|
+
|
|
44
|
+
Test the description: would an agent activate this skill in the right situation and NOT activate it otherwise?
|
|
45
|
+
|
|
46
|
+
### 4. Write SKILL.md
|
|
47
|
+
|
|
48
|
+
Use this structure:
|
|
49
|
+
|
|
50
|
+
```markdown
|
|
51
|
+
---
|
|
52
|
+
name: <name>
|
|
53
|
+
description: Use this skill when <trigger>. <One-line summary.>
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
# <Title>
|
|
57
|
+
|
|
58
|
+
## When to use
|
|
59
|
+
|
|
60
|
+
<Precise trigger conditions>
|
|
61
|
+
|
|
62
|
+
## Steps
|
|
63
|
+
|
|
64
|
+
### 1. <Step title>
|
|
65
|
+
<Decision logic, not documentation>
|
|
66
|
+
|
|
67
|
+
### 2. <Step title>
|
|
68
|
+
...
|
|
69
|
+
|
|
70
|
+
## What NOT to do
|
|
71
|
+
|
|
72
|
+
- <Common mistake or anti-pattern>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Rules:
|
|
76
|
+
- Encode the WHY behind each step (the constraint or decision), not just the WHAT
|
|
77
|
+
- Flag deterministic steps as candidates for script extraction (see below)
|
|
78
|
+
- Keep each step focused on one decision or action
|
|
79
|
+
|
|
80
|
+
### 5. Flag script-extraction candidates
|
|
81
|
+
|
|
82
|
+
Mark any step that:
|
|
83
|
+
- Produces the same output given the same input (no judgment needed)
|
|
84
|
+
- Involves text manipulation, file I/O, or structured data processing
|
|
85
|
+
|
|
86
|
+
Add a TODO comment in the skill body:
|
|
87
|
+
```
|
|
88
|
+
<!-- TODO: extract to scripts/<step>.mts -->
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### 6. Validate
|
|
92
|
+
|
|
93
|
+
Invoke `audit-skill` on the draft. Fix any CRITICAL findings before continuing.
|
|
94
|
+
|
|
95
|
+
Key checks to watch for:
|
|
96
|
+
- **Q1:** Does the description contain "Use this skill when"?
|
|
97
|
+
- **Q5:** Is the description ≤120 characters?
|
|
98
|
+
- **Q6:** Are there baked-in stack assumptions that should be detected at runtime?
|
|
99
|
+
|
|
100
|
+
### 7. Place and link
|
|
101
|
+
|
|
102
|
+
Use `create-skill` conventions:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
# If npx skills is available:
|
|
106
|
+
npx skills add <path-to-skill>
|
|
107
|
+
|
|
108
|
+
# Manual fallback:
|
|
109
|
+
ln -sf <path-to-skill> ~/.claude/skills/<name>
|
|
110
|
+
```
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: update-awesome-list
|
|
3
|
+
description: Use this skill when adding or updating a curated awesome-list entry, including summaries and README sync.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Update Awesome List
|
|
7
|
+
|
|
8
|
+
Add or update entries in the current repo's `awesome-skills.json`, then regenerate the README awesome-list section.
|
|
9
|
+
|
|
10
|
+
## Entry target
|
|
11
|
+
|
|
12
|
+
When the user gives a repo, do not assume the right recommendation unit. Inspect first, then ask whether they want:
|
|
13
|
+
|
|
14
|
+
- the whole repo as an entry
|
|
15
|
+
- one or more specific skills as entries
|
|
16
|
+
- a repo entry with highlighted skills
|
|
17
|
+
|
|
18
|
+
If the repo has many public skills, tell the user the exact count when available. If the count is greater than 12, tell the user it appears to be a broad-catalog repo and recommend adding specific skills, while still allowing:
|
|
19
|
+
|
|
20
|
+
- repo only
|
|
21
|
+
- repo with highlights
|
|
22
|
+
- specific skills only
|
|
23
|
+
|
|
24
|
+
Use natural-language narrowing when the user wants help selecting standout skills or highlights.
|
|
25
|
+
|
|
26
|
+
Inspect the repo with the bundled helper:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npx tsx skills/update-awesome-list/scripts/inspect-skills-repo.mts --repo owner/name
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Narrow by query when needed:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npx tsx skills/update-awesome-list/scripts/inspect-skills-repo.mts --repo owner/name --query "release"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Editing rules
|
|
39
|
+
|
|
40
|
+
1. Edit `awesome-skills.json`.
|
|
41
|
+
2. Store repo recommendations under the top-level `repos` object, keyed by normalized repo id (`owner/name`).
|
|
42
|
+
3. Store skill recommendations under the top-level `skills` object, keyed by canonical skill id (`owner/name::skill-name`).
|
|
43
|
+
4. Keep the embedded `repo` and `skill` values consistent with the object key.
|
|
44
|
+
5. Store a neutral `summary` for what the repo or skill does.
|
|
45
|
+
6. Store a separate `why_recommended` note for why the user or agent recommends it.
|
|
46
|
+
7. Keep tags short and lower-kebab-case.
|
|
47
|
+
8. Repo entries may include typed `highlights` using:
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"type": "skill",
|
|
52
|
+
"key": "audit-skill",
|
|
53
|
+
"summary": "Audit SKILL.md structure, quality, and security.",
|
|
54
|
+
"why_recommended": "Strong review rubric before installing or publishing skills.",
|
|
55
|
+
"tags": ["audit", "security"]
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Finish
|
|
60
|
+
|
|
61
|
+
After editing, regenerate the README bounded section so the human-facing list stays in sync with `awesome-skills.json`:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npx tsx skills/update-awesome-list/scripts/render-awesome-list.mts
|
|
65
|
+
```
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as fs from 'node:fs'
|
|
3
|
+
import * as path from 'node:path'
|
|
4
|
+
|
|
5
|
+
interface SkillSummary {
|
|
6
|
+
directory: string
|
|
7
|
+
name: string
|
|
8
|
+
description: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function normalizeRepo(repo: string): string {
|
|
12
|
+
return repo
|
|
13
|
+
.trim()
|
|
14
|
+
.replace(/^https?:\/\/github\.com\//, '')
|
|
15
|
+
.replace(/\.git$/, '')
|
|
16
|
+
.replace(/^\/+|\/+$/g, '')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function parseRepositoryFromPackage(cwd: string): string | null {
|
|
20
|
+
const manifestPath = path.join(cwd, 'package.json')
|
|
21
|
+
if (!fs.existsSync(manifestPath)) return null
|
|
22
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')) as { repository?: { url?: string } | string }
|
|
23
|
+
const repoUrl = typeof manifest.repository === 'string' ? manifest.repository : manifest.repository?.url
|
|
24
|
+
if (!repoUrl) return null
|
|
25
|
+
const match = repoUrl.match(/github\.com[:/](.+?)(?:\.git)?$/)
|
|
26
|
+
return match ? normalizeRepo(match[1]) : null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function parseFrontmatter(content: string): { name: string; description: string } {
|
|
30
|
+
const lines = content.split('\n')
|
|
31
|
+
let frontmatterCount = 0
|
|
32
|
+
let name = ''
|
|
33
|
+
let description = ''
|
|
34
|
+
for (const line of lines) {
|
|
35
|
+
if (line.trim() === '---') {
|
|
36
|
+
frontmatterCount += 1
|
|
37
|
+
if (frontmatterCount === 2) break
|
|
38
|
+
continue
|
|
39
|
+
}
|
|
40
|
+
if (frontmatterCount !== 1) continue
|
|
41
|
+
const nameMatch = line.match(/^name:\s*(.+)$/)
|
|
42
|
+
if (nameMatch) name = nameMatch[1].trim().replace(/^["']|["']$/g, '')
|
|
43
|
+
const descriptionMatch = line.match(/^description:\s*(.+)$/)
|
|
44
|
+
if (descriptionMatch) description = descriptionMatch[1].trim().replace(/^["']|["']$/g, '')
|
|
45
|
+
}
|
|
46
|
+
return { name, description }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function fetchRepoSkills(repo: string): Promise<SkillSummary[]> {
|
|
50
|
+
const response = await fetch(`https://api.github.com/repos/${repo}/contents/skills`, {
|
|
51
|
+
headers: {
|
|
52
|
+
Accept: 'application/vnd.github+json',
|
|
53
|
+
'User-Agent': 'cyber-skills-awesome-skills',
|
|
54
|
+
},
|
|
55
|
+
})
|
|
56
|
+
if (!response.ok) throw new Error(`Failed to inspect skills/ in ${repo}: ${response.status} ${response.statusText}`)
|
|
57
|
+
const entries = (await response.json()) as Array<{ type: string; name: string }>
|
|
58
|
+
const results: SkillSummary[] = []
|
|
59
|
+
for (const directory of entries
|
|
60
|
+
.filter((entry) => entry.type === 'dir')
|
|
61
|
+
.map((entry) => entry.name)
|
|
62
|
+
.sort()) {
|
|
63
|
+
const skillResponse = await fetch(`https://api.github.com/repos/${repo}/contents/skills/${directory}/SKILL.md`, {
|
|
64
|
+
headers: {
|
|
65
|
+
Accept: 'application/vnd.github+json',
|
|
66
|
+
'User-Agent': 'cyber-skills-awesome-skills',
|
|
67
|
+
},
|
|
68
|
+
})
|
|
69
|
+
if (!skillResponse.ok) continue
|
|
70
|
+
const body = (await skillResponse.json()) as { content?: string; encoding?: string }
|
|
71
|
+
if (!body.content || body.encoding !== 'base64') continue
|
|
72
|
+
const fm = parseFrontmatter(Buffer.from(body.content, 'base64').toString('utf8'))
|
|
73
|
+
results.push({ directory, ...fm })
|
|
74
|
+
}
|
|
75
|
+
return results
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function readLocalRepoSkills(cwd: string): SkillSummary[] {
|
|
79
|
+
const skillsDir = path.join(cwd, 'skills')
|
|
80
|
+
if (!fs.existsSync(skillsDir)) return []
|
|
81
|
+
return fs
|
|
82
|
+
.readdirSync(skillsDir, { withFileTypes: true })
|
|
83
|
+
.filter((entry) => entry.isDirectory())
|
|
84
|
+
.map((entry) => {
|
|
85
|
+
const skillMd = path.join(skillsDir, entry.name, 'SKILL.md')
|
|
86
|
+
const content = fs.existsSync(skillMd) ? fs.readFileSync(skillMd, 'utf8') : ''
|
|
87
|
+
const fm = parseFrontmatter(content)
|
|
88
|
+
return { directory: entry.name, ...fm }
|
|
89
|
+
})
|
|
90
|
+
.filter((item) => item.name && item.description)
|
|
91
|
+
.sort((a, b) => a.directory.localeCompare(b.directory))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const args = process.argv.slice(2)
|
|
95
|
+
const repoIndex = args.indexOf('--repo')
|
|
96
|
+
const queryIndex = args.indexOf('--query')
|
|
97
|
+
const repo = normalizeRepo(repoIndex !== -1 ? args[repoIndex + 1] : '')
|
|
98
|
+
const query = (queryIndex !== -1 ? args[queryIndex + 1] : '').toLowerCase()
|
|
99
|
+
if (!repo) {
|
|
100
|
+
console.error(
|
|
101
|
+
'Usage: tsx skills/update-awesome-list/scripts/inspect-skills-repo.mts --repo owner/name [--query term]',
|
|
102
|
+
)
|
|
103
|
+
process.exit(1)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const currentRepo = parseRepositoryFromPackage(process.cwd())
|
|
107
|
+
const skills = repo === currentRepo ? readLocalRepoSkills(process.cwd()) : await fetchRepoSkills(repo)
|
|
108
|
+
const filtered = query
|
|
109
|
+
? skills.filter((skill) => `${skill.directory} ${skill.name} ${skill.description}`.toLowerCase().includes(query))
|
|
110
|
+
: skills
|
|
111
|
+
console.log(`${repo} has ${skills.length} public skill(s).`)
|
|
112
|
+
for (const skill of filtered) console.log(`- ${skill.directory}: ${skill.description}`)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as fs from 'node:fs'
|
|
3
|
+
import * as path from 'node:path'
|
|
4
|
+
import { pathToFileURL } from 'node:url'
|
|
5
|
+
import { flattenAwesomeEntries, validateAwesomeList } from '../../find-awesome-skill/scripts/awesome-lib.mts'
|
|
6
|
+
|
|
7
|
+
interface Highlight {
|
|
8
|
+
type: string
|
|
9
|
+
key: string
|
|
10
|
+
summary: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface RepoEntry {
|
|
14
|
+
type: 'repo'
|
|
15
|
+
repo: string
|
|
16
|
+
kind: string
|
|
17
|
+
trust: 'authored' | 'recommended'
|
|
18
|
+
summary: string
|
|
19
|
+
why_recommended: string
|
|
20
|
+
tags: string[]
|
|
21
|
+
highlights?: Highlight[]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface SkillEntry {
|
|
25
|
+
type: 'skill'
|
|
26
|
+
repo: string
|
|
27
|
+
skill: string
|
|
28
|
+
kind: string
|
|
29
|
+
trust: 'authored' | 'recommended'
|
|
30
|
+
summary: string
|
|
31
|
+
why_recommended: string
|
|
32
|
+
tags: string[]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type Entry = RepoEntry | SkillEntry
|
|
36
|
+
|
|
37
|
+
function deriveInstallCommand(entry: Entry): string {
|
|
38
|
+
return entry.type === 'repo' ? `npx skills add ${entry.repo}` : `npx skills add ${entry.repo} --skill ${entry.skill}`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function renderAwesomeListMarkdown(entries: Entry[]): string {
|
|
42
|
+
const sections: string[] = ['## Awesome Skills', '']
|
|
43
|
+
for (const trust of ['authored', 'recommended'] as const) {
|
|
44
|
+
const grouped = entries
|
|
45
|
+
.filter((entry) => entry.trust === trust)
|
|
46
|
+
.sort(
|
|
47
|
+
(a, b) =>
|
|
48
|
+
a.repo.localeCompare(b.repo) ||
|
|
49
|
+
(a.type === 'skill' ? a.skill : '').localeCompare(b.type === 'skill' ? b.skill : ''),
|
|
50
|
+
)
|
|
51
|
+
if (grouped.length === 0) continue
|
|
52
|
+
sections.push(`### ${trust === 'authored' ? 'Authored' : 'Recommended'}`)
|
|
53
|
+
sections.push('')
|
|
54
|
+
for (const entry of grouped) {
|
|
55
|
+
const title = entry.type === 'repo' ? `\`${entry.repo}\`` : `\`${entry.repo}#${entry.skill}\``
|
|
56
|
+
sections.push(`- ${title} — ${entry.kind}`)
|
|
57
|
+
sections.push(` ${entry.summary}`)
|
|
58
|
+
sections.push(` Why recommended: ${entry.why_recommended}`)
|
|
59
|
+
if (entry.tags.length > 0) sections.push(` Tags: ${entry.tags.map((tag) => `\`${tag}\``).join(', ')}`)
|
|
60
|
+
sections.push(` Install: \`${deriveInstallCommand(entry)}\``)
|
|
61
|
+
if (entry.type === 'repo' && entry.highlights && entry.highlights.length > 0) {
|
|
62
|
+
sections.push(' Highlights:')
|
|
63
|
+
for (const highlight of entry.highlights) {
|
|
64
|
+
sections.push(` - \`${highlight.type}:${highlight.key}\` — ${highlight.summary}`)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
sections.push('')
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return sections.join('\n').trimEnd()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function updateMarkedSection(content: string, markerName: string, replacement: string): string {
|
|
74
|
+
const start = `<!-- ${markerName}:START -->`
|
|
75
|
+
const end = `<!-- ${markerName}:END -->`
|
|
76
|
+
if (!content.includes(start) || !content.includes(end))
|
|
77
|
+
throw new Error(`Missing ${markerName} markers in target file`)
|
|
78
|
+
return content.replace(new RegExp(`${start}[\\s\\S]*?${end}`, 'm'), `${start}\n${replacement}\n${end}`)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
82
|
+
const cwd = process.cwd()
|
|
83
|
+
const awesomePath = path.join(cwd, 'awesome-skills.json')
|
|
84
|
+
const readmePath = path.join(cwd, 'readme.md')
|
|
85
|
+
const awesome = validateAwesomeList(JSON.parse(fs.readFileSync(awesomePath, 'utf8')), awesomePath)
|
|
86
|
+
const markdown = renderAwesomeListMarkdown(flattenAwesomeEntries(awesome))
|
|
87
|
+
const updated = updateMarkedSection(fs.readFileSync(readmePath, 'utf8'), 'AWESOME-SKILLS', markdown)
|
|
88
|
+
|
|
89
|
+
fs.writeFileSync(readmePath, updated)
|
|
90
|
+
console.log(`Updated awesome list section in ${readmePath}`)
|
|
91
|
+
}
|