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,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,7 @@
1
+ import { dirname, join } from 'node:path'
2
+ import { fileURLToPath } from 'node:url'
3
+
4
+ /** Package root (parent of hooks/). */
5
+ export function getPackageRoot(): string {
6
+ return join(dirname(fileURLToPath(import.meta.url)), '..', '..')
7
+ }
@@ -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
+ }