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,159 @@
1
+ /**
2
+ * Smart Intent Evaluator
3
+ *
4
+ * LLM-based evaluation of user intent to determine assistance level.
5
+ * Lightweight - only analyzes the user's message and whether documents are attached.
6
+ * Does NOT read document contents - that's handled downstream.
7
+ */
8
+
9
+ import Anthropic from '@anthropic-ai/sdk';
10
+
11
+ const EVALUATOR_MODEL = 'claude-sonnet-4-20250514';
12
+ const EVALUATOR_MAX_TOKENS = 1024;
13
+
14
+ /**
15
+ * Evaluation prompt template
16
+ */
17
+ const EVALUATOR_PROMPT = `You are an intent evaluator for a bot-building platform. Analyze the user's message to determine:
18
+
19
+ 1. **Assistance Level**: Does this user need high assistance (guided flow) or low assistance (direct orchestration)?
20
+
21
+ **High Assistance** indicators:
22
+ - Vague or minimal instructions ("make a bot with this", "use this document")
23
+ - Questions about what to do ("what can you do with this?", "how should I set this up?")
24
+ - No specific bot configuration mentioned
25
+ - First-time user language
26
+ - Documents attached but no clear direction on how to use them
27
+
28
+ **Low Assistance** indicators:
29
+ - Specific bot requirements stated (name, purpose, fields to collect)
30
+ - Clear flow type mentioned (lead capture, appointment booking, support bot)
31
+ - Technical specificity (mentions webhooks, forms, specific integrations)
32
+ - Multiple configuration details provided
33
+ - Confident, directive language
34
+
35
+ 2. **Extracted Context**: Pull out any actionable configuration from the user's message:
36
+ - Bot name or naming hints ("called X", "named X")
37
+ - Organization/company name ("for Acme Corp")
38
+ - Bot purpose or objective
39
+ - Flow type (support, lead gen, appointments, FAQ, etc.)
40
+ - Specific fields or data to collect
41
+ - Greeting or tone preferences
42
+
43
+ Respond in this exact format:
44
+
45
+ ASSISTANCE_LEVEL: [high|low]
46
+
47
+ CONTEXT:
48
+ [Free-form text with extracted configuration context. Be specific and actionable. This will be passed to the main bot configuration assistant to use as defaults.]
49
+
50
+ REASONING:
51
+ [Brief explanation of why you classified the assistance level this way]`;
52
+
53
+ /**
54
+ * Evaluate user intent and extract context
55
+ *
56
+ * @param {string} userMessage - The user's input message
57
+ * @param {boolean} hasDocuments - Whether documents are attached
58
+ * @returns {Promise<{ assistanceLevel: 'high' | 'low', context: string, reasoning: string }>}
59
+ */
60
+ export async function evaluateIntent(userMessage, hasDocuments = false) {
61
+ const apiKey = process.env.BUILDER_ANTHROPIC_API_KEY;
62
+ if (!apiKey) {
63
+ console.warn('[Evaluator] No API key, defaulting to high assistance');
64
+ return {
65
+ assistanceLevel: 'high',
66
+ context: '',
67
+ reasoning: 'API key not configured, defaulting to guided flow',
68
+ };
69
+ }
70
+
71
+ // Build the evaluation input - just message + document presence metadata
72
+ let evaluationInput = `USER MESSAGE:\n${userMessage}`;
73
+
74
+ if (hasDocuments) {
75
+ evaluationInput += `\n\n[Documents are attached to this message]`;
76
+ }
77
+
78
+ try {
79
+ const client = new Anthropic({ apiKey });
80
+
81
+ const response = await client.messages.create({
82
+ model: EVALUATOR_MODEL,
83
+ max_tokens: EVALUATOR_MAX_TOKENS,
84
+ messages: [
85
+ {
86
+ role: 'user',
87
+ content: evaluationInput,
88
+ },
89
+ ],
90
+ system: EVALUATOR_PROMPT,
91
+ });
92
+
93
+ const responseText = response.content
94
+ .filter((block) => block.type === 'text')
95
+ .map((block) => block.text)
96
+ .join('\n');
97
+
98
+ return parseEvaluatorResponse(responseText);
99
+ } catch (error) {
100
+ console.error('[Evaluator] LLM call failed:', error.message);
101
+ // Fallback to high assistance on error
102
+ return {
103
+ assistanceLevel: 'high',
104
+ context: '',
105
+ reasoning: `Evaluation failed (${error.message}), defaulting to guided flow`,
106
+ };
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Parse the evaluator's response into structured format
112
+ *
113
+ * @param {string} responseText - Raw response from evaluator LLM
114
+ * @returns {{ assistanceLevel: 'high' | 'low', context: string, reasoning: string }}
115
+ */
116
+ function parseEvaluatorResponse(responseText) {
117
+ // Extract assistance level
118
+ const levelMatch = responseText.match(/ASSISTANCE_LEVEL:\s*(high|low)/i);
119
+ const assistanceLevel = levelMatch ? levelMatch[1].toLowerCase() : 'high';
120
+
121
+ // Extract context section
122
+ const contextMatch = responseText.match(/CONTEXT:\s*([\s\S]*?)(?=REASONING:|$)/i);
123
+ const context = contextMatch ? contextMatch[1].trim() : responseText;
124
+
125
+ // Extract reasoning section
126
+ const reasoningMatch = responseText.match(/REASONING:\s*([\s\S]*?)$/i);
127
+ const reasoning = reasoningMatch ? reasoningMatch[1].trim() : 'No reasoning provided';
128
+
129
+ return {
130
+ assistanceLevel,
131
+ context,
132
+ reasoning,
133
+ };
134
+ }
135
+
136
+ /**
137
+ * Quick heuristic check to determine if evaluation is needed
138
+ * (Skip evaluation for obvious cases to save latency)
139
+ *
140
+ * @param {string} userMessage - The user's input message
141
+ * @param {boolean} hasDocuments - Whether documents are attached
142
+ * @returns {{ skipEvaluation: boolean, defaultLevel: 'high' | 'low' | null }}
143
+ */
144
+ export function shouldSkipEvaluation(userMessage, hasDocuments) {
145
+ const wordCount = userMessage.split(/\s+/).filter((w) => w.length > 0).length;
146
+
147
+ // Very short message with documents = definitely high assistance
148
+ if (hasDocuments && wordCount <= 10) {
149
+ return { skipEvaluation: true, defaultLevel: 'high' };
150
+ }
151
+
152
+ // Very detailed message (100+ words) = definitely low assistance
153
+ if (wordCount >= 100) {
154
+ return { skipEvaluation: true, defaultLevel: 'low' };
155
+ }
156
+
157
+ // Otherwise, run the evaluator
158
+ return { skipEvaluation: false, defaultLevel: null };
159
+ }
@@ -0,0 +1,252 @@
1
+ /**
2
+ * Builder Save Executor
3
+ *
4
+ * Persists a builder session as a deployment row in SQLite. Does NOT build
5
+ * the ZIP artifact — that's a separate, on-demand step (see
6
+ * `lib/deployers/build.js` and `POST /api/deployments/[id]/build`).
7
+ *
8
+ * The chat UI surfaces a "Build & Download" CTA after this returns.
9
+ */
10
+
11
+ import { BuilderSessionRepository } from '../db/repositories/builderSessions.js';
12
+ import {
13
+ DeploymentRepository,
14
+ DEPLOYMENT_STATUS,
15
+ } from '../db/repositories/deployments.js';
16
+ import { DeploymentEventRepository } from '../db/repositories/deploymentEvents.js';
17
+ import { ApiKeyRepository } from '../db/repositories/apiKeys.js';
18
+ import { decryptApiKey, generateApiKey } from '../deployment-auth.js';
19
+ import { composeAndCache, getBuilderSession, validateForDeployment } from './session.js';
20
+ import { buildLLMConfig } from '../config-builder.js';
21
+
22
+ /**
23
+ * Save a modular session as a deployment row.
24
+ *
25
+ * @param {string} sessionId
26
+ * @param {string} userId
27
+ * @param {Object} options
28
+ * @param {string} [options.botSpaceId]
29
+ * @param {string} [options.redeploymentId] - If provided, update existing row
30
+ * @returns {Promise<Object>}
31
+ */
32
+ export async function saveBuilderConfig(sessionId, userId, options = {}) {
33
+ const { botSpaceId, redeploymentId } = options;
34
+ const isUpdate = !!redeploymentId;
35
+
36
+ const session = await getBuilderSession(sessionId, userId);
37
+
38
+ const validation = await validateForDeployment(sessionId, userId);
39
+ if (!validation.valid) {
40
+ throw new Error(`Session not ready to save: ${validation.errors.join(', ')}`);
41
+ }
42
+
43
+ const { instructions } = await composeAndCache(sessionId, userId);
44
+
45
+ const apiKeyRecords = await ApiKeyRepository.findByUserId(userId);
46
+ const apiKeyRecord = apiKeyRecords.find(
47
+ (key) => key.provider === session.coreConfig.provider
48
+ );
49
+ if (!apiKeyRecord) {
50
+ throw new Error(`No API key found for provider: ${session.coreConfig.provider}`);
51
+ }
52
+ const decryptedApiKey = decryptApiKey(apiKeyRecord.encryptedKey);
53
+
54
+ const deploymentConfig = buildDeploymentConfig(session, instructions, decryptedApiKey);
55
+ const documentIds = session.protocolData.knowledge?.documents?.map((d) => d.id) || [];
56
+ const enabledProtocols = session.enabledProtocols;
57
+
58
+ // All bots run vector retrieval. Knowledge protocol embeds documents;
59
+ // triage protocol embeds route descriptions; both end up in the same
60
+ // single cosine index. A bot with neither protocol simply has no
61
+ // embeddings — RAG is silently disabled at runtime in that case.
62
+ const ragMode = 'vector';
63
+ const embeddings = session.generatedConfigs?.embeddings || null;
64
+ if (enabledProtocols.knowledge && !embeddings?.storageKey) {
65
+ throw new Error(
66
+ 'Knowledge protocol is enabled but no embeddings were produced. Run process_documents first.'
67
+ );
68
+ }
69
+ if (enabledProtocols.triage && !embeddings?.storageKey) {
70
+ throw new Error(
71
+ 'Triage protocol is enabled but no embeddings were produced. Run generate_triage_config first.'
72
+ );
73
+ }
74
+
75
+ let deployment;
76
+ let deploymentApiKey;
77
+
78
+ if (isUpdate) {
79
+ const existing = await DeploymentRepository.findById(redeploymentId);
80
+ if (!existing) {
81
+ throw new Error(`Deployment ${redeploymentId} not found`);
82
+ }
83
+ if (existing.userId && existing.userId !== userId) {
84
+ throw new Error('Unauthorized to update this deployment');
85
+ }
86
+
87
+ deploymentApiKey = existing.apiKey;
88
+
89
+ deployment = await DeploymentRepository.update(redeploymentId, {
90
+ botName: session.coreConfig.botName,
91
+ config: {
92
+ ...deploymentConfig,
93
+ _modular: {
94
+ paradigm: 'modular',
95
+ enabledProtocols,
96
+ sessionId,
97
+ },
98
+ },
99
+ documentIds,
100
+ error: null,
101
+ });
102
+ } else {
103
+ deploymentApiKey = generateApiKey();
104
+ deployment = await DeploymentRepository.create({
105
+ userId,
106
+ botSpaceId: botSpaceId || null,
107
+ botName: session.coreConfig.botName,
108
+ flowType: 'modular',
109
+ status: DEPLOYMENT_STATUS.SAVED,
110
+ config: {
111
+ ...deploymentConfig,
112
+ _modular: {
113
+ paradigm: 'modular',
114
+ enabledProtocols,
115
+ sessionId,
116
+ },
117
+ },
118
+ documentIds,
119
+ apiKey: deploymentApiKey,
120
+ });
121
+ }
122
+
123
+ // Stamp embedding pointers onto the row. ragMode is always 'vector' now;
124
+ // setRagMode is kept as a no-op write so legacy rows keep their schema.
125
+ await DeploymentRepository.setRagMode(deployment.id, ragMode);
126
+ if (embeddings?.storageKey) {
127
+ await DeploymentRepository.setEmbeddings(deployment.id, {
128
+ storageKey: embeddings.storageKey,
129
+ model: embeddings.model,
130
+ chunkCount: embeddings.chunkCount,
131
+ });
132
+ } else {
133
+ await DeploymentRepository.clearEmbeddings(deployment.id);
134
+ }
135
+ // Re-fetch so the returned deployment reflects the embedding columns.
136
+ deployment = await DeploymentRepository.findById(deployment.id);
137
+
138
+ await BuilderSessionRepository.linkDeployment(sessionId, userId, deployment.id);
139
+ await DeploymentEventRepository.create({
140
+ deploymentId: deployment.id,
141
+ userId,
142
+ eventType: isUpdate ? 'updated' : 'created',
143
+ status: deployment.status,
144
+ config: deployment.config,
145
+ });
146
+
147
+ await BuilderSessionRepository.updateStepProgress(
148
+ sessionId,
149
+ userId,
150
+ 'deploy',
151
+ 'completed'
152
+ );
153
+
154
+ return {
155
+ success: true,
156
+ deploymentId: deployment.id,
157
+ status: deployment.status,
158
+ botName: session.coreConfig.botName,
159
+ sessionId,
160
+ isUpdate,
161
+ buildUrl: `/api/deployments/${deployment.id}/build`,
162
+ downloadUrl: `/api/deployments/${deployment.id}/download`,
163
+ };
164
+ }
165
+
166
+ /**
167
+ * Build deployment config from session data
168
+ * Produces the nested structure expected by validateDeploymentConfig:
169
+ * { config: {...}, llm: { provider, [provider]: {...} }, objective, ... }
170
+ */
171
+ function buildDeploymentConfig(session, instructions, apiKey) {
172
+ const { coreConfig, identityConfig, enabledProtocols, protocolData, generatedConfigs } = session;
173
+
174
+ const provider = coreConfig.provider || 'anthropic';
175
+ const model = coreConfig.model || 'claude-sonnet-4-20250514';
176
+
177
+ const configSection = {
178
+ instructions: './config/instructions.txt',
179
+ name: coreConfig.botName,
180
+ chatDisplayName: identityConfig.chatDisplayName || 'Bot',
181
+ placeholder: 'Type your message...',
182
+ firstMessage: identityConfig.firstMessage || `Welcome! I'm ${coreConfig.botName}. How can I help you?`,
183
+ suggestedPrompts: (identityConfig.suggestedPrompts || []).map((prompt) =>
184
+ typeof prompt === 'string' ? { suggestedPrompt: prompt } : prompt
185
+ ),
186
+ actionsBar: {
187
+ showBar: false,
188
+ showSourceButton: false,
189
+ showMetadataButton: false,
190
+ showCopyButton: false,
191
+ showSuggestedPrompts: false,
192
+ },
193
+ };
194
+
195
+ if (enabledProtocols.formGathering && protocolData.formGathering) {
196
+ configSection.isForm = true;
197
+ configSection.formStructure = './config/formFormat.json';
198
+ if (protocolData.formGathering.formCompletionWebhook) {
199
+ configSection.formCompletionWebhook = protocolData.formGathering.formCompletionWebhook;
200
+ }
201
+ configSection.afterSubmitChatMessage = protocolData.formGathering.afterSubmitChatMessage;
202
+ if (protocolData.formGathering.formSendHome) {
203
+ configSection.formSendHome = true;
204
+ }
205
+ }
206
+
207
+ if (enabledProtocols.appointments && protocolData.appointments) {
208
+ configSection.isCalendar = true;
209
+ configSection.calendarConfig = './config/calendarConfig.json';
210
+ }
211
+
212
+ if (enabledProtocols.triage && protocolData.triage) {
213
+ configSection.isTriage = true;
214
+ configSection.triageRoutes = './config/triageRoutes.json';
215
+ }
216
+
217
+ // Optical Read: chat-builder writes opticalRead.fields onto generatedConfigs
218
+ // (see generate_optical_read_config). Wire the artifact-side flag/path here
219
+ // and surface the field list at top-level for build.js to pick up.
220
+ const opticalReadFields = enabledProtocols.opticalRead
221
+ ? generatedConfigs?.opticalRead?.fields || []
222
+ : [];
223
+ if (enabledProtocols.opticalRead && opticalReadFields.length > 0) {
224
+ configSection.isOpticalRead = true;
225
+ configSection.opticalReadFields = './config/opticalReadFields.json';
226
+ // Default the upload-first entry point on for chat-builder output. The
227
+ // chat builder doesn't expose a per-bot toggle; users can opt out by
228
+ // editing the deployment in the wizard, which respects the saved value.
229
+ configSection.opticalReadShowUploadOnStart = true;
230
+ }
231
+
232
+ return {
233
+ config: configSection,
234
+ llm: buildLLMConfig(provider, apiKey, model, {}),
235
+ objective: identityConfig.objective,
236
+ botSummary: generatedConfigs?.botSummary || '',
237
+ formStructure: enabledProtocols.formGathering
238
+ ? protocolData.formGathering?.generatedFormJson
239
+ : undefined,
240
+ formCompletionWebhook: protocolData.formGathering?.formCompletionWebhook || undefined,
241
+ afterSubmitChatMessage: protocolData.formGathering?.afterSubmitChatMessage || undefined,
242
+ formSendHome: protocolData.formGathering?.formSendHome || undefined,
243
+ appointmentDestinations: enabledProtocols.appointments
244
+ ? protocolData.appointments?.destinations
245
+ : undefined,
246
+ triageRoutes: enabledProtocols.triage ? protocolData.triage?.routes : undefined,
247
+ opticalReadFields: enabledProtocols.opticalRead ? opticalReadFields : undefined,
248
+ paradigm: 'modular',
249
+ enabledProtocols,
250
+ _composedInstructions: instructions,
251
+ };
252
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Builder Flow Entry Point
3
+ *
4
+ * Exports all builder functionality for the protocol-aware wizard system.
5
+ * Supports both legacy step-card wizard and inverted flow ("Claude proposes, User disposes").
6
+ */
7
+
8
+ // Session management (legacy wizard mode)
9
+ export {
10
+ createBuilderSession,
11
+ getBuilderSession,
12
+ toggleProtocol,
13
+ generateStepFlow,
14
+ saveCoreConfig,
15
+ saveIdentityConfig,
16
+ saveProtocolConfig,
17
+ composeAndCache,
18
+ validateForDeployment,
19
+ deleteBuilderSession,
20
+ } from './session.js';
21
+
22
+ // Composer bridge
23
+ export { composeFromSession, previewComposition } from './composer-bridge.js';
24
+
25
+ // Validators
26
+ export {
27
+ PROTOCOL_VALIDATORS,
28
+ validateCoreConfig,
29
+ validateIdentityConfig,
30
+ validateEnabledProtocols,
31
+ validateSessionForDeployment,
32
+ } from './validators.js';
33
+
34
+ // Save executor (formerly the deployment executor — now writes to SQLite only;
35
+ // callers that want a built artifact must hit /api/deployments/[id]/build).
36
+ export { saveBuilderConfig } from './executor.js';
37
+
38
+ // Inverted flow tools
39
+ export { BUILDER_TOOLS, TOOL_LABELS, TOOL_ICONS, getToolByName, validateToolInput } from './tools.js';
40
+
41
+ // Inverted flow tool executors
42
+ export { executeBuilderTool, builderToolHandlers } from './tool-executors.js';
43
+
44
+ // Inverted flow system prompt
45
+ export { buildBuilderSystemPrompt } from './system-prompt.js';
46
+
47
+ // Smart intent evaluation
48
+ export { evaluateIntent, shouldSkipEvaluation } from './evaluator.js';
@@ -0,0 +1,248 @@
1
+ /**
2
+ * Session Manager for Builder Flow
3
+ *
4
+ * High-level session management functions that orchestrate
5
+ * repository operations and business logic.
6
+ */
7
+
8
+ import { BuilderSessionRepository } from '../db/repositories/builderSessions.js';
9
+ import { composeFromSession } from './composer-bridge.js';
10
+ import { validateSessionForDeployment, PROTOCOL_VALIDATORS } from './validators.js';
11
+
12
+ /**
13
+ * Create a new builder session
14
+ * @param {string|null} orgId - Organization ID (optional)
15
+ * @param {string} userId - User ID
16
+ * @returns {Promise<Object>} Created session
17
+ */
18
+ export async function createBuilderSession(orgId, userId) {
19
+ return BuilderSessionRepository.create({ userId });
20
+ }
21
+
22
+ /**
23
+ * Get session with access check
24
+ * @param {string} sessionId - Session ID
25
+ * @param {string} userId - User ID
26
+ * @returns {Promise<Object>} Session object
27
+ * @throws {Error} If session not found or access denied
28
+ */
29
+ export async function getBuilderSession(sessionId, userId) {
30
+ const session = await BuilderSessionRepository.findByIdAndUserId(sessionId, userId);
31
+ if (!session) {
32
+ throw new Error('Session not found or access denied');
33
+ }
34
+ return session;
35
+ }
36
+
37
+ /**
38
+ * Toggle protocol and regenerate step flow
39
+ * @param {string} sessionId - Session ID
40
+ * @param {string} userId - User ID
41
+ * @param {string} protocol - Protocol name
42
+ * @param {boolean} enabled - Enable or disable
43
+ * @returns {Promise<{ session: Object, steps: Array }>}
44
+ */
45
+ export async function toggleProtocol(sessionId, userId, protocol, enabled) {
46
+ const session = await getBuilderSession(sessionId, userId);
47
+
48
+ const enabledProtocols = {
49
+ ...session.enabledProtocols,
50
+ [protocol]: enabled,
51
+ };
52
+
53
+ const updatedSession = await BuilderSessionRepository.updateProtocols(
54
+ sessionId,
55
+ userId,
56
+ enabledProtocols
57
+ );
58
+
59
+ // Return updated session with regenerated step list
60
+ return {
61
+ session: updatedSession,
62
+ steps: generateStepFlow(updatedSession.enabledProtocols),
63
+ };
64
+ }
65
+
66
+ /**
67
+ * Generate step flow based on enabled protocols
68
+ * @param {Object} enabledProtocols - Protocol toggles
69
+ * @returns {Array} Array of step objects
70
+ */
71
+ export function generateStepFlow(enabledProtocols) {
72
+ const steps = [
73
+ { id: 'core', number: 1, section: 'Bot Setup', required: true },
74
+ { id: 'protocols', number: 2, section: 'Capabilities', required: true },
75
+ { id: 'identity', number: 3, section: 'Identity', required: true },
76
+ ];
77
+
78
+ let stepNumber = 4;
79
+
80
+ if (enabledProtocols.knowledge) {
81
+ steps.push({
82
+ id: 'knowledge',
83
+ number: stepNumber++,
84
+ section: 'Knowledge Base',
85
+ protocol: 'knowledge',
86
+ });
87
+ }
88
+
89
+ if (enabledProtocols.formGathering) {
90
+ steps.push({
91
+ id: 'formGathering',
92
+ number: stepNumber++,
93
+ section: 'Form Collection',
94
+ protocol: 'formGathering',
95
+ });
96
+ }
97
+
98
+ if (enabledProtocols.appointments) {
99
+ steps.push({
100
+ id: 'appointments',
101
+ number: stepNumber++,
102
+ section: 'Appointments',
103
+ protocol: 'appointments',
104
+ });
105
+ }
106
+
107
+ steps.push({
108
+ id: 'deploy',
109
+ number: stepNumber,
110
+ section: 'Deploy',
111
+ required: true,
112
+ });
113
+
114
+ return steps;
115
+ }
116
+
117
+ /**
118
+ * Save core configuration
119
+ * @param {string} sessionId - Session ID
120
+ * @param {string} userId - User ID
121
+ * @param {Object} config - Core configuration
122
+ * @returns {Promise<Object>} Updated session
123
+ */
124
+ export async function saveCoreConfig(sessionId, userId, config) {
125
+ await getBuilderSession(sessionId, userId); // Access check
126
+
127
+ const updatedSession = await BuilderSessionRepository.updateCoreConfig(
128
+ sessionId,
129
+ userId,
130
+ config
131
+ );
132
+
133
+ // Mark step as completed
134
+ await BuilderSessionRepository.updateStepProgress(sessionId, userId, 'core', 'completed');
135
+
136
+ return updatedSession;
137
+ }
138
+
139
+ /**
140
+ * Save identity configuration
141
+ * @param {string} sessionId - Session ID
142
+ * @param {string} userId - User ID
143
+ * @param {Object} config - Identity configuration
144
+ * @returns {Promise<Object>} Updated session
145
+ */
146
+ export async function saveIdentityConfig(sessionId, userId, config) {
147
+ await getBuilderSession(sessionId, userId); // Access check
148
+
149
+ const updatedSession = await BuilderSessionRepository.updateIdentityConfig(
150
+ sessionId,
151
+ userId,
152
+ config
153
+ );
154
+
155
+ await BuilderSessionRepository.updateStepProgress(sessionId, userId, 'identity', 'completed');
156
+
157
+ return updatedSession;
158
+ }
159
+
160
+ /**
161
+ * Save protocol-specific configuration
162
+ * @param {string} sessionId - Session ID
163
+ * @param {string} userId - User ID
164
+ * @param {string} protocol - Protocol name
165
+ * @param {Object} data - Protocol data
166
+ * @returns {Promise<Object>} Updated session
167
+ */
168
+ export async function saveProtocolConfig(sessionId, userId, protocol, data) {
169
+ const session = await getBuilderSession(sessionId, userId);
170
+
171
+ // Validate protocol data
172
+ if (PROTOCOL_VALIDATORS[protocol]) {
173
+ const validation = PROTOCOL_VALIDATORS[protocol](data);
174
+ if (!validation.valid) {
175
+ throw new Error(validation.error);
176
+ }
177
+ }
178
+
179
+ const updatedSession = await BuilderSessionRepository.updateProtocolData(
180
+ sessionId,
181
+ userId,
182
+ protocol,
183
+ data
184
+ );
185
+
186
+ // Mark protocol step as completed
187
+ await BuilderSessionRepository.updateStepProgress(sessionId, userId, protocol, 'completed');
188
+
189
+ return updatedSession;
190
+ }
191
+
192
+ /**
193
+ * Compose instructions and cache them
194
+ * @param {string} sessionId - Session ID
195
+ * @param {string} userId - User ID
196
+ * @returns {Promise<{ instructions: string, responseFormat: string, protocolsIncluded: string[], cached: boolean }>}
197
+ */
198
+ export async function composeAndCache(sessionId, userId) {
199
+ const session = await getBuilderSession(sessionId, userId);
200
+
201
+ // Check if cached instructions are still valid
202
+ if (session.composedInstructions) {
203
+ return {
204
+ instructions: session.composedInstructions,
205
+ responseFormat: session.responseFormat,
206
+ cached: true,
207
+ };
208
+ }
209
+
210
+ // Compose new instructions
211
+ const { instructions, responseFormat, protocolsIncluded } = await composeFromSession(session);
212
+
213
+ // Cache them
214
+ await BuilderSessionRepository.cacheComposedInstructions(
215
+ sessionId,
216
+ userId,
217
+ instructions,
218
+ responseFormat
219
+ );
220
+
221
+ return {
222
+ instructions,
223
+ responseFormat,
224
+ protocolsIncluded,
225
+ cached: false,
226
+ };
227
+ }
228
+
229
+ /**
230
+ * Validate session is ready for deployment
231
+ * @param {string} sessionId - Session ID
232
+ * @param {string} userId - User ID
233
+ * @returns {Promise<{ valid: boolean, errors: string[] }>}
234
+ */
235
+ export async function validateForDeployment(sessionId, userId) {
236
+ const session = await getBuilderSession(sessionId, userId);
237
+ return validateSessionForDeployment(session);
238
+ }
239
+
240
+ /**
241
+ * Delete a session
242
+ * @param {string} sessionId - Session ID
243
+ * @param {string} userId - User ID
244
+ * @returns {Promise<boolean>} True if deleted
245
+ */
246
+ export async function deleteBuilderSession(sessionId, userId) {
247
+ return BuilderSessionRepository.delete(sessionId, userId);
248
+ }