@etus/bhono-app 0.1.6 → 0.1.7
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 -2
- package/templates/base/.claude/commands/check-skill-rules.md +112 -29
- package/templates/base/.claude/commands/linear/implement-issue.md +383 -55
- package/templates/base/.claude/commands/ship.md +77 -13
- package/templates/base/.claude/hooks/package-lock.json +0 -419
- package/templates/base/.claude/hooks/skill-activation-prompt.ts +185 -113
- package/templates/base/.claude/hooks/skill-tool-guard.sh +6 -0
- package/templates/base/.claude/hooks/skill-tool-guard.ts +198 -0
- package/templates/base/.claude/scripts/validate-skill-rules.sh +55 -32
- package/templates/base/.claude/settings.json +18 -11
- package/templates/base/.claude/skills/skill-rules.json +326 -173
- package/templates/base/.env.example +3 -0
- package/templates/base/README.md +9 -7
- package/templates/base/config/eslint.config.js +1 -0
- package/templates/base/config/wrangler.json +16 -17
- package/templates/base/docs/SETUP-GUIDE.md +566 -0
- package/templates/base/docs/architecture/README.md +162 -8
- package/templates/base/docs/architecture/api-catalog.md +575 -0
- package/templates/base/docs/architecture/c4-component.md +309 -0
- package/templates/base/docs/architecture/c4-container.md +183 -0
- package/templates/base/docs/architecture/c4-context.md +106 -0
- package/templates/base/docs/architecture/dependencies.md +327 -0
- package/templates/base/docs/architecture/tech-debt.md +184 -0
- package/templates/base/package.json +20 -15
- package/templates/base/scripts/capture-prod-session.ts +2 -2
- package/templates/base/scripts/sync-template.sh +104 -0
- package/templates/base/src/server/db/sql.ts +24 -4
- package/templates/base/src/server/index.ts +1 -0
- package/templates/base/src/server/lib/audited-db.ts +10 -10
- package/templates/base/src/server/middleware/account.ts +1 -1
- package/templates/base/src/server/middleware/auth.ts +11 -11
- package/templates/base/src/server/middleware/rate-limit.ts +3 -6
- package/templates/base/src/server/routes/auth/handlers.ts +5 -5
- package/templates/base/src/server/routes/auth/test-login.ts +9 -9
- package/templates/base/src/server/routes/index.ts +9 -0
- package/templates/base/src/server/routes/invitations/handlers.ts +6 -6
- package/templates/base/src/server/routes/openapi.ts +1 -1
- package/templates/base/src/server/services/accounts.ts +9 -9
- package/templates/base/src/server/services/audits.ts +12 -12
- package/templates/base/src/server/services/auth.ts +15 -15
- package/templates/base/src/server/services/invitations.ts +16 -16
- package/templates/base/src/server/services/users.ts +13 -13
- package/templates/base/src/shared/types/api.ts +66 -198
- package/templates/base/tests/e2e/auth.setup.ts +1 -1
- package/templates/base/tests/unit/server/auth/guards.test.ts +1 -1
- package/templates/base/tests/unit/server/middleware/auth.test.ts +273 -0
- package/templates/base/tests/unit/server/routes/auth/handlers.test.ts +111 -0
- package/templates/base/tests/unit/server/routes/users/handlers.test.ts +69 -5
- package/templates/base/tests/unit/server/services/accounts.test.ts +148 -0
- package/templates/base/tests/unit/server/services/audits.test.ts +219 -0
- package/templates/base/tests/unit/server/services/auth.test.ts +480 -3
- package/templates/base/tests/unit/server/services/invitations.test.ts +178 -0
- package/templates/base/tests/unit/server/services/users.test.ts +363 -8
- package/templates/base/tests/unit/shared/schemas.test.ts +1 -1
- package/templates/base/vite.config.ts +3 -1
- package/templates/base/.github/workflows/test.yml +0 -127
- package/templates/base/.husky/pre-push +0 -26
- package/templates/base/auth-setup-error.png +0 -0
- package/templates/base/pnpm-lock.yaml +0 -8052
- package/templates/base/tests/e2e/_auth/.gitkeep +0 -0
- package/templates/base/tsconfig.tsbuildinfo +0 -1
|
@@ -1,142 +1,214 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Skill Activation Prompt Hook (UserPromptSubmit)
|
|
4
|
+
*
|
|
5
|
+
* Analyzes user prompts and suggests relevant skills based on:
|
|
6
|
+
* - Keywords (exact match, case-insensitive)
|
|
7
|
+
* - Intent patterns (regex-based semantic matching)
|
|
8
|
+
*
|
|
9
|
+
* Features:
|
|
10
|
+
* - Skips short prompts (< 15 chars) to avoid noise
|
|
11
|
+
* - Skips slash commands (already invoking a skill)
|
|
12
|
+
* - Groups suggestions by priority level
|
|
13
|
+
* - Returns structured JSON for Claude Code
|
|
14
|
+
*/
|
|
15
|
+
|
|
2
16
|
import { readFileSync } from 'fs';
|
|
3
17
|
import { join, dirname } from 'path';
|
|
4
18
|
import { fileURLToPath } from 'url';
|
|
5
19
|
|
|
6
|
-
const
|
|
7
|
-
|
|
20
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
|
|
22
|
+
// Minimum prompt length to trigger skill detection
|
|
23
|
+
const MIN_PROMPT_LENGTH = 15;
|
|
8
24
|
|
|
9
25
|
interface HookInput {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
26
|
+
session_id: string;
|
|
27
|
+
transcript_path?: string;
|
|
28
|
+
cwd?: string;
|
|
29
|
+
permission_mode?: string;
|
|
30
|
+
prompt: string;
|
|
15
31
|
}
|
|
16
32
|
|
|
17
33
|
interface PromptTriggers {
|
|
18
|
-
|
|
19
|
-
|
|
34
|
+
keywords?: string[];
|
|
35
|
+
intentPatterns?: string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface ToolGuard {
|
|
39
|
+
tool: string;
|
|
40
|
+
patterns: string[];
|
|
20
41
|
}
|
|
21
42
|
|
|
22
43
|
interface SkillRule {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
44
|
+
type: 'guardrail' | 'domain';
|
|
45
|
+
enforcement: 'block' | 'suggest' | 'warn';
|
|
46
|
+
priority: 'critical' | 'high' | 'medium' | 'low';
|
|
47
|
+
description?: string;
|
|
48
|
+
promptTriggers?: PromptTriggers;
|
|
49
|
+
toolGuards?: ToolGuard[];
|
|
27
50
|
}
|
|
28
51
|
|
|
29
52
|
interface SkillRules {
|
|
30
|
-
|
|
31
|
-
|
|
53
|
+
version: string;
|
|
54
|
+
skills: Record<string, SkillRule>;
|
|
32
55
|
}
|
|
33
56
|
|
|
34
57
|
interface MatchedSkill {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
58
|
+
name: string;
|
|
59
|
+
matchType: 'keyword' | 'intent';
|
|
60
|
+
config: SkillRule;
|
|
38
61
|
}
|
|
39
62
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
63
|
+
function shouldSkipPrompt(prompt: string): boolean {
|
|
64
|
+
// Skip very short prompts
|
|
65
|
+
if (prompt.length < MIN_PROMPT_LENGTH) {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Skip slash commands (user is already invoking a skill)
|
|
70
|
+
if (prompt.trim().startsWith('/')) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Skip common non-task prompts
|
|
75
|
+
const skipPatterns = [
|
|
76
|
+
/^(hi|hello|hey|thanks|thank you|ok|okay|yes|no|sure)[\s!.]*$/i,
|
|
77
|
+
/^(what|how|why|when|where|who|can you|could you|would you).*\?$/i, // Pure questions without action
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
// Don't skip questions that imply action
|
|
81
|
+
const actionQuestionPatterns = [
|
|
82
|
+
/can you (create|build|make|implement|add|fix|update|deploy)/i,
|
|
83
|
+
/how (do i|to|can i) (create|build|make|implement|add|fix|update|deploy)/i,
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
const isActionQuestion = actionQuestionPatterns.some(p => p.test(prompt));
|
|
87
|
+
if (isActionQuestion) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return skipPatterns.some(p => p.test(prompt.trim()));
|
|
92
|
+
}
|
|
45
93
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
94
|
+
function findMatches(prompt: string, rules: SkillRules): MatchedSkill[] {
|
|
95
|
+
const matches: MatchedSkill[] = [];
|
|
96
|
+
const promptLower = prompt.toLowerCase();
|
|
50
97
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
// Keyword matching
|
|
68
|
-
if (triggers.keywords) {
|
|
69
|
-
const keywordMatch = triggers.keywords.some(kw =>
|
|
70
|
-
prompt.includes(kw.toLowerCase())
|
|
71
|
-
);
|
|
72
|
-
if (keywordMatch) {
|
|
73
|
-
matchedSkills.push({ name: skillName, matchType: 'keyword', config });
|
|
74
|
-
continue;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Intent pattern matching
|
|
79
|
-
if (triggers.intentPatterns) {
|
|
80
|
-
const intentMatch = triggers.intentPatterns.some(pattern => {
|
|
81
|
-
const regex = new RegExp(pattern, 'i');
|
|
82
|
-
return regex.test(prompt);
|
|
83
|
-
});
|
|
84
|
-
if (intentMatch) {
|
|
85
|
-
matchedSkills.push({ name: skillName, matchType: 'intent', config });
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
98
|
+
for (const [skillName, config] of Object.entries(rules.skills)) {
|
|
99
|
+
const triggers = config.promptTriggers;
|
|
100
|
+
if (!triggers) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Keyword matching (exact, case-insensitive)
|
|
105
|
+
if (triggers.keywords) {
|
|
106
|
+
const keywordMatch = triggers.keywords.some(kw =>
|
|
107
|
+
promptLower.includes(kw.toLowerCase())
|
|
108
|
+
);
|
|
109
|
+
if (keywordMatch) {
|
|
110
|
+
matches.push({ name: skillName, matchType: 'keyword', config });
|
|
111
|
+
continue; // Don't check patterns if keyword matched
|
|
112
|
+
}
|
|
113
|
+
}
|
|
89
114
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const high = matchedSkills.filter(s => s.config.priority === 'high');
|
|
99
|
-
const medium = matchedSkills.filter(s => s.config.priority === 'medium');
|
|
100
|
-
const low = matchedSkills.filter(s => s.config.priority === 'low');
|
|
101
|
-
|
|
102
|
-
if (critical.length > 0) {
|
|
103
|
-
output += '⚠️ CRITICAL SKILLS (REQUIRED):\n';
|
|
104
|
-
critical.forEach(s => output += ` → ${s.name}\n`);
|
|
105
|
-
output += '\n';
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if (high.length > 0) {
|
|
109
|
-
output += '📚 RECOMMENDED SKILLS:\n';
|
|
110
|
-
high.forEach(s => output += ` → ${s.name}\n`);
|
|
111
|
-
output += '\n';
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
if (medium.length > 0) {
|
|
115
|
-
output += '💡 SUGGESTED SKILLS:\n';
|
|
116
|
-
medium.forEach(s => output += ` → ${s.name}\n`);
|
|
117
|
-
output += '\n';
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if (low.length > 0) {
|
|
121
|
-
output += '📌 OPTIONAL SKILLS:\n';
|
|
122
|
-
low.forEach(s => output += ` → ${s.name}\n`);
|
|
123
|
-
output += '\n';
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
output += 'ACTION: Use Skill tool BEFORE responding\n';
|
|
127
|
-
output += '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
|
|
128
|
-
|
|
129
|
-
console.log(output);
|
|
115
|
+
// Intent pattern matching (regex)
|
|
116
|
+
if (triggers.intentPatterns) {
|
|
117
|
+
const intentMatch = triggers.intentPatterns.some(pattern => {
|
|
118
|
+
try {
|
|
119
|
+
const regex = new RegExp(pattern, 'i');
|
|
120
|
+
return regex.test(prompt);
|
|
121
|
+
} catch {
|
|
122
|
+
return false;
|
|
130
123
|
}
|
|
124
|
+
});
|
|
125
|
+
if (intentMatch) {
|
|
126
|
+
matches.push({ name: skillName, matchType: 'intent', config });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return matches;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function formatOutput(matches: MatchedSkill[]): string {
|
|
135
|
+
// Group by priority
|
|
136
|
+
const byPriority = {
|
|
137
|
+
critical: matches.filter(s => s.config.priority === 'critical'),
|
|
138
|
+
high: matches.filter(s => s.config.priority === 'high'),
|
|
139
|
+
medium: matches.filter(s => s.config.priority === 'medium'),
|
|
140
|
+
low: matches.filter(s => s.config.priority === 'low'),
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const lines: string[] = ['🎯 SKILL ACTIVATION'];
|
|
131
144
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
145
|
+
if (byPriority.critical.length > 0) {
|
|
146
|
+
lines.push(`⚠️ REQUIRED: ${byPriority.critical.map(s => s.name).join(', ')}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (byPriority.high.length > 0) {
|
|
150
|
+
lines.push(`📚 RECOMMENDED: ${byPriority.high.map(s => s.name).join(', ')}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (byPriority.medium.length > 0) {
|
|
154
|
+
lines.push(`💡 SUGGESTED: ${byPriority.medium.map(s => s.name).join(', ')}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (byPriority.low.length > 0) {
|
|
158
|
+
lines.push(`📌 OPTIONAL: ${byPriority.low.map(s => s.name).join(', ')}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
lines.push('→ Use Skill tool BEFORE responding');
|
|
162
|
+
|
|
163
|
+
return lines.join('\n');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function main() {
|
|
167
|
+
try {
|
|
168
|
+
const input = readFileSync(0, 'utf-8');
|
|
169
|
+
const data: HookInput = JSON.parse(input);
|
|
170
|
+
const prompt = data.prompt;
|
|
171
|
+
|
|
172
|
+
// Early exit conditions
|
|
173
|
+
if (shouldSkipPrompt(prompt)) {
|
|
174
|
+
process.exit(0);
|
|
136
175
|
}
|
|
176
|
+
|
|
177
|
+
// Load skill rules
|
|
178
|
+
const rulesPath = join(__dirname, '..', 'skills', 'skill-rules.json');
|
|
179
|
+
let rules: SkillRules;
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
rules = JSON.parse(readFileSync(rulesPath, 'utf-8'));
|
|
183
|
+
} catch {
|
|
184
|
+
// Silent exit if rules file not found
|
|
185
|
+
process.exit(0);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Find matching skills
|
|
189
|
+
const matches = findMatches(prompt, rules);
|
|
190
|
+
|
|
191
|
+
// No matches = no output
|
|
192
|
+
if (matches.length === 0) {
|
|
193
|
+
process.exit(0);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Format and output
|
|
197
|
+
const message = formatOutput(matches);
|
|
198
|
+
|
|
199
|
+
const output = {
|
|
200
|
+
hookSpecificOutput: {
|
|
201
|
+
hookEventName: 'UserPromptSubmit',
|
|
202
|
+
additionalContext: `<system-reminder>${message}</system-reminder>`
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
console.log(JSON.stringify(output));
|
|
207
|
+
process.exit(0);
|
|
208
|
+
} catch (err) {
|
|
209
|
+
// Silent failure - don't break user experience
|
|
210
|
+
process.exit(0);
|
|
211
|
+
}
|
|
137
212
|
}
|
|
138
213
|
|
|
139
|
-
main()
|
|
140
|
-
console.error('Uncaught error:', err);
|
|
141
|
-
process.exit(1);
|
|
142
|
-
});
|
|
214
|
+
main();
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Skill Tool Guard Hook (PreToolUse)
|
|
4
|
+
*
|
|
5
|
+
* Recommends or blocks tool usage based on skill rules.
|
|
6
|
+
* Reads toolGuards from skill-rules.json to determine which
|
|
7
|
+
* skills should be used before specific tool patterns.
|
|
8
|
+
*
|
|
9
|
+
* Supported tools:
|
|
10
|
+
* - Bash: checks command content
|
|
11
|
+
* - Edit/Write: checks file_path
|
|
12
|
+
* - Read: checks file_path
|
|
13
|
+
* - Glob/Grep: checks pattern/path
|
|
14
|
+
* - Task: checks prompt
|
|
15
|
+
* - WebFetch: checks url
|
|
16
|
+
* - All others: checks JSON stringified input
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { readFileSync } from 'fs';
|
|
20
|
+
import { join, dirname } from 'path';
|
|
21
|
+
import { fileURLToPath } from 'url';
|
|
22
|
+
|
|
23
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
24
|
+
|
|
25
|
+
interface PreToolUseInput {
|
|
26
|
+
session_id: string;
|
|
27
|
+
tool_name: string;
|
|
28
|
+
tool_input: Record<string, unknown>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface ToolGuard {
|
|
32
|
+
tool: string;
|
|
33
|
+
patterns: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface SkillRule {
|
|
37
|
+
type: string;
|
|
38
|
+
enforcement: 'suggest' | 'warn' | 'block';
|
|
39
|
+
priority: string;
|
|
40
|
+
description?: string;
|
|
41
|
+
toolGuards?: ToolGuard[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface SkillRules {
|
|
45
|
+
version: string;
|
|
46
|
+
skills: Record<string, SkillRule>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface MatchedGuard {
|
|
50
|
+
skillName: string;
|
|
51
|
+
enforcement: string;
|
|
52
|
+
description?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Extract the content to check based on tool type
|
|
57
|
+
*/
|
|
58
|
+
function getContentToCheck(toolName: string, toolInput: Record<string, unknown>): string {
|
|
59
|
+
switch (toolName) {
|
|
60
|
+
case 'Bash':
|
|
61
|
+
return String(toolInput.command || '');
|
|
62
|
+
|
|
63
|
+
case 'Edit':
|
|
64
|
+
case 'Write':
|
|
65
|
+
case 'Read':
|
|
66
|
+
return String(toolInput.file_path || '');
|
|
67
|
+
|
|
68
|
+
case 'Glob':
|
|
69
|
+
return String(toolInput.pattern || '') + ' ' + String(toolInput.path || '');
|
|
70
|
+
|
|
71
|
+
case 'Grep':
|
|
72
|
+
return String(toolInput.pattern || '') + ' ' + String(toolInput.path || '');
|
|
73
|
+
|
|
74
|
+
case 'Task':
|
|
75
|
+
return String(toolInput.prompt || '') + ' ' + String(toolInput.description || '');
|
|
76
|
+
|
|
77
|
+
case 'WebFetch':
|
|
78
|
+
return String(toolInput.url || '');
|
|
79
|
+
|
|
80
|
+
case 'WebSearch':
|
|
81
|
+
return String(toolInput.query || '');
|
|
82
|
+
|
|
83
|
+
default:
|
|
84
|
+
// For unknown tools, stringify the entire input
|
|
85
|
+
return JSON.stringify(toolInput);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Check if a pattern matches the content
|
|
91
|
+
*/
|
|
92
|
+
function matchesPattern(content: string, pattern: string): boolean {
|
|
93
|
+
try {
|
|
94
|
+
const regex = new RegExp(pattern, 'i');
|
|
95
|
+
return regex.test(content);
|
|
96
|
+
} catch {
|
|
97
|
+
// If regex fails, try simple includes
|
|
98
|
+
return content.toLowerCase().includes(pattern.toLowerCase());
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function main() {
|
|
103
|
+
try {
|
|
104
|
+
const input = readFileSync(0, 'utf-8');
|
|
105
|
+
const data: PreToolUseInput = JSON.parse(input);
|
|
106
|
+
|
|
107
|
+
// Load skill rules
|
|
108
|
+
const rulesPath = join(__dirname, '..', 'skills', 'skill-rules.json');
|
|
109
|
+
let rules: SkillRules;
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
rules = JSON.parse(readFileSync(rulesPath, 'utf-8'));
|
|
113
|
+
} catch {
|
|
114
|
+
process.exit(0);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const contentToCheck = getContentToCheck(data.tool_name, data.tool_input);
|
|
118
|
+
const matchedGuards: MatchedGuard[] = [];
|
|
119
|
+
|
|
120
|
+
// Check each skill's toolGuards
|
|
121
|
+
for (const [skillName, config] of Object.entries(rules.skills)) {
|
|
122
|
+
const guards = config.toolGuards;
|
|
123
|
+
if (!guards || guards.length === 0) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
for (const guard of guards) {
|
|
128
|
+
// Check if tool matches (exact match or wildcard)
|
|
129
|
+
if (guard.tool !== data.tool_name && guard.tool !== '*') {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Check if any pattern matches
|
|
134
|
+
const matched = guard.patterns.some(pattern =>
|
|
135
|
+
matchesPattern(contentToCheck, pattern)
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
if (matched) {
|
|
139
|
+
matchedGuards.push({
|
|
140
|
+
skillName,
|
|
141
|
+
enforcement: config.enforcement,
|
|
142
|
+
description: config.description
|
|
143
|
+
});
|
|
144
|
+
break; // Only match once per skill
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// No matches = no output
|
|
150
|
+
if (matchedGuards.length === 0) {
|
|
151
|
+
process.exit(0);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Group by enforcement level
|
|
155
|
+
const critical = matchedGuards.filter(g => g.enforcement === 'block');
|
|
156
|
+
const warnings = matchedGuards.filter(g => g.enforcement === 'warn');
|
|
157
|
+
const suggestions = matchedGuards.filter(g => g.enforcement === 'suggest');
|
|
158
|
+
|
|
159
|
+
// Build message
|
|
160
|
+
const lines: string[] = [`⚡ TOOL GUARD (${data.tool_name})`];
|
|
161
|
+
|
|
162
|
+
if (critical.length > 0) {
|
|
163
|
+
lines.push(`🚫 BLOCKED: ${critical.map(g => g.skillName).join(', ')}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (warnings.length > 0) {
|
|
167
|
+
lines.push(`⚠️ WARNING: ${warnings.map(g => g.skillName).join(', ')}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (suggestions.length > 0) {
|
|
171
|
+
lines.push(`💡 CONSIDER: ${suggestions.map(g => g.skillName).join(', ')}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
lines.push('→ Use relevant skill before proceeding');
|
|
175
|
+
|
|
176
|
+
const message = lines.join('\n');
|
|
177
|
+
|
|
178
|
+
// Determine permission decision based on enforcement
|
|
179
|
+
// For now, only 'suggest' - block/warn not implemented yet
|
|
180
|
+
const permissionDecision = critical.length > 0 ? 'block' : 'allow';
|
|
181
|
+
|
|
182
|
+
const output = {
|
|
183
|
+
hookSpecificOutput: {
|
|
184
|
+
hookEventName: 'PreToolUse',
|
|
185
|
+
permissionDecision,
|
|
186
|
+
additionalContext: `<system-reminder>${message}</system-reminder>`
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
console.log(JSON.stringify(output));
|
|
191
|
+
process.exit(0);
|
|
192
|
+
} catch (err) {
|
|
193
|
+
// Silent failure - don't break tool execution
|
|
194
|
+
process.exit(0);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
main();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# Validates skill-rules.json structure and
|
|
3
|
-
# Runs on SessionStart to ensure the skill
|
|
2
|
+
# Validates skill-rules.json structure and hook system
|
|
3
|
+
# Runs on SessionStart to ensure the skill activation works
|
|
4
4
|
|
|
5
5
|
set -e
|
|
6
6
|
|
|
@@ -13,21 +13,24 @@ RULES_FILE="$CLAUDE_DIR/skills/skill-rules.json"
|
|
|
13
13
|
|
|
14
14
|
# Collect messages
|
|
15
15
|
MESSAGES=""
|
|
16
|
+
ERRORS=0
|
|
16
17
|
|
|
17
18
|
# Install dependencies if needed (silent)
|
|
18
19
|
if [ -f "$HOOKS_DIR/package.json" ] && [ ! -d "$HOOKS_DIR/node_modules" ]; then
|
|
19
20
|
cd "$HOOKS_DIR" && npm install --silent >/dev/null 2>&1
|
|
20
21
|
fi
|
|
21
22
|
|
|
22
|
-
# Make
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
# Make hooks executable
|
|
24
|
+
for hook in "$HOOKS_DIR"/*.sh; do
|
|
25
|
+
if [ -f "$hook" ]; then
|
|
26
|
+
chmod +x "$hook" 2>/dev/null || true
|
|
27
|
+
fi
|
|
28
|
+
done
|
|
26
29
|
|
|
27
30
|
# Validate skill-rules.json exists
|
|
28
31
|
if [ ! -f "$RULES_FILE" ]; then
|
|
29
32
|
MESSAGES+="❌ skill-rules.json not found\n"
|
|
30
|
-
|
|
33
|
+
ERRORS=$((ERRORS + 1))
|
|
31
34
|
|
|
32
35
|
jq -n --arg msg "$(echo -e "$MESSAGES")" '{
|
|
33
36
|
hookSpecificOutput: {
|
|
@@ -38,12 +41,12 @@ if [ ! -f "$RULES_FILE" ]; then
|
|
|
38
41
|
exit 1
|
|
39
42
|
fi
|
|
40
43
|
|
|
41
|
-
# Validate skill-rules.json is valid JSON
|
|
44
|
+
# Validate skill-rules.json is valid JSON and check version
|
|
42
45
|
if ! command -v jq >/dev/null 2>&1; then
|
|
43
|
-
# jq not available, skip JSON validation
|
|
44
46
|
MESSAGES+="⚠️ jq not found, skipping JSON validation\n"
|
|
45
47
|
elif ! jq empty "$RULES_FILE" 2>/dev/null; then
|
|
46
48
|
MESSAGES+="❌ skill-rules.json is not valid JSON\n"
|
|
49
|
+
ERRORS=$((ERRORS + 1))
|
|
47
50
|
|
|
48
51
|
jq -n --arg msg "$(echo -e "$MESSAGES")" '{
|
|
49
52
|
hookSpecificOutput: {
|
|
@@ -52,39 +55,59 @@ elif ! jq empty "$RULES_FILE" 2>/dev/null; then
|
|
|
52
55
|
}
|
|
53
56
|
}'
|
|
54
57
|
exit 1
|
|
58
|
+
else
|
|
59
|
+
# Check version
|
|
60
|
+
VERSION=$(jq -r '.version // "unknown"' "$RULES_FILE")
|
|
61
|
+
SKILL_COUNT=$(jq '.skills | length' "$RULES_FILE")
|
|
62
|
+
TOOLGUARDS_COUNT=$(jq '[.skills[] | select(.toolGuards != null)] | length' "$RULES_FILE")
|
|
55
63
|
fi
|
|
56
64
|
|
|
57
|
-
#
|
|
58
|
-
|
|
65
|
+
# Test skill-activation-prompt hook
|
|
66
|
+
TEST_PROMPT='{"session_id":"test","transcript_path":"/tmp","cwd":".","permission_mode":"auto","prompt":"analyze architecture of codebase"}'
|
|
59
67
|
if [ -x "$HOOKS_DIR/skill-activation-prompt.sh" ]; then
|
|
60
|
-
if echo "$
|
|
61
|
-
MESSAGES+="✅
|
|
68
|
+
if echo "$TEST_PROMPT" | "$HOOKS_DIR/skill-activation-prompt.sh" >/dev/null 2>&1; then
|
|
69
|
+
MESSAGES+="✅ skill-activation-prompt hook OK\n"
|
|
62
70
|
else
|
|
63
|
-
MESSAGES+="⚠️
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
jq -n --arg msg "$(echo -e "$MESSAGES")" '{
|
|
67
|
-
hookSpecificOutput: {
|
|
68
|
-
hookEventName: "SessionStart",
|
|
69
|
-
additionalContext: $msg
|
|
70
|
-
}
|
|
71
|
-
}'
|
|
72
|
-
exit 1
|
|
71
|
+
MESSAGES+="⚠️ skill-activation-prompt test failed\n"
|
|
72
|
+
ERRORS=$((ERRORS + 1))
|
|
73
73
|
fi
|
|
74
74
|
else
|
|
75
|
-
MESSAGES+="
|
|
75
|
+
MESSAGES+="❌ skill-activation-prompt.sh not found or not executable\n"
|
|
76
|
+
ERRORS=$((ERRORS + 1))
|
|
77
|
+
fi
|
|
76
78
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
79
|
+
# Test skill-tool-guard hook
|
|
80
|
+
TEST_TOOL='{"session_id":"test","tool_name":"Bash","tool_input":{"command":"git push origin main"}}'
|
|
81
|
+
if [ -x "$HOOKS_DIR/skill-tool-guard.sh" ]; then
|
|
82
|
+
if echo "$TEST_TOOL" | "$HOOKS_DIR/skill-tool-guard.sh" >/dev/null 2>&1; then
|
|
83
|
+
MESSAGES+="✅ skill-tool-guard hook OK\n"
|
|
84
|
+
else
|
|
85
|
+
MESSAGES+="⚠️ skill-tool-guard test failed\n"
|
|
86
|
+
ERRORS=$((ERRORS + 1))
|
|
87
|
+
fi
|
|
88
|
+
else
|
|
89
|
+
MESSAGES+="❌ skill-tool-guard.sh not found or not executable\n"
|
|
90
|
+
ERRORS=$((ERRORS + 1))
|
|
91
|
+
fi
|
|
92
|
+
|
|
93
|
+
# Summary
|
|
94
|
+
if [ $ERRORS -eq 0 ]; then
|
|
95
|
+
SUMMARY="✅ Skill-rules system validated (v${VERSION}, ${SKILL_COUNT} skills, ${TOOLGUARDS_COUNT} with toolGuards)"
|
|
96
|
+
else
|
|
97
|
+
SUMMARY="⚠️ Skill-rules validation: ${ERRORS} issue(s) found"
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
# Build final message
|
|
101
|
+
FINAL_MSG="$SUMMARY"
|
|
102
|
+
if [ -n "$MESSAGES" ]; then
|
|
103
|
+
# Only include details if there were issues
|
|
104
|
+
if [ $ERRORS -gt 0 ]; then
|
|
105
|
+
FINAL_MSG+="\n$(echo -e "$MESSAGES")"
|
|
106
|
+
fi
|
|
84
107
|
fi
|
|
85
108
|
|
|
86
109
|
# Output success message in Claude Code format with visual reminder
|
|
87
|
-
jq -n --arg msg "<important-reminder>IN YOUR FIRST REPLY AFTER SEEING THIS MESSAGE YOU MUST TELL THE USER:\n$
|
|
110
|
+
jq -n --arg msg "<important-reminder>IN YOUR FIRST REPLY AFTER SEEING THIS MESSAGE YOU MUST TELL THE USER:\n${FINAL_MSG}</important-reminder>" '{
|
|
88
111
|
hookSpecificOutput: {
|
|
89
112
|
hookEventName: "SessionStart",
|
|
90
113
|
additionalContext: $msg
|