cc-safe-setup 4.0.2 → 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 +162 -35
  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
@@ -363,14 +363,29 @@ function examples() {
363
363
  },
364
364
  };
365
365
 
366
+ // Optional category filter: --examples safety, --examples ux, etc.
367
+ const filterArg = process.argv[process.argv.indexOf('--examples') + 1] || process.argv[process.argv.indexOf('-e') + 1] || '';
368
+ const filter = filterArg.toLowerCase();
369
+
366
370
  console.log();
367
- console.log(c.bold + ' cc-safe-setup --examples' + c.reset);
368
- console.log(c.dim + ' 30 hooks beyond the 8 built-in ones' + c.reset);
371
+ console.log(c.bold + ' cc-safe-setup --examples' + c.reset + (filter ? ' ' + filter : ''));
372
+ console.log(c.dim + ' 38 hooks beyond the 8 built-in ones' + c.reset);
373
+ if (filter) console.log(c.dim + ' Filter: ' + filter + c.reset);
369
374
  console.log();
370
375
 
371
376
  for (const [cat, hooks] of Object.entries(CATEGORIES)) {
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
+
372
387
  console.log(' ' + c.bold + c.blue + cat + c.reset);
373
- for (const [file, desc] of Object.entries(hooks)) {
388
+ for (const [file, desc] of filteredHooks) {
374
389
  console.log(' ' + c.green + '*' + c.reset + ' ' + c.bold + file + c.reset);
375
390
  console.log(' ' + c.dim + desc + c.reset);
376
391
  }
@@ -791,70 +806,182 @@ async function fullSetup() {
791
806
  }
792
807
 
793
808
  async function dashboard() {
794
- const { createReadStream, watchFile } = await import('fs');
795
- const { createInterface: createRL } = await import('readline');
809
+ const fsModule = await import('fs');
796
810
 
797
811
  const BLOCK_LOG = join(HOME, '.claude', 'blocked-commands.log');
812
+ const ERROR_LOG = join(HOME, '.claude', 'session-errors.log');
798
813
  const COST_FILE = '/tmp/cc-cost-tracker-calls';
799
814
  const CONTEXT_FILE = '/tmp/cc-context-pct';
815
+ const HANDOFF_FILE = join(HOME, '.claude', 'session-handoff.md');
816
+ const W = 60; // dashboard width
800
817
 
801
818
  const clear = () => process.stdout.write('\x1b[2J\x1b[H');
802
819
 
803
- // Count hooks
804
- let hookCount = 0;
805
- 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;
806
847
  if (existsSync(SETTINGS_PATH)) {
807
848
  try {
808
849
  const s = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8'));
809
- for (const entries of Object.values(s.hooks || {})) {
810
- 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;
811
854
  }
812
855
  } catch {}
813
856
  }
814
- exampleCount = existsSync(join(HOOKS_DIR)) ?
815
- (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 {}
816
877
 
817
878
  function render() {
818
879
  clear();
819
880
 
820
- // Header
821
- console.log(c.bold + ' cc-safe-setup --dashboard' + c.reset + ' ' + c.dim + new Date().toLocaleTimeString() + c.reset);
822
- console.log(' ' + '─'.repeat(50));
881
+ const now = new Date();
882
+ const timeStr = now.toLocaleTimeString();
883
+ const dateStr = now.toLocaleDateString();
823
884
 
824
- // Status row
825
- const context = existsSync(CONTEXT_FILE) ? readFileSync(CONTEXT_FILE, 'utf-8').trim() + '%' : '?';
826
- const calls = existsSync(COST_FILE) ? readFileSync(COST_FILE, 'utf-8').trim() : '0';
827
- const cost = (parseInt(calls) * 0.105).toFixed(2);
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);
828
889
 
829
- console.log(' Hooks: ' + c.green + hookCount + c.reset + ' registered | Scripts: ' + exampleCount);
830
- console.log(' Context: ' + c.yellow + context + c.reset + ' | Cost: ~$' + cost + ' (' + calls + ' calls)');
831
- console.log(' ' + '─'.repeat(50));
832
-
833
- // Recent blocks
834
- 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 = {};
835
894
  if (existsSync(BLOCK_LOG)) {
836
895
  const lines = readFileSync(BLOCK_LOG, 'utf-8').split('\n').filter(l => l.trim());
837
- const recent = lines.slice(-5);
838
- for (const line of recent) {
839
- 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*(.+)$/);
840
898
  if (m) {
841
- const time = m[1].replace(/T/, ' ').replace(/\+.*/, '').slice(11, 16);
842
- 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() });
843
904
  }
844
905
  }
845
- 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));
846
925
  } else {
847
- console.log(c.dim + ' (no log yet)' + c.reset);
926
+ console.log(pad('Context: ' + c.dim + 'unknown' + c.reset, W));
927
+ }
928
+
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));
848
966
  }
967
+ if (recent.length === 0) console.log(pad(c.dim + '(none)' + c.reset, W));
849
968
 
850
- console.log(' ' + '─'.repeat(50));
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));
851
980
  console.log(c.dim + ' Refreshing every 3s. Ctrl+C to exit.' + c.reset);
852
981
  }
853
982
 
854
983
  render();
855
984
  setInterval(render, 3000);
856
-
857
- // Keep alive
858
985
  await new Promise(() => {});
859
986
  }
860
987
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-safe-setup",
3
- "version": "4.0.2",
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": {