cc-hook-registry 2.0.0 → 3.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.
Files changed (2) hide show
  1. package/index.mjs +144 -4
  2. package/package.json +1 -1
package/index.mjs CHANGED
@@ -112,6 +112,7 @@ if (!command || command === '--help' || command === '-h') {
112
112
  browse [category] Browse by category (safety, quality, approve, utility, monitoring, ux)
113
113
  install <id> Install a hook
114
114
  info <id> Show hook details
115
+ recommend Recommend hooks for current project
115
116
  stats Registry statistics
116
117
 
117
118
  Examples:
@@ -192,10 +193,55 @@ else if (command === 'install') {
192
193
  console.log();
193
194
 
194
195
  if (hook.install.startsWith('npx cc-safe-setup')) {
195
- try {
196
- execSync(hook.install, { stdio: 'inherit' });
197
- } catch (e) {
198
- console.log(c.yellow + ' Run the command manually: ' + hook.install + c.reset);
196
+ // Try direct download first (no cc-safe-setup dependency)
197
+ const exampleName = hook.install.match(/--install-example\s+(\S+)/)?.[1];
198
+ if (exampleName) {
199
+ const rawUrl = `https://raw.githubusercontent.com/yurukusa/cc-safe-setup/main/examples/${exampleName}.sh`;
200
+ const hookPath = join(HOME, '.claude', 'hooks', exampleName + '.sh');
201
+ try {
202
+ mkdirSync(join(HOME, '.claude', 'hooks'), { recursive: true });
203
+ const script = execSync(`curl -sL "${rawUrl}"`, { encoding: 'utf-8' });
204
+ if (script.startsWith('#!/bin/bash')) {
205
+ writeFileSync(hookPath, script);
206
+ chmodSync(hookPath, 0o755);
207
+
208
+ // Auto-register in settings.json
209
+ const trigger = script.includes('PreToolUse') ? 'PreToolUse' :
210
+ script.includes('PostToolUse') ? 'PostToolUse' :
211
+ script.includes('Stop') ? 'Stop' : 'PreToolUse';
212
+ const matcher = script.includes('Matcher: "Bash"') || script.includes('MATCHER: "Bash"') ? 'Bash' :
213
+ script.includes('Matcher: "Edit|Write"') ? 'Edit|Write' : '';
214
+
215
+ let settings = {};
216
+ if (existsSync(SETTINGS_PATH)) {
217
+ try { settings = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8')); } catch {}
218
+ }
219
+ if (!settings.hooks) settings.hooks = {};
220
+ if (!settings.hooks[trigger]) settings.hooks[trigger] = [];
221
+
222
+ const existing = settings.hooks[trigger].flatMap(e => (e.hooks || []).map(h => h.command));
223
+ if (!existing.some(cmd => cmd.includes(exampleName))) {
224
+ settings.hooks[trigger].push({
225
+ matcher,
226
+ hooks: [{ type: 'command', command: hookPath }],
227
+ });
228
+ mkdirSync(dirname(SETTINGS_PATH), { recursive: true });
229
+ writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
230
+ }
231
+
232
+ console.log(c.green + ' ✓ Installed: ' + hookPath + c.reset);
233
+ console.log(c.green + ' ✓ Registered in settings.json (' + trigger + ')' + c.reset);
234
+ console.log(c.dim + ' Restart Claude Code to activate.' + c.reset);
235
+ } else {
236
+ throw new Error('Invalid script');
237
+ }
238
+ } catch {
239
+ // Fallback to cc-safe-setup
240
+ console.log(c.dim + ' Direct download failed, using cc-safe-setup...' + c.reset);
241
+ try { execSync(hook.install, { stdio: 'inherit' }); } catch {}
242
+ }
243
+ } else {
244
+ try { execSync(hook.install, { stdio: 'inherit' }); } catch {}
199
245
  }
200
246
  } else {
201
247
  console.log(c.dim + ' This hook requires manual installation. Follow the instructions above.' + c.reset);
@@ -227,6 +273,100 @@ else if (command === 'info') {
227
273
  console.log();
228
274
  }
229
275
 
276
+ else if (command === 'recommend') {
277
+ console.log();
278
+ console.log(c.bold + ' Recommended hooks for this project' + c.reset);
279
+ console.log();
280
+
281
+ const cwd = process.cwd();
282
+ const recommendations = [];
283
+
284
+ // Always recommend safety essentials
285
+ recommendations.push({ id: 'destructive-guard', reason: 'Essential — prevents rm -rf disasters', priority: 1 });
286
+ recommendations.push({ id: 'branch-guard', reason: 'Essential — prevents push to main', priority: 1 });
287
+ recommendations.push({ id: 'secret-guard', reason: 'Essential — prevents .env commits', priority: 1 });
288
+
289
+ // Detect tech stack
290
+ if (existsSync(join(cwd, 'package.json'))) {
291
+ const pkg = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf-8'));
292
+ recommendations.push({ id: 'auto-approve-build', reason: 'Node.js project detected', priority: 2 });
293
+ if (pkg.dependencies?.prisma || pkg.devDependencies?.prisma) {
294
+ recommendations.push({ id: 'block-database-wipe', reason: 'Prisma detected — protect against migrate reset', priority: 1 });
295
+ }
296
+ if (pkg.scripts?.deploy || pkg.scripts?.['vercel-build']) {
297
+ recommendations.push({ id: 'deploy-guard', reason: 'Deploy script detected', priority: 2 });
298
+ }
299
+ }
300
+
301
+ if (existsSync(join(cwd, 'requirements.txt')) || existsSync(join(cwd, 'pyproject.toml'))) {
302
+ recommendations.push({ id: 'auto-approve-python', reason: 'Python project detected', priority: 2 });
303
+ }
304
+
305
+ if (existsSync(join(cwd, 'Dockerfile')) || existsSync(join(cwd, 'docker-compose.yml'))) {
306
+ recommendations.push({ id: 'auto-approve-docker', reason: 'Docker detected', priority: 2 });
307
+ }
308
+
309
+ if (existsSync(join(cwd, '.env')) || existsSync(join(cwd, '.env.local'))) {
310
+ recommendations.push({ id: 'env-source-guard', reason: '.env file present — prevent sourcing', priority: 1 });
311
+ }
312
+
313
+ if (existsSync(join(cwd, 'Gemfile'))) {
314
+ recommendations.push({ id: 'block-database-wipe', reason: 'Rails detected — protect against db:drop', priority: 1 });
315
+ }
316
+
317
+ if (existsSync(join(cwd, 'artisan'))) {
318
+ recommendations.push({ id: 'block-database-wipe', reason: 'Laravel detected — protect against migrate:fresh', priority: 1 });
319
+ }
320
+
321
+ // Always useful
322
+ recommendations.push({ id: 'compound-command-approver', reason: 'Fixes permission matching for cd && commands', priority: 2 });
323
+ recommendations.push({ id: 'loop-detector', reason: 'Prevents infinite command loops', priority: 3 });
324
+ recommendations.push({ id: 'session-handoff', reason: 'Saves state for next session', priority: 3 });
325
+ recommendations.push({ id: 'cost-tracker', reason: 'Track session costs', priority: 3 });
326
+
327
+ // Deduplicate and sort by priority
328
+ const seen = new Set();
329
+ const unique = recommendations.filter(r => { if (seen.has(r.id)) return false; seen.add(r.id); return true; });
330
+ unique.sort((a, b) => a.priority - b.priority);
331
+
332
+ // Check what's already installed
333
+ let installed = new Set();
334
+ if (existsSync(SETTINGS_PATH)) {
335
+ try {
336
+ const s = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8'));
337
+ for (const entries of Object.values(s.hooks || {})) {
338
+ for (const e of entries) {
339
+ for (const h of (e.hooks || [])) {
340
+ if (h.command) installed.add(h.command.split('/').pop().replace('.sh', ''));
341
+ }
342
+ }
343
+ }
344
+ } catch {}
345
+ }
346
+
347
+ for (const rec of unique) {
348
+ const hook = REGISTRY.find(h => h.id === rec.id);
349
+ if (!hook) continue;
350
+ const isInstalled = installed.has(rec.id);
351
+ const icon = isInstalled ? c.green + '✓' + c.reset : c.yellow + '○' + c.reset;
352
+ const status = isInstalled ? c.dim + '(installed)' + c.reset : '';
353
+ console.log(' ' + icon + ' ' + c.bold + rec.id + c.reset + ' ' + status);
354
+ console.log(' ' + c.dim + rec.reason + c.reset);
355
+ if (!isInstalled) {
356
+ console.log(' ' + c.dim + 'Install: npx cc-hook-registry install ' + rec.id + c.reset);
357
+ }
358
+ console.log();
359
+ }
360
+
361
+ const notInstalled = unique.filter(r => !installed.has(r.id));
362
+ if (notInstalled.length === 0) {
363
+ console.log(c.green + ' All recommended hooks are installed!' + c.reset);
364
+ } else {
365
+ console.log(c.dim + ' ' + notInstalled.length + ' recommended hook(s) not yet installed.' + c.reset);
366
+ }
367
+ console.log();
368
+ }
369
+
230
370
  else if (command === 'stats') {
231
371
  const categories = {};
232
372
  const sources = {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-hook-registry",
3
- "version": "2.0.0",
3
+ "version": "3.0.0",
4
4
  "description": "Search, browse, and install Claude Code hooks from the community. GitHub-based registry, no server needed.",
5
5
  "main": "index.mjs",
6
6
  "bin": {