mojulo 0.0.0 → 0.1.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.
- package/README.md +54 -4
- package/lib/audit-logger-new.js +11 -0
- package/lib/auth/gate.js +25 -0
- package/lib/auth/service.js +17 -0
- package/lib/auth/session.js +63 -0
- package/lib/builder/chat-processor.js +607 -0
- package/lib/builder/composer-bridge.js +82 -0
- package/lib/builder/evaluator.js +159 -0
- package/lib/builder/executor.js +252 -0
- package/lib/builder/index.js +48 -0
- package/lib/builder/session.js +248 -0
- package/lib/builder/system-prompt.js +422 -0
- package/lib/builder/tone-presets.js +75 -0
- package/lib/builder/tool-executors.js +1527 -0
- package/lib/builder/tools.js +338 -0
- package/lib/builder/validators.js +239 -0
- package/lib/composer/composer.js +225 -0
- package/lib/composer/index.js +40 -0
- package/lib/composer/protocols/00_base.txt +19 -0
- package/lib/composer/protocols/01_knowledge.txt +9 -0
- package/lib/composer/protocols/02_form-gathering.txt +32 -0
- package/lib/composer/protocols/03_appointments.txt +16 -0
- package/lib/composer/protocols/04_triage.txt +15 -0
- package/lib/composer/protocols/05_optical-read.txt +22 -0
- package/lib/composer/response-builder.js +98 -0
- package/lib/config-builder.js +650 -0
- package/lib/db/ids.js +10 -0
- package/lib/db/index.js +179 -0
- package/lib/db/repositories/apiKeys.js +72 -0
- package/lib/db/repositories/auditLogs.js +12 -0
- package/lib/db/repositories/botSpaces.js +12 -0
- package/lib/db/repositories/builderSessions.js +312 -0
- package/lib/db/repositories/deploymentEvents.js +12 -0
- package/lib/db/repositories/deployments.js +385 -0
- package/lib/db/repositories/documents.js +68 -0
- package/lib/db/repositories/mcpJobs.js +84 -0
- package/lib/deployers/bot-fleet.js +110 -0
- package/lib/deployers/bot-proxy.js +72 -0
- package/lib/deployers/build.js +89 -0
- package/lib/deployers/cloud-deploy.js +310 -0
- package/lib/deployers/docker.js +439 -0
- package/lib/deployers/fly.js +432 -0
- package/lib/deployers/index.js +38 -0
- package/lib/deployment-auth.js +36 -0
- package/lib/document-parser.js +171 -0
- package/lib/embedder/chunker.js +93 -0
- package/lib/embedder/local.js +101 -0
- package/lib/embedder/preview-rag.js +93 -0
- package/lib/envelope-schema.js +54 -0
- package/lib/fleet/scoped-sql.js +342 -0
- package/lib/form-schema-config/base.js +135 -0
- package/lib/form-schema-config/index.js +286 -0
- package/lib/form-schema-config/locales/af-ZA.js +153 -0
- package/lib/form-schema-config/locales/ar-AE.js +142 -0
- package/lib/form-schema-config/locales/ar-SA.js +164 -0
- package/lib/form-schema-config/locales/de-DE.js +152 -0
- package/lib/form-schema-config/locales/en-AU.js +161 -0
- package/lib/form-schema-config/locales/en-CA.js +115 -0
- package/lib/form-schema-config/locales/en-GB.js +132 -0
- package/lib/form-schema-config/locales/en-IN.js +219 -0
- package/lib/form-schema-config/locales/en-MY.js +171 -0
- package/lib/form-schema-config/locales/en-NG.js +198 -0
- package/lib/form-schema-config/locales/en-PH.js +186 -0
- package/lib/form-schema-config/locales/en-SG.js +153 -0
- package/lib/form-schema-config/locales/en-US.js +138 -0
- package/lib/form-schema-config/locales/es-ES.js +171 -0
- package/lib/form-schema-config/locales/es-MX.js +193 -0
- package/lib/form-schema-config/locales/fr-CA.js +138 -0
- package/lib/form-schema-config/locales/fr-FR.js +155 -0
- package/lib/form-schema-config/locales/hi-IN.js +219 -0
- package/lib/form-schema-config/locales/it-IT.js +157 -0
- package/lib/form-schema-config/locales/ja-JP.js +169 -0
- package/lib/form-schema-config/locales/ko-KR.js +140 -0
- package/lib/form-schema-config/locales/nl-NL.js +149 -0
- package/lib/form-schema-config/locales/pt-BR.js +168 -0
- package/lib/form-schema-config/locales/zh-CN.js +172 -0
- package/lib/form-schema-config/locales/zh-HK.js +142 -0
- package/lib/form-structure-schema.js +191 -0
- package/lib/llm-providers.js +828 -0
- package/lib/markdown.js +197 -0
- package/lib/mcp/catalysts/appointment-to-calendar.md +84 -0
- package/lib/mcp/catalysts/conversations-to-channel-digest.md +104 -0
- package/lib/mcp/catalysts/document-extract-to-store.md +92 -0
- package/lib/mcp/catalysts/knowledge-gap-miner.md +96 -0
- package/lib/mcp/catalysts/loader.js +144 -0
- package/lib/mcp/catalysts/qualify-lead-to-crm.md +83 -0
- package/lib/mcp/catalysts/scan-conversations-for-signal.md +92 -0
- package/lib/mcp/catalysts/submission-to-ticket.md +83 -0
- package/lib/mcp/catalysts/submissions-to-warehouse.md +103 -0
- package/lib/mcp/catalysts/weekly-submissions-digest.md +82 -0
- package/lib/mcp/jobs.js +64 -0
- package/lib/mcp/server.js +184 -0
- package/lib/mcp/session-binding.js +130 -0
- package/lib/mcp/tools/build.js +123 -0
- package/lib/mcp/tools/catalysts.js +477 -0
- package/lib/mcp/tools/context.js +325 -0
- package/lib/mcp/tools/fleet.js +391 -0
- package/lib/mcp/tools/jobs-tools.js +240 -0
- package/lib/mcp/tools/operate.js +314 -0
- package/lib/preview/build-preview-config.js +136 -0
- package/lib/rate-limiter.js +11 -0
- package/lib/resolve-api-key.js +142 -0
- package/lib/storage/index.js +40 -0
- package/messages/de.json +2136 -0
- package/messages/en.json +2136 -0
- package/messages/es.json +2136 -0
- package/messages/fr.json +2136 -0
- package/messages/it.json +2136 -0
- package/messages/ja.json +2136 -0
- package/messages/ko.json +2136 -0
- package/messages/nl.json +2136 -0
- package/messages/pl.json +2136 -0
- package/messages/pt.json +2136 -0
- package/messages/ru.json +2136 -0
- package/messages/uk.json +2136 -0
- package/messages/zh.json +2136 -0
- package/package.json +68 -5
- package/scripts/mcp-config.mjs +162 -0
- package/scripts/mcp-stdio-loader.mjs +42 -0
- package/scripts/mcp-stdio.mjs +108 -0
- package/scripts/mojulo-paths.mjs +48 -0
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import { getDb } from '../index.js';
|
|
3
|
+
import { newId } from '../ids.js';
|
|
4
|
+
|
|
5
|
+
export const DEPLOYMENT_STATUS = {
|
|
6
|
+
SAVED: 'saved',
|
|
7
|
+
BUILDING: 'building',
|
|
8
|
+
READY: 'ready',
|
|
9
|
+
STALE: 'stale',
|
|
10
|
+
BUILD_FAILED: 'build_failed',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const CLOUD_STATUS = {
|
|
14
|
+
PENDING: 'pending',
|
|
15
|
+
DEPLOYING: 'deploying',
|
|
16
|
+
RUNNING: 'running',
|
|
17
|
+
PAUSED: 'paused',
|
|
18
|
+
FAILED: 'failed',
|
|
19
|
+
DESTROYED: 'destroyed',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function rowToDeployment(row) {
|
|
23
|
+
if (!row) return null;
|
|
24
|
+
return {
|
|
25
|
+
id: row.id,
|
|
26
|
+
botName: row.bot_name,
|
|
27
|
+
flowType: row.flow_type,
|
|
28
|
+
status: row.status,
|
|
29
|
+
config: row.config ? JSON.parse(row.config) : null,
|
|
30
|
+
configHash: row.config_hash,
|
|
31
|
+
lastBuiltHash: row.last_built_hash,
|
|
32
|
+
artifactPath: row.artifact_path,
|
|
33
|
+
documentIds: row.document_ids ? JSON.parse(row.document_ids) : [],
|
|
34
|
+
apiKey: row.api_key,
|
|
35
|
+
error: row.error,
|
|
36
|
+
url: row.url || null,
|
|
37
|
+
lastSeenAt: row.last_seen_at ? new Date(row.last_seen_at) : null,
|
|
38
|
+
ragMode: row.rag_mode || 'vector',
|
|
39
|
+
embeddingStorageKey: row.embedding_storage_key || null,
|
|
40
|
+
embeddingModel: row.embedding_model || null,
|
|
41
|
+
embeddingChunkCount:
|
|
42
|
+
row.embedding_chunk_count != null ? row.embedding_chunk_count : null,
|
|
43
|
+
cloudProvider: row.cloud_provider || null,
|
|
44
|
+
cloudAppName: row.cloud_app_name || null,
|
|
45
|
+
cloudStatus: row.cloud_status || null,
|
|
46
|
+
cloudUrl: row.cloud_url || null,
|
|
47
|
+
cloudProgress: row.cloud_progress ? JSON.parse(row.cloud_progress) : [],
|
|
48
|
+
cloudOptions: row.cloud_options ? JSON.parse(row.cloud_options) : null,
|
|
49
|
+
cloudError: row.cloud_error || null,
|
|
50
|
+
cloudLastDeployedAt: row.cloud_last_deployed_at
|
|
51
|
+
? new Date(row.cloud_last_deployed_at)
|
|
52
|
+
: null,
|
|
53
|
+
cloudMachineId: row.cloud_machine_id || null,
|
|
54
|
+
cloudVolumeId: row.cloud_volume_id || null,
|
|
55
|
+
createdAt: new Date(row.created_at),
|
|
56
|
+
updatedAt: new Date(row.updated_at),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function canonicalize(value) {
|
|
61
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
62
|
+
if (value && typeof value === 'object') {
|
|
63
|
+
return Object.keys(value)
|
|
64
|
+
.sort()
|
|
65
|
+
.reduce((acc, k) => {
|
|
66
|
+
acc[k] = canonicalize(value[k]);
|
|
67
|
+
return acc;
|
|
68
|
+
}, {});
|
|
69
|
+
}
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function hashConfig(config, documentIds = []) {
|
|
74
|
+
const projection = {
|
|
75
|
+
config: canonicalize(config || {}),
|
|
76
|
+
documentIds: [...(documentIds || [])].sort(),
|
|
77
|
+
};
|
|
78
|
+
return crypto
|
|
79
|
+
.createHash('sha256')
|
|
80
|
+
.update(JSON.stringify(projection))
|
|
81
|
+
.digest('hex');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const DeploymentRepository = {
|
|
85
|
+
hashConfig,
|
|
86
|
+
|
|
87
|
+
async findById(id) {
|
|
88
|
+
const db = getDb();
|
|
89
|
+
const row = db.prepare('SELECT * FROM deployments WHERE id = ?').get(id);
|
|
90
|
+
return rowToDeployment(row);
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
async findByIdAndUserId(id, _userId) {
|
|
94
|
+
return this.findById(id);
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
async findByBotSpaceId(_botSpaceId) {
|
|
98
|
+
return this.list();
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
async list() {
|
|
102
|
+
const db = getDb();
|
|
103
|
+
const rows = db.prepare('SELECT * FROM deployments ORDER BY created_at DESC').all();
|
|
104
|
+
return rows.map(rowToDeployment);
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
async create({
|
|
108
|
+
botName,
|
|
109
|
+
flowType = 'modular',
|
|
110
|
+
status = DEPLOYMENT_STATUS.SAVED,
|
|
111
|
+
config,
|
|
112
|
+
apiKey,
|
|
113
|
+
documentIds = [],
|
|
114
|
+
artifactPath = null,
|
|
115
|
+
}) {
|
|
116
|
+
const db = getDb();
|
|
117
|
+
const id = newId('dep');
|
|
118
|
+
const now = Date.now();
|
|
119
|
+
const configHash = hashConfig(config, documentIds);
|
|
120
|
+
db.prepare(
|
|
121
|
+
`INSERT INTO deployments (id, bot_name, flow_type, status, config, config_hash, last_built_hash, artifact_path, document_ids, api_key, error, created_at, updated_at)
|
|
122
|
+
VALUES (?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, NULL, ?, ?)`
|
|
123
|
+
).run(
|
|
124
|
+
id,
|
|
125
|
+
botName,
|
|
126
|
+
flowType,
|
|
127
|
+
status,
|
|
128
|
+
JSON.stringify(config || {}),
|
|
129
|
+
configHash,
|
|
130
|
+
artifactPath,
|
|
131
|
+
JSON.stringify(documentIds),
|
|
132
|
+
apiKey,
|
|
133
|
+
now,
|
|
134
|
+
now
|
|
135
|
+
);
|
|
136
|
+
return this.findById(id);
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Update a deployment row. Recomputes config_hash from the merged config +
|
|
141
|
+
* documentIds. If the hash changed and a build already exists, transitions
|
|
142
|
+
* status to 'stale' (unless the caller passes an explicit status).
|
|
143
|
+
*/
|
|
144
|
+
async update(id, changes) {
|
|
145
|
+
const db = getDb();
|
|
146
|
+
const existing = await this.findById(id);
|
|
147
|
+
if (!existing) return null;
|
|
148
|
+
|
|
149
|
+
const merged = {
|
|
150
|
+
botName: changes.botName ?? existing.botName,
|
|
151
|
+
config: changes.config ?? existing.config,
|
|
152
|
+
artifactPath:
|
|
153
|
+
changes.artifactPath !== undefined ? changes.artifactPath : existing.artifactPath,
|
|
154
|
+
documentIds: changes.documentIds ?? existing.documentIds,
|
|
155
|
+
error: changes.error !== undefined ? changes.error : existing.error,
|
|
156
|
+
lastBuiltHash:
|
|
157
|
+
changes.lastBuiltHash !== undefined ? changes.lastBuiltHash : existing.lastBuiltHash,
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const newHash = hashConfig(merged.config, merged.documentIds);
|
|
161
|
+
const hashChanged = newHash !== existing.configHash;
|
|
162
|
+
|
|
163
|
+
let nextStatus = changes.status ?? existing.status;
|
|
164
|
+
if (
|
|
165
|
+
changes.status === undefined &&
|
|
166
|
+
hashChanged &&
|
|
167
|
+
(existing.status === DEPLOYMENT_STATUS.READY ||
|
|
168
|
+
existing.status === DEPLOYMENT_STATUS.BUILD_FAILED)
|
|
169
|
+
) {
|
|
170
|
+
nextStatus = DEPLOYMENT_STATUS.STALE;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
db.prepare(
|
|
174
|
+
`UPDATE deployments
|
|
175
|
+
SET bot_name = ?, status = ?, config = ?, config_hash = ?, last_built_hash = ?, artifact_path = ?, document_ids = ?, error = ?, updated_at = ?
|
|
176
|
+
WHERE id = ?`
|
|
177
|
+
).run(
|
|
178
|
+
merged.botName,
|
|
179
|
+
nextStatus,
|
|
180
|
+
JSON.stringify(merged.config || {}),
|
|
181
|
+
newHash,
|
|
182
|
+
merged.lastBuiltHash,
|
|
183
|
+
merged.artifactPath,
|
|
184
|
+
JSON.stringify(merged.documentIds || []),
|
|
185
|
+
merged.error,
|
|
186
|
+
Date.now(),
|
|
187
|
+
id
|
|
188
|
+
);
|
|
189
|
+
return this.findById(id);
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Persist the result of a successful build: stamp last_built_hash to the
|
|
194
|
+
* current config_hash, store the artifact path, set status to 'ready'.
|
|
195
|
+
*/
|
|
196
|
+
async setBuildResult(id, { artifactPath }) {
|
|
197
|
+
const db = getDb();
|
|
198
|
+
const existing = await this.findById(id);
|
|
199
|
+
if (!existing) return null;
|
|
200
|
+
db.prepare(
|
|
201
|
+
`UPDATE deployments
|
|
202
|
+
SET status = ?, artifact_path = ?, last_built_hash = ?, error = NULL, updated_at = ?
|
|
203
|
+
WHERE id = ?`
|
|
204
|
+
).run(
|
|
205
|
+
DEPLOYMENT_STATUS.READY,
|
|
206
|
+
artifactPath,
|
|
207
|
+
existing.configHash,
|
|
208
|
+
Date.now(),
|
|
209
|
+
id
|
|
210
|
+
);
|
|
211
|
+
return this.findById(id);
|
|
212
|
+
},
|
|
213
|
+
|
|
214
|
+
async setBuildFailed(id, errorMessage) {
|
|
215
|
+
const db = getDb();
|
|
216
|
+
db.prepare(
|
|
217
|
+
`UPDATE deployments
|
|
218
|
+
SET status = ?, error = ?, updated_at = ?
|
|
219
|
+
WHERE id = ?`
|
|
220
|
+
).run(DEPLOYMENT_STATUS.BUILD_FAILED, errorMessage || 'Build failed', Date.now(), id);
|
|
221
|
+
return this.findById(id);
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
async markBuilding(id) {
|
|
225
|
+
const db = getDb();
|
|
226
|
+
db.prepare(
|
|
227
|
+
`UPDATE deployments
|
|
228
|
+
SET status = ?, error = NULL, updated_at = ?
|
|
229
|
+
WHERE id = ?`
|
|
230
|
+
).run(DEPLOYMENT_STATUS.BUILDING, Date.now(), id);
|
|
231
|
+
return this.findById(id);
|
|
232
|
+
},
|
|
233
|
+
|
|
234
|
+
async delete(id) {
|
|
235
|
+
const db = getDb();
|
|
236
|
+
db.prepare('DELETE FROM deployments WHERE id = ?').run(id);
|
|
237
|
+
},
|
|
238
|
+
|
|
239
|
+
async setUrl(id, url) {
|
|
240
|
+
const db = getDb();
|
|
241
|
+
const now = Date.now();
|
|
242
|
+
db.prepare(
|
|
243
|
+
`UPDATE deployments SET url = ?, last_seen_at = ?, updated_at = ? WHERE id = ?`
|
|
244
|
+
).run(url, now, now, id);
|
|
245
|
+
return this.findById(id);
|
|
246
|
+
},
|
|
247
|
+
|
|
248
|
+
async clearUrl(id) {
|
|
249
|
+
const db = getDb();
|
|
250
|
+
db.prepare(
|
|
251
|
+
`UPDATE deployments SET url = NULL, last_seen_at = NULL, updated_at = ? WHERE id = ?`
|
|
252
|
+
).run(Date.now(), id);
|
|
253
|
+
return this.findById(id);
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
async touchLastSeen(id) {
|
|
257
|
+
const db = getDb();
|
|
258
|
+
db.prepare(`UPDATE deployments SET last_seen_at = ? WHERE id = ?`).run(Date.now(), id);
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
async setEmbeddings(id, { storageKey, model, chunkCount }) {
|
|
262
|
+
const db = getDb();
|
|
263
|
+
db.prepare(
|
|
264
|
+
`UPDATE deployments
|
|
265
|
+
SET embedding_storage_key = ?, embedding_model = ?, embedding_chunk_count = ?, updated_at = ?
|
|
266
|
+
WHERE id = ?`
|
|
267
|
+
).run(storageKey, model, chunkCount, Date.now(), id);
|
|
268
|
+
return this.findById(id);
|
|
269
|
+
},
|
|
270
|
+
|
|
271
|
+
async clearEmbeddings(id) {
|
|
272
|
+
const db = getDb();
|
|
273
|
+
db.prepare(
|
|
274
|
+
`UPDATE deployments
|
|
275
|
+
SET embedding_storage_key = NULL, embedding_model = NULL, embedding_chunk_count = NULL, updated_at = ?
|
|
276
|
+
WHERE id = ?`
|
|
277
|
+
).run(Date.now(), id);
|
|
278
|
+
return this.findById(id);
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
async setRagMode(id, mode) {
|
|
282
|
+
if (mode !== 'keyword' && mode !== 'vector') {
|
|
283
|
+
throw new Error(`Invalid rag mode: ${mode}`);
|
|
284
|
+
}
|
|
285
|
+
const db = getDb();
|
|
286
|
+
db.prepare(
|
|
287
|
+
`UPDATE deployments SET rag_mode = ?, updated_at = ? WHERE id = ?`
|
|
288
|
+
).run(mode, Date.now(), id);
|
|
289
|
+
return this.findById(id);
|
|
290
|
+
},
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Stamp a cloud deploy as kicked off. Resets progress to a fresh empty
|
|
294
|
+
* array and clears any prior error.
|
|
295
|
+
*/
|
|
296
|
+
async startCloudDeploy(id, { provider, appName, options }) {
|
|
297
|
+
const db = getDb();
|
|
298
|
+
db.prepare(
|
|
299
|
+
`UPDATE deployments
|
|
300
|
+
SET cloud_provider = ?, cloud_app_name = ?, cloud_status = ?,
|
|
301
|
+
cloud_options = ?, cloud_progress = ?, cloud_error = NULL,
|
|
302
|
+
updated_at = ?
|
|
303
|
+
WHERE id = ?`
|
|
304
|
+
).run(
|
|
305
|
+
provider,
|
|
306
|
+
appName,
|
|
307
|
+
CLOUD_STATUS.DEPLOYING,
|
|
308
|
+
JSON.stringify(options || {}),
|
|
309
|
+
JSON.stringify([]),
|
|
310
|
+
Date.now(),
|
|
311
|
+
id
|
|
312
|
+
);
|
|
313
|
+
return this.findById(id);
|
|
314
|
+
},
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Append a progress event to cloud_progress. Read-modify-write — fine for
|
|
318
|
+
* the single-user mode this runs in.
|
|
319
|
+
*/
|
|
320
|
+
async appendCloudProgress(id, { step, message }) {
|
|
321
|
+
const existing = await this.findById(id);
|
|
322
|
+
if (!existing) return null;
|
|
323
|
+
const next = [
|
|
324
|
+
...(existing.cloudProgress || []),
|
|
325
|
+
{ step, message, timestamp: new Date().toISOString() },
|
|
326
|
+
];
|
|
327
|
+
const db = getDb();
|
|
328
|
+
db.prepare(
|
|
329
|
+
`UPDATE deployments SET cloud_progress = ?, updated_at = ? WHERE id = ?`
|
|
330
|
+
).run(JSON.stringify(next), Date.now(), id);
|
|
331
|
+
return this.findById(id);
|
|
332
|
+
},
|
|
333
|
+
|
|
334
|
+
async finishCloudDeploy(id, { url, machineId, volumeId }) {
|
|
335
|
+
const db = getDb();
|
|
336
|
+
const now = Date.now();
|
|
337
|
+
db.prepare(
|
|
338
|
+
`UPDATE deployments
|
|
339
|
+
SET cloud_status = ?, cloud_url = ?, cloud_machine_id = ?,
|
|
340
|
+
cloud_volume_id = ?, cloud_last_deployed_at = ?, cloud_error = NULL,
|
|
341
|
+
updated_at = ?
|
|
342
|
+
WHERE id = ?`
|
|
343
|
+
).run(
|
|
344
|
+
CLOUD_STATUS.RUNNING,
|
|
345
|
+
url || null,
|
|
346
|
+
machineId || null,
|
|
347
|
+
volumeId || null,
|
|
348
|
+
now,
|
|
349
|
+
now,
|
|
350
|
+
id
|
|
351
|
+
);
|
|
352
|
+
return this.findById(id);
|
|
353
|
+
},
|
|
354
|
+
|
|
355
|
+
async failCloudDeploy(id, errorMessage) {
|
|
356
|
+
const db = getDb();
|
|
357
|
+
db.prepare(
|
|
358
|
+
`UPDATE deployments
|
|
359
|
+
SET cloud_status = ?, cloud_error = ?, updated_at = ?
|
|
360
|
+
WHERE id = ?`
|
|
361
|
+
).run(CLOUD_STATUS.FAILED, errorMessage || 'Cloud deploy failed', Date.now(), id);
|
|
362
|
+
return this.findById(id);
|
|
363
|
+
},
|
|
364
|
+
|
|
365
|
+
async setCloudStatus(id, status) {
|
|
366
|
+
const db = getDb();
|
|
367
|
+
db.prepare(
|
|
368
|
+
`UPDATE deployments SET cloud_status = ?, updated_at = ? WHERE id = ?`
|
|
369
|
+
).run(status, Date.now(), id);
|
|
370
|
+
return this.findById(id);
|
|
371
|
+
},
|
|
372
|
+
|
|
373
|
+
async clearCloudDeploy(id) {
|
|
374
|
+
const db = getDb();
|
|
375
|
+
db.prepare(
|
|
376
|
+
`UPDATE deployments
|
|
377
|
+
SET cloud_provider = NULL, cloud_app_name = NULL, cloud_status = ?,
|
|
378
|
+
cloud_url = NULL, cloud_progress = NULL, cloud_options = NULL,
|
|
379
|
+
cloud_error = NULL, cloud_last_deployed_at = NULL,
|
|
380
|
+
cloud_machine_id = NULL, cloud_volume_id = NULL, updated_at = ?
|
|
381
|
+
WHERE id = ?`
|
|
382
|
+
).run(CLOUD_STATUS.DESTROYED, Date.now(), id);
|
|
383
|
+
return this.findById(id);
|
|
384
|
+
},
|
|
385
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { getDb } from '../index.js';
|
|
2
|
+
import { newId } from '../ids.js';
|
|
3
|
+
import { deleteFile } from '../../storage/index.js';
|
|
4
|
+
|
|
5
|
+
function rowToDocument(row) {
|
|
6
|
+
if (!row) return null;
|
|
7
|
+
return {
|
|
8
|
+
id: row.id,
|
|
9
|
+
originalName: row.original_name,
|
|
10
|
+
mimeType: row.mime_type,
|
|
11
|
+
sizeBytes: row.size_bytes,
|
|
12
|
+
storagePath: row.storage_path,
|
|
13
|
+
parsedText: row.parsed_text,
|
|
14
|
+
createdAt: new Date(row.created_at),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const DocumentRepository = {
|
|
19
|
+
async findById(id) {
|
|
20
|
+
const db = getDb();
|
|
21
|
+
const row = db.prepare('SELECT * FROM documents WHERE id = ?').get(id);
|
|
22
|
+
return rowToDocument(row);
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
async findByIds(ids) {
|
|
26
|
+
if (!ids || ids.length === 0) return [];
|
|
27
|
+
const db = getDb();
|
|
28
|
+
const placeholders = ids.map(() => '?').join(',');
|
|
29
|
+
const rows = db.prepare(`SELECT * FROM documents WHERE id IN (${placeholders})`).all(...ids);
|
|
30
|
+
return rows.map(rowToDocument);
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
// Kept for API parity with the builder stream code. In Lite there are no bot spaces,
|
|
34
|
+
// so this always returns the full document list — the builder can see
|
|
35
|
+
// everything the local user has uploaded.
|
|
36
|
+
async findByBotSpaceId(_botSpaceId) {
|
|
37
|
+
const db = getDb();
|
|
38
|
+
const rows = db.prepare('SELECT * FROM documents ORDER BY created_at DESC').all();
|
|
39
|
+
return rows.map(rowToDocument);
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
async create({ originalName, mimeType, sizeBytes, storagePath, parsedText = null }) {
|
|
43
|
+
const db = getDb();
|
|
44
|
+
const id = newId('doc');
|
|
45
|
+
const now = Date.now();
|
|
46
|
+
db.prepare(
|
|
47
|
+
`INSERT INTO documents (id, original_name, mime_type, size_bytes, storage_path, parsed_text, created_at)
|
|
48
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
49
|
+
).run(id, originalName, mimeType, Number(sizeBytes) || 0, storagePath, parsedText, now);
|
|
50
|
+
return this.findById(id);
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
// Cascades blob removal so the row and the file in data/storage/ go together.
|
|
54
|
+
// Storage failures are logged and swallowed — the row is the source of truth
|
|
55
|
+
// for whether the doc "exists" in the app.
|
|
56
|
+
async delete(id) {
|
|
57
|
+
const db = getDb();
|
|
58
|
+
const row = db.prepare('SELECT storage_path FROM documents WHERE id = ?').get(id);
|
|
59
|
+
if (row?.storage_path) {
|
|
60
|
+
try {
|
|
61
|
+
await deleteFile(row.storage_path);
|
|
62
|
+
} catch (err) {
|
|
63
|
+
console.warn(`[documents] storage delete failed for ${id} (continuing):`, err.message);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
db.prepare('DELETE FROM documents WHERE id = ?').run(id);
|
|
67
|
+
},
|
|
68
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { getDb } from '../index.js';
|
|
2
|
+
import { newId } from '../ids.js';
|
|
3
|
+
|
|
4
|
+
export const JOB_STATUS = {
|
|
5
|
+
PENDING: 'pending',
|
|
6
|
+
RUNNING: 'running',
|
|
7
|
+
DONE: 'done',
|
|
8
|
+
ERROR: 'error',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const JOB_TTL_MS = 24 * 60 * 60 * 1000;
|
|
12
|
+
|
|
13
|
+
function rowToJob(row) {
|
|
14
|
+
if (!row) return null;
|
|
15
|
+
return {
|
|
16
|
+
id: row.id,
|
|
17
|
+
tool: row.tool,
|
|
18
|
+
status: row.status,
|
|
19
|
+
progress: row.progress != null ? row.progress : null,
|
|
20
|
+
result: row.result ? JSON.parse(row.result) : null,
|
|
21
|
+
error: row.error || null,
|
|
22
|
+
mcpSessionId: row.mcp_session_id || null,
|
|
23
|
+
builderSessionId: row.builder_session_id || null,
|
|
24
|
+
createdAt: new Date(row.created_at),
|
|
25
|
+
updatedAt: new Date(row.updated_at),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const McpJobRepository = {
|
|
30
|
+
async create({ tool, mcpSessionId = null, builderSessionId = null }) {
|
|
31
|
+
const db = getDb();
|
|
32
|
+
const id = newId('mcpjob');
|
|
33
|
+
const now = Date.now();
|
|
34
|
+
db.prepare(
|
|
35
|
+
`INSERT INTO mcp_jobs (id, tool, status, progress, result, error, mcp_session_id, builder_session_id, created_at, updated_at)
|
|
36
|
+
VALUES (?, ?, ?, ?, NULL, NULL, ?, ?, ?, ?)`
|
|
37
|
+
).run(id, tool, JOB_STATUS.PENDING, 0, mcpSessionId, builderSessionId, now, now);
|
|
38
|
+
// Drop old rows opportunistically — keeps the table bounded without a cron.
|
|
39
|
+
db.prepare('DELETE FROM mcp_jobs WHERE created_at < ?').run(now - JOB_TTL_MS);
|
|
40
|
+
return this.findById(id);
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
async findById(id) {
|
|
44
|
+
const db = getDb();
|
|
45
|
+
const row = db.prepare('SELECT * FROM mcp_jobs WHERE id = ?').get(id);
|
|
46
|
+
return rowToJob(row);
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
async setRunning(id) {
|
|
50
|
+
const db = getDb();
|
|
51
|
+
db.prepare(
|
|
52
|
+
'UPDATE mcp_jobs SET status = ?, updated_at = ? WHERE id = ?'
|
|
53
|
+
).run(JOB_STATUS.RUNNING, Date.now(), id);
|
|
54
|
+
return this.findById(id);
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
async setProgress(id, progress) {
|
|
58
|
+
const db = getDb();
|
|
59
|
+
const clamped = Math.max(0, Math.min(100, Math.round(progress ?? 0)));
|
|
60
|
+
db.prepare('UPDATE mcp_jobs SET progress = ?, updated_at = ? WHERE id = ?').run(
|
|
61
|
+
clamped,
|
|
62
|
+
Date.now(),
|
|
63
|
+
id
|
|
64
|
+
);
|
|
65
|
+
return this.findById(id);
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
async setDone(id, result) {
|
|
69
|
+
const db = getDb();
|
|
70
|
+
db.prepare(
|
|
71
|
+
'UPDATE mcp_jobs SET status = ?, progress = ?, result = ?, error = NULL, updated_at = ? WHERE id = ?'
|
|
72
|
+
).run(JOB_STATUS.DONE, 100, JSON.stringify(result ?? null), Date.now(), id);
|
|
73
|
+
return this.findById(id);
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
async setError(id, error) {
|
|
77
|
+
const db = getDb();
|
|
78
|
+
const msg = error?.message || String(error || 'Unknown error');
|
|
79
|
+
db.prepare(
|
|
80
|
+
'UPDATE mcp_jobs SET status = ?, error = ?, updated_at = ? WHERE id = ?'
|
|
81
|
+
).run(JOB_STATUS.ERROR, msg, Date.now(), id);
|
|
82
|
+
return this.findById(id);
|
|
83
|
+
},
|
|
84
|
+
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fleet fan-out helpers for the `/data` pane.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the single-bot fetchFromBot pattern in a parallel-with-concurrency-cap
|
|
5
|
+
* driver that hits every connected deployment. Returns per-bot results plus
|
|
6
|
+
* a count of unreachable bots so callers can surface a partial-results banner.
|
|
7
|
+
*
|
|
8
|
+
* Nothing in this module touches the control-plane DB — aggregates live only
|
|
9
|
+
* in process memory for the duration of one request.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { DeploymentRepository } from '@/lib/db/repositories/deployments';
|
|
13
|
+
import { fetchFromBot } from '@/lib/deployers/bot-proxy';
|
|
14
|
+
|
|
15
|
+
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
16
|
+
const DEFAULT_CONCURRENCY = 8;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Return all deployments that have a `url` set (i.e. Connect Bot has fired
|
|
20
|
+
* at some point). We don't gate on freshness — a connected-but-stale bot
|
|
21
|
+
* still gets the fan-out call; it just fails timeout and shows up in the
|
|
22
|
+
* unreachable count.
|
|
23
|
+
*/
|
|
24
|
+
export async function listConnectedDeployments() {
|
|
25
|
+
const all = await DeploymentRepository.list();
|
|
26
|
+
return all.filter((d) => !!d.url);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Fan a GET out to every connected deployment.
|
|
31
|
+
*
|
|
32
|
+
* @param {string} path - bot-side path (e.g. `/api/analytics/summary?...`)
|
|
33
|
+
* @param {object} [opts]
|
|
34
|
+
* @param {number} [opts.timeoutMs] per-call timeout
|
|
35
|
+
* @param {number} [opts.concurrency] max parallel in-flight calls
|
|
36
|
+
* @param {Array} [opts.deployments] override the deployment list (otherwise calls listConnectedDeployments)
|
|
37
|
+
*
|
|
38
|
+
* @returns {Promise<{
|
|
39
|
+
* results: Array<{ deployment: object, ok: true, data: any } | { deployment: object, ok: false, reason: string, status?: number, message?: string }>,
|
|
40
|
+
* totalCount: number,
|
|
41
|
+
* reachableCount: number,
|
|
42
|
+
* unreachableCount: number
|
|
43
|
+
* }>}
|
|
44
|
+
*/
|
|
45
|
+
export async function fanOut(path, opts = {}) {
|
|
46
|
+
const {
|
|
47
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
48
|
+
concurrency = DEFAULT_CONCURRENCY,
|
|
49
|
+
deployments,
|
|
50
|
+
} = opts;
|
|
51
|
+
|
|
52
|
+
const targets = deployments || (await listConnectedDeployments());
|
|
53
|
+
const results = new Array(targets.length);
|
|
54
|
+
|
|
55
|
+
let cursor = 0;
|
|
56
|
+
async function worker() {
|
|
57
|
+
while (cursor < targets.length) {
|
|
58
|
+
const myIndex = cursor++;
|
|
59
|
+
const deployment = targets[myIndex];
|
|
60
|
+
results[myIndex] = await callOne(deployment, path, timeoutMs);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const pool = Math.min(concurrency, targets.length);
|
|
65
|
+
await Promise.all(Array.from({ length: pool }, () => worker()));
|
|
66
|
+
|
|
67
|
+
const reachableCount = results.filter((r) => r.ok).length;
|
|
68
|
+
return {
|
|
69
|
+
results,
|
|
70
|
+
totalCount: targets.length,
|
|
71
|
+
reachableCount,
|
|
72
|
+
unreachableCount: targets.length - reachableCount,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function callOne(deployment, path, timeoutMs) {
|
|
77
|
+
let response;
|
|
78
|
+
try {
|
|
79
|
+
response = await fetchFromBot(deployment, path, { timeoutMs });
|
|
80
|
+
} catch (err) {
|
|
81
|
+
return {
|
|
82
|
+
deployment,
|
|
83
|
+
ok: false,
|
|
84
|
+
reason: err.name === 'AbortError' ? 'timeout' : 'network',
|
|
85
|
+
message: err.message,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
if (!response.ok) {
|
|
89
|
+
return {
|
|
90
|
+
deployment,
|
|
91
|
+
ok: false,
|
|
92
|
+
reason: 'bad_status',
|
|
93
|
+
status: response.status,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
let data;
|
|
97
|
+
try {
|
|
98
|
+
data = await response.json();
|
|
99
|
+
} catch (err) {
|
|
100
|
+
return {
|
|
101
|
+
deployment,
|
|
102
|
+
ok: false,
|
|
103
|
+
reason: 'bad_json',
|
|
104
|
+
message: err.message,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
// Don't await — best-effort freshness update.
|
|
108
|
+
DeploymentRepository.touchLastSeen(deployment.id).catch(() => {});
|
|
109
|
+
return { deployment, ok: true, data };
|
|
110
|
+
}
|