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,187 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const MAX_STDIN = 1024 * 1024;
5
+ const path = require('path');
6
+ const { splitShellSegments } = require('../lib/shell-split');
7
+
8
+ const DEV_COMMAND_WORDS = new Set([
9
+ 'npm',
10
+ 'pnpm',
11
+ 'yarn',
12
+ 'bun',
13
+ 'npx',
14
+ 'tmux'
15
+ ]);
16
+ const SKIPPABLE_PREFIX_WORDS = new Set(['env', 'command', 'builtin', 'exec', 'noglob', 'sudo', 'nohup']);
17
+ const PREFIX_OPTION_VALUE_WORDS = {
18
+ env: new Set(['-u', '-C', '-S', '--unset', '--chdir', '--split-string']),
19
+ sudo: new Set([
20
+ '-u',
21
+ '-g',
22
+ '-h',
23
+ '-p',
24
+ '-r',
25
+ '-t',
26
+ '-C',
27
+ '--user',
28
+ '--group',
29
+ '--host',
30
+ '--prompt',
31
+ '--role',
32
+ '--type',
33
+ '--close-from'
34
+ ])
35
+ };
36
+
37
+ function readToken(input, startIndex) {
38
+ let index = startIndex;
39
+ while (index < input.length && /\s/.test(input[index])) index += 1;
40
+ if (index >= input.length) return null;
41
+
42
+ let token = '';
43
+ let quote = null;
44
+
45
+ while (index < input.length) {
46
+ const ch = input[index];
47
+
48
+ if (quote) {
49
+ if (ch === quote) {
50
+ quote = null;
51
+ index += 1;
52
+ continue;
53
+ }
54
+
55
+ if (ch === '\\' && quote === '"' && index + 1 < input.length) {
56
+ token += input[index + 1];
57
+ index += 2;
58
+ continue;
59
+ }
60
+
61
+ token += ch;
62
+ index += 1;
63
+ continue;
64
+ }
65
+
66
+ if (ch === '"' || ch === "'") {
67
+ quote = ch;
68
+ index += 1;
69
+ continue;
70
+ }
71
+
72
+ if (/\s/.test(ch)) break;
73
+
74
+ if (ch === '\\' && index + 1 < input.length) {
75
+ token += input[index + 1];
76
+ index += 2;
77
+ continue;
78
+ }
79
+
80
+ token += ch;
81
+ index += 1;
82
+ }
83
+
84
+ return { token, end: index };
85
+ }
86
+
87
+ function shouldSkipOptionValue(wrapper, optionToken) {
88
+ if (!wrapper || !optionToken || optionToken.includes('=')) return false;
89
+ const optionSet = PREFIX_OPTION_VALUE_WORDS[wrapper];
90
+ return Boolean(optionSet && optionSet.has(optionToken));
91
+ }
92
+
93
+ function isOptionToken(token) {
94
+ return token.startsWith('-') && token.length > 1;
95
+ }
96
+
97
+ function normalizeCommandWord(token) {
98
+ if (!token) return '';
99
+ const base = path.basename(token).toLowerCase();
100
+ return base.replace(/\.(cmd|exe|bat)$/i, '');
101
+ }
102
+
103
+ function getLeadingCommandWord(segment) {
104
+ let index = 0;
105
+ let activeWrapper = null;
106
+ let skipNextValue = false;
107
+
108
+ while (index < segment.length) {
109
+ const parsed = readToken(segment, index);
110
+ if (!parsed) return null;
111
+ index = parsed.end;
112
+
113
+ const token = parsed.token;
114
+ if (!token) continue;
115
+
116
+ if (skipNextValue) {
117
+ skipNextValue = false;
118
+ continue;
119
+ }
120
+
121
+ if (token === '--') {
122
+ activeWrapper = null;
123
+ continue;
124
+ }
125
+
126
+ if (/^[A-Za-z_][A-Za-z0-9_]*=.*/.test(token)) continue;
127
+
128
+ const normalizedToken = normalizeCommandWord(token);
129
+
130
+ if (SKIPPABLE_PREFIX_WORDS.has(normalizedToken)) {
131
+ activeWrapper = normalizedToken;
132
+ continue;
133
+ }
134
+
135
+ if (activeWrapper && isOptionToken(token)) {
136
+ if (shouldSkipOptionValue(activeWrapper, token)) {
137
+ skipNextValue = true;
138
+ }
139
+ continue;
140
+ }
141
+
142
+ return normalizedToken;
143
+ }
144
+
145
+ return null;
146
+ }
147
+
148
+ let raw = '';
149
+ process.stdin.setEncoding('utf8');
150
+ process.stdin.on('data', chunk => {
151
+ if (raw.length < MAX_STDIN) {
152
+ const remaining = MAX_STDIN - raw.length;
153
+ raw += chunk.substring(0, remaining);
154
+ }
155
+ });
156
+
157
+ process.stdin.on('end', () => {
158
+ try {
159
+ const input = JSON.parse(raw);
160
+ const cmd = String(input.tool_input?.command || '');
161
+
162
+ if (process.platform !== 'win32') {
163
+ const segments = splitShellSegments(cmd);
164
+ const tmuxLauncher = /^\s*tmux\s+(new|new-session|new-window|split-window)\b/;
165
+ const devPattern = /\b(npm\s+run\s+dev|pnpm(?:\s+run)?\s+dev|yarn\s+dev|bun\s+run\s+dev)\b/;
166
+
167
+ const hasBlockedDev = segments.some(segment => {
168
+ const commandWord = getLeadingCommandWord(segment);
169
+ if (!commandWord || !DEV_COMMAND_WORDS.has(commandWord)) {
170
+ return false;
171
+ }
172
+ return devPattern.test(segment) && !tmuxLauncher.test(segment);
173
+ });
174
+
175
+ if (hasBlockedDev) {
176
+ console.error('[Hook] BLOCKED: Dev server must run in tmux for log access');
177
+ console.error('[Hook] Use: tmux new-session -d -s dev "npm run dev"');
178
+ console.error('[Hook] Then: tmux attach -t dev');
179
+ process.exit(2);
180
+ }
181
+ }
182
+ } catch {
183
+ // ignore parse errors and pass through
184
+ }
185
+
186
+ process.stdout.write(raw);
187
+ });
@@ -0,0 +1,28 @@
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 (/\bgit\s+push\b/.test(cmd)) {
20
+ console.error('[Hook] Review changes before push...');
21
+ console.error('[Hook] Continuing with push (remove this hook to add interactive review)');
22
+ }
23
+ } catch {
24
+ // ignore parse errors and pass through
25
+ }
26
+
27
+ process.stdout.write(raw);
28
+ });
@@ -0,0 +1,33 @@
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 (
21
+ process.platform !== 'win32' &&
22
+ !process.env.TMUX &&
23
+ /(npm (install|test)|pnpm (install|test)|yarn (install|test)?|bun (install|test)|cargo build|make\b|docker\b|pytest|vitest|playwright)/.test(cmd)
24
+ ) {
25
+ console.error('[Hook] Consider running in tmux for session persistence');
26
+ console.error('[Hook] tmux new -s dev | tmux attach -t dev');
27
+ }
28
+ } catch {
29
+ // ignore parse errors and pass through
30
+ }
31
+
32
+ process.stdout.write(raw);
33
+ });
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * PreCompact Hook - Save state before context compaction
4
+ *
5
+ * Cross-platform (Windows, macOS, Linux)
6
+ *
7
+ * Runs before Claude compacts context, giving you a chance to
8
+ * preserve important state that might get lost in summarization.
9
+ */
10
+
11
+ const path = require('path');
12
+ const {
13
+ getSessionsDir,
14
+ getDateTimeString,
15
+ getTimeString,
16
+ findFiles,
17
+ ensureDir,
18
+ appendFile,
19
+ log
20
+ } = require('../lib/utils');
21
+
22
+ async function main() {
23
+ const sessionsDir = getSessionsDir();
24
+ const compactionLog = path.join(sessionsDir, 'compaction-log.txt');
25
+
26
+ ensureDir(sessionsDir);
27
+
28
+ // Log compaction event with timestamp
29
+ const timestamp = getDateTimeString();
30
+ appendFile(compactionLog, `[${timestamp}] Context compaction triggered\n`);
31
+
32
+ // If there's an active session file, note the compaction
33
+ const sessions = findFiles(sessionsDir, '*-session.tmp');
34
+
35
+ if (sessions.length > 0) {
36
+ const activeSession = sessions[0].path;
37
+ const timeStr = getTimeString();
38
+ appendFile(activeSession, `\n---\n**[Compaction occurred at ${timeStr}]** - Context was summarized\n`);
39
+ }
40
+
41
+ log('[PreCompact] State saved before compaction');
42
+ process.exit(0);
43
+ }
44
+
45
+ main().catch(err => {
46
+ console.error('[PreCompact] Error:', err.message);
47
+ process.exit(0);
48
+ });
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Backward-compatible doc warning hook entrypoint.
4
+ * Kept for consumers that still reference pre-write-doc-warn.js directly.
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ require('./doc-file-warning.js');
@@ -0,0 +1,168 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Quality Gate Hook
4
+ *
5
+ * Runs lightweight quality checks after file edits.
6
+ * - Targets one file when file_path is provided
7
+ * - Falls back to no-op when language/tooling is unavailable
8
+ *
9
+ * For JS/TS files with Biome, this hook is skipped because
10
+ * post-edit-format.js already runs `biome check --write`.
11
+ * This hook still handles .json/.md files for Biome, and all
12
+ * Prettier / Go / Python checks.
13
+ */
14
+
15
+ 'use strict';
16
+
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+ const { spawnSync } = require('child_process');
20
+
21
+ const { findProjectRoot, detectFormatter, resolveFormatterBin } = require('../lib/resolve-formatter');
22
+
23
+ const MAX_STDIN = 1024 * 1024;
24
+
25
+ /**
26
+ * Execute a command synchronously, returning the spawnSync result.
27
+ *
28
+ * @param {string} command - Executable path or name
29
+ * @param {string[]} args - Arguments to pass
30
+ * @param {string} [cwd] - Working directory (defaults to process.cwd())
31
+ * @returns {import('child_process').SpawnSyncReturns<string>}
32
+ */
33
+ function exec(command, args, cwd = process.cwd()) {
34
+ return spawnSync(command, args, {
35
+ cwd,
36
+ encoding: 'utf8',
37
+ env: process.env,
38
+ timeout: 15000
39
+ });
40
+ }
41
+
42
+ /**
43
+ * Write a message to stderr for logging.
44
+ *
45
+ * @param {string} msg - Message to log
46
+ */
47
+ function log(msg) {
48
+ process.stderr.write(`${msg}\n`);
49
+ }
50
+
51
+ /**
52
+ * Run quality-gate checks for a single file based on its extension.
53
+ * Skips JS/TS files when Biome is configured (handled by post-edit-format).
54
+ *
55
+ * @param {string} filePath - Path to the edited file
56
+ */
57
+ function maybeRunQualityGate(filePath) {
58
+ if (!filePath || !fs.existsSync(filePath)) {
59
+ return;
60
+ }
61
+
62
+ // Resolve to absolute path so projectRoot-relative comparisons work
63
+ filePath = path.resolve(filePath);
64
+
65
+ const ext = path.extname(filePath).toLowerCase();
66
+ const fix = String(process.env.ECC_QUALITY_GATE_FIX || '').toLowerCase() === 'true';
67
+ const strict = String(process.env.ECC_QUALITY_GATE_STRICT || '').toLowerCase() === 'true';
68
+
69
+ if (['.ts', '.tsx', '.js', '.jsx', '.json', '.md'].includes(ext)) {
70
+ const projectRoot = findProjectRoot(path.dirname(filePath));
71
+ const formatter = detectFormatter(projectRoot);
72
+
73
+ if (formatter === 'biome') {
74
+ // JS/TS already handled by post-edit-format via `biome check --write`
75
+ if (['.ts', '.tsx', '.js', '.jsx'].includes(ext)) {
76
+ return;
77
+ }
78
+
79
+ // .json / .md — still need quality gate
80
+ const resolved = resolveFormatterBin(projectRoot, 'biome');
81
+ if (!resolved) return;
82
+ const args = [...resolved.prefix, 'check', filePath];
83
+ if (fix) args.push('--write');
84
+ const result = exec(resolved.bin, args, projectRoot);
85
+ if (result.status !== 0 && strict) {
86
+ log(`[QualityGate] Biome check failed for ${filePath}`);
87
+ }
88
+ return;
89
+ }
90
+
91
+ if (formatter === 'prettier') {
92
+ const resolved = resolveFormatterBin(projectRoot, 'prettier');
93
+ if (!resolved) return;
94
+ const args = [...resolved.prefix, fix ? '--write' : '--check', filePath];
95
+ const result = exec(resolved.bin, args, projectRoot);
96
+ if (result.status !== 0 && strict) {
97
+ log(`[QualityGate] Prettier check failed for ${filePath}`);
98
+ }
99
+ return;
100
+ }
101
+
102
+ // No formatter configured — skip
103
+ return;
104
+ }
105
+
106
+ if (ext === '.go') {
107
+ if (fix) {
108
+ const r = exec('gofmt', ['-w', filePath]);
109
+ if (r.status !== 0 && strict) {
110
+ log(`[QualityGate] gofmt failed for ${filePath}`);
111
+ }
112
+ } else if (strict) {
113
+ const r = exec('gofmt', ['-l', filePath]);
114
+ if (r.status !== 0) {
115
+ log(`[QualityGate] gofmt failed for ${filePath}`);
116
+ } else if (r.stdout && r.stdout.trim()) {
117
+ log(`[QualityGate] gofmt check failed for ${filePath}`);
118
+ }
119
+ }
120
+ return;
121
+ }
122
+
123
+ if (ext === '.py') {
124
+ const args = ['format'];
125
+ if (!fix) args.push('--check');
126
+ args.push(filePath);
127
+ const r = exec('ruff', args);
128
+ if (r.status !== 0 && strict) {
129
+ log(`[QualityGate] Ruff check failed for ${filePath}`);
130
+ }
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Core logic — exported so run-with-flags.js can call directly.
136
+ *
137
+ * @param {string} rawInput - Raw JSON string from stdin
138
+ * @returns {string} The original input (pass-through)
139
+ */
140
+ function run(rawInput) {
141
+ try {
142
+ const input = JSON.parse(rawInput);
143
+ const filePath = String(input.tool_input?.file_path || '');
144
+ maybeRunQualityGate(filePath);
145
+ } catch {
146
+ // Ignore parse errors.
147
+ }
148
+ return rawInput;
149
+ }
150
+
151
+ // ── stdin entry point (backwards-compatible) ────────────────────
152
+ if (require.main === module) {
153
+ let raw = '';
154
+ process.stdin.setEncoding('utf8');
155
+ process.stdin.on('data', chunk => {
156
+ if (raw.length < MAX_STDIN) {
157
+ const remaining = MAX_STDIN - raw.length;
158
+ raw += chunk.substring(0, remaining);
159
+ }
160
+ });
161
+
162
+ process.stdin.on('end', () => {
163
+ const result = run(raw);
164
+ process.stdout.write(result);
165
+ });
166
+ }
167
+
168
+ module.exports = { run };
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ HOOK_ID="${1:-}"
5
+ REL_SCRIPT_PATH="${2:-}"
6
+ PROFILES_CSV="${3:-standard,strict}"
7
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8
+ PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-$(cd "${SCRIPT_DIR}/../.." && pwd)}"
9
+
10
+ # Preserve stdin for passthrough or script execution
11
+ INPUT="$(cat)"
12
+
13
+ if [[ -z "$HOOK_ID" || -z "$REL_SCRIPT_PATH" ]]; then
14
+ printf '%s' "$INPUT"
15
+ exit 0
16
+ fi
17
+
18
+ # Ask Node helper if this hook is enabled
19
+ ENABLED="$(node "${PLUGIN_ROOT}/scripts/hooks/check-hook-enabled.js" "$HOOK_ID" "$PROFILES_CSV" 2>/dev/null || echo yes)"
20
+ if [[ "$ENABLED" != "yes" ]]; then
21
+ printf '%s' "$INPUT"
22
+ exit 0
23
+ fi
24
+
25
+ SCRIPT_PATH="${PLUGIN_ROOT}/${REL_SCRIPT_PATH}"
26
+ if [[ ! -f "$SCRIPT_PATH" ]]; then
27
+ echo "[Hook] Script not found for ${HOOK_ID}: ${SCRIPT_PATH}" >&2
28
+ printf '%s' "$INPUT"
29
+ exit 0
30
+ fi
31
+
32
+ printf '%s' "$INPUT" | "$SCRIPT_PATH"
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Executes a hook script only when enabled by cc4pm hook profile flags.
4
+ *
5
+ * Usage:
6
+ * node run-with-flags.js <hookId> <scriptRelativePath> [profilesCsv]
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const { spawnSync } = require('child_process');
14
+ const { isHookEnabled } = require('../lib/hook-flags');
15
+
16
+ const MAX_STDIN = 1024 * 1024;
17
+
18
+ function readStdinRaw() {
19
+ return new Promise(resolve => {
20
+ let raw = '';
21
+ process.stdin.setEncoding('utf8');
22
+ process.stdin.on('data', chunk => {
23
+ if (raw.length < MAX_STDIN) {
24
+ const remaining = MAX_STDIN - raw.length;
25
+ raw += chunk.substring(0, remaining);
26
+ }
27
+ });
28
+ process.stdin.on('end', () => resolve(raw));
29
+ process.stdin.on('error', () => resolve(raw));
30
+ });
31
+ }
32
+
33
+ function getPluginRoot() {
34
+ if (process.env.CLAUDE_PLUGIN_ROOT && process.env.CLAUDE_PLUGIN_ROOT.trim()) {
35
+ return process.env.CLAUDE_PLUGIN_ROOT;
36
+ }
37
+ return path.resolve(__dirname, '..', '..');
38
+ }
39
+
40
+ async function main() {
41
+ const [, , hookId, relScriptPath, profilesCsv] = process.argv;
42
+ const raw = await readStdinRaw();
43
+
44
+ if (!hookId || !relScriptPath) {
45
+ process.stdout.write(raw);
46
+ process.exit(0);
47
+ }
48
+
49
+ if (!isHookEnabled(hookId, { profiles: profilesCsv })) {
50
+ process.stdout.write(raw);
51
+ process.exit(0);
52
+ }
53
+
54
+ const pluginRoot = getPluginRoot();
55
+ const resolvedRoot = path.resolve(pluginRoot);
56
+ const scriptPath = path.resolve(pluginRoot, relScriptPath);
57
+
58
+ // Prevent path traversal outside the plugin root
59
+ if (!scriptPath.startsWith(resolvedRoot + path.sep)) {
60
+ process.stderr.write(`[Hook] Path traversal rejected for ${hookId}: ${scriptPath}\n`);
61
+ process.stdout.write(raw);
62
+ process.exit(0);
63
+ }
64
+
65
+ if (!fs.existsSync(scriptPath)) {
66
+ process.stderr.write(`[Hook] Script not found for ${hookId}: ${scriptPath}\n`);
67
+ process.stdout.write(raw);
68
+ process.exit(0);
69
+ }
70
+
71
+ // Prefer direct require() when the hook exports a run(rawInput) function.
72
+ // This eliminates one Node.js process spawn (~50-100ms savings per hook).
73
+ //
74
+ // SAFETY: Only require() hooks that export run(). Legacy hooks execute
75
+ // side effects at module scope (stdin listeners, process.exit, main() calls)
76
+ // which would interfere with the parent process or cause double execution.
77
+ let hookModule;
78
+ const src = fs.readFileSync(scriptPath, 'utf8');
79
+ const hasRunExport = /\bmodule\.exports\b/.test(src) && /\brun\b/.test(src);
80
+
81
+ if (hasRunExport) {
82
+ try {
83
+ hookModule = require(scriptPath);
84
+ } catch (requireErr) {
85
+ process.stderr.write(`[Hook] require() failed for ${hookId}: ${requireErr.message}\n`);
86
+ // Fall through to legacy spawnSync path
87
+ }
88
+ }
89
+
90
+ if (hookModule && typeof hookModule.run === 'function') {
91
+ try {
92
+ const output = hookModule.run(raw);
93
+ if (output !== null && output !== undefined) process.stdout.write(output);
94
+ } catch (runErr) {
95
+ process.stderr.write(`[Hook] run() error for ${hookId}: ${runErr.message}\n`);
96
+ process.stdout.write(raw);
97
+ }
98
+ process.exit(0);
99
+ }
100
+
101
+ // Legacy path: spawn a child Node process for hooks without run() export
102
+ const result = spawnSync('node', [scriptPath], {
103
+ input: raw,
104
+ encoding: 'utf8',
105
+ env: process.env,
106
+ cwd: process.cwd(),
107
+ timeout: 30000
108
+ });
109
+
110
+ if (result.stdout) process.stdout.write(result.stdout);
111
+ if (result.stderr) process.stderr.write(result.stderr);
112
+
113
+ const code = Number.isInteger(result.status) ? result.status : 0;
114
+ process.exit(code);
115
+ }
116
+
117
+ main().catch(err => {
118
+ process.stderr.write(`[Hook] run-with-flags error: ${err.message}\n`);
119
+ process.exit(0);
120
+ });
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const MAX_STDIN = 1024 * 1024;
5
+ let raw = '';
6
+ process.stdin.setEncoding('utf8');
7
+ process.stdin.on('data', chunk => {
8
+ if (raw.length < MAX_STDIN) {
9
+ const remaining = MAX_STDIN - raw.length;
10
+ raw += chunk.substring(0, remaining);
11
+ }
12
+ });
13
+ process.stdin.on('end', () => {
14
+ process.stdout.write(raw);
15
+ });