@hailer/mcp 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Claude Code PostToolUse Hook - Asks user about spawning app builder agent
4
+ *
5
+ * This hook triggers after scaffold_hailer_app completes successfully
6
+ * and ASKS the user if they want to spawn the app builder agent.
7
+ */
8
+
9
+ const path = require('path');
10
+
11
+ // Read hook input from stdin
12
+ let input = '';
13
+ process.stdin.setEncoding('utf8');
14
+ process.stdin.on('data', chunk => input += chunk);
15
+ process.stdin.on('end', () => {
16
+ try {
17
+ const data = JSON.parse(input);
18
+ processHook(data);
19
+ } catch {
20
+ // Invalid JSON, exit silently
21
+ process.exit(0);
22
+ }
23
+ });
24
+
25
+ function processHook(data) {
26
+ const { tool_name, tool_input, tool_output } = data;
27
+
28
+ // Only trigger for scaffold_hailer_app
29
+ if (tool_name !== 'mcp__hailer__scaffold_hailer_app') {
30
+ process.exit(0);
31
+ }
32
+
33
+ // Check if scaffold was successful (look for "Setup Complete" in output)
34
+ if (!tool_output || !tool_output.includes('Setup Complete')) {
35
+ process.exit(0);
36
+ }
37
+
38
+ // Extract project info from tool_input
39
+ const projectName = tool_input?.projectName || 'the app';
40
+ const description = tool_input?.description || 'Hailer data app';
41
+
42
+ // Try to determine project path
43
+ const cwd = process.env.CLAUDE_PROJECT_DIR || process.cwd();
44
+ const projectPath = tool_input?.targetDirectory
45
+ ? path.join(tool_input.targetDirectory, projectName)
46
+ : path.join(cwd, projectName);
47
+
48
+ // Build the AskUserQuestion instruction
49
+ const output = `
50
+ ============================================================
51
+ ✅ App Scaffolded Successfully!
52
+ ============================================================
53
+
54
+ **Project:** ${projectName}
55
+ **Location:** ${projectPath}
56
+ **Dev Server:** http://localhost:3000 (running)
57
+
58
+ The app structure is ready. Now you need to build the actual components.
59
+
60
+ ----------------------------------------
61
+ ⚠️ MANDATORY: ASK THE USER
62
+ ----------------------------------------
63
+
64
+ You MUST use AskUserQuestion with EXACTLY this format:
65
+
66
+ \`\`\`json
67
+ {
68
+ "questions": [
69
+ {
70
+ "question": "Would you like me to spawn a builder agent to create the app components?",
71
+ "header": "Build App",
72
+ "options": [
73
+ {
74
+ "label": "Yes, spawn builder",
75
+ "description": "Spawn isolated agent with TypeScript + SDK patterns"
76
+ },
77
+ {
78
+ "label": "No, I'll build manually",
79
+ "description": "Continue in current session"
80
+ }
81
+ ],
82
+ "multiSelect": false
83
+ }
84
+ ]
85
+ }
86
+ \`\`\`
87
+
88
+ ----------------------------------------
89
+ IF USER SAYS YES: Follow these steps
90
+ ----------------------------------------
91
+
92
+ 1. Load the spawn-app-builder skill:
93
+ \`\`\`javascript
94
+ Skill("spawn-app-builder")
95
+ \`\`\`
96
+
97
+ 2. Get workflow context:
98
+ \`\`\`javascript
99
+ list_workflows_minimal()
100
+ \`\`\`
101
+
102
+ 3. Use the Task tool template from the skill with:
103
+ - PROJECT_PATH: ${projectPath}
104
+ - TASK_DESCRIPTION: ${description}
105
+ - WORKFLOWS_INFO: (from list_workflows_minimal)
106
+
107
+ ----------------------------------------
108
+ IF USER SAYS NO: Load skills in current session
109
+ ----------------------------------------
110
+
111
+ \`\`\`javascript
112
+ Skill("building-hailer-apps-skill")
113
+ Skill("hailer-app-builder")
114
+ \`\`\`
115
+
116
+ Then continue building in the current conversation.
117
+
118
+ ============================================================
119
+ ASK THE USER NOW - Do not skip this question!
120
+ ============================================================
121
+ `;
122
+
123
+ console.error(output);
124
+ process.exit(0);
125
+ }
@@ -23,12 +23,19 @@ process.stdin.on('end', () => {
23
23
  try {
24
24
  const data = JSON.parse(input);
25
25
  processHook(data);
26
- } catch (e) {
26
+ } catch {
27
27
  // Invalid JSON, exit silently
28
28
  process.exit(0);
29
29
  }
30
30
  });
31
31
 
32
+ // Mapping from followUp types to skill file names
33
+ const FOLLOW_UP_SKILLS = {
34
+ 'build-app-flow': 'MCP-build-data-app-skill',
35
+ 'spawn-builder-flow': 'spawn-app-builder',
36
+ 'publish-template-flow': 'MCP-publish-template-skill'
37
+ };
38
+
32
39
  // ALL keywords are now ambiguous - always ask for clarification
33
40
  const AMBIGUOUS_KEYWORDS = [
34
41
  // Pull operations
@@ -187,12 +194,63 @@ const AMBIGUOUS_KEYWORDS = [
187
194
  question: 'What would you like to do with Hailer apps?',
188
195
  options: [
189
196
  { label: 'Scaffold new app', description: 'Create a new app project from template', skill: 'MCP-scaffold-hailer-app-skill' },
197
+ { label: 'Build data app', description: 'Build app to visualize/manage workspace data', skill: 'MCP-scaffold-hailer-app-skill', followUp: 'build-app-flow' },
190
198
  { label: 'Create app entry', description: 'Register an app in Hailer (no scaffold)', skill: 'MCP-create-app-skill' },
191
199
  { label: 'List apps', description: 'View existing apps in workspace', skill: 'MCP-list-apps-skill' },
192
200
  { label: 'Update app', description: 'Modify app properties', skill: 'MCP-update-app-skill' }
193
201
  ]
194
202
  }
195
203
  },
204
+ // Build data app / visualize data operations
205
+ {
206
+ keyword: /\b(visualize|dashboard|data.?manager|manage.?data|data.?app)\b/i,
207
+ contextPatterns: [
208
+ /\b(build|create|make).*(visualize|dashboard|data)\b/i
209
+ ],
210
+ skill: 'MCP-scaffold-hailer-app-skill',
211
+ disambiguation: {
212
+ keyword: 'data app',
213
+ question: 'What kind of data app would you like to build?',
214
+ options: [
215
+ { label: 'Full data manager', description: 'Browse, edit, create across all workflows', skill: 'MCP-scaffold-hailer-app-skill', followUp: 'build-app-flow' },
216
+ { label: 'Dashboard overview', description: 'Cards/stats for workflows at a glance', skill: 'MCP-scaffold-hailer-app-skill', followUp: 'build-app-flow' },
217
+ { label: 'Specific workflow app', description: 'Focus on one or few workflows', skill: 'MCP-scaffold-hailer-app-skill', followUp: 'build-app-flow' }
218
+ ]
219
+ }
220
+ },
221
+ // Write app code / Code the app operations
222
+ {
223
+ keyword: /\b(code|write|implement|develop).{0,20}(app|component|feature)\b/i,
224
+ contextPatterns: [
225
+ /\b(start|begin).{0,10}(code|coding|writing|implementing)\b/i,
226
+ /\bwrite.{0,10}(code|component|feature)\b/i,
227
+ /\bcode.{0,10}(app|component)\b/i
228
+ ],
229
+ skill: 'spawn-app-builder',
230
+ disambiguation: {
231
+ keyword: 'code app',
232
+ question: 'What would you like to do?',
233
+ options: [
234
+ { label: 'Spawn app builder agent', description: 'Launch isolated agent with TypeScript + SDK skills embedded', skill: 'spawn-app-builder', followUp: 'spawn-builder-flow' },
235
+ { label: 'Load skills in current session', description: 'Load skills here instead of spawning new agent', skill: 'hailer-app-builder', loadMultiple: ['building-hailer-apps-skill', 'hailer-app-builder'] },
236
+ { label: 'Scaffold new app first', description: 'Create project structure then write code', skill: 'MCP-scaffold-hailer-app-skill' }
237
+ ]
238
+ }
239
+ },
240
+ // Continue app development / dev on app / work on app / build more / add features
241
+ {
242
+ keyword: /\b(dev|work|continue|build|add|improve|enhance|extend|modify|change|update|fix).{0,15}(on|the|to|for)?.{0,10}(app|component|dashboard|feature|ui|code)\b/i,
243
+ contextPatterns: [], // Always require disambiguation for safety
244
+ skill: 'spawn-app-builder',
245
+ disambiguation: {
246
+ keyword: 'dev app',
247
+ question: 'How should I proceed with app development?',
248
+ options: [
249
+ { label: 'Spawn builder agent', description: 'RECOMMENDED: Isolated agent with TypeScript + SDK skills', skill: 'spawn-app-builder', followUp: 'spawn-builder-flow' },
250
+ { label: 'Load skills here', description: 'Continue in this conversation with skills loaded', skill: 'hailer-app-builder', loadMultiple: ['building-hailer-apps-skill', 'hailer-app-builder'] }
251
+ ]
252
+ }
253
+ },
196
254
  // Publish/Deploy operations
197
255
  {
198
256
  keyword: /\b(publish|deploy)\b/i,
@@ -205,10 +263,30 @@ const AMBIGUOUS_KEYWORDS = [
205
263
  question: 'What would you like to publish or deploy?',
206
264
  options: [
207
265
  { label: 'Publish Hailer app', description: 'Build and upload app to production', skill: 'MCP-publish-hailer-app-skill' },
266
+ { label: 'Publish workspace template', description: 'Share workspace config in marketplace', skill: 'MCP-publish-template-skill' },
208
267
  { label: 'Share app with users', description: 'Add members to access an app', skill: 'MCP-add-app-member-skill' }
209
268
  ]
210
269
  }
211
270
  },
271
+ // Template/Marketplace operations
272
+ {
273
+ keyword: /\b(template|marketplace)\b/i,
274
+ contextPatterns: [
275
+ /\b(publish|create|install|list).?template\b/i,
276
+ /\btemplate.?(publish|create|install|list)\b/i
277
+ ],
278
+ skill: 'MCP-publish-template-skill',
279
+ disambiguation: {
280
+ keyword: 'template',
281
+ question: 'What would you like to do with templates?',
282
+ options: [
283
+ { label: 'Publish template', description: 'Share workspace config in marketplace', skill: 'MCP-publish-template-skill', followUp: 'publish-template-flow' },
284
+ { label: 'Install template', description: 'Add template to workspace', skill: null },
285
+ { label: 'List templates', description: 'View available templates', skill: null },
286
+ { label: 'Get template details', description: 'View template info', skill: null }
287
+ ]
288
+ }
289
+ },
212
290
  // Function field / Calculated field operations
213
291
  {
214
292
  keyword: /\b(function.?field|calculated.?field|formula)\b/i,
@@ -311,8 +389,8 @@ const AMBIGUOUS_KEYWORDS = [
311
389
  }
312
390
  ];
313
391
 
314
- // No more direct keywords - all require disambiguation
315
- const DIRECT_KEYWORDS = [];
392
+ // Note: DIRECT_KEYWORDS removed - all keywords now require disambiguation
393
+ // Flow instructions are now loaded from skill files instead of inline functions
316
394
 
317
395
  function processHook(data) {
318
396
  const { prompt, cwd } = data;
@@ -334,6 +412,7 @@ function processHook(data) {
334
412
  const skillsToLoad = [];
335
413
  const disambiguationsNeeded = [];
336
414
  const seenDisambiguations = new Set(); // Avoid duplicate questions
415
+ const loadedFollowUpSkills = new Set(); // Avoid duplicate follow-up skill content
337
416
 
338
417
  // Check all ambiguous keywords
339
418
  for (const entry of AMBIGUOUS_KEYWORDS) {
@@ -406,6 +485,10 @@ function processHook(data) {
406
485
  output += ` - If "${opt.label}": Load Skill(${opt.skill}), then run with: yes | npm run workflows-sync\n`;
407
486
  } else if (opt.action === 'cancel') {
408
487
  output += ` - If "${opt.label}": Acknowledge cancellation, do NOT run any sync command\n`;
488
+ } else if (opt.followUp === 'publish-template-flow') {
489
+ output += ` - If "${opt.label}": Load Skill(${opt.skill}), then use EXACTLY this AskUserQuestion (see below)\n`;
490
+ } else if (opt.loadMultiple && Array.isArray(opt.loadMultiple)) {
491
+ output += ` - If "${opt.label}": Load BOTH skills in order: Skill(${opt.loadMultiple.join('), Skill(')}), then proceed\n`;
409
492
  } else if (opt.skill) {
410
493
  output += ` - If "${opt.label}": Load Skill(${opt.skill}), then proceed\n`;
411
494
  } else {
@@ -413,6 +496,24 @@ function processHook(data) {
413
496
  }
414
497
  }
415
498
  output += '\n';
499
+
500
+ // Add follow-up skill content (deduplicated - each skill only included once)
501
+ for (const opt of disamb.options) {
502
+ if (opt.followUp && !loadedFollowUpSkills.has(opt.followUp)) {
503
+ const skillName = FOLLOW_UP_SKILLS[opt.followUp];
504
+ if (skillName) {
505
+ const skillPath = path.join(projectDir, '.claude', 'skills', skillName, 'SKILL.md');
506
+ if (fs.existsSync(skillPath)) {
507
+ loadedFollowUpSkills.add(opt.followUp);
508
+ output += '\n' + '-'.repeat(40) + '\n';
509
+ output += `📚 FOLLOW-UP SKILL: ${skillName}\n`;
510
+ output += '-'.repeat(40) + '\n';
511
+ output += fs.readFileSync(skillPath, 'utf8');
512
+ output += '\n';
513
+ }
514
+ }
515
+ }
516
+ }
416
517
  }
417
518
 
418
519
  output += '='.repeat(60) + '\n';
@@ -445,8 +546,8 @@ function processHook(data) {
445
546
  output += '='.repeat(60) + '\n';
446
547
  }
447
548
 
448
- // Output to stdout - UserPromptSubmit hooks add stdout to context
449
- console.log(output);
549
+ // Output to stderr - displays to user
550
+ console.error(output);
450
551
 
451
552
  process.exit(0);
452
553
  }
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Claude Code PreToolUse Hook - Ensures all required fields before publish_template
4
+ *
5
+ * This hook intercepts publish_template calls and blocks them if required fields
6
+ * are missing, prompting Claude to gather all information first.
7
+ *
8
+ * Required fields from Hailer UI:
9
+ * - title (Name) - max 64 chars
10
+ * - description - max 4096 chars
11
+ * - version - e.g. "1.0.0"
12
+ * - versionDescription - release notes
13
+ * - publisher - publishing company name
14
+ * - iconFileId - uploaded icon (JPEG/PNG)
15
+ * - imageFileIds - preview images array (optional but recommended)
16
+ */
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 {
27
+ // Invalid JSON, allow the call
28
+ console.log(JSON.stringify({ decision: 'allow' }));
29
+ process.exit(0);
30
+ }
31
+ });
32
+
33
+ function processHook(data) {
34
+ const { tool_name, tool_input } = data;
35
+
36
+ // Only intercept publish_template (MCP tool name format)
37
+ if (!tool_name || !tool_name.includes('publish_template')) {
38
+ console.log(JSON.stringify({ decision: 'allow' }));
39
+ process.exit(0);
40
+ }
41
+
42
+ // Parse tool input
43
+ let args = {};
44
+ try {
45
+ if (typeof tool_input === 'string') {
46
+ args = JSON.parse(tool_input);
47
+ } else if (typeof tool_input === 'object') {
48
+ args = tool_input;
49
+ }
50
+ } catch {
51
+ // Can't parse, let it through
52
+ console.log(JSON.stringify({ decision: 'allow' }));
53
+ process.exit(0);
54
+ }
55
+
56
+ // Check for missing required fields
57
+ const missing = [];
58
+ if (!args.title) missing.push('title (Name, max 64 chars)');
59
+ if (!args.description) missing.push('description (max 4096 chars)');
60
+ if (!args.version) missing.push('version (e.g. "1.0.0")');
61
+ if (!args.versionDescription) missing.push('versionDescription (release notes)');
62
+ if (!args.publisher) missing.push('publisher (company name)');
63
+ if (!args.iconFileId) missing.push('iconFileId (upload icon first)');
64
+
65
+ if (missing.length > 0) {
66
+ console.log(JSON.stringify({
67
+ decision: 'block',
68
+ message: `BLOCKED: publish_template is missing required fields.
69
+
70
+ Missing: ${missing.join(', ')}
71
+
72
+ Before calling publish_template, you MUST gather ALL information from the user.
73
+
74
+ Use AskUserQuestion with these questions:
75
+
76
+ \`\`\`json
77
+ {
78
+ "questions": [
79
+ {
80
+ "question": "What should be the template name? (max 64 characters)",
81
+ "header": "Name",
82
+ "options": [
83
+ { "label": "Enter name", "description": "Type a descriptive template name" },
84
+ { "label": "Use workspace name", "description": "Use current workspace name as template name" }
85
+ ],
86
+ "multiSelect": false
87
+ }
88
+ ]
89
+ }
90
+ \`\`\`
91
+
92
+ Then ask for:
93
+ 1. **Name** (title) - Template display name (max 64 chars)
94
+ 2. **Description** - What this template includes (max 4096 chars)
95
+ 3. **Version** - Semantic version like "1.0.0"
96
+ 4. **Version description** - Release notes for this version
97
+ 5. **Publisher** - Company or person publishing this template
98
+ 6. **Icon** - Ask how to provide icon (URL, file path, or existing fileId)
99
+
100
+ After gathering info:
101
+ 1. Upload icon with isPublic: true (CRITICAL!):
102
+ upload_files({ files: [{ path: "...", isPublic: true }] })
103
+ 2. Then call publish_template with ALL fields:
104
+
105
+ publish_template({
106
+ title: "Template Name",
107
+ description: "What it does...",
108
+ version: "1.0.0",
109
+ versionDescription: "Initial release with...",
110
+ publisher: "Company Name",
111
+ iconFileId: "<24-char-id>",
112
+ imageFileIds: ["<24-char-id>"] // Optional: preview images
113
+ })
114
+
115
+ DO NOT call publish_template until you have ALL required information.`
116
+ }));
117
+ process.exit(0);
118
+ }
119
+
120
+ // All required fields provided, allow the call
121
+ console.log(JSON.stringify({ decision: 'allow' }));
122
+ process.exit(0);
123
+ }
@@ -24,7 +24,7 @@ process.stdin.on('end', () => {
24
24
  try {
25
25
  const data = JSON.parse(input);
26
26
  processHook(data);
27
- } catch (e) {
27
+ } catch {
28
28
  // Invalid JSON, exit silently
29
29
  process.exit(0);
30
30
  }
@@ -30,6 +30,28 @@
30
30
  "timeout": 5
31
31
  }
32
32
  ]
33
+ },
34
+ {
35
+ "matcher": "mcp__hailer__publish_template",
36
+ "hooks": [
37
+ {
38
+ "type": "command",
39
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/publish-template-guard.cjs\"",
40
+ "timeout": 5
41
+ }
42
+ ]
43
+ }
44
+ ],
45
+ "PostToolUse": [
46
+ {
47
+ "matcher": "mcp__hailer__scaffold_hailer_app",
48
+ "hooks": [
49
+ {
50
+ "type": "command",
51
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/post-scaffold-hook.cjs\"",
52
+ "timeout": 5
53
+ }
54
+ ]
33
55
  }
34
56
  ]
35
57
  }