@webpieces/ai-hook-rules 0.2.119 → 0.2.121
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.
- package/package.json +3 -4
- package/src/bin/postinstall.d.ts +2 -0
- package/src/bin/postinstall.js +154 -0
- package/src/bin/postinstall.js.map +1 -0
- package/bin/setup-ai-hooks.sh +0 -137
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/ai-hook-rules",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.121",
|
|
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
7
|
"bin": {
|
|
8
|
-
"wp-setup-ai-hooks": "./bin/
|
|
8
|
+
"wp-setup-ai-hooks": "./src/bin/postinstall.js"
|
|
9
9
|
},
|
|
10
10
|
"exports": {
|
|
11
11
|
".": "./src/index.js",
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
},
|
|
17
17
|
"files": [
|
|
18
18
|
"src/**/*",
|
|
19
|
-
"bin/**/*",
|
|
20
19
|
"templates/**/*",
|
|
21
20
|
"openclaw.plugin.json",
|
|
22
21
|
"README.md"
|
|
@@ -29,7 +28,7 @@
|
|
|
29
28
|
"directory": "packages/tooling/ai-hook-rules"
|
|
30
29
|
},
|
|
31
30
|
"dependencies": {
|
|
32
|
-
"@webpieces/rules-config": "0.2.
|
|
31
|
+
"@webpieces/rules-config": "0.2.121"
|
|
33
32
|
},
|
|
34
33
|
"publishConfig": {
|
|
35
34
|
"access": "public"
|
|
@@ -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"]}
|
package/bin/setup-ai-hooks.sh
DELETED
|
@@ -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 ""
|