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.
@@ -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
+ }