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,102 @@
|
|
|
1
|
+
#!/usr/bin/env node --experimental-strip-types
|
|
2
|
+
import * as fs from 'node:fs'
|
|
3
|
+
import * as path from 'node:path'
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
5
|
+
|
|
6
|
+
const packageRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
7
|
+
|
|
8
|
+
function usage() {
|
|
9
|
+
process.stderr.write(`Usage: cyber-skills <command> [args]
|
|
10
|
+
|
|
11
|
+
Commands:
|
|
12
|
+
run-hook <name> Run a runtime hook by name
|
|
13
|
+
register-hooks --set <set> Register agent hooks (init | commit-discipline)
|
|
14
|
+
inject-commit-discipline Merge ## Commit Discipline into AGENTS.md
|
|
15
|
+
|
|
16
|
+
Options:
|
|
17
|
+
--root <path> Repo root (default: cwd)
|
|
18
|
+
--dry-run Preview without writing
|
|
19
|
+
--verbose Human-readable status on stderr
|
|
20
|
+
--commit-skill <name> Commit helper skill name (inject-commit-discipline)
|
|
21
|
+
`)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const [, , command, ...args] = process.argv
|
|
25
|
+
|
|
26
|
+
if (!command) {
|
|
27
|
+
usage()
|
|
28
|
+
process.exit(1)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function parseCommonArgs(argv: string[]) {
|
|
32
|
+
return {
|
|
33
|
+
dryRun: argv.includes('--dry-run'),
|
|
34
|
+
verbose: argv.includes('--verbose'),
|
|
35
|
+
root: (() => {
|
|
36
|
+
const i = argv.indexOf('--root')
|
|
37
|
+
return i !== -1 ? argv[i + 1]! : process.cwd()
|
|
38
|
+
})(),
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (command === 'run-hook') {
|
|
43
|
+
const hookName = args[0]
|
|
44
|
+
if (!hookName) {
|
|
45
|
+
process.stderr.write('Usage: cyber-skills run-hook <name>\n')
|
|
46
|
+
process.exit(1)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const hookFile = path.join(packageRoot, 'hooks', 'runtime', `${hookName}.mts`)
|
|
50
|
+
if (!fs.existsSync(hookFile)) {
|
|
51
|
+
process.stderr.write(`Hook not found: ${hookName}\n`)
|
|
52
|
+
process.exit(1)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const { default: run } = await import(pathToFileURL(hookFile).href)
|
|
56
|
+
await run()
|
|
57
|
+
} else if (command === 'register-hooks') {
|
|
58
|
+
const { dryRun, verbose, root } = parseCommonArgs(args)
|
|
59
|
+
const setIdx = args.indexOf('--set')
|
|
60
|
+
const set = setIdx !== -1 ? args[setIdx + 1] : undefined
|
|
61
|
+
|
|
62
|
+
if (!set || (set !== 'init' && set !== 'commit-discipline')) {
|
|
63
|
+
process.stderr.write('Usage: cyber-skills register-hooks --set init|commit-discipline [--root <path>]\n')
|
|
64
|
+
process.exit(1)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const { registerHooksForSet } = await import(
|
|
68
|
+
pathToFileURL(path.join(packageRoot, 'hooks', 'register-agent-hooks.mts')).href
|
|
69
|
+
)
|
|
70
|
+
const results = registerHooksForSet(set, { root, dryRun })
|
|
71
|
+
|
|
72
|
+
if (verbose) {
|
|
73
|
+
if (dryRun) process.stderr.write('Dry run — no files written.\n\n')
|
|
74
|
+
for (const r of results) {
|
|
75
|
+
process.stderr.write(`${r.agent} | ${r.hook} | ${r.status}\n`)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
} else if (command === 'inject-commit-discipline') {
|
|
79
|
+
const { dryRun, verbose, root } = parseCommonArgs(args)
|
|
80
|
+
const skillIdx = args.indexOf('--commit-skill')
|
|
81
|
+
const commitSkill = skillIdx !== -1 ? args[skillIdx + 1] : undefined
|
|
82
|
+
|
|
83
|
+
if (!commitSkill) {
|
|
84
|
+
process.stderr.write('Usage: cyber-skills inject-commit-discipline --commit-skill <name>\n')
|
|
85
|
+
process.exit(1)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const { injectCommitDiscipline } = await import(
|
|
89
|
+
pathToFileURL(path.join(packageRoot, 'hooks', 'inject-commit-discipline.mts')).href
|
|
90
|
+
)
|
|
91
|
+
try {
|
|
92
|
+
const result = injectCommitDiscipline({ root, commitSkill, dryRun, verbose })
|
|
93
|
+
process.stdout.write(`${JSON.stringify(result)}\n`)
|
|
94
|
+
} catch (err) {
|
|
95
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`)
|
|
96
|
+
process.exit(1)
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
process.stderr.write(`Unknown command: ${command}\n`)
|
|
100
|
+
usage()
|
|
101
|
+
process.exit(1)
|
|
102
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { hookCommand } from '../lib/hook-command.mts'
|
|
2
|
+
import type { HookDefinition } from '../register-agent-hooks.mts'
|
|
3
|
+
|
|
4
|
+
export function getCommitDisciplineHooks(root = process.cwd()): HookDefinition[] {
|
|
5
|
+
const command = hookCommand('run-hook commit-discipline', root)
|
|
6
|
+
return [
|
|
7
|
+
{
|
|
8
|
+
id: 'commit-discipline',
|
|
9
|
+
label: 'SessionStart › commit-discipline',
|
|
10
|
+
command,
|
|
11
|
+
claude: { event: 'SessionStart' },
|
|
12
|
+
codex: { event: 'SessionStart' },
|
|
13
|
+
},
|
|
14
|
+
]
|
|
15
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { HookDefinition } from '../register-agent-hooks.mts'
|
|
2
|
+
|
|
3
|
+
const MARK_INTERNAL = 'bash .agents/hooks/mark-internal.sh'
|
|
4
|
+
const INJECT_AUGMENTATIONS = 'bash .agents/hooks/inject-local-augmentations.sh'
|
|
5
|
+
|
|
6
|
+
export const INIT_HOOKS: HookDefinition[] = [
|
|
7
|
+
{
|
|
8
|
+
id: 'mark-internal',
|
|
9
|
+
label: 'PostToolUse › mark-internal',
|
|
10
|
+
command: MARK_INTERNAL,
|
|
11
|
+
claude: { event: 'PostToolUse', matcher: 'Write|Edit' },
|
|
12
|
+
cursor: { event: 'afterFileEdit' },
|
|
13
|
+
codex: { event: 'PostToolUse', matcher: 'Write|Edit' },
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
id: 'inject-local-augmentations',
|
|
17
|
+
label: 'SessionStart › inject-local-augmentations',
|
|
18
|
+
command: INJECT_AUGMENTATIONS,
|
|
19
|
+
claude: { event: 'SessionStart' },
|
|
20
|
+
codex: { event: 'SessionStart' },
|
|
21
|
+
},
|
|
22
|
+
]
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Inject or update the ## Commit Discipline section in AGENTS.md.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
7
|
+
import { join } from 'node:path'
|
|
8
|
+
|
|
9
|
+
import { mergeCommitDisciplineIntoAgentsMd } from './lib/commit-discipline-content.mts'
|
|
10
|
+
|
|
11
|
+
export interface InjectOptions {
|
|
12
|
+
root?: string
|
|
13
|
+
commitSkill: string
|
|
14
|
+
dryRun?: boolean
|
|
15
|
+
verbose?: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function injectCommitDiscipline(options: InjectOptions): { path: string; changed: boolean } {
|
|
19
|
+
const root = options.root ?? process.cwd()
|
|
20
|
+
const agentsPath = join(root, 'AGENTS.md')
|
|
21
|
+
|
|
22
|
+
if (!existsSync(agentsPath)) {
|
|
23
|
+
throw new Error('AGENTS.md not found — run the init skill first or create AGENTS.md')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const before = readFileSync(agentsPath, 'utf8')
|
|
27
|
+
const after = mergeCommitDisciplineIntoAgentsMd(before, options.commitSkill)
|
|
28
|
+
const changed = before !== after
|
|
29
|
+
|
|
30
|
+
if (changed && !options.dryRun) {
|
|
31
|
+
writeFileSync(agentsPath, after)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (options.verbose) {
|
|
35
|
+
process.stderr.write(
|
|
36
|
+
changed ? 'Updated ## Commit Discipline in AGENTS.md\n' : '## Commit Discipline already up to date\n',
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return { path: agentsPath, changed }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (process.argv[1] === import.meta.filename) {
|
|
44
|
+
const args = process.argv.slice(2)
|
|
45
|
+
const dryRun = args.includes('--dry-run')
|
|
46
|
+
const verbose = args.includes('--verbose')
|
|
47
|
+
const skillIdx = args.indexOf('--commit-skill')
|
|
48
|
+
const rootIdx = args.indexOf('--root')
|
|
49
|
+
const commitSkill = skillIdx !== -1 ? args[skillIdx + 1] : undefined
|
|
50
|
+
const root = rootIdx !== -1 ? args[rootIdx + 1] : process.cwd()
|
|
51
|
+
|
|
52
|
+
if (!commitSkill) {
|
|
53
|
+
process.stderr.write(
|
|
54
|
+
'Usage: inject-commit-discipline.mts --commit-skill <name> [--root <path>] [--dry-run] [--verbose]\n',
|
|
55
|
+
)
|
|
56
|
+
process.exit(1)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const result = injectCommitDiscipline({ root, commitSkill, dryRun, verbose })
|
|
61
|
+
process.stdout.write(`${JSON.stringify(result)}\n`)
|
|
62
|
+
} catch (err) {
|
|
63
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`)
|
|
64
|
+
process.exit(1)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export const COMMIT_DISCIPLINE_HEADING = '## Commit Discipline'
|
|
2
|
+
|
|
3
|
+
export function formatCommitDisciplineSection(commitSkill: string): string {
|
|
4
|
+
return `${COMMIT_DISCIPLINE_HEADING}
|
|
5
|
+
|
|
6
|
+
Commit every self-contained unit of work — code, config, skills — as its own commit before moving on.
|
|
7
|
+
|
|
8
|
+
- Conventional Commits: \`feat:\`, \`fix:\`, \`refactor:\`, \`test:\`, \`docs:\`, \`chore:\`
|
|
9
|
+
- One concern per commit; never batch unrelated changes
|
|
10
|
+
- Use \`git add -p\` for mixed changes in one file
|
|
11
|
+
- Never commit with red tests; run validation commands first
|
|
12
|
+
- Use the \`${commitSkill}\` skill when committing (staging, splitting, message writing)
|
|
13
|
+
`
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Extract body of ## Commit Discipline from AGENTS.md, or null if missing. */
|
|
17
|
+
export function parseCommitDisciplineSection(agentsMd: string): string | null {
|
|
18
|
+
const heading = COMMIT_DISCIPLINE_HEADING
|
|
19
|
+
const start = agentsMd.indexOf(heading)
|
|
20
|
+
if (start === -1) return null
|
|
21
|
+
|
|
22
|
+
const afterHeading = agentsMd.slice(start + heading.length)
|
|
23
|
+
const nextHeading = afterHeading.search(/\n## /)
|
|
24
|
+
const body = nextHeading === -1 ? afterHeading : afterHeading.slice(0, nextHeading)
|
|
25
|
+
const trimmed = body.trim()
|
|
26
|
+
return trimmed.length > 0 ? `${heading}\n\n${trimmed}\n` : `${heading}\n`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Replace or append the Commit Discipline section in AGENTS.md. */
|
|
30
|
+
export function mergeCommitDisciplineIntoAgentsMd(agentsMd: string, commitSkill: string): string {
|
|
31
|
+
const section = formatCommitDisciplineSection(commitSkill)
|
|
32
|
+
const existing = parseCommitDisciplineSection(agentsMd)
|
|
33
|
+
|
|
34
|
+
if (existing !== null) {
|
|
35
|
+
const heading = COMMIT_DISCIPLINE_HEADING
|
|
36
|
+
const start = agentsMd.indexOf(heading)
|
|
37
|
+
const afterHeading = agentsMd.slice(start + heading.length)
|
|
38
|
+
const nextHeading = afterHeading.search(/\n## /)
|
|
39
|
+
const end = nextHeading === -1 ? agentsMd.length : start + heading.length + nextHeading
|
|
40
|
+
return agentsMd.slice(0, start) + section.trimEnd() + '\n' + agentsMd.slice(end).replace(/^\n*/, '\n')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const skillAugHeading = '## Skill Augmentations'
|
|
44
|
+
const augIdx = agentsMd.indexOf(skillAugHeading)
|
|
45
|
+
if (augIdx !== -1) {
|
|
46
|
+
const afterAug = agentsMd.slice(augIdx)
|
|
47
|
+
const nextHeading = afterAug.search(/\n## /)
|
|
48
|
+
const insertAt = nextHeading === -1 ? agentsMd.length : augIdx + nextHeading
|
|
49
|
+
return `${agentsMd.slice(0, insertAt).trimEnd()}\n\n${section.trimEnd()}\n${agentsMd.slice(insertAt)}`
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return `${agentsMd.trimEnd()}\n\n${section.trimEnd()}\n`
|
|
53
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { getPackageRoot } from './package-root.mts'
|
|
5
|
+
|
|
6
|
+
function getPackageVersion(): string {
|
|
7
|
+
const pkgPath = join(getPackageRoot(), 'package.json')
|
|
8
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as { version: string }
|
|
9
|
+
return pkg.version
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Build a hook command string for registration in agent settings.
|
|
14
|
+
* Prefers local node_modules bin when present (offline/local-agent setups);
|
|
15
|
+
* otherwise pins npx to the package version that ran register-hooks.
|
|
16
|
+
*/
|
|
17
|
+
export function hookCommand(subcommand: string, root = process.cwd()): string {
|
|
18
|
+
const localBin = join(root, 'node_modules', '.bin', 'cyber-skills')
|
|
19
|
+
if (existsSync(localBin)) {
|
|
20
|
+
return `${localBin} ${subcommand}`
|
|
21
|
+
}
|
|
22
|
+
const version = getPackageVersion()
|
|
23
|
+
return `npx cyber-skills@${version} ${subcommand}`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** True when an existing registered command refers to the same hook id. */
|
|
27
|
+
export function commandMatchesHook(existing: string, hookId: string, expectedCommand: string): boolean {
|
|
28
|
+
if (existing === expectedCommand) return true
|
|
29
|
+
if (hookId === 'commit-discipline' && existing.includes('run-hook commit-discipline')) return true
|
|
30
|
+
return false
|
|
31
|
+
}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Registers agent runtime hooks for detected AI agents (Claude Code, Cursor, Codex).
|
|
4
|
+
* Idempotent: safe to re-run; only writes when a hook is missing.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
8
|
+
import { join } from 'node:path'
|
|
9
|
+
|
|
10
|
+
import { getCommitDisciplineHooks } from './definitions/commit-discipline.mts'
|
|
11
|
+
import { INIT_HOOKS } from './definitions/init.mts'
|
|
12
|
+
import { commandMatchesHook } from './lib/hook-command.mts'
|
|
13
|
+
|
|
14
|
+
interface ClaudeHookEntry {
|
|
15
|
+
type: string
|
|
16
|
+
command: string
|
|
17
|
+
}
|
|
18
|
+
interface ClaudeHookGroup {
|
|
19
|
+
matcher?: string
|
|
20
|
+
hooks: ClaudeHookEntry[]
|
|
21
|
+
}
|
|
22
|
+
interface ClaudeSettings {
|
|
23
|
+
hooks?: {
|
|
24
|
+
PostToolUse?: ClaudeHookGroup[]
|
|
25
|
+
SessionStart?: ClaudeHookGroup[]
|
|
26
|
+
[key: string]: ClaudeHookGroup[] | undefined
|
|
27
|
+
}
|
|
28
|
+
[key: string]: unknown
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface CursorHookEntry {
|
|
32
|
+
command: string
|
|
33
|
+
}
|
|
34
|
+
interface CursorSettings {
|
|
35
|
+
version?: number
|
|
36
|
+
hooks?: {
|
|
37
|
+
afterFileEdit?: CursorHookEntry[]
|
|
38
|
+
sessionStart?: CursorHookEntry[]
|
|
39
|
+
[key: string]: CursorHookEntry[] | undefined
|
|
40
|
+
}
|
|
41
|
+
[key: string]: unknown
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type HookStatus = 'registered' | 'already present' | 'skipped (dir not found)'
|
|
45
|
+
|
|
46
|
+
export interface HookResult {
|
|
47
|
+
agent: string
|
|
48
|
+
hook: string
|
|
49
|
+
status: HookStatus
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface RegisterOptions {
|
|
53
|
+
root?: string
|
|
54
|
+
dryRun?: boolean
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type HookDefinition = {
|
|
58
|
+
id: string
|
|
59
|
+
label: string
|
|
60
|
+
command: string
|
|
61
|
+
claude?: { event: 'PostToolUse' | 'SessionStart'; matcher?: string }
|
|
62
|
+
cursor?: { event: 'afterFileEdit' | 'sessionStart' }
|
|
63
|
+
codex?: { event: 'PostToolUse' | 'SessionStart'; matcher?: string }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export type HookSet = 'init' | 'commit-discipline'
|
|
67
|
+
|
|
68
|
+
function readJson<T>(path: string): T {
|
|
69
|
+
return JSON.parse(readFileSync(path, 'utf8')) as T
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function writeJson(path: string, data: unknown) {
|
|
73
|
+
writeFileSync(path, JSON.stringify(data, null, 2) + '\n')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function commandExistsInGroups(groups: ClaudeHookGroup[], def: HookDefinition): boolean {
|
|
77
|
+
return groups.some((g) => g.hooks.some((h) => commandMatchesHook(h.command, def.id, def.command)))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function registerClaudeHook(
|
|
81
|
+
settings: ClaudeSettings,
|
|
82
|
+
def: HookDefinition,
|
|
83
|
+
event: 'PostToolUse' | 'SessionStart',
|
|
84
|
+
): boolean {
|
|
85
|
+
settings.hooks ??= {}
|
|
86
|
+
settings.hooks[event] ??= []
|
|
87
|
+
|
|
88
|
+
if (commandExistsInGroups(settings.hooks[event]!, def)) {
|
|
89
|
+
return false
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (event === 'PostToolUse') {
|
|
93
|
+
const matcher = def.claude?.matcher ?? 'Write|Edit'
|
|
94
|
+
const group = settings.hooks.PostToolUse!.find((g) => g.matcher === matcher)
|
|
95
|
+
if (group) {
|
|
96
|
+
group.hooks.push({ type: 'command', command: def.command })
|
|
97
|
+
} else {
|
|
98
|
+
settings.hooks.PostToolUse!.push({
|
|
99
|
+
matcher,
|
|
100
|
+
hooks: [{ type: 'command', command: def.command }],
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
} else {
|
|
104
|
+
if (settings.hooks.SessionStart!.length > 0) {
|
|
105
|
+
settings.hooks.SessionStart![0]!.hooks.push({ type: 'command', command: def.command })
|
|
106
|
+
} else {
|
|
107
|
+
settings.hooks.SessionStart!.push({ hooks: [{ type: 'command', command: def.command }] })
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return true
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function registerCursorHook(
|
|
114
|
+
settings: CursorSettings,
|
|
115
|
+
def: HookDefinition,
|
|
116
|
+
event: 'afterFileEdit' | 'sessionStart',
|
|
117
|
+
): boolean {
|
|
118
|
+
settings.version ??= 1
|
|
119
|
+
settings.hooks ??= {}
|
|
120
|
+
settings.hooks[event] ??= []
|
|
121
|
+
|
|
122
|
+
const list = settings.hooks[event]!
|
|
123
|
+
if (list.some((h) => commandMatchesHook(h.command, def.id, def.command))) {
|
|
124
|
+
return false
|
|
125
|
+
}
|
|
126
|
+
list.push({ command: def.command })
|
|
127
|
+
return true
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function registerAgentHooks(hooks: HookDefinition[], options: RegisterOptions = {}): HookResult[] {
|
|
131
|
+
const root = options.root ?? process.cwd()
|
|
132
|
+
const dryRun = options.dryRun ?? false
|
|
133
|
+
const results: HookResult[] = []
|
|
134
|
+
|
|
135
|
+
const claudeDir = join(root, '.claude')
|
|
136
|
+
const claudeSettingsPath = join(claudeDir, 'settings.json')
|
|
137
|
+
|
|
138
|
+
if (existsSync(claudeDir)) {
|
|
139
|
+
const settings: ClaudeSettings = existsSync(claudeSettingsPath) ? readJson(claudeSettingsPath) : {}
|
|
140
|
+
let changed = false
|
|
141
|
+
|
|
142
|
+
for (const def of hooks) {
|
|
143
|
+
if (def.claude) {
|
|
144
|
+
const registered = registerClaudeHook(settings, def, def.claude.event)
|
|
145
|
+
if (registered) {
|
|
146
|
+
changed = true
|
|
147
|
+
results.push({ agent: 'Claude Code', hook: def.label, status: 'registered' })
|
|
148
|
+
} else {
|
|
149
|
+
results.push({ agent: 'Claude Code', hook: def.label, status: 'already present' })
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (changed && !dryRun) {
|
|
155
|
+
writeJson(claudeSettingsPath, settings)
|
|
156
|
+
}
|
|
157
|
+
} else {
|
|
158
|
+
const claudeHooks = hooks.filter((h) => h.claude)
|
|
159
|
+
if (claudeHooks.length > 0) {
|
|
160
|
+
results.push({
|
|
161
|
+
agent: 'Claude Code',
|
|
162
|
+
hook: claudeHooks.map((h) => h.label).join(', '),
|
|
163
|
+
status: 'skipped (dir not found)',
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const cursorDir = join(root, '.cursor')
|
|
169
|
+
const cursorHooksPath = join(cursorDir, 'hooks.json')
|
|
170
|
+
|
|
171
|
+
if (existsSync(cursorDir)) {
|
|
172
|
+
const settings: CursorSettings = existsSync(cursorHooksPath) ? readJson(cursorHooksPath) : { version: 1 }
|
|
173
|
+
let changed = false
|
|
174
|
+
|
|
175
|
+
for (const def of hooks) {
|
|
176
|
+
if (def.cursor) {
|
|
177
|
+
const registered = registerCursorHook(settings, def, def.cursor.event)
|
|
178
|
+
if (registered) {
|
|
179
|
+
changed = true
|
|
180
|
+
results.push({ agent: 'Cursor', hook: def.label, status: 'registered' })
|
|
181
|
+
} else {
|
|
182
|
+
results.push({ agent: 'Cursor', hook: def.label, status: 'already present' })
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (changed && !dryRun) {
|
|
188
|
+
writeJson(cursorHooksPath, settings)
|
|
189
|
+
}
|
|
190
|
+
} else {
|
|
191
|
+
const cursorHooks = hooks.filter((h) => h.cursor)
|
|
192
|
+
if (cursorHooks.length > 0) {
|
|
193
|
+
results.push({
|
|
194
|
+
agent: 'Cursor',
|
|
195
|
+
hook: cursorHooks.map((h) => h.label).join(', '),
|
|
196
|
+
status: 'skipped (dir not found)',
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const codexDir = join(root, '.codex-plugin')
|
|
202
|
+
const codexHooksPath = join(codexDir, 'hooks.json')
|
|
203
|
+
|
|
204
|
+
if (existsSync(codexDir)) {
|
|
205
|
+
const settings: ClaudeSettings = existsSync(codexHooksPath) ? readJson(codexHooksPath) : {}
|
|
206
|
+
let changed = false
|
|
207
|
+
|
|
208
|
+
for (const def of hooks) {
|
|
209
|
+
if (def.codex) {
|
|
210
|
+
const registered = registerClaudeHook(settings, def, def.codex.event)
|
|
211
|
+
if (registered) {
|
|
212
|
+
changed = true
|
|
213
|
+
results.push({ agent: 'Codex', hook: def.label, status: 'registered' })
|
|
214
|
+
} else {
|
|
215
|
+
results.push({ agent: 'Codex', hook: def.label, status: 'already present' })
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (changed && !dryRun) {
|
|
221
|
+
writeJson(codexHooksPath, settings)
|
|
222
|
+
}
|
|
223
|
+
} else {
|
|
224
|
+
const codexHooks = hooks.filter((h) => h.codex)
|
|
225
|
+
if (codexHooks.length > 0) {
|
|
226
|
+
results.push({
|
|
227
|
+
agent: 'Codex',
|
|
228
|
+
hook: codexHooks.map((h) => h.label).join(', '),
|
|
229
|
+
status: 'skipped (dir not found)',
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return results
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function hooksForSet(set: HookSet, root?: string): HookDefinition[] {
|
|
238
|
+
switch (set) {
|
|
239
|
+
case 'init':
|
|
240
|
+
return INIT_HOOKS
|
|
241
|
+
case 'commit-discipline':
|
|
242
|
+
return getCommitDisciplineHooks(root)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function registerHooksForSet(set: HookSet, options: RegisterOptions = {}): HookResult[] {
|
|
247
|
+
const root = options.root ?? process.cwd()
|
|
248
|
+
return registerAgentHooks(hooksForSet(set, root), options)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function printResults(results: HookResult[], dryRun: boolean) {
|
|
252
|
+
if (dryRun) process.stderr.write('Dry run — no files written.\n\n')
|
|
253
|
+
|
|
254
|
+
const agentWidth = Math.max(...results.map((r) => r.agent.length), 'Agent'.length)
|
|
255
|
+
const hookWidth = Math.max(...results.map((r) => r.hook.length), 'Hook'.length)
|
|
256
|
+
const statusWidth = Math.max(...results.map((r) => r.status.length), 'Status'.length)
|
|
257
|
+
|
|
258
|
+
function pad(s: string, n: number) {
|
|
259
|
+
return s.padEnd(n)
|
|
260
|
+
}
|
|
261
|
+
function row(a: string, h: string, s: string) {
|
|
262
|
+
return `| ${pad(a, agentWidth)} | ${pad(h, hookWidth)} | ${pad(s, statusWidth)} |`
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
process.stderr.write(`${row('Agent', 'Hook', 'Status')}\n`)
|
|
266
|
+
process.stderr.write(`|-${'-'.repeat(agentWidth)}-|-${'-'.repeat(hookWidth)}-|-${'-'.repeat(statusWidth)}-|\n`)
|
|
267
|
+
for (const r of results) {
|
|
268
|
+
process.stderr.write(`${row(r.agent, r.hook, r.status)}\n`)
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (process.argv[1] === import.meta.filename) {
|
|
273
|
+
const args = process.argv.slice(2)
|
|
274
|
+
const dryRun = args.includes('--dry-run')
|
|
275
|
+
const verbose = args.includes('--verbose')
|
|
276
|
+
const setIdx = args.indexOf('--set')
|
|
277
|
+
const rootIdx = args.indexOf('--root')
|
|
278
|
+
const set = setIdx !== -1 ? (args[setIdx + 1] as HookSet) : undefined
|
|
279
|
+
const root = rootIdx !== -1 ? args[rootIdx + 1]! : process.cwd()
|
|
280
|
+
|
|
281
|
+
if (!set || (set !== 'init' && set !== 'commit-discipline')) {
|
|
282
|
+
process.stderr.write(
|
|
283
|
+
'Usage: register-agent-hooks.mts --set init|commit-discipline [--root <path>] [--dry-run] [--verbose]\n',
|
|
284
|
+
)
|
|
285
|
+
process.exit(1)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const results = registerHooksForSet(set, { root, dryRun })
|
|
289
|
+
if (verbose) printResults(results, dryRun)
|
|
290
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { formatCommitDisciplineSection, parseCommitDisciplineSection } from '../lib/commit-discipline-content.mts'
|
|
5
|
+
|
|
6
|
+
async function readStdin(): Promise<string> {
|
|
7
|
+
const chunks: Buffer[] = []
|
|
8
|
+
for await (const chunk of process.stdin) {
|
|
9
|
+
chunks.push(chunk as Buffer)
|
|
10
|
+
}
|
|
11
|
+
return Buffer.concat(chunks).toString('utf8').trim()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default async function run() {
|
|
15
|
+
const verbose = process.argv.includes('--verbose')
|
|
16
|
+
const input = await readStdin()
|
|
17
|
+
|
|
18
|
+
if (input && verbose) {
|
|
19
|
+
process.stderr.write(`commit-discipline hook: received ${input.length} bytes on stdin\n`)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let context: string
|
|
23
|
+
const agentsPath = join(process.cwd(), 'AGENTS.md')
|
|
24
|
+
try {
|
|
25
|
+
const agentsMd = readFileSync(agentsPath, 'utf8')
|
|
26
|
+
context = parseCommitDisciplineSection(agentsMd) ?? formatCommitDisciplineSection('commit')
|
|
27
|
+
} catch {
|
|
28
|
+
context = formatCommitDisciplineSection('commit')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const payload = {
|
|
32
|
+
hookSpecificOutput: {
|
|
33
|
+
hookEventName: 'SessionStart',
|
|
34
|
+
additionalContext: context.trim(),
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
process.stdout.write(`${JSON.stringify(payload)}\n`)
|
|
39
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cyber-skills",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Opinionated skills, hooks, and workflows for AI agents",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"agent-skill",
|
|
7
|
+
"claude-code",
|
|
8
|
+
"cursor",
|
|
9
|
+
"codex"
|
|
10
|
+
],
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/cyberuni/cyber-skills.git"
|
|
14
|
+
},
|
|
15
|
+
"author": "unional <homawong@gmail.com>",
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"type": "module",
|
|
18
|
+
"bin": {
|
|
19
|
+
"cyber-skills": "./bin/cyber-skills.mts"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"bin",
|
|
23
|
+
"hooks",
|
|
24
|
+
"skills"
|
|
25
|
+
],
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@biomejs/biome": "^2.4.15",
|
|
28
|
+
"@changesets/cli": "^2.29.4",
|
|
29
|
+
"@commitlint/cli": "^21.0.1",
|
|
30
|
+
"@commitlint/config-conventional": "^21.0.1",
|
|
31
|
+
"@repobuddy/biome": "^2.3.1",
|
|
32
|
+
"@types/node": "^24.10.1",
|
|
33
|
+
"husky": "^9.1.7",
|
|
34
|
+
"knip": "^6.14.1",
|
|
35
|
+
"tsx": "^4.22.3",
|
|
36
|
+
"typescript": "^6.0.0",
|
|
37
|
+
"vitest": "^4.1.7"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=22"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"check": "biome check --write .",
|
|
44
|
+
"cs": "changeset",
|
|
45
|
+
"format": "biome format --write .",
|
|
46
|
+
"knip": "knip",
|
|
47
|
+
"lint": "biome check .",
|
|
48
|
+
"release": "changeset publish",
|
|
49
|
+
"render:awesome-list": "tsx skills/update-awesome-list/scripts/render-awesome-list.mts",
|
|
50
|
+
"test": "vitest run",
|
|
51
|
+
"test:audit": "tsx skills/audit-skill/scripts/validate-skills.mts",
|
|
52
|
+
"test:watch": "vitest",
|
|
53
|
+
"typecheck": "tsc --noEmit",
|
|
54
|
+
"verify": "pnpm typecheck && pnpm lint && pnpm test && pnpm test:audit",
|
|
55
|
+
"version": "changeset version"
|
|
56
|
+
}
|
|
57
|
+
}
|