@hailer/mcp 0.1.14 → 0.1.16

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 (112) hide show
  1. package/.claude/agents/agent-giuseppe-app-builder.md +7 -6
  2. package/.claude/agents/agent-lars-code-inspector.md +26 -14
  3. package/dist/agents/bot-manager.d.ts +48 -0
  4. package/dist/agents/bot-manager.js +254 -0
  5. package/dist/agents/factory.d.ts +150 -0
  6. package/dist/agents/factory.js +650 -0
  7. package/dist/agents/giuseppe/ai.d.ts +83 -0
  8. package/dist/agents/giuseppe/ai.js +466 -0
  9. package/dist/agents/giuseppe/bot.d.ts +110 -0
  10. package/dist/agents/giuseppe/bot.js +780 -0
  11. package/dist/agents/giuseppe/config.d.ts +25 -0
  12. package/dist/agents/giuseppe/config.js +227 -0
  13. package/dist/agents/giuseppe/files.d.ts +52 -0
  14. package/dist/agents/giuseppe/files.js +338 -0
  15. package/dist/agents/giuseppe/git.d.ts +48 -0
  16. package/dist/agents/giuseppe/git.js +298 -0
  17. package/dist/agents/giuseppe/index.d.ts +97 -0
  18. package/dist/agents/giuseppe/index.js +258 -0
  19. package/dist/agents/giuseppe/lsp.d.ts +113 -0
  20. package/dist/agents/giuseppe/lsp.js +485 -0
  21. package/dist/agents/giuseppe/monitor.d.ts +118 -0
  22. package/dist/agents/giuseppe/monitor.js +621 -0
  23. package/dist/agents/giuseppe/prompt.d.ts +5 -0
  24. package/dist/agents/giuseppe/prompt.js +94 -0
  25. package/dist/agents/giuseppe/registries/pending-classification.d.ts +28 -0
  26. package/dist/agents/giuseppe/registries/pending-classification.js +50 -0
  27. package/dist/agents/giuseppe/registries/pending-fix.d.ts +30 -0
  28. package/dist/agents/giuseppe/registries/pending-fix.js +42 -0
  29. package/dist/agents/giuseppe/registries/pending.d.ts +27 -0
  30. package/dist/agents/giuseppe/registries/pending.js +49 -0
  31. package/dist/agents/giuseppe/specialist.d.ts +47 -0
  32. package/dist/agents/giuseppe/specialist.js +237 -0
  33. package/dist/agents/giuseppe/types.d.ts +123 -0
  34. package/dist/agents/giuseppe/types.js +9 -0
  35. package/dist/agents/hailer-expert/index.d.ts +8 -0
  36. package/dist/agents/hailer-expert/index.js +14 -0
  37. package/dist/agents/hal/daemon.d.ts +142 -0
  38. package/dist/agents/hal/daemon.js +1103 -0
  39. package/dist/agents/hal/definitions.d.ts +55 -0
  40. package/dist/agents/hal/definitions.js +263 -0
  41. package/dist/agents/hal/index.d.ts +3 -0
  42. package/dist/agents/hal/index.js +8 -0
  43. package/dist/agents/index.d.ts +18 -0
  44. package/dist/agents/index.js +48 -0
  45. package/dist/agents/shared/base.d.ts +216 -0
  46. package/dist/agents/shared/base.js +846 -0
  47. package/dist/agents/shared/services/agent-registry.d.ts +107 -0
  48. package/dist/agents/shared/services/agent-registry.js +629 -0
  49. package/dist/agents/shared/services/conversation-manager.d.ts +50 -0
  50. package/dist/agents/shared/services/conversation-manager.js +136 -0
  51. package/dist/agents/shared/services/mcp-client.d.ts +56 -0
  52. package/dist/agents/shared/services/mcp-client.js +124 -0
  53. package/dist/agents/shared/services/message-classifier.d.ts +37 -0
  54. package/dist/agents/shared/services/message-classifier.js +187 -0
  55. package/dist/agents/shared/services/message-formatter.d.ts +89 -0
  56. package/dist/agents/shared/services/message-formatter.js +371 -0
  57. package/dist/agents/shared/services/session-logger.d.ts +106 -0
  58. package/dist/agents/shared/services/session-logger.js +446 -0
  59. package/dist/agents/shared/services/tool-executor.d.ts +41 -0
  60. package/dist/agents/shared/services/tool-executor.js +169 -0
  61. package/dist/agents/shared/services/workspace-schema-cache.d.ts +125 -0
  62. package/dist/agents/shared/services/workspace-schema-cache.js +578 -0
  63. package/dist/agents/shared/specialist.d.ts +91 -0
  64. package/dist/agents/shared/specialist.js +399 -0
  65. package/dist/agents/shared/tool-schema-loader.d.ts +62 -0
  66. package/dist/agents/shared/tool-schema-loader.js +232 -0
  67. package/dist/agents/shared/types.d.ts +327 -0
  68. package/dist/agents/shared/types.js +121 -0
  69. package/dist/app.js +21 -4
  70. package/dist/cli.js +0 -0
  71. package/dist/client/agents/orchestrator.d.ts +1 -0
  72. package/dist/client/agents/orchestrator.js +12 -1
  73. package/dist/commands/seed-config.d.ts +9 -0
  74. package/dist/commands/seed-config.js +372 -0
  75. package/dist/config.d.ts +10 -0
  76. package/dist/config.js +61 -1
  77. package/dist/core.d.ts +8 -0
  78. package/dist/core.js +137 -6
  79. package/dist/lib/discussion-lock.d.ts +42 -0
  80. package/dist/lib/discussion-lock.js +110 -0
  81. package/dist/mcp/UserContextCache.js +2 -2
  82. package/dist/mcp/hailer-clients.d.ts +15 -0
  83. package/dist/mcp/hailer-clients.js +100 -6
  84. package/dist/mcp/signal-handler.d.ts +16 -5
  85. package/dist/mcp/signal-handler.js +173 -122
  86. package/dist/mcp/tools/activity.js +9 -1
  87. package/dist/mcp/tools/bot-config.d.ts +184 -9
  88. package/dist/mcp/tools/bot-config.js +2177 -163
  89. package/dist/mcp/tools/giuseppe-tools.d.ts +21 -0
  90. package/dist/mcp/tools/giuseppe-tools.js +525 -0
  91. package/dist/mcp/utils/hailer-api-client.d.ts +42 -1
  92. package/dist/mcp/utils/hailer-api-client.js +128 -2
  93. package/dist/mcp/webhook-handler.d.ts +87 -0
  94. package/dist/mcp/webhook-handler.js +343 -0
  95. package/dist/mcp/workspace-cache.d.ts +5 -0
  96. package/dist/mcp/workspace-cache.js +11 -0
  97. package/dist/mcp-server.js +55 -5
  98. package/dist/modules/bug-reports/giuseppe-agent.d.ts +58 -0
  99. package/dist/modules/bug-reports/giuseppe-agent.js +467 -0
  100. package/dist/modules/bug-reports/giuseppe-ai.d.ts +25 -1
  101. package/dist/modules/bug-reports/giuseppe-ai.js +133 -2
  102. package/dist/modules/bug-reports/giuseppe-bot.d.ts +3 -2
  103. package/dist/modules/bug-reports/giuseppe-bot.js +75 -36
  104. package/dist/modules/bug-reports/giuseppe-daemon.d.ts +80 -0
  105. package/dist/modules/bug-reports/giuseppe-daemon.js +617 -0
  106. package/dist/modules/bug-reports/giuseppe-files.d.ts +12 -0
  107. package/dist/modules/bug-reports/giuseppe-files.js +37 -0
  108. package/dist/modules/bug-reports/giuseppe-lsp.d.ts +113 -0
  109. package/dist/modules/bug-reports/giuseppe-lsp.js +485 -0
  110. package/dist/modules/bug-reports/index.d.ts +1 -0
  111. package/dist/modules/bug-reports/index.js +31 -29
  112. package/package.json +5 -4
@@ -14,6 +14,12 @@ class HailerApiClient {
14
14
  constructor(clients) {
15
15
  this.clients = clients;
16
16
  }
17
+ /**
18
+ * Get the underlying HailerClient for direct access (e.g., SignalHandler)
19
+ */
20
+ getClient() {
21
+ return this.clients;
22
+ }
17
23
  /**
18
24
  * Makes a socket API call - thin wrapper around clients.socket.request
19
25
  */
@@ -77,6 +83,7 @@ class HailerApiClient {
77
83
  sortOrder: options?.sortOrder || "desc",
78
84
  includeStats: options?.includeStats !== undefined ? options?.includeStats : true,
79
85
  returnFlat: options?.returnFlat !== undefined ? options?.returnFlat : true,
86
+ fields: options?.fields || undefined,
80
87
  };
81
88
  this.logger.debug('📋 V3 Activity List API Call', {
82
89
  endpoint: 'v3.activity.list',
@@ -201,10 +208,35 @@ class HailerApiClient {
201
208
  }
202
209
  /**
203
210
  * Fetch activity by ID - uses Socket API
211
+ * WARNING: This uses activities.load which evaluates function fields and may fail
212
+ * For bot config, prefer fetchActivityByIdSafe()
204
213
  */
205
214
  async fetchActivityById(activityId) {
206
215
  return await this.request('activities.load', [activityId]);
207
216
  }
217
+ /**
218
+ * Fetch activity by ID safely using v3.activity.list API
219
+ * This doesn't evaluate function fields so it works even when function fields are broken
220
+ */
221
+ async fetchActivityByIdSafe(activityId, workflowId, phaseId) {
222
+ // v3.activity.list doesn't support filtering by _id, so we list and filter client-side
223
+ if (!workflowId || !phaseId) {
224
+ throw new Error('fetchActivityByIdSafe requires workflowId and phaseId');
225
+ }
226
+ const query = { processId: workflowId, phaseId };
227
+ const options = { limit: 100, returnFlat: true };
228
+ const result = await this.request('v3.activity.list', [query, options]);
229
+ const activities = result?.activities || result?.list || result?.data || [];
230
+ this.logger.debug('fetchActivityByIdSafe response', {
231
+ activityId,
232
+ workflowId,
233
+ phaseId,
234
+ resultKeys: Object.keys(result || {}),
235
+ activityCount: activities.length,
236
+ activityIds: activities.slice(0, 5).map((a) => a._id)
237
+ });
238
+ return activities.find((a) => a._id === activityId) || null;
239
+ }
208
240
  /**
209
241
  * Send discussion message - uses Socket API
210
242
  */
@@ -291,9 +323,103 @@ class HailerApiClient {
291
323
  }
292
324
  /**
293
325
  * Fetch workspace initialization data - uses Socket API
326
+ * @param include - What to include: 'users', 'network', 'networks', 'teams', etc.
327
+ */
328
+ async fetchInit(include = ['users', 'network']) {
329
+ return await this.request('v2.core.init', [include]);
330
+ }
331
+ /**
332
+ * Update current user's profile info (firstname, lastname, etc.)
333
+ * Only works for the currently authenticated user
334
+ * @param key - Field to update: firstname, lastname, email, status, etc.
335
+ * @param value - New value as string
336
+ */
337
+ async setUserInfo(key, value) {
338
+ await this.request('user.set_user_info', [key, [value]]);
339
+ this.logger.debug('User info updated', { key, value });
340
+ }
341
+ /**
342
+ * Update current user's display name (firstname + lastname)
343
+ * Parses a full name into first/last parts
344
+ * @param fullName - Full display name like "HAL 9000"
345
+ */
346
+ async setUserDisplayName(fullName) {
347
+ const parts = fullName.trim().split(/\s+/);
348
+ const firstname = parts[0] || fullName;
349
+ const lastname = parts.slice(1).join(' ') || '';
350
+ await this.setUserInfo('firstname', firstname);
351
+ if (lastname) {
352
+ await this.setUserInfo('lastname', lastname);
353
+ }
354
+ this.logger.info('User display name updated', { fullName, firstname, lastname });
355
+ }
356
+ /**
357
+ * Find user by name - searches init data for firstname/lastname match
358
+ * @param name - Full name to search for (firstname lastname)
359
+ * @returns User ID if found, null otherwise
360
+ */
361
+ async findUserByName(name) {
362
+ try {
363
+ const init = await this.fetchInit(['users']);
364
+ const users = Object.values(init?.users || {});
365
+ const searchName = name.toLowerCase().trim();
366
+ // Try to find user by full name match
367
+ const matchingUser = users.find(u => {
368
+ const fullName = `${u.firstname || ''} ${u.lastname || ''}`.toLowerCase().trim();
369
+ return fullName === searchName;
370
+ });
371
+ if (matchingUser) {
372
+ this.logger.debug('findUserByName: found user', { name, userId: matchingUser._id });
373
+ return matchingUser._id;
374
+ }
375
+ return null;
376
+ }
377
+ catch (error) {
378
+ this.logger.debug('findUserByName failed', { name, error });
379
+ return null;
380
+ }
381
+ }
382
+ /**
383
+ * List all workflows in the workspace - extracts from init data
384
+ * @param workspaceId - Optional workspace ID to filter workflows (uses CID field)
294
385
  */
295
- async fetchInit() {
296
- return await this.request('v2.core.init', [{}]);
386
+ async listWorkflows(workspaceId) {
387
+ // Must include 'processes' to get workflows
388
+ const init = await this.fetchInit(['users', 'network', 'processes']);
389
+ // Init returns processes (workflows) as an object keyed by process ID
390
+ if (init?.processes) {
391
+ let workflows = Object.entries(init.processes).map(([id, process]) => ({
392
+ _id: id,
393
+ ...process
394
+ }));
395
+ // Filter by workspace ID if specified (fixes API caching issue after workspace switch)
396
+ if (workspaceId) {
397
+ workflows = workflows.filter(w => w.cid === workspaceId);
398
+ this.logger.debug('Filtered workflows by workspace', {
399
+ workspaceId,
400
+ totalCount: Object.keys(init.processes).length,
401
+ filteredCount: workflows.length
402
+ });
403
+ }
404
+ return workflows;
405
+ }
406
+ return [];
407
+ }
408
+ /**
409
+ * Get workflow details including phases and fields
410
+ */
411
+ async getWorkflow(workflowId) {
412
+ // Must include 'processes' to get workflow data
413
+ const init = await this.fetchInit(['processes']);
414
+ if (init?.processes?.[workflowId]) {
415
+ return {
416
+ _id: workflowId,
417
+ ...init.processes[workflowId],
418
+ phases: init.processes[workflowId].phases || {},
419
+ fields: init.processes[workflowId].fields || {}
420
+ };
421
+ }
422
+ throw new Error(`Workflow ${workflowId} not found`);
297
423
  }
298
424
  /**
299
425
  * Global search across workspace - uses Socket API
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Webhook Handler for Bot Config Updates
3
+ *
4
+ * Receives activity updates from Hailer workflow webhooks and updates
5
+ * local .bot-config/{workspaceId}.json files.
6
+ */
7
+ /**
8
+ * Generate HMAC-SHA256 signature for webhook payload
9
+ */
10
+ export declare function generateWebhookSignature(payload: string, secret: string): string;
11
+ /**
12
+ * Verify HMAC-SHA256 signature of webhook payload
13
+ * @returns true if signature is valid, false otherwise
14
+ */
15
+ export declare function verifyWebhookSignature(payload: string, signature: string, secret: string): boolean;
16
+ /**
17
+ * Get webhook token for secure endpoints.
18
+ *
19
+ * Production: WEBHOOK_TOKEN env var (injected by AWS Secrets Manager)
20
+ * Development: Falls back to file-based token for local testing
21
+ */
22
+ export declare function getWebhookToken(): string;
23
+ /**
24
+ * Get the full webhook path with token (no prefix for security through obscurity)
25
+ */
26
+ export declare function getWebhookPath(): string;
27
+ interface WebhookField {
28
+ id: string;
29
+ type: string;
30
+ value: any;
31
+ key: string;
32
+ }
33
+ interface WebhookPayload {
34
+ _id: string;
35
+ name: string;
36
+ fields: WebhookField[];
37
+ currentPhase: string;
38
+ process: string;
39
+ cid: string;
40
+ uid: string;
41
+ created: number;
42
+ updated: number;
43
+ }
44
+ interface BotEntry {
45
+ activityId: string;
46
+ userId: string | null;
47
+ email: string;
48
+ password: string;
49
+ botType: string;
50
+ enabled: boolean;
51
+ displayName?: string;
52
+ }
53
+ interface WorkspaceConfig {
54
+ workspaceId: string;
55
+ workspaceName: string;
56
+ orchestrator?: {
57
+ activityId: string;
58
+ userId: string;
59
+ email: string;
60
+ password: string;
61
+ displayName?: string;
62
+ };
63
+ specialists: BotEntry[];
64
+ lastSynced: string;
65
+ }
66
+ type BotUpdateCallback = (workspaceId: string, bot: BotEntry, action: 'add' | 'update' | 'remove') => void;
67
+ export declare function onBotUpdate(callback: BotUpdateCallback): void;
68
+ /**
69
+ * Process webhook payload and update workspace config
70
+ */
71
+ export declare function handleBotConfigWebhook(payload: WebhookPayload): {
72
+ success: boolean;
73
+ action: string;
74
+ workspaceId: string;
75
+ botType: string | null;
76
+ error?: string;
77
+ };
78
+ /**
79
+ * Get workspace config (for debugging/status)
80
+ */
81
+ export declare function getWorkspaceConfig(workspaceId: string): WorkspaceConfig | null;
82
+ /**
83
+ * List all workspace configs
84
+ */
85
+ export declare function listWorkspaceConfigs(): WorkspaceConfig[];
86
+ export {};
87
+ //# sourceMappingURL=webhook-handler.d.ts.map
@@ -0,0 +1,343 @@
1
+ "use strict";
2
+ /**
3
+ * Webhook Handler for Bot Config Updates
4
+ *
5
+ * Receives activity updates from Hailer workflow webhooks and updates
6
+ * local .bot-config/{workspaceId}.json files.
7
+ */
8
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
+ if (k2 === undefined) k2 = k;
10
+ var desc = Object.getOwnPropertyDescriptor(m, k);
11
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
12
+ desc = { enumerable: true, get: function() { return m[k]; } };
13
+ }
14
+ Object.defineProperty(o, k2, desc);
15
+ }) : (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ o[k2] = m[k];
18
+ }));
19
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
20
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
21
+ }) : function(o, v) {
22
+ o["default"] = v;
23
+ });
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.generateWebhookSignature = generateWebhookSignature;
43
+ exports.verifyWebhookSignature = verifyWebhookSignature;
44
+ exports.getWebhookToken = getWebhookToken;
45
+ exports.getWebhookPath = getWebhookPath;
46
+ exports.onBotUpdate = onBotUpdate;
47
+ exports.handleBotConfigWebhook = handleBotConfigWebhook;
48
+ exports.getWorkspaceConfig = getWorkspaceConfig;
49
+ exports.listWorkspaceConfigs = listWorkspaceConfigs;
50
+ const fs = __importStar(require("fs"));
51
+ const path = __importStar(require("path"));
52
+ const crypto = __importStar(require("crypto"));
53
+ const logger_1 = require("../lib/logger");
54
+ const config_1 = require("../config");
55
+ const logger = (0, logger_1.createLogger)({ component: 'webhook-handler' });
56
+ const BOT_CONFIG_DIR = '.bot-config';
57
+ const WEBHOOK_SECRET_FILE = 'webhook-secret.txt';
58
+ // ============================================================================
59
+ // WEBHOOK TOKEN SECURITY
60
+ // ============================================================================
61
+ /**
62
+ * Constant-time string comparison to prevent timing attacks
63
+ */
64
+ function timingSafeEqual(a, b) {
65
+ if (a.length !== b.length) {
66
+ // Still perform comparison to maintain constant time
67
+ crypto.timingSafeEqual(Buffer.from(a), Buffer.from(a));
68
+ return false;
69
+ }
70
+ return crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b));
71
+ }
72
+ /**
73
+ * Generate HMAC-SHA256 signature for webhook payload
74
+ */
75
+ function generateWebhookSignature(payload, secret) {
76
+ return crypto.createHmac('sha256', secret).update(payload).digest('hex');
77
+ }
78
+ /**
79
+ * Verify HMAC-SHA256 signature of webhook payload
80
+ * @returns true if signature is valid, false otherwise
81
+ */
82
+ function verifyWebhookSignature(payload, signature, secret) {
83
+ if (!signature || !secret) {
84
+ logger.warn('Missing signature or secret for webhook verification');
85
+ return false;
86
+ }
87
+ const expectedSignature = generateWebhookSignature(payload, secret);
88
+ // Use constant-time comparison to prevent timing attacks
89
+ const isValid = timingSafeEqual(signature, expectedSignature);
90
+ if (!isValid) {
91
+ logger.warn('Invalid webhook signature', {
92
+ provided: signature.slice(0, 8) + '...',
93
+ expected: expectedSignature.slice(0, 8) + '...',
94
+ });
95
+ }
96
+ return isValid;
97
+ }
98
+ /**
99
+ * Get webhook token for secure endpoints.
100
+ *
101
+ * Production: WEBHOOK_TOKEN env var (injected by AWS Secrets Manager)
102
+ * Development: Falls back to file-based token for local testing
103
+ */
104
+ function getWebhookToken() {
105
+ // Production: Always use environment variable (AWS Secrets Manager injects this)
106
+ if (process.env.WEBHOOK_TOKEN) {
107
+ logger.debug('Using webhook token from environment');
108
+ return process.env.WEBHOOK_TOKEN;
109
+ }
110
+ // Development only: File-based fallback
111
+ if (process.env.NODE_ENV === 'production') {
112
+ throw new Error('WEBHOOK_TOKEN environment variable is required in production');
113
+ }
114
+ const configDir = path.join(process.cwd(), BOT_CONFIG_DIR);
115
+ const secretPath = path.join(configDir, WEBHOOK_SECRET_FILE);
116
+ // Try to read existing dev token
117
+ if (fs.existsSync(secretPath)) {
118
+ try {
119
+ const token = fs.readFileSync(secretPath, 'utf-8').trim();
120
+ if (token.length >= 16) {
121
+ logger.debug('Using webhook token from file (dev mode)');
122
+ return token;
123
+ }
124
+ }
125
+ catch (error) {
126
+ logger.warn('Failed to read webhook secret file', { error });
127
+ }
128
+ }
129
+ // Generate new dev token
130
+ const token = crypto.randomBytes(16).toString('hex');
131
+ if (!fs.existsSync(configDir)) {
132
+ fs.mkdirSync(configDir, { recursive: true });
133
+ }
134
+ fs.writeFileSync(secretPath, token, { mode: 0o600 });
135
+ const maskedToken = `${token.slice(0, 4)}...${token.slice(-4)}`;
136
+ logger.info('Generated dev webhook token', { maskedToken, path: secretPath });
137
+ return token;
138
+ }
139
+ /**
140
+ * Get the full webhook path with token (no prefix for security through obscurity)
141
+ */
142
+ function getWebhookPath() {
143
+ return `/${getWebhookToken()}`;
144
+ }
145
+ let botUpdateCallback = null;
146
+ function onBotUpdate(callback) {
147
+ botUpdateCallback = callback;
148
+ }
149
+ /**
150
+ * Get field value from webhook payload by key
151
+ */
152
+ function getFieldValue(fields, key) {
153
+ const field = fields.find((f) => f.key === key);
154
+ return field?.value ?? null;
155
+ }
156
+ /**
157
+ * Load existing workspace config or create empty one
158
+ */
159
+ function loadWorkspaceConfig(workspaceId) {
160
+ const configDir = path.join(process.cwd(), BOT_CONFIG_DIR);
161
+ const configPath = path.join(configDir, `${workspaceId}.json`);
162
+ if (fs.existsSync(configPath)) {
163
+ try {
164
+ return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
165
+ }
166
+ catch (error) {
167
+ logger.warn('Failed to load workspace config', { workspaceId, error: String(error) });
168
+ }
169
+ }
170
+ return {
171
+ workspaceId,
172
+ workspaceName: workspaceId,
173
+ specialists: [],
174
+ lastSynced: new Date().toISOString(),
175
+ };
176
+ }
177
+ /**
178
+ * Save workspace config to file
179
+ */
180
+ function saveWorkspaceConfig(config) {
181
+ const configDir = path.join(process.cwd(), BOT_CONFIG_DIR);
182
+ // Ensure directory exists
183
+ if (!fs.existsSync(configDir)) {
184
+ fs.mkdirSync(configDir, { recursive: true });
185
+ }
186
+ const configPath = path.join(configDir, `${config.workspaceId}.json`);
187
+ config.lastSynced = new Date().toISOString();
188
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
189
+ logger.info('Saved workspace config', {
190
+ workspaceId: config.workspaceId,
191
+ path: configPath,
192
+ });
193
+ }
194
+ /**
195
+ * Process webhook payload and update workspace config
196
+ */
197
+ function handleBotConfigWebhook(payload) {
198
+ const workspaceId = payload.cid;
199
+ logger.info('Processing bot config webhook', {
200
+ activityId: payload._id,
201
+ activityName: payload.name,
202
+ workspaceId,
203
+ phase: payload.currentPhase,
204
+ });
205
+ // Extract fields
206
+ const email = getFieldValue(payload.fields, 'agentEmailInHailer');
207
+ const password = getFieldValue(payload.fields, 'password');
208
+ const botType = getFieldValue(payload.fields, 'botType');
209
+ const userId = getFieldValue(payload.fields, 'hailerProfile');
210
+ const schemaConfigStr = getFieldValue(payload.fields, 'schemaConfig');
211
+ // Validate required fields
212
+ if (!email || !password) {
213
+ logger.warn('Webhook missing credentials', {
214
+ activityId: payload._id,
215
+ hasEmail: !!email,
216
+ hasPassword: !!password,
217
+ });
218
+ return {
219
+ success: false,
220
+ action: 'skip',
221
+ workspaceId,
222
+ botType,
223
+ error: 'Missing email or password',
224
+ };
225
+ }
226
+ // Parse schema config to determine if deployed or retired
227
+ let deployedPhaseId = null;
228
+ let retiredPhaseId = null;
229
+ if (schemaConfigStr) {
230
+ try {
231
+ const schemaConfig = JSON.parse(schemaConfigStr);
232
+ deployedPhaseId = schemaConfig.deployedPhaseId;
233
+ retiredPhaseId = schemaConfig.retiredPhaseId;
234
+ }
235
+ catch (e) {
236
+ logger.warn('Failed to parse schemaConfig', { schemaConfigStr });
237
+ }
238
+ }
239
+ const isDeployed = deployedPhaseId ? payload.currentPhase === deployedPhaseId : true;
240
+ const isRetired = retiredPhaseId ? payload.currentPhase === retiredPhaseId : false;
241
+ const enabled = isDeployed && !isRetired;
242
+ // Load existing config
243
+ const config = loadWorkspaceConfig(workspaceId);
244
+ const botEntry = {
245
+ activityId: payload._id,
246
+ userId: userId || null,
247
+ email,
248
+ password,
249
+ botType: botType || 'unknown',
250
+ enabled,
251
+ displayName: payload.name, // Activity name from Agent Directory
252
+ };
253
+ let action;
254
+ // Handle orchestrator
255
+ if (botType === 'orchestrator') {
256
+ if (enabled) {
257
+ config.orchestrator = {
258
+ activityId: payload._id,
259
+ userId: userId || '',
260
+ email,
261
+ password,
262
+ displayName: payload.name,
263
+ };
264
+ action = 'update';
265
+ logger.info('Updated orchestrator', { workspaceId, email: (0, config_1.maskEmail)(email), displayName: payload.name });
266
+ }
267
+ else {
268
+ // Orchestrator disabled - remove it
269
+ if (config.orchestrator?.activityId === payload._id) {
270
+ delete config.orchestrator;
271
+ action = 'remove';
272
+ logger.info('Removed orchestrator', { workspaceId, email: (0, config_1.maskEmail)(email) });
273
+ }
274
+ else {
275
+ action = 'update';
276
+ }
277
+ }
278
+ }
279
+ else {
280
+ // Handle specialist
281
+ const existingIndex = config.specialists.findIndex((s) => s.activityId === payload._id);
282
+ if (existingIndex >= 0) {
283
+ // Update existing
284
+ config.specialists[existingIndex] = botEntry;
285
+ action = enabled ? 'update' : 'remove';
286
+ }
287
+ else if (enabled) {
288
+ // Add new
289
+ config.specialists.push(botEntry);
290
+ action = 'add';
291
+ }
292
+ else {
293
+ action = 'update';
294
+ }
295
+ logger.info('Updated specialist', {
296
+ workspaceId,
297
+ email,
298
+ botType,
299
+ enabled,
300
+ action,
301
+ });
302
+ }
303
+ // Save config
304
+ saveWorkspaceConfig(config);
305
+ // Trigger callback for hot reload
306
+ if (botUpdateCallback) {
307
+ botUpdateCallback(workspaceId, botEntry, action);
308
+ }
309
+ return {
310
+ success: true,
311
+ action,
312
+ workspaceId,
313
+ botType,
314
+ };
315
+ }
316
+ /**
317
+ * Get workspace config (for debugging/status)
318
+ */
319
+ function getWorkspaceConfig(workspaceId) {
320
+ const config = loadWorkspaceConfig(workspaceId);
321
+ return config.orchestrator || config.specialists.length > 0 ? config : null;
322
+ }
323
+ /**
324
+ * List all workspace configs
325
+ */
326
+ function listWorkspaceConfigs() {
327
+ const configDir = path.join(process.cwd(), BOT_CONFIG_DIR);
328
+ if (!fs.existsSync(configDir))
329
+ return [];
330
+ const files = fs.readdirSync(configDir).filter((f) => f.endsWith('.json'));
331
+ const configs = [];
332
+ for (const file of files) {
333
+ try {
334
+ const content = fs.readFileSync(path.join(configDir, file), 'utf-8');
335
+ configs.push(JSON.parse(content));
336
+ }
337
+ catch (error) {
338
+ logger.warn('Failed to load workspace config', { file, error: String(error) });
339
+ }
340
+ }
341
+ return configs;
342
+ }
343
+ //# sourceMappingURL=webhook-handler.js.map
@@ -5,6 +5,7 @@ export interface UserInfo {
5
5
  firstname: string;
6
6
  lastname: string;
7
7
  fullName: string;
8
+ email?: string;
8
9
  companies: string[];
9
10
  default_profilepic?: string;
10
11
  lastSeen: number;
@@ -31,6 +32,10 @@ export declare function createWorkspaceCache(init: HailerV2CoreInitResponse, con
31
32
  * Gets user information by ID
32
33
  */
33
34
  export declare function getUserById(cache: WorkspaceCache, userId: string): UserInfo | undefined;
35
+ /**
36
+ * Gets user information by email (case-insensitive)
37
+ */
38
+ export declare function getUserByEmail(cache: WorkspaceCache, email: string): UserInfo | undefined;
34
39
  /**
35
40
  * Gets workspace by name (case-insensitive)
36
41
  */
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createWorkspaceCache = createWorkspaceCache;
4
4
  exports.getUserById = getUserById;
5
+ exports.getUserByEmail = getUserByEmail;
5
6
  exports.getWorkspaceByName = getWorkspaceByName;
6
7
  exports.resolveWorkspaceId = resolveWorkspaceId;
7
8
  /**
@@ -33,11 +34,14 @@ function createWorkspaceCache(init, config) {
33
34
  const users = [];
34
35
  const usersById = {};
35
36
  Object.values(init.users || {}).forEach((user) => {
37
+ // Cast to any to access email field (API returns it but type doesn't include it)
38
+ const userAny = user;
36
39
  let userInfo = {
37
40
  id: user._id,
38
41
  firstname: user.firstname,
39
42
  lastname: user.lastname,
40
43
  fullName: `${user.firstname} ${user.lastname}`,
44
+ email: userAny.email, // Include email if available
41
45
  companies: user.companies || [],
42
46
  default_profilepic: config.compactUserData ? undefined : user.default_profilepic,
43
47
  lastSeen: config.compactUserData ? 0 : (user.lastSeen || 0),
@@ -76,6 +80,13 @@ function createWorkspaceCache(init, config) {
76
80
  function getUserById(cache, userId) {
77
81
  return cache.usersById[userId];
78
82
  }
83
+ /**
84
+ * Gets user information by email (case-insensitive)
85
+ */
86
+ function getUserByEmail(cache, email) {
87
+ const lowerEmail = email.toLowerCase();
88
+ return cache.users.find(user => user.email?.toLowerCase() === lowerEmail);
89
+ }
79
90
  /**
80
91
  * Gets workspace by name (case-insensitive)
81
92
  */