@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
@@ -1,2456 +0,0 @@
1
- "use strict";
2
- /**
3
- * Bot Configuration MCP Tools
4
- *
5
- * Manages which bots are enabled/disabled in the workspace.
6
- * Config is stored in the Agent Directory workflow - agents are activities.
7
- * - Deployed agents phase = enabled
8
- * - Retired agents phase = disabled
9
- *
10
- * Architecture (User ID-based discovery):
11
- * - In-memory state is primary (for speed), keyed by USER ID
12
- * - Agent Directory workflow is persistence layer (agents as activities, phases for state)
13
- * - On startup: Load config from Hailer by extracting hailerProfile (user ID) from each agent
14
- * - On change: Move agent activity between phases
15
- * - Credentials: Auto-synced from Hailer to local storage on discovery
16
- * - Orchestrator: Simply the first deployed bot with credentials (no special field needed)
17
- */
18
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
19
- if (k2 === undefined) k2 = k;
20
- var desc = Object.getOwnPropertyDescriptor(m, k);
21
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
22
- desc = { enumerable: true, get: function() { return m[k]; } };
23
- }
24
- Object.defineProperty(o, k2, desc);
25
- }) : (function(o, m, k, k2) {
26
- if (k2 === undefined) k2 = k;
27
- o[k2] = m[k];
28
- }));
29
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
30
- Object.defineProperty(o, "default", { enumerable: true, value: v });
31
- }) : function(o, v) {
32
- o["default"] = v;
33
- });
34
- var __importStar = (this && this.__importStar) || (function () {
35
- var ownKeys = function(o) {
36
- ownKeys = Object.getOwnPropertyNames || function (o) {
37
- var ar = [];
38
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
39
- return ar;
40
- };
41
- return ownKeys(o);
42
- };
43
- return function (mod) {
44
- if (mod && mod.__esModule) return mod;
45
- var result = {};
46
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
47
- __setModuleDefault(result, mod);
48
- return result;
49
- };
50
- })();
51
- Object.defineProperty(exports, "__esModule", { value: true });
52
- exports.botConfigTools = exports.checkSpecialistStatusTool = exports.disableBotTool = exports.enableBotTool = exports.listBotsConfigTool = exports.AVAILABLE_BOTS = void 0;
53
- exports.getWorkspaceByWorkflowId = getWorkspaceByWorkflowId;
54
- exports.getAllConfiguredWorkspaces = getAllConfiguredWorkspaces;
55
- exports.setActiveWorkspace = setActiveWorkspace;
56
- exports.getActiveWorkspace = getActiveWorkspace;
57
- exports.getWorkspaceSchema = getWorkspaceSchema;
58
- exports.setWorkspaceSchema = setWorkspaceSchema;
59
- exports.getAgentDirectoryWorkflowId = getAgentDirectoryWorkflowId;
60
- exports.getPhaseIds = getPhaseIds;
61
- exports.getOrchestratorUserId = getOrchestratorUserId;
62
- exports.setOrchestratorUserId = setOrchestratorUserId;
63
- exports.onBotStateChange = onBotStateChange;
64
- exports.onBotCredentialsChange = onBotCredentialsChange;
65
- exports.onDaemonRestartNeeded = onDaemonRestartNeeded;
66
- exports.cleanupBotConfig = cleanupBotConfig;
67
- exports.getBotState = getBotState;
68
- exports.getBotUserId = getBotUserId;
69
- exports.getBotType = getBotType;
70
- exports.getEnabledBotsWithTypes = getEnabledBotsWithTypes;
71
- exports.setBotUserId = setBotUserId;
72
- exports.setBotEnabled = setBotEnabled;
73
- exports.handleActivityPhaseChange = handleActivityPhaseChange;
74
- exports.reloadConfigFromHailer = reloadConfigFromHailer;
75
- exports.initBotConfigPersistence = initBotConfigPersistence;
76
- exports.initWorkspaceSchema = initWorkspaceSchema;
77
- exports.isPersistenceInitialized = isPersistenceInitialized;
78
- exports.getBotCredentials = getBotCredentials;
79
- exports.updateBotCredentials = updateBotCredentials;
80
- exports.getLocalBotCredentials = getLocalBotCredentials;
81
- exports.getAllLocalBotCredentials = getAllLocalBotCredentials;
82
- exports.getPersistenceStatus = getPersistenceStatus;
83
- const zod_1 = require("zod");
84
- const fs = __importStar(require("fs"));
85
- const path = __importStar(require("path"));
86
- const tool_registry_1 = require("../../tool-registry");
87
- const index_1 = require("../../utils/index");
88
- const signal_handler_1 = require("../../signal-handler");
89
- const config_1 = require("../../../config");
90
- const logger = (0, index_1.createLogger)({ component: 'bot-config-tools' });
91
- // ============================================================================
92
- // Local State Persistence (fallback)
93
- // ============================================================================
94
- const LOCAL_STATE_FILE = path.join(process.cwd(), '.bot-state.json');
95
- const SCHEMAS_DIR = path.join(process.cwd(), '.bot-schemas');
96
- // Per-workspace schema storage (keyed by workspaceId)
97
- const workspaceSchemas = new Map();
98
- // Reverse lookup: Agent Directory workflow ID → workspace ID
99
- const workflowToWorkspace = new Map();
100
- /**
101
- * Ensure the schemas directory exists
102
- */
103
- function ensureSchemasDir() {
104
- if (!fs.existsSync(SCHEMAS_DIR)) {
105
- fs.mkdirSync(SCHEMAS_DIR, { recursive: true });
106
- }
107
- }
108
- /**
109
- * Save schema config for a specific workspace
110
- */
111
- function saveSchemaConfig(workspaceId) {
112
- try {
113
- ensureSchemasDir();
114
- const schema = workspaceSchemas.get(workspaceId);
115
- if (!schema) {
116
- logger.warn('No schema to save for workspace', { workspaceId });
117
- return;
118
- }
119
- const filePath = path.join(SCHEMAS_DIR, `${workspaceId}.json`);
120
- fs.writeFileSync(filePath, JSON.stringify(schema, null, 2));
121
- logger.debug('Schema config saved', { workspaceId, file: filePath });
122
- }
123
- catch (error) {
124
- logger.warn('Failed to save schema config', { workspaceId, error: error instanceof Error ? error.message : String(error) });
125
- }
126
- }
127
- /**
128
- * Load schema config for a specific workspace
129
- */
130
- function loadSchemaConfig(workspaceId) {
131
- try {
132
- const filePath = path.join(SCHEMAS_DIR, `${workspaceId}.json`);
133
- if (fs.existsSync(filePath)) {
134
- const data = fs.readFileSync(filePath, 'utf-8');
135
- const config = JSON.parse(data);
136
- workspaceSchemas.set(workspaceId, config);
137
- if (config.agentDirectoryWorkflowId) {
138
- workflowToWorkspace.set(config.agentDirectoryWorkflowId, workspaceId);
139
- }
140
- logger.info('Schema config loaded', {
141
- workspaceId,
142
- workflowId: config.agentDirectoryWorkflowId ?? undefined,
143
- deployedPhaseId: config.deployedPhaseId ?? undefined
144
- });
145
- return config;
146
- }
147
- }
148
- catch (error) {
149
- logger.warn('Failed to load schema config', { workspaceId, error: error instanceof Error ? error.message : String(error) });
150
- }
151
- return null;
152
- }
153
- /**
154
- * Load all schema configs from the schemas directory
155
- */
156
- function loadAllSchemaConfigs() {
157
- try {
158
- // Migrate from old single-file format if it exists
159
- const oldSchemaFile = path.join(process.cwd(), '.bot-schema.json');
160
- if (fs.existsSync(oldSchemaFile)) {
161
- logger.info('Found legacy .bot-schema.json - migration needed (manual)');
162
- // Don't auto-migrate - user needs to know which workspace it belongs to
163
- }
164
- if (!fs.existsSync(SCHEMAS_DIR)) {
165
- logger.debug('No schemas directory found');
166
- return;
167
- }
168
- const files = fs.readdirSync(SCHEMAS_DIR).filter(f => f.endsWith('.json'));
169
- for (const file of files) {
170
- const workspaceId = file.replace('.json', '');
171
- loadSchemaConfig(workspaceId);
172
- }
173
- logger.info('Loaded schema configs', { count: workspaceSchemas.size, workspaces: Array.from(workspaceSchemas.keys()) });
174
- }
175
- catch (error) {
176
- logger.warn('Failed to load schema configs', { error: error instanceof Error ? error.message : String(error) });
177
- }
178
- }
179
- /**
180
- * Get workspace ID by Agent Directory workflow ID (reverse lookup)
181
- */
182
- function getWorkspaceByWorkflowId(workflowId) {
183
- return workflowToWorkspace.get(workflowId) ?? null;
184
- }
185
- /**
186
- * Get all configured workspace IDs
187
- */
188
- function getAllConfiguredWorkspaces() {
189
- return Array.from(workspaceSchemas.keys());
190
- }
191
- /**
192
- * Set the active workspace ID (called when switching workspaces)
193
- */
194
- function setActiveWorkspace(workspaceId) {
195
- activeWorkspaceId = workspaceId;
196
- logger.info('Active workspace changed', { workspaceId });
197
- }
198
- /**
199
- * Get the active workspace ID
200
- */
201
- function getActiveWorkspace() {
202
- return activeWorkspaceId;
203
- }
204
- /**
205
- * Get schema for a workspace
206
- */
207
- function getWorkspaceSchema(workspaceId) {
208
- return workspaceSchemas.get(workspaceId) ?? null;
209
- }
210
- /**
211
- * Set schema for a workspace
212
- */
213
- function setWorkspaceSchema(workspaceId, schema) {
214
- workspaceSchemas.set(workspaceId, schema);
215
- if (schema.agentDirectoryWorkflowId) {
216
- workflowToWorkspace.set(schema.agentDirectoryWorkflowId, workspaceId);
217
- }
218
- saveSchemaConfig(workspaceId);
219
- }
220
- /**
221
- * Load bot state from local file (LEGACY - for migration only)
222
- * @returns loaded state or empty object if file doesn't exist
223
- */
224
- function loadStateFromLocalFile() {
225
- try {
226
- if (fs.existsSync(LOCAL_STATE_FILE)) {
227
- const data = fs.readFileSync(LOCAL_STATE_FILE, 'utf-8');
228
- const parsed = JSON.parse(data);
229
- // Migrate old format: {botId: boolean} -> {botId: {enabled: boolean, userId: null, botType: null}}
230
- const migrated = {};
231
- for (const [botId, value] of Object.entries(parsed)) {
232
- if (typeof value === 'boolean') {
233
- // Old format - migrate
234
- migrated[botId] = { enabled: value, userId: null, botType: null };
235
- }
236
- else if (typeof value === 'object' && value !== null) {
237
- // New format - ensure botType exists (may be missing from older saves)
238
- const entry = value;
239
- migrated[botId] = { enabled: entry.enabled ?? false, userId: entry.userId ?? null, botType: entry.botType ?? null };
240
- }
241
- }
242
- logger.info('Bot state loaded from local file', { file: '.bot-state.json' });
243
- return migrated;
244
- }
245
- }
246
- catch (error) {
247
- logger.warn('Failed to load bot state from local file', {
248
- error: error instanceof Error ? error.message : String(error)
249
- });
250
- }
251
- return {};
252
- }
253
- /**
254
- * Load bot state from .bot-config/ directory (webhook-maintained configs)
255
- * This is the new primary source of truth for bot configuration
256
- * @returns loaded state keyed by userId
257
- */
258
- function loadBotStateFromBotConfig() {
259
- const state = {};
260
- const configDir = path.join(process.cwd(), '.bot-config');
261
- try {
262
- if (!fs.existsSync(configDir)) {
263
- logger.debug('.bot-config directory does not exist');
264
- return state;
265
- }
266
- const files = fs.readdirSync(configDir).filter(f => f.endsWith('.json'));
267
- logger.debug('Loading bot state from .bot-config/', { fileCount: files.length });
268
- for (const file of files) {
269
- try {
270
- const content = fs.readFileSync(path.join(configDir, file), 'utf-8');
271
- const config = JSON.parse(content);
272
- // Load orchestrator (always enabled if present in config)
273
- if (config.orchestrator?.userId) {
274
- state[config.orchestrator.userId] = {
275
- enabled: true,
276
- userId: config.orchestrator.userId,
277
- botType: 'orchestrator'
278
- };
279
- logger.debug('Loaded orchestrator from .bot-config/', {
280
- file,
281
- userId: config.orchestrator.userId
282
- });
283
- }
284
- // Load specialists with their enabled flags
285
- if (Array.isArray(config.specialists)) {
286
- for (const spec of config.specialists) {
287
- if (spec.userId) {
288
- state[spec.userId] = {
289
- enabled: spec.enabled ?? false,
290
- userId: spec.userId,
291
- botType: spec.botType ?? null
292
- };
293
- logger.debug('Loaded specialist from .bot-config/', {
294
- file,
295
- userId: spec.userId,
296
- enabled: spec.enabled,
297
- botType: spec.botType
298
- });
299
- }
300
- }
301
- }
302
- }
303
- catch (error) {
304
- logger.warn('Failed to parse .bot-config file', {
305
- file,
306
- error: error instanceof Error ? error.message : String(error)
307
- });
308
- }
309
- }
310
- logger.info('Bot state loaded from .bot-config/', { botCount: Object.keys(state).length });
311
- }
312
- catch (error) {
313
- logger.warn('Failed to load bot state from .bot-config/', {
314
- error: error instanceof Error ? error.message : String(error)
315
- });
316
- }
317
- return state;
318
- }
319
- // ============================================================================
320
- // Agent Directory Workflow Configuration (Dynamic Discovery)
321
- // ============================================================================
322
- // Legacy globals - kept for backwards compatibility but now read from first workspace
323
- // New code should use getWorkspaceSchema(workspaceId) instead
324
- let activeWorkspaceId = null;
325
- // Helper getters that read from active workspace (backwards compatibility)
326
- function getActiveSchema() {
327
- if (!activeWorkspaceId)
328
- return null;
329
- return workspaceSchemas.get(activeWorkspaceId) ?? null;
330
- }
331
- // Legacy accessors - these read from active workspace for backwards compatibility
332
- function get_agentDirectoryWorkflowId() { return getActiveSchema()?.agentDirectoryWorkflowId ?? null; }
333
- function get_deployedPhaseId() { return getActiveSchema()?.deployedPhaseId ?? null; }
334
- function get_retiredPhaseId() { return getActiveSchema()?.retiredPhaseId ?? null; }
335
- function get_hailerProfileFieldId() { return getActiveSchema()?.hailerProfileFieldId ?? null; }
336
- function get_emailFieldId() { return getActiveSchema()?.emailFieldId ?? null; }
337
- function get_passwordFieldId() { return getActiveSchema()?.passwordFieldId ?? null; }
338
- function get_botTypeFieldId() { return getActiveSchema()?.botTypeFieldId ?? null; }
339
- // Load all workspace schemas on module init
340
- loadAllSchemaConfigs();
341
- // Set active workspace to first loaded (backwards compatibility)
342
- const firstWorkspace = Array.from(workspaceSchemas.keys())[0];
343
- if (firstWorkspace) {
344
- activeWorkspaceId = firstWorkspace;
345
- logger.info('Active workspace set from loaded schemas', { workspaceId: activeWorkspaceId });
346
- }
347
- // Workflow/phase name patterns for discovery
348
- const AGENT_DIRECTORY_PATTERNS = ['Agent Directory', 'AI Agents', '🤖 Agent Directory'];
349
- const DEPLOYED_PHASE_PATTERNS = ['deployed', 'active', 'enabled'];
350
- const RETIRED_PHASE_PATTERNS = ['retired', 'disabled', 'inactive'];
351
- // Field key patterns for discovery
352
- const FIELD_KEY_HAILER_PROFILE = ['hailerProfile', 'Agent Hailer profile'];
353
- const FIELD_KEY_EMAIL = ['agentEmailInHailer', 'email', 'Email of Hailer profile'];
354
- const FIELD_KEY_PASSWORD = ['password'];
355
- const FIELD_KEY_BOT_TYPE = ['botType', 'bot_type', 'type'];
356
- const FIELD_KEY_SCHEMA_CONFIG = ['schemaConfig', 'schema_config', 'schema_config_f3a'];
357
- /**
358
- * Find field ID by trying multiple key patterns
359
- */
360
- function findFieldId(fields, patterns) {
361
- for (const pattern of patterns) {
362
- if (fields[pattern])
363
- return fields[pattern];
364
- }
365
- return null;
366
- }
367
- /**
368
- * Find phase by name pattern (case-insensitive partial match)
369
- */
370
- function findPhaseByPattern(phases, patterns) {
371
- for (const pattern of patterns) {
372
- const match = phases.find(p => p.name.toLowerCase().includes(pattern.toLowerCase()));
373
- if (match)
374
- return match.id;
375
- }
376
- return null;
377
- }
378
- /**
379
- * Discover Agent Directory workflow schema dynamically for a specific workspace
380
- * @param client - Hailer client (connected to the workspace)
381
- * @param workspaceId - The workspace ID to discover schema for
382
- */
383
- async function discoverAgentDirectorySchema(client, workspaceId, hintProcessId) {
384
- try {
385
- // Get list of workflows - filter by workspace ID to avoid API caching issues
386
- const workflows = await client.listWorkflows(workspaceId);
387
- // Log all workflow names for debugging
388
- logger.debug('Available workflows for discovery', {
389
- workspaceId,
390
- count: workflows.length,
391
- names: workflows.map((w) => w.name).slice(0, 10) // First 10 for brevity
392
- });
393
- // Find ALL workflows matching Agent Directory pattern
394
- const matchingWorkflows = workflows.filter((w) => AGENT_DIRECTORY_PATTERNS.some(p => w.name?.includes(p)));
395
- logger.debug('Matching Agent Directory workflows', {
396
- workspaceId,
397
- count: matchingWorkflows.length,
398
- workflows: matchingWorkflows.map((w) => ({ id: w._id, name: w.name }))
399
- });
400
- // First, check if hintProcessId points to a valid Agent Directory
401
- // (even if name doesn't match our patterns - structure-based detection)
402
- let agentDir = null;
403
- if (hintProcessId) {
404
- let hintWorkflow = workflows.find((w) => w._id === hintProcessId);
405
- // If workflow not in list, try fetching directly (API might return stale cached list)
406
- if (!hintWorkflow) {
407
- logger.debug('Hint workflow not in list, fetching directly', { hintProcessId, listCount: workflows.length });
408
- try {
409
- // Use process.get to fetch workflow directly by ID
410
- const directWorkflow = await client.getClient().socket.request('process.get', [hintProcessId]);
411
- if (directWorkflow && directWorkflow._id) {
412
- hintWorkflow = directWorkflow;
413
- logger.info('Fetched workflow directly by ID', {
414
- workflowId: hintProcessId,
415
- name: directWorkflow.name,
416
- workspaceId: directWorkflow.cid
417
- });
418
- }
419
- }
420
- catch (fetchErr) {
421
- logger.debug('Could not fetch workflow directly', { hintProcessId, error: fetchErr instanceof Error ? fetchErr.message : JSON.stringify(fetchErr) });
422
- }
423
- }
424
- if (hintWorkflow) {
425
- const phases = Object.values(hintWorkflow.phases || {});
426
- const hasDeployedPhase = phases.some(p => DEPLOYED_PHASE_PATTERNS.some(pat => p.name?.toLowerCase().includes(pat)));
427
- const hasRetiredPhase = phases.some(p => RETIRED_PHASE_PATTERNS.some(pat => p.name?.toLowerCase().includes(pat)));
428
- const fieldKeys = Object.values(hintWorkflow.fields || {}).map((f) => f.key?.toLowerCase() || '');
429
- const hasProfileField = fieldKeys.some(k => k.includes('hailerprofile') || k.includes('profile'));
430
- if (hasDeployedPhase && hasRetiredPhase && hasProfileField) {
431
- agentDir = hintWorkflow;
432
- logger.info('Using hinted workflow (structure matches Agent Directory)', {
433
- workspaceId,
434
- hintProcessId,
435
- name: hintWorkflow.name,
436
- phases: phases.map(p => p.name)
437
- });
438
- }
439
- }
440
- }
441
- // If no hint match, try name-based matching
442
- if (!agentDir) {
443
- if (matchingWorkflows.length === 0) {
444
- logger.warn('Agent Directory workflow not found by name pattern', {
445
- workspaceId,
446
- patterns: AGENT_DIRECTORY_PATTERNS,
447
- availableWorkflows: workflows.map((w) => w.name)
448
- });
449
- return false;
450
- }
451
- // If we have a hint, check if it matches one of the name-matched workflows
452
- if (hintProcessId) {
453
- const hintMatch = matchingWorkflows.find((w) => w._id === hintProcessId);
454
- if (hintMatch) {
455
- agentDir = hintMatch;
456
- logger.info('Using hinted workflow from signal', { workspaceId, hintProcessId });
457
- }
458
- }
459
- // If still no match, prefer unknown workflow over already-known
460
- if (!agentDir && matchingWorkflows.length > 1) {
461
- const unknownWorkflows = matchingWorkflows.filter((w) => !workflowToWorkspace.has(w._id));
462
- if (unknownWorkflows.length > 0) {
463
- agentDir = unknownWorkflows[0];
464
- logger.info('Preferring unknown workflow over already-known one', {
465
- workspaceId,
466
- chosen: agentDir._id,
467
- skipped: matchingWorkflows.filter((w) => workflowToWorkspace.has(w._id)).map((w) => w._id)
468
- });
469
- }
470
- }
471
- // Fallback to first match
472
- if (!agentDir) {
473
- agentDir = matchingWorkflows[0];
474
- }
475
- }
476
- const discoveredWorkflowId = agentDir._id;
477
- // Use the workflow's actual workspace ID if available (from direct fetch)
478
- // This handles the case where the workflow belongs to a different workspace than we're searching in
479
- const actualWorkspaceId = agentDir.cid || workspaceId;
480
- if (actualWorkspaceId !== workspaceId) {
481
- logger.info('Workflow belongs to different workspace than expected', {
482
- expectedWorkspaceId: workspaceId,
483
- actualWorkspaceId,
484
- workflowId: discoveredWorkflowId
485
- });
486
- }
487
- logger.info('Discovered Agent Directory workflow', { workspaceId: actualWorkspaceId, id: discoveredWorkflowId, name: agentDir.name });
488
- // Use workflow data directly from listWorkflows (already includes phases/fields)
489
- const workflowDetails = agentDir;
490
- // Extract phases
491
- const phases = [];
492
- if (workflowDetails.phases) {
493
- for (const [phaseId, phase] of Object.entries(workflowDetails.phases)) {
494
- phases.push({ id: phaseId, name: phase.name || phaseId });
495
- }
496
- }
497
- const discoveredDeployedPhaseId = findPhaseByPattern(phases, DEPLOYED_PHASE_PATTERNS);
498
- const discoveredRetiredPhaseId = findPhaseByPattern(phases, RETIRED_PHASE_PATTERNS);
499
- logger.info('Discovered phases', {
500
- workspaceId: actualWorkspaceId,
501
- deployed: discoveredDeployedPhaseId,
502
- retired: discoveredRetiredPhaseId,
503
- allPhases: phases.map(p => p.name)
504
- });
505
- // Extract field IDs by key
506
- const fieldMap = {};
507
- if (workflowDetails.fields) {
508
- for (const [fieldId, field] of Object.entries(workflowDetails.fields)) {
509
- const key = field.key;
510
- if (key)
511
- fieldMap[key] = fieldId;
512
- }
513
- }
514
- const discoveredHailerProfileFieldId = findFieldId(fieldMap, FIELD_KEY_HAILER_PROFILE);
515
- const discoveredEmailFieldId = findFieldId(fieldMap, FIELD_KEY_EMAIL);
516
- const discoveredPasswordFieldId = findFieldId(fieldMap, FIELD_KEY_PASSWORD);
517
- const discoveredBotTypeFieldId = findFieldId(fieldMap, FIELD_KEY_BOT_TYPE);
518
- logger.info('Discovered field IDs', {
519
- workspaceId: actualWorkspaceId,
520
- hailerProfile: discoveredHailerProfileFieldId,
521
- email: discoveredEmailFieldId,
522
- password: discoveredPasswordFieldId,
523
- botType: discoveredBotTypeFieldId
524
- });
525
- // Build and store schema config for this workspace
526
- const schema = {
527
- agentDirectoryWorkflowId: discoveredWorkflowId,
528
- deployedPhaseId: discoveredDeployedPhaseId,
529
- retiredPhaseId: discoveredRetiredPhaseId,
530
- hailerProfileFieldId: discoveredHailerProfileFieldId,
531
- emailFieldId: discoveredEmailFieldId,
532
- passwordFieldId: discoveredPasswordFieldId,
533
- botTypeFieldId: discoveredBotTypeFieldId
534
- };
535
- setWorkspaceSchema(actualWorkspaceId, schema);
536
- // Set as active workspace if none set
537
- if (!activeWorkspaceId) {
538
- activeWorkspaceId = actualWorkspaceId;
539
- logger.info('Set active workspace', { workspaceId: actualWorkspaceId });
540
- }
541
- return true;
542
- }
543
- catch (error) {
544
- logger.error('Failed to discover Agent Directory schema', {
545
- workspaceId,
546
- error: error instanceof Error ? error.message : String(error)
547
- });
548
- return false;
549
- }
550
- }
551
- /**
552
- * Sync bot credentials from a workspace's Agent Directory
553
- * Called after schema discovery to load deployed bots
554
- */
555
- async function syncCredentialsFromWorkspace(client, workspaceId) {
556
- const schema = workspaceSchemas.get(workspaceId);
557
- if (!schema?.agentDirectoryWorkflowId || !schema?.deployedPhaseId) {
558
- logger.debug('Cannot sync credentials - missing schema', { workspaceId });
559
- return;
560
- }
561
- try {
562
- logger.info('Syncing credentials from workspace Agent Directory', { workspaceId });
563
- // Fetch deployed agents
564
- const deployedActivities = await client.fetchActivityList(schema.agentDirectoryWorkflowId, schema.deployedPhaseId, 100, { returnFlat: true });
565
- const deployedList = Array.isArray(deployedActivities)
566
- ? deployedActivities
567
- : (deployedActivities?.data || deployedActivities?.activities || []);
568
- logger.info('Found deployed agents in workspace', { workspaceId, count: deployedList.length });
569
- for (const activity of deployedList) {
570
- const userId = extractUserIdFromActivity(activity, schema);
571
- if (!userId)
572
- continue;
573
- // Extract and save credentials
574
- const creds = extractCredentialsFromActivity(activity, schema);
575
- if (creds) {
576
- updateLocalCredentials(userId, creds);
577
- logger.info('Synced credentials from workspace', { workspaceId, userId, email: (0, config_1.maskEmail)(creds.email) });
578
- }
579
- // Extract bot type and update state
580
- const botType = extractBotTypeFromActivity(activity, schema);
581
- botState[userId] = {
582
- enabled: true,
583
- userId,
584
- botType: botType ?? botState[userId]?.botType ?? null
585
- };
586
- }
587
- // Also sync retired agents (for completeness)
588
- if (schema.retiredPhaseId) {
589
- const retiredActivities = await client.fetchActivityList(schema.agentDirectoryWorkflowId, schema.retiredPhaseId, 100, { returnFlat: true });
590
- const retiredList = Array.isArray(retiredActivities)
591
- ? retiredActivities
592
- : (retiredActivities?.data || retiredActivities?.activities || []);
593
- for (const activity of retiredList) {
594
- const userId = extractUserIdFromActivity(activity, schema);
595
- if (!userId)
596
- continue;
597
- const creds = extractCredentialsFromActivity(activity, schema);
598
- if (creds) {
599
- updateLocalCredentials(userId, creds);
600
- }
601
- const botType = extractBotTypeFromActivity(activity, schema);
602
- botState[userId] = {
603
- enabled: false,
604
- userId,
605
- botType: botType ?? botState[userId]?.botType ?? null
606
- };
607
- }
608
- }
609
- // Note: Persistence now handled by webhooks via .bot-config/ files
610
- // saveStateToLocalFile(botState); // REMOVED: Webhooks maintain .bot-config/ as source of truth
611
- logger.info('Credentials synced from workspace', { workspaceId });
612
- }
613
- catch (error) {
614
- logger.error('Failed to sync credentials from workspace', {
615
- workspaceId,
616
- error: error instanceof Error ? error.message : String(error)
617
- });
618
- }
619
- }
620
- /**
621
- * Sync credentials from ALL workspaces that have loaded schemas
622
- * Called at startup to ensure credentials are synced for all discovered Agent Directories
623
- */
624
- async function syncCredentialsFromAllWorkspaces(client) {
625
- const workspaceIds = Array.from(workspaceSchemas.keys());
626
- if (workspaceIds.length === 0) {
627
- logger.debug('No workspace schemas loaded - skipping credential sync');
628
- return;
629
- }
630
- logger.info('Syncing credentials from all workspace schemas', { workspaceCount: workspaceIds.length });
631
- // Remember original workspace to switch back
632
- let originalWorkspaceId = null;
633
- try {
634
- const init = await client.fetchInit(['network']);
635
- originalWorkspaceId = init?.network?._id;
636
- }
637
- catch (e) {
638
- logger.warn('Could not determine original workspace');
639
- }
640
- for (const wsId of workspaceIds) {
641
- const schema = workspaceSchemas.get(wsId);
642
- if (!schema?.agentDirectoryWorkflowId || !schema?.deployedPhaseId) {
643
- logger.debug('Skipping workspace - incomplete schema', { workspaceId: wsId });
644
- continue;
645
- }
646
- try {
647
- // Switch to this workspace
648
- logger.info('Switching to workspace for credential sync', { workspaceId: wsId });
649
- await client.getClient().socket.request('core.switch_ecosystem', [wsId]);
650
- // Verify switch
651
- const verifyInit = await client.fetchInit(['network']);
652
- const currentWs = verifyInit?.network?._id;
653
- if (currentWs !== wsId) {
654
- logger.warn('Workspace switch failed', { target: wsId, current: currentWs });
655
- continue;
656
- }
657
- // Sync credentials
658
- await syncCredentialsFromWorkspace(client, wsId);
659
- }
660
- catch (error) {
661
- logger.warn('Failed to sync credentials from workspace', {
662
- workspaceId: wsId,
663
- error: error instanceof Error ? error.message : String(error)
664
- });
665
- }
666
- }
667
- // Switch back to original workspace
668
- if (originalWorkspaceId) {
669
- try {
670
- await client.getClient().socket.request('core.switch_ecosystem', [originalWorkspaceId]);
671
- logger.info('Switched back to original workspace', { workspaceId: originalWorkspaceId });
672
- }
673
- catch (e) {
674
- logger.warn('Failed to switch back to original workspace');
675
- }
676
- }
677
- }
678
- /**
679
- * Get discovered workflow ID for a workspace (or active workspace if not specified)
680
- */
681
- function getAgentDirectoryWorkflowId(workspaceId) {
682
- const wsId = workspaceId ?? activeWorkspaceId;
683
- if (!wsId)
684
- return null;
685
- return workspaceSchemas.get(wsId)?.agentDirectoryWorkflowId ?? null;
686
- }
687
- /**
688
- * Get discovered phase IDs for a workspace (or active workspace if not specified)
689
- */
690
- function getPhaseIds(workspaceId) {
691
- const wsId = workspaceId ?? activeWorkspaceId;
692
- if (!wsId)
693
- return { deployed: null, retired: null };
694
- const schema = workspaceSchemas.get(wsId);
695
- return { deployed: schema?.deployedPhaseId ?? null, retired: schema?.retiredPhaseId ?? null };
696
- }
697
- // Dynamically discovered activity IDs (populated at runtime from Hailer)
698
- // NOW KEYED BY USER ID instead of bot ID
699
- let discoveredActivityIds = {};
700
- // Legacy hardcoded IDs (fallback only)
701
- const LEGACY_AGENT_ACTIVITY_IDS = {
702
- hal: '6957848b8d347a6c707ee3f6',
703
- hailerExpert: '695784913c659fc4d8fe0e02',
704
- bugFixer: '695e42deca61319424bc8b23',
705
- vastuullisuus: '695e42dfca61319424bc8b2e'
706
- };
707
- // Get activity ID for a bot - prefers discovered IDs over legacy
708
- function getActivityIdForBot(botId) {
709
- return discoveredActivityIds[botId] || LEGACY_AGENT_ACTIVITY_IDS[botId];
710
- }
711
- // Persistence state
712
- let persistenceInitialized = false;
713
- let hailerClient = null; // Will be set during initialization
714
- /**
715
- * Available bots - single source of truth
716
- *
717
- * All bots that can be enabled/disabled in the workspace.
718
- * This list should match the AI Hub app and agent definitions.
719
- */
720
- exports.AVAILABLE_BOTS = [
721
- {
722
- id: 'hal',
723
- name: 'HAL',
724
- description: 'Main orchestrator - handles general chat and coordinates specialists',
725
- icon: '🤖'
726
- },
727
- {
728
- id: 'bugFixer',
729
- name: 'Bug Fixer',
730
- description: 'Bug fixing specialist - analyzes and repairs Hailer app issues',
731
- icon: '🔧'
732
- },
733
- {
734
- id: 'hailerExpert',
735
- name: 'Hailer Expert',
736
- description: 'Workflow & data specialist - bulk operations, insights, reports',
737
- icon: '📊'
738
- },
739
- {
740
- id: 'vastuullisuus',
741
- name: 'Vastuullisuus',
742
- description: 'Sustainability analysis for SMEs',
743
- icon: '🌱'
744
- }
745
- ];
746
- // In-memory bot state (synced with workflow via AI Hub)
747
- // Only contains bots registered via AI Hub - no legacy defaults
748
- let botState = {};
749
- // Activity signal deduplication to prevent duplicate processing on socket reconnects
750
- // Maps activityId+phaseId to timestamp of last processing
751
- const processedSignals = new Map();
752
- const SIGNAL_DEDUP_WINDOW_MS = 5000; // 5 seconds
753
- /**
754
- * Check if an activity signal was recently processed (deduplication)
755
- * Returns true if signal should be skipped (already processed)
756
- */
757
- function isDuplicateSignal(activityId, phaseId) {
758
- const key = `${activityId}:${phaseId}`;
759
- const now = Date.now();
760
- const lastProcessed = processedSignals.get(key);
761
- if (lastProcessed && now - lastProcessed < SIGNAL_DEDUP_WINDOW_MS) {
762
- return true;
763
- }
764
- // Mark as processed
765
- processedSignals.set(key, now);
766
- // Cleanup old entries periodically (keep map from growing unbounded)
767
- if (processedSignals.size > 1000) {
768
- const cutoff = now - SIGNAL_DEDUP_WINDOW_MS;
769
- for (const [k, timestamp] of processedSignals) {
770
- if (timestamp < cutoff) {
771
- processedSignals.delete(k);
772
- }
773
- }
774
- }
775
- return false;
776
- }
777
- // Track orchestrator user ID (set during sync from Agent Directory)
778
- let orchestratorUserId = null;
779
- /**
780
- * Get the orchestrator user ID
781
- * Returns the user ID of the bot marked as orchestrator in Agent Directory
782
- */
783
- function getOrchestratorUserId() {
784
- return orchestratorUserId;
785
- }
786
- /**
787
- * Set the orchestrator user ID
788
- * Called during Agent Directory sync - set to first deployed bot with credentials
789
- */
790
- function setOrchestratorUserId(userId) {
791
- const wasNull = orchestratorUserId === null;
792
- orchestratorUserId = userId;
793
- logger.info('Orchestrator user ID set', { userId: userId ?? undefined });
794
- // Notify daemon to restart if we just discovered an orchestrator
795
- if (wasNull && userId !== null) {
796
- notifyDaemonRestartNeeded();
797
- }
798
- }
799
- // Load local state immediately on module load (before any async operations)
800
- const localStateOnLoad = loadStateFromLocalFile();
801
- if (Object.keys(localStateOnLoad).length > 0) {
802
- // Accept ALL entries including user-ID based bots (not just AVAILABLE_BOTS)
803
- Object.keys(localStateOnLoad).forEach(botId => {
804
- botState[botId] = localStateOnLoad[botId];
805
- });
806
- logger.info('Bot state restored from local file on module load', { config: botState });
807
- }
808
- // Callbacks for when bot state changes
809
- const stateChangeCallbacks = [];
810
- function onBotStateChange(callback) {
811
- stateChangeCallbacks.push(callback);
812
- }
813
- // Callbacks for when bot credentials change
814
- const credentialChangeCallbacks = [];
815
- function onBotCredentialsChange(callback) {
816
- credentialChangeCallbacks.push(callback);
817
- }
818
- // Callbacks for when daemon restart is needed (new orchestrator discovered)
819
- const daemonRestartCallbacks = [];
820
- function onDaemonRestartNeeded(callback) {
821
- daemonRestartCallbacks.push(callback);
822
- }
823
- // Debounce mechanism to prevent multiple rapid restart triggers
824
- let restartDebounceTimer = null;
825
- const RESTART_DEBOUNCE_MS = 500;
826
- /**
827
- * Cleanup function to clear pending timers and state on shutdown
828
- * Should be called from Core.stop() to prevent orphaned callbacks
829
- */
830
- function cleanupBotConfig() {
831
- if (restartDebounceTimer) {
832
- clearTimeout(restartDebounceTimer);
833
- restartDebounceTimer = null;
834
- }
835
- // Clear deduplication cache
836
- processedSignals.clear();
837
- // Clear callbacks to prevent memory leaks
838
- stateChangeCallbacks.length = 0;
839
- daemonRestartCallbacks.length = 0;
840
- logger.debug('Bot config cleanup completed');
841
- }
842
- function notifyDaemonRestartNeeded() {
843
- // Clear any pending restart to reset the debounce window
844
- if (restartDebounceTimer) {
845
- logger.debug('Daemon restart already pending, resetting debounce timer');
846
- clearTimeout(restartDebounceTimer);
847
- }
848
- // Debounce the restart notification
849
- restartDebounceTimer = setTimeout(() => {
850
- restartDebounceTimer = null;
851
- logger.info('Notifying daemon restart needed (new orchestrator discovered)');
852
- daemonRestartCallbacks.forEach(cb => {
853
- try {
854
- cb();
855
- }
856
- catch (error) {
857
- logger.error('Daemon restart callback failed', { error });
858
- }
859
- });
860
- }, RESTART_DEBOUNCE_MS);
861
- }
862
- function getBotState() {
863
- const result = {};
864
- // First, read from webhook configs (.bot-config/*.json)
865
- const configDir = path.join(process.cwd(), '.bot-config');
866
- if (fs.existsSync(configDir)) {
867
- const files = fs.readdirSync(configDir).filter(f => f.endsWith('.json'));
868
- for (const file of files) {
869
- try {
870
- const content = fs.readFileSync(path.join(configDir, file), 'utf-8');
871
- const config = JSON.parse(content);
872
- // Add orchestrator
873
- if (config.orchestrator?.userId) {
874
- result[config.orchestrator.userId] = true; // Orchestrator in config = enabled
875
- }
876
- // Add specialists
877
- if (config.specialists) {
878
- for (const spec of config.specialists) {
879
- if (spec.userId) {
880
- result[spec.userId] = spec.enabled;
881
- }
882
- }
883
- }
884
- }
885
- catch (error) {
886
- logger.warn('Failed to read webhook config', { file, error: String(error) });
887
- }
888
- }
889
- }
890
- // Merge with in-memory state (webhook config takes precedence)
891
- for (const [botId, entry] of Object.entries(botState)) {
892
- if (!(botId in result)) {
893
- result[botId] = entry.enabled;
894
- }
895
- }
896
- return result;
897
- }
898
- /**
899
- * Get the user ID associated with a bot
900
- */
901
- function getBotUserId(botId) {
902
- return botState[botId]?.userId ?? null;
903
- }
904
- /**
905
- * Get the bot type for a user ID
906
- * Returns values like 'orchestrator', 'workflow-expert', 'bug-fixer', etc.
907
- */
908
- function getBotType(userId) {
909
- return botState[userId]?.botType ?? null;
910
- }
911
- /**
912
- * Get all enabled bots with their types
913
- * Returns map of userId -> botType for enabled bots only
914
- */
915
- function getEnabledBotsWithTypes() {
916
- const result = new Map();
917
- for (const [userId, entry] of Object.entries(botState)) {
918
- if (entry.enabled) {
919
- result.set(userId, entry.botType);
920
- }
921
- }
922
- return result;
923
- }
924
- /**
925
- * Set the user ID associated with a bot
926
- */
927
- function setBotUserId(botId, userId) {
928
- if (!botState[botId]) {
929
- botState[botId] = { enabled: false, userId: null, botType: null };
930
- }
931
- botState[botId].userId = userId;
932
- // saveStateToLocalFile(botState); // REMOVED: Webhooks maintain .bot-config/ as source of truth
933
- logger.debug('Bot userId updated', { botId, userId: userId ?? undefined });
934
- }
935
- /**
936
- * Set bot enabled state
937
- *
938
- * Updates in-memory state and persists to Agent Directory by moving
939
- * the agent activity to the appropriate phase (deployed/retired).
940
- */
941
- function setBotEnabled(botId, enabled) {
942
- if (!exports.AVAILABLE_BOTS.find(b => b.id === botId)) {
943
- throw new Error(`Unknown bot: ${botId}`);
944
- }
945
- const existing = botState[botId];
946
- botState[botId] = {
947
- enabled,
948
- userId: existing?.userId ?? null,
949
- botType: existing?.botType ?? null
950
- };
951
- // Notify listeners
952
- stateChangeCallbacks.forEach(cb => cb(botId, enabled));
953
- logger.info('Bot state changed', { botId, enabled });
954
- // Note: Persistence now handled by webhooks via .bot-config/ files
955
- // saveStateToLocalFile(botState); // REMOVED: Webhooks maintain .bot-config/ as source of truth
956
- // Persist to Hailer (triggers webhook which updates .bot-config/)
957
- persistBotToHailer(botId, enabled).catch(err => logger.warn('Hailer persistence failed', { botId, err: err instanceof Error ? err.message : String(err) }));
958
- }
959
- /**
960
- * Handle activity phase change signal from Hailer
961
- *
962
- * Called by signal-handler when an activity.updated signal is received.
963
- * Updates bot state if the activity belongs to Agent Directory workflow.
964
- *
965
- * NEW: Now looks up activities by user ID instead of bot type.
966
- *
967
- * @param processId - Workflow ID from the signal
968
- * @param activityIds - Array of activity IDs that were updated
969
- * @param newPhaseId - The phase the activities moved to
970
- */
971
- async function handleActivityPhaseChange(processId, activityIds, newPhaseId) {
972
- // Look up which workspace this workflow belongs to (reverse lookup)
973
- let workspaceId = workflowToWorkspace.get(processId);
974
- // If no workspace found, try to auto-discover by checking all accessible workspaces
975
- if (!workspaceId && hailerClient) {
976
- logger.info('Unknown workflow - attempting auto-discovery across workspaces', { processId });
977
- try {
978
- // Get all workspaces the user has access to
979
- const init = await hailerClient.fetchInit(['networks']);
980
- const allWorkspaces = init?.networks ? Object.keys(init.networks) : [];
981
- logger.debug('Checking workspaces for unknown workflow', { processId, workspaceCount: allWorkspaces.length });
982
- for (const wsId of allWorkspaces) {
983
- // Skip if we already have schema for this workspace AND the signal's processId matches
984
- // (Don't skip if the signal is from a different workflow - might be a new Agent Directory)
985
- const existingSchema = workspaceSchemas.get(wsId);
986
- if (existingSchema?.agentDirectoryWorkflowId) {
987
- // If signal processId matches cached schema, use cached data
988
- if (existingSchema.agentDirectoryWorkflowId === processId) {
989
- logger.debug('Signal matches cached schema - using cached workflow', { workspaceId: wsId, processId });
990
- workspaceId = wsId;
991
- break;
992
- }
993
- // If schema exists but points to different workflow, we might need to re-discover
994
- // Only skip if we're sure this workspace can't contain the signal's workflow
995
- if (workflowToWorkspace.has(existingSchema.agentDirectoryWorkflowId)) {
996
- logger.debug('Skipping workspace - schema points to different workflow', {
997
- workspaceId: wsId,
998
- cachedWorkflowId: existingSchema.agentDirectoryWorkflowId,
999
- signalProcessId: processId
1000
- });
1001
- continue;
1002
- }
1003
- }
1004
- try {
1005
- // Switch to this workspace
1006
- logger.info('Switching to workspace for discovery', { workspaceId: wsId });
1007
- await hailerClient.getClient().socket.request('core.switch_ecosystem', [wsId]);
1008
- // Force a fresh init to confirm we're in the right workspace
1009
- const verifyInit = await hailerClient.fetchInit(['network']);
1010
- const currentWs = verifyInit?.network?._id;
1011
- logger.info('Workspace switch verified', { targetWorkspace: wsId, currentWorkspace: currentWs });
1012
- if (currentWs !== wsId) {
1013
- logger.warn('Workspace switch failed - still in wrong workspace', { targetWorkspace: wsId, currentWorkspace: currentWs });
1014
- continue;
1015
- }
1016
- // Try to discover Agent Directory - pass processId as hint
1017
- // If the signal came from an Agent Directory, this will prefer that workflow
1018
- const discovered = await discoverAgentDirectorySchema(hailerClient, wsId, processId);
1019
- if (discovered) {
1020
- // Sync credentials from this workspace's Agent Directory
1021
- await syncCredentialsFromWorkspace(hailerClient, wsId);
1022
- // Check if this processId is now known
1023
- workspaceId = workflowToWorkspace.get(processId);
1024
- if (workspaceId) {
1025
- logger.info('Auto-discovered schema for workspace', { workspaceId, processId });
1026
- break;
1027
- }
1028
- }
1029
- }
1030
- catch (switchError) {
1031
- logger.debug('Could not discover in workspace', { workspaceId: wsId, error: switchError instanceof Error ? switchError.message : String(switchError) });
1032
- }
1033
- }
1034
- }
1035
- catch (error) {
1036
- logger.warn('Auto-discovery failed', { processId, error: error instanceof Error ? error.message : String(error) });
1037
- }
1038
- }
1039
- // Still no workspace found after discovery attempt
1040
- if (!workspaceId) {
1041
- logger.debug('Skipping signal - workflow not an Agent Directory', { processId });
1042
- return;
1043
- }
1044
- // Get schema for this workspace
1045
- let schema = workspaceSchemas.get(workspaceId);
1046
- if (!schema) {
1047
- logger.debug('Skipping signal - no schema for workspace', { workspaceId, processId });
1048
- return;
1049
- }
1050
- logger.info('Processing Agent Directory signal', { workspaceId, processId, activityIds, newPhaseId });
1051
- // If we have IDs cached, check phase match
1052
- let isDeployed = schema.deployedPhaseId ? newPhaseId === schema.deployedPhaseId : false;
1053
- let isRetired = schema.retiredPhaseId ? newPhaseId === schema.retiredPhaseId : false;
1054
- // Check if we need to discover phase IDs (workflow is known but phases aren't)
1055
- const needsDiscovery = !schema.deployedPhaseId || !schema.retiredPhaseId;
1056
- // Also check if field IDs need discovery (separate from workflow/phase IDs)
1057
- const needsFieldDiscovery = !schema.passwordFieldId || !schema.emailFieldId || !schema.hailerProfileFieldId;
1058
- // Run field discovery if we have the workflow ID but not field IDs
1059
- if (needsFieldDiscovery && schema.agentDirectoryWorkflowId && hailerClient) {
1060
- await discoverFieldIds(hailerClient, workspaceId, schema.agentDirectoryWorkflowId);
1061
- // Refresh schema after discovery
1062
- schema = workspaceSchemas.get(workspaceId) ?? schema;
1063
- }
1064
- for (const activityId of activityIds) {
1065
- // Skip duplicate signals (socket reconnects can re-deliver same signal)
1066
- if (isDuplicateSignal(activityId, newPhaseId)) {
1067
- logger.debug('Skipping duplicate activity signal', { activityId, newPhaseId });
1068
- continue;
1069
- }
1070
- // Fetch the activity to check for schema config and user ID
1071
- if (!hailerClient) {
1072
- logger.debug('No Hailer client for activity fetch', { activityId });
1073
- continue;
1074
- }
1075
- let activity;
1076
- try {
1077
- // Try fetchActivityByIdSafe first (uses v3.activity.list, workspace-scoped)
1078
- activity = await hailerClient.fetchActivityByIdSafe(activityId, processId, newPhaseId);
1079
- // Fallback: use activities.load for cross-workspace access
1080
- // This works when signal comes from a different workspace than current socket context
1081
- if (!activity) {
1082
- logger.debug('Activity not found via v3.activity.list, trying activities.load', { activityId, processId, phaseId: newPhaseId });
1083
- activity = await hailerClient.fetchActivityById(activityId);
1084
- }
1085
- if (!activity) {
1086
- logger.warn('Activity not found for phase change', { activityId, processId, phaseId: newPhaseId });
1087
- continue;
1088
- }
1089
- }
1090
- catch (e) {
1091
- logger.warn('Could not fetch activity for phase change', { activityId, processId, phaseId: newPhaseId, error: e instanceof Error ? e.message : String(e) });
1092
- continue;
1093
- }
1094
- // Try to discover schema config from this activity (written by AI Hub)
1095
- if (needsDiscovery) {
1096
- const activitySchemaConfig = extractSchemaConfigFromActivity(activity);
1097
- if (activitySchemaConfig) {
1098
- // This IS an Agent Directory activity - apply config
1099
- applySchemaConfig(workspaceId, activitySchemaConfig);
1100
- // Discover field IDs if not already known
1101
- if (hailerClient && schema.agentDirectoryWorkflowId) {
1102
- await discoverFieldIds(hailerClient, workspaceId, schema.agentDirectoryWorkflowId);
1103
- }
1104
- // Refresh schema and re-check phase match with newly discovered IDs
1105
- schema = workspaceSchemas.get(workspaceId) ?? schema;
1106
- isDeployed = newPhaseId === schema.deployedPhaseId;
1107
- isRetired = newPhaseId === schema.retiredPhaseId;
1108
- logger.info('Schema config discovered from signal', {
1109
- workspaceId,
1110
- processId,
1111
- workflowId: schema.agentDirectoryWorkflowId ?? undefined,
1112
- deployedPhaseId: schema.deployedPhaseId ?? undefined,
1113
- retiredPhaseId: schema.retiredPhaseId ?? undefined
1114
- });
1115
- }
1116
- else {
1117
- // No schema config = not an Agent Directory activity (or not yet configured)
1118
- // Check if activity has hailerProfile to determine if it's a bot
1119
- const userId = extractUserIdFromActivity(activity);
1120
- if (!userId) {
1121
- logger.debug('Activity has no schemaConfig or hailerProfile - not an agent', { activityId });
1122
- continue;
1123
- }
1124
- // Has hailerProfile but no schemaConfig - might be Agent Directory but not configured yet
1125
- logger.debug('Activity has hailerProfile but no schemaConfig', { activityId, userId });
1126
- }
1127
- }
1128
- // Skip if phase doesn't match known phases
1129
- if (!isDeployed && !isRetired) {
1130
- continue;
1131
- }
1132
- // Extract user ID from activity
1133
- let userId = extractUserIdFromActivity(activity);
1134
- // If no hailerProfile, try to auto-link by matching activity name to user name
1135
- if (!userId && hailerClient) {
1136
- const activityName = activity.name;
1137
- if (activityName) {
1138
- try {
1139
- // Try to find user by name match
1140
- const foundUserId = await hailerClient.findUserByName(activityName);
1141
- if (foundUserId) {
1142
- logger.info('Auto-linking hailerProfile by name match', {
1143
- activityId,
1144
- activityName,
1145
- userId: foundUserId
1146
- });
1147
- // Update activity with hailerProfile (users field expects array)
1148
- // Use field ID 6965fe1179ec994375269e94 for hailerProfile
1149
- const hailerProfileFieldIdForUpdate = '6965fe1179ec994375269e94';
1150
- await hailerClient.updateActivities([{
1151
- _id: activityId,
1152
- fields: { [hailerProfileFieldIdForUpdate]: [foundUserId] }
1153
- }], {});
1154
- userId = foundUserId;
1155
- }
1156
- else {
1157
- logger.debug('No user found matching activity name', {
1158
- activityName,
1159
- hint: 'Set the "Hailer Profile" field manually in the Agent Directory, or ensure activity name matches user\'s full name exactly'
1160
- });
1161
- }
1162
- }
1163
- catch (e) {
1164
- logger.warn('Failed to auto-link hailerProfile by name', {
1165
- activityId,
1166
- error: e instanceof Error ? e.message : String(e)
1167
- });
1168
- }
1169
- }
1170
- }
1171
- if (!userId) {
1172
- logger.debug('Activity has no hailerProfile user ID', { activityId });
1173
- continue;
1174
- }
1175
- // Cache the activity ID for this user
1176
- discoveredActivityIds[userId] = activityId;
1177
- // Extract bot type
1178
- const extractedBotType = extractBotTypeFromActivity(activity);
1179
- // Sync credentials
1180
- const creds = extractCredentialsFromActivity(activity);
1181
- if (creds) {
1182
- updateLocalCredentials(userId, creds);
1183
- }
1184
- logger.info('Agent Directory activity phase change detected', {
1185
- activityId,
1186
- userId,
1187
- newPhaseId,
1188
- isDeployed,
1189
- isRetired
1190
- });
1191
- const enabled = isDeployed;
1192
- const existingEntry = botState[userId];
1193
- botState[userId] = {
1194
- enabled,
1195
- userId, // Self-referential for compatibility
1196
- botType: extractedBotType ?? existingEntry?.botType ?? null
1197
- };
1198
- // saveStateToLocalFile(botState); // REMOVED: Webhooks maintain .bot-config/ as source of truth
1199
- // Notify listeners
1200
- stateChangeCallbacks.forEach(cb => cb(userId, enabled));
1201
- logger.info('Bot state updated from phase change', { userId, enabled, activityId });
1202
- // Only restart daemon if orchestrator state changes
1203
- // For specialists, the stateChangeCallbacks above will handle updating the orchestrator
1204
- const botType = botState[userId]?.botType;
1205
- if (botType === 'orchestrator') {
1206
- logger.info('Orchestrator state changed, triggering daemon restart');
1207
- notifyDaemonRestartNeeded();
1208
- }
1209
- else {
1210
- logger.debug('Specialist state changed, no daemon restart needed', { userId, botType });
1211
- }
1212
- }
1213
- }
1214
- /**
1215
- * Force reload bot config from Hailer
1216
- *
1217
- * Can be called to manually refresh state from Hailer.
1218
- * NEW: Now works with user IDs instead of bot IDs.
1219
- *
1220
- * Also handles hot-reload: if schema discovery hasn't succeeded yet
1221
- * (e.g., AI Hub template wasn't installed at startup), it will
1222
- * retry discovery and initialize the daemon.
1223
- */
1224
- async function reloadConfigFromHailer() {
1225
- // If schema discovery hasn't succeeded yet, log that we're retrying
1226
- // (loadConfigFromHailer will attempt discovery internally)
1227
- if (workspaceSchemas.size === 0) {
1228
- logger.info('No workspace schemas discovered yet, reloadConfigFromHailer will attempt discovery...');
1229
- }
1230
- const config = await loadConfigFromHailer();
1231
- if (config) {
1232
- for (const [userId, enabled] of Object.entries(config)) {
1233
- if (botState[userId]?.enabled !== enabled) {
1234
- const existingEntry = botState[userId];
1235
- botState[userId] = {
1236
- enabled,
1237
- userId, // Self-referential
1238
- botType: existingEntry?.botType ?? null
1239
- };
1240
- stateChangeCallbacks.forEach(cb => cb(userId, enabled));
1241
- logger.info('Bot state reloaded from Hailer', { userId, enabled });
1242
- }
1243
- }
1244
- // saveStateToLocalFile(botState); // REMOVED: Webhooks maintain .bot-config/ as source of truth
1245
- }
1246
- }
1247
- // ============================================================================
1248
- // Persistence Layer Functions
1249
- // ============================================================================
1250
- /**
1251
- * Initialize persistence layer
1252
- * Called once during MCP server startup
1253
- *
1254
- * Loads bot config from local files only (no Hailer API calls on startup).
1255
- * Real-time updates from Hailer are handled via signal handler when activities change.
1256
- *
1257
- * @param client - HailerApiClient instance for API calls
1258
- */
1259
- /**
1260
- * Set the target workspace ID for bot config operations
1261
- * Called by signal handler when workspace is switched
1262
- */
1263
- async function initBotConfigPersistence(client) {
1264
- if (persistenceInitialized) {
1265
- logger.debug('Bot config persistence already initialized');
1266
- return;
1267
- }
1268
- hailerClient = client;
1269
- // Set up signal handler for real-time activity signals
1270
- try {
1271
- signal_handler_1.SignalHandler.getOrCreate(client.getClient());
1272
- logger.info('Signal handler initialized for bot config monitoring');
1273
- }
1274
- catch (error) {
1275
- logger.warn('Failed to initialize signal handler', {
1276
- error: error instanceof Error ? error.message : String(error)
1277
- });
1278
- }
1279
- // Check if .bot-config/ directory has any files
1280
- const configDir = path.join(process.cwd(), '.bot-config');
1281
- const hasConfigFiles = fs.existsSync(configDir) &&
1282
- fs.readdirSync(configDir).some(f => f.endsWith('.json'));
1283
- // Priority 1: Load from .bot-config/ directory (webhook-maintained configs)
1284
- if (hasConfigFiles) {
1285
- const botConfigState = loadBotStateFromBotConfig();
1286
- if (Object.keys(botConfigState).length > 0) {
1287
- Object.keys(botConfigState).forEach(userId => {
1288
- botState[userId] = botConfigState[userId];
1289
- });
1290
- logger.info('Bot config loaded from .bot-config/ directory', { botCount: Object.keys(botConfigState).length });
1291
- }
1292
- }
1293
- else {
1294
- // Priority 2: Fallback to legacy .bot-state.json if .bot-config/ is empty
1295
- const localState = loadStateFromLocalFile();
1296
- if (Object.keys(localState).length > 0) {
1297
- Object.keys(localState).forEach(userId => {
1298
- botState[userId] = localState[userId];
1299
- });
1300
- logger.info('Bot config loaded from legacy .bot-state.json', { botCount: Object.keys(localState).length });
1301
- }
1302
- }
1303
- // Only sync credentials if .bot-config/ is empty (first-time setup)
1304
- // Otherwise, webhooks have already maintained the credentials
1305
- if (!hasConfigFiles) {
1306
- logger.info('No .bot-config/ files found - syncing credentials from workspaces');
1307
- await syncCredentialsFromAllWorkspaces(client);
1308
- }
1309
- else {
1310
- logger.info('Skipping credential sync - .bot-config/ files already exist (webhook-maintained)');
1311
- }
1312
- logger.info('Bot config persistence initialized');
1313
- persistenceInitialized = true;
1314
- }
1315
- /**
1316
- * Initialize workspace schema for a specific workspace
1317
- * Called by bot clients after connecting to discover the Agent Directory in that workspace
1318
- * @param client - HailerApiClient for API calls
1319
- * @param workspaceId - The workspace ID to initialize schema for
1320
- */
1321
- async function initWorkspaceSchema(client, workspaceId) {
1322
- // Check if we already have schema for this workspace
1323
- const existingSchema = workspaceSchemas.get(workspaceId);
1324
- if (existingSchema?.agentDirectoryWorkflowId) {
1325
- logger.debug('Workspace schema already initialized', { workspaceId });
1326
- return true;
1327
- }
1328
- // Try to load from local file first
1329
- const loadedSchema = loadSchemaConfig(workspaceId);
1330
- if (loadedSchema?.agentDirectoryWorkflowId) {
1331
- logger.info('Workspace schema loaded from file', { workspaceId });
1332
- // Sync credentials from this workspace's Agent Directory
1333
- await syncCredentialsFromWorkspace(client, workspaceId);
1334
- return true;
1335
- }
1336
- // Discover schema from Hailer
1337
- logger.info('Discovering Agent Directory schema for workspace', { workspaceId });
1338
- const discovered = await discoverAgentDirectorySchema(client, workspaceId);
1339
- if (discovered) {
1340
- logger.info('Workspace schema discovered successfully', { workspaceId });
1341
- // Sync credentials from this workspace's Agent Directory
1342
- await syncCredentialsFromWorkspace(client, workspaceId);
1343
- }
1344
- else {
1345
- logger.debug('Agent Directory not found in workspace (may not have AI Hub installed)', { workspaceId });
1346
- }
1347
- return discovered;
1348
- }
1349
- /**
1350
- * Extract bot type from activity's botType field
1351
- */
1352
- function extractBotTypeFromActivity(activity, schema) {
1353
- const fields = activity.fields || {};
1354
- const fieldsAndValues = activity.fieldsAndValues || [];
1355
- const botTypeFieldId = schema?.botTypeFieldId ?? get_botTypeFieldId();
1356
- // Try fields object format - by field KEY first (from returnFlat: true)
1357
- if (fields.botType) {
1358
- return typeof fields.botType === 'string' ? fields.botType : (fields.botType?.value || null);
1359
- }
1360
- // Try fields object format - by field ID
1361
- if (botTypeFieldId) {
1362
- const botTypeField = fields[botTypeFieldId];
1363
- if (botTypeField) {
1364
- return botTypeField.value || botTypeField;
1365
- }
1366
- }
1367
- // Try fieldsAndValues array format
1368
- const fav = fieldsAndValues.find((f) => (botTypeFieldId && f.fieldId === botTypeFieldId) || f.fieldName === 'Bot Type');
1369
- if (fav?.value) {
1370
- return typeof fav.value === 'string' ? fav.value : null;
1371
- }
1372
- return null;
1373
- }
1374
- /**
1375
- * Check if a string looks like a MongoDB ObjectId (24 hex characters)
1376
- */
1377
- function looksLikeUserId(value) {
1378
- return /^[a-f0-9]{24}$/i.test(value);
1379
- }
1380
- /**
1381
- * Extract user ID from activity's hailerProfile field
1382
- */
1383
- function extractUserIdFromActivity(activity, schema) {
1384
- const fields = activity.fields || {};
1385
- const fieldsAndValues = activity.fieldsAndValues || [];
1386
- const hailerProfileFieldId = schema?.hailerProfileFieldId ?? get_hailerProfileFieldId();
1387
- // Debug: log fields for troubleshooting
1388
- logger.debug('Extracting user ID - available fields', {
1389
- activityId: activity._id,
1390
- fieldKeys: Object.keys(fields),
1391
- hailerProfileFieldId,
1392
- fieldsSnapshot: JSON.stringify(fields).slice(0, 500)
1393
- });
1394
- // Try fields object format - by field KEY first (from returnFlat: true)
1395
- if (fields.hailerProfile) {
1396
- const value = fields.hailerProfile;
1397
- return typeof value === 'string' ? value : (Array.isArray(value) ? value[0] : null);
1398
- }
1399
- // Try fields object format - by field ID
1400
- if (hailerProfileFieldId) {
1401
- const profileField = fields[hailerProfileFieldId];
1402
- if (profileField) {
1403
- const value = profileField.value || profileField;
1404
- return typeof value === 'string' ? value : (Array.isArray(value) ? value[0] : null);
1405
- }
1406
- }
1407
- // Try fieldsAndValues array format
1408
- const fav = fieldsAndValues.find((f) => hailerProfileFieldId && f.fieldId === hailerProfileFieldId);
1409
- if (fav?.value) {
1410
- return typeof fav.value === 'string' ? fav.value : (Array.isArray(fav.value) ? fav.value[0] : null);
1411
- }
1412
- // Fallback: iterate ALL fields and look for users field type
1413
- // This handles activities.load returning fields by ID with {type, value} structure
1414
- for (const [fieldId, fieldValue] of Object.entries(fields)) {
1415
- // Check for {type: "users", value: "..."} structure from activities.load
1416
- if (fieldValue && typeof fieldValue === 'object' && !Array.isArray(fieldValue)) {
1417
- const typedField = fieldValue;
1418
- if (typedField.type === 'users' && typedField.value) {
1419
- const value = typedField.value;
1420
- const userId = typeof value === 'string' ? value : (Array.isArray(value) ? value[0] : null);
1421
- if (userId && looksLikeUserId(userId)) {
1422
- logger.debug('Found user ID in typed users field', { fieldId, userId });
1423
- return userId;
1424
- }
1425
- }
1426
- }
1427
- // Users fields are typically arrays of user IDs
1428
- if (Array.isArray(fieldValue) && fieldValue.length > 0) {
1429
- const firstVal = fieldValue[0];
1430
- if (typeof firstVal === 'string' && looksLikeUserId(firstVal)) {
1431
- logger.debug('Found user ID in array field', { fieldId, userId: firstVal });
1432
- return firstVal;
1433
- }
1434
- }
1435
- // Also check for single user ID string
1436
- if (typeof fieldValue === 'string' && looksLikeUserId(fieldValue)) {
1437
- // Skip known non-user fields (we're looking for hailerProfile specifically)
1438
- // Avoid returning schemaConfig field ID or other IDs
1439
- if (!fieldValue.startsWith('{')) { // Not JSON
1440
- logger.debug('Found potential user ID in string field', { fieldId, userId: fieldValue });
1441
- // Only return if this looks like a user field (not other activity links)
1442
- // For now, return the first match - could be refined
1443
- }
1444
- }
1445
- }
1446
- return null;
1447
- }
1448
- /**
1449
- * Extract credentials and display name from activity
1450
- */
1451
- function extractCredentialsFromActivity(activity, schema) {
1452
- const fields = activity.fields || {};
1453
- const fieldsAndValues = activity.fieldsAndValues || [];
1454
- const emailFieldId = schema?.emailFieldId ?? get_emailFieldId();
1455
- const passwordFieldId = schema?.passwordFieldId ?? get_passwordFieldId();
1456
- let email = null;
1457
- let password = null;
1458
- // Try fields object format - by field KEY first (from returnFlat: true)
1459
- if (fields.agentEmailInHailer) {
1460
- email = typeof fields.agentEmailInHailer === 'string' ? fields.agentEmailInHailer : (fields.agentEmailInHailer?.value || null);
1461
- }
1462
- if (fields.password) {
1463
- password = typeof fields.password === 'string' ? fields.password : (fields.password?.value || null);
1464
- }
1465
- // Try fields object format - by field ID
1466
- if (!email && emailFieldId) {
1467
- const emailField = fields[emailFieldId];
1468
- if (emailField) {
1469
- email = emailField.value || emailField;
1470
- }
1471
- }
1472
- if (!password && passwordFieldId) {
1473
- const passwordField = fields[passwordFieldId];
1474
- if (passwordField) {
1475
- password = passwordField.value || passwordField;
1476
- }
1477
- }
1478
- // Try fieldsAndValues array format
1479
- if (!email) {
1480
- const emailFav = fieldsAndValues.find((f) => emailFieldId && f.fieldId === emailFieldId);
1481
- email = emailFav?.value || null;
1482
- }
1483
- if (!password) {
1484
- const passwordFav = fieldsAndValues.find((f) => passwordFieldId && f.fieldId === passwordFieldId);
1485
- password = passwordFav?.value || null;
1486
- }
1487
- // Fallback: iterate ALL fields and find email/password by pattern
1488
- // This is only used if field discovery hasn't run - prefer using discovered field IDs
1489
- if (!email || !password) {
1490
- for (const [fieldId, fieldValue] of Object.entries(fields)) {
1491
- const strValue = typeof fieldValue === 'string' ? fieldValue : fieldValue?.value;
1492
- if (typeof strValue !== 'string')
1493
- continue;
1494
- // Email pattern: contains @
1495
- if (!email && strValue.includes('@') && strValue.includes('.')) {
1496
- const cleanEmail = strValue.replace(/['"]/g, '').trim();
1497
- logger.debug('Found email by pattern', { fieldId, email: (0, config_1.maskEmail)(cleanEmail) });
1498
- email = cleanEmail;
1499
- }
1500
- }
1501
- // Note: password fallback removed - rely on field discovery for correct password field
1502
- if (!password) {
1503
- logger.debug('Password not found - field discovery may not have run');
1504
- }
1505
- }
1506
- if (email && password) {
1507
- // Include activity name as display name for the bot
1508
- const displayName = activity.name || undefined;
1509
- return { email, password, displayName };
1510
- }
1511
- return null;
1512
- }
1513
- /**
1514
- * Extract schema config from activity (written by AI Hub for MCP sync)
1515
- * Returns the workflow/phase IDs if found, null otherwise
1516
- */
1517
- function extractSchemaConfigFromActivity(activity) {
1518
- const fields = activity.fields || {};
1519
- // Debug: log available field keys
1520
- logger.debug('Extracting schema config - available fields', {
1521
- activityId: activity._id,
1522
- fieldKeys: Object.keys(fields),
1523
- lookingFor: FIELD_KEY_SCHEMA_CONFIG
1524
- });
1525
- // Try fields object format - by field KEY first (from returnFlat: true)
1526
- let configJson = null;
1527
- for (const key of FIELD_KEY_SCHEMA_CONFIG) {
1528
- if (fields[key]) {
1529
- configJson = typeof fields[key] === 'string' ? fields[key] : (fields[key]?.value || null);
1530
- logger.debug('Found schema config field by key', { key, value: configJson });
1531
- if (configJson)
1532
- break;
1533
- }
1534
- }
1535
- // If not found by key, iterate ALL fields and try to parse as schema config
1536
- // (activities.load returns fields keyed by field ID, not field key)
1537
- if (!configJson) {
1538
- for (const [fieldId, fieldValue] of Object.entries(fields)) {
1539
- const valueStr = typeof fieldValue === 'string' ? fieldValue : fieldValue?.value;
1540
- if (typeof valueStr === 'string' && valueStr.startsWith('{')) {
1541
- try {
1542
- const parsed = JSON.parse(valueStr);
1543
- if (parsed.agentDirectoryWorkflowId && parsed.deployedPhaseId && parsed.retiredPhaseId) {
1544
- logger.debug('Found schema config by field ID', { fieldId, value: valueStr });
1545
- configJson = valueStr;
1546
- break;
1547
- }
1548
- }
1549
- catch {
1550
- // Not valid JSON, skip
1551
- }
1552
- }
1553
- }
1554
- }
1555
- if (!configJson) {
1556
- logger.debug('No schema config found in fields');
1557
- return null;
1558
- }
1559
- try {
1560
- const config = JSON.parse(configJson);
1561
- // Validate required fields
1562
- if (config.agentDirectoryWorkflowId && config.deployedPhaseId && config.retiredPhaseId) {
1563
- return config;
1564
- }
1565
- logger.warn('Schema config missing required fields', { config });
1566
- return null;
1567
- }
1568
- catch (e) {
1569
- logger.warn('Failed to parse schema config JSON', { configJson, error: e });
1570
- return null;
1571
- }
1572
- }
1573
- /**
1574
- * Apply schema config from activity to a workspace
1575
- */
1576
- function applySchemaConfig(workspaceId, config) {
1577
- // Get existing schema or create new one
1578
- const existingSchema = workspaceSchemas.get(workspaceId);
1579
- const schema = {
1580
- agentDirectoryWorkflowId: config.agentDirectoryWorkflowId,
1581
- deployedPhaseId: config.deployedPhaseId,
1582
- retiredPhaseId: config.retiredPhaseId,
1583
- hailerProfileFieldId: existingSchema?.hailerProfileFieldId ?? null,
1584
- emailFieldId: existingSchema?.emailFieldId ?? null,
1585
- passwordFieldId: existingSchema?.passwordFieldId ?? null,
1586
- botTypeFieldId: existingSchema?.botTypeFieldId ?? null
1587
- };
1588
- setWorkspaceSchema(workspaceId, schema);
1589
- logger.info('Schema config applied from activity', {
1590
- workspaceId,
1591
- workflowId: config.agentDirectoryWorkflowId,
1592
- deployedPhaseId: config.deployedPhaseId,
1593
- retiredPhaseId: config.retiredPhaseId
1594
- });
1595
- }
1596
- /**
1597
- * Discover field IDs from workflow schema for a workspace
1598
- */
1599
- async function discoverFieldIds(client, workspaceId, workflowId, phaseId) {
1600
- const schema = workspaceSchemas.get(workspaceId);
1601
- // Skip if already discovered
1602
- if (schema?.passwordFieldId && schema?.emailFieldId && schema?.hailerProfileFieldId) {
1603
- return;
1604
- }
1605
- try {
1606
- // Use the phase ID if provided, otherwise try deployed phase
1607
- const targetPhaseId = phaseId || schema?.deployedPhaseId;
1608
- if (!targetPhaseId) {
1609
- logger.debug('No phase ID for field discovery', { workspaceId });
1610
- return;
1611
- }
1612
- // Get workflow fields from init data (has 'key' property)
1613
- const init = await client.fetchInit(['processes']);
1614
- const workflow = init?.processes?.find?.((p) => p._id === workflowId)
1615
- || (init?.processes?.[workflowId]); // Could be object or array
1616
- if (!workflow?.fields) {
1617
- logger.debug('Workflow fields not found in init', { workspaceId, workflowId });
1618
- return;
1619
- }
1620
- // Extract field IDs by key from workflow.fields
1621
- const fieldMap = {};
1622
- for (const [fieldId, field] of Object.entries(workflow.fields)) {
1623
- const fieldKey = field.key;
1624
- if (fieldKey) {
1625
- fieldMap[fieldKey] = fieldId;
1626
- }
1627
- }
1628
- logger.debug('Field map from init', {
1629
- workspaceId,
1630
- keys: Object.keys(fieldMap),
1631
- password: fieldMap['password'],
1632
- email: fieldMap['agentEmailInHailer'],
1633
- hailerProfile: fieldMap['hailerProfile']
1634
- });
1635
- // Update the schema with discovered field IDs
1636
- const updatedSchema = {
1637
- agentDirectoryWorkflowId: schema?.agentDirectoryWorkflowId ?? null,
1638
- deployedPhaseId: schema?.deployedPhaseId ?? null,
1639
- retiredPhaseId: schema?.retiredPhaseId ?? null,
1640
- hailerProfileFieldId: findFieldId(fieldMap, FIELD_KEY_HAILER_PROFILE),
1641
- emailFieldId: findFieldId(fieldMap, FIELD_KEY_EMAIL),
1642
- passwordFieldId: findFieldId(fieldMap, FIELD_KEY_PASSWORD),
1643
- botTypeFieldId: findFieldId(fieldMap, FIELD_KEY_BOT_TYPE)
1644
- };
1645
- setWorkspaceSchema(workspaceId, updatedSchema);
1646
- logger.info('Discovered field IDs from schema', {
1647
- workspaceId,
1648
- hailerProfile: updatedSchema.hailerProfileFieldId,
1649
- email: updatedSchema.emailFieldId,
1650
- password: updatedSchema.passwordFieldId,
1651
- botType: updatedSchema.botTypeFieldId
1652
- });
1653
- }
1654
- catch (error) {
1655
- logger.warn('Failed to discover field IDs', {
1656
- workspaceId,
1657
- workflowId,
1658
- error: error instanceof Error ? error.message : String(error)
1659
- });
1660
- }
1661
- }
1662
- /**
1663
- * Update local credentials file with synced credentials from Hailer
1664
- */
1665
- function updateLocalCredentials(userId, creds) {
1666
- const localCreds = loadLocalCredentials();
1667
- localCreds[userId] = {
1668
- email: creds.email,
1669
- password: creds.password,
1670
- ...(creds.displayName && { displayName: creds.displayName })
1671
- };
1672
- saveLocalCredentials(localCreds);
1673
- logger.debug('Credentials synced from Hailer', { userId, email: (0, config_1.maskEmail)(creds.email), displayName: creds.displayName });
1674
- }
1675
- /**
1676
- * Load config from Agent Directory workflow
1677
- *
1678
- * Discovers agents by their hailerProfile (user ID) field.
1679
- * Auto-syncs credentials from Hailer to local storage.
1680
- * Sets the first deployed bot with credentials as the orchestrator (simple approach).
1681
- * Checks which phase each agent activity is in:
1682
- * - deployedPhaseId = enabled
1683
- * - retiredPhaseId = disabled
1684
- *
1685
- * @returns bot config (keyed by user ID) or null if not found
1686
- */
1687
- async function loadConfigFromHailer() {
1688
- if (!hailerClient) {
1689
- logger.debug('Cannot load config: missing Hailer client');
1690
- return null;
1691
- }
1692
- // Get active workspace schema, or try to discover if not set
1693
- let schema = activeWorkspaceId ? workspaceSchemas.get(activeWorkspaceId) : null;
1694
- // If schema IDs not set, try to discover them dynamically
1695
- if (!schema?.agentDirectoryWorkflowId || !schema?.deployedPhaseId || !schema?.retiredPhaseId) {
1696
- logger.info('Schema IDs not set, attempting dynamic discovery...');
1697
- // Get workspace ID from client's init if we don't have one
1698
- if (!activeWorkspaceId) {
1699
- try {
1700
- const init = await hailerClient.fetchInit(['network']);
1701
- activeWorkspaceId = init?.network?._id;
1702
- }
1703
- catch (e) {
1704
- logger.warn('Could not determine workspace ID');
1705
- }
1706
- }
1707
- if (!activeWorkspaceId) {
1708
- logger.debug('Cannot discover schema: no workspace ID');
1709
- return null;
1710
- }
1711
- const discovered = await discoverAgentDirectorySchema(hailerClient, activeWorkspaceId);
1712
- if (!discovered) {
1713
- logger.debug('Cannot load config: schema discovery failed - will retry on signal');
1714
- return null;
1715
- }
1716
- schema = workspaceSchemas.get(activeWorkspaceId);
1717
- logger.info('Schema discovery successful', {
1718
- workspaceId: activeWorkspaceId,
1719
- workflowId: schema?.agentDirectoryWorkflowId ?? 'unknown',
1720
- deployedPhaseId: schema?.deployedPhaseId ?? 'unknown',
1721
- retiredPhaseId: schema?.retiredPhaseId ?? 'unknown'
1722
- });
1723
- }
1724
- // After discovery, verify we have all required IDs
1725
- if (!schema?.agentDirectoryWorkflowId || !schema?.deployedPhaseId || !schema?.retiredPhaseId) {
1726
- logger.warn('Schema discovery incomplete - missing required IDs');
1727
- return null;
1728
- }
1729
- // Now TypeScript knows these are non-null
1730
- const workflowId = schema.agentDirectoryWorkflowId;
1731
- const deployedPhase = schema.deployedPhaseId;
1732
- const retiredPhase = schema.retiredPhaseId;
1733
- try {
1734
- const config = {};
1735
- // Reset discovered IDs (now keyed by user ID)
1736
- discoveredActivityIds = {};
1737
- // Reset orchestrator user ID (will be set to first deployed bot with credentials)
1738
- orchestratorUserId = null;
1739
- // Fetch activities from deployed phase (enabled agents)
1740
- logger.debug('Fetching deployed agents...', { phaseId: deployedPhase });
1741
- const deployedActivities = await hailerClient.fetchActivityList(workflowId, deployedPhase, 100, // limit
1742
- { returnFlat: true });
1743
- // Mark agents in deployed phase as enabled
1744
- const deployedList = Array.isArray(deployedActivities)
1745
- ? deployedActivities
1746
- : (deployedActivities?.data || deployedActivities?.activities || []);
1747
- // Debug: log first activity structure to understand format
1748
- if (deployedList.length > 0) {
1749
- logger.debug('First deployed activity structure', {
1750
- activityId: deployedList[0]._id,
1751
- name: deployedList[0].name,
1752
- hasFields: !!deployedList[0].fields,
1753
- hasFieldsAndValues: !!deployedList[0].fieldsAndValues,
1754
- fieldKeys: deployedList[0].fields ? Object.keys(deployedList[0].fields) : [],
1755
- fieldsAndValuesLength: deployedList[0].fieldsAndValues?.length || 0
1756
- });
1757
- }
1758
- for (const activity of deployedList) {
1759
- const userId = extractUserIdFromActivity(activity);
1760
- if (userId) {
1761
- config[userId] = true;
1762
- discoveredActivityIds[userId] = activity._id;
1763
- // Extract botType from activity
1764
- const botType = extractBotTypeFromActivity(activity);
1765
- // Auto-sync credentials from Hailer
1766
- const creds = extractCredentialsFromActivity(activity);
1767
- if (creds) {
1768
- updateLocalCredentials(userId, creds);
1769
- // Set orchestrator based on botType='orchestrator', or fall back to first deployed bot with credentials
1770
- if (botType === 'orchestrator' && !orchestratorUserId) {
1771
- setOrchestratorUserId(userId);
1772
- logger.info('Orchestrator set from botType field', { userId, activityId: activity._id, name: activity.name });
1773
- }
1774
- else if (!orchestratorUserId) {
1775
- setOrchestratorUserId(userId);
1776
- logger.info('Orchestrator set to first deployed bot with credentials', { userId, activityId: activity._id, name: activity.name });
1777
- }
1778
- }
1779
- // Store botType in bot state
1780
- const existingEntry = botState[userId];
1781
- botState[userId] = {
1782
- enabled: true,
1783
- userId,
1784
- botType: botType ?? existingEntry?.botType ?? null
1785
- };
1786
- logger.debug('Agent found (enabled)', { userId, activityId: activity._id, name: activity.name, botType });
1787
- }
1788
- }
1789
- // Fetch activities from retired phase (disabled agents)
1790
- logger.debug('Fetching retired agents...', { phaseId: retiredPhase });
1791
- const retiredActivities = await hailerClient.fetchActivityList(workflowId, retiredPhase, 100, // limit
1792
- { returnFlat: true });
1793
- // Mark agents in retired phase as disabled
1794
- const retiredList = Array.isArray(retiredActivities)
1795
- ? retiredActivities
1796
- : (retiredActivities?.data || retiredActivities?.activities || []);
1797
- for (const activity of retiredList) {
1798
- const userId = extractUserIdFromActivity(activity);
1799
- if (userId) {
1800
- config[userId] = false;
1801
- discoveredActivityIds[userId] = activity._id;
1802
- // Extract botType from activity
1803
- const botType = extractBotTypeFromActivity(activity);
1804
- // Also sync credentials for retired bots
1805
- const creds = extractCredentialsFromActivity(activity);
1806
- if (creds) {
1807
- updateLocalCredentials(userId, creds);
1808
- }
1809
- // Store botType in bot state
1810
- const existingEntry = botState[userId];
1811
- botState[userId] = {
1812
- enabled: false,
1813
- userId,
1814
- botType: botType ?? existingEntry?.botType ?? null
1815
- };
1816
- logger.debug('Agent found (disabled)', { userId, activityId: activity._id, name: activity.name, botType });
1817
- }
1818
- }
1819
- logger.info('Discovered agents by user ID', { discoveredActivityIds });
1820
- logger.info('Bot config loaded from Agent Directory', {
1821
- deployedCount: deployedList.length,
1822
- retiredCount: retiredList.length,
1823
- userIdCount: Object.keys(config).length,
1824
- orchestratorUserId
1825
- });
1826
- return config;
1827
- }
1828
- catch (error) {
1829
- logger.warn('Failed to load bot config from Agent Directory', {
1830
- error: error instanceof Error ? error.message : String(error)
1831
- });
1832
- return null;
1833
- }
1834
- }
1835
- /**
1836
- * Persist a single bot's state to Hailer
1837
- *
1838
- * Moves the bot's agent activity between phases:
1839
- * - enabled = move to deployedPhaseId
1840
- * - disabled = move to retiredPhaseId
1841
- *
1842
- * @param botId - The bot ID to persist
1843
- * @param enabled - Whether the bot is enabled
1844
- */
1845
- async function persistBotToHailer(botId, enabled) {
1846
- if (!hailerClient || !persistenceInitialized) {
1847
- logger.debug('Persistence not initialized, skipping save');
1848
- return;
1849
- }
1850
- const activityId = getActivityIdForBot(botId);
1851
- if (!activityId) {
1852
- logger.warn('No activity ID found for bot (not discovered yet)', { botId });
1853
- return;
1854
- }
1855
- const targetPhaseId = enabled ? get_deployedPhaseId() : get_retiredPhaseId();
1856
- try {
1857
- logger.debug('Moving agent to phase...', {
1858
- botId,
1859
- activityId,
1860
- enabled,
1861
- targetPhaseId
1862
- });
1863
- // Use updateActivities to move the agent activity to the appropriate phase
1864
- await hailerClient.updateActivities([{ _id: activityId }], { phaseId: targetPhaseId });
1865
- logger.info('Agent moved to phase', {
1866
- botId,
1867
- activityId,
1868
- phase: enabled ? 'deployed' : 'retired',
1869
- phaseId: targetPhaseId
1870
- });
1871
- }
1872
- catch (error) {
1873
- logger.warn('Failed to persist bot state to Hailer', {
1874
- botId,
1875
- activityId,
1876
- error: error instanceof Error ? error.message : String(error)
1877
- });
1878
- }
1879
- }
1880
- /**
1881
- * Check if persistence is initialized
1882
- */
1883
- function isPersistenceInitialized() {
1884
- return persistenceInitialized;
1885
- }
1886
- // ============================================================================
1887
- // Local Credential Storage (Fallback)
1888
- // ============================================================================
1889
- const LOCAL_CREDENTIALS_FILE = path.join(process.cwd(), '.bot-credentials.json');
1890
- /**
1891
- * Load credentials from local file storage
1892
- */
1893
- function loadLocalCredentials() {
1894
- try {
1895
- if (fs.existsSync(LOCAL_CREDENTIALS_FILE)) {
1896
- const data = fs.readFileSync(LOCAL_CREDENTIALS_FILE, 'utf-8');
1897
- return JSON.parse(data);
1898
- }
1899
- }
1900
- catch (error) {
1901
- logger.warn('Failed to load local credentials', {
1902
- error: error instanceof Error ? error.message : String(error)
1903
- });
1904
- }
1905
- return {};
1906
- }
1907
- /**
1908
- * Save credentials to local file storage
1909
- */
1910
- function saveLocalCredentials(creds) {
1911
- try {
1912
- fs.writeFileSync(LOCAL_CREDENTIALS_FILE, JSON.stringify(creds, null, 2));
1913
- logger.debug('Local credentials saved', { file: LOCAL_CREDENTIALS_FILE });
1914
- }
1915
- catch (error) {
1916
- logger.warn('Failed to save local credentials', {
1917
- error: error instanceof Error ? error.message : String(error)
1918
- });
1919
- }
1920
- }
1921
- // ============================================================================
1922
- // Credential Management Functions
1923
- // ============================================================================
1924
- // Agent Directory field IDs for credentials
1925
- // These are the field IDs in the Agent Directory workflow schema
1926
- const AGENT_DIRECTORY_FIELD_IDS = {
1927
- email: '695784898d347a6c707ee375', // agentEmailInHailer
1928
- password: '695784898d347a6c707ee376', // password
1929
- firstName: '695784898d347a6c707ee36c', // firstName (Etunimi)
1930
- lastName: '695784898d347a6c707ee36d', // lastName (Sukunimi)
1931
- };
1932
- /**
1933
- * Get the Hailer user ID for a bot from the Agent Directory's hailerProfile field
1934
- * Returns null if not found or not configured
1935
- */
1936
- async function getAgentUserIdFromDirectory(botId, hailerClient) {
1937
- if (!hailerClient)
1938
- return null;
1939
- const AGENT_DIRECTORY_WORKFLOW = '695784898d347a6c707ee365';
1940
- const DEPLOYED_PHASE = '695784898d347a6c707ee37d';
1941
- const HAILER_PROFILE_FIELD = '695784898d347a6c707ee374';
1942
- try {
1943
- // Find bot activity by name matching botId
1944
- const response = await hailerClient.request('v3.activity.list', [
1945
- { processId: AGENT_DIRECTORY_WORKFLOW, phaseId: DEPLOYED_PHASE },
1946
- { limit: 50 }
1947
- ]);
1948
- // Match bot by name (case-insensitive, partial match)
1949
- const botActivity = response.data?.find(a => a.name.toLowerCase().includes(botId.toLowerCase()));
1950
- if (!botActivity) {
1951
- logger.debug('Bot activity not found in Agent Directory', { botId });
1952
- return null;
1953
- }
1954
- // Get hailerProfile field value
1955
- const profileField = botActivity.fieldsAndValues?.find(f => f.fieldId === HAILER_PROFILE_FIELD);
1956
- if (!profileField?.value) {
1957
- logger.debug('Bot has no hailerProfile configured', { botId, activityId: botActivity._id });
1958
- return null;
1959
- }
1960
- // Users field can be array or single value
1961
- const userId = Array.isArray(profileField.value) ? profileField.value[0] : profileField.value;
1962
- logger.debug('Found user ID for bot', { botId, userId });
1963
- return userId;
1964
- }
1965
- catch (error) {
1966
- logger.warn('Failed to get agent user ID from directory', {
1967
- botId,
1968
- error: error instanceof Error ? error.message : String(error)
1969
- });
1970
- return null;
1971
- }
1972
- }
1973
- /**
1974
- * Get bot credentials from Agent Directory
1975
- *
1976
- * Reads the email and password fields from the bot's activity.
1977
- * Password is masked for security unless showPassword is true.
1978
- * Falls back to local file storage if Hailer is unavailable.
1979
- *
1980
- * @param botIdOrActivityId - The bot ID (hal, bugFixer, etc.) or activity ID
1981
- * @param showPassword - If true, returns unmasked password. Default false for security.
1982
- * @returns Credentials or null if not found
1983
- */
1984
- async function getBotCredentials(botIdOrActivityId, showPassword = false) {
1985
- // Support both bot IDs (hal, bugFixer) and activity IDs
1986
- let botId = botIdOrActivityId;
1987
- let activityId = getActivityIdForBot(botIdOrActivityId);
1988
- // If not found by bot ID, try reverse lookup by activity ID
1989
- if (!activityId) {
1990
- // Check if input is an activity ID in discovered or legacy lists
1991
- const allActivityIds = { ...discoveredActivityIds, ...LEGACY_AGENT_ACTIVITY_IDS };
1992
- const reverseEntry = Object.entries(allActivityIds).find(([, aId]) => aId === botIdOrActivityId);
1993
- if (reverseEntry) {
1994
- botId = reverseEntry[0];
1995
- activityId = reverseEntry[1];
1996
- }
1997
- else {
1998
- // Not in predefined list - assume input is a raw activity ID
1999
- activityId = botIdOrActivityId;
2000
- botId = botIdOrActivityId; // Use activity ID as identifier
2001
- logger.debug('Using raw activity ID for credentials lookup', { activityId });
2002
- }
2003
- }
2004
- // Try local storage first as it may have newer credentials
2005
- const localCreds = loadLocalCredentials();
2006
- // NEW: Try to get user ID from Agent Directory first
2007
- const userId = await getAgentUserIdFromDirectory(botId, hailerClient);
2008
- if (userId && localCreds[userId]) {
2009
- logger.debug('Found credentials by user ID', { botId, userId, showPassword });
2010
- return {
2011
- email: localCreds[userId].email,
2012
- password: showPassword ? localCreds[userId].password : '********',
2013
- };
2014
- }
2015
- // Fallback: Try bot ID directly (backwards compatibility)
2016
- const localBotCreds = localCreds[botId];
2017
- if (localBotCreds) {
2018
- logger.debug('Found credentials by bot ID (legacy)', { botId, showPassword });
2019
- }
2020
- if (!hailerClient) {
2021
- // No Hailer connection - use local storage only
2022
- if (localBotCreds) {
2023
- logger.debug('Using local credentials (Hailer unavailable)', { botId, showPassword });
2024
- return {
2025
- email: localBotCreds.email,
2026
- password: showPassword ? localBotCreds.password : '********',
2027
- };
2028
- }
2029
- logger.warn('Cannot get credentials: Hailer client not initialized and no local credentials', { botId });
2030
- return null;
2031
- }
2032
- // Try Hailer first if available
2033
- try {
2034
- logger.debug('Fetching bot credentials from Hailer...', { botId, activityId });
2035
- // Fetch the activity to get field values
2036
- const activity = await hailerClient.fetchActivityById(activityId);
2037
- if (!activity) {
2038
- logger.warn('Activity not found', { botId, activityId });
2039
- // Fall back to local storage
2040
- if (localBotCreds) {
2041
- logger.debug('Activity not found in Hailer, using local credentials', { botId, showPassword });
2042
- return {
2043
- email: localBotCreds.email,
2044
- password: showPassword ? localBotCreds.password : '********',
2045
- };
2046
- }
2047
- return null;
2048
- }
2049
- // Extract email and password from fieldsAndValues
2050
- const fields = activity.fieldsAndValues || {};
2051
- const email = fields[AGENT_DIRECTORY_FIELD_IDS.email];
2052
- const password = fields[AGENT_DIRECTORY_FIELD_IDS.password];
2053
- logger.debug('Bot credentials fetched from Hailer', {
2054
- botId,
2055
- hasEmail: !!email,
2056
- hasPassword: !!password,
2057
- showPassword
2058
- });
2059
- // If Hailer has no credentials stored, fall back to local storage
2060
- if (!email && !password && localBotCreds) {
2061
- logger.debug('Hailer has empty credentials, falling back to local storage', { botId });
2062
- return {
2063
- email: localBotCreds.email,
2064
- password: showPassword ? localBotCreds.password : '********'
2065
- };
2066
- }
2067
- return {
2068
- email: email || undefined,
2069
- // Mask password for security unless showPassword is true
2070
- password: password ? (showPassword ? password : '********') : undefined
2071
- };
2072
- }
2073
- catch (error) {
2074
- logger.warn('Failed to get bot credentials from Hailer, trying local storage', {
2075
- botId,
2076
- error: error instanceof Error ? error.message : String(error)
2077
- });
2078
- // Fall back to local storage
2079
- if (localBotCreds) {
2080
- return {
2081
- email: localBotCreds.email,
2082
- password: showPassword ? localBotCreds.password : '********'
2083
- };
2084
- }
2085
- return null;
2086
- }
2087
- }
2088
- /**
2089
- * Update bot credentials in Agent Directory
2090
- *
2091
- * Writes the email and/or password to the bot's activity fields.
2092
- * Only updates fields that are provided.
2093
- * Saves to local file storage as backup (and uses as fallback if Hailer unavailable).
2094
- *
2095
- * @param botIdOrActivityId - The bot ID (hal, bugFixer, etc.) or raw activity ID
2096
- * @param credentials - Credentials to update (email and/or password)
2097
- */
2098
- async function updateBotCredentials(botIdOrActivityId, credentials) {
2099
- let activityId = getActivityIdForBot(botIdOrActivityId);
2100
- let botId = botIdOrActivityId;
2101
- // If not found by bot ID, try reverse lookup or use as raw activity ID
2102
- if (!activityId) {
2103
- const allActivityIds = { ...discoveredActivityIds, ...LEGACY_AGENT_ACTIVITY_IDS };
2104
- const reverseEntry = Object.entries(allActivityIds).find(([, aId]) => aId === botIdOrActivityId);
2105
- if (reverseEntry) {
2106
- botId = reverseEntry[0];
2107
- activityId = reverseEntry[1];
2108
- }
2109
- else {
2110
- // Assume input is a raw activity ID
2111
- activityId = botIdOrActivityId;
2112
- logger.debug('Using raw activity ID for credentials update', { activityId });
2113
- }
2114
- }
2115
- // Check if email is already used by another bot
2116
- if (credentials.email) {
2117
- const localCreds = loadLocalCredentials();
2118
- for (const [existingBotId, existingCreds] of Object.entries(localCreds)) {
2119
- if (existingBotId !== botId && existingCreds.email.toLowerCase() === credentials.email.toLowerCase()) {
2120
- const errorMsg = `Email is already used by another bot`;
2121
- logger.warn('Email already in use', { botId, email: (0, config_1.maskEmail)(credentials.email), usedBy: existingBotId });
2122
- throw new Error(errorMsg);
2123
- }
2124
- }
2125
- }
2126
- // Always save to local storage first (as backup and for hot-reload)
2127
- if (credentials.email && credentials.password && credentials.password !== '********') {
2128
- const localCreds = loadLocalCredentials();
2129
- localCreds[botId] = {
2130
- email: credentials.email,
2131
- password: credentials.password
2132
- };
2133
- saveLocalCredentials(localCreds);
2134
- logger.info('Bot credentials saved locally', { botId });
2135
- }
2136
- // Notify listeners (even if Hailer save fails, local save succeeded)
2137
- credentialChangeCallbacks.forEach(cb => cb(botId));
2138
- if (!hailerClient) {
2139
- logger.info('Hailer client not initialized - credentials saved locally only', { botId });
2140
- return; // Success - saved locally
2141
- }
2142
- // Build field updates - only include fields that were provided
2143
- const fields = {};
2144
- if (credentials.email !== undefined) {
2145
- fields[AGENT_DIRECTORY_FIELD_IDS.email] = credentials.email;
2146
- }
2147
- // Only update password if it's not the masked value
2148
- if (credentials.password !== undefined && credentials.password !== '********') {
2149
- fields[AGENT_DIRECTORY_FIELD_IDS.password] = credentials.password;
2150
- }
2151
- if (Object.keys(fields).length === 0) {
2152
- logger.debug('No credentials to update in Hailer', { botId });
2153
- return;
2154
- }
2155
- try {
2156
- logger.info('Updating bot credentials in Hailer...', {
2157
- botId,
2158
- activityId,
2159
- updatingEmail: !!fields[AGENT_DIRECTORY_FIELD_IDS.email],
2160
- updatingPassword: !!fields[AGENT_DIRECTORY_FIELD_IDS.password]
2161
- });
2162
- // Update the activity fields
2163
- await hailerClient.updateActivities([{ _id: activityId, fields }], {});
2164
- logger.info('Bot credentials updated in Hailer', { botId });
2165
- }
2166
- catch (error) {
2167
- logger.warn('Failed to update bot credentials in Hailer (already saved locally)', {
2168
- botId,
2169
- error: error instanceof Error ? error.message : String(error)
2170
- });
2171
- // Don't throw - credentials are already saved locally and listeners already notified
2172
- }
2173
- }
2174
- /**
2175
- * Get unmasked bot credentials for daemon creation
2176
- *
2177
- * NEW: Now primarily looks up by user ID instead of bot ID.
2178
- * This function returns the actual password (not masked) from local storage.
2179
- * Should only be used internally for creating bot daemon instances.
2180
- *
2181
- * @param userIdOrBotId - The user ID or legacy bot ID (hal, bugFixer, etc.)
2182
- * @returns Unmasked credentials or null if not found
2183
- */
2184
- function getLocalBotCredentials(userIdOrBotId) {
2185
- const localCreds = loadLocalCredentials();
2186
- // Direct lookup by user ID (new approach)
2187
- if (localCreds[userIdOrBotId]) {
2188
- return localCreds[userIdOrBotId];
2189
- }
2190
- // Legacy: Try getting userId from bot-state, then lookup by userId
2191
- const userId = getBotUserId(userIdOrBotId);
2192
- if (userId && localCreds[userId]) {
2193
- return localCreds[userId];
2194
- }
2195
- return null;
2196
- }
2197
- /**
2198
- * Get all local bot credentials (for daemon initialization)
2199
- * Returns all bots stored in .bot-credentials.json
2200
- */
2201
- function getAllLocalBotCredentials() {
2202
- return loadLocalCredentials();
2203
- }
2204
- /**
2205
- * Get persistence status info (for debugging)
2206
- */
2207
- function getPersistenceStatus() {
2208
- return {
2209
- initialized: persistenceInitialized,
2210
- workflowId: get_agentDirectoryWorkflowId(),
2211
- deployedPhaseId: get_deployedPhaseId(),
2212
- retiredPhaseId: get_retiredPhaseId(),
2213
- agentActivityIds: { ...LEGACY_AGENT_ACTIVITY_IDS, ...discoveredActivityIds },
2214
- hasClient: hailerClient !== null
2215
- };
2216
- }
2217
- /**
2218
- * List available bots and their status
2219
- */
2220
- exports.listBotsConfigTool = {
2221
- name: 'list_bots_config',
2222
- group: tool_registry_1.ToolGroup.READ,
2223
- description: 'List available AI bots and their enabled/disabled status',
2224
- schema: zod_1.z.object({}),
2225
- execute: async (_params, _context) => {
2226
- const bots = exports.AVAILABLE_BOTS.map(bot => ({
2227
- ...bot,
2228
- enabled: botState[bot.id]?.enabled || false
2229
- }));
2230
- return {
2231
- content: [{
2232
- type: 'text',
2233
- text: JSON.stringify({ bots }, null, 2)
2234
- }]
2235
- };
2236
- }
2237
- };
2238
- /**
2239
- * Enable a bot
2240
- */
2241
- exports.enableBotTool = {
2242
- name: 'enable_bot',
2243
- group: tool_registry_1.ToolGroup.WRITE,
2244
- description: 'Enable an AI bot in this workspace',
2245
- schema: zod_1.z.object({
2246
- botId: zod_1.z.string().describe('Bot ID to enable (hal, bugFixer, hailerExpert, vastuullisuus)')
2247
- }),
2248
- execute: async (params, _context) => {
2249
- const { botId } = params;
2250
- if (!exports.AVAILABLE_BOTS.find(b => b.id === botId)) {
2251
- return {
2252
- content: [{
2253
- type: 'text',
2254
- text: `Unknown bot: ${botId}. Available: ${exports.AVAILABLE_BOTS.map(b => b.id).join(', ')}`
2255
- }],
2256
- isError: true
2257
- };
2258
- }
2259
- // Use setBotEnabled to trigger persistence
2260
- setBotEnabled(botId, true);
2261
- return {
2262
- content: [{
2263
- type: 'text',
2264
- text: `${botId} bot enabled`
2265
- }]
2266
- };
2267
- }
2268
- };
2269
- /**
2270
- * Disable a bot
2271
- */
2272
- exports.disableBotTool = {
2273
- name: 'disable_bot',
2274
- group: tool_registry_1.ToolGroup.WRITE,
2275
- description: 'Disable an AI bot in this workspace',
2276
- schema: zod_1.z.object({
2277
- botId: zod_1.z.string().describe('Bot ID to disable (hal, bugFixer, hailerExpert, vastuullisuus)')
2278
- }),
2279
- execute: async (params, _context) => {
2280
- const { botId } = params;
2281
- if (!exports.AVAILABLE_BOTS.find(b => b.id === botId)) {
2282
- return {
2283
- content: [{
2284
- type: 'text',
2285
- text: `Unknown bot: ${botId}. Available: ${exports.AVAILABLE_BOTS.map(b => b.id).join(', ')}`
2286
- }],
2287
- isError: true
2288
- };
2289
- }
2290
- // Use setBotEnabled to trigger persistence
2291
- setBotEnabled(botId, false);
2292
- return {
2293
- content: [{
2294
- type: 'text',
2295
- text: `${botId} bot disabled`
2296
- }]
2297
- };
2298
- }
2299
- };
2300
- /**
2301
- * Check specialist status from Agent Directory
2302
- * Returns real-time status showing which specialists are deployed vs retired
2303
- */
2304
- exports.checkSpecialistStatusTool = {
2305
- name: 'check_specialist_status',
2306
- group: tool_registry_1.ToolGroup.READ,
2307
- description: 'Check real-time status of AI specialists from Agent Directory. Returns which specialists are deployed (active) vs retired (disabled).',
2308
- schema: zod_1.z.object({
2309
- specialistName: zod_1.z.string().optional().describe("Optional: specific specialist name to check (e.g., 'Hailer Expert', 'Bug Fixer'). If not provided, returns all specialists.")
2310
- }),
2311
- execute: async (params, _context) => {
2312
- const { specialistName } = params;
2313
- // Check if we have the required IDs
2314
- let workflowId = get_agentDirectoryWorkflowId();
2315
- let deployedPhase = get_deployedPhaseId();
2316
- let retiredPhase = get_retiredPhaseId();
2317
- if (!workflowId || !deployedPhase || !retiredPhase) {
2318
- // Try to discover schema if not already done
2319
- if (hailerClient && activeWorkspaceId) {
2320
- const discovered = await discoverAgentDirectorySchema(hailerClient, activeWorkspaceId);
2321
- workflowId = get_agentDirectoryWorkflowId();
2322
- deployedPhase = get_deployedPhaseId();
2323
- retiredPhase = get_retiredPhaseId();
2324
- if (!discovered || !workflowId || !deployedPhase || !retiredPhase) {
2325
- return {
2326
- content: [{
2327
- type: 'text',
2328
- text: '**Agent Directory not configured**\n\nThe Agent Directory workflow was not found or not properly configured. Make sure the AI Hub template is installed in your workspace.'
2329
- }],
2330
- isError: true
2331
- };
2332
- }
2333
- }
2334
- else {
2335
- return {
2336
- content: [{
2337
- type: 'text',
2338
- text: '**Agent Directory not available**\n\nNo Hailer connection available to query Agent Directory.'
2339
- }],
2340
- isError: true
2341
- };
2342
- }
2343
- }
2344
- if (!hailerClient) {
2345
- return {
2346
- content: [{
2347
- type: 'text',
2348
- text: '**Connection Error**\n\nHailer client not initialized.'
2349
- }],
2350
- isError: true
2351
- };
2352
- }
2353
- try {
2354
- const deployedSpecialists = [];
2355
- const retiredSpecialists = [];
2356
- // Fetch deployed (active) specialists
2357
- const deployedActivities = await hailerClient.fetchActivityList(workflowId, deployedPhase, 100, { returnFlat: true });
2358
- const deployedList = Array.isArray(deployedActivities)
2359
- ? deployedActivities
2360
- : (deployedActivities?.data || deployedActivities?.activities || []);
2361
- for (const activity of deployedList) {
2362
- const botType = extractBotTypeFromActivity(activity);
2363
- const name = activity.name || 'Unknown';
2364
- // Filter by specialist name if provided
2365
- if (specialistName) {
2366
- const normalizedSearch = specialistName.toLowerCase();
2367
- const normalizedName = name.toLowerCase();
2368
- const normalizedBotType = (botType || '').toLowerCase();
2369
- if (!normalizedName.includes(normalizedSearch) && !normalizedBotType.includes(normalizedSearch)) {
2370
- continue;
2371
- }
2372
- }
2373
- deployedSpecialists.push({
2374
- name,
2375
- botType,
2376
- activityId: activity._id
2377
- });
2378
- }
2379
- // Fetch retired (disabled) specialists
2380
- const retiredActivities = await hailerClient.fetchActivityList(workflowId, retiredPhase, 100, { returnFlat: true });
2381
- const retiredList = Array.isArray(retiredActivities)
2382
- ? retiredActivities
2383
- : (retiredActivities?.data || retiredActivities?.activities || []);
2384
- for (const activity of retiredList) {
2385
- const botType = extractBotTypeFromActivity(activity);
2386
- const name = activity.name || 'Unknown';
2387
- // Filter by specialist name if provided
2388
- if (specialistName) {
2389
- const normalizedSearch = specialistName.toLowerCase();
2390
- const normalizedName = name.toLowerCase();
2391
- const normalizedBotType = (botType || '').toLowerCase();
2392
- if (!normalizedName.includes(normalizedSearch) && !normalizedBotType.includes(normalizedSearch)) {
2393
- continue;
2394
- }
2395
- }
2396
- retiredSpecialists.push({
2397
- name,
2398
- botType,
2399
- activityId: activity._id
2400
- });
2401
- }
2402
- // Format output
2403
- let output = '**Specialist Status** (live from Agent Directory)\n\n';
2404
- if (deployedSpecialists.length > 0) {
2405
- output += '✅ **Deployed (Active):**\n';
2406
- for (const specialist of deployedSpecialists) {
2407
- const typeDisplay = specialist.botType ? ` (${specialist.botType})` : '';
2408
- output += `- ${specialist.name}${typeDisplay}\n`;
2409
- }
2410
- }
2411
- else {
2412
- output += '✅ **Deployed (Active):** None\n';
2413
- }
2414
- output += '\n';
2415
- if (retiredSpecialists.length > 0) {
2416
- output += '❌ **Retired (Disabled):**\n';
2417
- for (const specialist of retiredSpecialists) {
2418
- const typeDisplay = specialist.botType ? ` (${specialist.botType})` : '';
2419
- output += `- ${specialist.name}${typeDisplay}\n`;
2420
- }
2421
- }
2422
- else {
2423
- output += '❌ **Retired (Disabled):** None\n';
2424
- }
2425
- // Add note if filtering
2426
- if (specialistName && deployedSpecialists.length === 0 && retiredSpecialists.length === 0) {
2427
- output = `**No specialists found matching "${specialistName}"**\n\nTry searching without a filter to see all specialists.`;
2428
- }
2429
- return {
2430
- content: [{
2431
- type: 'text',
2432
- text: output
2433
- }]
2434
- };
2435
- }
2436
- catch (error) {
2437
- logger.error('Failed to check specialist status', {
2438
- error: error instanceof Error ? error.message : String(error)
2439
- });
2440
- return {
2441
- content: [{
2442
- type: 'text',
2443
- text: `**Error checking specialist status**\n\n${error instanceof Error ? error.message : String(error)}`
2444
- }],
2445
- isError: true
2446
- };
2447
- }
2448
- }
2449
- };
2450
- exports.botConfigTools = [
2451
- exports.listBotsConfigTool,
2452
- exports.enableBotTool,
2453
- exports.disableBotTool,
2454
- exports.checkSpecialistStatusTool
2455
- ];
2456
- //# sourceMappingURL=core.js.map