@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.
- package/.claude/agents/agent-giuseppe-app-builder.md +7 -6
- package/.claude/agents/agent-lars-code-inspector.md +26 -14
- package/dist/agents/bot-manager.d.ts +48 -0
- package/dist/agents/bot-manager.js +254 -0
- package/dist/agents/factory.d.ts +150 -0
- package/dist/agents/factory.js +650 -0
- package/dist/agents/giuseppe/ai.d.ts +83 -0
- package/dist/agents/giuseppe/ai.js +466 -0
- package/dist/agents/giuseppe/bot.d.ts +110 -0
- package/dist/agents/giuseppe/bot.js +780 -0
- package/dist/agents/giuseppe/config.d.ts +25 -0
- package/dist/agents/giuseppe/config.js +227 -0
- package/dist/agents/giuseppe/files.d.ts +52 -0
- package/dist/agents/giuseppe/files.js +338 -0
- package/dist/agents/giuseppe/git.d.ts +48 -0
- package/dist/agents/giuseppe/git.js +298 -0
- package/dist/agents/giuseppe/index.d.ts +97 -0
- package/dist/agents/giuseppe/index.js +258 -0
- package/dist/agents/giuseppe/lsp.d.ts +113 -0
- package/dist/agents/giuseppe/lsp.js +485 -0
- package/dist/agents/giuseppe/monitor.d.ts +118 -0
- package/dist/agents/giuseppe/monitor.js +621 -0
- package/dist/agents/giuseppe/prompt.d.ts +5 -0
- package/dist/agents/giuseppe/prompt.js +94 -0
- package/dist/agents/giuseppe/registries/pending-classification.d.ts +28 -0
- package/dist/agents/giuseppe/registries/pending-classification.js +50 -0
- package/dist/agents/giuseppe/registries/pending-fix.d.ts +30 -0
- package/dist/agents/giuseppe/registries/pending-fix.js +42 -0
- package/dist/agents/giuseppe/registries/pending.d.ts +27 -0
- package/dist/agents/giuseppe/registries/pending.js +49 -0
- package/dist/agents/giuseppe/specialist.d.ts +47 -0
- package/dist/agents/giuseppe/specialist.js +237 -0
- package/dist/agents/giuseppe/types.d.ts +123 -0
- package/dist/agents/giuseppe/types.js +9 -0
- package/dist/agents/hailer-expert/index.d.ts +8 -0
- package/dist/agents/hailer-expert/index.js +14 -0
- package/dist/agents/hal/daemon.d.ts +142 -0
- package/dist/agents/hal/daemon.js +1103 -0
- package/dist/agents/hal/definitions.d.ts +55 -0
- package/dist/agents/hal/definitions.js +263 -0
- package/dist/agents/hal/index.d.ts +3 -0
- package/dist/agents/hal/index.js +8 -0
- package/dist/agents/index.d.ts +18 -0
- package/dist/agents/index.js +48 -0
- package/dist/agents/shared/base.d.ts +216 -0
- package/dist/agents/shared/base.js +846 -0
- package/dist/agents/shared/services/agent-registry.d.ts +107 -0
- package/dist/agents/shared/services/agent-registry.js +629 -0
- package/dist/agents/shared/services/conversation-manager.d.ts +50 -0
- package/dist/agents/shared/services/conversation-manager.js +136 -0
- package/dist/agents/shared/services/mcp-client.d.ts +56 -0
- package/dist/agents/shared/services/mcp-client.js +124 -0
- package/dist/agents/shared/services/message-classifier.d.ts +37 -0
- package/dist/agents/shared/services/message-classifier.js +187 -0
- package/dist/agents/shared/services/message-formatter.d.ts +89 -0
- package/dist/agents/shared/services/message-formatter.js +371 -0
- package/dist/agents/shared/services/session-logger.d.ts +106 -0
- package/dist/agents/shared/services/session-logger.js +446 -0
- package/dist/agents/shared/services/tool-executor.d.ts +41 -0
- package/dist/agents/shared/services/tool-executor.js +169 -0
- package/dist/agents/shared/services/workspace-schema-cache.d.ts +125 -0
- package/dist/agents/shared/services/workspace-schema-cache.js +578 -0
- package/dist/agents/shared/specialist.d.ts +91 -0
- package/dist/agents/shared/specialist.js +399 -0
- package/dist/agents/shared/tool-schema-loader.d.ts +62 -0
- package/dist/agents/shared/tool-schema-loader.js +232 -0
- package/dist/agents/shared/types.d.ts +327 -0
- package/dist/agents/shared/types.js +121 -0
- package/dist/app.js +21 -4
- package/dist/cli.js +0 -0
- package/dist/client/agents/orchestrator.d.ts +1 -0
- package/dist/client/agents/orchestrator.js +12 -1
- package/dist/commands/seed-config.d.ts +9 -0
- package/dist/commands/seed-config.js +372 -0
- package/dist/config.d.ts +10 -0
- package/dist/config.js +61 -1
- package/dist/core.d.ts +8 -0
- package/dist/core.js +137 -6
- package/dist/lib/discussion-lock.d.ts +42 -0
- package/dist/lib/discussion-lock.js +110 -0
- package/dist/mcp/UserContextCache.js +2 -2
- package/dist/mcp/hailer-clients.d.ts +15 -0
- package/dist/mcp/hailer-clients.js +100 -6
- package/dist/mcp/signal-handler.d.ts +16 -5
- package/dist/mcp/signal-handler.js +173 -122
- package/dist/mcp/tools/activity.js +9 -1
- package/dist/mcp/tools/bot-config.d.ts +184 -9
- package/dist/mcp/tools/bot-config.js +2177 -163
- package/dist/mcp/tools/giuseppe-tools.d.ts +21 -0
- package/dist/mcp/tools/giuseppe-tools.js +525 -0
- package/dist/mcp/utils/hailer-api-client.d.ts +42 -1
- package/dist/mcp/utils/hailer-api-client.js +128 -2
- package/dist/mcp/webhook-handler.d.ts +87 -0
- package/dist/mcp/webhook-handler.js +343 -0
- package/dist/mcp/workspace-cache.d.ts +5 -0
- package/dist/mcp/workspace-cache.js +11 -0
- package/dist/mcp-server.js +55 -5
- package/dist/modules/bug-reports/giuseppe-agent.d.ts +58 -0
- package/dist/modules/bug-reports/giuseppe-agent.js +467 -0
- package/dist/modules/bug-reports/giuseppe-ai.d.ts +25 -1
- package/dist/modules/bug-reports/giuseppe-ai.js +133 -2
- package/dist/modules/bug-reports/giuseppe-bot.d.ts +3 -2
- package/dist/modules/bug-reports/giuseppe-bot.js +75 -36
- package/dist/modules/bug-reports/giuseppe-daemon.d.ts +80 -0
- package/dist/modules/bug-reports/giuseppe-daemon.js +617 -0
- package/dist/modules/bug-reports/giuseppe-files.d.ts +12 -0
- package/dist/modules/bug-reports/giuseppe-files.js +37 -0
- package/dist/modules/bug-reports/giuseppe-lsp.d.ts +113 -0
- package/dist/modules/bug-reports/giuseppe-lsp.js +485 -0
- package/dist/modules/bug-reports/index.d.ts +1 -0
- package/dist/modules/bug-reports/index.js +31 -29
- 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
|
|
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
|
-
//
|
|
92
|
+
// Local State Persistence (fallback)
|
|
30
93
|
// ============================================================================
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
//
|
|
36
|
-
const
|
|
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 (
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
90
|
-
|
|
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
|
-
*
|
|
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
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
148
|
-
|
|
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
|
|
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
|
-
* -
|
|
163
|
-
* -
|
|
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
|
-
//
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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:
|
|
181
|
-
const deployedActivities = await hailerClient.fetchActivityList(
|
|
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
|
|
189
|
-
if (
|
|
190
|
-
config[
|
|
191
|
-
|
|
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:
|
|
196
|
-
const retiredActivities = await hailerClient.fetchActivityList(
|
|
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
|
|
204
|
-
if (
|
|
205
|
-
config[
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
|
239
|
-
* - disabled = move to
|
|
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 =
|
|
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 ?
|
|
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
|
-
*
|
|
281
|
-
|
|
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
|
-
*
|
|
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
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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('
|
|
292
|
-
//
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
if (
|
|
298
|
-
logger.debug('
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
//
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
|
339
|
-
|
|
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:
|
|
348
|
-
deployedPhaseId:
|
|
349
|
-
retiredPhaseId:
|
|
350
|
-
agentActivityIds: { ...
|
|
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
|