@hailer/mcp 0.2.7 → 1.0.21

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 (40) hide show
  1. package/.claude/skills/client-bot-architecture/skill.md +340 -0
  2. package/.claude/skills/publish-hailer-app/SKILL.md +11 -0
  3. package/dist/app.d.ts +1 -1
  4. package/dist/app.js +116 -84
  5. package/dist/bot/chat-bot.d.ts +31 -0
  6. package/dist/bot/chat-bot.js +356 -0
  7. package/dist/cli.d.ts +9 -1
  8. package/dist/cli.js +71 -2
  9. package/dist/config.d.ts +15 -2
  10. package/dist/config.js +53 -3
  11. package/dist/lib/logger.js +11 -11
  12. package/dist/mcp/hailer-clients.js +12 -11
  13. package/dist/mcp/tool-registry.d.ts +4 -0
  14. package/dist/mcp/tool-registry.js +78 -1
  15. package/dist/mcp/tools/activity.js +47 -0
  16. package/dist/mcp/tools/discussion.js +44 -1
  17. package/dist/mcp/tools/metrics.d.ts +13 -0
  18. package/dist/mcp/tools/metrics.js +546 -0
  19. package/dist/mcp/tools/user.d.ts +1 -0
  20. package/dist/mcp/tools/user.js +94 -1
  21. package/dist/mcp/tools/workflow.js +109 -40
  22. package/dist/mcp/webhook-handler.js +7 -4
  23. package/dist/mcp-server.js +22 -6
  24. package/dist/stdio-server.d.ts +14 -0
  25. package/dist/stdio-server.js +101 -0
  26. package/package.json +6 -6
  27. package/scripts/test-hal-tools.ts +154 -0
  28. package/test-billing-server.js +136 -0
  29. package/dist/lib/discussion-lock.d.ts +0 -42
  30. package/dist/lib/discussion-lock.js +0 -110
  31. package/dist/mcp/tools/bot-config/constants.d.ts +0 -23
  32. package/dist/mcp/tools/bot-config/constants.js +0 -94
  33. package/dist/mcp/tools/bot-config/core.d.ts +0 -253
  34. package/dist/mcp/tools/bot-config/core.js +0 -2456
  35. package/dist/mcp/tools/bot-config/index.d.ts +0 -10
  36. package/dist/mcp/tools/bot-config/index.js +0 -59
  37. package/dist/mcp/tools/bot-config/tools.d.ts +0 -7
  38. package/dist/mcp/tools/bot-config/tools.js +0 -15
  39. package/dist/mcp/tools/bot-config/types.d.ts +0 -50
  40. package/dist/mcp/tools/bot-config/types.js +0 -6
@@ -91,23 +91,24 @@ class HailerClientManager {
91
91
  password: this.password,
92
92
  ...(isLocalDev && { rejectUnauthorized: false }), // Add for local dev with self-signed certs
93
93
  };
94
- // Track timeout for proper cleanup
94
+ // Track timeout with single settled flag to prevent race conditions
95
95
  let timeoutId = null;
96
- let timeoutCleared = false;
96
+ let settled = false;
97
97
  try {
98
98
  // Create socket client using @hailer/cli with cancellable timeout
99
99
  this.socketClient = (await Promise.race([
100
100
  cli_1.Client.create(clientOptions).then(client => {
101
- // Clear timeout on success
102
- if (timeoutId && !timeoutCleared) {
103
- clearTimeout(timeoutId);
104
- timeoutCleared = true;
101
+ if (!settled) {
102
+ settled = true;
103
+ if (timeoutId)
104
+ clearTimeout(timeoutId);
105
105
  }
106
106
  return client;
107
107
  }),
108
108
  new Promise((_, reject) => {
109
109
  timeoutId = setTimeout(() => {
110
- if (!timeoutCleared) {
110
+ if (!settled) {
111
+ settled = true;
111
112
  reject(new Error(`Timeout connecting to: ${this.host}`));
112
113
  }
113
114
  }, 30000);
@@ -115,10 +116,10 @@ class HailerClientManager {
115
116
  ]));
116
117
  }
117
118
  catch (error) {
118
- // Ensure timeout is cleared on error too
119
- if (timeoutId && !timeoutCleared) {
119
+ // Ensure timeout is cleared on error
120
+ if (timeoutId && !settled) {
121
+ settled = true;
120
122
  clearTimeout(timeoutId);
121
- timeoutCleared = true;
122
123
  }
123
124
  logger.error('Failed to create socket client', error, { username: this.username });
124
125
  throw error;
@@ -345,7 +346,7 @@ function registerBotCredentials(botId, email, password, options) {
345
346
  config_1.environment.CLIENT_CONFIGS[apiKey] = {
346
347
  email,
347
348
  password,
348
- apiBaseUrl: 'https://api.hailer.com',
349
+ apiBaseUrl: config_1.environment.BOT_API_BASE_URL || 'https://api.hailer.com',
349
350
  ...(options?.allowedGroups && { allowedGroups: options.allowedGroups }),
350
351
  };
351
352
  logger.info('Bot credentials registered', {
@@ -109,6 +109,10 @@ export declare class ToolRegistry {
109
109
  * Execute tool with validation
110
110
  */
111
111
  executeTool(name: string, args: any, context: UserContext): Promise<any>;
112
+ /**
113
+ * Pre-transform install_workflow args to fix common LLM mistakes BEFORE validation
114
+ */
115
+ private preTransformInstallWorkflow;
112
116
  /**
113
117
  * Get tool metadata (for access control checks)
114
118
  */
@@ -277,9 +277,14 @@ class ToolRegistry {
277
277
  name,
278
278
  apiKey: context.apiKey.substring(0, 8) + '...'
279
279
  });
280
+ // Pre-transform for specific tools (fix common LLM mistakes before validation)
281
+ let transformedArgs = args;
282
+ if (name === 'install_workflow' && args.workflowTemplates) {
283
+ transformedArgs = this.preTransformInstallWorkflow(args);
284
+ }
280
285
  // Zod validation
281
286
  try {
282
- const validated = tool.schema.parse(args);
287
+ const validated = tool.schema.parse(transformedArgs);
283
288
  return await tool.execute(validated, context);
284
289
  }
285
290
  catch (error) {
@@ -318,6 +323,78 @@ Then retry \`${name}\` with the correct format from the skill examples.` : `šŸ’”
318
323
  throw error;
319
324
  }
320
325
  }
326
+ /**
327
+ * Pre-transform install_workflow args to fix common LLM mistakes BEFORE validation
328
+ */
329
+ preTransformInstallWorkflow(args) {
330
+ let templates = args.workflowTemplates;
331
+ // Fix: single object instead of array
332
+ if (!Array.isArray(templates)) {
333
+ templates = [templates];
334
+ }
335
+ // Transform each template
336
+ templates = templates.map((template, tIdx) => {
337
+ const result = { ...template };
338
+ // Fix: fields as array instead of object
339
+ if (Array.isArray(template.fields)) {
340
+ const fieldsObj = {};
341
+ template.fields.forEach((field, idx) => {
342
+ const fieldId = `_${1000 + idx}`;
343
+ const { name, ...rest } = field;
344
+ fieldsObj[fieldId] = { ...rest, label: rest.label || name };
345
+ });
346
+ result.fields = fieldsObj;
347
+ }
348
+ else if (template.fields) {
349
+ // Fix: field IDs not matching pattern
350
+ const fieldsObj = {};
351
+ let idx = 0;
352
+ for (const [key, field] of Object.entries(template.fields)) {
353
+ const fieldId = /^_\d{4}$/.test(key) ? key : `_${1000 + idx}`;
354
+ const f = field;
355
+ const { name, ...rest } = f;
356
+ fieldsObj[fieldId] = { ...rest, label: rest.label || name || key };
357
+ idx++;
358
+ }
359
+ result.fields = fieldsObj;
360
+ }
361
+ // Fix: phases as array instead of object
362
+ if (Array.isArray(template.phases)) {
363
+ const phasesObj = {};
364
+ template.phases.forEach((phase, idx) => {
365
+ const phaseId = `_${2000 + idx}`;
366
+ phasesObj[phaseId] = { name: phase.name };
367
+ });
368
+ result.phases = phasesObj;
369
+ }
370
+ else if (template.phases) {
371
+ // Fix: phase IDs not matching pattern
372
+ const phasesObj = {};
373
+ let idx = 0;
374
+ for (const [key, phase] of Object.entries(template.phases)) {
375
+ const phaseId = /^_\d{4}$/.test(key) ? key : `_${2000 + idx}`;
376
+ const p = phase;
377
+ phasesObj[phaseId] = { name: p.name || key };
378
+ idx++;
379
+ }
380
+ result.phases = phasesObj;
381
+ }
382
+ // Fix: wrong field types
383
+ if (result.fields) {
384
+ for (const fieldId of Object.keys(result.fields)) {
385
+ const f = result.fields[fieldId];
386
+ // select → textpredefinedoptions
387
+ if (f.type === 'select')
388
+ f.type = 'textpredefinedoptions';
389
+ // user → users
390
+ if (f.type === 'user')
391
+ f.type = 'users';
392
+ }
393
+ }
394
+ return result;
395
+ });
396
+ return { ...args, workflowTemplates: templates };
397
+ }
321
398
  /**
322
399
  * Get tool metadata (for access control checks)
323
400
  */
@@ -658,6 +658,16 @@ exports.createActivityTool = {
658
658
  }),
659
659
  async execute(args, context) {
660
660
  try {
661
+ // DEBUG: Log raw input to diagnose LLM tool calling issues
662
+ logger.info("create_activity called", {
663
+ hasName: !!args.name,
664
+ hasActivities: !!args.activities,
665
+ activitiesType: typeof args.activities,
666
+ activitiesIsArray: Array.isArray(args.activities),
667
+ activitiesLength: Array.isArray(args.activities) ? args.activities.length :
668
+ (typeof args.activities === 'string' ? args.activities.length : 0),
669
+ rawArgs: JSON.stringify(args, null, 2).slice(0, 500), // First 500 chars to avoid huge logs
670
+ });
661
671
  // Helper function to process a single activity object
662
672
  const processActivity = (activityData) => {
663
673
  const activity = {
@@ -797,6 +807,43 @@ exports.createActivityTool = {
797
807
  options.discussionId = args.discussionId;
798
808
  }
799
809
  }
810
+ // Pre-validate required fields before API call
811
+ const workflow = context.init.processes?.find((p) => p._id === args.workflowId);
812
+ if (workflow?.fields) {
813
+ const requiredFields = [];
814
+ for (const [fieldId, field] of Object.entries(workflow.fields)) {
815
+ const f = field;
816
+ if (f.required) {
817
+ requiredFields.push({
818
+ id: fieldId,
819
+ label: f.label || f.key || fieldId,
820
+ key: f.key || fieldId,
821
+ });
822
+ }
823
+ }
824
+ if (requiredFields.length > 0) {
825
+ const missingByActivity = [];
826
+ for (let i = 0; i < activitiesToCreate.length; i++) {
827
+ const activity = activitiesToCreate[i];
828
+ const activityFields = activity.fields || {};
829
+ const missing = requiredFields.filter(rf => activityFields[rf.id] === undefined &&
830
+ activityFields[rf.key] === undefined);
831
+ if (missing.length > 0) {
832
+ const activityName = activity.name || `Activity ${i + 1}`;
833
+ missingByActivity.push(`${activityName}: missing ${missing.map(m => `"${m.label}" (${m.key})`).join(', ')}`);
834
+ }
835
+ }
836
+ if (missingByActivity.length > 0) {
837
+ const fieldList = requiredFields.map(f => `- ${f.label} (key: ${f.key}, id: ${f.id})`).join('\n');
838
+ return {
839
+ content: [{
840
+ type: "text",
841
+ text: `āŒ Missing required fields - cannot create activities.\n\n**Required fields for this workflow:**\n${fieldList}\n\n**Activities with missing fields:**\n${missingByActivity.map(m => `- ${m}`).join('\n')}\n\nšŸ’” Add the missing fields to each activity's \`fields\` object using either the field key or ID.`,
842
+ }],
843
+ };
844
+ }
845
+ }
846
+ }
800
847
  // Call API
801
848
  const result = await context.hailer.createActivities(args.workflowId, activitiesToCreate, options);
802
849
  // Handle response
@@ -14,6 +14,7 @@ const zod_1 = require("zod");
14
14
  const tool_registry_1 = require("../tool-registry");
15
15
  const index_1 = require("../utils/index");
16
16
  const workspace_cache_1 = require("../workspace-cache");
17
+ const hailer_clients_1 = require("../hailer-clients");
17
18
  const logger = (0, index_1.createLogger)({ component: 'discussion-tools' });
18
19
  // ============================================================================
19
20
  // HELPER FUNCTIONS
@@ -786,7 +787,17 @@ exports.joinDiscussionTool = {
786
787
  // ============================================================================
787
788
  // TOOL 5: LEAVE DISCUSSION
788
789
  // ============================================================================
789
- const leaveDiscussionDescription = `Leave a discussion - Works for activity discussions (provide activityId) or general discussions (provide discussionId)`;
790
+ const leaveDiscussionDescription = `Leave a discussion - Bot removes itself from a discussion.
791
+
792
+ **Parameters (at least one required):**
793
+ - \`discussionId\`: The discussion ID to leave (preferred for DMs and general discussions)
794
+ - \`activityId\`: The activity ID whose discussion to leave (for activity-linked discussions)
795
+
796
+ **Examples:**
797
+ - Leave by discussion ID: \`leave_discussion({ discussionId: "69525ce11d392644b7323050" })\`
798
+ - Leave by activity ID: \`leave_discussion({ activityId: "69525ce11d392644b732304f" })\`
799
+
800
+ **Note:** You MUST provide at least one parameter. The parameter cannot be inferred automatically.`;
790
801
  exports.leaveDiscussionTool = {
791
802
  name: 'leave_discussion',
792
803
  group: tool_registry_1.ToolGroup.WRITE,
@@ -806,6 +817,14 @@ exports.leaveDiscussionTool = {
806
817
  message: "Either activityId or discussionId must be provided"
807
818
  }),
808
819
  async execute(args, context) {
820
+ // DEBUG: Log raw input to diagnose LLM tool calling issues
821
+ logger.info("leave_discussion called", {
822
+ hasActivityId: !!args.activityId,
823
+ hasDiscussionId: !!args.discussionId,
824
+ activityId: args.activityId || '(not provided)',
825
+ discussionId: args.discussionId || '(not provided)',
826
+ rawArgs: JSON.stringify(args, null, 2),
827
+ });
809
828
  logger.debug('Leaving discussion', {
810
829
  activityId: args.activityId,
811
830
  discussionId: args.discussionId,
@@ -969,6 +988,30 @@ exports.inviteDiscussionMembersTool = {
969
988
  };
970
989
  }
971
990
  const existingMembers = new Set(discussion?.participants || []);
991
+ // Ensure the bot is a member before inviting others
992
+ // This allows the bot to post messages after inviting users
993
+ try {
994
+ const botUserId = await (0, hailer_clients_1.getCurrentUserId)(context.client);
995
+ if (botUserId && !existingMembers.has(botUserId)) {
996
+ try {
997
+ await context.hailer.joinDiscussion(args.discussionId);
998
+ logger.debug('Bot joined discussion before inviting', { discussionId: args.discussionId });
999
+ }
1000
+ catch (joinError) {
1001
+ // Non-fatal: bot might not be able to join some discussion types
1002
+ logger.debug('Bot join before invite failed (non-fatal)', {
1003
+ discussionId: args.discussionId,
1004
+ error: joinError instanceof Error ? joinError.message : String(joinError)
1005
+ });
1006
+ }
1007
+ }
1008
+ }
1009
+ catch (userIdError) {
1010
+ // Non-fatal: if we can't get the bot's user ID, continue with the invite
1011
+ logger.debug('Could not get bot user ID (non-fatal)', {
1012
+ error: userIdError instanceof Error ? userIdError.message : String(userIdError)
1013
+ });
1014
+ }
972
1015
  // Filter out users who are already members
973
1016
  const alreadyMembers = [];
974
1017
  const newUsers = [];
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Victoria Metrics Tools - Query and List Metrics
3
+ *
4
+ * Tools for querying Hailer metrics from Victoria Metrics:
5
+ * - Query metrics with PromQL (READ)
6
+ * - List available metrics (READ)
7
+ */
8
+ import { Tool } from '../tool-registry';
9
+ export declare const queryMetricTool: Tool;
10
+ export declare const listMetricsTool: Tool;
11
+ export declare const searchWorkspaceForMetricsTool: Tool;
12
+ export declare const searchUserForMetricsTool: Tool;
13
+ //# sourceMappingURL=metrics.d.ts.map