@webpieces/ai-hook-rules 0.2.118 → 0.2.120

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.
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ // Thin shim that delegates to the compiled TypeScript postinstall.
3
+ // This file must be plain JS because it runs during `pnpm install`
4
+ // BEFORE any build step (especially in workspaces).
5
+ //
6
+ // In workspace: the compiled .js doesn't exist yet, so we silently exit.
7
+ // In consumer: the compiled .js exists in the npm package, so we run it.
8
+ 'use strict';
9
+
10
+ const path = require('path');
11
+ const fs = require('fs');
12
+
13
+ const compiled = path.join(__dirname, '..', 'src', 'bin', 'postinstall.js');
14
+ if (fs.existsSync(compiled)) {
15
+ require(compiled).main().catch(function (err) {
16
+ console.error(' [ai-hook-rules] postinstall warning:', err.message);
17
+ });
18
+ }
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@webpieces/ai-hook-rules",
3
- "version": "0.2.118",
3
+ "version": "0.2.120",
4
4
  "description": "Pluggable write-time validation framework for AI coding agents (@webpieces/ai-hook-rules). Claude Code PreToolUse + openclaw before_tool_call adapters share one rule engine.",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
7
- "bin": {
8
- "wp-setup-ai-hooks": "./bin/setup-ai-hooks.sh"
7
+ "scripts": {
8
+ "postinstall": "node bin/postinstall.js"
9
9
  },
10
10
  "exports": {
11
11
  ".": "./src/index.js",
@@ -29,7 +29,7 @@
29
29
  "directory": "packages/tooling/ai-hook-rules"
30
30
  },
31
31
  "dependencies": {
32
- "@webpieces/rules-config": "0.2.118"
32
+ "@webpieces/rules-config": "0.2.120"
33
33
  },
34
34
  "publishConfig": {
35
35
  "access": "public"
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export declare function main(): Promise<void>;
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.main = main;
5
+ const tslib_1 = require("tslib");
6
+ const fs = tslib_1.__importStar(require("fs"));
7
+ const path = tslib_1.__importStar(require("path"));
8
+ const readline = tslib_1.__importStar(require("readline"));
9
+ const BRIDGE_CONTENT = `#!/usr/bin/env node\nrequire('@webpieces/ai-hook-rules/claude-code').main();\n`;
10
+ function findProjectRoot() {
11
+ // Walk up from this file's location to escape node_modules
12
+ // e.g. /project/node_modules/@webpieces/ai-hook-rules/src/bin/postinstall.js -> /project
13
+ let dir = __dirname;
14
+ while (dir !== path.dirname(dir)) {
15
+ dir = path.dirname(dir);
16
+ const base = path.basename(dir);
17
+ if (base === 'node_modules') {
18
+ return path.dirname(dir);
19
+ }
20
+ }
21
+ return null;
22
+ }
23
+ function createBridgeFile(projectRoot) {
24
+ const hooksDir = path.join(projectRoot, '.webpieces', 'ai-hooks');
25
+ const bridgePath = path.join(hooksDir, 'claude-code-hook.js');
26
+ fs.mkdirSync(hooksDir, { recursive: true });
27
+ fs.writeFileSync(bridgePath, BRIDGE_CONTENT);
28
+ fs.chmodSync(bridgePath, 0o755);
29
+ console.log(' [ai-hook-rules] Created .webpieces/ai-hooks/claude-code-hook.js');
30
+ }
31
+ function seedConfigIfMissing(projectRoot) {
32
+ const configPath = path.join(projectRoot, 'webpieces.ai-hooks.json');
33
+ if (fs.existsSync(configPath))
34
+ return;
35
+ const templatePath = path.join(__dirname, '..', '..', 'templates', 'webpieces.ai-hooks.seed.json');
36
+ if (!fs.existsSync(templatePath))
37
+ return;
38
+ fs.copyFileSync(templatePath, configPath);
39
+ console.log(' [ai-hook-rules] Created webpieces.ai-hooks.json (default config)');
40
+ }
41
+ function settingsAlreadyHasHook(settingsPath) {
42
+ if (!fs.existsSync(settingsPath))
43
+ return false;
44
+ const content = fs.readFileSync(settingsPath, 'utf8');
45
+ return content.includes('claude-code-hook.js');
46
+ }
47
+ function loadTemplate() {
48
+ const templatePath = path.join(__dirname, '..', '..', 'templates', 'claude-settings-hook.json');
49
+ const raw = fs.readFileSync(templatePath, 'utf8');
50
+ return JSON.parse(raw);
51
+ }
52
+ function mergeHookIntoSettings(settingsPath) {
53
+ const template = loadTemplate();
54
+ const hookEntry = template.hooks.PreToolUse[0];
55
+ let settings = {};
56
+ if (fs.existsSync(settingsPath)) {
57
+ settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
58
+ }
59
+ if (!settings.hooks) {
60
+ settings.hooks = {};
61
+ }
62
+ if (!Array.isArray(settings.hooks.PreToolUse)) {
63
+ settings.hooks.PreToolUse = [];
64
+ }
65
+ settings.hooks.PreToolUse.push(hookEntry);
66
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 4) + '\n');
67
+ }
68
+ function backupSettings(settingsPath) {
69
+ if (fs.existsSync(settingsPath)) {
70
+ const bakPath = settingsPath + '.bak';
71
+ fs.copyFileSync(settingsPath, bakPath);
72
+ console.log(' [ai-hook-rules] Backed up .claude/settings.json to .claude/settings.json.bak');
73
+ }
74
+ }
75
+ function printManualInstructions() {
76
+ console.log('');
77
+ console.log(' [ai-hook-rules] To enable AI code-quality hooks, add this to .claude/settings.json:');
78
+ console.log('');
79
+ console.log(' {');
80
+ console.log(' "hooks": {');
81
+ console.log(' "PreToolUse": [{');
82
+ console.log(' "matcher": "Write|Edit|MultiEdit|Bash",');
83
+ console.log(' "hooks": [{');
84
+ console.log(' "type": "command",');
85
+ console.log(' "command": "node .webpieces/ai-hooks/claude-code-hook.js"');
86
+ console.log(' }]');
87
+ console.log(' }]');
88
+ console.log(' }');
89
+ console.log(' }');
90
+ console.log('');
91
+ }
92
+ function promptUser(settingsPath) {
93
+ return new Promise((resolve) => {
94
+ const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
95
+ if (!isInteractive) {
96
+ console.log(' [ai-hook-rules] Non-interactive terminal detected, skipping .claude/settings.json setup.');
97
+ printManualInstructions();
98
+ resolve();
99
+ return;
100
+ }
101
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
102
+ console.log('');
103
+ console.log(' [ai-hook-rules] Would like to add a PreToolUse hook to .claude/settings.json');
104
+ console.log(' This enables AI code-quality validation (rules configured in webpieces.ai-hooks.json).');
105
+ if (fs.existsSync(settingsPath)) {
106
+ console.log(' Your current settings.json will be backed up to .claude/settings.json.bak');
107
+ }
108
+ console.log('');
109
+ rl.question(' Proceed? [y/N] ', (answer) => {
110
+ rl.close();
111
+ const yes = answer.trim().toLowerCase() === 'y' || answer.trim().toLowerCase() === 'yes';
112
+ if (yes) {
113
+ backupSettings(settingsPath);
114
+ mergeHookIntoSettings(settingsPath);
115
+ console.log(' [ai-hook-rules] Added PreToolUse hook to .claude/settings.json');
116
+ }
117
+ else {
118
+ printManualInstructions();
119
+ }
120
+ resolve();
121
+ });
122
+ });
123
+ }
124
+ async function main() {
125
+ const projectRoot = findProjectRoot();
126
+ if (!projectRoot) {
127
+ // Not running from node_modules (maybe local dev / workspace) — skip
128
+ return;
129
+ }
130
+ // 1. Always create the bridge file
131
+ createBridgeFile(projectRoot);
132
+ // 2. Seed config if missing
133
+ seedConfigIfMissing(projectRoot);
134
+ // 3. Check if .claude/ exists — if not, skip settings.json
135
+ const claudeDir = path.join(projectRoot, '.claude');
136
+ if (!fs.existsSync(claudeDir)) {
137
+ return;
138
+ }
139
+ // 4. Check if settings.json already has the hook — if yes, done
140
+ const settingsPath = path.join(claudeDir, 'settings.json');
141
+ if (settingsAlreadyHasHook(settingsPath)) {
142
+ return;
143
+ }
144
+ // 5. Prompt user to add the hook
145
+ await promptUser(settingsPath);
146
+ }
147
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions
148
+ if (require.main === module) {
149
+ main().catch((err) => {
150
+ // Fail open — don't break pnpm install if postinstall crashes
151
+ console.error(' [ai-hook-rules] postinstall warning:', err.message);
152
+ });
153
+ }
154
+ //# sourceMappingURL=postinstall.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"postinstall.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/postinstall.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport * as readline from 'readline';\n\nconst BRIDGE_CONTENT = `#!/usr/bin/env node\\nrequire('@webpieces/ai-hook-rules/claude-code').main();\\n`;\n\nfunction findProjectRoot(): string | null {\n // Walk up from this file's location to escape node_modules\n // e.g. /project/node_modules/@webpieces/ai-hook-rules/src/bin/postinstall.js -> /project\n let dir = __dirname;\n while (dir !== path.dirname(dir)) {\n dir = path.dirname(dir);\n const base = path.basename(dir);\n if (base === 'node_modules') {\n return path.dirname(dir);\n }\n }\n return null;\n}\n\nfunction createBridgeFile(projectRoot: string): void {\n const hooksDir = path.join(projectRoot, '.webpieces', 'ai-hooks');\n const bridgePath = path.join(hooksDir, 'claude-code-hook.js');\n\n fs.mkdirSync(hooksDir, { recursive: true });\n fs.writeFileSync(bridgePath, BRIDGE_CONTENT);\n fs.chmodSync(bridgePath, 0o755);\n console.log(' [ai-hook-rules] Created .webpieces/ai-hooks/claude-code-hook.js');\n}\n\nfunction seedConfigIfMissing(projectRoot: string): void {\n const configPath = path.join(projectRoot, 'webpieces.ai-hooks.json');\n if (fs.existsSync(configPath)) return;\n\n const templatePath = path.join(__dirname, '..', '..', 'templates', 'webpieces.ai-hooks.seed.json');\n if (!fs.existsSync(templatePath)) return;\n\n fs.copyFileSync(templatePath, configPath);\n console.log(' [ai-hook-rules] Created webpieces.ai-hooks.json (default config)');\n}\n\nfunction settingsAlreadyHasHook(settingsPath: string): boolean {\n if (!fs.existsSync(settingsPath)) return false;\n\n const content = fs.readFileSync(settingsPath, 'utf8');\n return content.includes('claude-code-hook.js');\n}\n\ninterface HookEntry {\n matcher: string;\n hooks: Array<{ type: string; command: string }>;\n}\n\ninterface SettingsTemplate {\n hooks: {\n PreToolUse: HookEntry[];\n };\n}\n\ninterface ClaudeSettings {\n hooks?: {\n PreToolUse?: HookEntry[];\n };\n [key: string]: string | number | boolean | object | null | undefined;\n}\n\nfunction loadTemplate(): SettingsTemplate {\n const templatePath = path.join(__dirname, '..', '..', 'templates', 'claude-settings-hook.json');\n const raw = fs.readFileSync(templatePath, 'utf8');\n return JSON.parse(raw) as SettingsTemplate;\n}\n\nfunction mergeHookIntoSettings(settingsPath: string): void {\n const template = loadTemplate();\n const hookEntry = template.hooks.PreToolUse[0];\n\n let settings: ClaudeSettings = {};\n if (fs.existsSync(settingsPath)) {\n settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')) as ClaudeSettings;\n }\n\n if (!settings.hooks) {\n settings.hooks = {};\n }\n if (!Array.isArray(settings.hooks.PreToolUse)) {\n settings.hooks.PreToolUse = [];\n }\n\n settings.hooks.PreToolUse.push(hookEntry);\n fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 4) + '\\n');\n}\n\nfunction backupSettings(settingsPath: string): void {\n if (fs.existsSync(settingsPath)) {\n const bakPath = settingsPath + '.bak';\n fs.copyFileSync(settingsPath, bakPath);\n console.log(' [ai-hook-rules] Backed up .claude/settings.json to .claude/settings.json.bak');\n }\n}\n\nfunction printManualInstructions(): void {\n console.log('');\n console.log(' [ai-hook-rules] To enable AI code-quality hooks, add this to .claude/settings.json:');\n console.log('');\n console.log(' {');\n console.log(' \"hooks\": {');\n console.log(' \"PreToolUse\": [{');\n console.log(' \"matcher\": \"Write|Edit|MultiEdit|Bash\",');\n console.log(' \"hooks\": [{');\n console.log(' \"type\": \"command\",');\n console.log(' \"command\": \"node .webpieces/ai-hooks/claude-code-hook.js\"');\n console.log(' }]');\n console.log(' }]');\n console.log(' }');\n console.log(' }');\n console.log('');\n}\n\nfunction promptUser(settingsPath: string): Promise<void> {\n return new Promise((resolve: () => void) => {\n const isInteractive = process.stdin.isTTY && process.stdout.isTTY;\n if (!isInteractive) {\n console.log(' [ai-hook-rules] Non-interactive terminal detected, skipping .claude/settings.json setup.');\n printManualInstructions();\n resolve();\n return;\n }\n\n const rl = readline.createInterface({ input: process.stdin, output: process.stdout });\n\n console.log('');\n console.log(' [ai-hook-rules] Would like to add a PreToolUse hook to .claude/settings.json');\n console.log(' This enables AI code-quality validation (rules configured in webpieces.ai-hooks.json).');\n if (fs.existsSync(settingsPath)) {\n console.log(' Your current settings.json will be backed up to .claude/settings.json.bak');\n }\n console.log('');\n\n rl.question(' Proceed? [y/N] ', (answer: string) => {\n rl.close();\n const yes = answer.trim().toLowerCase() === 'y' || answer.trim().toLowerCase() === 'yes';\n if (yes) {\n backupSettings(settingsPath);\n mergeHookIntoSettings(settingsPath);\n console.log(' [ai-hook-rules] Added PreToolUse hook to .claude/settings.json');\n } else {\n printManualInstructions();\n }\n resolve();\n });\n });\n}\n\nexport async function main(): Promise<void> {\n const projectRoot = findProjectRoot();\n if (!projectRoot) {\n // Not running from node_modules (maybe local dev / workspace) — skip\n return;\n }\n\n // 1. Always create the bridge file\n createBridgeFile(projectRoot);\n\n // 2. Seed config if missing\n seedConfigIfMissing(projectRoot);\n\n // 3. Check if .claude/ exists — if not, skip settings.json\n const claudeDir = path.join(projectRoot, '.claude');\n if (!fs.existsSync(claudeDir)) {\n return;\n }\n\n // 4. Check if settings.json already has the hook — if yes, done\n const settingsPath = path.join(claudeDir, 'settings.json');\n if (settingsAlreadyHasHook(settingsPath)) {\n return;\n }\n\n // 5. Prompt user to add the hook\n await promptUser(settingsPath);\n}\n\n// eslint-disable-next-line @webpieces/no-unmanaged-exceptions\nif (require.main === module) {\n main().catch((err: Error) => {\n // Fail open — don't break pnpm install if postinstall crashes\n console.error(' [ai-hook-rules] postinstall warning:', err.message);\n });\n}\n"]}
@@ -1,137 +0,0 @@
1
- #!/bin/bash
2
- set -e
3
-
4
- # setup-ai-hooks.sh — Wires the @webpieces/ai-hook-rules framework into a project.
5
- #
6
- # For Claude Code: creates .webpieces/ai-hooks/claude-code-hook.js bootstrap,
7
- # seeds webpieces.ai-hooks.json, and merges .claude/settings.json.
8
- #
9
- # Usage:
10
- # npx wp-setup-ai-hooks
11
-
12
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
13
-
14
- # Detect workspace vs consumer
15
- if [[ "$SCRIPT_DIR" == *"node_modules/@webpieces/ai-hook-rules"* ]]; then
16
- # Running in consumer project (from node_modules)
17
- PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
18
- AI_HOOKS_PKG="@webpieces/ai-hook-rules"
19
- ADAPTER_REQUIRE="require('${AI_HOOKS_PKG}/claude-code').main();"
20
- TEMPLATES_DIR="$SCRIPT_DIR/../templates"
21
- else
22
- # Running in webpieces-ts workspace
23
- PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
24
- ADAPTER_REQUIRE="require('${PROJECT_ROOT}/dist/packages/tooling/ai-hook-rules/src/adapters/claude-code-hook').main();"
25
- TEMPLATES_DIR="$SCRIPT_DIR/../templates"
26
- fi
27
-
28
- cd "$PROJECT_ROOT" || exit 1
29
-
30
- echo ""
31
- echo "🔧 Setting up @webpieces/ai-hook-rules..."
32
- echo " Project root: $PROJECT_ROOT"
33
- echo ""
34
-
35
- # 1. Create .webpieces/ai-hooks/
36
- mkdir -p .webpieces/ai-hooks
37
-
38
- # 2. Generate the bootstrap
39
- BOOTSTRAP=".webpieces/ai-hooks/claude-code-hook.js"
40
- cat > "$BOOTSTRAP" <<JSEOF
41
- #!/usr/bin/env node
42
- ${ADAPTER_REQUIRE}
43
- JSEOF
44
- chmod +x "$BOOTSTRAP"
45
- echo " ✅ Created $BOOTSTRAP"
46
-
47
- # 3. Seed webpieces.ai-hooks.json if missing
48
- if [ ! -f "webpieces.ai-hooks.json" ]; then
49
- cp "$TEMPLATES_DIR/webpieces.ai-hooks.seed.json" "webpieces.ai-hooks.json"
50
- echo " ✅ Created webpieces.ai-hooks.json (default config)"
51
- else
52
- echo " ℹ️ webpieces.ai-hooks.json already exists (keeping yours)"
53
- fi
54
-
55
- # 4. Add .webpieces/ to .gitignore if missing
56
- if [ -f ".gitignore" ]; then
57
- if ! grep -q "^\.webpieces/" ".gitignore" 2>/dev/null; then
58
- echo "" >> .gitignore
59
- echo "# Generated @webpieces/ai-hook-rules artifacts" >> .gitignore
60
- echo ".webpieces/" >> .gitignore
61
- echo " ✅ Added .webpieces/ to .gitignore"
62
- fi
63
- else
64
- echo ".webpieces/" > .gitignore
65
- echo " ✅ Created .gitignore with .webpieces/"
66
- fi
67
-
68
- # 5. Create or merge .claude/settings.json
69
- mkdir -p .claude
70
-
71
- SETTINGS=".claude/settings.json"
72
- HOOK_COMMAND="node .webpieces/ai-hooks/claude-code-hook.js"
73
-
74
- if [ ! -f "$SETTINGS" ]; then
75
- # Fresh settings file
76
- cat > "$SETTINGS" <<JSONEOF
77
- {
78
- "hooks": {
79
- "PreToolUse": [
80
- {
81
- "matcher": "Write|Edit|MultiEdit",
82
- "hooks": [
83
- {
84
- "type": "command",
85
- "command": "${HOOK_COMMAND}"
86
- }
87
- ]
88
- }
89
- ]
90
- }
91
- }
92
- JSONEOF
93
- echo " ✅ Created $SETTINGS with PreToolUse hook"
94
- else
95
- # Settings exist — check if hook is already wired
96
- if grep -q "claude-code-hook.js" "$SETTINGS" 2>/dev/null; then
97
- echo " ℹ️ $SETTINGS already has the ai-hooks hook wired"
98
- else
99
- echo ""
100
- echo " ⚠️ $SETTINGS exists but doesn't have the ai-hooks hook."
101
- echo " Please manually add this to your hooks.PreToolUse array:"
102
- echo ""
103
- echo ' {'
104
- echo ' "matcher": "Write|Edit|MultiEdit",'
105
- echo ' "hooks": [{'
106
- echo ' "type": "command",'
107
- echo " \"command\": \"${HOOK_COMMAND}\""
108
- echo ' }]'
109
- echo ' }'
110
- echo ""
111
- fi
112
- fi
113
-
114
- # 6. Smoke test
115
- echo ""
116
- echo "🧪 Running smoke test..."
117
- SMOKE_RESULT=$(echo '{"tool_name":"Write","tool_input":{"file_path":"'${PROJECT_ROOT}'/x.ts","content":"const x: any = 1;"}}' | node "$BOOTSTRAP" 2>&1; echo "EXIT:$?")
118
- SMOKE_EXIT=$(echo "$SMOKE_RESULT" | grep "EXIT:" | sed 's/EXIT://')
119
-
120
- if [ "$SMOKE_EXIT" = "2" ]; then
121
- echo " ✅ Smoke test passed (exit 2 = correctly blocked)"
122
- else
123
- echo " ⚠️ Smoke test returned exit $SMOKE_EXIT (expected 2). The hook may not be working."
124
- echo " Check that the project built successfully: nx build ai-hooks"
125
- fi
126
-
127
- echo ""
128
- echo "🎉 Setup complete!"
129
- echo ""
130
- echo " Claude Code: restart your session for the hook to activate."
131
- echo " Edit webpieces.ai-hooks.json to toggle rules or tune options."
132
- echo ""
133
- echo " Openclaw: install globally with:"
134
- echo " openclaw plugins install @webpieces/ai-hook-rules"
135
- echo " openclaw plugins enable @webpieces/ai-hook-rules"
136
- echo " Then drop webpieces.ai-hooks.json into any project."
137
- echo ""