@hailer/mcp 0.1.14 → 0.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/.claude/agents/agent-giuseppe-app-builder.md +7 -6
  2. package/.claude/agents/agent-lars-code-inspector.md +26 -14
  3. package/dist/agents/bot-manager.d.ts +48 -0
  4. package/dist/agents/bot-manager.js +254 -0
  5. package/dist/agents/factory.d.ts +150 -0
  6. package/dist/agents/factory.js +650 -0
  7. package/dist/agents/giuseppe/ai.d.ts +83 -0
  8. package/dist/agents/giuseppe/ai.js +466 -0
  9. package/dist/agents/giuseppe/bot.d.ts +110 -0
  10. package/dist/agents/giuseppe/bot.js +780 -0
  11. package/dist/agents/giuseppe/config.d.ts +25 -0
  12. package/dist/agents/giuseppe/config.js +227 -0
  13. package/dist/agents/giuseppe/files.d.ts +52 -0
  14. package/dist/agents/giuseppe/files.js +338 -0
  15. package/dist/agents/giuseppe/git.d.ts +48 -0
  16. package/dist/agents/giuseppe/git.js +298 -0
  17. package/dist/agents/giuseppe/index.d.ts +97 -0
  18. package/dist/agents/giuseppe/index.js +258 -0
  19. package/dist/agents/giuseppe/lsp.d.ts +113 -0
  20. package/dist/agents/giuseppe/lsp.js +485 -0
  21. package/dist/agents/giuseppe/monitor.d.ts +118 -0
  22. package/dist/agents/giuseppe/monitor.js +621 -0
  23. package/dist/agents/giuseppe/prompt.d.ts +5 -0
  24. package/dist/agents/giuseppe/prompt.js +94 -0
  25. package/dist/agents/giuseppe/registries/pending-classification.d.ts +28 -0
  26. package/dist/agents/giuseppe/registries/pending-classification.js +50 -0
  27. package/dist/agents/giuseppe/registries/pending-fix.d.ts +30 -0
  28. package/dist/agents/giuseppe/registries/pending-fix.js +42 -0
  29. package/dist/agents/giuseppe/registries/pending.d.ts +27 -0
  30. package/dist/agents/giuseppe/registries/pending.js +49 -0
  31. package/dist/agents/giuseppe/specialist.d.ts +47 -0
  32. package/dist/agents/giuseppe/specialist.js +237 -0
  33. package/dist/agents/giuseppe/types.d.ts +123 -0
  34. package/dist/agents/giuseppe/types.js +9 -0
  35. package/dist/agents/hailer-expert/index.d.ts +8 -0
  36. package/dist/agents/hailer-expert/index.js +14 -0
  37. package/dist/agents/hal/daemon.d.ts +142 -0
  38. package/dist/agents/hal/daemon.js +1103 -0
  39. package/dist/agents/hal/definitions.d.ts +55 -0
  40. package/dist/agents/hal/definitions.js +263 -0
  41. package/dist/agents/hal/index.d.ts +3 -0
  42. package/dist/agents/hal/index.js +8 -0
  43. package/dist/agents/index.d.ts +18 -0
  44. package/dist/agents/index.js +48 -0
  45. package/dist/agents/shared/base.d.ts +216 -0
  46. package/dist/agents/shared/base.js +846 -0
  47. package/dist/agents/shared/services/agent-registry.d.ts +107 -0
  48. package/dist/agents/shared/services/agent-registry.js +629 -0
  49. package/dist/agents/shared/services/conversation-manager.d.ts +50 -0
  50. package/dist/agents/shared/services/conversation-manager.js +136 -0
  51. package/dist/agents/shared/services/mcp-client.d.ts +56 -0
  52. package/dist/agents/shared/services/mcp-client.js +124 -0
  53. package/dist/agents/shared/services/message-classifier.d.ts +37 -0
  54. package/dist/agents/shared/services/message-classifier.js +187 -0
  55. package/dist/agents/shared/services/message-formatter.d.ts +89 -0
  56. package/dist/agents/shared/services/message-formatter.js +371 -0
  57. package/dist/agents/shared/services/session-logger.d.ts +106 -0
  58. package/dist/agents/shared/services/session-logger.js +446 -0
  59. package/dist/agents/shared/services/tool-executor.d.ts +41 -0
  60. package/dist/agents/shared/services/tool-executor.js +169 -0
  61. package/dist/agents/shared/services/workspace-schema-cache.d.ts +125 -0
  62. package/dist/agents/shared/services/workspace-schema-cache.js +578 -0
  63. package/dist/agents/shared/specialist.d.ts +91 -0
  64. package/dist/agents/shared/specialist.js +399 -0
  65. package/dist/agents/shared/tool-schema-loader.d.ts +62 -0
  66. package/dist/agents/shared/tool-schema-loader.js +232 -0
  67. package/dist/agents/shared/types.d.ts +327 -0
  68. package/dist/agents/shared/types.js +121 -0
  69. package/dist/app.js +21 -4
  70. package/dist/cli.js +0 -0
  71. package/dist/client/agents/orchestrator.d.ts +1 -0
  72. package/dist/client/agents/orchestrator.js +12 -1
  73. package/dist/commands/seed-config.d.ts +9 -0
  74. package/dist/commands/seed-config.js +372 -0
  75. package/dist/config.d.ts +10 -0
  76. package/dist/config.js +61 -1
  77. package/dist/core.d.ts +8 -0
  78. package/dist/core.js +137 -6
  79. package/dist/lib/discussion-lock.d.ts +42 -0
  80. package/dist/lib/discussion-lock.js +110 -0
  81. package/dist/mcp/UserContextCache.js +2 -2
  82. package/dist/mcp/hailer-clients.d.ts +15 -0
  83. package/dist/mcp/hailer-clients.js +100 -6
  84. package/dist/mcp/signal-handler.d.ts +16 -5
  85. package/dist/mcp/signal-handler.js +173 -122
  86. package/dist/mcp/tools/activity.js +9 -1
  87. package/dist/mcp/tools/bot-config.d.ts +184 -9
  88. package/dist/mcp/tools/bot-config.js +2177 -163
  89. package/dist/mcp/tools/giuseppe-tools.d.ts +21 -0
  90. package/dist/mcp/tools/giuseppe-tools.js +525 -0
  91. package/dist/mcp/utils/hailer-api-client.d.ts +42 -1
  92. package/dist/mcp/utils/hailer-api-client.js +128 -2
  93. package/dist/mcp/webhook-handler.d.ts +87 -0
  94. package/dist/mcp/webhook-handler.js +343 -0
  95. package/dist/mcp/workspace-cache.d.ts +5 -0
  96. package/dist/mcp/workspace-cache.js +11 -0
  97. package/dist/mcp-server.js +55 -5
  98. package/dist/modules/bug-reports/giuseppe-agent.d.ts +58 -0
  99. package/dist/modules/bug-reports/giuseppe-agent.js +467 -0
  100. package/dist/modules/bug-reports/giuseppe-ai.d.ts +25 -1
  101. package/dist/modules/bug-reports/giuseppe-ai.js +133 -2
  102. package/dist/modules/bug-reports/giuseppe-bot.d.ts +3 -2
  103. package/dist/modules/bug-reports/giuseppe-bot.js +75 -36
  104. package/dist/modules/bug-reports/giuseppe-daemon.d.ts +80 -0
  105. package/dist/modules/bug-reports/giuseppe-daemon.js +617 -0
  106. package/dist/modules/bug-reports/giuseppe-files.d.ts +12 -0
  107. package/dist/modules/bug-reports/giuseppe-files.js +37 -0
  108. package/dist/modules/bug-reports/giuseppe-lsp.d.ts +113 -0
  109. package/dist/modules/bug-reports/giuseppe-lsp.js +485 -0
  110. package/dist/modules/bug-reports/index.d.ts +1 -0
  111. package/dist/modules/bug-reports/index.js +31 -29
  112. package/package.json +5 -4
@@ -7,38 +7,707 @@
7
7
  * - Deployed agents phase = enabled
8
8
  * - Retired agents phase = disabled
9
9
  *
10
- * Architecture:
11
- * - In-memory state is primary (for speed)
10
+ * Architecture (User ID-based discovery):
11
+ * - In-memory state is primary (for speed), keyed by USER ID
12
12
  * - Agent Directory workflow is persistence layer (agents as activities, phases for state)
13
- * - On startup: Load config from Hailer by checking which phase each agent is in
13
+ * - On startup: Load config from Hailer by extracting hailerProfile (user ID) from each agent
14
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)
15
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
+ })();
16
51
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.botConfigTools = exports.disableBotTool = exports.enableBotTool = exports.listBotsConfigTool = exports.AVAILABLE_BOTS = void 0;
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;
18
63
  exports.onBotStateChange = onBotStateChange;
64
+ exports.onBotCredentialsChange = onBotCredentialsChange;
65
+ exports.onDaemonRestartNeeded = onDaemonRestartNeeded;
66
+ exports.cleanupBotConfig = cleanupBotConfig;
19
67
  exports.getBotState = getBotState;
68
+ exports.getBotUserId = getBotUserId;
69
+ exports.getBotType = getBotType;
70
+ exports.getEnabledBotsWithTypes = getEnabledBotsWithTypes;
71
+ exports.setBotUserId = setBotUserId;
20
72
  exports.setBotEnabled = setBotEnabled;
73
+ exports.handleActivityPhaseChange = handleActivityPhaseChange;
74
+ exports.reloadConfigFromHailer = reloadConfigFromHailer;
21
75
  exports.initBotConfigPersistence = initBotConfigPersistence;
76
+ exports.initWorkspaceSchema = initWorkspaceSchema;
22
77
  exports.isPersistenceInitialized = isPersistenceInitialized;
78
+ exports.getBotCredentials = getBotCredentials;
79
+ exports.updateBotCredentials = updateBotCredentials;
80
+ exports.getLocalBotCredentials = getLocalBotCredentials;
81
+ exports.getAllLocalBotCredentials = getAllLocalBotCredentials;
23
82
  exports.getPersistenceStatus = getPersistenceStatus;
24
83
  const zod_1 = require("zod");
84
+ const fs = __importStar(require("fs"));
85
+ const path = __importStar(require("path"));
25
86
  const tool_registry_1 = require("../tool-registry");
26
87
  const index_1 = require("../utils/index");
88
+ const signal_handler_1 = require("../signal-handler");
89
+ const config_1 = require("../../config");
27
90
  const logger = (0, index_1.createLogger)({ component: 'bot-config-tools' });
28
91
  // ============================================================================
29
- // Agent Directory Workflow Configuration
92
+ // Local State Persistence (fallback)
30
93
  // ============================================================================
31
- // Agent Directory workflow config
32
- const AGENT_DIRECTORY_WORKFLOW_ID = '695784898d347a6c707ee365';
33
- const DEPLOYED_PHASE_ID = '695784898d347a6c707ee37d'; // enabled
34
- const RETIRED_PHASE_ID = '695784898d347a6c707ee37e'; // disabled
35
- // Mapping from bot IDs to their activity IDs in the Agent Directory workflow
36
- const AGENT_ACTIVITY_IDS = {
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 = {
37
702
  hal: '6957848b8d347a6c707ee3f6',
38
703
  hailerExpert: '695784913c659fc4d8fe0e02',
39
704
  giuseppe: '695e42deca61319424bc8b23',
40
705
  vastuullisuus: '695e42dfca61319424bc8b2e'
41
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
+ }
42
711
  // Persistence state
43
712
  let persistenceInitialized = false;
44
713
  let hailerClient = null; // Will be set during initialization
@@ -73,154 +742,1086 @@ exports.AVAILABLE_BOTS = [
73
742
  description: 'Sustainability analysis for SMEs',
74
743
  icon: '🌱'
75
744
  }
76
- ];
77
- // In-memory bot state (will be synced with workflow)
78
- const botState = {
79
- hal: true, // HAL enabled by default
80
- giuseppe: false,
81
- hailerExpert: false,
82
- vastuullisuus: false
83
- };
84
- // Callbacks for when bot state changes
85
- const stateChangeCallbacks = [];
86
- function onBotStateChange(callback) {
87
- stateChangeCallbacks.push(callback);
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;
88
1447
  }
89
- function getBotState() {
90
- return { ...botState };
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;
91
1512
  }
92
1513
  /**
93
- * Set bot enabled state
94
- *
95
- * Updates in-memory state and persists to Agent Directory by moving
96
- * the agent activity to the appropriate phase (deployed/retired).
1514
+ * Extract schema config from activity (written by AI Hub for MCP sync)
1515
+ * Returns the workflow/phase IDs if found, null otherwise
97
1516
  */
98
- function setBotEnabled(botId, enabled) {
99
- if (!exports.AVAILABLE_BOTS.find(b => b.id === botId)) {
100
- throw new Error(`Unknown bot: ${botId}`);
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;
101
1571
  }
102
- botState[botId] = enabled;
103
- // Notify listeners
104
- stateChangeCallbacks.forEach(cb => cb(botId, enabled));
105
- logger.info('Bot state changed', { botId, enabled });
106
- // Persist single bot to Hailer asynchronously (more efficient than full sync)
107
- persistBotToHailer(botId, enabled).catch(err => logger.warn('Persistence failed', { botId, err: err instanceof Error ? err.message : String(err) }));
108
1572
  }
109
- // ============================================================================
110
- // Persistence Layer Functions
111
- // ============================================================================
112
1573
  /**
113
- * Initialize persistence layer
114
- * Called once during MCP server startup
115
- *
116
- * Loads agent enabled state from the Agent Directory workflow:
117
- * - Agents in DEPLOYED_PHASE_ID are enabled
118
- * - Agents in RETIRED_PHASE_ID are disabled
119
- *
120
- * @param client - HailerApiClient instance for API calls
1574
+ * Apply schema config from activity to a workspace
121
1575
  */
122
- async function initBotConfigPersistence(client) {
123
- if (persistenceInitialized) {
124
- logger.debug('Bot config persistence already initialized');
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) {
125
1603
  return;
126
1604
  }
127
- hailerClient = client;
128
1605
  try {
129
- logger.info('Initializing bot config persistence from Agent Directory...', {
130
- workflowId: AGENT_DIRECTORY_WORKFLOW_ID,
131
- deployedPhaseId: DEPLOYED_PHASE_ID,
132
- retiredPhaseId: RETIRED_PHASE_ID
133
- });
134
- // Load agent state from Agent Directory workflow
135
- const existingConfig = await loadConfigFromHailer();
136
- if (existingConfig) {
137
- // Update in-memory state with persisted values
138
- Object.keys(existingConfig).forEach(botId => {
139
- if (exports.AVAILABLE_BOTS.find(b => b.id === botId)) {
140
- botState[botId] = existingConfig[botId];
141
- }
142
- });
143
- logger.info('Bot config loaded from Agent Directory', {
144
- config: botState
145
- });
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;
146
1619
  }
147
- logger.info('Bot config persistence initialized');
148
- persistenceInitialized = true;
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
+ });
149
1653
  }
150
1654
  catch (error) {
151
- logger.warn('Failed to initialize bot config persistence, using defaults', {
1655
+ logger.warn('Failed to discover field IDs', {
1656
+ workspaceId,
1657
+ workflowId,
152
1658
  error: error instanceof Error ? error.message : String(error)
153
1659
  });
154
- // Still mark as initialized to prevent repeated failures
155
- persistenceInitialized = true;
156
1660
  }
157
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
+ }
158
1675
  /**
159
1676
  * Load config from Agent Directory workflow
160
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).
161
1681
  * Checks which phase each agent activity is in:
162
- * - DEPLOYED_PHASE_ID = enabled
163
- * - RETIRED_PHASE_ID = disabled
1682
+ * - deployedPhaseId = enabled
1683
+ * - retiredPhaseId = disabled
164
1684
  *
165
- * @returns bot config or null if not found
1685
+ * @returns bot config (keyed by user ID) or null if not found
166
1686
  */
167
1687
  async function loadConfigFromHailer() {
168
1688
  if (!hailerClient) {
169
1689
  logger.debug('Cannot load config: missing Hailer client');
170
1690
  return null;
171
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;
172
1733
  try {
173
1734
  const config = {};
174
- // Create reverse mapping: activityId -> botId
175
- const activityIdToBotId = {};
176
- for (const [botId, activityId] of Object.entries(AGENT_ACTIVITY_IDS)) {
177
- activityIdToBotId[activityId] = botId;
178
- }
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;
179
1739
  // Fetch activities from deployed phase (enabled agents)
180
- logger.debug('Fetching deployed agents...', { phaseId: DEPLOYED_PHASE_ID });
181
- const deployedActivities = await hailerClient.fetchActivityList(AGENT_DIRECTORY_WORKFLOW_ID, DEPLOYED_PHASE_ID, 100, // limit
1740
+ logger.debug('Fetching deployed agents...', { phaseId: deployedPhase });
1741
+ const deployedActivities = await hailerClient.fetchActivityList(workflowId, deployedPhase, 100, // limit
182
1742
  { returnFlat: true });
183
1743
  // Mark agents in deployed phase as enabled
184
1744
  const deployedList = Array.isArray(deployedActivities)
185
1745
  ? deployedActivities
186
- : (deployedActivities?.activities || []);
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
+ }
187
1758
  for (const activity of deployedList) {
188
- const botId = activityIdToBotId[activity._id];
189
- if (botId) {
190
- config[botId] = true;
191
- logger.debug('Agent found in deployed phase (enabled)', { botId, activityId: activity._id });
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 });
192
1787
  }
193
1788
  }
194
1789
  // Fetch activities from retired phase (disabled agents)
195
- logger.debug('Fetching retired agents...', { phaseId: RETIRED_PHASE_ID });
196
- const retiredActivities = await hailerClient.fetchActivityList(AGENT_DIRECTORY_WORKFLOW_ID, RETIRED_PHASE_ID, 100, // limit
1790
+ logger.debug('Fetching retired agents...', { phaseId: retiredPhase });
1791
+ const retiredActivities = await hailerClient.fetchActivityList(workflowId, retiredPhase, 100, // limit
197
1792
  { returnFlat: true });
198
1793
  // Mark agents in retired phase as disabled
199
1794
  const retiredList = Array.isArray(retiredActivities)
200
1795
  ? retiredActivities
201
- : (retiredActivities?.activities || []);
1796
+ : (retiredActivities?.data || retiredActivities?.activities || []);
202
1797
  for (const activity of retiredList) {
203
- const botId = activityIdToBotId[activity._id];
204
- if (botId) {
205
- config[botId] = false;
206
- logger.debug('Agent found in retired phase (disabled)', { botId, activityId: activity._id });
207
- }
208
- }
209
- // For any bots not found in either phase, default based on AVAILABLE_BOTS defaults
210
- for (const bot of exports.AVAILABLE_BOTS) {
211
- if (config[bot.id] === undefined) {
212
- // Default: HAL enabled, others disabled
213
- config[bot.id] = bot.id === 'hal';
214
- logger.debug('Agent not found in any phase, using default', {
215
- botId: bot.id,
216
- default: config[bot.id]
217
- });
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 });
218
1817
  }
219
1818
  }
1819
+ logger.info('Discovered agents by user ID', { discoveredActivityIds });
220
1820
  logger.info('Bot config loaded from Agent Directory', {
221
1821
  deployedCount: deployedList.length,
222
1822
  retiredCount: retiredList.length,
223
- config
1823
+ userIdCount: Object.keys(config).length,
1824
+ orchestratorUserId
224
1825
  });
225
1826
  return config;
226
1827
  }
@@ -235,8 +1836,8 @@ async function loadConfigFromHailer() {
235
1836
  * Persist a single bot's state to Hailer
236
1837
  *
237
1838
  * Moves the bot's agent activity between phases:
238
- * - enabled = move to DEPLOYED_PHASE_ID
239
- * - disabled = move to RETIRED_PHASE_ID
1839
+ * - enabled = move to deployedPhaseId
1840
+ * - disabled = move to retiredPhaseId
240
1841
  *
241
1842
  * @param botId - The bot ID to persist
242
1843
  * @param enabled - Whether the bot is enabled
@@ -246,12 +1847,12 @@ async function persistBotToHailer(botId, enabled) {
246
1847
  logger.debug('Persistence not initialized, skipping save');
247
1848
  return;
248
1849
  }
249
- const activityId = AGENT_ACTIVITY_IDS[botId];
1850
+ const activityId = getActivityIdForBot(botId);
250
1851
  if (!activityId) {
251
- logger.warn('No activity ID found for bot', { botId });
1852
+ logger.warn('No activity ID found for bot (not discovered yet)', { botId });
252
1853
  return;
253
1854
  }
254
- const targetPhaseId = enabled ? DEPLOYED_PHASE_ID : RETIRED_PHASE_ID;
1855
+ const targetPhaseId = enabled ? get_deployedPhaseId() : get_retiredPhaseId();
255
1856
  try {
256
1857
  logger.debug('Moving agent to phase...', {
257
1858
  botId,
@@ -277,66 +1878,328 @@ async function persistBotToHailer(botId, enabled) {
277
1878
  }
278
1879
  }
279
1880
  /**
280
- * Save current bot state to Hailer (legacy function for full sync)
281
- * Called asynchronously after state changes
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
282
1975
  *
283
- * Note: This syncs all bots. For single bot changes, persistBotToHailer is more efficient.
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, giuseppe, etc.) or activity ID
1981
+ * @param showPassword - If true, returns unmasked password. Default false for security.
1982
+ * @returns Credentials or null if not found
284
1983
  */
285
- async function persistToHailer() {
286
- if (!hailerClient || !persistenceInitialized) {
287
- logger.debug('Persistence not initialized, skipping save');
288
- return;
1984
+ async function getBotCredentials(botIdOrActivityId, showPassword = false) {
1985
+ // Support both bot IDs (hal, giuseppe) 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;
289
2031
  }
2032
+ // Try Hailer first if available
290
2033
  try {
291
- logger.debug('Persisting all bot config to Hailer...', { state: botState });
292
- // Build updates for all bots
293
- const enabledUpdates = [];
294
- const disabledUpdates = [];
295
- for (const [botId, enabled] of Object.entries(botState)) {
296
- const activityId = AGENT_ACTIVITY_IDS[botId];
297
- if (!activityId) {
298
- logger.debug('No activity ID for bot, skipping', { botId });
299
- continue;
300
- }
301
- if (enabled) {
302
- enabledUpdates.push({ _id: activityId });
303
- }
304
- else {
305
- disabledUpdates.push({ _id: activityId });
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
+ };
306
2046
  }
2047
+ return null;
307
2048
  }
308
- // Move enabled agents to deployed phase
309
- if (enabledUpdates.length > 0) {
310
- await hailerClient.updateActivities(enabledUpdates, { phaseId: DEPLOYED_PHASE_ID });
311
- logger.debug('Moved agents to deployed phase', {
312
- count: enabledUpdates.length,
313
- phaseId: DEPLOYED_PHASE_ID
314
- });
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
+ };
315
2066
  }
316
- // Move disabled agents to retired phase
317
- if (disabledUpdates.length > 0) {
318
- await hailerClient.updateActivities(disabledUpdates, { phaseId: RETIRED_PHASE_ID });
319
- logger.debug('Moved agents to retired phase', {
320
- count: disabledUpdates.length,
321
- phaseId: RETIRED_PHASE_ID
322
- });
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, giuseppe, 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
+ }
323
2124
  }
324
- logger.info('Bot config persisted to Agent Directory', {
325
- enabledCount: enabledUpdates.length,
326
- disabledCount: disabledUpdates.length
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]
327
2161
  });
2162
+ // Update the activity fields
2163
+ await hailerClient.updateActivities([{ _id: activityId, fields }], {});
2164
+ logger.info('Bot credentials updated in Hailer', { botId });
328
2165
  }
329
2166
  catch (error) {
330
- logger.warn('Failed to persist bot config to Hailer', {
2167
+ logger.warn('Failed to update bot credentials in Hailer (already saved locally)', {
2168
+ botId,
331
2169
  error: error instanceof Error ? error.message : String(error)
332
2170
  });
2171
+ // Don't throw - credentials are already saved locally and listeners already notified
333
2172
  }
334
2173
  }
335
2174
  /**
336
- * Check if persistence is initialized
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, giuseppe, etc.)
2182
+ * @returns Unmasked credentials or null if not found
337
2183
  */
338
- function isPersistenceInitialized() {
339
- return persistenceInitialized;
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();
340
2203
  }
341
2204
  /**
342
2205
  * Get persistence status info (for debugging)
@@ -344,10 +2207,10 @@ function isPersistenceInitialized() {
344
2207
  function getPersistenceStatus() {
345
2208
  return {
346
2209
  initialized: persistenceInitialized,
347
- workflowId: AGENT_DIRECTORY_WORKFLOW_ID,
348
- deployedPhaseId: DEPLOYED_PHASE_ID,
349
- retiredPhaseId: RETIRED_PHASE_ID,
350
- agentActivityIds: { ...AGENT_ACTIVITY_IDS },
2210
+ workflowId: get_agentDirectoryWorkflowId(),
2211
+ deployedPhaseId: get_deployedPhaseId(),
2212
+ retiredPhaseId: get_retiredPhaseId(),
2213
+ agentActivityIds: { ...LEGACY_AGENT_ACTIVITY_IDS, ...discoveredActivityIds },
351
2214
  hasClient: hailerClient !== null
352
2215
  };
353
2216
  }
@@ -362,7 +2225,7 @@ exports.listBotsConfigTool = {
362
2225
  execute: async (_params, _context) => {
363
2226
  const bots = exports.AVAILABLE_BOTS.map(bot => ({
364
2227
  ...bot,
365
- enabled: botState[bot.id] || false
2228
+ enabled: botState[bot.id]?.enabled || false
366
2229
  }));
367
2230
  return {
368
2231
  content: [{
@@ -434,9 +2297,160 @@ exports.disableBotTool = {
434
2297
  };
435
2298
  }
436
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', 'Giuseppe'). 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
+ };
437
2450
  exports.botConfigTools = [
438
2451
  exports.listBotsConfigTool,
439
2452
  exports.enableBotTool,
440
- exports.disableBotTool
2453
+ exports.disableBotTool,
2454
+ exports.checkSpecialistStatusTool
441
2455
  ];
442
2456
  //# sourceMappingURL=bot-config.js.map