botforje 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +381 -0
  3. package/dist/action/action.js +44 -0
  4. package/dist/action/catalog.js +40 -0
  5. package/dist/action/cooldown.js +60 -0
  6. package/dist/action/executor.js +23 -0
  7. package/dist/action/template.js +13 -0
  8. package/dist/action/types.js +2 -0
  9. package/dist/action/webhook.js +36 -0
  10. package/dist/actions/action.js +26 -0
  11. package/dist/actions/cooldown.js +66 -0
  12. package/dist/actions/request.js +40 -0
  13. package/dist/api/routes/auth.js +56 -0
  14. package/dist/api/routes/bots.js +51 -0
  15. package/dist/api/routes/config.js +24 -0
  16. package/dist/api/routes/health.js +15 -0
  17. package/dist/api/routes/index.js +14 -0
  18. package/dist/api/routes/messages.js +69 -0
  19. package/dist/api/routes/sessions.js +97 -0
  20. package/dist/api/routes/status.js +38 -0
  21. package/dist/api/server.js +153 -0
  22. package/dist/auth/service.js +102 -0
  23. package/dist/boot/fleet.js +207 -0
  24. package/dist/bot/bot.js +30 -0
  25. package/dist/bot/fleet.js +211 -0
  26. package/dist/bot/fuzzy.js +20 -0
  27. package/dist/bot/mapper.js +47 -0
  28. package/dist/bot/types.js +2 -0
  29. package/dist/bot/validation.js +38 -0
  30. package/dist/bot.js +31 -0
  31. package/dist/cli/create-bot.js +84 -0
  32. package/dist/cli.js +138 -0
  33. package/dist/commands/auth.js +200 -0
  34. package/dist/commands/create-bot.js +64 -0
  35. package/dist/commands/guide.js +563 -0
  36. package/dist/commands/lock.js +73 -0
  37. package/dist/commands/status.js +145 -0
  38. package/dist/commands/unlock.js +88 -0
  39. package/dist/commands/validate.js +19 -0
  40. package/dist/config/mapper.js +152 -0
  41. package/dist/config/schema.js +2 -0
  42. package/dist/config/validation.js +705 -0
  43. package/dist/config/watcher.js +145 -0
  44. package/dist/config/yaml.js +197 -0
  45. package/dist/fleet.js +247 -0
  46. package/dist/flow/executor.js +286 -0
  47. package/dist/flow/flow.js +2 -0
  48. package/dist/flow/mapper.js +72 -0
  49. package/dist/flow/state.js +115 -0
  50. package/dist/flow/types.js +2 -0
  51. package/dist/graph/executor.js +294 -0
  52. package/dist/graph/graph.js +2 -0
  53. package/dist/graph/state.js +118 -0
  54. package/dist/helpers/data.js +42 -0
  55. package/dist/helpers/fuzzy.js +30 -0
  56. package/dist/helpers/logger.js +89 -0
  57. package/dist/helpers/validation.js +38 -0
  58. package/dist/index.js +92 -0
  59. package/dist/messages/contracts.js +2 -0
  60. package/dist/messages/inbox.js +58 -0
  61. package/dist/messages/outbox.js +136 -0
  62. package/dist/services/auto-response.js +62 -0
  63. package/dist/services/cooldown.js +60 -0
  64. package/dist/services/fleet.js +207 -0
  65. package/dist/services/flow-executor.js +195 -0
  66. package/dist/services/flow-state.js +118 -0
  67. package/dist/services/inbox.js +50 -0
  68. package/dist/services/message-handler.js +78 -0
  69. package/dist/services/message-queue.js +138 -0
  70. package/dist/services/outbox.js +138 -0
  71. package/dist/services/session.js +118 -0
  72. package/dist/services/webhook.js +87 -0
  73. package/dist/utils/logger.js +89 -0
  74. package/dist/utils/webhook.js +36 -0
  75. package/dist/validation/validate.js +592 -0
  76. package/dist/whatsapp/client.js +293 -0
  77. package/dist/whatsapp/session.js +158 -0
  78. package/dist/whatsapp/types.js +109 -0
  79. package/dist/whatsapp/whatsapp.js +109 -0
  80. package/package.json +97 -0
  81. package/scripts/postinstall.js +45 -0
  82. package/scripts/preuninstall.js +48 -0
  83. package/scripts/setup-systemd.js +91 -0
  84. package/service/botforje.service.template +25 -0
@@ -0,0 +1,592 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.validateConfig = validateConfig;
37
+ const fs = __importStar(require("fs/promises"));
38
+ const fsSync = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
40
+ const yaml = __importStar(require("js-yaml"));
41
+ const yaml_1 = require("../config/yaml");
42
+ const validation_1 = require("../helpers/validation");
43
+ class FileContext {
44
+ constructor(filepath, content) {
45
+ this.errors = [];
46
+ this.filepath = filepath;
47
+ this.content = content;
48
+ this.lines = content.split('\n');
49
+ }
50
+ add(message, field) {
51
+ const line = field ? this.findLine(field) : null;
52
+ this.errors.push({ file: this.filepath, line, message });
53
+ }
54
+ get validationErrors() {
55
+ return this.errors;
56
+ }
57
+ findLine(field) {
58
+ for (let i = 0; i < this.lines.length; i++) {
59
+ if (new RegExp(`^\\s*${escapeRegex(field)}:`).test(this.lines[i])) {
60
+ return i + 1;
61
+ }
62
+ }
63
+ return null;
64
+ }
65
+ }
66
+ function escapeRegex(s) {
67
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
68
+ }
69
+ function isValidLogLevel(value) {
70
+ return typeof value === 'string' && ['info', 'debug', 'warn', 'error'].includes(value);
71
+ }
72
+ function isValidHttpMethod(value) {
73
+ return typeof value === 'string' && ['GET', 'POST', 'PUT', 'PATCH'].includes(value.toUpperCase());
74
+ }
75
+ async function readAndParseYaml(filepath, ctx) {
76
+ try {
77
+ const raw = await fs.readFile(filepath, 'utf-8');
78
+ ctx.content = raw;
79
+ ctx.lines = raw.split('\n');
80
+ const parsed = yaml.load(raw);
81
+ if (parsed === null || parsed === undefined) {
82
+ ctx.add('File is empty');
83
+ return null;
84
+ }
85
+ if (typeof parsed !== 'object' || Array.isArray(parsed)) {
86
+ ctx.add('File must contain a YAML object, not an array or scalar', undefined);
87
+ return null;
88
+ }
89
+ return parsed;
90
+ }
91
+ catch (e) {
92
+ const msg = e instanceof Error ? e.message : String(e);
93
+ ctx.add(`YAML parse error: ${msg}`);
94
+ return null;
95
+ }
96
+ }
97
+ function validateConfigFile(config, ctx) {
98
+ if (config.chromiumPath !== undefined && typeof config.chromiumPath !== 'string') {
99
+ ctx.add('chromiumPath must be a string', 'chromiumPath');
100
+ }
101
+ if (config.apiPort !== undefined) {
102
+ if (typeof config.apiPort !== 'number' || !Number.isInteger(config.apiPort) || config.apiPort < 0) {
103
+ ctx.add('apiPort must be a non-negative integer', 'apiPort');
104
+ }
105
+ }
106
+ if (config.apiEnabled !== undefined && typeof config.apiEnabled !== 'boolean') {
107
+ ctx.add('apiEnabled must be a boolean', 'apiEnabled');
108
+ }
109
+ if (config.logLevel !== undefined && !isValidLogLevel(config.logLevel)) {
110
+ ctx.add('logLevel must be one of: info, debug, warn, error', 'logLevel');
111
+ }
112
+ if (config.sessionTimeout !== undefined) {
113
+ if (typeof config.sessionTimeout !== 'number' || !Number.isInteger(config.sessionTimeout) || config.sessionTimeout < 0) {
114
+ ctx.add('sessionTimeout must be a non-negative integer', 'sessionTimeout');
115
+ }
116
+ }
117
+ }
118
+ function validateActionFile(id, data, ctx) {
119
+ if (typeof data !== 'object' || data === null || Array.isArray(data)) {
120
+ ctx.add('Action must be a YAML object');
121
+ return null;
122
+ }
123
+ const a = data;
124
+ const hasReply = a.reply !== undefined;
125
+ const hasWebhook = a.webhook !== undefined;
126
+ const hasLocation = a.location !== undefined;
127
+ if (!hasReply && !hasWebhook && !hasLocation) {
128
+ ctx.add('Action must define reply, webhook, location, or a combination');
129
+ return null;
130
+ }
131
+ if (hasReply && typeof a.reply !== 'string') {
132
+ ctx.add('action.reply must be a string', 'reply');
133
+ }
134
+ if (hasLocation) {
135
+ if (typeof a.location !== 'object' || a.location === null || Array.isArray(a.location)) {
136
+ ctx.add('action.location must be an object', 'location');
137
+ }
138
+ else {
139
+ const loc = a.location;
140
+ if (typeof loc.latitude !== 'number' || !Number.isFinite(loc.latitude)) {
141
+ ctx.add('action.location.latitude must be a number', 'latitude');
142
+ }
143
+ else if (loc.latitude < -90 || loc.latitude > 90) {
144
+ ctx.add('action.location.latitude must be between -90 and 90', 'latitude');
145
+ }
146
+ if (typeof loc.longitude !== 'number' || !Number.isFinite(loc.longitude)) {
147
+ ctx.add('action.location.longitude must be a number', 'longitude');
148
+ }
149
+ else if (loc.longitude < -180 || loc.longitude > 180) {
150
+ ctx.add('action.location.longitude must be between -180 and 180', 'longitude');
151
+ }
152
+ if (loc.name !== undefined && typeof loc.name !== 'string') {
153
+ ctx.add('action.location.name must be a string', 'name');
154
+ }
155
+ if (loc.address !== undefined && typeof loc.address !== 'string') {
156
+ ctx.add('action.location.address must be a string', 'address');
157
+ }
158
+ if (loc.url !== undefined && typeof loc.url !== 'string') {
159
+ ctx.add('action.location.url must be a string', 'url');
160
+ }
161
+ if (loc.description !== undefined && typeof loc.description !== 'string') {
162
+ ctx.add('action.location.description must be a string', 'description');
163
+ }
164
+ }
165
+ }
166
+ if (hasWebhook) {
167
+ if (typeof a.webhook !== 'object' || a.webhook === null || Array.isArray(a.webhook)) {
168
+ ctx.add('action.webhook must be an object', 'webhook');
169
+ }
170
+ else {
171
+ const wh = a.webhook;
172
+ if (typeof wh.url !== 'string') {
173
+ ctx.add('action.webhook.url must be a string', 'url');
174
+ }
175
+ if (wh.method !== undefined && !isValidHttpMethod(wh.method)) {
176
+ ctx.add('action.webhook.method must be GET, POST, PUT, or PATCH', 'method');
177
+ }
178
+ if (wh.timeout !== undefined && (typeof wh.timeout !== 'number' || wh.timeout < 0)) {
179
+ ctx.add('action.webhook.timeout must be a non-negative number', 'timeout');
180
+ }
181
+ if (wh.retry !== undefined && (typeof wh.retry !== 'number' || wh.retry < 0 || !Number.isInteger(wh.retry))) {
182
+ ctx.add('action.webhook.retry must be a non-negative integer', 'retry');
183
+ }
184
+ if (wh.headers !== undefined && (typeof wh.headers !== 'object' || wh.headers === null || Array.isArray(wh.headers))) {
185
+ ctx.add('action.webhook.headers must be an object', 'headers');
186
+ }
187
+ }
188
+ }
189
+ if (a.cooldown !== undefined) {
190
+ if (typeof a.cooldown !== 'number' || a.cooldown < 0) {
191
+ ctx.add('action.cooldown must be a non-negative number', 'cooldown');
192
+ }
193
+ }
194
+ if (a.cooldown_reply !== undefined && typeof a.cooldown_reply !== 'string') {
195
+ ctx.add('action.cooldown_reply must be a string', 'cooldown_reply');
196
+ }
197
+ return hasReply || hasWebhook || hasLocation ? data : null;
198
+ }
199
+ function validateGraphFile(id, data, ctx, allNodes) {
200
+ if (typeof data !== 'object' || data === null || Array.isArray(data)) {
201
+ ctx.add('Graph must be a YAML object');
202
+ return null;
203
+ }
204
+ const g = data;
205
+ if (typeof g.root !== 'string' || !g.root) {
206
+ ctx.add('graph.root is required and must be a non-empty string', 'root');
207
+ return null;
208
+ }
209
+ if (!g.nodes || typeof g.nodes !== 'object' || Array.isArray(g.nodes)) {
210
+ ctx.add('graph.nodes is required and must be an object', 'nodes');
211
+ return null;
212
+ }
213
+ const nodes = g.nodes;
214
+ const nodeKeys = Object.keys(nodes);
215
+ if (nodeKeys.length === 0) {
216
+ ctx.add('graph.nodes must have at least one node', 'nodes');
217
+ return null;
218
+ }
219
+ nodeKeys.forEach(nk => allNodes.add(nk));
220
+ if (!nodes[g.root]) {
221
+ ctx.add(`graph.root "${g.root}" not found in nodes`, 'root');
222
+ }
223
+ if (g.fallback_node !== undefined) {
224
+ if (typeof g.fallback_node !== 'string') {
225
+ ctx.add('graph.fallback_node must be a string', 'fallback_node');
226
+ }
227
+ else if (!nodes[g.fallback_node]) {
228
+ ctx.add(`graph.fallback_node "${g.fallback_node}" not found in nodes`, 'fallback_node');
229
+ }
230
+ }
231
+ if (g.timeout !== undefined) {
232
+ if (typeof g.timeout !== 'number' || g.timeout < 0) {
233
+ ctx.add('graph.timeout must be a non-negative number', 'timeout');
234
+ }
235
+ }
236
+ if (g.triggers !== undefined) {
237
+ ctx.add('graphs no longer support triggers; remove the triggers field; the bot auto-enters the graph root on first message', 'triggers');
238
+ }
239
+ for (const [nodeId, nodeData] of Object.entries(nodes)) {
240
+ if (typeof nodeData !== 'object' || nodeData === null) {
241
+ ctx.add(`graph.nodes.${nodeId} must be an object`, nodeId);
242
+ continue;
243
+ }
244
+ const node = nodeData;
245
+ if (typeof node.action !== 'string' || !node.action) {
246
+ ctx.add(`graph.nodes.${nodeId}.action is required and must be a non-empty string`, nodeId);
247
+ }
248
+ if (node.edges !== undefined) {
249
+ if (!Array.isArray(node.edges)) {
250
+ ctx.add(`graph.nodes.${nodeId}.edges must be an array`, nodeId);
251
+ }
252
+ else {
253
+ for (let ei = 0; ei < node.edges.length; ei++) {
254
+ const edge = node.edges[ei];
255
+ if (typeof edge !== 'object' || edge === null) {
256
+ ctx.add(`graph.nodes.${nodeId}.edges[${ei}] must be an object`, nodeId);
257
+ continue;
258
+ }
259
+ const e = edge;
260
+ if (e.match !== undefined) {
261
+ if (typeof e.match !== 'string' && !Array.isArray(e.match)) {
262
+ ctx.add(`graph.nodes.${nodeId}.edges[${ei}].match must be a string or array`, nodeId);
263
+ }
264
+ }
265
+ if (e.fuzzy_threshold !== undefined && (typeof e.fuzzy_threshold !== 'number' || e.fuzzy_threshold < 0 || e.fuzzy_threshold > 1)) {
266
+ ctx.add(`graph.nodes.${nodeId}.edges[${ei}].fuzzy_threshold must be between 0 and 1`, nodeId);
267
+ }
268
+ if (typeof e.goto !== 'string' || !e.goto) {
269
+ ctx.add(`graph.nodes.${nodeId}.edges[${ei}].goto is required and must be a non-empty string`, nodeId);
270
+ }
271
+ }
272
+ }
273
+ }
274
+ }
275
+ return data;
276
+ }
277
+ function findUnreachableNodes(graph) {
278
+ const reachable = new Set();
279
+ const queue = [graph.root];
280
+ reachable.add(graph.root);
281
+ while (queue.length > 0) {
282
+ const current = queue.shift();
283
+ const node = graph.nodes[current];
284
+ if (!node || !node.edges)
285
+ continue;
286
+ for (const edge of node.edges) {
287
+ if (edge.goto && !reachable.has(edge.goto) && graph.nodes[edge.goto]) {
288
+ reachable.add(edge.goto);
289
+ queue.push(edge.goto);
290
+ }
291
+ }
292
+ }
293
+ return Object.keys(graph.nodes).filter(n => !reachable.has(n));
294
+ }
295
+ function validateBotFile(id, data, ctx) {
296
+ if (typeof data !== 'object' || data === null || Array.isArray(data)) {
297
+ ctx.add('Bot must be a YAML object');
298
+ return null;
299
+ }
300
+ const b = data;
301
+ if (b.graph !== undefined) {
302
+ if (typeof b.graph !== 'string' || !b.graph) {
303
+ ctx.add('bot.graph must be a non-empty string', 'graph');
304
+ }
305
+ }
306
+ if (b.settings !== undefined) {
307
+ if (typeof b.settings !== 'object' || b.settings === null || Array.isArray(b.settings)) {
308
+ ctx.add('bot.settings must be an object', 'settings');
309
+ }
310
+ else {
311
+ const s = b.settings;
312
+ if (s.simulate_typing !== undefined && typeof s.simulate_typing !== 'boolean') {
313
+ ctx.add('bot.settings.simulate_typing must be a boolean', 'simulate_typing');
314
+ }
315
+ if (s.typing_delay !== undefined && (typeof s.typing_delay !== 'number' || s.typing_delay < 0)) {
316
+ ctx.add('bot.settings.typing_delay must be a non-negative number', 'typing_delay');
317
+ }
318
+ if (s.queue_delay !== undefined && (typeof s.queue_delay !== 'number' || s.queue_delay < 0)) {
319
+ ctx.add('bot.settings.queue_delay must be a non-negative number', 'queue_delay');
320
+ }
321
+ if (s.read_receipts !== undefined && typeof s.read_receipts !== 'boolean') {
322
+ ctx.add('bot.settings.read_receipts must be a boolean', 'read_receipts');
323
+ }
324
+ if (s.ignore_groups !== undefined && typeof s.ignore_groups !== 'boolean') {
325
+ ctx.add('bot.settings.ignore_groups must be a boolean', 'ignore_groups');
326
+ }
327
+ if (s.ignored_senders !== undefined) {
328
+ if (!Array.isArray(s.ignored_senders)) {
329
+ ctx.add('bot.settings.ignored_senders must be an array', 'ignored_senders');
330
+ }
331
+ else {
332
+ for (let si = 0; si < s.ignored_senders.length; si++) {
333
+ if (typeof s.ignored_senders[si] !== 'string') {
334
+ ctx.add(`bot.settings.ignored_senders[${si}] must be a string`, 'ignored_senders');
335
+ }
336
+ }
337
+ }
338
+ }
339
+ if (s.admin_numbers !== undefined) {
340
+ if (!Array.isArray(s.admin_numbers)) {
341
+ ctx.add('bot.settings.admin_numbers must be an array', 'admin_numbers');
342
+ }
343
+ }
344
+ }
345
+ }
346
+ return data;
347
+ }
348
+ function validateIdWithContext(id, kind, ctx) {
349
+ try {
350
+ (0, validation_1.validateId)(id, kind);
351
+ }
352
+ catch (e) {
353
+ ctx.add(e.message);
354
+ }
355
+ }
356
+ async function validateConfig(configPath) {
357
+ const targetPath = configPath || (0, yaml_1.getDefaultConfigPath)();
358
+ const configDir = path.dirname(targetPath);
359
+ const errors = [];
360
+ const actionsDir = path.join(configDir, 'actions');
361
+ const graphsDir = path.join(configDir, 'graphs');
362
+ const botsDir = path.join(configDir, 'bots');
363
+ const allInlineActions = new Map();
364
+ const allDirActions = new Map();
365
+ const allInlineGraphs = new Map();
366
+ const allDirGraphs = new Map();
367
+ const allInlineBots = new Map();
368
+ const allDirBots = new Map();
369
+ if (!fsSync.existsSync(targetPath)) {
370
+ errors.push({ file: targetPath, line: null, message: `Config file not found at ${targetPath}` });
371
+ return { errors, valid: false };
372
+ }
373
+ const configRaw = await fs.readFile(targetPath, 'utf-8');
374
+ const configCtx = new FileContext(targetPath, configRaw);
375
+ let configParsed = null;
376
+ try {
377
+ const parsed = yaml.load(configRaw);
378
+ if (parsed === null || parsed === undefined) {
379
+ configCtx.add('Config file is empty');
380
+ }
381
+ else if (typeof parsed !== 'object' || Array.isArray(parsed)) {
382
+ configCtx.add('Config must contain a YAML object at the root');
383
+ }
384
+ else {
385
+ configParsed = parsed;
386
+ }
387
+ }
388
+ catch (e) {
389
+ const msg = e instanceof Error ? e.message : String(e);
390
+ configCtx.add(`YAML parse error: ${msg}`);
391
+ }
392
+ if (configParsed && typeof configParsed === 'object' && !Array.isArray(configParsed)) {
393
+ const configObj = configParsed;
394
+ validateConfigFile(configObj, configCtx);
395
+ if (configObj.bots !== undefined) {
396
+ if (typeof configObj.bots !== 'object' || configObj.bots === null || Array.isArray(configObj.bots)) {
397
+ configCtx.add('bots must be a YAML object (map of bot IDs to bot configs)', 'bots');
398
+ }
399
+ else {
400
+ for (const [botId, botData] of Object.entries(configObj.bots)) {
401
+ validateIdWithContext(botId, 'Bot', configCtx);
402
+ const botCtx = new FileContext(targetPath, configRaw);
403
+ const botConfig = validateBotFile(botId, botData, botCtx);
404
+ if (botConfig)
405
+ allInlineBots.set(botId, botConfig);
406
+ botCtx.validationErrors.forEach(e => {
407
+ e.message = `bots.${botId}: ${e.message}`;
408
+ errors.push(e);
409
+ });
410
+ }
411
+ }
412
+ }
413
+ if (configObj.actions !== undefined) {
414
+ if (typeof configObj.actions !== 'object' || configObj.actions === null || Array.isArray(configObj.actions)) {
415
+ configCtx.add('actions must be a YAML object (map of action IDs to action configs)', 'actions');
416
+ }
417
+ else {
418
+ for (const [actionId, actionData] of Object.entries(configObj.actions)) {
419
+ validateIdWithContext(actionId, 'Action', configCtx);
420
+ const actionCtx = new FileContext(targetPath, configRaw);
421
+ const actionConfig = validateActionFile(actionId, actionData, actionCtx);
422
+ if (actionConfig)
423
+ allInlineActions.set(actionId, actionConfig);
424
+ actionCtx.validationErrors.forEach(e => {
425
+ e.message = `actions.${actionId}: ${e.message}`;
426
+ errors.push(e);
427
+ });
428
+ }
429
+ }
430
+ }
431
+ if (configObj.graphs !== undefined) {
432
+ if (typeof configObj.graphs !== 'object' || configObj.graphs === null || Array.isArray(configObj.graphs)) {
433
+ configCtx.add('graphs must be a YAML object (map of graph IDs to graph configs)', 'graphs');
434
+ }
435
+ else {
436
+ for (const [graphId, graphData] of Object.entries(configObj.graphs)) {
437
+ validateIdWithContext(graphId, 'Graph', configCtx);
438
+ const allNodes = new Set();
439
+ const graphCtx = new FileContext(targetPath, configRaw);
440
+ const graphConfig = validateGraphFile(graphId, graphData, graphCtx, allNodes);
441
+ if (graphConfig) {
442
+ allInlineGraphs.set(graphId, graphConfig);
443
+ }
444
+ graphCtx.validationErrors.forEach(e => {
445
+ e.message = `graphs.${graphId}: ${e.message}`;
446
+ errors.push(e);
447
+ });
448
+ if (graphConfig && allNodes.size > 0) {
449
+ const edgeErrors = validateGraphEdgeTargets(graphId, graphConfig, allNodes, targetPath, configRaw);
450
+ edgeErrors.forEach(e => errors.push(e));
451
+ const unreachable = findUnreachableNodes(graphConfig);
452
+ for (const nodeName of unreachable) {
453
+ errors.push({
454
+ file: targetPath,
455
+ line: null,
456
+ message: `graphs.${graphId}: warning: node "${nodeName}" is unreachable from root "${graphConfig.root}"`,
457
+ });
458
+ }
459
+ }
460
+ }
461
+ }
462
+ }
463
+ }
464
+ errors.push(...configCtx.validationErrors);
465
+ if (fsSync.existsSync(actionsDir)) {
466
+ const actionFiles = await loadDirFiles(actionsDir);
467
+ for (const file of actionFiles) {
468
+ const actionId = path.basename(file, path.extname(file));
469
+ validateIdWithContext(actionId, 'Action', new FileContext(actionsDir, ''));
470
+ const content = await fs.readFile(file, 'utf-8');
471
+ const ctx = new FileContext(file, content);
472
+ const parsed = await readAndParseYaml(file, ctx);
473
+ if (parsed) {
474
+ const actionConfig = validateActionFile(actionId, parsed, ctx);
475
+ if (actionConfig)
476
+ allDirActions.set(actionId, actionConfig);
477
+ }
478
+ errors.push(...ctx.validationErrors);
479
+ }
480
+ }
481
+ if (fsSync.existsSync(graphsDir)) {
482
+ const graphFiles = await loadDirFiles(graphsDir);
483
+ for (const file of graphFiles) {
484
+ const graphId = path.basename(file, path.extname(file));
485
+ validateIdWithContext(graphId, 'Graph', new FileContext(graphsDir, ''));
486
+ const content = await fs.readFile(file, 'utf-8');
487
+ const ctx = new FileContext(file, content);
488
+ const allNodes = new Set();
489
+ const parsed = await readAndParseYaml(file, ctx);
490
+ if (parsed) {
491
+ const graphConfig = validateGraphFile(graphId, parsed, ctx, allNodes);
492
+ if (graphConfig) {
493
+ allDirGraphs.set(graphId, graphConfig);
494
+ }
495
+ if (graphConfig && allNodes.size > 0) {
496
+ const edgeErrors = validateGraphEdgeTargets(graphId, graphConfig, allNodes, file, content);
497
+ edgeErrors.forEach(e => errors.push(e));
498
+ const unreachable = findUnreachableNodes(graphConfig);
499
+ for (const nodeName of unreachable) {
500
+ errors.push({
501
+ file: file,
502
+ line: null,
503
+ message: `graphs.${graphId}: warning: node "${nodeName}" is unreachable from root "${graphConfig.root}"`,
504
+ });
505
+ }
506
+ }
507
+ }
508
+ errors.push(...ctx.validationErrors);
509
+ }
510
+ }
511
+ if (fsSync.existsSync(botsDir)) {
512
+ const botFiles = await loadDirFiles(botsDir);
513
+ for (const file of botFiles) {
514
+ const botId = path.basename(file, path.extname(file));
515
+ validateIdWithContext(botId, 'Bot', new FileContext(botsDir, ''));
516
+ const content = await fs.readFile(file, 'utf-8');
517
+ const ctx = new FileContext(file, content);
518
+ const parsed = await readAndParseYaml(file, ctx);
519
+ if (parsed) {
520
+ const botConfig = validateBotFile(botId, parsed, ctx);
521
+ if (botConfig)
522
+ allDirBots.set(botId, botConfig);
523
+ }
524
+ errors.push(...ctx.validationErrors);
525
+ }
526
+ }
527
+ const mergedActions = new Map([...allDirActions, ...allInlineActions]);
528
+ const mergedGraphs = new Map([...allDirGraphs, ...allInlineGraphs]);
529
+ const mergedBots = new Map([...allDirBots, ...allInlineBots]);
530
+ if (mergedBots.size === 0) {
531
+ errors.push({ file: targetPath, line: null, message: 'No bots defined. At least one bot is required.' });
532
+ }
533
+ for (const [graphId, graph] of mergedGraphs) {
534
+ for (const [nodeId, node] of Object.entries(graph.nodes)) {
535
+ if (node.action && !mergedActions.has(node.action)) {
536
+ errors.push({
537
+ file: targetPath,
538
+ line: null,
539
+ message: `Graph "${graphId}" node "${nodeId}" references action "${node.action}" which is not defined`,
540
+ });
541
+ }
542
+ }
543
+ }
544
+ for (const [botId, bot] of mergedBots) {
545
+ if (bot.graph && !mergedGraphs.has(bot.graph)) {
546
+ errors.push({
547
+ file: targetPath,
548
+ line: null,
549
+ message: `Bot "${botId}" references graph "${bot.graph}" which is not defined`,
550
+ });
551
+ }
552
+ }
553
+ if (mergedBots.size === 0 && fsSync.existsSync(botsDir)) {
554
+ const botFiles = await fs.readdir(botsDir);
555
+ const yamlBots = botFiles.filter(f => f.endsWith('.yml') || f.endsWith('.yaml'));
556
+ if (yamlBots.length > 0) {
557
+ errors.push({
558
+ file: targetPath,
559
+ line: null,
560
+ message: 'Bot files found in bots/ but none were valid',
561
+ });
562
+ }
563
+ }
564
+ return {
565
+ errors,
566
+ valid: errors.length === 0
567
+ };
568
+ }
569
+ function validateGraphEdgeTargets(graphId, graph, allNodes, sourceFile, sourceContent) {
570
+ const edgeErrors = [];
571
+ for (const [nodeId, node] of Object.entries(graph.nodes)) {
572
+ if (node.edges) {
573
+ for (let ei = 0; ei < node.edges.length; ei++) {
574
+ const edge = node.edges[ei];
575
+ if (edge.goto && !allNodes.has(edge.goto)) {
576
+ edgeErrors.push({
577
+ file: sourceFile,
578
+ line: null,
579
+ message: `Graph "${graphId}" node "${nodeId}" edges[${ei}] goto "${edge.goto}" not found in nodes`,
580
+ });
581
+ }
582
+ }
583
+ }
584
+ }
585
+ return edgeErrors;
586
+ }
587
+ async function loadDirFiles(dir) {
588
+ const entries = await fs.readdir(dir);
589
+ return entries
590
+ .filter(f => f.endsWith('.yml') || f.endsWith('.yaml'))
591
+ .map(f => path.join(dir, f));
592
+ }