cc4pm 1.8.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 (108) hide show
  1. package/.claude-plugin/README.md +17 -0
  2. package/.claude-plugin/plugin.json +25 -0
  3. package/LICENSE +21 -0
  4. package/README.md +157 -0
  5. package/README.zh-CN.md +134 -0
  6. package/contexts/dev.md +20 -0
  7. package/contexts/research.md +26 -0
  8. package/contexts/review.md +22 -0
  9. package/examples/CLAUDE.md +100 -0
  10. package/examples/statusline.json +19 -0
  11. package/examples/user-CLAUDE.md +109 -0
  12. package/install.sh +17 -0
  13. package/manifests/install-components.json +173 -0
  14. package/manifests/install-modules.json +335 -0
  15. package/manifests/install-profiles.json +75 -0
  16. package/package.json +117 -0
  17. package/schemas/ecc-install-config.schema.json +58 -0
  18. package/schemas/hooks.schema.json +197 -0
  19. package/schemas/install-components.schema.json +56 -0
  20. package/schemas/install-modules.schema.json +105 -0
  21. package/schemas/install-profiles.schema.json +45 -0
  22. package/schemas/install-state.schema.json +210 -0
  23. package/schemas/package-manager.schema.json +23 -0
  24. package/schemas/plugin.schema.json +58 -0
  25. package/scripts/ci/catalog.js +83 -0
  26. package/scripts/ci/validate-agents.js +81 -0
  27. package/scripts/ci/validate-commands.js +135 -0
  28. package/scripts/ci/validate-hooks.js +239 -0
  29. package/scripts/ci/validate-install-manifests.js +211 -0
  30. package/scripts/ci/validate-no-personal-paths.js +63 -0
  31. package/scripts/ci/validate-rules.js +81 -0
  32. package/scripts/ci/validate-skills.js +54 -0
  33. package/scripts/claw.js +468 -0
  34. package/scripts/doctor.js +110 -0
  35. package/scripts/ecc.js +194 -0
  36. package/scripts/hooks/auto-tmux-dev.js +88 -0
  37. package/scripts/hooks/check-console-log.js +71 -0
  38. package/scripts/hooks/check-hook-enabled.js +12 -0
  39. package/scripts/hooks/cost-tracker.js +78 -0
  40. package/scripts/hooks/doc-file-warning.js +63 -0
  41. package/scripts/hooks/evaluate-session.js +100 -0
  42. package/scripts/hooks/insaits-security-monitor.py +269 -0
  43. package/scripts/hooks/insaits-security-wrapper.js +88 -0
  44. package/scripts/hooks/post-bash-build-complete.js +27 -0
  45. package/scripts/hooks/post-bash-pr-created.js +36 -0
  46. package/scripts/hooks/post-edit-console-warn.js +54 -0
  47. package/scripts/hooks/post-edit-format.js +109 -0
  48. package/scripts/hooks/post-edit-typecheck.js +96 -0
  49. package/scripts/hooks/pre-bash-dev-server-block.js +187 -0
  50. package/scripts/hooks/pre-bash-git-push-reminder.js +28 -0
  51. package/scripts/hooks/pre-bash-tmux-reminder.js +33 -0
  52. package/scripts/hooks/pre-compact.js +48 -0
  53. package/scripts/hooks/pre-write-doc-warn.js +9 -0
  54. package/scripts/hooks/quality-gate.js +168 -0
  55. package/scripts/hooks/run-with-flags-shell.sh +32 -0
  56. package/scripts/hooks/run-with-flags.js +120 -0
  57. package/scripts/hooks/session-end-marker.js +15 -0
  58. package/scripts/hooks/session-end.js +299 -0
  59. package/scripts/hooks/session-start.js +97 -0
  60. package/scripts/hooks/suggest-compact.js +80 -0
  61. package/scripts/install-apply.js +137 -0
  62. package/scripts/install-plan.js +254 -0
  63. package/scripts/lib/hook-flags.js +74 -0
  64. package/scripts/lib/install/apply.js +23 -0
  65. package/scripts/lib/install/config.js +82 -0
  66. package/scripts/lib/install/request.js +113 -0
  67. package/scripts/lib/install/runtime.js +42 -0
  68. package/scripts/lib/install-executor.js +605 -0
  69. package/scripts/lib/install-lifecycle.js +763 -0
  70. package/scripts/lib/install-manifests.js +305 -0
  71. package/scripts/lib/install-state.js +120 -0
  72. package/scripts/lib/install-targets/antigravity-project.js +9 -0
  73. package/scripts/lib/install-targets/claude-home.js +10 -0
  74. package/scripts/lib/install-targets/codex-home.js +10 -0
  75. package/scripts/lib/install-targets/cursor-project.js +10 -0
  76. package/scripts/lib/install-targets/helpers.js +89 -0
  77. package/scripts/lib/install-targets/opencode-home.js +10 -0
  78. package/scripts/lib/install-targets/registry.js +64 -0
  79. package/scripts/lib/orchestration-session.js +299 -0
  80. package/scripts/lib/package-manager.d.ts +119 -0
  81. package/scripts/lib/package-manager.js +431 -0
  82. package/scripts/lib/project-detect.js +428 -0
  83. package/scripts/lib/resolve-formatter.js +185 -0
  84. package/scripts/lib/session-adapters/canonical-session.js +138 -0
  85. package/scripts/lib/session-adapters/claude-history.js +149 -0
  86. package/scripts/lib/session-adapters/dmux-tmux.js +80 -0
  87. package/scripts/lib/session-adapters/registry.js +111 -0
  88. package/scripts/lib/session-aliases.d.ts +136 -0
  89. package/scripts/lib/session-aliases.js +481 -0
  90. package/scripts/lib/session-manager.d.ts +131 -0
  91. package/scripts/lib/session-manager.js +464 -0
  92. package/scripts/lib/shell-split.js +86 -0
  93. package/scripts/lib/skill-improvement/amendify.js +89 -0
  94. package/scripts/lib/skill-improvement/evaluate.js +59 -0
  95. package/scripts/lib/skill-improvement/health.js +118 -0
  96. package/scripts/lib/skill-improvement/observations.js +108 -0
  97. package/scripts/lib/tmux-worktree-orchestrator.js +491 -0
  98. package/scripts/lib/utils.d.ts +183 -0
  99. package/scripts/lib/utils.js +543 -0
  100. package/scripts/list-installed.js +90 -0
  101. package/scripts/orchestrate-codex-worker.sh +92 -0
  102. package/scripts/orchestrate-worktrees.js +108 -0
  103. package/scripts/orchestration-status.js +62 -0
  104. package/scripts/repair.js +97 -0
  105. package/scripts/session-inspect.js +150 -0
  106. package/scripts/setup-package-manager.js +204 -0
  107. package/scripts/skill-create-output.js +244 -0
  108. package/scripts/uninstall.js +96 -0
@@ -0,0 +1,269 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ InsAIts Security Monitor -- PreToolUse Hook for Claude Code
4
+ ============================================================
5
+
6
+ Real-time security monitoring for Claude Code tool inputs.
7
+ Detects credential exposure, prompt injection, behavioral anomalies,
8
+ hallucination chains, and 20+ other anomaly types -- runs 100% locally.
9
+
10
+ Writes audit events to .insaits_audit_session.jsonl for forensic tracing.
11
+
12
+ Setup:
13
+ pip install insa-its
14
+ export ECC_ENABLE_INSAITS=1
15
+
16
+ Add to .claude/settings.json:
17
+ {
18
+ "hooks": {
19
+ "PreToolUse": [
20
+ {
21
+ "matcher": "Bash|Write|Edit|MultiEdit",
22
+ "hooks": [
23
+ {
24
+ "type": "command",
25
+ "command": "node scripts/hooks/insaits-security-wrapper.js"
26
+ }
27
+ ]
28
+ }
29
+ ]
30
+ }
31
+ }
32
+
33
+ How it works:
34
+ Claude Code passes tool input as JSON on stdin.
35
+ This script runs InsAIts anomaly detection on the content.
36
+ Exit code 0 = clean (pass through).
37
+ Exit code 2 = critical issue found (blocks tool execution).
38
+ Stderr output = non-blocking warning shown to Claude.
39
+
40
+ Environment variables:
41
+ INSAITS_DEV_MODE Set to "true" to enable dev mode (no API key needed).
42
+ Defaults to "false" (strict mode).
43
+ INSAITS_MODEL LLM model identifier for fingerprinting. Default: claude-opus.
44
+ INSAITS_FAIL_MODE "open" (default) = continue on SDK errors.
45
+ "closed" = block tool execution on SDK errors.
46
+ INSAITS_VERBOSE Set to any value to enable debug logging.
47
+
48
+ Detections include:
49
+ - Credential exposure (API keys, tokens, passwords)
50
+ - Prompt injection patterns
51
+ - Hallucination indicators (phantom citations, fact contradictions)
52
+ - Behavioral anomalies (context loss, semantic drift)
53
+ - Tool description divergence
54
+ - Shorthand emergence / jargon drift
55
+
56
+ All processing is local -- no data leaves your machine.
57
+
58
+ Author: Cristi Bogdan -- YuyAI (https://github.com/Nomadu27/InsAIts)
59
+ License: Apache 2.0
60
+ """
61
+
62
+ from __future__ import annotations
63
+
64
+ import hashlib
65
+ import json
66
+ import logging
67
+ import os
68
+ import sys
69
+ import time
70
+ from typing import Any, Dict, List, Tuple
71
+
72
+ # Configure logging to stderr so it does not interfere with stdout protocol
73
+ logging.basicConfig(
74
+ stream=sys.stderr,
75
+ format="[InsAIts] %(message)s",
76
+ level=logging.DEBUG if os.environ.get("INSAITS_VERBOSE") else logging.WARNING,
77
+ )
78
+ log = logging.getLogger("insaits-hook")
79
+
80
+ # Try importing InsAIts SDK
81
+ try:
82
+ from insa_its import insAItsMonitor
83
+ INSAITS_AVAILABLE: bool = True
84
+ except ImportError:
85
+ INSAITS_AVAILABLE = False
86
+
87
+ # --- Constants ---
88
+ AUDIT_FILE: str = ".insaits_audit_session.jsonl"
89
+ MIN_CONTENT_LENGTH: int = 10
90
+ MAX_SCAN_LENGTH: int = 4000
91
+ DEFAULT_MODEL: str = "claude-opus"
92
+ BLOCKING_SEVERITIES: frozenset = frozenset({"CRITICAL"})
93
+
94
+
95
+ def extract_content(data: Dict[str, Any]) -> Tuple[str, str]:
96
+ """Extract inspectable text from a Claude Code tool input payload.
97
+
98
+ Returns:
99
+ A (text, context) tuple where *text* is the content to scan and
100
+ *context* is a short label for the audit log.
101
+ """
102
+ tool_name: str = data.get("tool_name", "")
103
+ tool_input: Dict[str, Any] = data.get("tool_input", {})
104
+
105
+ text: str = ""
106
+ context: str = ""
107
+
108
+ if tool_name in ("Write", "Edit", "MultiEdit"):
109
+ text = tool_input.get("content", "") or tool_input.get("new_string", "")
110
+ context = "file:" + str(tool_input.get("file_path", ""))[:80]
111
+ elif tool_name == "Bash":
112
+ # PreToolUse: the tool hasn't executed yet, inspect the command
113
+ command: str = str(tool_input.get("command", ""))
114
+ text = command
115
+ context = "bash:" + command[:80]
116
+ elif "content" in data:
117
+ content: Any = data["content"]
118
+ if isinstance(content, list):
119
+ text = "\n".join(
120
+ b.get("text", "") for b in content if b.get("type") == "text"
121
+ )
122
+ elif isinstance(content, str):
123
+ text = content
124
+ context = str(data.get("task", ""))
125
+
126
+ return text, context
127
+
128
+
129
+ def write_audit(event: Dict[str, Any]) -> None:
130
+ """Append an audit event to the JSONL audit log.
131
+
132
+ Creates a new dict to avoid mutating the caller's *event*.
133
+ """
134
+ try:
135
+ enriched: Dict[str, Any] = {
136
+ **event,
137
+ "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
138
+ }
139
+ enriched["hash"] = hashlib.sha256(
140
+ json.dumps(enriched, sort_keys=True).encode()
141
+ ).hexdigest()[:16]
142
+ with open(AUDIT_FILE, "a", encoding="utf-8") as f:
143
+ f.write(json.dumps(enriched) + "\n")
144
+ except OSError as exc:
145
+ log.warning("Failed to write audit log %s: %s", AUDIT_FILE, exc)
146
+
147
+
148
+ def get_anomaly_attr(anomaly: Any, key: str, default: str = "") -> str:
149
+ """Get a field from an anomaly that may be a dict or an object.
150
+
151
+ The SDK's ``send_message()`` returns anomalies as dicts, while
152
+ other code paths may return dataclass/object instances. This
153
+ helper handles both transparently.
154
+ """
155
+ if isinstance(anomaly, dict):
156
+ return str(anomaly.get(key, default))
157
+ return str(getattr(anomaly, key, default))
158
+
159
+
160
+ def format_feedback(anomalies: List[Any]) -> str:
161
+ """Format detected anomalies as feedback for Claude Code.
162
+
163
+ Returns:
164
+ A human-readable multi-line string describing each finding.
165
+ """
166
+ lines: List[str] = [
167
+ "== InsAIts Security Monitor -- Issues Detected ==",
168
+ "",
169
+ ]
170
+ for i, a in enumerate(anomalies, 1):
171
+ sev: str = get_anomaly_attr(a, "severity", "MEDIUM")
172
+ atype: str = get_anomaly_attr(a, "type", "UNKNOWN")
173
+ detail: str = get_anomaly_attr(a, "details", "")
174
+ lines.extend([
175
+ f"{i}. [{sev}] {atype}",
176
+ f" {detail[:120]}",
177
+ "",
178
+ ])
179
+ lines.extend([
180
+ "-" * 56,
181
+ "Fix the issues above before continuing.",
182
+ "Audit log: " + AUDIT_FILE,
183
+ ])
184
+ return "\n".join(lines)
185
+
186
+
187
+ def main() -> None:
188
+ """Entry point for the Claude Code PreToolUse hook."""
189
+ raw: str = sys.stdin.read().strip()
190
+ if not raw:
191
+ sys.exit(0)
192
+
193
+ try:
194
+ data: Dict[str, Any] = json.loads(raw)
195
+ except json.JSONDecodeError:
196
+ data = {"content": raw}
197
+
198
+ text, context = extract_content(data)
199
+
200
+ # Skip very short content (e.g. "OK", empty bash results)
201
+ if len(text.strip()) < MIN_CONTENT_LENGTH:
202
+ sys.exit(0)
203
+
204
+ if not INSAITS_AVAILABLE:
205
+ log.warning("Not installed. Run: pip install insa-its")
206
+ sys.exit(0)
207
+
208
+ # Wrap SDK calls so an internal error does not crash the hook
209
+ try:
210
+ monitor: insAItsMonitor = insAItsMonitor(
211
+ session_name="claude-code-hook",
212
+ dev_mode=os.environ.get(
213
+ "INSAITS_DEV_MODE", "false"
214
+ ).lower() in ("1", "true", "yes"),
215
+ )
216
+ result: Dict[str, Any] = monitor.send_message(
217
+ text=text[:MAX_SCAN_LENGTH],
218
+ sender_id="claude-code",
219
+ llm_id=os.environ.get("INSAITS_MODEL", DEFAULT_MODEL),
220
+ )
221
+ except Exception as exc: # Broad catch intentional: unknown SDK internals
222
+ fail_mode: str = os.environ.get("INSAITS_FAIL_MODE", "open").lower()
223
+ if fail_mode == "closed":
224
+ sys.stdout.write(
225
+ f"InsAIts SDK error ({type(exc).__name__}); "
226
+ "blocking execution to avoid unscanned input.\n"
227
+ )
228
+ sys.exit(2)
229
+ log.warning(
230
+ "SDK error (%s), skipping security scan: %s",
231
+ type(exc).__name__, exc,
232
+ )
233
+ sys.exit(0)
234
+
235
+ anomalies: List[Any] = result.get("anomalies", [])
236
+
237
+ # Write audit event regardless of findings
238
+ write_audit({
239
+ "tool": data.get("tool_name", "unknown"),
240
+ "context": context,
241
+ "anomaly_count": len(anomalies),
242
+ "anomaly_types": [get_anomaly_attr(a, "type") for a in anomalies],
243
+ "text_length": len(text),
244
+ })
245
+
246
+ if not anomalies:
247
+ log.debug("Clean -- no anomalies detected.")
248
+ sys.exit(0)
249
+
250
+ # Determine maximum severity
251
+ has_critical: bool = any(
252
+ get_anomaly_attr(a, "severity").upper() in BLOCKING_SEVERITIES
253
+ for a in anomalies
254
+ )
255
+
256
+ feedback: str = format_feedback(anomalies)
257
+
258
+ if has_critical:
259
+ # stdout feedback -> Claude Code shows to the model
260
+ sys.stdout.write(feedback + "\n")
261
+ sys.exit(2) # PreToolUse exit 2 = block tool execution
262
+ else:
263
+ # Non-critical: warn via stderr (non-blocking)
264
+ log.warning("\n%s", feedback)
265
+ sys.exit(0)
266
+
267
+
268
+ if __name__ == "__main__":
269
+ main()
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * InsAIts Security Monitor — wrapper for run-with-flags compatibility.
4
+ *
5
+ * This thin wrapper receives stdin from the hooks infrastructure and
6
+ * delegates to the Python-based insaits-security-monitor.py script.
7
+ *
8
+ * The wrapper exists because run-with-flags.js spawns child scripts
9
+ * via `node`, so a JS entry point is needed to bridge to Python.
10
+ */
11
+
12
+ 'use strict';
13
+
14
+ const path = require('path');
15
+ const { spawnSync } = require('child_process');
16
+
17
+ const MAX_STDIN = 1024 * 1024;
18
+
19
+ function isEnabled(value) {
20
+ return ['1', 'true', 'yes', 'on'].includes(String(value || '').toLowerCase());
21
+ }
22
+
23
+ let raw = '';
24
+ process.stdin.setEncoding('utf8');
25
+ process.stdin.on('data', chunk => {
26
+ if (raw.length < MAX_STDIN) {
27
+ raw += chunk.substring(0, MAX_STDIN - raw.length);
28
+ }
29
+ });
30
+
31
+ process.stdin.on('end', () => {
32
+ if (!isEnabled(process.env.ECC_ENABLE_INSAITS)) {
33
+ process.stdout.write(raw);
34
+ process.exit(0);
35
+ }
36
+
37
+ const scriptDir = __dirname;
38
+ const pyScript = path.join(scriptDir, 'insaits-security-monitor.py');
39
+
40
+ // Try python3 first (macOS/Linux), fall back to python (Windows)
41
+ const pythonCandidates = ['python3', 'python'];
42
+ let result;
43
+
44
+ for (const pythonBin of pythonCandidates) {
45
+ result = spawnSync(pythonBin, [pyScript], {
46
+ input: raw,
47
+ encoding: 'utf8',
48
+ env: process.env,
49
+ cwd: process.cwd(),
50
+ timeout: 14000,
51
+ });
52
+
53
+ // ENOENT means binary not found — try next candidate
54
+ if (result.error && result.error.code === 'ENOENT') {
55
+ continue;
56
+ }
57
+ break;
58
+ }
59
+
60
+ if (!result || (result.error && result.error.code === 'ENOENT')) {
61
+ process.stderr.write('[InsAIts] python3/python not found. Install Python 3.9+ and: pip install insa-its\n');
62
+ process.stdout.write(raw);
63
+ process.exit(0);
64
+ }
65
+
66
+ // Log non-ENOENT spawn errors (timeout, signal kill, etc.) so users
67
+ // know the security monitor did not run — fail-open with a warning.
68
+ if (result.error) {
69
+ process.stderr.write(`[InsAIts] Security monitor failed to run: ${result.error.message}\n`);
70
+ process.stdout.write(raw);
71
+ process.exit(0);
72
+ }
73
+
74
+ // result.status is null when the process was killed by a signal or
75
+ // timed out. Check BEFORE writing stdout to avoid leaking partial
76
+ // or corrupt monitor output. Pass through original raw input instead.
77
+ if (!Number.isInteger(result.status)) {
78
+ const signal = result.signal || 'unknown';
79
+ process.stderr.write(`[InsAIts] Security monitor killed (signal: ${signal}). Tool execution continues.\n`);
80
+ process.stdout.write(raw);
81
+ process.exit(0);
82
+ }
83
+
84
+ if (result.stdout) process.stdout.write(result.stdout);
85
+ if (result.stderr) process.stderr.write(result.stderr);
86
+
87
+ process.exit(result.status);
88
+ });
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const MAX_STDIN = 1024 * 1024;
5
+ let raw = '';
6
+
7
+ process.stdin.setEncoding('utf8');
8
+ process.stdin.on('data', chunk => {
9
+ if (raw.length < MAX_STDIN) {
10
+ const remaining = MAX_STDIN - raw.length;
11
+ raw += chunk.substring(0, remaining);
12
+ }
13
+ });
14
+
15
+ process.stdin.on('end', () => {
16
+ try {
17
+ const input = JSON.parse(raw);
18
+ const cmd = String(input.tool_input?.command || '');
19
+ if (/(npm run build|pnpm build|yarn build)/.test(cmd)) {
20
+ console.error('[Hook] Build completed - async analysis running in background');
21
+ }
22
+ } catch {
23
+ // ignore parse errors and pass through
24
+ }
25
+
26
+ process.stdout.write(raw);
27
+ });
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const MAX_STDIN = 1024 * 1024;
5
+ let raw = '';
6
+
7
+ process.stdin.setEncoding('utf8');
8
+ process.stdin.on('data', chunk => {
9
+ if (raw.length < MAX_STDIN) {
10
+ const remaining = MAX_STDIN - raw.length;
11
+ raw += chunk.substring(0, remaining);
12
+ }
13
+ });
14
+
15
+ process.stdin.on('end', () => {
16
+ try {
17
+ const input = JSON.parse(raw);
18
+ const cmd = String(input.tool_input?.command || '');
19
+
20
+ if (/\bgh\s+pr\s+create\b/.test(cmd)) {
21
+ const out = String(input.tool_output?.output || '');
22
+ const match = out.match(/https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+/);
23
+ if (match) {
24
+ const prUrl = match[0];
25
+ const repo = prUrl.replace(/https:\/\/github\.com\/([^/]+\/[^/]+)\/pull\/\d+/, '$1');
26
+ const prNum = prUrl.replace(/.+\/pull\/(\d+)/, '$1');
27
+ console.error(`[Hook] PR created: ${prUrl}`);
28
+ console.error(`[Hook] To review: gh pr review ${prNum} --repo ${repo}`);
29
+ }
30
+ }
31
+ } catch {
32
+ // ignore parse errors and pass through
33
+ }
34
+
35
+ process.stdout.write(raw);
36
+ });
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * PostToolUse Hook: Warn about console.log statements after edits
4
+ *
5
+ * Cross-platform (Windows, macOS, Linux)
6
+ *
7
+ * Runs after Edit tool use. If the edited JS/TS file contains console.log
8
+ * statements, warns with line numbers to help remove debug statements
9
+ * before committing.
10
+ */
11
+
12
+ const { readFile } = require('../lib/utils');
13
+
14
+ const MAX_STDIN = 1024 * 1024; // 1MB limit
15
+ let data = '';
16
+ process.stdin.setEncoding('utf8');
17
+
18
+ process.stdin.on('data', chunk => {
19
+ if (data.length < MAX_STDIN) {
20
+ const remaining = MAX_STDIN - data.length;
21
+ data += chunk.substring(0, remaining);
22
+ }
23
+ });
24
+
25
+ process.stdin.on('end', () => {
26
+ try {
27
+ const input = JSON.parse(data);
28
+ const filePath = input.tool_input?.file_path;
29
+
30
+ if (filePath && /\.(ts|tsx|js|jsx)$/.test(filePath)) {
31
+ const content = readFile(filePath);
32
+ if (!content) { process.stdout.write(data); process.exit(0); }
33
+ const lines = content.split('\n');
34
+ const matches = [];
35
+
36
+ lines.forEach((line, idx) => {
37
+ if (/console\.log/.test(line)) {
38
+ matches.push((idx + 1) + ': ' + line.trim());
39
+ }
40
+ });
41
+
42
+ if (matches.length > 0) {
43
+ console.error('[Hook] WARNING: console.log found in ' + filePath);
44
+ matches.slice(0, 5).forEach(m => console.error(m));
45
+ console.error('[Hook] Remove console.log before committing');
46
+ }
47
+ }
48
+ } catch {
49
+ // Invalid input — pass through
50
+ }
51
+
52
+ process.stdout.write(data);
53
+ process.exit(0);
54
+ });
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * PostToolUse Hook: Auto-format JS/TS files after edits
4
+ *
5
+ * Cross-platform (Windows, macOS, Linux)
6
+ *
7
+ * Runs after Edit tool use. If the edited file is a JS/TS file,
8
+ * auto-detects the project formatter (Biome or Prettier) by looking
9
+ * for config files, then formats accordingly.
10
+ *
11
+ * For Biome, uses `check --write` (format + lint in one pass) to
12
+ * avoid a redundant second invocation from quality-gate.js.
13
+ *
14
+ * Prefers the local node_modules/.bin binary over npx to skip
15
+ * package-resolution overhead (~200-500ms savings per invocation).
16
+ *
17
+ * Fails silently if no formatter is found or installed.
18
+ */
19
+
20
+ const { execFileSync, spawnSync } = require('child_process');
21
+ const path = require('path');
22
+
23
+ // Shell metacharacters that cmd.exe interprets as command separators/operators
24
+ const UNSAFE_PATH_CHARS = /[&|<>^%!]/;
25
+
26
+ const { findProjectRoot, detectFormatter, resolveFormatterBin } = require('../lib/resolve-formatter');
27
+
28
+ const MAX_STDIN = 1024 * 1024; // 1MB limit
29
+
30
+ /**
31
+ * Core logic — exported so run-with-flags.js can call directly
32
+ * without spawning a child process.
33
+ *
34
+ * @param {string} rawInput - Raw JSON string from stdin
35
+ * @returns {string} The original input (pass-through)
36
+ */
37
+ function run(rawInput) {
38
+ try {
39
+ const input = JSON.parse(rawInput);
40
+ const filePath = input.tool_input?.file_path;
41
+
42
+ if (filePath && /\.(ts|tsx|js|jsx)$/.test(filePath)) {
43
+ try {
44
+ const resolvedFilePath = path.resolve(filePath);
45
+ const projectRoot = findProjectRoot(path.dirname(resolvedFilePath));
46
+ const formatter = detectFormatter(projectRoot);
47
+ if (!formatter) return rawInput;
48
+
49
+ const resolved = resolveFormatterBin(projectRoot, formatter);
50
+ if (!resolved) return rawInput;
51
+
52
+ // Biome: `check --write` = format + lint in one pass
53
+ // Prettier: `--write` = format only
54
+ const args = formatter === 'biome' ? [...resolved.prefix, 'check', '--write', resolvedFilePath] : [...resolved.prefix, '--write', resolvedFilePath];
55
+
56
+ if (process.platform === 'win32' && resolved.bin.endsWith('.cmd')) {
57
+ // Windows: .cmd files require shell to execute. Guard against
58
+ // command injection by rejecting paths with shell metacharacters.
59
+ if (UNSAFE_PATH_CHARS.test(resolvedFilePath)) {
60
+ throw new Error('File path contains unsafe shell characters');
61
+ }
62
+ const result = spawnSync(resolved.bin, args, {
63
+ cwd: projectRoot,
64
+ shell: true,
65
+ stdio: 'pipe',
66
+ timeout: 15000
67
+ });
68
+ if (result.error) throw result.error;
69
+ if (typeof result.status === 'number' && result.status !== 0) {
70
+ throw new Error(result.stderr?.toString() || `Formatter exited with status ${result.status}`);
71
+ }
72
+ } else {
73
+ execFileSync(resolved.bin, args, {
74
+ cwd: projectRoot,
75
+ stdio: ['pipe', 'pipe', 'pipe'],
76
+ timeout: 15000
77
+ });
78
+ }
79
+ } catch {
80
+ // Formatter not installed, file missing, or failed — non-blocking
81
+ }
82
+ }
83
+ } catch {
84
+ // Invalid input — pass through
85
+ }
86
+
87
+ return rawInput;
88
+ }
89
+
90
+ // ── stdin entry point (backwards-compatible) ────────────────────
91
+ if (require.main === module) {
92
+ let data = '';
93
+ process.stdin.setEncoding('utf8');
94
+
95
+ process.stdin.on('data', chunk => {
96
+ if (data.length < MAX_STDIN) {
97
+ const remaining = MAX_STDIN - data.length;
98
+ data += chunk.substring(0, remaining);
99
+ }
100
+ });
101
+
102
+ process.stdin.on('end', () => {
103
+ data = run(data);
104
+ process.stdout.write(data);
105
+ process.exit(0);
106
+ });
107
+ }
108
+
109
+ module.exports = { run };
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * PostToolUse Hook: TypeScript check after editing .ts/.tsx files
4
+ *
5
+ * Cross-platform (Windows, macOS, Linux)
6
+ *
7
+ * Runs after Edit tool use on TypeScript files. Walks up from the file's
8
+ * directory to find the nearest tsconfig.json, then runs tsc --noEmit
9
+ * and reports only errors related to the edited file.
10
+ */
11
+
12
+ const { execFileSync } = require("child_process");
13
+ const fs = require("fs");
14
+ const path = require("path");
15
+
16
+ const MAX_STDIN = 1024 * 1024; // 1MB limit
17
+ let data = "";
18
+ process.stdin.setEncoding("utf8");
19
+
20
+ process.stdin.on("data", (chunk) => {
21
+ if (data.length < MAX_STDIN) {
22
+ const remaining = MAX_STDIN - data.length;
23
+ data += chunk.substring(0, remaining);
24
+ }
25
+ });
26
+
27
+ process.stdin.on("end", () => {
28
+ try {
29
+ const input = JSON.parse(data);
30
+ const filePath = input.tool_input?.file_path;
31
+
32
+ if (filePath && /\.(ts|tsx)$/.test(filePath)) {
33
+ const resolvedPath = path.resolve(filePath);
34
+ if (!fs.existsSync(resolvedPath)) {
35
+ process.stdout.write(data);
36
+ process.exit(0);
37
+ }
38
+ // Find nearest tsconfig.json by walking up (max 20 levels to prevent infinite loop)
39
+ let dir = path.dirname(resolvedPath);
40
+ const root = path.parse(dir).root;
41
+ let depth = 0;
42
+
43
+ while (dir !== root && depth < 20) {
44
+ if (fs.existsSync(path.join(dir, "tsconfig.json"))) {
45
+ break;
46
+ }
47
+ dir = path.dirname(dir);
48
+ depth++;
49
+ }
50
+
51
+ if (fs.existsSync(path.join(dir, "tsconfig.json"))) {
52
+ try {
53
+ // Use npx.cmd on Windows to avoid shell: true which enables command injection
54
+ const npxBin = process.platform === "win32" ? "npx.cmd" : "npx";
55
+ execFileSync(npxBin, ["tsc", "--noEmit", "--pretty", "false"], {
56
+ cwd: dir,
57
+ encoding: "utf8",
58
+ stdio: ["pipe", "pipe", "pipe"],
59
+ timeout: 30000,
60
+ });
61
+ } catch (err) {
62
+ // tsc exits non-zero when there are errors — filter to edited file
63
+ const output = (err.stdout || "") + (err.stderr || "");
64
+ // Compute paths that uniquely identify the edited file.
65
+ // tsc output uses paths relative to its cwd (the tsconfig dir),
66
+ // so check for the relative path, absolute path, and original path.
67
+ // Avoid bare basename matching — it causes false positives when
68
+ // multiple files share the same name (e.g., src/utils.ts vs tests/utils.ts).
69
+ const relPath = path.relative(dir, resolvedPath);
70
+ const candidates = new Set([filePath, resolvedPath, relPath]);
71
+ const relevantLines = output
72
+ .split("\n")
73
+ .filter((line) => {
74
+ for (const candidate of candidates) {
75
+ if (line.includes(candidate)) return true;
76
+ }
77
+ return false;
78
+ })
79
+ .slice(0, 10);
80
+
81
+ if (relevantLines.length > 0) {
82
+ console.error(
83
+ "[Hook] TypeScript errors in " + path.basename(filePath) + ":",
84
+ );
85
+ relevantLines.forEach((line) => console.error(line));
86
+ }
87
+ }
88
+ }
89
+ }
90
+ } catch {
91
+ // Invalid input — pass through
92
+ }
93
+
94
+ process.stdout.write(data);
95
+ process.exit(0);
96
+ });