cc-safe-setup 4.0.3 → 5.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 (3) hide show
  1. package/README.md +1 -0
  2. package/index.mjs +155 -34
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -249,6 +249,7 @@ Or browse all available examples in [`examples/`](examples/):
249
249
  - [Hooks Cookbook](https://github.com/yurukusa/claude-code-hooks/blob/main/COOKBOOK.md) — 25 recipes from real GitHub Issues ([interactive version](https://yurukusa.github.io/claude-code-hooks/))
250
250
  - [Japanese guide (Qiita)](https://qiita.com/yurukusa/items/a9714b33f5d974e8f1e8) — この記事の日本語解説
251
251
  - [Hook Test Runner](https://github.com/yurukusa/cc-hook-test) — `npx cc-hook-test <hook.sh>` to auto-test any hook
252
+ - [Hook Registry](https://github.com/yurukusa/cc-hook-registry) — `npx cc-hook-registry search database` to find community hooks
252
253
  - [Hooks Cheat Sheet](https://yurukusa.github.io/cc-safe-setup/cheatsheet.html) — printable A4 quick reference
253
254
  - [Ecosystem Comparison](https://yurukusa.github.io/cc-safe-setup/ecosystem.html) — all Claude Code hook projects compared
254
255
  - [The incident that inspired this tool](https://github.com/anthropics/claude-code/issues/36339) — NTFS junction rm -rf
package/index.mjs CHANGED
@@ -374,9 +374,18 @@ function examples() {
374
374
  console.log();
375
375
 
376
376
  for (const [cat, hooks] of Object.entries(CATEGORIES)) {
377
- if (filter && !cat.toLowerCase().includes(filter)) continue;
377
+ // Filter by category name OR hook name/description
378
+ const filteredHooks = filter
379
+ ? Object.entries(hooks).filter(([file, desc]) =>
380
+ cat.toLowerCase().includes(filter) ||
381
+ file.toLowerCase().includes(filter) ||
382
+ desc.toLowerCase().includes(filter))
383
+ : Object.entries(hooks);
384
+
385
+ if (filteredHooks.length === 0) continue;
386
+
378
387
  console.log(' ' + c.bold + c.blue + cat + c.reset);
379
- for (const [file, desc] of Object.entries(hooks)) {
388
+ for (const [file, desc] of filteredHooks) {
380
389
  console.log(' ' + c.green + '*' + c.reset + ' ' + c.bold + file + c.reset);
381
390
  console.log(' ' + c.dim + desc + c.reset);
382
391
  }
@@ -797,70 +806,182 @@ async function fullSetup() {
797
806
  }
798
807
 
799
808
  async function dashboard() {
800
- const { createReadStream, watchFile } = await import('fs');
801
- const { createInterface: createRL } = await import('readline');
809
+ const fsModule = await import('fs');
802
810
 
803
811
  const BLOCK_LOG = join(HOME, '.claude', 'blocked-commands.log');
812
+ const ERROR_LOG = join(HOME, '.claude', 'session-errors.log');
804
813
  const COST_FILE = '/tmp/cc-cost-tracker-calls';
805
814
  const CONTEXT_FILE = '/tmp/cc-context-pct';
815
+ const HANDOFF_FILE = join(HOME, '.claude', 'session-handoff.md');
816
+ const W = 60; // dashboard width
806
817
 
807
818
  const clear = () => process.stdout.write('\x1b[2J\x1b[H');
808
819
 
809
- // Count hooks
810
- let hookCount = 0;
811
- let exampleCount = 0;
820
+ // ANSI box drawing helpers
821
+ const box = {
822
+ tl: '┌', tr: '┐', bl: '└', br: '┘',
823
+ h: '─', v: '│', lt: '├', rt: '┤',
824
+ };
825
+
826
+ function hline(left, right, w) { return left + box.h.repeat(w - 2) + right; }
827
+ function pad(text, w) {
828
+ const stripped = text.replace(/\x1b\[[0-9;]*m/g, '');
829
+ const padding = Math.max(0, w - 2 - stripped.length);
830
+ return box.v + ' ' + text + ' '.repeat(padding) + box.v;
831
+ }
832
+ function progressBar(pct, w, filledColor, emptyColor) {
833
+ const barW = w - 2;
834
+ const filled = Math.round(pct / 100 * barW);
835
+ return filledColor + '█'.repeat(filled) + emptyColor + '░'.repeat(barW - filled) + c.reset;
836
+ }
837
+ function sparkline(values, w) {
838
+ const chars = ' ▁▂▃▄▅▆▇';
839
+ const max = Math.max(...values, 1);
840
+ return values.slice(-w).map(v => chars[Math.min(7, Math.round(v / max * 7))]).join('');
841
+ }
842
+
843
+ // Collect hook info
844
+ let hooksByTrigger = {};
845
+ let totalHooks = 0;
846
+ let scriptCount = 0;
812
847
  if (existsSync(SETTINGS_PATH)) {
813
848
  try {
814
849
  const s = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8'));
815
- for (const entries of Object.values(s.hooks || {})) {
816
- hookCount += entries.reduce((n, e) => n + (e.hooks || []).length, 0);
850
+ for (const [trigger, entries] of Object.entries(s.hooks || {})) {
851
+ const count = entries.reduce((n, e) => n + (e.hooks || []).length, 0);
852
+ hooksByTrigger[trigger] = count;
853
+ totalHooks += count;
817
854
  }
818
855
  } catch {}
819
856
  }
820
- exampleCount = existsSync(join(HOOKS_DIR)) ?
821
- (await import('fs')).readdirSync(HOOKS_DIR).filter(f => f.endsWith('.sh')).length : 0;
857
+ if (existsSync(HOOKS_DIR)) {
858
+ scriptCount = fsModule.readdirSync(HOOKS_DIR).filter(f => f.endsWith('.sh')).length;
859
+ }
860
+
861
+ // Audit score (cached)
862
+ let auditScore = '?';
863
+ try {
864
+ // Quick inline audit
865
+ let risks = 0;
866
+ const s = existsSync(SETTINGS_PATH) ? JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8')) : {};
867
+ const pre = s.hooks?.PreToolUse || [];
868
+ const post = s.hooks?.PostToolUse || [];
869
+ if (pre.length === 0) risks += 30;
870
+ const allCmds = JSON.stringify(pre).toLowerCase();
871
+ if (!allCmds.match(/destructive|guard|rm.*rf/)) risks += 20;
872
+ if (!allCmds.match(/branch|push|main/)) risks += 20;
873
+ if (!allCmds.match(/secret|env|credential/)) risks += 20;
874
+ if (post.length === 0) risks += 10;
875
+ auditScore = Math.max(0, 100 - risks);
876
+ } catch {}
822
877
 
823
878
  function render() {
824
879
  clear();
825
880
 
826
- // Header
827
- console.log(c.bold + ' cc-safe-setup --dashboard' + c.reset + ' ' + c.dim + new Date().toLocaleTimeString() + c.reset);
828
- console.log(' ' + '─'.repeat(50));
829
-
830
- // Status row
831
- const context = existsSync(CONTEXT_FILE) ? readFileSync(CONTEXT_FILE, 'utf-8').trim() + '%' : '?';
832
- const calls = existsSync(COST_FILE) ? readFileSync(COST_FILE, 'utf-8').trim() : '0';
833
- const cost = (parseInt(calls) * 0.105).toFixed(2);
881
+ const now = new Date();
882
+ const timeStr = now.toLocaleTimeString();
883
+ const dateStr = now.toLocaleDateString();
834
884
 
835
- console.log(' Hooks: ' + c.green + hookCount + c.reset + ' registered | Scripts: ' + exampleCount);
836
- console.log(' Context: ' + c.yellow + context + c.reset + ' | Cost: ~$' + cost + ' (' + calls + ' calls)');
837
- console.log(' ' + ''.repeat(50));
885
+ // Read live data
886
+ const contextPct = existsSync(CONTEXT_FILE) ? parseInt(readFileSync(CONTEXT_FILE, 'utf-8').trim()) || 0 : -1;
887
+ const toolCalls = existsSync(COST_FILE) ? parseInt(readFileSync(COST_FILE, 'utf-8').trim()) || 0 : 0;
888
+ const costEst = (toolCalls * 0.105).toFixed(2);
838
889
 
839
- // Recent blocks
840
- console.log(c.bold + ' Recent Blocks' + c.reset);
890
+ // Parse block log
891
+ let blocks = [];
892
+ let blocksByHour = new Array(24).fill(0);
893
+ let blockReasons = {};
841
894
  if (existsSync(BLOCK_LOG)) {
842
895
  const lines = readFileSync(BLOCK_LOG, 'utf-8').split('\n').filter(l => l.trim());
843
- const recent = lines.slice(-5);
844
- for (const line of recent) {
845
- const m = line.match(/^\[([^\]]+)\]\s*BLOCKED:\s*(.+?)\s*\|/);
896
+ for (const line of lines) {
897
+ const m = line.match(/^\[([^\]]+)\]\s*BLOCKED:\s*(.+?)\s*\|\s*cmd:\s*(.+)$/);
846
898
  if (m) {
847
- const time = m[1].replace(/T/, ' ').replace(/\+.*/, '').slice(11, 16);
848
- console.log(' ' + c.dim + time + c.reset + ' ' + c.red + m[2].trim() + c.reset);
899
+ const hour = new Date(m[1]).getHours();
900
+ if (!isNaN(hour)) blocksByHour[hour]++;
901
+ const reason = m[2].trim();
902
+ blockReasons[reason] = (blockReasons[reason] || 0) + 1;
903
+ blocks.push({ time: m[1], reason, cmd: m[3].trim() });
849
904
  }
850
905
  }
851
- if (recent.length === 0) console.log(c.dim + ' (none)' + c.reset);
906
+ }
907
+
908
+ const totalBlocks = blocks.length;
909
+ const todayBlocks = blocks.filter(b => b.time.startsWith(dateStr.split('/').reverse().join('-'))).length;
910
+
911
+ // === RENDER ===
912
+ console.log(hline(box.tl, box.tr, W));
913
+ console.log(pad(c.bold + 'cc-safe-setup dashboard' + c.reset + ' ' + c.dim + timeStr + c.reset, W));
914
+ console.log(hline(box.lt, box.rt, W));
915
+
916
+ // Status panel
917
+ const scoreColor = auditScore >= 80 ? c.green : auditScore >= 50 ? c.yellow : c.red;
918
+ const grade = auditScore >= 80 ? 'A' : auditScore >= 60 ? 'B' : auditScore >= 40 ? 'C' : 'F';
919
+ console.log(pad('Score: ' + scoreColor + auditScore + '/100' + c.reset + ' (Grade ' + grade + ') Hooks: ' + c.green + totalHooks + c.reset + ' Scripts: ' + scriptCount, W));
920
+
921
+ // Context bar
922
+ if (contextPct >= 0) {
923
+ const ctxColor = contextPct > 40 ? c.green : contextPct > 20 ? c.yellow : c.red;
924
+ console.log(pad('Context: ' + ctxColor + contextPct + '%' + c.reset + ' ' + progressBar(contextPct, 30, ctxColor, c.dim), W));
852
925
  } else {
853
- console.log(c.dim + ' (no log yet)' + c.reset);
926
+ console.log(pad('Context: ' + c.dim + 'unknown' + c.reset, W));
854
927
  }
855
928
 
856
- console.log(' ' + '─'.repeat(50));
929
+ // Cost
930
+ console.log(pad('Cost: ~$' + costEst + ' (' + toolCalls + ' tool calls, Opus)', W));
931
+ console.log(pad('Blocks: ' + c.red + totalBlocks + c.reset + ' total | Today: ' + todayBlocks, W));
932
+
933
+ // Hooks by trigger
934
+ console.log(hline(box.lt, box.rt, W));
935
+ console.log(pad(c.bold + 'Hooks by Trigger' + c.reset, W));
936
+ for (const [trigger, count] of Object.entries(hooksByTrigger)) {
937
+ const bar = '█'.repeat(Math.min(count, 20));
938
+ console.log(pad(c.dim + trigger.padEnd(18) + c.reset + c.blue + bar + c.reset + ' ' + count, W));
939
+ }
940
+
941
+ // Hourly activity sparkline
942
+ console.log(hline(box.lt, box.rt, W));
943
+ console.log(pad(c.bold + 'Block Activity (24h)' + c.reset, W));
944
+ console.log(pad(c.yellow + sparkline(blocksByHour, 24) + c.reset + ' ' + c.dim + '0h' + ' '.repeat(20) + '23h' + c.reset, W));
945
+
946
+ // Top block reasons
947
+ console.log(hline(box.lt, box.rt, W));
948
+ console.log(pad(c.bold + 'Top Block Reasons' + c.reset, W));
949
+ const sortedReasons = Object.entries(blockReasons).sort((a, b) => b[1] - a[1]).slice(0, 5);
950
+ const maxR = sortedReasons[0]?.[1] || 1;
951
+ for (const [reason, count] of sortedReasons) {
952
+ const bar = '▓'.repeat(Math.ceil(count / maxR * 15));
953
+ console.log(pad(c.red + bar + c.reset + ' ' + count + ' ' + c.dim + reason.slice(0, 25) + c.reset, W));
954
+ }
955
+ if (sortedReasons.length === 0) {
956
+ console.log(pad(c.dim + '(no blocks recorded)' + c.reset, W));
957
+ }
958
+
959
+ // Recent blocks
960
+ console.log(hline(box.lt, box.rt, W));
961
+ console.log(pad(c.bold + 'Recent Blocks' + c.reset, W));
962
+ const recent = blocks.slice(-5);
963
+ for (const b of recent) {
964
+ const time = b.time.replace(/T/, ' ').replace(/\+.*/, '').slice(11, 16);
965
+ console.log(pad(c.dim + time + c.reset + ' ' + c.red + b.reason.slice(0, 35) + c.reset, W));
966
+ }
967
+ if (recent.length === 0) console.log(pad(c.dim + '(none)' + c.reset, W));
968
+
969
+ // Session errors
970
+ let errorCount = 0;
971
+ if (existsSync(ERROR_LOG)) {
972
+ errorCount = readFileSync(ERROR_LOG, 'utf-8').split('\n').filter(l => l.trim()).length;
973
+ }
974
+ if (errorCount > 0) {
975
+ console.log(hline(box.lt, box.rt, W));
976
+ console.log(pad(c.yellow + 'Session errors: ' + errorCount + c.reset, W));
977
+ }
978
+
979
+ console.log(hline(box.bl, box.br, W));
857
980
  console.log(c.dim + ' Refreshing every 3s. Ctrl+C to exit.' + c.reset);
858
981
  }
859
982
 
860
983
  render();
861
984
  setInterval(render, 3000);
862
-
863
- // Keep alive
864
985
  await new Promise(() => {});
865
986
  }
866
987
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-safe-setup",
3
- "version": "4.0.3",
3
+ "version": "5.0.0",
4
4
  "description": "One command to make Claude Code safe for autonomous operation. 8 built-in + 38 examples. 22 commands including dashboard, create, audit, lint, diff, benchmark. 2,500+ daily npm downloads.",
5
5
  "main": "index.mjs",
6
6
  "bin": {