mojulo 0.0.0 → 0.1.0

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 (121) hide show
  1. package/README.md +53 -4
  2. package/lib/audit-logger-new.js +11 -0
  3. package/lib/auth/gate.js +25 -0
  4. package/lib/auth/service.js +17 -0
  5. package/lib/auth/session.js +63 -0
  6. package/lib/builder/chat-processor.js +607 -0
  7. package/lib/builder/composer-bridge.js +82 -0
  8. package/lib/builder/evaluator.js +159 -0
  9. package/lib/builder/executor.js +252 -0
  10. package/lib/builder/index.js +48 -0
  11. package/lib/builder/session.js +248 -0
  12. package/lib/builder/system-prompt.js +422 -0
  13. package/lib/builder/tone-presets.js +75 -0
  14. package/lib/builder/tool-executors.js +1418 -0
  15. package/lib/builder/tools.js +338 -0
  16. package/lib/builder/validators.js +239 -0
  17. package/lib/composer/composer.js +225 -0
  18. package/lib/composer/index.js +40 -0
  19. package/lib/composer/protocols/00_base.txt +19 -0
  20. package/lib/composer/protocols/01_knowledge.txt +9 -0
  21. package/lib/composer/protocols/02_form-gathering.txt +32 -0
  22. package/lib/composer/protocols/03_appointments.txt +16 -0
  23. package/lib/composer/protocols/04_triage.txt +15 -0
  24. package/lib/composer/protocols/05_optical-read.txt +22 -0
  25. package/lib/composer/response-builder.js +98 -0
  26. package/lib/config-builder.js +650 -0
  27. package/lib/db/ids.js +10 -0
  28. package/lib/db/index.js +179 -0
  29. package/lib/db/repositories/apiKeys.js +72 -0
  30. package/lib/db/repositories/auditLogs.js +12 -0
  31. package/lib/db/repositories/botSpaces.js +12 -0
  32. package/lib/db/repositories/builderSessions.js +312 -0
  33. package/lib/db/repositories/deploymentEvents.js +12 -0
  34. package/lib/db/repositories/deployments.js +385 -0
  35. package/lib/db/repositories/documents.js +68 -0
  36. package/lib/db/repositories/mcpJobs.js +84 -0
  37. package/lib/deployers/bot-fleet.js +110 -0
  38. package/lib/deployers/bot-proxy.js +72 -0
  39. package/lib/deployers/build.js +89 -0
  40. package/lib/deployers/cloud-deploy.js +310 -0
  41. package/lib/deployers/docker.js +439 -0
  42. package/lib/deployers/fly.js +432 -0
  43. package/lib/deployers/index.js +38 -0
  44. package/lib/deployment-auth.js +36 -0
  45. package/lib/document-parser.js +171 -0
  46. package/lib/embedder/chunker.js +93 -0
  47. package/lib/embedder/local.js +101 -0
  48. package/lib/embedder/preview-rag.js +93 -0
  49. package/lib/envelope-schema.js +54 -0
  50. package/lib/fleet/scoped-sql.js +342 -0
  51. package/lib/form-schema-config/base.js +135 -0
  52. package/lib/form-schema-config/index.js +286 -0
  53. package/lib/form-schema-config/locales/af-ZA.js +153 -0
  54. package/lib/form-schema-config/locales/ar-AE.js +142 -0
  55. package/lib/form-schema-config/locales/ar-SA.js +164 -0
  56. package/lib/form-schema-config/locales/de-DE.js +152 -0
  57. package/lib/form-schema-config/locales/en-AU.js +161 -0
  58. package/lib/form-schema-config/locales/en-CA.js +115 -0
  59. package/lib/form-schema-config/locales/en-GB.js +132 -0
  60. package/lib/form-schema-config/locales/en-IN.js +219 -0
  61. package/lib/form-schema-config/locales/en-MY.js +171 -0
  62. package/lib/form-schema-config/locales/en-NG.js +198 -0
  63. package/lib/form-schema-config/locales/en-PH.js +186 -0
  64. package/lib/form-schema-config/locales/en-SG.js +153 -0
  65. package/lib/form-schema-config/locales/en-US.js +138 -0
  66. package/lib/form-schema-config/locales/es-ES.js +171 -0
  67. package/lib/form-schema-config/locales/es-MX.js +193 -0
  68. package/lib/form-schema-config/locales/fr-CA.js +138 -0
  69. package/lib/form-schema-config/locales/fr-FR.js +155 -0
  70. package/lib/form-schema-config/locales/hi-IN.js +219 -0
  71. package/lib/form-schema-config/locales/it-IT.js +157 -0
  72. package/lib/form-schema-config/locales/ja-JP.js +169 -0
  73. package/lib/form-schema-config/locales/ko-KR.js +140 -0
  74. package/lib/form-schema-config/locales/nl-NL.js +149 -0
  75. package/lib/form-schema-config/locales/pt-BR.js +168 -0
  76. package/lib/form-schema-config/locales/zh-CN.js +172 -0
  77. package/lib/form-schema-config/locales/zh-HK.js +142 -0
  78. package/lib/form-structure-schema.js +191 -0
  79. package/lib/llm-providers.js +828 -0
  80. package/lib/markdown.js +197 -0
  81. package/lib/mcp/catalysts/appointment-to-calendar.md +84 -0
  82. package/lib/mcp/catalysts/conversations-to-channel-digest.md +104 -0
  83. package/lib/mcp/catalysts/document-extract-to-store.md +92 -0
  84. package/lib/mcp/catalysts/knowledge-gap-miner.md +96 -0
  85. package/lib/mcp/catalysts/loader.js +144 -0
  86. package/lib/mcp/catalysts/qualify-lead-to-crm.md +83 -0
  87. package/lib/mcp/catalysts/scan-conversations-for-signal.md +92 -0
  88. package/lib/mcp/catalysts/submission-to-ticket.md +83 -0
  89. package/lib/mcp/catalysts/submissions-to-warehouse.md +103 -0
  90. package/lib/mcp/catalysts/weekly-submissions-digest.md +82 -0
  91. package/lib/mcp/jobs.js +64 -0
  92. package/lib/mcp/server.js +184 -0
  93. package/lib/mcp/session-binding.js +130 -0
  94. package/lib/mcp/tools/build.js +123 -0
  95. package/lib/mcp/tools/catalysts.js +477 -0
  96. package/lib/mcp/tools/context.js +325 -0
  97. package/lib/mcp/tools/fleet.js +391 -0
  98. package/lib/mcp/tools/jobs-tools.js +240 -0
  99. package/lib/mcp/tools/operate.js +314 -0
  100. package/lib/preview/build-preview-config.js +136 -0
  101. package/lib/rate-limiter.js +11 -0
  102. package/lib/resolve-api-key.js +142 -0
  103. package/lib/storage/index.js +40 -0
  104. package/messages/de.json +2136 -0
  105. package/messages/en.json +2136 -0
  106. package/messages/es.json +2136 -0
  107. package/messages/fr.json +2136 -0
  108. package/messages/it.json +2136 -0
  109. package/messages/ja.json +2136 -0
  110. package/messages/ko.json +2136 -0
  111. package/messages/nl.json +2136 -0
  112. package/messages/pl.json +2136 -0
  113. package/messages/pt.json +2136 -0
  114. package/messages/ru.json +2136 -0
  115. package/messages/uk.json +2136 -0
  116. package/messages/zh.json +2136 -0
  117. package/package.json +61 -5
  118. package/scripts/mcp-config.mjs +162 -0
  119. package/scripts/mcp-stdio-loader.mjs +42 -0
  120. package/scripts/mcp-stdio.mjs +108 -0
  121. package/scripts/mojulo-paths.mjs +48 -0
@@ -0,0 +1,650 @@
1
+ /**
2
+ * Config Builder Utility
3
+ * Transforms simple form data into the config.json structure expected by dragbot-factory
4
+ */
5
+
6
+ import {
7
+ LLM_PROVIDERS,
8
+ buildBedrockModelId,
9
+ stripBedrockModelPrefix,
10
+ getAllowedProtocolsForModel,
11
+ } from './llm-providers.js';
12
+
13
+ /**
14
+ * Extract terms and conditions text from formStructure's consentToTC field
15
+ * @param {Object} formStructure - The form structure object with sections
16
+ * @returns {string} The T&C text or empty string
17
+ */
18
+ function extractTermsFromFormStructure(formStructure) {
19
+ if (!formStructure?.sections) return '';
20
+
21
+ for (const section of formStructure.sections) {
22
+ for (const field of section.fields || []) {
23
+ if (field.id === 'consentToTC' && field.termsText) {
24
+ return field.termsText;
25
+ }
26
+ }
27
+ }
28
+ return '';
29
+ }
30
+
31
+ /**
32
+ * Inject consentToTC field into form structure if terms and conditions exist
33
+ * @param {Object} formStructure - The form structure object with sections
34
+ * @param {string} termsAndConditions - The T&C text
35
+ * @returns {Object} Modified form structure with consentToTC field appended
36
+ */
37
+ function injectConsentToTCField(formStructure, termsAndConditions) {
38
+ if (!formStructure || !termsAndConditions?.trim()) {
39
+ return formStructure;
40
+ }
41
+
42
+ // Deep clone to avoid mutating original
43
+ const modified = JSON.parse(JSON.stringify(formStructure));
44
+
45
+ // Create the consentToTC field
46
+ const consentField = {
47
+ id: 'consentToTC',
48
+ label: 'I agree to the Terms and Conditions',
49
+ type: 'checkbox',
50
+ required: true,
51
+ termsText: termsAndConditions.trim()
52
+ };
53
+
54
+ // Append to the last section's fields
55
+ if (modified.sections && modified.sections.length > 0) {
56
+ const lastSection = modified.sections[modified.sections.length - 1];
57
+ lastSection.fields.push(consentField);
58
+ }
59
+
60
+ return modified;
61
+ }
62
+
63
+ /**
64
+ * Build LLM configuration section
65
+ *
66
+ * When apiKey is empty (saved-key-by-reference flow), the selected provider's
67
+ * credential fields are emitted blank — the server fills them in by decrypting
68
+ * the api_keys row before persisting the deployment. See resolve-api-key.js.
69
+ */
70
+ export function buildLLMConfig(provider, apiKey, model, additionalSettings = {}) {
71
+ // Build config with all providers (empty configs for non-selected providers)
72
+ const llmConfig = { provider };
73
+ const usingSavedKey = !apiKey;
74
+
75
+ // Add config for each provider
76
+ Object.keys(LLM_PROVIDERS).forEach(key => {
77
+ const keyConfig = LLM_PROVIDERS[key];
78
+
79
+ if (key === 'ollama') {
80
+ // Ollama runs against a self-hosted endpoint with no credentials. The
81
+ // wizard collects host + model; everything else uses defaults baked
82
+ // into the runtime adapter. additionalSettings.ollamaHost falls back
83
+ // to the provider's defaultHost so a user who never touches the host
84
+ // field still ends up with a working artifact.
85
+ if (key === provider) {
86
+ llmConfig[key] = {
87
+ host: additionalSettings.ollamaHost || keyConfig.defaultHost,
88
+ model,
89
+ format: 'json',
90
+ timeout: 300000,
91
+ };
92
+ } else {
93
+ llmConfig[key] = {
94
+ host: keyConfig.defaultHost,
95
+ model: keyConfig.defaultModel,
96
+ format: 'json',
97
+ timeout: 300000,
98
+ };
99
+ }
100
+ } else if (key === 'bedrock') {
101
+ // Bedrock uses different config structure (credentials stored as JSON in apiKey)
102
+ if (key === provider && !usingSavedKey) {
103
+ // Parse credentials from apiKey JSON
104
+ const credentials = JSON.parse(apiKey);
105
+ // Ensure region is always set (fallback to us-east-1)
106
+ const region = credentials.region || 'us-east-1';
107
+ // Apply geographic prefix to model ID for cross-region inference
108
+ const prefixedModel = buildBedrockModelId(model, region);
109
+ llmConfig[key] = {
110
+ region,
111
+ useIamRole: credentials.useIamRole,
112
+ accessKeyId: credentials.accessKeyId,
113
+ secretAccessKey: credentials.secretAccessKey,
114
+ model: prefixedModel,
115
+ };
116
+ } else if (key === provider && usingSavedKey) {
117
+ // Saved-key path: emit user's model unprefixed; server will decrypt
118
+ // the saved credentials, set the region, and apply the geo prefix.
119
+ llmConfig[key] = {
120
+ region: 'us-east-1',
121
+ useIamRole: false,
122
+ accessKeyId: null,
123
+ secretAccessKey: null,
124
+ model,
125
+ };
126
+ } else {
127
+ // Empty Bedrock config
128
+ llmConfig[key] = {
129
+ region: 'us-east-1',
130
+ useIamRole: true,
131
+ accessKeyId: null,
132
+ secretAccessKey: null,
133
+ model: keyConfig.defaultModel,
134
+ };
135
+ }
136
+ } else {
137
+ // Standard providers with baseURL/endpoint
138
+ if (key === provider) {
139
+ const baseConfig = {
140
+ apiKey,
141
+ model,
142
+ baseURL: keyConfig.baseURL,
143
+ endpoint: keyConfig.endpoint,
144
+ timeout: 300000
145
+ };
146
+
147
+ // Add provider-specific settings
148
+ if (key === 'openai') {
149
+ baseConfig.organization = additionalSettings.organization || '';
150
+ }
151
+ if (key === 'anthropic') {
152
+ baseConfig.maxTokens = additionalSettings.maxTokens || 4096;
153
+ }
154
+
155
+ llmConfig[key] = baseConfig;
156
+ } else {
157
+ // Empty config for non-selected provider
158
+ const emptyConfig = {
159
+ apiKey: '',
160
+ model: keyConfig.defaultModel,
161
+ baseURL: keyConfig.baseURL,
162
+ endpoint: keyConfig.endpoint,
163
+ timeout: 300000
164
+ };
165
+
166
+ // Add provider-specific fields for empty configs
167
+ if (key === 'openai') {
168
+ emptyConfig.organization = '';
169
+ }
170
+ if (key === 'anthropic') {
171
+ emptyConfig.maxTokens = 4096;
172
+ }
173
+
174
+ llmConfig[key] = emptyConfig;
175
+ }
176
+ }
177
+ });
178
+
179
+ return llmConfig;
180
+ }
181
+
182
+ /**
183
+ * Build full deployment config from form data
184
+ *
185
+ * @param {Object} formData - Simple form state
186
+ * @param {string} formData.botName - Bot name
187
+ * @param {string} formData.objective - Bot objective (MANDATORY)
188
+ * @param {string} formData.firstMessage - Welcome message
189
+ * @param {string} formData.provider - LLM provider (openai|anthropic|bedrock)
190
+ * @param {string} formData.apiKey - API key for selected provider
191
+ * @param {string} formData.model - Model name
192
+ * @param {Array} formData.suggestedPrompts - Optional suggested prompts
193
+ * @param {Object} formData.uiSettings - Optional UI customization
194
+ * @param {Array} formData.triageDestinations - Triage destinations (for triage flow)
195
+ * @param {string} flowType - The wizard flow type ('standard', 'conversational', 'triage', 'modular')
196
+ * @param {Object} options - Additional options
197
+ * @param {Object} options.enabledProtocols - For modular flow: { knowledge, formGathering, appointments }
198
+ * @returns {Object} Full deployment config
199
+ */
200
+ export function buildDeploymentConfig(formData, flowType = 'conversational', options = {}) {
201
+ const { enabledProtocols, apiKeyId } = options;
202
+
203
+ const isModularTriage = flowType === 'modular' && enabledProtocols?.triage;
204
+ const isAppointments = flowType === 'modular' && enabledProtocols?.appointments;
205
+ const isOpticalRead = flowType === 'modular' && enabledProtocols?.opticalRead;
206
+ const isSkipRag = !!formData.skipRag;
207
+
208
+ // Ollama is credential-less — no apiKey, no saved-key reference. Validate
209
+ // host + model only; the deployer bakes the host into config.json and the
210
+ // bot's adapter calls it directly. Default host falls back to the provider
211
+ // entry's defaultHost in buildLLMConfig if the field is empty.
212
+ const isOllama = formData.provider === 'ollama';
213
+ const required = (apiKeyId || isOllama)
214
+ ? ['botName', 'objective', 'provider', 'model']
215
+ : ['botName', 'objective', 'provider', 'apiKey', 'model'];
216
+ const missing = required.filter(field => !formData[field]);
217
+
218
+ if (missing.length > 0) {
219
+ throw new Error(`Missing required fields: ${missing.join(', ')}`);
220
+ }
221
+
222
+ // Final-line-of-defense protocol gate. The wizard prunes enabledProtocols
223
+ // when the user changes provider/model, and the chat builder's
224
+ // recommend_protocols clamps its suggestions; this catches any path that
225
+ // bypasses both (direct API caller, replay of a stale session, etc.).
226
+ // Restricted Ollama models (qwen3, mistral-nemo) can only ship the
227
+ // knowledge protocol — refuse to compile a config that says otherwise.
228
+ if (flowType === 'modular' && enabledProtocols) {
229
+ const allowedForModel = getAllowedProtocolsForModel(formData.provider, formData.model);
230
+ if (allowedForModel) {
231
+ const disallowed = Object.entries(enabledProtocols)
232
+ .filter(([id, on]) => on && !allowedForModel.has(id))
233
+ .map(([id]) => id);
234
+ if (disallowed.length > 0) {
235
+ throw new Error(
236
+ `${formData.model} only supports: ${Array.from(allowedForModel).join(', ')}. ` +
237
+ `Disable these protocols or switch to a more capable model: ${disallowed.join(', ')}.`
238
+ );
239
+ }
240
+ }
241
+ }
242
+
243
+ const configSection = {
244
+ instructions: './config/instructions.txt',
245
+ name: formData.botName,
246
+ chatDisplayName: formData.uiSettings?.chatDisplayName || 'Bot',
247
+ placeholder: formData.uiSettings?.placeholder || 'Type your message...',
248
+ firstMessage: formData.firstMessage || `Welcome! I'm ${formData.botName}. How can I help you?`,
249
+ suggestedPrompts: formData.suggestedPrompts || [],
250
+ actionsBar: {
251
+ showBar: false,
252
+ showSourceButton: false,
253
+ showMetadataButton: false,
254
+ showCopyButton: false,
255
+ showSuggestedPrompts: false
256
+ }
257
+ };
258
+
259
+ // Add form-related fields if form structure exists
260
+ if (formData.formStructure?.generatedJson) {
261
+ // Validate required form fields - webhook is optional if formSendHome is enabled
262
+ const hasWebhook = formData.formCompletionWebhook && formData.formCompletionWebhook.trim();
263
+ const hasFormSendHome = formData.formSendHome;
264
+ if (!hasWebhook && !hasFormSendHome) {
265
+ throw new Error('Either formCompletionWebhook or formSendHome is required when form collection is enabled');
266
+ }
267
+ if (!formData.afterSubmitChatMessage || !formData.afterSubmitChatMessage.trim()) {
268
+ throw new Error('afterSubmitChatMessage is required when form collection is enabled');
269
+ }
270
+
271
+ configSection.isForm = true;
272
+ configSection.formStructure = './config/formFormat.json';
273
+ if (hasWebhook) {
274
+ configSection.formCompletionWebhook = formData.formCompletionWebhook;
275
+ }
276
+ configSection.afterSubmitChatMessage = formData.afterSubmitChatMessage;
277
+ // formSendHome flag - URL will be injected by deployer
278
+ if (hasFormSendHome) {
279
+ configSection.formSendHome = true;
280
+ }
281
+ }
282
+
283
+ // Add calendar flag for appointments flow
284
+ if (isAppointments) {
285
+ configSection.isCalendar = true;
286
+ configSection.calendarConfig = './config/calendarConfig.json';
287
+ }
288
+
289
+ // Add triage flag for modular triage flow
290
+ if (isModularTriage) {
291
+ configSection.isTriage = true;
292
+ configSection.triageRoutes = './config/triageRoutes.json';
293
+ }
294
+
295
+ // Add optical read flag for modular optical-read flow. The bot runtime
296
+ // reads opticalReadFields.json at boot like formFormat.json — it gates the
297
+ // /api/extract endpoint and primes the upload-card UX.
298
+ if (isOpticalRead) {
299
+ configSection.isOpticalRead = true;
300
+ configSection.opticalReadFields = './config/opticalReadFields.json';
301
+ if (formData.opticalReadShowUploadOnStart) {
302
+ // Frontend reads this to render an upload card in the suggested-prompts
303
+ // strip on first load; field list ships separately (see opticalReadFields).
304
+ configSection.opticalReadShowUploadOnStart = true;
305
+ }
306
+ if (formData.opticalReadAfterSubmitMessage && formData.opticalReadAfterSubmitMessage.trim()) {
307
+ // Optional follow-up message rendered in the chat after the user
308
+ // submits the extracted-fields panel. Independent from form-gathering's
309
+ // afterSubmitChatMessage so each protocol can have its own copy.
310
+ configSection.opticalReadAfterSubmitMessage = formData.opticalReadAfterSubmitMessage.trim();
311
+ }
312
+ }
313
+
314
+ return {
315
+ // Config section (UI settings)
316
+ config: configSection,
317
+
318
+ // LLM section
319
+ llm: buildLLMConfig(
320
+ formData.provider,
321
+ formData.apiKey,
322
+ formData.model,
323
+ {
324
+ ...(formData.llmSettings || {}),
325
+ ollamaHost: formData.ollamaHost,
326
+ }
327
+ ),
328
+
329
+ // Extra fields for deployer (not part of config.json)
330
+ objective: formData.objective,
331
+ botSummary: formData.botSummary || undefined,
332
+ skipRag: isSkipRag || undefined,
333
+ formStructure: injectConsentToTCField(
334
+ formData.formStructure?.generatedJson,
335
+ formData.termsAndConditions
336
+ ) || undefined,
337
+ formCompletionWebhook: formData.formCompletionWebhook || undefined,
338
+ afterSubmitChatMessage: formData.afterSubmitChatMessage || undefined,
339
+ formSendHome: formData.formSendHome || undefined, // Deployer will inject URL
340
+
341
+ // Triage-specific: store routes for modular flow edit mode
342
+ triageRoutes: isModularTriage ? formData.triageRoutes : undefined,
343
+
344
+ // Appointments-specific: store destinations for edit mode
345
+ appointmentDestinations: isAppointments ? formData.appointmentDestinations : undefined,
346
+
347
+ // Optical Read: store fields for the deployer + edit-mode round trip.
348
+ // The deployer also gets these via the top-level opticalReadFields body
349
+ // field for parity with appointmentDestinations / triageDestinations.
350
+ opticalReadFields: isOpticalRead ? formData.opticalReadFields : undefined
351
+ };
352
+ }
353
+
354
+ /**
355
+ * Reverse buildDeploymentConfig - converts stored config back to form state
356
+ * Used for cloning/editing existing deployments
357
+ *
358
+ * @param {Object} config - Deployment config from database
359
+ * @returns {Object} Form data suitable for ConfigForm
360
+ */
361
+ export function parseDeploymentConfig(config) {
362
+ const provider = config.llm.provider;
363
+ const providerConfig = config.llm[provider];
364
+
365
+ // For Bedrock, apiKey is JSON credentials; for others it's the key itself.
366
+ // Ollama has no credentials — just a host URL that round-trips into the
367
+ // wizard's ollamaHost field.
368
+ let apiKey = '';
369
+ let ollamaHost = '';
370
+ let model = providerConfig.model;
371
+ if (provider === 'bedrock') {
372
+ apiKey = JSON.stringify({
373
+ region: providerConfig.region,
374
+ useIamRole: providerConfig.useIamRole,
375
+ accessKeyId: providerConfig.accessKeyId,
376
+ secretAccessKey: providerConfig.secretAccessKey,
377
+ });
378
+ // Strip geographic prefix from model ID so it matches dropdown options
379
+ model = stripBedrockModelPrefix(model);
380
+ } else if (provider === 'ollama') {
381
+ ollamaHost = providerConfig.host || '';
382
+ } else {
383
+ apiKey = providerConfig.apiKey || '';
384
+ }
385
+
386
+ return {
387
+ // Bot Identity
388
+ botName: config.config.name,
389
+ objective: config.objective || '',
390
+ firstMessage: config.config.firstMessage,
391
+
392
+ // LLM Configuration
393
+ provider,
394
+ model,
395
+ apiKey,
396
+ ollamaHost,
397
+
398
+ // Form Structure (if it exists)
399
+ formStructure: config.formStructure ? {
400
+ naturalLanguageInput: '', // Not stored, will be empty
401
+ generatedJson: config.formStructure
402
+ } : undefined,
403
+
404
+ // Form Collection Settings
405
+ formCompletionWebhook: config.formCompletionWebhook || '',
406
+ afterSubmitChatMessage: config.afterSubmitChatMessage || '',
407
+ formSendHome: config.formSendHome || false,
408
+ termsAndConditions: extractTermsFromFormStructure(config.formStructure),
409
+
410
+ skipRag: config.skipRag || false,
411
+
412
+ // UI Settings
413
+ uiSettings: {
414
+ chatDisplayName: config.config.chatDisplayName || 'Bot',
415
+ placeholder: config.config.placeholder || 'Type your message...',
416
+ },
417
+
418
+ // Suggested Prompts
419
+ suggestedPrompts: config.config.suggestedPrompts || [],
420
+
421
+ // Triage Destinations (for triage flow edit mode)
422
+ triageDestinations: config.triageDestinations || [],
423
+
424
+ // Appointment Destinations (for appointments flow edit mode)
425
+ appointmentDestinations: config.appointmentDestinations || []
426
+ };
427
+ }
428
+
429
+ /**
430
+ * Parse modular deployment config - reconstructs ModularWizardContext state
431
+ * Used for editing modular bots
432
+ *
433
+ * @param {Object} config - Deployment config from database (with _modular metadata)
434
+ * @param {Object} [options]
435
+ * @param {boolean} [options.hasStoredApiKey] - From the GET endpoint; the
436
+ * config has been credential-redacted, so the wizard relies on this flag
437
+ * to know a key is on file and gate the credential requirement.
438
+ * @returns {Object} State suitable for ModularWizardProvider initialData
439
+ */
440
+ export function parseModularDeploymentConfig(config, options = {}) {
441
+ // Extract modular metadata (persisted by /api/deploy)
442
+ const modularMeta = config._modular || {};
443
+ const enabledProtocols = modularMeta.enabledProtocols || {
444
+ // Infer protocols from config if metadata not available (legacy fallback)
445
+ knowledge: !config.skipRag,
446
+ formGathering: !!config.formStructure || !!config.config?.isForm,
447
+ appointments: (config.appointmentDestinations?.length > 0) || !!config.config?.isCalendar,
448
+ triage: (config.triageRoutes?.length > 0) || !!config.config?.isTriage,
449
+ opticalRead: (config.opticalReadFields?.length > 0) || !!config.config?.isOpticalRead,
450
+ };
451
+
452
+ // Compute core fields (handles Bedrock vs standard providers; Ollama
453
+ // carries a host instead of a key)
454
+ const coreProvider = config.llm?.provider || 'anthropic';
455
+ const coreProviderConfig = config.llm?.[coreProvider] || {};
456
+ let coreApiKey = '';
457
+ let coreOllamaHost = '';
458
+ let coreModel = coreProviderConfig.model || '';
459
+ if (coreProvider === 'bedrock') {
460
+ coreApiKey = JSON.stringify({
461
+ region: coreProviderConfig.region,
462
+ useIamRole: coreProviderConfig.useIamRole,
463
+ accessKeyId: coreProviderConfig.accessKeyId,
464
+ secretAccessKey: coreProviderConfig.secretAccessKey,
465
+ });
466
+ // Strip geographic prefix from model ID so it matches dropdown options
467
+ coreModel = stripBedrockModelPrefix(coreModel);
468
+ } else if (coreProvider === 'ollama') {
469
+ coreOllamaHost = coreProviderConfig.host || '';
470
+ } else {
471
+ coreApiKey = coreProviderConfig.apiKey || '';
472
+ }
473
+
474
+ return {
475
+ // Protocol toggles
476
+ enabledProtocols,
477
+
478
+ // Core fields
479
+ core: {
480
+ provider: coreProvider,
481
+ model: coreModel,
482
+ apiKey: coreApiKey,
483
+ ollamaHost: coreOllamaHost,
484
+ apiKeyId: null,
485
+ hasStoredApiKey: !!options.hasStoredApiKey,
486
+ botName: config.config?.name || '',
487
+ objective: config.objective || '',
488
+ botSummary: config.botSummary || '',
489
+ },
490
+
491
+ // Identity fields
492
+ identity: {
493
+ firstMessage: config.config?.firstMessage || '',
494
+ chatDisplayName: config.config?.chatDisplayName || 'Bot',
495
+ placeholder: config.config?.placeholder || 'Type your message...',
496
+ suggestedPrompts: config.config?.suggestedPrompts || [],
497
+ },
498
+
499
+ // Protocol-specific data
500
+ protocolData: {
501
+ knowledge: {
502
+ skipRag: config.skipRag || false,
503
+ documents: [], // Documents need to be fetched separately by ID
504
+ embeddings: config._modular?.embeddings || config.embeddings || null,
505
+ },
506
+ formGathering: {
507
+ formLocale: 'en-US',
508
+ formStructureInput: '', // Not stored
509
+ generatedFormJson: config.formStructure ? JSON.stringify(config.formStructure, null, 2) : null,
510
+ formCompletionWebhook: config.formCompletionWebhook || config.config?.formCompletionWebhook || '',
511
+ afterSubmitChatMessage: config.afterSubmitChatMessage || config.config?.afterSubmitChatMessage || '',
512
+ formSendHome: config.formSendHome || config.config?.formSendHome || false,
513
+ termsAndConditions: extractTermsFromFormStructure(config.formStructure),
514
+ },
515
+ appointments: {
516
+ destinations: config.appointmentDestinations || [],
517
+ },
518
+ triage: {
519
+ routes: config.triageRoutes || [],
520
+ },
521
+ opticalRead: {
522
+ fields: config.opticalReadFields || [],
523
+ showUploadOnStart: !!config.config?.opticalReadShowUploadOnStart,
524
+ afterSubmitMessage: config.config?.opticalReadAfterSubmitMessage || '',
525
+ },
526
+ },
527
+
528
+ // Deployment config reference
529
+ deploymentConfig: null,
530
+ };
531
+ }
532
+
533
+ /**
534
+ * Detect if a deployment config is from the modular paradigm
535
+ * @param {Object} config - Deployment config from database
536
+ * @returns {boolean} True if this is a modular deployment
537
+ */
538
+ export function isModularDeployment(config) {
539
+ return config?._modular?.paradigm === 'modular';
540
+ }
541
+
542
+ /**
543
+ * Validate form structure JSON
544
+ */
545
+ function validateFormStructure(formStructure) {
546
+ if (!formStructure || typeof formStructure !== 'object') {
547
+ return 'Form structure must be a valid object';
548
+ }
549
+
550
+ if (!Array.isArray(formStructure.sections) || formStructure.sections.length === 0) {
551
+ return 'Form structure must have at least one section';
552
+ }
553
+
554
+ // Validate each section
555
+ for (let i = 0; i < formStructure.sections.length; i++) {
556
+ const section = formStructure.sections[i];
557
+
558
+ if (!section.id) {
559
+ return `Section ${i + 1} is missing an id`;
560
+ }
561
+
562
+ if (!section.label) {
563
+ return `Section ${i + 1} is missing a label`;
564
+ }
565
+
566
+ if (!Array.isArray(section.fields) || section.fields.length === 0) {
567
+ return `Section "${section.label}" must have at least one field`;
568
+ }
569
+
570
+ // Validate each field
571
+ for (let j = 0; j < section.fields.length; j++) {
572
+ const field = section.fields[j];
573
+
574
+ if (!field.id) {
575
+ return `Field ${j + 1} in section "${section.label}" is missing an id`;
576
+ }
577
+
578
+ if (!field.label) {
579
+ return `Field "${field.id}" in section "${section.label}" is missing a label`;
580
+ }
581
+
582
+ if (!field.type) {
583
+ return `Field "${field.id}" in section "${section.label}" is missing a type`;
584
+ }
585
+
586
+ // Set default value for required if not provided
587
+ if (field.required === undefined) {
588
+ field.required = false;
589
+ } else if (typeof field.required !== 'boolean') {
590
+ return `Field "${field.id}" in section "${section.label}" must have a required boolean`;
591
+ }
592
+
593
+ // Validate dropdown has options
594
+ if (field.type === 'dropdown' && (!Array.isArray(field.options) || field.options.length === 0)) {
595
+ return `Field "${field.id}" in section "${section.label}" is a dropdown but has no options`;
596
+ }
597
+ }
598
+ }
599
+
600
+ return null; // Valid
601
+ }
602
+
603
+ /**
604
+ * Validate deployment config
605
+ */
606
+ export function validateDeploymentConfig(config) {
607
+ const errors = [];
608
+
609
+ // Check config section
610
+ if (!config.config?.name) errors.push('Bot name is required');
611
+ if (!config.config?.firstMessage) errors.push('First message is required');
612
+
613
+ // Check LLM section
614
+ if (!config.llm?.provider) errors.push('LLM provider is required');
615
+ if (!config.llm[config.llm.provider]?.model) errors.push('Model is required');
616
+
617
+ // Validate credentials based on provider
618
+ const provider = config.llm?.provider;
619
+ const providerConfig = config.llm?.[provider];
620
+ if (provider === 'bedrock') {
621
+ // Bedrock stores credentials as separate fields (accessKeyId, secretAccessKey, useIamRole)
622
+ if (!providerConfig?.useIamRole && (!providerConfig?.accessKeyId || !providerConfig?.secretAccessKey)) {
623
+ errors.push('AWS Access Key ID and Secret Access Key are required (or enable IAM Role)');
624
+ }
625
+ } else if (provider === 'ollama') {
626
+ // Ollama is credential-less; host is the only required transport field.
627
+ // buildLLMConfig falls back to defaultHost when the wizard leaves it
628
+ // blank, so this should only fail if someone hand-constructs a config.
629
+ if (!providerConfig?.host) errors.push('Ollama host URL is required');
630
+ } else {
631
+ // Standard API key validation for other providers
632
+ if (!providerConfig?.apiKey) errors.push('API key is required');
633
+ }
634
+
635
+ // Check mandatory custom fields
636
+ if (!config.objective) errors.push('Objective is required');
637
+
638
+ // Validate form structure (optional, but if provided must be valid)
639
+ if (config.formStructure) {
640
+ const formError = validateFormStructure(config.formStructure);
641
+ if (formError) {
642
+ errors.push(`Form structure validation failed: ${formError}`);
643
+ }
644
+ }
645
+
646
+ return {
647
+ valid: errors.length === 0,
648
+ errors
649
+ };
650
+ }
package/lib/db/ids.js ADDED
@@ -0,0 +1,10 @@
1
+ import { randomBytes, randomUUID } from 'crypto';
2
+
3
+ export function newId(prefix) {
4
+ const id = randomUUID();
5
+ return prefix ? `${prefix}_${id}` : id;
6
+ }
7
+
8
+ export function newApiKey() {
9
+ return `lite_${randomBytes(24).toString('hex')}`;
10
+ }