@hailer/mcp 0.0.2 → 0.0.3
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/.claude/hooks/PreToolUse.sh +52 -0
- package/.claude/hooks/prompt-skill-loader.cjs +452 -0
- package/.claude/hooks/sdk-delete-guard.cjs +117 -0
- package/.claude/hooks/skill-loader.cjs +142 -0
- package/.claude/settings.json +29 -1
- package/.claude/skills/MCP-populate-workflow-data-skill/SKILL.md +395 -0
- package/.claude/skills/SDK-create-function-field-skill/SKILL.md +313 -0
- package/.claude/skills/SDK-generate-skill/SKILL.md +223 -0
- package/.claude/skills/SDK-init-skill/SKILL.md +177 -0
- package/.claude/skills/SDK-workspace-setup-skill/SKILL.md +605 -0
- package/.claude/skills/SDK-ws-config-skill/SKILL.md +435 -0
- package/CHANGELOG.md +62 -0
- package/README.md +22 -275
- package/package.json +5 -2
- /package/.claude/skills/{add-app-member-skill → MCP-add-app-member-skill}/SKILL.md +0 -0
- /package/.claude/skills/{create-app-skill → MCP-create-app-skill}/SKILL.md +0 -0
- /package/.claude/skills/{create-insight-skill → MCP-create-insight-skill}/SKILL.md +0 -0
- /package/.claude/skills/{get-insight-data-skill → MCP-get-insight-data-skill}/SKILL.md +0 -0
- /package/.claude/skills/{insight-api → MCP-insight-api}/SKILL.md +0 -0
- /package/.claude/skills/{insight-api → MCP-insight-api}/references/insight-endpoints.md +0 -0
- /package/.claude/skills/{install-workflow-skill → MCP-install-workflow-skill}/SKILL.md +0 -0
- /package/.claude/skills/{list-apps-skill → MCP-list-apps-skill}/SKILL.md +0 -0
- /package/.claude/skills/{list-workflows-minimal-skill → MCP-list-workflows-minimal-skill}/SKILL.md +0 -0
- /package/.claude/skills/{local-first-skill → MCP-local-first-skill}/SKILL.md +0 -0
- /package/.claude/skills/{preview-insight-skill → MCP-preview-insight-skill}/SKILL.md +0 -0
- /package/.claude/skills/{publish-hailer-app-skill → MCP-publish-hailer-app-skill}/SKILL.md +0 -0
- /package/.claude/skills/{remove-app-member-skill → MCP-remove-app-member-skill}/SKILL.md +0 -0
- /package/.claude/skills/{remove-app-skill → MCP-remove-app-skill}/SKILL.md +0 -0
- /package/.claude/skills/{remove-insight-skill → MCP-remove-insight-skill}/SKILL.md +0 -0
- /package/.claude/skills/{remove-workflow-skill → MCP-remove-workflow-skill}/SKILL.md +0 -0
- /package/.claude/skills/{scaffold-hailer-app-skill → MCP-scaffold-hailer-app-skill}/SKILL.md +0 -0
- /package/.claude/skills/{update-app-skill → MCP-update-app-skill}/SKILL.md +0 -0
- /package/.claude/skills/{update-workflow-field-skill → MCP-update-workflow-field-skill}/SKILL.md +0 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Claude Code PreToolUse hook - auto-loads SDK skills before bash commands
|
|
3
|
+
|
|
4
|
+
TOOL="$1"
|
|
5
|
+
|
|
6
|
+
# Only process Bash tool
|
|
7
|
+
if [[ "$TOOL" != "Bash" ]]; then
|
|
8
|
+
exit 0
|
|
9
|
+
fi
|
|
10
|
+
|
|
11
|
+
# Read tool input from stdin
|
|
12
|
+
INPUT=$(cat)
|
|
13
|
+
COMMAND=$(echo "$INPUT" | jq -r '.command // empty')
|
|
14
|
+
|
|
15
|
+
# Marker directory for tracking loaded skills this session
|
|
16
|
+
MARKER_DIR="/tmp/.claude-skills-loaded"
|
|
17
|
+
mkdir -p "$MARKER_DIR"
|
|
18
|
+
|
|
19
|
+
# SDK command to skill mapping
|
|
20
|
+
load_skill_if_needed() {
|
|
21
|
+
local skill_name="$1"
|
|
22
|
+
local marker_file="$MARKER_DIR/$skill_name"
|
|
23
|
+
local skill_path=".claude/skills/$skill_name/SKILL.md"
|
|
24
|
+
|
|
25
|
+
if [[ ! -f "$marker_file" ]] && [[ -f "$skill_path" ]]; then
|
|
26
|
+
echo "" >&2
|
|
27
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" >&2
|
|
28
|
+
echo "📚 AUTO-LOADING SKILL: $skill_name" >&2
|
|
29
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" >&2
|
|
30
|
+
echo "" >&2
|
|
31
|
+
cat "$skill_path" >&2
|
|
32
|
+
echo "" >&2
|
|
33
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" >&2
|
|
34
|
+
echo "" >&2
|
|
35
|
+
touch "$marker_file"
|
|
36
|
+
fi
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# Check for SDK commands and load appropriate skills
|
|
40
|
+
case "$COMMAND" in
|
|
41
|
+
*"npm run pull"*|*"npm run push"*|*"npm run workflows"*|*"npm run phases"*|*"npm run fields"*|*"npm run groups"*|*"npm run teams"*|*"npm run insights"*)
|
|
42
|
+
load_skill_if_needed "SDK-ws-config-skill"
|
|
43
|
+
;;
|
|
44
|
+
*"npm run generate"*|*"hailer-sdk generate"*)
|
|
45
|
+
load_skill_if_needed "SDK-generate-skill"
|
|
46
|
+
;;
|
|
47
|
+
*"hailer-sdk init"*)
|
|
48
|
+
load_skill_if_needed "SDK-init-skill"
|
|
49
|
+
;;
|
|
50
|
+
esac
|
|
51
|
+
|
|
52
|
+
exit 0
|
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Claude Code UserPromptSubmit Hook - Auto-loads skills based on prompt keywords
|
|
4
|
+
*
|
|
5
|
+
* This hook detects keywords in user prompts and triggers disambiguation
|
|
6
|
+
* questions to clarify user intent before loading skills.
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - Keyword-based skill loading
|
|
10
|
+
* - Disambiguation prompts for ALL keywords (no assumptions)
|
|
11
|
+
* - Marker files to avoid loading same skill twice per session
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const os = require('os');
|
|
17
|
+
|
|
18
|
+
// Read hook input from stdin
|
|
19
|
+
let input = '';
|
|
20
|
+
process.stdin.setEncoding('utf8');
|
|
21
|
+
process.stdin.on('data', chunk => input += chunk);
|
|
22
|
+
process.stdin.on('end', () => {
|
|
23
|
+
try {
|
|
24
|
+
const data = JSON.parse(input);
|
|
25
|
+
processHook(data);
|
|
26
|
+
} catch (e) {
|
|
27
|
+
// Invalid JSON, exit silently
|
|
28
|
+
process.exit(0);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// ALL keywords are now ambiguous - always ask for clarification
|
|
33
|
+
const AMBIGUOUS_KEYWORDS = [
|
|
34
|
+
// Pull operations
|
|
35
|
+
{
|
|
36
|
+
keyword: /\bpull\b/i,
|
|
37
|
+
contextPatterns: [
|
|
38
|
+
/\b(config|workspace|fields?|phases?|groups?|teams?|ws-config)\b/i,
|
|
39
|
+
/\bnpm run pull\b/i
|
|
40
|
+
],
|
|
41
|
+
skill: 'SDK-ws-config-skill',
|
|
42
|
+
disambiguation: {
|
|
43
|
+
keyword: 'pull',
|
|
44
|
+
question: 'What would you like to pull?',
|
|
45
|
+
options: [
|
|
46
|
+
{ label: 'Workspace configuration', description: 'Pull workflow configs, fields, phases (npm run pull)', skill: 'SDK-ws-config-skill' },
|
|
47
|
+
{ label: 'Activity data', description: 'Fetch activities from a workflow (MCP tools)', skill: null }
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
// Push operations
|
|
52
|
+
{
|
|
53
|
+
keyword: /\bpush\b/i,
|
|
54
|
+
contextPatterns: [
|
|
55
|
+
/\b(config|workspace|fields?|phases?|groups?|teams?|ws-config)\b/i,
|
|
56
|
+
/\bnpm run push\b/i
|
|
57
|
+
],
|
|
58
|
+
skill: 'SDK-ws-config-skill',
|
|
59
|
+
disambiguation: {
|
|
60
|
+
keyword: 'push',
|
|
61
|
+
question: 'What would you like to push?',
|
|
62
|
+
options: [
|
|
63
|
+
{ label: 'Workspace configuration', description: 'Push workflow configs, fields, phases (npm run push)', skill: 'SDK-ws-config-skill' },
|
|
64
|
+
{ label: 'Activity updates', description: 'Update activities in a workflow (MCP tools)', skill: null }
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
// Generate operations
|
|
69
|
+
{
|
|
70
|
+
keyword: /\bgenerate\b/i,
|
|
71
|
+
contextPatterns: [
|
|
72
|
+
/\b(types?|enums?|typescript|ts)\b/i,
|
|
73
|
+
/\bnpm run generate\b/i
|
|
74
|
+
],
|
|
75
|
+
skill: 'SDK-generate-skill',
|
|
76
|
+
disambiguation: {
|
|
77
|
+
keyword: 'generate',
|
|
78
|
+
question: 'What would you like to generate?',
|
|
79
|
+
options: [
|
|
80
|
+
{ label: 'TypeScript types/enums', description: 'Generate types from Hailer workspace (npm run generate)', skill: 'SDK-generate-skill' },
|
|
81
|
+
{ label: 'Sample data', description: 'Create test activities in workflows', skill: 'MCP-populate-workflow-data-skill' },
|
|
82
|
+
{ label: 'Reports/insights', description: 'Create SQL-like insights', skill: 'MCP-create-insight-skill' }
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
// Setup operations
|
|
87
|
+
{
|
|
88
|
+
keyword: /\bsetup\b/i,
|
|
89
|
+
contextPatterns: [
|
|
90
|
+
/\b(project|init|mcp|bot.?account)\b/i,
|
|
91
|
+
/\bhailer-sdk init\b/i
|
|
92
|
+
],
|
|
93
|
+
skill: 'SDK-init-skill',
|
|
94
|
+
disambiguation: {
|
|
95
|
+
keyword: 'setup',
|
|
96
|
+
question: 'What would you like to set up?',
|
|
97
|
+
options: [
|
|
98
|
+
{ label: 'New Hailer project', description: 'Initialize project with hailer-sdk init', skill: 'SDK-init-skill' },
|
|
99
|
+
{ label: 'Workspace structure', description: 'Create workflows, fields, phases', skill: 'SDK-workspace-setup-skill' },
|
|
100
|
+
{ label: 'Hailer app', description: 'Scaffold a new Hailer application', skill: 'MCP-scaffold-hailer-app-skill' }
|
|
101
|
+
]
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
// Initialize operations
|
|
105
|
+
{
|
|
106
|
+
keyword: /\b(init|initialize)\b/i,
|
|
107
|
+
contextPatterns: [
|
|
108
|
+
/\bhailer-sdk init\b/i,
|
|
109
|
+
/\bnew project\b/i
|
|
110
|
+
],
|
|
111
|
+
skill: 'SDK-init-skill',
|
|
112
|
+
disambiguation: {
|
|
113
|
+
keyword: 'init',
|
|
114
|
+
question: 'What would you like to initialize?',
|
|
115
|
+
options: [
|
|
116
|
+
{ label: 'New Hailer project', description: 'Initialize project with hailer-sdk init', skill: 'SDK-init-skill' },
|
|
117
|
+
{ label: 'Bot account', description: 'Set up a bot account for automation', skill: 'SDK-init-skill' },
|
|
118
|
+
{ label: 'MCP server', description: 'Configure MCP server integration', skill: 'SDK-init-skill' }
|
|
119
|
+
]
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
// Workflow operations
|
|
123
|
+
{
|
|
124
|
+
keyword: /\bworkflow\b/i,
|
|
125
|
+
contextPatterns: [
|
|
126
|
+
/\b(install|create|new).?workflow\b/i,
|
|
127
|
+
/\bworkflow.?(install|create|new)\b/i
|
|
128
|
+
],
|
|
129
|
+
skill: 'MCP-install-workflow-skill',
|
|
130
|
+
disambiguation: {
|
|
131
|
+
keyword: 'workflow',
|
|
132
|
+
question: 'What would you like to do with workflows?',
|
|
133
|
+
options: [
|
|
134
|
+
{ label: 'Create new workflow', description: 'Install a new workflow structure', skill: 'MCP-install-workflow-skill' },
|
|
135
|
+
{ label: 'List workflows', description: 'View existing workflows in workspace', skill: null },
|
|
136
|
+
{ label: 'Update workflow structure', description: 'Modify fields, phases via SDK', skill: 'SDK-ws-config-skill' },
|
|
137
|
+
{ label: 'Remove workflow', description: 'Delete a workflow from workspace', skill: 'MCP-remove-workflow-skill' }
|
|
138
|
+
]
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
// Insight/Report operations
|
|
142
|
+
{
|
|
143
|
+
keyword: /\b(insight|report|analytics)\b/i,
|
|
144
|
+
contextPatterns: [
|
|
145
|
+
/\b(create|new|build).?(insight|report)\b/i,
|
|
146
|
+
/\b(insight|report).?(create|new|build)\b/i
|
|
147
|
+
],
|
|
148
|
+
skill: 'MCP-create-insight-skill',
|
|
149
|
+
disambiguation: {
|
|
150
|
+
keyword: 'insight',
|
|
151
|
+
question: 'What would you like to do with insights?',
|
|
152
|
+
options: [
|
|
153
|
+
{ label: 'Create new insight', description: 'Build a new SQL-like report', skill: 'MCP-create-insight-skill' },
|
|
154
|
+
{ label: 'List insights', description: 'View existing insights in workspace', skill: null },
|
|
155
|
+
{ label: 'Get insight data', description: 'Execute an insight and retrieve results', skill: 'MCP-get-insight-data-skill' },
|
|
156
|
+
{ label: 'Preview/test SQL', description: 'Test a query before saving', skill: 'MCP-preview-insight-skill' }
|
|
157
|
+
]
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
// SQL/Query operations (alias for insight)
|
|
161
|
+
{
|
|
162
|
+
keyword: /\b(sql|query)\b/i,
|
|
163
|
+
contextPatterns: [
|
|
164
|
+
/\b(run|execute|test|preview).?(sql|query)\b/i
|
|
165
|
+
],
|
|
166
|
+
skill: 'MCP-preview-insight-skill',
|
|
167
|
+
disambiguation: {
|
|
168
|
+
keyword: 'sql',
|
|
169
|
+
question: 'What would you like to do with SQL queries?',
|
|
170
|
+
options: [
|
|
171
|
+
{ label: 'Create insight with SQL', description: 'Build a saved SQL report', skill: 'MCP-create-insight-skill' },
|
|
172
|
+
{ label: 'Preview/test SQL', description: 'Test a query before saving', skill: 'MCP-preview-insight-skill' },
|
|
173
|
+
{ label: 'Run existing insight', description: 'Execute a saved insight', skill: 'MCP-get-insight-data-skill' }
|
|
174
|
+
]
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
// App operations
|
|
178
|
+
{
|
|
179
|
+
keyword: /\b(app|application)\b/i,
|
|
180
|
+
contextPatterns: [
|
|
181
|
+
/\b(scaffold|create|new|build).?app\b/i,
|
|
182
|
+
/\bhailer.?app\b/i
|
|
183
|
+
],
|
|
184
|
+
skill: 'MCP-scaffold-hailer-app-skill',
|
|
185
|
+
disambiguation: {
|
|
186
|
+
keyword: 'app',
|
|
187
|
+
question: 'What would you like to do with Hailer apps?',
|
|
188
|
+
options: [
|
|
189
|
+
{ label: 'Scaffold new app', description: 'Create a new app project from template', skill: 'MCP-scaffold-hailer-app-skill' },
|
|
190
|
+
{ label: 'Create app entry', description: 'Register an app in Hailer (no scaffold)', skill: 'MCP-create-app-skill' },
|
|
191
|
+
{ label: 'List apps', description: 'View existing apps in workspace', skill: 'MCP-list-apps-skill' },
|
|
192
|
+
{ label: 'Update app', description: 'Modify app properties', skill: 'MCP-update-app-skill' }
|
|
193
|
+
]
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
// Publish/Deploy operations
|
|
197
|
+
{
|
|
198
|
+
keyword: /\b(publish|deploy)\b/i,
|
|
199
|
+
contextPatterns: [
|
|
200
|
+
/\b(publish|deploy).?app\b/i
|
|
201
|
+
],
|
|
202
|
+
skill: 'MCP-publish-hailer-app-skill',
|
|
203
|
+
disambiguation: {
|
|
204
|
+
keyword: 'publish',
|
|
205
|
+
question: 'What would you like to publish or deploy?',
|
|
206
|
+
options: [
|
|
207
|
+
{ label: 'Publish Hailer app', description: 'Build and upload app to production', skill: 'MCP-publish-hailer-app-skill' },
|
|
208
|
+
{ label: 'Share app with users', description: 'Add members to access an app', skill: 'MCP-add-app-member-skill' }
|
|
209
|
+
]
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
// Function field / Calculated field operations
|
|
213
|
+
{
|
|
214
|
+
keyword: /\b(function.?field|calculated.?field|formula)\b/i,
|
|
215
|
+
contextPatterns: [
|
|
216
|
+
/\b(create|add|new).?(function|calculated).?field\b/i
|
|
217
|
+
],
|
|
218
|
+
skill: 'SDK-create-function-field-skill',
|
|
219
|
+
disambiguation: {
|
|
220
|
+
keyword: 'function field',
|
|
221
|
+
question: 'What would you like to do with function fields?',
|
|
222
|
+
options: [
|
|
223
|
+
{ label: 'Create function field', description: 'Add a new calculated field to workflow', skill: 'SDK-create-function-field-skill' },
|
|
224
|
+
{ label: 'Update function field', description: 'Modify existing function field formula', skill: 'MCP-update-workflow-field-skill' },
|
|
225
|
+
{ label: 'Test function', description: 'Test function code against real data', skill: null }
|
|
226
|
+
]
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
// Sample data / Populate operations
|
|
230
|
+
{
|
|
231
|
+
keyword: /\b(sample.?data|test.?data|populate|bulk.?create)\b/i,
|
|
232
|
+
contextPatterns: [
|
|
233
|
+
/\b(create|add|generate).?(sample|test).?data\b/i
|
|
234
|
+
],
|
|
235
|
+
skill: 'MCP-populate-workflow-data-skill',
|
|
236
|
+
disambiguation: {
|
|
237
|
+
keyword: 'data',
|
|
238
|
+
question: 'What would you like to do with data?',
|
|
239
|
+
options: [
|
|
240
|
+
{ label: 'Populate with sample data', description: 'Generate realistic test activities', skill: 'MCP-populate-workflow-data-skill' },
|
|
241
|
+
{ label: 'Bulk create activities', description: 'Create multiple activities at once', skill: null },
|
|
242
|
+
{ label: 'Import data', description: 'Import from external source', skill: null }
|
|
243
|
+
]
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
// Field operations
|
|
247
|
+
{
|
|
248
|
+
keyword: /\b(add|modify|update|delete|remove|create).{0,20}field\b/i,
|
|
249
|
+
contextPatterns: [
|
|
250
|
+
/\bnpm run (fields|push)\b/i,
|
|
251
|
+
/\bworkspace.?config\b/i
|
|
252
|
+
],
|
|
253
|
+
skill: 'SDK-ws-config-skill',
|
|
254
|
+
disambiguation: {
|
|
255
|
+
keyword: 'field',
|
|
256
|
+
question: 'How would you like to modify fields?',
|
|
257
|
+
options: [
|
|
258
|
+
{ label: 'Via SDK (local files)', description: 'Edit workspace/ files and push (version controlled)', skill: 'SDK-ws-config-skill' },
|
|
259
|
+
{ label: 'Via MCP (direct API)', description: 'Update field directly via API', skill: 'MCP-update-workflow-field-skill' }
|
|
260
|
+
]
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
// Phase operations
|
|
264
|
+
{
|
|
265
|
+
keyword: /\b(add|modify|update|delete|remove|create).{0,20}phase\b/i,
|
|
266
|
+
contextPatterns: [
|
|
267
|
+
/\bnpm run (phases|push)\b/i,
|
|
268
|
+
/\bworkspace.?config\b/i
|
|
269
|
+
],
|
|
270
|
+
skill: 'SDK-ws-config-skill',
|
|
271
|
+
disambiguation: {
|
|
272
|
+
keyword: 'phase',
|
|
273
|
+
question: 'How would you like to modify phases?',
|
|
274
|
+
options: [
|
|
275
|
+
{ label: 'Via SDK (local files)', description: 'Edit workspace/ files and push (version controlled)', skill: 'SDK-ws-config-skill' },
|
|
276
|
+
{ label: 'Via MCP (direct API)', description: 'Update phase directly via API', skill: null }
|
|
277
|
+
]
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
// Activity operations
|
|
281
|
+
{
|
|
282
|
+
keyword: /\b(activities|activity)\b/i,
|
|
283
|
+
contextPatterns: [
|
|
284
|
+
/\b(list|show|get|fetch|create|update|delete).?(activities|activity)\b/i
|
|
285
|
+
],
|
|
286
|
+
skill: null,
|
|
287
|
+
disambiguation: {
|
|
288
|
+
keyword: 'activity',
|
|
289
|
+
question: 'What would you like to do with activities?',
|
|
290
|
+
options: [
|
|
291
|
+
{ label: 'List activities', description: 'View activities in a workflow', skill: null },
|
|
292
|
+
{ label: 'Create activity', description: 'Add a new activity to workflow', skill: null },
|
|
293
|
+
{ label: 'Update activity', description: 'Modify an existing activity', skill: null },
|
|
294
|
+
{ label: 'Bulk create activities', description: 'Create multiple activities at once', skill: 'MCP-populate-workflow-data-skill' }
|
|
295
|
+
]
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
// Sync operations (DESTRUCTIVE - require confirmation)
|
|
299
|
+
{
|
|
300
|
+
keyword: /\b(sync|workflows?.?sync)\b/i,
|
|
301
|
+
contextPatterns: [], // Always require confirmation, never auto-proceed
|
|
302
|
+
skill: 'SDK-ws-config-skill',
|
|
303
|
+
disambiguation: {
|
|
304
|
+
keyword: 'sync',
|
|
305
|
+
question: '⚠️ Sync operations can DELETE resources from Hailer. Items removed from local config will be PERMANENTLY deleted remotely. Proceed?',
|
|
306
|
+
options: [
|
|
307
|
+
{ label: 'Yes, run sync', description: 'I understand - proceed with destructive sync operation', skill: 'SDK-ws-config-skill', action: 'confirm-sync' },
|
|
308
|
+
{ label: 'No, cancel', description: 'Abort - don\'t delete anything', skill: null, action: 'cancel' }
|
|
309
|
+
]
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
];
|
|
313
|
+
|
|
314
|
+
// No more direct keywords - all require disambiguation
|
|
315
|
+
const DIRECT_KEYWORDS = [];
|
|
316
|
+
|
|
317
|
+
function processHook(data) {
|
|
318
|
+
const { prompt, cwd } = data;
|
|
319
|
+
|
|
320
|
+
if (!prompt) {
|
|
321
|
+
process.exit(0);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Marker directory for tracking loaded skills this session
|
|
325
|
+
const markerDir = path.join(os.tmpdir(), '.claude-skills-loaded');
|
|
326
|
+
if (!fs.existsSync(markerDir)) {
|
|
327
|
+
fs.mkdirSync(markerDir, { recursive: true });
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Find project directory
|
|
331
|
+
const projectDir = process.env.CLAUDE_PROJECT_DIR || cwd || process.cwd();
|
|
332
|
+
|
|
333
|
+
// Track what we'll output
|
|
334
|
+
const skillsToLoad = [];
|
|
335
|
+
const disambiguationsNeeded = [];
|
|
336
|
+
const seenDisambiguations = new Set(); // Avoid duplicate questions
|
|
337
|
+
|
|
338
|
+
// Check all ambiguous keywords
|
|
339
|
+
for (const entry of AMBIGUOUS_KEYWORDS) {
|
|
340
|
+
if (entry.keyword.test(prompt)) {
|
|
341
|
+
// Check if context makes it clear
|
|
342
|
+
const hasContext = entry.contextPatterns && entry.contextPatterns.some(p => p.test(prompt));
|
|
343
|
+
|
|
344
|
+
if (hasContext && entry.skill) {
|
|
345
|
+
// Context is clear - load the skill
|
|
346
|
+
const markerFile = path.join(markerDir, entry.skill);
|
|
347
|
+
if (!fs.existsSync(markerFile)) {
|
|
348
|
+
const skillPath = path.join(projectDir, '.claude', 'skills', entry.skill, 'SKILL.md');
|
|
349
|
+
if (fs.existsSync(skillPath)) {
|
|
350
|
+
skillsToLoad.push({
|
|
351
|
+
skill: entry.skill,
|
|
352
|
+
description: entry.disambiguation.question,
|
|
353
|
+
path: skillPath,
|
|
354
|
+
markerFile
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
} else {
|
|
359
|
+
// Context unclear - need disambiguation (avoid duplicates)
|
|
360
|
+
const disambKey = entry.disambiguation.keyword;
|
|
361
|
+
if (!seenDisambiguations.has(disambKey)) {
|
|
362
|
+
seenDisambiguations.add(disambKey);
|
|
363
|
+
disambiguationsNeeded.push(entry.disambiguation);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Nothing to output
|
|
370
|
+
if (skillsToLoad.length === 0 && disambiguationsNeeded.length === 0) {
|
|
371
|
+
process.exit(0);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Build output
|
|
375
|
+
let output = '\n';
|
|
376
|
+
|
|
377
|
+
// Output disambiguation prompts first
|
|
378
|
+
if (disambiguationsNeeded.length > 0) {
|
|
379
|
+
output += '='.repeat(60) + '\n';
|
|
380
|
+
output += '⚠️ MANDATORY ACTION REQUIRED - DO NOT SKIP\n';
|
|
381
|
+
output += '='.repeat(60) + '\n\n';
|
|
382
|
+
output += 'STOP! You MUST use the AskUserQuestion tool BEFORE doing anything else.\n';
|
|
383
|
+
output += 'The user\'s intent is ambiguous. Do NOT guess or assume.\n\n';
|
|
384
|
+
|
|
385
|
+
for (const disamb of disambiguationsNeeded) {
|
|
386
|
+
output += '-'.repeat(40) + '\n';
|
|
387
|
+
output += `Ambiguous keyword detected: "${disamb.keyword}"\n\n`;
|
|
388
|
+
output += 'USE THIS EXACT AskUserQuestion CALL:\n\n';
|
|
389
|
+
output += '```json\n';
|
|
390
|
+
output += JSON.stringify({
|
|
391
|
+
questions: [{
|
|
392
|
+
question: disamb.question,
|
|
393
|
+
header: disamb.keyword.charAt(0).toUpperCase() + disamb.keyword.slice(1),
|
|
394
|
+
options: disamb.options.map(opt => ({
|
|
395
|
+
label: opt.label,
|
|
396
|
+
description: opt.description
|
|
397
|
+
})),
|
|
398
|
+
multiSelect: false
|
|
399
|
+
}]
|
|
400
|
+
}, null, 2);
|
|
401
|
+
output += '\n```\n\n';
|
|
402
|
+
|
|
403
|
+
output += 'After user responds:\n';
|
|
404
|
+
for (const opt of disamb.options) {
|
|
405
|
+
if (opt.action === 'confirm-sync') {
|
|
406
|
+
output += ` - If "${opt.label}": Load Skill(${opt.skill}), then run with: yes | npm run workflows-sync\n`;
|
|
407
|
+
} else if (opt.action === 'cancel') {
|
|
408
|
+
output += ` - If "${opt.label}": Acknowledge cancellation, do NOT run any sync command\n`;
|
|
409
|
+
} else if (opt.skill) {
|
|
410
|
+
output += ` - If "${opt.label}": Load Skill(${opt.skill}), then proceed\n`;
|
|
411
|
+
} else {
|
|
412
|
+
output += ` - If "${opt.label}": Use MCP tools directly (no skill needed)\n`;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
output += '\n';
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
output += '='.repeat(60) + '\n';
|
|
419
|
+
output += 'REMEMBER: Ask FIRST, then act. Never assume user intent.\n';
|
|
420
|
+
output += '='.repeat(60) + '\n\n';
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Output skills that can be loaded directly (only when context is clear)
|
|
424
|
+
if (skillsToLoad.length > 0) {
|
|
425
|
+
output += '='.repeat(60) + '\n';
|
|
426
|
+
output += 'AUTO-LOADED SKILLS (context was clear from prompt)\n';
|
|
427
|
+
output += '='.repeat(60) + '\n\n';
|
|
428
|
+
|
|
429
|
+
for (const skill of skillsToLoad) {
|
|
430
|
+
const skillContent = fs.readFileSync(skill.path, 'utf8');
|
|
431
|
+
|
|
432
|
+
output += '-'.repeat(60) + '\n';
|
|
433
|
+
output += `SKILL: ${skill.skill}\n`;
|
|
434
|
+
output += `Purpose: ${skill.description}\n`;
|
|
435
|
+
output += '-'.repeat(60) + '\n\n';
|
|
436
|
+
output += skillContent;
|
|
437
|
+
output += '\n\n';
|
|
438
|
+
|
|
439
|
+
// Mark skill as loaded
|
|
440
|
+
fs.writeFileSync(skill.markerFile, new Date().toISOString());
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
output += '='.repeat(60) + '\n';
|
|
444
|
+
output += 'END OF AUTO-LOADED SKILLS\n';
|
|
445
|
+
output += '='.repeat(60) + '\n';
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Output to stdout - UserPromptSubmit hooks add stdout to context
|
|
449
|
+
console.log(output);
|
|
450
|
+
|
|
451
|
+
process.exit(0);
|
|
452
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* SDK Delete Guard Hook
|
|
4
|
+
*
|
|
5
|
+
* PreToolUse hook that catches SDK commands which may delete resources
|
|
6
|
+
* and blocks them, instructing Claude to use `yes |` prefix if user confirms.
|
|
7
|
+
*
|
|
8
|
+
* Triggered by: npm run push, npm run *-sync, npm run *-push commands
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// Commands that can cause deletions
|
|
12
|
+
const DELETE_RISK_PATTERNS = [
|
|
13
|
+
/npm run push\b/, // Full push - can delete workflows, fields, phases
|
|
14
|
+
/npm run workflows-sync\b/, // Sync workflows - can delete workflows
|
|
15
|
+
/npm run workflows-push\b/, // Push workflows - can delete workflows
|
|
16
|
+
/npm run phases-push\b/, // Push phases - can delete phases
|
|
17
|
+
/npm run fields-push\b/, // Push fields - can delete fields
|
|
18
|
+
/npm run groups-push\b/, // Push groups - can delete groups
|
|
19
|
+
/npm run teams-push\b/, // Push teams - can delete teams
|
|
20
|
+
/npm run insights-push\b/, // Push insights - can delete insights
|
|
21
|
+
/hailer-sdk ws-config push\b/,
|
|
22
|
+
/hailer-sdk ws-config.*sync\b/,
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
// Commands that are already safe (have yes | prefix)
|
|
26
|
+
const SAFE_PATTERNS = [
|
|
27
|
+
/^yes\s*\|/,
|
|
28
|
+
/^echo\s+[yYnN]\s*\|/,
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
// Read stdin
|
|
32
|
+
async function readStdin() {
|
|
33
|
+
return new Promise((resolve) => {
|
|
34
|
+
let data = '';
|
|
35
|
+
process.stdin.setEncoding('utf8');
|
|
36
|
+
process.stdin.on('data', chunk => data += chunk);
|
|
37
|
+
process.stdin.on('end', () => resolve(data));
|
|
38
|
+
setTimeout(() => resolve(data), 100);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function main() {
|
|
43
|
+
try {
|
|
44
|
+
const input = await readStdin();
|
|
45
|
+
|
|
46
|
+
if (!input.trim()) {
|
|
47
|
+
process.exit(0);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const hookData = JSON.parse(input);
|
|
51
|
+
const command = hookData.tool_input?.command || '';
|
|
52
|
+
|
|
53
|
+
// If command already has yes/echo prefix, allow it
|
|
54
|
+
const isSafe = SAFE_PATTERNS.some(pattern => pattern.test(command));
|
|
55
|
+
if (isSafe) {
|
|
56
|
+
process.exit(0);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Check if this is a delete-risk command
|
|
60
|
+
const isDeleteRisk = DELETE_RISK_PATTERNS.some(pattern => pattern.test(command));
|
|
61
|
+
|
|
62
|
+
if (!isDeleteRisk) {
|
|
63
|
+
process.exit(0);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Output instructions to stderr (appears as system reminder)
|
|
67
|
+
// This ensures Claude sees and follows the instructions
|
|
68
|
+
const instructions = `============================================================
|
|
69
|
+
⚠️ MANDATORY ACTION REQUIRED - DO NOT SKIP
|
|
70
|
+
============================================================
|
|
71
|
+
|
|
72
|
+
STOP! You MUST use the AskUserQuestion tool BEFORE running this command.
|
|
73
|
+
This is a DESTRUCTIVE operation that may DELETE resources from Hailer.
|
|
74
|
+
|
|
75
|
+
USE THIS EXACT AskUserQuestion CALL:
|
|
76
|
+
|
|
77
|
+
\`\`\`json
|
|
78
|
+
{
|
|
79
|
+
"questions": [{
|
|
80
|
+
"question": "This command may delete resources from Hailer. Items removed from local config will be PERMANENTLY deleted. Proceed with: ${command}?",
|
|
81
|
+
"header": "Confirm Delete",
|
|
82
|
+
"options": [
|
|
83
|
+
{ "label": "Yes, delete", "description": "Proceed with the destructive operation" },
|
|
84
|
+
{ "label": "No, cancel", "description": "Abort - don't delete anything" }
|
|
85
|
+
],
|
|
86
|
+
"multiSelect": false
|
|
87
|
+
}]
|
|
88
|
+
}
|
|
89
|
+
\`\`\`
|
|
90
|
+
|
|
91
|
+
After user responds:
|
|
92
|
+
- If "Yes, delete": Run: yes | ${command}
|
|
93
|
+
- If "No, cancel": Acknowledge cancellation, do NOT run the command
|
|
94
|
+
|
|
95
|
+
============================================================
|
|
96
|
+
REMEMBER: ASK FIRST using AskUserQuestion, then act based on response.
|
|
97
|
+
============================================================`;
|
|
98
|
+
|
|
99
|
+
// Output to stderr so it appears as system reminder
|
|
100
|
+
console.error(instructions);
|
|
101
|
+
|
|
102
|
+
// Block the command - Claude must ask first
|
|
103
|
+
const response = {
|
|
104
|
+
permissionDecision: "deny",
|
|
105
|
+
permissionDecisionReason: "Command blocked. Follow the instructions above to ask user for confirmation first."
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
console.log(JSON.stringify(response));
|
|
109
|
+
process.exit(0);
|
|
110
|
+
|
|
111
|
+
} catch (error) {
|
|
112
|
+
console.error(`[sdk-delete-guard] Error: ${error.message}`);
|
|
113
|
+
process.exit(0);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
main();
|