@hailer/mcp 0.0.2 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/.claude/hooks/PreToolUse.sh +52 -0
  2. package/.claude/hooks/prompt-skill-loader.cjs +452 -0
  3. package/.claude/hooks/sdk-delete-guard.cjs +117 -0
  4. package/.claude/hooks/skill-loader.cjs +142 -0
  5. package/.claude/settings.json +29 -1
  6. package/.claude/skills/MCP-populate-workflow-data-skill/SKILL.md +395 -0
  7. package/.claude/skills/SDK-create-function-field-skill/SKILL.md +313 -0
  8. package/.claude/skills/SDK-generate-skill/SKILL.md +223 -0
  9. package/.claude/skills/SDK-init-skill/SKILL.md +177 -0
  10. package/.claude/skills/SDK-workspace-setup-skill/SKILL.md +605 -0
  11. package/.claude/skills/SDK-ws-config-skill/SKILL.md +435 -0
  12. package/CHANGELOG.md +67 -0
  13. package/README.md +22 -275
  14. package/package.json +5 -2
  15. /package/.claude/skills/{add-app-member-skill → MCP-add-app-member-skill}/SKILL.md +0 -0
  16. /package/.claude/skills/{create-app-skill → MCP-create-app-skill}/SKILL.md +0 -0
  17. /package/.claude/skills/{create-insight-skill → MCP-create-insight-skill}/SKILL.md +0 -0
  18. /package/.claude/skills/{get-insight-data-skill → MCP-get-insight-data-skill}/SKILL.md +0 -0
  19. /package/.claude/skills/{insight-api → MCP-insight-api}/SKILL.md +0 -0
  20. /package/.claude/skills/{insight-api → MCP-insight-api}/references/insight-endpoints.md +0 -0
  21. /package/.claude/skills/{install-workflow-skill → MCP-install-workflow-skill}/SKILL.md +0 -0
  22. /package/.claude/skills/{list-apps-skill → MCP-list-apps-skill}/SKILL.md +0 -0
  23. /package/.claude/skills/{list-workflows-minimal-skill → MCP-list-workflows-minimal-skill}/SKILL.md +0 -0
  24. /package/.claude/skills/{local-first-skill → MCP-local-first-skill}/SKILL.md +0 -0
  25. /package/.claude/skills/{preview-insight-skill → MCP-preview-insight-skill}/SKILL.md +0 -0
  26. /package/.claude/skills/{publish-hailer-app-skill → MCP-publish-hailer-app-skill}/SKILL.md +0 -0
  27. /package/.claude/skills/{remove-app-member-skill → MCP-remove-app-member-skill}/SKILL.md +0 -0
  28. /package/.claude/skills/{remove-app-skill → MCP-remove-app-skill}/SKILL.md +0 -0
  29. /package/.claude/skills/{remove-insight-skill → MCP-remove-insight-skill}/SKILL.md +0 -0
  30. /package/.claude/skills/{remove-workflow-skill → MCP-remove-workflow-skill}/SKILL.md +0 -0
  31. /package/.claude/skills/{scaffold-hailer-app-skill → MCP-scaffold-hailer-app-skill}/SKILL.md +0 -0
  32. /package/.claude/skills/{update-app-skill → MCP-update-app-skill}/SKILL.md +0 -0
  33. /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();